@webqit/webflo 0.10.2 → 0.10.5

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 (50) hide show
  1. package/README.md +750 -3
  2. package/package.json +3 -3
  3. package/src/Context.js +8 -4
  4. package/src/config-pi/deployment/Env.js +2 -2
  5. package/src/config-pi/deployment/Layout.js +2 -2
  6. package/src/config-pi/deployment/Origins.js +2 -2
  7. package/src/config-pi/deployment/Virtualization.js +2 -2
  8. package/src/config-pi/runtime/Client.js +45 -14
  9. package/src/config-pi/runtime/Server.js +26 -10
  10. package/src/config-pi/runtime/client/Worker.js +2 -2
  11. package/src/config-pi/runtime/server/Headers.js +2 -2
  12. package/src/config-pi/runtime/server/Redirects.js +2 -2
  13. package/src/config-pi/static/Manifest.js +2 -2
  14. package/src/config-pi/static/Ssg.js +2 -2
  15. package/src/runtime-pi/client/Runtime.js +28 -23
  16. package/src/runtime-pi/client/RuntimeClient.js +28 -15
  17. package/src/runtime-pi/client/generate.js +251 -77
  18. package/src/runtime-pi/client/{generate.oohtml.js → oohtml/full.js} +0 -0
  19. package/src/runtime-pi/client/oohtml/namespacing.js +7 -0
  20. package/src/runtime-pi/client/oohtml/scripting.js +8 -0
  21. package/src/runtime-pi/client/oohtml/templating.js +8 -0
  22. package/src/runtime-pi/client/worker/Worker.js +21 -23
  23. package/src/runtime-pi/server/Router.js +2 -2
  24. package/src/runtime-pi/server/Runtime.js +8 -3
  25. package/src/runtime-pi/server/RuntimeClient.js +21 -11
  26. package/src/webflo.js +7 -9
  27. package/test/site/public/bundle.html +3 -0
  28. package/test/site/public/bundle.html.json +3 -0
  29. package/test/site/public/bundle.js +2 -0
  30. package/test/site/public/bundle.js.gz +0 -0
  31. package/test/site/public/bundle.webflo.js +15 -0
  32. package/test/site/public/bundle.webflo.js.gz +0 -0
  33. package/test/site/public/index.html +30 -0
  34. package/test/site/public/page-2/bundle.html +5 -0
  35. package/test/site/public/page-2/bundle.html.json +1 -0
  36. package/test/site/public/page-2/bundle.js +2 -0
  37. package/test/site/public/page-2/bundle.js.gz +0 -0
  38. package/test/site/public/page-2/index.html +47 -0
  39. package/test/site/public/page-2/logo-130x130.png +0 -0
  40. package/test/site/public/page-2/main.html +3 -0
  41. package/test/site/public/page-4/subpage/bundle.html +0 -0
  42. package/test/site/public/page-4/subpage/bundle.html.json +1 -0
  43. package/test/site/public/page-4/subpage/bundle.js +2 -0
  44. package/test/site/public/page-4/subpage/bundle.js.gz +0 -0
  45. package/test/site/public/page-4/subpage/index.html +31 -0
  46. package/test/site/public/worker.js +3 -0
  47. package/test/site/public/worker.js.gz +0 -0
  48. package/test/site/server/index.js +8 -0
  49. package/src/Cli.js +0 -131
  50. package/src/Configurator.js +0 -97
