@webqit/webflo 0.10.3 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +1509 -3
  2. package/bundle.html.json +1665 -0
  3. package/package.json +3 -3
  4. package/src/Context.js +8 -4
  5. package/src/config-pi/deployment/Env.js +2 -2
  6. package/src/config-pi/deployment/Layout.js +2 -2
  7. package/src/config-pi/deployment/Origins.js +2 -2
  8. package/src/config-pi/deployment/Virtualization.js +2 -2
  9. package/src/config-pi/runtime/Client.js +39 -11
  10. package/src/config-pi/runtime/Server.js +26 -10
  11. package/src/config-pi/runtime/client/Worker.js +32 -14
  12. package/src/config-pi/runtime/server/Headers.js +2 -2
  13. package/src/config-pi/runtime/server/Redirects.js +2 -2
  14. package/src/config-pi/static/Manifest.js +2 -2
  15. package/src/config-pi/static/Ssg.js +2 -2
  16. package/src/runtime-pi/Router.js +1 -1
  17. package/src/runtime-pi/client/Runtime.js +116 -62
  18. package/src/runtime-pi/client/RuntimeClient.js +28 -43
  19. package/src/runtime-pi/client/Workport.js +163 -0
  20. package/src/runtime-pi/client/generate.js +285 -77
  21. package/src/runtime-pi/client/{generate.oohtml.js → oohtml/full.js} +0 -0
  22. package/src/runtime-pi/client/oohtml/namespacing.js +7 -0
  23. package/src/runtime-pi/client/oohtml/scripting.js +8 -0
  24. package/src/runtime-pi/client/oohtml/templating.js +8 -0
  25. package/src/runtime-pi/client/worker/Worker.js +58 -24
  26. package/src/runtime-pi/client/worker/Workport.js +80 -0
  27. package/src/runtime-pi/server/Router.js +2 -2
  28. package/src/runtime-pi/server/Runtime.js +30 -11
  29. package/src/runtime-pi/server/RuntimeClient.js +24 -14
  30. package/src/runtime-pi/util.js +2 -2
  31. package/src/webflo.js +7 -9
  32. package/test/site/package.json +9 -0
  33. package/test/site/public/bundle.html +6 -0
  34. package/test/site/public/bundle.html.json +4 -0
  35. package/test/site/public/bundle.js +2 -0
  36. package/test/site/public/bundle.js.gz +0 -0
  37. package/test/site/public/bundle.webflo.js +15 -0
  38. package/test/site/public/bundle.webflo.js.gz +0 -0
  39. package/test/site/public/index.html +30 -0
  40. package/test/site/public/index1.html +35 -0
  41. package/test/site/public/page-2/bundle.html +5 -0
  42. package/test/site/public/page-2/bundle.html.json +1 -0
  43. package/test/site/public/page-2/bundle.js +2 -0
  44. package/test/site/public/page-2/bundle.js.gz +0 -0
  45. package/test/site/public/page-2/index.html +46 -0
  46. package/test/site/public/page-2/logo-130x130.png +0 -0
  47. package/test/site/public/page-2/main.html +3 -0
  48. package/test/site/public/page-3/logo-130x130.png +0 -0
  49. package/test/site/public/page-4/subpage/bundle.html +0 -0
  50. package/test/site/public/page-4/subpage/bundle.html.json +1 -0
  51. package/test/site/public/page-4/subpage/bundle.js +2 -0
  52. package/test/site/public/page-4/subpage/bundle.js.gz +0 -0
  53. package/test/site/public/page-4/subpage/index.html +31 -0
  54. package/test/site/public/sparoots.json +5 -0
  55. package/test/site/public/worker.js +3 -0
  56. package/test/site/public/worker.js.gz +0 -0
  57. package/test/site/server/index.js +16 -0
  58. package/src/Cli.js +0 -131
  59. package/src/Configurator.js +0 -97
  60. package/src/runtime-pi/client/WorkerComm.js +0 -102
@@ -5,11 +5,13 @@
5
5
  import Fs from 'fs';
6
6
  import Url from 'url';
7
7
  import Path from 'path';
8
- import { _beforeLast } from '@webqit/util/str/index.js';
8
+ import Jsdom from 'jsdom';
9
+ import EsBuild from 'esbuild';
10
+ import { _afterLast, _beforeLast } from '@webqit/util/str/index.js';
9
11
  import { _isObject, _isArray } from '@webqit/util/js/index.js';
10
- import * as DotJs from '@webqit/backpack/src/dotfiles/DotJs.js';
12
+ import { jsFile } from '@webqit/backpack/src/dotfile/index.js';
11
13
  import { gzipSync, brotliCompressSync } from 'zlib';
