@webqit/webflo 0.11.21 → 0.11.24

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 (86) hide show
  1. package/.gitignore +7 -7
  2. package/LICENSE +20 -20
  3. package/README.md +2074 -2071
  4. package/package.json +82 -82
  5. package/src/Context.js +79 -79
  6. package/src/config-pi/deployment/Env.js +69 -69
  7. package/src/config-pi/deployment/Layout.js +65 -65
  8. package/src/config-pi/deployment/Origins.js +133 -133
  9. package/src/config-pi/deployment/Virtualization.js +65 -65
  10. package/src/config-pi/deployment/index.js +17 -17
  11. package/src/config-pi/index.js +15 -15
  12. package/src/config-pi/runtime/Client.js +101 -101
  13. package/src/config-pi/runtime/Server.js +128 -128
  14. package/src/config-pi/runtime/client/Worker.js +135 -135
  15. package/src/config-pi/runtime/client/index.js +11 -11
  16. package/src/config-pi/runtime/index.js +17 -17
  17. package/src/config-pi/runtime/server/Headers.js +77 -77
  18. package/src/config-pi/runtime/server/Redirects.js +73 -73
  19. package/src/config-pi/runtime/server/index.js +13 -13
  20. package/src/config-pi/static/Manifest.js +321 -321
  21. package/src/config-pi/static/Ssg.js +51 -51
  22. package/src/config-pi/static/index.js +13 -13
  23. package/src/deployment-pi/index.js +10 -10
  24. package/src/deployment-pi/origins/index.js +215 -215
  25. package/src/index.js +19 -19
  26. package/src/runtime-pi/Router.js +131 -131
  27. package/src/runtime-pi/client/Context.js +6 -6
  28. package/src/runtime-pi/client/Router.js +47 -47
  29. package/src/runtime-pi/client/Runtime.js +357 -341
  30. package/src/runtime-pi/client/RuntimeClient.js +98 -98
  31. package/src/runtime-pi/client/Storage.js +56 -56
  32. package/src/runtime-pi/client/Url.js +205 -205
  33. package/src/runtime-pi/client/Workport.js +163 -163
  34. package/src/runtime-pi/client/generate.js +467 -467
  35. package/src/runtime-pi/client/index.js +23 -23
  36. package/src/runtime-pi/client/oohtml/full.js +6 -6
  37. package/src/runtime-pi/client/oohtml/namespacing.js +6 -6
  38. package/src/runtime-pi/client/oohtml/scripting.js +7 -7
  39. package/src/runtime-pi/client/oohtml/templating.js +7 -7
  40. package/src/runtime-pi/client/whatwag.js +27 -27
  41. package/src/runtime-pi/client/worker/Context.js +6 -6
  42. package/src/runtime-pi/client/worker/Worker.js +291 -291
  43. package/src/runtime-pi/client/worker/WorkerClient.js +46 -46
  44. package/src/runtime-pi/client/worker/Workport.js +79 -79
  45. package/src/runtime-pi/client/worker/index.js +23 -23
  46. package/src/runtime-pi/index.js +13 -13
  47. package/src/runtime-pi/server/Context.js +15 -15
  48. package/src/runtime-pi/server/Router.js +157 -157
  49. package/src/runtime-pi/server/Runtime.js +547 -547
  50. package/src/runtime-pi/server/RuntimeClient.js +112 -112
  51. package/src/runtime-pi/server/index.js +23 -23
  52. package/src/runtime-pi/server/whatwag.js +35 -35
  53. package/src/runtime-pi/util.js +162 -162
  54. package/src/runtime-pi/xFormData.js +59 -59
  55. package/src/runtime-pi/xHeaders.js +87 -87
  56. package/src/runtime-pi/xHttpEvent.js +92 -92
  57. package/src/runtime-pi/xHttpMessage.js +179 -179
  58. package/src/runtime-pi/xRequest.js +73 -73
  59. package/src/runtime-pi/xRequestHeaders.js +94 -94
  60. package/src/runtime-pi/xResponse.js +68 -68
  61. package/src/runtime-pi/xResponseHeaders.js +109 -109
  62. package/src/runtime-pi/xURL.js +110 -110
  63. package/src/runtime-pi/xfetch.js +6 -6
  64. package/src/services-pi/certbot/http-auth-hook.js +22 -22
  65. package/src/services-pi/certbot/http-cleanup-hook.js +22 -22
  66. package/src/services-pi/certbot/index.js +79 -79
  67. package/src/services-pi/index.js +8 -8
  68. package/src/static-pi/index.js +10 -10
  69. package/src/webflo.js +31 -31
  70. package/test/index.test.js +26 -25
  71. package/test/site/package.json +9 -9
  72. package/test/site/public/bundle.html +5 -5
  73. package/test/site/public/bundle.html.json +3 -3
  74. package/test/site/public/bundle.js +2 -2
  75. package/test/site/public/bundle.webflo.js +15 -15
  76. package/test/site/public/index.html +29 -29
  77. package/test/site/public/index1.html +34 -34
  78. package/test/site/public/page-2/bundle.html +4 -4
  79. package/test/site/public/page-2/bundle.js +2 -2
  80. package/test/site/public/page-2/index.html +45 -45
  81. package/test/site/public/page-2/main.html +2 -2
  82. package/test/site/public/page-4/subpage/bundle.js +2 -2
  83. package/test/site/public/page-4/subpage/index.html +30 -30
  84. package/test/site/public/sparoots.json +4 -4
  85. package/test/site/public/worker.js +3 -3
  86. package/test/site/server/index.js +15 -15