@@ -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,201 @@ 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 scanSubscopes = scope => {
46
+ let dir = Path.join(dirPublic, scope), passes = 0;
47
+ return [ Fs.readdirSync(dir).reduce((scopes, f) => {
48
+ let resource = Path.join(dir, f);
49
+ if (Fs.statSync(resource).isDirectory()) {
50
+ let subscope = Path.join(scope, f);
51
+ if (Fs.existsSync(Path.join(resource, 'index.html'))) {
52
+ return scopes.concat(subscope);
53
+ }
54
+ passes ++;
55
+ return scopes.concat(scanSubscopes(subscope)[ 0 ]);
56
+ }
57
+ return scopes;
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(scope) {
63
+ let [ subscopes, passes ] = scanSubscopes(scope);
64
+ let routing = { scope, subscopes, passes };
65
+ let codeSplitting = !!(scope !== '/' || subscopes.length);
66
+ let outfileMain = Path.join(scope, clientConfig.bundle_filename),
67
+ outfileWebflo = _beforeLast(clientConfig.bundle_filename, '.js') + '.webflo.js';
68
+ let gen = { imports: {}, code: [], };
69
+ // ------------------
70
+ const initWebflo = gen => {
71
+ if (clientConfig.oohtml_support === 'namespacing') {
72
+ gen.imports[`${dirSelf}/oohtml/namespacing.js`] = null;
73
+ } else if (clientConfig.oohtml_support === 'scripting') {
74
+ gen.imports[`${dirSelf}/oohtml/scripting.js`] = null;
75
+ } else if (clientConfig.oohtml_support === 'templating') {
76
+ gen.imports[`${dirSelf}/oohtml/templating.js`] = null;
77
+ } else if (clientConfig.oohtml_support !== 'none') {
78
+ gen.imports[`${dirSelf}/oohtml/full.js`] = null;
79
+ }
80
+ gen.imports[`${dirSelf}/index.js`] = `* as Webflo`;
81
+ gen.code.push(``);
82
+ gen.code.push(`if (!globalThis.WebQit) {`);
83
+ gen.code.push(` globalThis.WebQit = {}`);
84
+ gen.code.push(`}`);
85
+ gen.code.push(`WebQit.Webflo = Webflo`);
86
+ return gen;
87
+ };
88
+ // ------------------
89
+ if (!codeSplitting) {
90
+ initWebflo(gen);
91
+ } else if (scope === '/') {
92
+ if (cx.logger) {
93
+ cx.logger.log(cx.logger.style.keyword(`-----------------`));
94
+ cx.logger.log(`Base Build`);
95
+ cx.logger.log(cx.logger.style.keyword(`-----------------`));
96
+ }
97
+ let gen1 = initWebflo({ imports: {}, code: [], });
98
+ await bundle.call(cx, gen1, Path.join(dirPublic, outfileWebflo), true/* asModule */);
99
+ }
100
+ // ------------------
101
+ if (cx.logger) {
102
+ cx.logger.log(cx.logger.style.keyword(`-----------------`));
103
+ cx.logger.log(`Client Build ` + cx.logger.style.comment(`(scope:${scope}; is-split:${codeSplitting})`));
104
+ cx.logger.log(cx.logger.style.keyword(`-----------------`));
105
+ }
106
+ gen.code.push(`const { start } = WebQit.Webflo`);
107
+ // ------------------
108
+ // Bundle
109
+ declareStart.call(cx, gen, dirClient, dirPublic, clientConfig, routing);
110
+ await bundle.call(cx, gen, Path.join(dirPublic, outfileMain), true/* asModule */);
111
+ // ------------------
112
+ // Embed/unembed
113
+ let targetDocumentFile = Path.join(dirPublic, scope, 'index.html'),
114
+ outfileWebfloPublic = Path.join(clientConfig.public_base_url, outfileWebflo),
115
+ outfileMainPublic = Path.join(clientConfig.public_base_url, outfileMain),
116
+ embedList = [],
117
+ unembedList = [];
118
+ if (cx.flags['auto-embeds']) {
119
+ if (codeSplitting) {
120
+ embedList.push(outfileWebfloPublic);
121
+ } else {
122
+ unembedList.push(outfileWebfloPublic);
123
+ }
124
+ embedList.push(outfileMainPublic);
125
+ } else {
126
+ unembedList.push(outfileWebfloPublic, outfileMainPublic);
127
+ }
128
+ handleEmbeds(targetDocumentFile, embedList, unembedList);
129
+ // ------------------
130
+ // Recurse
131
+ if (cx.flags.recursive) {
132
+ while (subscopes.length) {
133
+ await generateClient(subscopes.shift());
134
+ }
135
+ }
136
+ };
43
137
  // -----------
44
138
  // 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('');
139
+ const generateWorker = async function(scope) {
140
+ let subscopes = [];
141
+ let routing = { scope, subscopes };
142
+ let gen = { imports: {}, code: [], };
143
+ if (cx.logger) {
144
+ cx.logger.log(cx.logger.style.comment(`-----------------`));
145
+ cx.logger.log(`Worker Build - scope:${scope}`);
146
+ cx.logger.log(cx.logger.style.comment(`-----------------`));
147
+ }
148
+ // ------------------
149
+ // >> Modules import
150
+ gen.imports[`${dirSelf}/worker/index.js`] = `{ start }`;
151
+ gen.code.push(``);
152
+ // ------------------
153
+ // Bundle
154
+ if (workerConfig.cache_only_urls.length) {
155
+ workerConfig.cache_only_urls = workerConfig.cache_only_urls.reduce((urls, url) => {
156
+ // TODO: if (urlPattern(url, self.origin).isPattern()) {}
157
+ return urls.concat(url);
158
+ }, []);
159
+ }
160
+ declareStart.call(cx, gen, dirWorker, dirPublic, workerConfig, routing);
161
+ await bundle.call(cx, gen, Path.join(dirPublic, scope, clientConfig.worker_filename));
162
+ if (cx.flags.recursive) {
163
+ while (subscopes.length) {
164
+ await generateWorker(subscopes.shift());
165
+ }
166
+ }
167
+ };
168
+ // -----------
169
+ // Generate now...
170
+ await generateClient('/');
171
+ if (clientConfig.service_worker_support) {
172
+ await generateWorker('/');
49
173
  }
50
174
  }
51
175
 
52
176
  /**
53
177
  * Compile routes.
54
178
  *
55
- * @param string modulesDir
56
- * @param string routesDir
57
- * @param object paramsObj
58
- * @param string desc
179
+ * @param object gen
180
+ * @param string routesDir
181
+ * @param string targetPublic
182
+ * @param object paramsObj
183
+ * @param object routing
59
184
  *
60
185
  * @return Object
61
186
  */
62
- function getGen(modulesDir, routesDir, paramsObj, desc) {
187
+ function declareStart(gen, routesDir, targetDir, paramsObj, routing) {
63
188
  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
189
  // ------------------
77
190
  // >> Routes mapping
78
191
  gen.code.push(`// >> Routes`);
79
- declareRoutesObj.call(cx, gen, routesDir, 'layout', '');
192
+ declareRoutesObj.call(cx, gen, routesDir, targetDir, 'layout', routing);
80
193
  gen.code.push(``);
81
194
  // ------------------
82
195
  // >> Params
83
196
  gen.code.push(`// >> Params`);
84
- declareParamsObj.call(cx, gen, paramsObj, 'params');
197
+ declareParamsObj.call(cx, gen, { ...paramsObj, routing }, 'params');
85
198
  gen.code.push(``);
86
199
  // ------------------
87
200
  // >> Startup
88
201
  gen.code.push(`// >> Startup`);
89
202
  gen.code.push(`start.call({ layout, params })`);
90
- return gen;
91
203
  }
92
204
 
93
205
  /**
94
206
  * Compile routes.
95
207
  *
96
- * @param object gen
97
- * @param string routesDir
98
- * @param string varName
208
+ * @param object gen
209
+ * @param string routesDir
210
+ * @param string targetDir
211
+ * @param string varName
212
+ * @param object routing
99
213
  *
100
214
  * @return void
101
215
  */
102
- function declareRoutesObj(gen, routesDir, varName) {
216
+ function declareRoutesObj(gen, routesDir, targetDir, varName, routing) {
103
217
  const cx = this || {};
104
- cx.logger && cx.logger.log(`> Declaring routes...`);
218
+ let _routesDir = Path.join(routesDir, routing.scope),
219
+ _targetDir = Path.join(targetDir, routing.scope);
220
+ cx.logger && cx.logger.log(cx.logger.style.keyword(`> `) + `Declaring routes...`);
105
221
  // ----------------
106
222
  // Directory walker
107
223
  const walk = (dir, callback) => {
108
224
  Fs.readdirSync(dir).forEach(f => {
109
225
  let resource = Path.join(dir, f);
226
+ let namespace = _beforeLast('/' + Path.relative(routesDir, resource), '/index.js') || '/';
110
227
  if (Fs.statSync(resource).isDirectory()) {
228
+ if (routing.subscopes.includes(namespace)) return;
111
229
  walk(resource, callback);
112
230
  } else {
113
- let ext = Path.extname(resource) || '';
114
- callback(resource, ext);
231
+ let relativePath = Path.relative(_targetDir, resource);
232
+ callback(resource, namespace, relativePath);
115
233
  }
116
234
  });
117
235
  };
@@ -119,29 +237,25 @@ function declareRoutesObj(gen, routesDir, varName) {
119
237
  // >> Routes mapping
120
238
  gen.code.push(`const ${varName} = {};`);
121
239
  let indexCount = 0;
122
- if (routesDir && Fs.existsSync(routesDir)) {
123
- let clientDirname = routesDir.replace(/\\/g, '/').split('/').pop();
124
- walk(routesDir, (file, ext) => {
240
+ if (Fs.existsSync(_routesDir)) {
241
+ walk(_routesDir, (file, namespace, relativePath) => {
125
242
  //relativePath = relativePath.replace(/\\/g, '/');
126
243
  if (file.replace(/\\/g, '/').endsWith('/index.js')) {
127
- let relativePath = Path.relative(routesDir, file).replace(/\\/g, '/');
128
244
  // Import code
129
245
  let routeName = 'index' + (++ indexCount);
130
246
  // IMPORTANT: we;re taking a step back here so that the parent-child relationship for
131
247
  // the directories be involved
132
- gen.imports[`../${clientDirname}/${relativePath}`] = '* as ' + routeName;
248
+ gen.imports[relativePath] = '* as ' + routeName;
133
249
  // Definition code
134
- let routePath = _beforeLast('/' + relativePath, '/index.js');
135
- gen.code.push(`${varName}['${routePath || '/'}'] = ${routeName};`);
250
+ gen.code.push(`${varName}['${namespace}'] = ${routeName};`);
136
251
  // Show
137
- cx.logger && cx.logger.log(`> ./${relativePath}`);
252
+ 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
253
  }
139
254
  });
140
255
  }
141
256
  if (!indexCount) {
142
- cx.logger && cx.logger.log(`> (none)`);
257
+ cx.logger && cx.logger.log(cx.logger.style.comment(` (none)`));
143
258
  }
144
- cx.logger && cx.logger.log(``);
145
259
  }
146
260
 
147
261
  /**
@@ -149,7 +263,7 @@ function declareRoutesObj(gen, routesDir, varName) {
149
263
  *
150
264
  * @param object gen
151
265
  * @param object paramsObj
152
- * @param string varName
266
+ * @param string varName
153
267
  *
154
268
  * @return void
155
269
  */
@@ -189,7 +303,9 @@ function declareParamsObj(gen, paramsObj, varName = null, indentation = 0) {
189
303
  */
190
304
  async function bundle(gen, outfile, asModule = false) {
191
305
  const cx = this || {};
192
- const compression = cx.flags.compress;
306
+ const compression = !cx.flags.compression ? false : (
307
+ cx.flags.compression === true ? ['gz'] : cx.flags.compression.split(',').map(s => s.trim())
308
+ );
193
309
  const moduleFile = `${_beforeLast(outfile, '.')}.esm.js`;
194
310
 
195
311
  // ------------------
@@ -197,11 +313,10 @@ async function bundle(gen, outfile, asModule = false) {
197
313
  if (cx.logger) {
198
314
  let waiting = cx.logger.waiting(cx.logger.f`Writing the ES module file: ${moduleFile}`);
199
315
  waiting.start();
200
- DotJs.write(gen, moduleFile, 'ES Module file');
316
+ jsFile.write(gen, moduleFile, 'ES Module file');
201
317
  waiting.stop();
202
- cx.logger.info(cx.logger.f`The module file: ${moduleFile}`);
203
318
  } else {
204
- DotJs.write(gen, moduleFile, 'ES Module file');
319
+ jsFile.write(gen, moduleFile, 'ES Module file');
205
320
  }
206
321
 
207
322
  // ----------------
@@ -227,34 +342,93 @@ async function bundle(gen, outfile, asModule = false) {
227
342
  let waiting;
228
343
  if (cx.logger) {
229
344
  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}`);
345
+ cx.logger.log(cx.logger.style.keyword(`> `) + 'Bundling...');
234
346
  waiting.start();
235
347
  }
236
- // Run
348
+ // Main
237
349
  await EsBuild.build(bundlingConfig);
238
- if (waiting) waiting.stop();
239
- // Remove moduleFile build
240
- Fs.unlinkSync(bundlingConfig.entryPoints[0]);
241
-
242
- // ----------------
243
350
  // Compress...
351
+ let compressedFiles = [], removals = [];
244
352
  if (compression) {
245
- if (cx.logger) {
246
- waiting = cx.logger.waiting(`Compressing...`);
247
- waiting.start();
248
- }
249
353
  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');
354
+ if (compression.includes('gz')) {
355
+ const gzip = gzipSync(contents, {});
356
+ Fs.writeFileSync(bundlingConfig.outfile + '.gz', gzip);
357
+ compressedFiles.push(bundlingConfig.outfile + '.gz');
358
+ } else {
359
+ removals.push(bundlingConfig.outfile + '.gz');
360
+ }
361
+ if (compression.includes('br')) {
362
+ const brotli = brotliCompressSync(contents, {});
363
+ Fs.writeFileSync(bundlingConfig.outfile + '.br', brotli);
364
+ compressedFiles.push(bundlingConfig.outfile + '.br');
365
+ } else {
366
+ removals.push(bundlingConfig.outfile + '.br');
367
+ }
368
+ }
369
+ // Remove moduleFile build
370
+ Fs.unlinkSync(bundlingConfig.entryPoints[0]);
371
+ removals.forEach(file => Fs.unlinkSync(file));
372
+ if (waiting) waiting.stop();
373
+ // ----------------
374
+ // Stats
375
+ if (cx.logger) {
376
+ [bundlingConfig.outfile].concat(compressedFiles).forEach(file => {
377
+ let ext = '.' + _afterLast(file, '.');
378
+ cx.logger.info(cx.logger.style.comment(` [${ext}]: `) + cx.logger.style.url(file) + cx.logger.style.comment(` (${Fs.statSync(file).size / 1024} KB)`));
379
+ });
380
+ cx.logger.log('');
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Handles auto-embeds
386
+ *
387
+ * @param String targetDocumentFile
388
+ * @param Array embedList
389
+ * @param Array unembedList
390
+ *
391
+ * @return Void
392
+ */
393
+ function handleEmbeds(targetDocumentFile, embedList, unembedList) {
394
+ let targetDocument, successLevel = 0;
395
+ if (Fs.existsSync(targetDocumentFile) && (targetDocument = Fs.readFileSync(targetDocumentFile).toString()) && targetDocument.trim().startsWith('<!DOCTYPE html')) {
396
+ successLevel = 1;
397
+ let dom = new Jsdom.JSDOM(targetDocument), by = 'webflo', touched;
398
+ let embed = (src, before) => {
399
+ let embedded = dom.window.document.querySelector(`script[src="${src}"]`);
400
+ if (!embedded) {
401
+ embedded = dom.window.document.createElement('script');
402
+ embedded.setAttribute('type', 'module');
403
+ embedded.setAttribute('src', src);
404
+ embedded.setAttribute('by', by);
405
+ if (before) {
406
+ before.before(embedded, `\n\t\t`);
407
+ } else {
408
+ dom.window.document.head.appendChild(embedded);
409
+ }
410
+ touched = true;
411
+ }
412
+ return embedded;
413
+ };
414
+ let unembed = src => {
415
+ src = Path.join('/', src);
416
+ let embedded = dom.window.document.querySelector(`script[src="${src}"][by="${by}"]`);
417
+ if (embedded) {
418
+ embedded.remove();
419
+ touched = true;
420
+ }
421
+ };
422
+ embedList.reverse().reduce((prev, src) => {
423
+ return embed(src, prev);
424
+ }, dom.window.document.querySelector(`script[src]`) || dom.window.document.querySelector(`script`));
425
+ unembedList.forEach(src => {
426
+ unembed(src);
427
+ });
428
+ if (touched) {
429
+ Fs.writeFileSync(targetDocumentFile, dom.serialize());
430
+ successLevel = 2;
258
431
  }
259
432
  }
433
+ return successLevel;
260
434
  }
@@ -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 );
@@ -43,7 +43,7 @@ export default class Worker {
43
43
  // Add files to cache
44
44
  evt.waitUntil( self.caches.open(this.cx.params.cache_name).then(cache => {
45
45
  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());
46
+ const cache_only_urls = (this.cx.params.cache_only_urls || []).map(c => c.trim()).filter(c => c && !urlPattern(c, self.origin).isPattern());
47
47
  return cache.addAll(cache_only_urls);
48
48
  }) );
49
49
  }
@@ -72,14 +72,25 @@ export default class Worker {
72
72
 
73
73
  // -------------
74
74
  // ONFETCH
75
- self.addEventListener('fetch', async evt => {
75
+ self.addEventListener('fetch', event => {
76
76
  // URL schemes that might arrive here but not supported; e.g.: chrome-extension://
77
- if (!evt.request.url.startsWith('http')) return;
78
- const deriveInit = req => [
79
- 'method', 'headers', 'body', 'mode', 'credentials', 'cache', 'redirect', 'referrer', 'integrity',
80
- ].reduce((init, prop) => ({ [prop]: prop === 'body' && !req.body ? req : req[prop], ...init }), {});
81
- const requestInit = deriveInit(evt.request.clone());
82
- evt.respondWith(this.go(evt.request.url, requestInit, { event: evt }));
77
+ if (!event.request.url.startsWith('http')) return;
78
+ event.respondWith((async (req, evt) => {
79
+ const requestInit = [
80
+ 'method', 'headers', 'mode', 'credentials', 'cache', 'redirect', 'referrer', 'integrity',
81
+ ].reduce((init, prop) => ({ [prop]: req[prop], ...init }), {});
82
+ if (!['GET', 'HEAD'].includes(req.method)) {
83
+ requestInit.body = await req.text();
84
+ }
85
+ // Now, the following is key:
86
+ // The browser likes to use "force-cache" for "navigate" requests, when, e.g: re-entering your site with the back button
87
+ // Problem here, force-cache forces out JSON not HTML as per webflo's design.
88
+ // So, we detect this scenerio and avoid it.
89
+ if (req.cache === 'force-cache'/* && req.mode === 'navigate' - even webflo client init call also comes with that... needs investigation */) {
90
+ requestInit.cache = 'default';
91
+ }
92
+ return this.go(req.url, requestInit, { event: evt });
93
+ })(event.request, event));
83
94
  });
84
95
 
85
96
  // ---------------
@@ -115,7 +126,7 @@ export default class Worker {
115
126
  let httpEvent = new HttpEvent(request, detail, (id = null, persistent = false) => this.getSession(httpEvent, id, persistent));
116
127
  httpEvent.port.listen(message => {
117
128
  if (message.$type === 'handler:hints' && message.session) {
118
- // TODO: Sync sesseion data from client
129
+ // TODO: Sync session data from client
119
130
  return Promise.resolve();
120
131
  }
121
132
  });
@@ -132,20 +143,7 @@ export default class Worker {
132
143
  }
133
144
 
134
145
  // Generates request object
135
- async generateRequest(href, init) {
136
- // Now, the following is key:
137
- // The browser likes to use "force-cache" for "navigate" requests
138
- // when, for example, the back button was used.
139
- // Thus the origin server would still not be contacted by the self.fetch() below, leading to inconsistencies in responses.
140
- // So, we detect this scenerio and avoid it.
141
- if (init.mode === 'navigate' && init.cache === 'force-cache') {
142
- init = { ...init, cache: 'default' };
143
- }
144
- if (init.method === 'POST' && init.body instanceof self.Request) {
145
- init = { ...init, body: await init.body.text(), };
146
- } else if (['GET', 'HEAD'].includes(init.method.toUpperCase()) && init.body) {
147
- init = { ...init, body: null };
148
- }
146
+ generateRequest(href, init) {
149
147
  let request = new Request(href, init);
150
148
  return request;
151
149
  }
@@ -88,9 +88,9 @@ export default class Router extends _Router {
88
88
  response = new httpEvent.Response(null, { status: 500, statusText: `Error reading static file: ${filename}` } );
89
89
  } else {
90
90
  // if the file is found, set Content-type and send data
91
- const type = Mime.lookup(ext);
91
+ let mime = Mime.lookup(ext);
92
92
  response = new httpEvent.Response(data, { headers: {
93
- contentType: type === 'application/javascript' ? 'text/javascript' : type,
93
+ contentType: mime === 'application/javascript' ? 'text/javascript' : mime,
94
94
  contentLength: Buffer.byteLength(data),
95
95
  } });
96
96
  if (enc) {
@@ -245,9 +245,14 @@ export default class Runtime {
245
245
  Observer.set(this, 'location', {});
246
246
  Observer.set(this, 'network', {});
247
247
  // ---------------
248
- if (this.cx.app.title && this.cx.logger) {
249
- this.cx.logger.info(`> Server running (${this.cx.app.title || ''})`);
250
- }
248
+ this.ready.then(() => {
249
+ if (!this.cx.logger) return;
250
+ if (this.cx.server.shared) {
251
+ this.cx.logger.info(`> Server running (shared)`);
252
+ } else {
253
+ this.cx.logger.info(`> Server running (${this.cx.app.title || ''})::${this.cx.server.port}`);
254
+ }
255
+ });
251
256
  }
252
257
 
253
258
  /**
@@ -45,12 +45,13 @@ export default class RuntimeClient {
45
45
  // --------
46
46
  // Rendering
47
47
  // --------
48
+
48
49
  if (response.ok && response.bodyAttrs.inputType === 'object' && httpEvent.request.headers.accept.match('text/html')) {
49
50
  let rendering = await this.render(httpEvent, router, response);
50
- if (typeof rendering !== 'string') {
51
- throw new Error('render() must return a window object or a string response.');
51
+ if (typeof rendering !== 'string' && !(typeof rendering === 'object' && rendering && typeof rendering.toString === 'function')) {
52
+ throw new Error('render() must return a string response or an object that implements toString()..');
52
53
  }
53
- response = new httpEvent.Response(rendering, {
54
+ response = new httpEvent.Response(rendering.toString(), {
54
55
  status: response.status,
55
56
  headers: { ...response.headers.json(), contentType: 'text/html' },
56
57
  });
@@ -81,19 +82,28 @@ export default class RuntimeClient {
81
82
  SOURCE: renderFile,
82
83
  URL: httpEvent.url.href,
83
84
  ROOT: this.cx.CWD,
85
+ OOHTML_LEVEL: this.cx.server.oohtml_support,
84
86
  });
85
87
  const { window } = await import('@webqit/oohtml-ssr/instance.js?' + instanceParams);
86
88
  // --------
87
89
  // OOHTML would waiting for DOM-ready in order to be initialized
88
- await new Promise(res => window.WebQit.DOM.ready(res));
89
- await new Promise(res => (window.document.templatesReadyState === 'complete' && res(), window.document.addEventListener('templatesreadystatechange', res)));
90
- if (!window.document.state.env) {
91
- window.document.setState({
92
- env: 'server',
93
- }, { update: true });
90
+ if (window.WebQit.DOM) {
91
+ await new Promise(res => window.WebQit.DOM.ready(res));
92
+ }
93
+ if (window.document.templates) {
94
+ await new Promise(res => (window.document.templatesReadyState === 'complete' && res(), window.document.addEventListener('templatesreadystatechange', res)));
95
+ }
96
+ if (window.document.state) {
97
+ if (!window.document.state.env) {
98
+ window.document.setState({
99
+ env: 'server',
100
+ }, { update: true });
101
+ }
102
+ window.document.setState({ page: data, url: httpEvent.url }, { update: 'merge' });
103
+ }
104
+ if (window.document.templates) {
105
+ window.document.body.setAttribute('template', 'page/' + httpEvent.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
94
106
  }
95
- window.document.setState({ page: data, url: httpEvent.url }, { update: 'merge' });
96
- window.document.body.setAttribute('template', 'page/' + httpEvent.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
97
107
  await new Promise(res => setTimeout(res, 10));
98
108
  return window;
99
109
  });