12
- import EsBuild from 'esbuild';
14
+ import { urlPattern } from '../util.js';
13
15
 
14
16
  /**
15
17
  * @generate
@@ -20,6 +22,9 @@ export async function generate() {
20
22
  if (!cx.config.runtime?.Client) {
21
23
  throw new Error(`The Client configurator "config.runtime.Client" is required in context.`);
22
24
  }
25
+ if (!cx.config.deployment?.Layout) {
26
+ throw new Error(`The Client configurator "config.deployment.Layout" is required in context.`);
27
+ }
23
28
  const clientConfig = await (new cx.config.runtime.Client(cx)).read();
24
29
  if (clientConfig.support_service_worker && !cx.config.runtime.client?.Worker) {
25
30
  throw new Error(`The Service Worker configurator "config.runtime.client.Worker" is required in context.`);
@@ -30,88 +35,235 @@ export async function generate() {
30
35
  throw new Error(`The Layout configurator "config.deployment.Layout" is required in context.`);
31
36
  }
32
37
  const layoutConfig = await (new cx.config.deployment.Layout(cx)).read();
38
+ // -----------
33
39
  const dirPublic = Path.resolve(cx.CWD || '', layoutConfig.PUBLIC_DIR);
40
+ const dirClient = Path.resolve(cx.CWD || '', layoutConfig.CLIENT_DIR);
41
+ const dirWorker = Path.resolve(cx.CWD || '', layoutConfig.WORKER_DIR);
34
42
  const dirSelf = Path.dirname(Url.fileURLToPath(import.meta.url)).replace(/\\/g, '/');
35
43
  // -----------
44
+ // Scan Subdocuments
45
+ const scanSubroots = (sparoot, rootFileName) => {
46
+ let dir = Path.join(dirPublic, sparoot), passes = 0;
47
+ return [ Fs.readdirSync(dir).reduce((sparoots, f) => {
48
+ let resource = Path.join(dir, f);
49
+ if (Fs.statSync(resource).isDirectory()) {
50
+ let subsparoot = Path.join(sparoot, f);
51
+ if (Fs.existsSync(Path.join(resource, rootFileName))) {
52
+ return sparoots.concat(subsparoot);
53
+ }
54
+ passes ++;
55
+ return sparoots.concat(scanSubroots(subsparoot, rootFileName)[ 0 ]);
56
+ }
57
+ return sparoots;
58
+ }, []), passes ];
59
+ };
60
+ // -----------
36
61
  // Generate client build
37
- let genClient = getGen.call(cx, dirSelf, layoutConfig.CLIENT_DIR, clientConfig, `The Client Build.`);
38
- if (clientConfig.support_oohtml) {
39
- genClient.imports = { [`${dirSelf}/generate.oohtml.js`]: null, ...genClient.imports };
40
- }
41
- await bundle.call(cx, genClient, `${dirPublic}/${clientConfig.bundle_filename}`, true/* asModule */);
42
- cx.logger && cx.logger.log('');
62
+ const generateClient = async function(sparoot, spaGraphCallback = null) {
63
+ let [ subsparoots, targets ] = (sparoot && scanSubroots(sparoot, 'index.html')) || [ [], false ];
64
+ if (!sparoot) sparoot = '/';
65
+ let spaRouting = { root: sparoot, subroots: subsparoots, targets };
66
+ let codeSplitting = !!(sparoot !== '/' || subsparoots.length);
67
+ let outfileMain = Path.join(sparoot, clientConfig.bundle_filename),
68
+ outfileWebflo = _beforeLast(clientConfig.bundle_filename, '.js') + '.webflo.js';
69
+ let gen = { imports: {}, code: [], };
70
+ // ------------------
71
+ const initWebflo = gen => {
72
+ if (clientConfig.oohtml_support === 'namespacing') {
73
+ gen.imports[`${dirSelf}/oohtml/namespacing.js`] = null;
74
+ } else if (clientConfig.oohtml_support === 'scripting') {
75
+ gen.imports[`${dirSelf}/oohtml/scripting.js`] = null;
76
+ } else if (clientConfig.oohtml_support === 'templating') {
77
+ gen.imports[`${dirSelf}/oohtml/templating.js`] = null;
78
+ } else if (clientConfig.oohtml_support !== 'none') {
79
+ gen.imports[`${dirSelf}/oohtml/full.js`] = null;
80
+ }
81
+ gen.imports[`${dirSelf}/index.js`] = `* as Webflo`;
82
+ gen.code.push(``);
83
+ gen.code.push(`if (!globalThis.WebQit) {`);
84
+ gen.code.push(` globalThis.WebQit = {}`);
85
+ gen.code.push(`}`);
86
+ gen.code.push(`WebQit.Webflo = Webflo`);
87
+ return gen;
88
+ };
89
+ // ------------------
90
+ if (!codeSplitting) {
91
+ initWebflo(gen);
92
+ } else if (sparoot === '/') {
93
+ if (cx.logger) {
94
+ cx.logger.log(cx.logger.style.keyword(`-----------------`));
95
+ cx.logger.log(`Base Build`);
96
+ cx.logger.log(cx.logger.style.keyword(`-----------------`));
97
+ }
98
+ let gen1 = initWebflo({ imports: {}, code: [], });
99
+ await bundle.call(cx, gen1, Path.join(dirPublic, outfileWebflo), true/* asModule */);
100
+ }
101
+ // ------------------
102
+ if (cx.logger) {
103
+ cx.logger.log(cx.logger.style.keyword(`-----------------`));
104
+ cx.logger.log(`Client Build ` + cx.logger.style.comment(`(sparoot:${sparoot}; is-split:${codeSplitting})`));
105
+ cx.logger.log(cx.logger.style.keyword(`-----------------`));
106
+ }
107
+ gen.code.push(`const { start } = WebQit.Webflo`);
108
+ // ------------------
109
+ // Bundle
110
+ declareStart.call(cx, gen, dirClient, dirPublic, clientConfig, spaRouting);
111
+ await bundle.call(cx, gen, Path.join(dirPublic, outfileMain), true/* asModule */);
112
+ // ------------------
113
+ // Embed/unembed
114
+ let targetDocumentFile = Path.join(dirPublic, sparoot, 'index.html'),
115
+ outfileWebfloPublic = Path.join(clientConfig.public_base_url, outfileWebflo),
116
+ outfileMainPublic = Path.join(clientConfig.public_base_url, outfileMain),
117
+ embedList = [],
118
+ unembedList = [];
119
+ if (cx.flags['auto-embed']) {
120
+ if (codeSplitting) {
121
+ embedList.push(outfileWebfloPublic);
122
+ } else {
123
+ unembedList.push(outfileWebfloPublic);
124
+ }
125
+ embedList.push(outfileMainPublic);
126
+ } else {
127
+ unembedList.push(outfileWebfloPublic, outfileMainPublic);
128
+ }
129
+ handleEmbeds(targetDocumentFile, embedList, unembedList);
130
+ // ------------------
131
+ // Recurse
132
+ spaGraphCallback && spaGraphCallback(sparoot, subsparoots);
133
+ if (cx.flags.recursive) {
134
+ while (subsparoots.length) {
135
+ await generateClient(subsparoots.shift(), spaGraphCallback);
136
+ }
137
+ }
138
+ };
43
139
  // -----------