@@ -1,468 +1,468 @@
1
-
2
- /**
3
- * imports
4
- */
5
- import Fs from 'fs';
6
- import Url from 'url';
7
- import Path from 'path';
8
- import Jsdom from 'jsdom';
9
- import EsBuild from 'esbuild';
10
- import { _afterLast, _beforeLast } from '@webqit/util/str/index.js';
11
- import { _isObject, _isArray } from '@webqit/util/js/index.js';
12
- import { jsFile } from '@webqit/backpack/src/dotfile/index.js';
13
- import { gzipSync, brotliCompressSync } from 'zlib';
14
- import { urlPattern } from '../util.js';
15
-
16
- /**
17
- * @generate
18
- */
19
- export async function generate() {
20
- const cx = this || {};
21
- // -----------
22
- if (!cx.config.runtime?.Client) {
23
- throw new Error(`The Client configurator "config.runtime.Client" is required in context.`);
24
- }
25
- if (!cx.config.deployment?.Layout) {
26
- throw new Error(`The Client configurator "config.deployment.Layout" is required in context.`);
27
- }
28
- const clientConfig = await (new cx.config.runtime.Client(cx)).read();
29
- if (clientConfig.support_service_worker && !cx.config.runtime.client?.Worker) {
30
- throw new Error(`The Service Worker configurator "config.runtime.client.Worker" is required in context.`);
31
- }
32
- const workerConfig = await (new cx.config.runtime.client.Worker(cx)).read();
33
- // -----------
34
- if (!cx.config.deployment?.Layout) {
35
- throw new Error(`The Layout configurator "config.deployment.Layout" is required in context.`);
36
- }
37
- const layoutConfig = await (new cx.config.deployment.Layout(cx)).read();
38
- // -----------
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);
42
- const dirSelf = Path.dirname(Url.fileURLToPath(import.meta.url)).replace(/\\/g, '/');
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
- // -----------
61
- // Generate client build
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
- };
139
- // -----------
140
- // Generate worker build
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('/');
207
- }
208
- }
209
-
210
- /**
211
- * Compile routes.
212
- *
213
- * @param object gen
214
- * @param string routesDir
215
- * @param string targetPublic
216
- * @param object paramsObj
217
- * @param object routing
218
- *
219
- * @return Object
220
- */
221
- function declareStart(gen, routesDir, targetDir, paramsObj, routing) {
222
- const cx = this || {};
223
- // ------------------
224
- // >> Routes mapping
225
- gen.code.push(`// >> Routes`);
226
- declareRoutesObj.call(cx, gen, routesDir, targetDir, 'layout', routing);
227
- gen.code.push(``);
228
- // ------------------
229
- // >> Params
230
- gen.code.push(`// >> Params`);
231
- declareParamsObj.call(cx, gen, { ...paramsObj, routing }, 'params');
232
- gen.code.push(``);
233
- // ------------------
234
- // >> Startup
235
- gen.code.push(`// >> Startup`);
236
- gen.code.push(`start.call({ layout, params })`);
237
- }
238
-
239
- /**
240
- * Compile routes.
241
- *
242
- * @param object gen
243
- * @param string routesDir
244
- * @param string targetDir
245
- * @param string varName
246
- * @param object routing
247
- *
248
- * @return void
249
- */
250
- function declareRoutesObj(gen, routesDir, targetDir, varName, routing) {
251
- const cx = this || {};
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...`);
255
- // ----------------
256
- // Directory walker
257
- const walk = (dir, callback) => {
258
- Fs.readdirSync(dir).forEach(f => {
259
- let resource = Path.join(dir, f);
260
- let namespace = _beforeLast('/' + Path.relative(routesDir, resource), '/index.js') || '/';
261
- if (Fs.statSync(resource).isDirectory()) {
262
- if (routing.subroots.includes(namespace)) return;
263
- walk(resource, callback);
264
- } else {
265
- let relativePath = Path.relative(_targetDir, resource);
266
- callback(resource, namespace, relativePath);
267
- }
268
- });
269
- };
270
- // ----------------
271
- // >> Routes mapping
272
- gen.code.push(`const ${varName} = {};`);
273
- let indexCount = 0;
274
- if (Fs.existsSync(_routesDir)) {
275
- walk(_routesDir, (file, namespace, relativePath) => {
276
- //relativePath = relativePath.replace(/\\/g, '/');
277
- if (file.replace(/\\/g, '/').endsWith('/index.js')) {
278
- // Import code
279
- let routeName = 'index' + (++ indexCount);
280
- // IMPORTANT: we;re taking a step back here so that the parent-child relationship for
281
- // the directories be involved
282
- gen.imports[relativePath] = '* as ' + routeName;
283
- // Definition code
284
- gen.code.push(`${varName}['${namespace}'] = ${routeName};`);
285
- // Show
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)`));
287
- }
288
- });
289
- }
290
- if (!indexCount) {
291
- cx.logger && cx.logger.log(cx.logger.style.comment(` (none)`));
292
- }
293
- }
294
-
295
- /**
296
- * Compile params.
297
- *
298
- * @param object gen
299
- * @param object paramsObj
300
- * @param string varName
301
- *
302
- * @return void
303
- */
304
- function declareParamsObj(gen, paramsObj, varName = null, indentation = 0) {
305
- const cx = this || {};
306
- // ----------------
307
- // Params compilation
308
- if (varName) gen.code.push(`const ${varName} = {`);
309
- _isArray(paramsObj)
310
- Object.keys(paramsObj).forEach(name => {
311
- let _name = ` ${' '.repeat(indentation)}${(_isArray(paramsObj) ? '' : (name.includes(' ') ? `'${name}'` : name) + ': ')}`;
312
- if ([ 'boolean', 'number' ].includes(typeof paramsObj[name])) {
313
- gen.code.push(`${_name}${paramsObj[name]},`);
314
- } else if (_isArray(paramsObj[name])) {
315
- gen.code.push(`${_name}[`);
316
- declareParamsObj.call(cx, gen, paramsObj[name], null, indentation + 1);
317
- gen.code.push(` ${' '.repeat(indentation)}],`);
318
- } else if (_isObject(paramsObj[name])) {
319
- gen.code.push(`${_name}{`);
320
- declareParamsObj.call(cx, gen, paramsObj[name], null, indentation + 1);
321
- gen.code.push(` ${' '.repeat(indentation)}},`);
322
- } else {
323
- gen.code.push(`${_name}'${paramsObj[name]}',`);
324
- }
325
- });
326
- if (varName) gen.code.push(`};`);
327
- }
328
-
329
- /**
330
- * Bundle generated file
331
- *
332
- * @param object gen
333
- * @param String outfile
334
- * @param boolean asModule
335
- *
336
- * @return Promise
337
- */
338
- async function bundle(gen, outfile, asModule = false) {
339
- const cx = this || {};
340
- const compression = !cx.flags.compression ? false : (
341
- cx.flags.compression === true ? ['gz'] : cx.flags.compression.split(',').map(s => s.trim())
342
- );
343
- const moduleFile = `${_beforeLast(outfile, '.')}.esm.js`;
344
-
345
- // ------------------
346
- // >> Show waiting...
347
- if (cx.logger) {
348
- let waiting = cx.logger.waiting(cx.logger.f`Writing the ES module file: ${moduleFile}`);
349
- waiting.start();
350
- jsFile.write(gen, moduleFile, 'ES Module file');
351
- waiting.stop();
352
- } else {
353
- jsFile.write(gen, moduleFile, 'ES Module file');
354
- }
355
-
356
- // ----------------
357
- // >> Webpack config
358
- const bundlingConfig = {
359
- entryPoints: [ moduleFile ],
360
- outfile,
361
- bundle: true,
362
- minify: true,
363
- banner: { js: '/** @webqit/webflo */', },
364
- footer: { js: '', },
365
- format: 'esm',
366
- };
367
- if (!asModule) {
368
- // Support top-level await
369
- // See: https://github.com/evanw/esbuild/issues/253#issuecomment-826147115
370
- bundlingConfig.banner.js += '(async () => {';
371
- bundlingConfig.footer.js += '})();';
372
- }
373
-
374
- // ----------------
375
- // The bundling process
376
- let waiting;
377
- if (cx.logger) {
378
- waiting = cx.logger.waiting(`Bundling...`);
379
- cx.logger.log(cx.logger.style.keyword(`> `) + 'Bundling...');
380
- waiting.start();
381
- }
382
- // Main
383
- await EsBuild.build(bundlingConfig);
384
- // Compress...
385
- let compressedFiles = [], removals = [];
386
- if (compression) {
387
- const contents = Fs.readFileSync(bundlingConfig.outfile);
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;
465
- }
466
- }
467
- return successLevel;
1
+
2
+ /**
3
+ * imports
4
+ */
5
+ import Fs from 'fs';
6
+ import Url from 'url';
7
+ import Path from 'path';
8
+ import Jsdom from 'jsdom';
9
+ import EsBuild from 'esbuild';
10
+ import { _afterLast, _beforeLast } from '@webqit/util/str/index.js';
11
+ import { _isObject, _isArray } from '@webqit/util/js/index.js';
12
+ import { jsFile } from '@webqit/backpack/src/dotfile/index.js';
13
+ import { gzipSync, brotliCompressSync } from 'zlib';
14
+ import { urlPattern } from '../util.js';
15
+
16
+ /**
17
+ * @generate
18
+ */
19
+ export async function generate() {
20
+ const cx = this || {};
21
+ // -----------
22
+ if (!cx.config.runtime?.Client) {
23
+ throw new Error(`The Client configurator "config.runtime.Client" is required in context.`);
24
+ }
25
+ if (!cx.config.deployment?.Layout) {
26
+ throw new Error(`The Client configurator "config.deployment.Layout" is required in context.`);
27
+ }
28
+ const clientConfig = await (new cx.config.runtime.Client(cx)).read();
29
+ if (clientConfig.support_service_worker && !cx.config.runtime.client?.Worker) {
30
+ throw new Error(`The Service Worker configurator "config.runtime.client.Worker" is required in context.`);
31
+ }
32
+ const workerConfig = await (new cx.config.runtime.client.Worker(cx)).read();
33
+ // -----------
34
+ if (!cx.config.deployment?.Layout) {
35
+ throw new Error(`The Layout configurator "config.deployment.Layout" is required in context.`);
36
+ }
37
+ const layoutConfig = await (new cx.config.deployment.Layout(cx)).read();
38
+ // -----------
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);
42
+ const dirSelf = Path.dirname(Url.fileURLToPath(import.meta.url)).replace(/\\/g, '/');
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
+ // -----------
61
+ // Generate client build
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
+ };
139
+ // -----------
140
+ // Generate worker build
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('/');
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Compile routes.
212
+ *
213
+ * @param object gen
214
+ * @param string routesDir
215
+ * @param string targetPublic
216
+ * @param object paramsObj
217
+ * @param object routing
218
+ *
219
+ * @return Object
220
+ */
221
+ function declareStart(gen, routesDir, targetDir, paramsObj, routing) {
222
+ const cx = this || {};
223
+ // ------------------
224
+ // >> Routes mapping
225
+ gen.code.push(`// >> Routes`);
226
+ declareRoutesObj.call(cx, gen, routesDir, targetDir, 'layout', routing);
227
+ gen.code.push(``);
228
+ // ------------------
229
+ // >> Params
230
+ gen.code.push(`// >> Params`);
231
+ declareParamsObj.call(cx, gen, { ...paramsObj, routing }, 'params');
232
+ gen.code.push(``);
233
+ // ------------------
234
+ // >> Startup
235
+ gen.code.push(`// >> Startup`);
236
+ gen.code.push(`start.call({ layout, params })`);
237
+ }
238
+
239
+ /**
240
+ * Compile routes.
241
+ *
242
+ * @param object gen
243
+ * @param string routesDir
244
+ * @param string targetDir
245
+ * @param string varName
246
+ * @param object routing
247
+ *
248
+ * @return void
249
+ */
250
+ function declareRoutesObj(gen, routesDir, targetDir, varName, routing) {
251
+ const cx = this || {};
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...`);
255
+ // ----------------
256
+ // Directory walker
257
+ const walk = (dir, callback) => {
258
+ Fs.readdirSync(dir).forEach(f => {
259
+ let resource = Path.join(dir, f);
260
+ let namespace = _beforeLast('/' + Path.relative(routesDir, resource), '/index.js') || '/';
261
+ if (Fs.statSync(resource).isDirectory()) {
262
+ if (routing.subroots.includes(namespace)) return;
263
+ walk(resource, callback);
264
+ } else {
265
+ let relativePath = Path.relative(_targetDir, resource);
266
+ callback(resource, namespace, relativePath);
267
+ }
268
+ });
269
+ };
270
+ // ----------------
271
+ // >> Routes mapping
272
+ gen.code.push(`const ${varName} = {};`);
273
+ let indexCount = 0;
274
+ if (Fs.existsSync(_routesDir)) {
275
+ walk(_routesDir, (file, namespace, relativePath) => {
276
+ //relativePath = relativePath.replace(/\\/g, '/');
277
+ if (file.replace(/\\/g, '/').endsWith('/index.js')) {
278
+ // Import code
279
+ let routeName = 'index' + (++ indexCount);
280
+ // IMPORTANT: we;re taking a step back here so that the parent-child relationship for
281
+ // the directories be involved
282
+ gen.imports[relativePath] = '* as ' + routeName;
283
+ // Definition code
284
+ gen.code.push(`${varName}['${namespace}'] = ${routeName};`);
285
+ // Show
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)`));
287
+ }
288
+ });
289
+ }
290
+ if (!indexCount) {
291
+ cx.logger && cx.logger.log(cx.logger.style.comment(` (none)`));
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Compile params.
297
+ *
298
+ * @param object gen
299
+ * @param object paramsObj
300
+ * @param string varName
301
+ *
302
+ * @return void
303
+ */
304
+ function declareParamsObj(gen, paramsObj, varName = null, indentation = 0) {
305
+ const cx = this || {};
306
+ // ----------------
307
+ // Params compilation
308
+ if (varName) gen.code.push(`const ${varName} = {`);
309
+ _isArray(paramsObj)
310
+ Object.keys(paramsObj).forEach(name => {
311
+ let _name = ` ${' '.repeat(indentation)}${(_isArray(paramsObj) ? '' : (name.includes(' ') ? `'${name}'` : name) + ': ')}`;
312
+ if ([ 'boolean', 'number' ].includes(typeof paramsObj[name])) {
313
+ gen.code.push(`${_name}${paramsObj[name]},`);
314
+ } else if (_isArray(paramsObj[name])) {
315
+ gen.code.push(`${_name}[`);
316
+ declareParamsObj.call(cx, gen, paramsObj[name], null, indentation + 1);
317
+ gen.code.push(` ${' '.repeat(indentation)}],`);
318
+ } else if (_isObject(paramsObj[name])) {
319
+ gen.code.push(`${_name}{`);
320
+ declareParamsObj.call(cx, gen, paramsObj[name], null, indentation + 1);
321
+ gen.code.push(` ${' '.repeat(indentation)}},`);
322
+ } else {
323
+ gen.code.push(`${_name}'${paramsObj[name]}',`);
324
+ }
325
+ });
326
+ if (varName) gen.code.push(`};`);
327
+ }
328
+
329
+ /**
330
+ * Bundle generated file
331
+ *
332
+ * @param object gen
333
+ * @param String outfile
334
+ * @param boolean asModule
335
+ *
336
+ * @return Promise
337
+ */
338
+ async function bundle(gen, outfile, asModule = false) {
339
+ const cx = this || {};
340
+ const compression = !cx.flags.compression ? false : (
341
+ cx.flags.compression === true ? ['gz'] : cx.flags.compression.split(',').map(s => s.trim())
342
+ );
343
+ const moduleFile = `${_beforeLast(outfile, '.')}.esm.js`;
344
+
345
+ // ------------------
346
+ // >> Show waiting...
347
+ if (cx.logger) {
348
+ let waiting = cx.logger.waiting(cx.logger.f`Writing the ES module file: ${moduleFile}`);
349
+ waiting.start();
350
+ jsFile.write(gen, moduleFile, 'ES Module file');
351
+ waiting.stop();
352
+ } else {
353
+ jsFile.write(gen, moduleFile, 'ES Module file');
354
+ }
355
+
356
+ // ----------------
357
+ // >> Webpack config
358
+ const bundlingConfig = {
359
+ entryPoints: [ moduleFile ],
360
+ outfile,
361
+ bundle: true,
362
+ minify: true,
363
+ banner: { js: '/** @webqit/webflo */', },
364
+ footer: { js: '', },
365
+ format: 'esm',
366
+ };
367
+ if (!asModule) {
368
+ // Support top-level await
369
+ // See: https://github.com/evanw/esbuild/issues/253#issuecomment-826147115
370
+ bundlingConfig.banner.js += '(async () => {';
371
+ bundlingConfig.footer.js += '})();';
372
+ }
373
+
374
+ // ----------------
375
+ // The bundling process
376
+ let waiting;
377
+ if (cx.logger) {
378
+ waiting = cx.logger.waiting(`Bundling...`);
379
+ cx.logger.log(cx.logger.style.keyword(`> `) + 'Bundling...');
380
+ waiting.start();
381
+ }
382
+ // Main
383
+ await EsBuild.build(bundlingConfig);
384
+ // Compress...
385
+ let compressedFiles = [], removals = [];
386
+ if (compression) {
387
+ const contents = Fs.readFileSync(bundlingConfig.outfile);
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;
465
+ }
466
+ }
467
+ return successLevel;
468
468
  }