44
140
  // Generate worker build
45
- if (clientConfig.support_service_worker) {
46
- let genWorker = getGen.call(cx, `${dirSelf}/worker`, layoutConfig.WORKER_DIR, workerConfig, `The Worker Build.`);
47
- await bundle.call(cx, genWorker, `${dirPublic}/${clientConfig.worker_filename}`);
48
- cx.logger && cx.logger.log('');
141
+ const generateWorker = async function(workerroot, workerGraphCallbak = null) {
142
+ let [ subworkerroots, targets ] = workerroot && scanSubroots(workerroot, 'workerroot') || [ [], false ];
143
+ if (!workerroot) workerroot = '/';
144
+ let workerRouting = { root: workerroot, subroots: subworkerroots, targets };
145
+ let gen = { imports: {}, code: [], };
146
+ if (cx.logger) {
147
+ cx.logger.log(cx.logger.style.comment(`-----------------`));
148
+ cx.logger.log(`Worker Build - workerroot:${workerroot}`);
149
+ cx.logger.log(cx.logger.style.comment(`-----------------`));
150
+ }
151
+ // ------------------
152
+ // >> Modules import
153
+ gen.imports[`${dirSelf}/worker/index.js`] = `{ start }`;
154
+ gen.code.push(``);
155
+ // ------------------
156
+ // Bundle
157
+ if (workerConfig.cache_only_urls.length) {
158
+ // Separate URLs from patterns
159
+ let [ urls, patterns ] = workerConfig.cache_only_urls.reduce(([ urls, patterns ], url) => {
160
+ let patternInstance = urlPattern(url, 'http://localhost'),
161
+ isPattern = patternInstance.isPattern();
162
+ if (isPattern && (patternInstance.pattern.pattern.hostname !== 'localhost' || patternInstance.pattern.pattern.port)) {
163
+ throw new Error(`Pattern URLs must have no origin part. Recieved "${url}".`);
164
+ }
165
+ return isPattern ? [ urls, patterns.concat(patternInstance) ] : [ urls.concat(url), patterns ];
166
+ }, [ [], [] ]);
167
+ // Resolve patterns
168
+ if (patterns.length) {
169
+ // List all files
170
+ let scan = dir => Fs.readdirSync(dir).reduce((result, f) => {
171
+ let resource = Path.join(dir, f);
172
+ return result.concat(Fs.statSync(resource).isDirectory() ? scan(resource) : '/' + Path.relative(dirPublic, resource));
173
+ }, []);
174
+ let files = scan(dirPublic);
175
+ // Resolve patterns from files
176
+ workerConfig.cache_only_urls = patterns.reduce((all, pattern) => {
177
+ let matchedFiles = files.filter(file => pattern.test(file, 'http://localhost'));
178
+ if (matchedFiles.length) return all.concat(matchedFiles);
179
+ throw new Error(`The pattern "${pattern.pattern.pattern.pathname}" didn't match any files.`);
180
+ }, urls);
181
+ }
182
+ }
183
+ declareStart.call(cx, gen, dirWorker, dirPublic, workerConfig, workerRouting);
184
+ await bundle.call(cx, gen, Path.join(dirPublic, workerroot, clientConfig.worker_filename));
185
+ // ------------------
186
+ // Recurse
187
+ workerGraphCallbak && workerGraphCallbak(workerroot, subworkerroots);
188
+ if (cx.flags.recursive) {
189
+ while (subworkerroots.length) {
190
+ await generateWorker(subworkerroots.shift());
191
+ }
192
+ }
193
+ };
194
+ // -----------
195
+ // Generate now...
196
+ let sparootsFile = Path.join(dirPublic, 'sparoots.json');
197
+ if (clientConfig.spa_routing !== false) {
198
+ const sparoots = [];
199
+ await generateClient('/', root => sparoots.push(root));
200
+ Fs.writeFileSync(sparootsFile, JSON.stringify(sparoots, null, 4));
201
+ } else {
202
+ await generateClient();
203
+ Fs.existsSync(sparootsFile) && Fs.unlinkSync(sparootsFile);
204
+ }
205
+ if (clientConfig.service_worker_support) {
206
+ await generateWorker('/');
49
207
  }
50
208
  }
51
209
 
52
210
  /**
53
211
  * Compile routes.
54
212
  *
55
- * @param string modulesDir
56
- * @param string routesDir
57
- * @param object paramsObj
58
- * @param string desc
213
+ * @param object gen
214
+ * @param string routesDir
215
+ * @param string targetPublic
216
+ * @param object paramsObj
217
+ * @param object routing
59
218
  *
60
219
  * @return Object
61
220
  */
62
- function getGen(modulesDir, routesDir, paramsObj, desc) {
221
+ function declareStart(gen, routesDir, targetDir, paramsObj, routing) {
63
222
  const cx = this || {};
64
- if (cx.logger) {
65
- cx.logger.log(cx.logger.style.comment(`-----------------`));
66
- cx.logger.log(desc);
67
- cx.logger.log(cx.logger.style.comment(`-----------------`));
68
- cx.logger.log('');
69
- }
70
- // ------------------
71
- const gen = { imports: {}, code: [], };
72
- // ------------------
73
- // >> Modules import
74
- gen.imports[`${modulesDir}/index.js`] = `{ start }`;
75
- gen.code.push(``);
76
223
  // ------------------
77
224
  // >> Routes mapping
78
225
  gen.code.push(`// >> Routes`);
79
- declareRoutesObj.call(cx, gen, routesDir, 'layout', '');
226
+ declareRoutesObj.call(cx, gen, routesDir, targetDir, 'layout', routing);
80
227
  gen.code.push(``);
81
228
  // ------------------
82
229
  // >> Params
83
230
  gen.code.push(`// >> Params`);
84
- declareParamsObj.call(cx, gen, paramsObj, 'params');
231
+ declareParamsObj.call(cx, gen, { ...paramsObj, routing }, 'params');
85
232
  gen.code.push(``);
86
233
  // ------------------
87
234
  // >> Startup
88
235
  gen.code.push(`// >> Startup`);
89
236
  gen.code.push(`start.call({ layout, params })`);
90
- return gen;
91
237
  }
92
238
 
93
239
  /**
94
240
  * Compile routes.
95
241
  *
96
- * @param object gen
97
- * @param string routesDir
98
- * @param string varName
242
+ * @param object gen
243
+ * @param string routesDir
244
+ * @param string targetDir
245
+ * @param string varName
246
+ * @param object routing
99
247
  *
100
248
  * @return void
101
249
  */
102
- function declareRoutesObj(gen, routesDir, varName) {
250
+ function declareRoutesObj(gen, routesDir, targetDir, varName, routing) {
103
251
  const cx = this || {};
104
- cx.logger && cx.logger.log(`> Declaring routes...`);
252
+ let _routesDir = Path.join(routesDir, routing.root),
253
+ _targetDir = Path.join(targetDir, routing.root);
254
+ cx.logger && cx.logger.log(cx.logger.style.keyword(`> `) + `Declaring routes...`);
105
255
  // ----------------
106
256
  // Directory walker
107
257
  const walk = (dir, callback) => {
108
258
  Fs.readdirSync(dir).forEach(f => {
109
259
  let resource = Path.join(dir, f);
260
+ let namespace = _beforeLast('/' + Path.relative(routesDir, resource), '/index.js') || '/';
110
261
  if (Fs.statSync(resource).isDirectory()) {
262
+ if (routing.subroots.includes(namespace)) return;
111
263
  walk(resource, callback);
112
264
  } else {
113
- let ext = Path.extname(resource) || '';
114
- callback(resource, ext);
265
+ let relativePath = Path.relative(_targetDir, resource);
266
+ callback(resource, namespace, relativePath);
115
267
  }
116
268
  });
117
269
  };
@@ -119,29 +271,25 @@ function declareRoutesObj(gen, routesDir, varName) {
119
271
  // >> Routes mapping
120
272
  gen.code.push(`const ${varName} = {};`);
121
273
  let indexCount = 0;
122
- if (routesDir && Fs.existsSync(routesDir)) {
123
- let clientDirname = routesDir.replace(/\\/g, '/').split('/').pop();
124
- walk(routesDir, (file, ext) => {
274
+ if (Fs.existsSync(_routesDir)) {
275
+ walk(_routesDir, (file, namespace, relativePath) => {
125
276
  //relativePath = relativePath.replace(/\\/g, '/');
126
277
  if (file.replace(/\\/g, '/').endsWith('/index.js')) {
127
- let relativePath = Path.relative(routesDir, file).replace(/\\/g, '/');
128
278
  // Import code
129
279
  let routeName = 'index' + (++ indexCount);
130
280
  // IMPORTANT: we;re taking a step back here so that the parent-child relationship for
131
281
  // the directories be involved
132
- gen.imports[`../${clientDirname}/${relativePath}`] = '* as ' + routeName;
282
+ gen.imports[relativePath] = '* as ' + routeName;
133
283
  // Definition code
134
- let routePath = _beforeLast('/' + relativePath, '/index.js');
135
- gen.code.push(`${varName}['${routePath || '/'}'] = ${routeName};`);
284
+ gen.code.push(`${varName}['${namespace}'] = ${routeName};`);
136
285
  // Show
137
- cx.logger && cx.logger.log(`> ./${relativePath}`);
286
+ cx.logger && cx.logger.log(cx.logger.style.comment(` [${namespace}]: `) + cx.logger.style.url(relativePath) + cx.logger.style.comment(` (${Fs.statSync(file).size / 1024} KB)`));
138
287
  }
139
288
  });
140
289
  }
141
290
  if (!indexCount) {
142
- cx.logger && cx.logger.log(`> (none)`);
291
+ cx.logger && cx.logger.log(cx.logger.style.comment(` (none)`));
143
292
  }
144
- cx.logger && cx.logger.log(``);
145
293
  }
146
294
 
147
295
  /**
@@ -149,7 +297,7 @@ function declareRoutesObj(gen, routesDir, varName) {
149
297
  *
150
298
  * @param object gen
151
299
  * @param object paramsObj
152
- * @param string varName
300
+ * @param string varName
153
301
  *
154
302
  * @return void
155
303
  */
@@ -189,7 +337,9 @@ function declareParamsObj(gen, paramsObj, varName = null, indentation = 0) {
189
337
  */
190
338
  async function bundle(gen, outfile, asModule = false) {
191
339
  const cx = this || {};
192
- const compression = cx.flags.compress;
340
+ const compression = !cx.flags.compression ? false : (
341
+ cx.flags.compression === true ? ['gz'] : cx.flags.compression.split(',').map(s => s.trim())
342
+ );
193
343
  const moduleFile = `${_beforeLast(outfile, '.')}.esm.js`;
194
344
 
195
345
  // ------------------
@@ -197,11 +347,10 @@ async function bundle(gen, outfile, asModule = false) {
197
347
  if (cx.logger) {
198
348
  let waiting = cx.logger.waiting(cx.logger.f`Writing the ES module file: ${moduleFile}`);
199
349
  waiting.start();
200
- DotJs.write(gen, moduleFile, 'ES Module file');
350
+ jsFile.write(gen, moduleFile, 'ES Module file');
201
351
  waiting.stop();
202
- cx.logger.info(cx.logger.f`The module file: ${moduleFile}`);
203
352
  } else {
204
- DotJs.write(gen, moduleFile, 'ES Module file');
353
+ jsFile.write(gen, moduleFile, 'ES Module file');
205
354
  }
206
355
 
207
356
  // ----------------
@@ -227,34 +376,93 @@ async function bundle(gen, outfile, asModule = false) {
227
376
  let waiting;
228
377
  if (cx.logger) {
229
378
  waiting = cx.logger.waiting(`Bundling...`);
230
- cx.logger.log('');
231
- cx.logger.log('> Bundling...');
232
- cx.logger.info(cx.logger.f`FROM: ${bundlingConfig.entryPoints[0]}`);
233
- cx.logger.info(cx.logger.f`TO: ${bundlingConfig.outfile}`);
379
+ cx.logger.log(cx.logger.style.keyword(`> `) + 'Bundling...');
234
380
  waiting.start();
235
381
  }
236
- // Run
382
+ // Main
237
383
  await EsBuild.build(bundlingConfig);
238
- if (waiting) waiting.stop();
239
- // Remove moduleFile build
240
- Fs.unlinkSync(bundlingConfig.entryPoints[0]);
241
-
242
- // ----------------
243
384
  // Compress...
385
+ let compressedFiles = [], removals = [];
244
386
  if (compression) {
245
- if (cx.logger) {
246
- waiting = cx.logger.waiting(`Compressing...`);
247
- waiting.start();
248
- }
249
387
  const contents = Fs.readFileSync(bundlingConfig.outfile);
250
- const gzip = gzipSync(contents, {});
251
- const brotli = brotliCompressSync(contents, {});
252
- Fs.writeFileSync(`${bundlingConfig.outfile}.gz`, gzip);
253
- Fs.writeFileSync(`${bundlingConfig.outfile}.br`, brotli);
254
- if (waiting) {
255
- waiting.stop();
256
- cx.logger.log('');
257
- cx.logger.log('> Compression: .gz, .br');
388
+ if (compression.includes('gz')) {
389
+ const gzip = gzipSync(contents, {});
390
+ Fs.writeFileSync(bundlingConfig.outfile + '.gz', gzip);
391
+ compressedFiles.push(bundlingConfig.outfile + '.gz');
392
+ } else {
393
+ removals.push(bundlingConfig.outfile + '.gz');
394
+ }
395
+ if (compression.includes('br')) {
396
+ const brotli = brotliCompressSync(contents, {});
397
+ Fs.writeFileSync(bundlingConfig.outfile + '.br', brotli);
398
+ compressedFiles.push(bundlingConfig.outfile + '.br');
399
+ } else {
400
+ removals.push(bundlingConfig.outfile + '.br');
401
+ }
402
+ }
403
+ // Remove moduleFile build
404
+ Fs.unlinkSync(bundlingConfig.entryPoints[0]);
405
+ removals.forEach(file => Fs.existsSync(file) && Fs.unlinkSync(file));
406
+ if (waiting) waiting.stop();
407
+ // ----------------
408
+ // Stats
409
+ if (cx.logger) {
410
+ [bundlingConfig.outfile].concat(compressedFiles).forEach(file => {
411
+ let ext = '.' + _afterLast(file, '.');
412
+ cx.logger.info(cx.logger.style.comment(` [${ext}]: `) + cx.logger.style.url(file) + cx.logger.style.comment(` (${Fs.statSync(file).size / 1024} KB)`));
413
+ });
414
+ cx.logger.log('');
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Handles auto-embeds
420
+ *
421
+ * @param String targetDocumentFile
422
+ * @param Array embedList
423
+ * @param Array unembedList
424
+ *
425
+ * @return Void
426
+ */
427
+ function handleEmbeds(targetDocumentFile, embedList, unembedList) {
428
+ let targetDocument, successLevel = 0;
429
+ if (Fs.existsSync(targetDocumentFile) && (targetDocument = Fs.readFileSync(targetDocumentFile).toString()) && targetDocument.trim().startsWith('<!DOCTYPE html')) {
430
+ successLevel = 1;
431
+ let dom = new Jsdom.JSDOM(targetDocument), by = 'webflo', touched;
432
+ let embed = (src, before) => {
433
+ let embedded = dom.window.document.querySelector(`script[src="${src}"]`);
434
+ if (!embedded) {
435
+ embedded = dom.window.document.createElement('script');
436
+ embedded.setAttribute('type', 'module');
437
+ embedded.setAttribute('src', src);
438
+ embedded.setAttribute('by', by);
439
+ if (before) {
440
+ before.before(embedded, `\n\t\t`);
441
+ } else {
442
+ dom.window.document.head.appendChild(embedded);
443
+ }
444
+ touched = true;
445
+ }
446
+ return embedded;
447
+ };
448
+ let unembed = src => {
449
+ src = Path.join('/', src);
450
+ let embedded = dom.window.document.querySelector(`script[src="${src}"][by="${by}"]`);
451
+ if (embedded) {
452
+ embedded.remove();
453
+ touched = true;
454
+ }
455
+ };
456
+ embedList.reverse().reduce((prev, src) => {
457
+ return embed(src, prev);
458
+ }, dom.window.document.querySelector(`script[src]`) || dom.window.document.querySelector(`script`));
459
+ unembedList.forEach(src => {
460
+ unembed(src);
461
+ });
462
+ if (touched) {
463
+ Fs.writeFileSync(targetDocumentFile, dom.serialize());
464
+ successLevel = 2;
258
465
  }
259
466
  }
467
+ return successLevel;
260
468
  }
@@ -0,0 +1,7 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import { NamespacedHTML } from '@webqit/oohtml-ssr/apis.js';
6
+
7
+ NamespacedHTML.call( window );
@@ -0,0 +1,8 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import { StateAPI, Subscript } from '@webqit/oohtml-ssr/apis.js';
6
+
7
+ StateAPI.call( window );
8
+ Subscript.call( window );
@@ -0,0 +1,8 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import { HTMLModules, HTMLImports } from '@webqit/oohtml-ssr/apis.js';
6
+
7
+ HTMLModules.call( window );
8
+ HTMLImports.call( window );
@@ -5,6 +5,7 @@
5
5
  import { _any } from '@webqit/util/arr/index.js';
6
6
  import { HttpEvent, Request, Response, Observer } from '../Runtime.js';
7
7
  import { urlPattern } from '../../util.js';
8
+ import Workport from './Workport.js';
8
9
 
9
10
  /**
10
11
  * ---------------------------
@@ -43,7 +44,7 @@ export default class Worker {
43
44
  // Add files to cache
44
45
  evt.waitUntil( self.caches.open(this.cx.params.cache_name).then(cache => {
45
46
  if (this.cx.logger) { this.cx.logger.log('[ServiceWorker] Pre-caching resources.'); }
46
- const cache_only_urls = (this.cx.params.cache_only_urls || []).map(c => c.trim()).filter(c => c && !c.endsWith('/') && !urlPattern(c, self.origin).isPattern());
47
+ const cache_only_urls = (this.cx.params.cache_only_urls || []).map(c => c.trim()).filter(c => c && !urlPattern(c, self.origin).isPattern());
47
48
  return cache.addAll(cache_only_urls);
48
49
  }) );
49
50
  }
@@ -71,11 +72,13 @@ export default class Worker {
71
72
  });
72
73
 
73
74
  // -------------
74
- // ONFETCH
75
+ // ONFETCH
75
76
  self.addEventListener('fetch', event => {
76
77
  // URL schemes that might arrive here but not supported; e.g.: chrome-extension://
77
78
  if (!event.request.url.startsWith('http')) return;
78
79
  event.respondWith((async (req, evt) => {
80
+ let requestingClient = await self.clients.get(event.clientId);
81
+ this.workport.setCurrentClient(requestingClient);
79
82
  const requestInit = [
80
83
  'method', 'headers', 'mode', 'credentials', 'cache', 'redirect', 'referrer', 'integrity',
81
84
  ].reduce((init, prop) => ({ [prop]: req[prop], ...init }), {});
@@ -93,6 +96,33 @@ export default class Worker {
93
96
  })(event.request, event));
94
97
  });
95
98
 
99
+ // -------------
100
+ // Workport
101
+ let workport = new Workport();
102
+ Observer.set(this, 'workport', workport);
103
+ workport.messaging.listen(async evt => {
104
+ let responsePort = evt.ports[0];
105
+ let client = this.clients.get('*');
106
+ let response = client.alert && await client.alert(evt);
107
+ if (responsePort) {
108
+ if (response instanceof Promise) {
109
+ response.then(data => {
110
+ responsePort.postMessage(data);
111
+ });
112
+ } else {
113
+ responsePort.postMessage(response);
114
+ }
115
+ }
116
+ });
117
+ workport.notifications.listen(async evt => {
118
+ let client = this.clients.get('*');
119
+ client.alert && await client.alert(evt);
120
+ });
121
+ workport.push.listen(async evt => {
122
+ let client = this.clients.get('*');
123
+ client.alert && await client.alert(evt);
124
+ });
125
+
96
126
  // ---------------
97
127
  Observer.set(this, 'location', {});
98
128
  Observer.set(this, 'network', {});
@@ -163,22 +193,26 @@ export default class Worker {
163
193
  }
164
194
  const matchUrl = (patterns, url) => _any((patterns || []).map(p => p.trim()).filter(p => p), p => urlPattern(p, self.origin).test(url));
165
195
  const execFetch = () => {
166
- if (matchUrl(this.cx.params.cache_only_urls, request.url)) {
167
- Observer.set(this.network, 'strategy', 'cache-only');
168
- return this.cacheFetch(request, { networkFallback: false, cacheRefresh: false });
196
+ // network_first_urls
197
+ if (!this.cx.params.default_fetching_strategy || this.cx.params.default_fetching_strategy === 'network-first' || matchUrl(this.cx.params.network_first_urls, request.url)) {
198
+ Observer.set(this.network, 'strategy', 'network-first');
199
+ return this.networkFetch(request, { cacheFallback: true, cacheRefresh: true });
200
+ }
201
+ // cache_first_urls
202
+ if (this.cx.params.default_fetching_strategy === 'cache-first' || matchUrl(this.cx.params.cache_first_urls, request.url)) {
203
+ Observer.set(this.network, 'strategy', 'cache-first');
204
+ return this.cacheFetch(request, { networkFallback: true, cacheRefresh: true });
169
205
  }
170
206
  // network_only_urls
171
- if (matchUrl(this.cx.params.network_only_urls, request.url)) {
207
+ if (this.cx.params.default_fetching_strategy === 'network-only' || matchUrl(this.cx.params.network_only_urls, request.url)) {
172
208
  Observer.set(this.network, 'strategy', 'network-only');
173
209
  return this.networkFetch(request, { cacheFallback: false, cacheRefresh: false });
174
210
  }
175
- // cache_first_urls
176
- if (matchUrl(this.cx.params.cache_first_urls, request.url)) {
177
- Observer.set(this.network, 'strategy', 'cache-first');
178
- return this.cacheFetch(request, { networkFallback: true, cacheRefresh: true });
211
+ // cache_only_urls
212
+ if (this.cx.params.default_fetching_strategy === 'cache-only' || matchUrl(this.cx.params.cache_only_urls, request.url)) {
213
+ Observer.set(this.network, 'strategy', 'cache-only');
214
+ return this.cacheFetch(request, { networkFallback: false, cacheRefresh: false });
179
215
  }
180
- Observer.set(this.network, 'strategy', 'network-first');
181
- return this.networkFetch(request, { cacheFallback: true, cacheRefresh: true });
182
216
  };
183
217
  let response = execFetch(request);
184
218
  // This catch() is NOT intended to handle failure of the fetch
@@ -187,18 +221,6 @@ export default class Worker {
187
221
  return response.then(_response => Response.compat(_response));
188
222
  }
189
223
 
190
- // Caching strategy: cache_first
191
- cacheFetch(request, params = {}) {
192
- return this.getRequestCache(request).then(cache => cache.match(request).then(response => {
193
- // Nothing cache, use network
194
- if (!response && params.networkFallback) return this.networkFetch(request, { ...params, cacheFallback: false });
195
- // Note: fetch, but for refreshing purposes only... not the returned response
196
- if (response && params.cacheRefresh) this.networkFetch(request, { ...params, justRefreshing: true });
197
- Observer.set(this.network, 'cache', true);
198
- return response;
199
- }));
200
- }
201
-
202
224
  // Caching strategy: network_first
203
225
  networkFetch(request, params = {}) {
204
226
  if (!params.cacheFallback) {
@@ -215,6 +237,18 @@ export default class Worker {
215
237
  }));
216
238
  }
217
239
 
240
+ // Caching strategy: cache_first
241
+ cacheFetch(request, params = {}) {
242
+ return this.getRequestCache(request).then(cache => cache.match(request).then(response => {
243
+ // Nothing cache, use network
244
+ if (!response && params.networkFallback) return this.networkFetch(request, { ...params, cacheFallback: false });
245
+ // Note: fetch, but for refreshing purposes only... not the returned response
246
+ if (response && params.cacheRefresh) this.networkFetch(request, { ...params, justRefreshing: true });
247
+ Observer.set(this.network, 'cache', true);
248
+ return response;
249
+ }));
250
+ }
251
+
218
252
  // Caches response
219
253
  refreshCache(request, response) {
220
254
  // Check if we received a valid response