@webqit/webflo 0.11.61-0 → 1.0.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 (118) hide show
  1. package/.gitignore +7 -7
  2. package/LICENSE +20 -20
  3. package/README.md +2079 -2074
  4. package/docker/Dockerfile +42 -42
  5. package/docker/README.md +91 -91
  6. package/docker/package.json +2 -2
  7. package/package.json +80 -81
  8. package/src/{Context.js → AbstractContext.js} +71 -79
  9. package/src/config-pi/deployment/Env.js +68 -68
  10. package/src/config-pi/deployment/Layout.js +63 -63
  11. package/src/config-pi/deployment/Origins.js +139 -139
  12. package/src/config-pi/deployment/Proxy.js +74 -74
  13. package/src/config-pi/deployment/index.js +17 -17
  14. package/src/config-pi/index.js +15 -15
  15. package/src/config-pi/runtime/Client.js +116 -98
  16. package/src/config-pi/runtime/Server.js +125 -125
  17. package/src/config-pi/runtime/client/Worker.js +109 -134
  18. package/src/config-pi/runtime/client/index.js +11 -11
  19. package/src/config-pi/runtime/index.js +17 -17
  20. package/src/config-pi/runtime/server/Headers.js +74 -74
  21. package/src/config-pi/runtime/server/Redirects.js +69 -69
  22. package/src/config-pi/runtime/server/index.js +13 -13
  23. package/src/config-pi/static/Manifest.js +319 -319
  24. package/src/config-pi/static/Ssg.js +49 -49
  25. package/src/config-pi/static/index.js +13 -13
  26. package/src/deployment-pi/index.js +10 -10
  27. package/src/deployment-pi/origins/index.js +216 -216
  28. package/src/index.js +11 -19
  29. package/src/runtime-pi/HttpEvent.js +126 -106
  30. package/src/runtime-pi/HttpUser.js +126 -0
  31. package/src/runtime-pi/MessagingOverBroadcast.js +9 -0
  32. package/src/runtime-pi/MessagingOverChannel.js +85 -0
  33. package/src/runtime-pi/MessagingOverSocket.js +106 -0
  34. package/src/runtime-pi/MultiportMessagingAPI.js +81 -0
  35. package/src/runtime-pi/WebfloCookieStorage.js +27 -0
  36. package/src/runtime-pi/WebfloEventTarget.js +39 -0
  37. package/src/runtime-pi/WebfloMessageEvent.js +58 -0
  38. package/src/runtime-pi/WebfloMessagingAPI.js +69 -0
  39. package/src/runtime-pi/{Router.js → WebfloRouter.js} +99 -130
  40. package/src/runtime-pi/WebfloRuntime.js +52 -0
  41. package/src/runtime-pi/WebfloStorage.js +109 -0
  42. package/src/runtime-pi/client/ClientMessaging.js +5 -0
  43. package/src/runtime-pi/client/Context.js +3 -7
  44. package/src/runtime-pi/client/CookieStorage.js +17 -0
  45. package/src/runtime-pi/client/Router.js +38 -48
  46. package/src/runtime-pi/client/SessionStorage.js +33 -0
  47. package/src/runtime-pi/client/Url.js +156 -205
  48. package/src/runtime-pi/client/WebfloClient.js +544 -0
  49. package/src/runtime-pi/client/WebfloRootClient1.js +179 -0
  50. package/src/runtime-pi/client/WebfloRootClient2.js +109 -0
  51. package/src/runtime-pi/client/WebfloSubClient.js +165 -0
  52. package/src/runtime-pi/client/Workport.js +118 -178
  53. package/src/runtime-pi/client/generate.js +480 -471
  54. package/src/runtime-pi/client/index.js +16 -21
  55. package/src/runtime-pi/client/worker/ClientMessaging.js +5 -0
  56. package/src/runtime-pi/client/worker/Context.js +3 -7
  57. package/src/runtime-pi/client/worker/CookieStorage.js +17 -0
  58. package/src/runtime-pi/client/worker/SessionStorage.js +13 -0
  59. package/src/runtime-pi/client/worker/WebfloWorker.js +294 -0
  60. package/src/runtime-pi/client/worker/Workport.js +17 -85
  61. package/src/runtime-pi/client/worker/index.js +10 -21
  62. package/src/runtime-pi/index.js +6 -13
  63. package/src/runtime-pi/server/ClientMessaging.js +18 -0
  64. package/src/runtime-pi/server/ClientMessagingRegistry.js +57 -0
  65. package/src/runtime-pi/server/Context.js +11 -15
  66. package/src/runtime-pi/server/CookieStorage.js +17 -0
  67. package/src/runtime-pi/server/Router.js +93 -159
  68. package/src/runtime-pi/server/SessionStorage.js +53 -0
  69. package/src/runtime-pi/server/WebfloServer.js +755 -0
  70. package/src/runtime-pi/server/index.js +10 -21
  71. package/src/runtime-pi/util-http.js +322 -86
  72. package/src/runtime-pi/util-url.js +146 -146
  73. package/src/runtime-pi/xURL.js +108 -105
  74. package/src/runtime-pi/xfetch.js +22 -22
  75. package/src/services-pi/cert/http-auth-hook.js +22 -22
  76. package/src/services-pi/cert/http-cleanup-hook.js +22 -22
  77. package/src/services-pi/cert/index.js +79 -79
  78. package/src/services-pi/index.js +8 -8
  79. package/src/static-pi/index.js +10 -10
  80. package/src/webflo.js +30 -30
  81. package/test/index.test.js +26 -26
  82. package/test/site/package.json +9 -9
  83. package/test/site/public/bundle.html +5 -5
  84. package/test/site/public/bundle.html.json +3 -3
  85. package/test/site/public/bundle.js +2 -2
  86. package/test/site/public/bundle.webflo.js +15 -15
  87. package/test/site/public/index.html +29 -29
  88. package/test/site/public/index1.html +34 -34
  89. package/test/site/public/page-2/bundle.html +4 -4
  90. package/test/site/public/page-2/bundle.js +2 -2
  91. package/test/site/public/page-2/index.html +45 -45
  92. package/test/site/public/page-2/main.html +2 -2
  93. package/test/site/public/page-4/subpage/bundle.js +2 -2
  94. package/test/site/public/page-4/subpage/index.html +30 -30
  95. package/test/site/public/sparoots.json +4 -4
  96. package/test/site/public/worker.js +3 -3
  97. package/test/site/server/index.js +15 -15
  98. package/src/runtime-pi/Application.js +0 -29
  99. package/src/runtime-pi/Cookies.js +0 -82
  100. package/src/runtime-pi/Runtime.js +0 -21
  101. package/src/runtime-pi/client/Application.js +0 -100
  102. package/src/runtime-pi/client/Runtime.js +0 -332
  103. package/src/runtime-pi/client/createStorage.js +0 -57
  104. package/src/runtime-pi/client/oohtml/full.js +0 -7
  105. package/src/runtime-pi/client/oohtml/namespacing.js +0 -7
  106. package/src/runtime-pi/client/oohtml/scripting.js +0 -8
  107. package/src/runtime-pi/client/oohtml/templating.js +0 -8
  108. package/src/runtime-pi/client/worker/Application.js +0 -44
  109. package/src/runtime-pi/client/worker/Runtime.js +0 -269
  110. package/src/runtime-pi/server/Application.js +0 -116
  111. package/src/runtime-pi/server/Runtime.js +0 -557
  112. package/src/runtime-pi/xFormData.js +0 -24
  113. package/src/runtime-pi/xHeaders.js +0 -146
  114. package/src/runtime-pi/xRequest.js +0 -46
  115. package/src/runtime-pi/xRequestHeaders.js +0 -109
  116. package/src/runtime-pi/xResponse.js +0 -33
  117. package/src/runtime-pi/xResponseHeaders.js +0 -117
  118. package/src/runtime-pi/xxHttpMessage.js +0 -102
@@ -1,472 +1,481 @@
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 { pattern } from '../util-url.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
- gen.code.push(`self.WebQit = {}`);
156
- gen.code.push(``);
157
- // ------------------
158
- // Bundle
159
- if (workerConfig.cache_only_urls.length) {
160
- // Separate URLs from patterns
161
- let [ urls, patterns ] = workerConfig.cache_only_urls.reduce(([ urls, patterns ], url) => {
162
- let patternInstance = pattern(url, 'http://localhost'),
163
- isPattern = patternInstance.isPattern();
164
- if (isPattern && (patternInstance.pattern.pattern.hostname !== 'localhost' || patternInstance.pattern.pattern.port)) {
165
- throw new Error(`Pattern URLs must have no origin part. Recieved "${url}".`);
166
- }
167
- return isPattern ? [ urls, patterns.concat(patternInstance) ] : [ urls.concat(url), patterns ];
168
- }, [ [], [] ]);
169
- // Resolve patterns
170
- if (patterns.length) {
171
- // List all files
172
- let scan = dir => Fs.readdirSync(dir).reduce((result, f) => {
173
- let resource = Path.join(dir, f);
174
- return result.concat(Fs.statSync(resource).isDirectory() ? scan(resource) : '/' + Path.relative(dirPublic, resource));
175
- }, []);
176
- let files = scan(dirPublic);
177
- // Resolve patterns from files
178
- workerConfig.cache_only_urls = patterns.reduce((all, pattern) => {
179
- let matchedFiles = files.filter(file => pattern.test(file, 'http://localhost'));
180
- if (matchedFiles.length) return all.concat(matchedFiles);
181
- throw new Error(`The pattern "${pattern.pattern.pattern.pathname}" didn't match any files.`);
182
- }, urls);
183
- }
184
- }
185
- declareStart.call(cx, gen, dirWorker, dirPublic, workerConfig, workerRouting);
186
- await bundle.call(cx, gen, Path.join(dirPublic, workerroot, clientConfig.worker_filename));
187
- // ------------------
188
- // Recurse
189
- workerGraphCallbak && workerGraphCallbak(workerroot, subworkerroots);
190
- if (cx.flags.recursive) {
191
- while (subworkerroots.length) {
192
- await generateWorker(subworkerroots.shift());
193
- }
194
- }
195
- };
196
- // -----------
197
- // Generate now...
198
- let sparootsFile = Path.join(dirPublic, 'sparoots.json');
199
- if (clientConfig.spa_routing !== false) {
200
- const sparoots = [];
201
- await generateClient('/', root => sparoots.push(root));
202
- Fs.writeFileSync(sparootsFile, JSON.stringify(sparoots, null, 4));
203
- } else {
204
- await generateClient();
205
- Fs.existsSync(sparootsFile) && Fs.unlinkSync(sparootsFile);
206
- }
207
- if (clientConfig.service_worker_support) {
208
- await generateWorker('/');
209
- }
210
- }
211
-
212
- /**
213
- * Compile routes.
214
- *
215
- * @param object gen
216
- * @param string routesDir
217
- * @param string targetPublic
218
- * @param object paramsObj
219
- * @param object routing
220
- *
221
- * @return Object
222
- */
223
- function declareStart(gen, routesDir, targetDir, paramsObj, routing) {
224
- const cx = this || {};
225
- // ------------------
226
- // >> Routes mapping
227
- gen.code.push(`// >> Routes`);
228
- declareRoutesObj.call(cx, gen, routesDir, targetDir, 'layout', routing);
229
- gen.code.push(``);
230
- // ------------------
231
- // >> Params
232
- gen.code.push(`// >> Params`);
233
- declareParamsObj.call(cx, gen, { ...paramsObj, routing }, 'params');
234
- gen.code.push(``);
235
- // ------------------
236
- // >> Startup
237
- gen.code.push(`// >> Startup`);
238
- gen.code.push(`WebQit.app = await start.call({ layout, params })`);
239
- }
240
-
241
- /**
242
- * Compile routes.
243
- *
244
- * @param object gen
245
- * @param string routesDir
246
- * @param string targetDir
247
- * @param string varName
248
- * @param object routing
249
- *
250
- * @return void
251
- */
252
- function declareRoutesObj(gen, routesDir, targetDir, varName, routing) {
253
- const cx = this || {};
254
- let _routesDir = Path.join(routesDir, routing.root),
255
- _targetDir = Path.join(targetDir, routing.root);
256
- cx.logger && cx.logger.log(cx.logger.style.keyword(`> `) + `Declaring routes...`);
257
- // ----------------
258
- // Directory walker
259
- const walk = (dir, callback) => {
260
- Fs.readdirSync(dir).forEach(f => {
261
- let resource = Path.join(dir, f);
262
- let _namespace = '/' + Path.relative(routesDir, resource).replace(/\\/g, '/');
263
- let namespace = _beforeLast(_namespace, '/index.js') || '/';
264
- if (Fs.statSync(resource).isDirectory()) {
265
- if (routing.subroots.includes(namespace)) return;
266
- walk(resource, callback);
267
- } else {
268
- let relativePath = Path.relative(_targetDir, resource).replace(/\\/g, '/');
269
- callback(resource, namespace, relativePath);
270
- }
271
- });
272
- };
273
- // ----------------
274
- // >> Routes mapping
275
- gen.code.push(`const ${varName} = {};`);
276
- let indexCount = 0;
277
- if (Fs.existsSync(_routesDir)) {
278
- walk(_routesDir, (file, namespace, relativePath) => {
279
- if (relativePath.endsWith('/index.js')) {
280
- // Import code
281
- let routeName = 'index' + (++ indexCount);
282
- // IMPORTANT: we;re taking a step back here so that the parent-child relationship for
283
- // the directories be involved
284
- gen.imports[relativePath] = '* as ' + routeName;
285
- // Definition code
286
- gen.code.push(`${varName}['${namespace}'] = ${routeName};`);
287
- // Show
288
- 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)`));
289
- }
290
- });
291
- }
292
- if (!indexCount) {
293
- cx.logger && cx.logger.log(cx.logger.style.comment(` (none)`));
294
- }
295
- }
296
-
297
- /**
298
- * Compile params.
299
- *
300
- * @param object gen
301
- * @param object paramsObj
302
- * @param string varName
303
- *
304
- * @return void
305
- */
306
- function declareParamsObj(gen, paramsObj, varName = null, indentation = 0) {
307
- const cx = this || {};
308
- // ----------------
309
- // Params compilation
310
- if (varName) gen.code.push(`const ${varName} = {`);
311
- _isArray(paramsObj)
312
- Object.keys(paramsObj).forEach(name => {
313
- let _name = ` ${' '.repeat(indentation)}${(_isArray(paramsObj) ? '' : (name.includes(' ') ? `'${name}'` : name) + ': ')}`;
314
- if ([ 'boolean', 'number' ].includes(typeof paramsObj[name])) {
315
- gen.code.push(`${_name}${paramsObj[name]},`);
316
- } else if (_isArray(paramsObj[name])) {
317
- gen.code.push(`${_name}[`);
318
- declareParamsObj.call(cx, gen, paramsObj[name], null, indentation + 1);
319
- gen.code.push(` ${' '.repeat(indentation)}],`);
320
- } else if (_isObject(paramsObj[name])) {
321
- gen.code.push(`${_name}{`);
322
- declareParamsObj.call(cx, gen, paramsObj[name], null, indentation + 1);
323
- gen.code.push(` ${' '.repeat(indentation)}},`);
324
- } else {
325
- gen.code.push(`${_name}'${paramsObj[name]}',`);
326
- }
327
- });
328
- if (varName) gen.code.push(`};`);
329
- }
330
-
331
- /**
332
- * Bundle generated file
333
- *
334
- * @param object gen
335
- * @param String outfile
336
- * @param boolean asModule
337
- *
338
- * @return Promise
339
- */
340
- async function bundle(gen, outfile, asModule = false) {
341
- const cx = this || {};
342
- const compression = !cx.flags.compression ? false : (
343
- cx.flags.compression === true ? ['gz'] : cx.flags.compression.split(',').map(s => s.trim())
344
- );
345
- const moduleFile = `${_beforeLast(outfile, '.')}.esm.js`;
346
-
347
- // ------------------
348
- // >> Show waiting...
349
- if (cx.logger) {
350
- let waiting = cx.logger.waiting(cx.logger.f`Writing the ES module file: ${moduleFile}`);
351
- waiting.start();
352
- jsFile.write(gen, moduleFile, 'ES Module file');
353
- waiting.stop();
354
- } else {
355
- jsFile.write(gen, moduleFile, 'ES Module file');
356
- }
357
-
358
- // ----------------
359
- // >> Webpack config
360
- const bundlingConfig = {
361
- entryPoints: [ moduleFile ],
362
- outfile,
363
- bundle: true,
364
- minify: true,
365
- banner: { js: '/** @webqit/webflo */', },
366
- footer: { js: '', },
367
- format: 'esm',
368
- };
369
- if (!asModule) {
370
- // Support top-level await
371
- // See: https://github.com/evanw/esbuild/issues/253#issuecomment-826147115
372
- bundlingConfig.banner.js += '(async () => {';
373
- bundlingConfig.footer.js += '})();';
374
- }
375
-
376
- // ----------------
377
- // The bundling process
378
- let waiting;
379
- if (cx.logger) {
380
- waiting = cx.logger.waiting(`Bundling...`);
381
- cx.logger.log(cx.logger.style.keyword(`> `) + 'Bundling...');
382
- waiting.start();
383
- }
384
- // Main
385
- await EsBuild.build(bundlingConfig);
386
- // Compress...
387
- let compressedFiles = [], removals = [];
388
- if (compression) {
389
- const contents = Fs.readFileSync(bundlingConfig.outfile);
390
- if (compression.includes('gz')) {
391
- const gzip = gzipSync(contents, {});
392
- Fs.writeFileSync(bundlingConfig.outfile + '.gz', gzip);
393
- compressedFiles.push(bundlingConfig.outfile + '.gz');
394
- } else {
395
- removals.push(bundlingConfig.outfile + '.gz');
396
- }
397
- if (compression.includes('br')) {
398
- const brotli = brotliCompressSync(contents, {});
399
- Fs.writeFileSync(bundlingConfig.outfile + '.br', brotli);
400
- compressedFiles.push(bundlingConfig.outfile + '.br');
401
- } else {
402
- removals.push(bundlingConfig.outfile + '.br');
403
- }
404
- }
405
- // Remove moduleFile build
406
- Fs.unlinkSync(bundlingConfig.entryPoints[0]);
407
- removals.forEach(file => Fs.existsSync(file) && Fs.unlinkSync(file));
408
- if (waiting) waiting.stop();
409
- // ----------------
410
- // Stats
411
- if (cx.logger) {
412
- [bundlingConfig.outfile].concat(compressedFiles).forEach(file => {
413
- let ext = '.' + _afterLast(file, '.');
414
- cx.logger.info(cx.logger.style.comment(` [${ext}]: `) + cx.logger.style.url(file) + cx.logger.style.comment(` (${Fs.statSync(file).size / 1024} KB)`));
415
- });
416
- cx.logger.log('');
417
- }
418
- }
419
-
420
- /**
421
- * Handles auto-embeds
422
- *
423
- * @param String targetDocumentFile
424
- * @param Array embedList
425
- * @param Array unembedList
426
- *
427
- * @return Void
428
- */
429
- function handleEmbeds(targetDocumentFile, embedList, unembedList) {
430
- let targetDocument, successLevel = 0;
431
- if (Fs.existsSync(targetDocumentFile) && (targetDocument = Fs.readFileSync(targetDocumentFile).toString()) && targetDocument.trim().startsWith('<!DOCTYPE html')) {
432
- successLevel = 1;
433
- let dom = new Jsdom.JSDOM(targetDocument), by = 'webflo', touched;
434
- let embed = (src, before) => {
435
- src = src.replace(/\\/g, '/');
436
- let embedded = dom.window.document.querySelector(`script[src="${src}"]`);
437
- if (!embedded) {
438
- embedded = dom.window.document.createElement('script');
439
- embedded.setAttribute('type', 'module');
440
- embedded.setAttribute('src', src);
441
- embedded.setAttribute('by', by);
442
- if (before) {
443
- before.before(embedded, `\n\t\t`);
444
- } else {
445
- dom.window.document.head.appendChild(embedded);
446
- }
447
- touched = true;
448
- }
449
- return embedded;
450
- };
451
- let unembed = src => {
452
- src = Path.join('/', src);
453
- src = src.replace(/\\/g, '/');
454
- let embedded = dom.window.document.querySelector(`script[src="${src}"][by="${by}"]`);
455
- if (embedded) {
456
- embedded.remove();
457
- touched = true;
458
- }
459
- };
460
- embedList.reverse().reduce((prev, src) => {
461
- return embed(src, prev);
462
- }, dom.window.document.querySelector(`script[src]`) || dom.window.document.querySelector(`script`));
463
- unembedList.forEach(src => {
464
- unembed(src);
465
- });
466
- if (touched) {
467
- Fs.writeFileSync(targetDocumentFile, dom.serialize());
468
- successLevel = 2;
469
- }
470
- }
471
- 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 { pattern } from '../util-url.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.service_worker?.filename && !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
+ if (clientConfig.bundle_public_env || workerConfig.bundle_public_env) {
44
+ if (!cx.config.deployment?.Env) {
45
+ throw new Error(`The Layout configurator "config.deployment.Env" is required in context to bundle public env.`);
46
+ }
47
+ const envConfig = await (new cx.config.deployment.Env(cx)).read();
48
+ for (const key in envConfig.entries) {
49
+ if (!key.includes('PUBLIC_') && !key.includes('_PUBLIC')) continue;
50
+ if (clientConfig.bundle_public_env) {
51
+ if (!clientConfig.env) { clientConfig.env = {}; }
52
+ clientConfig.env[key] = envConfig.entries[key];
53
+ }
54
+ if (workerConfig.bundle_public_env) {
55
+ if (!workerConfig.env) { workerConfig.env = {}; }
56
+ workerConfig.env[key] = envConfig.entries[key];
57
+ }
58
+ }
59
+ }
60
+ // -----------
61
+ // Scan Subdocuments
62
+ const scanSubroots = (sparoot, rootFileName) => {
63
+ let dir = Path.join(dirPublic, sparoot), passes = 0;
64
+ return [ Fs.readdirSync(dir).reduce((sparoots, f) => {
65
+ let resource = Path.join(dir, f);
66
+ if (Fs.statSync(resource).isDirectory()) {
67
+ let subsparoot = Path.join(sparoot, f);
68
+ if (Fs.existsSync(Path.join(resource, rootFileName))) {
69
+ return sparoots.concat(subsparoot);
70
+ }
71
+ passes ++;
72
+ return sparoots.concat(scanSubroots(subsparoot, rootFileName)[ 0 ]);
73
+ }
74
+ return sparoots;
75
+ }, []), passes ];
76
+ };
77
+ // -----------
78
+ // Generate client build
79
+ const generateClient = async function(sparoot, spaGraphCallback = null) {
80
+ let [ subsparoots, targets ] = (sparoot && scanSubroots(sparoot, 'index.html')) || [ [], false ];
81
+ if (!sparoot) sparoot = '/';
82
+ let spaRouting = { root: sparoot, subroots: subsparoots, targets };
83
+ let codeSplitting = !!(sparoot !== '/' || subsparoots.length);
84
+ let outfileMain = Path.join(sparoot, clientConfig.bundle_filename),
85
+ outfileWebflo = _beforeLast(clientConfig.bundle_filename, '.js') + '.webflo.js';
86
+ let gen = { imports: {}, code: [], };
87
+ // ------------------
88
+ const initWebflo = gen => {
89
+ gen.imports[`${dirSelf}/index.js`] = `* as Webflo`;
90
+ gen.code.push(``);
91
+ gen.code.push(`if (!self.webqit) {self.webqit = {};}`);
92
+ gen.code.push(`webqit.Webflo = Webflo`);
93
+ return gen;
94
+ };
95
+ // ------------------
96
+ if (!codeSplitting) {
97
+ initWebflo(gen);
98
+ } else if (sparoot === '/') {
99
+ if (cx.logger) {
100
+ cx.logger.log(cx.logger.style.keyword(`-----------------`));
101
+ cx.logger.log(`Base Build`);
102
+ cx.logger.log(cx.logger.style.keyword(`-----------------`));
103
+ }
104
+ let gen1 = initWebflo({ imports: {}, code: [], });
105
+ await bundle.call(cx, gen1, Path.join(dirPublic, outfileWebflo), true/* asModule */);
106
+ }
107
+ // ------------------
108
+ if (cx.logger) {
109
+ cx.logger.log(cx.logger.style.keyword(`-----------------`));
110
+ cx.logger.log(`Client Build ` + cx.logger.style.comment(`(sparoot:${sparoot}; is-split:${codeSplitting})`));
111
+ cx.logger.log(cx.logger.style.keyword(`-----------------`));
112
+ }
113
+ gen.code.push(`const { start } = webqit.Webflo`);
114
+ // ------------------
115
+ // Bundle
116
+ declareStart.call(cx, gen, dirClient, dirPublic, clientConfig, spaRouting);
117
+ await bundle.call(cx, gen, Path.join(dirPublic, outfileMain), true/* asModule */);
118
+ // ------------------
119
+ // Embed/unembed
120
+ let targetDocumentFile = Path.join(dirPublic, sparoot, 'index.html'),
121
+ outfileWebfloPublic = Path.join(clientConfig.public_base_url, outfileWebflo),
122
+ outfileMainPublic = Path.join(clientConfig.public_base_url, outfileMain),
123
+ embedList = [],
124
+ unembedList = [];
125
+ if (cx.flags['auto-embed']) {
126
+ if (codeSplitting) {
127
+ embedList.push(outfileWebfloPublic);
128
+ } else {
129
+ unembedList.push(outfileWebfloPublic);
130
+ }
131
+ embedList.push(outfileMainPublic);
132
+ } else {
133
+ unembedList.push(outfileWebfloPublic, outfileMainPublic);
134
+ }
135
+ handleEmbeds(targetDocumentFile, embedList, unembedList);
136
+ // ------------------
137
+ // Recurse
138
+ spaGraphCallback && spaGraphCallback(sparoot, subsparoots);
139
+ if (cx.flags.recursive) {
140
+ while (subsparoots.length) {
141
+ await generateClient(subsparoots.shift(), spaGraphCallback);
142
+ }
143
+ }
144
+ };
145
+ // -----------
146
+ // Generate worker build
147
+ const generateWorker = async function(workerroot, workerGraphCallback = null) {
148
+ let [ subworkerroots, targets ] = workerroot && scanSubroots(workerroot, 'workerroot') || [ [], false ];
149
+ if (!workerroot) workerroot = '/';
150
+ let workerRouting = { root: workerroot, subroots: subworkerroots, targets };
151
+ let gen = { imports: {}, code: [], };
152
+ if (cx.logger) {
153
+ cx.logger.log(cx.logger.style.comment(`-----------------`));
154
+ cx.logger.log(`Worker Build - workerroot:${workerroot}`);
155
+ cx.logger.log(cx.logger.style.comment(`-----------------`));
156
+ }
157
+ // ------------------
158
+ // >> Modules import
159
+ gen.imports[`${dirSelf}/worker/index.js`] = `{ start }`;
160
+ gen.code.push(``);
161
+ gen.code.push(`self.webqit = {}`);
162
+ gen.code.push(``);
163
+ // ------------------
164
+ // Bundle
165
+ for (const strategy of [ 'cache_first_urls', 'cache_only_urls' ]) {
166
+ if (workerConfig[strategy].length) {
167
+ // Separate URLs from patterns
168
+ let [ urls, patterns ] = workerConfig[strategy].reduce(([ urls, patterns ], url) => {
169
+ let patternInstance = pattern(url, 'http://localhost'),
170
+ isPattern = patternInstance.isPattern();
171
+ if (isPattern && (patternInstance.pattern.pattern.hostname !== 'localhost' || patternInstance.pattern.pattern.port)) {
172
+ throw new Error(`Pattern URLs must have no origin part. Recieved "${url}".`);
173
+ }
174
+ return isPattern ? [ urls, patterns.concat(patternInstance) ] : [ urls.concat(url), patterns ];
175
+ }, [ [], [] ]);
176
+ // Resolve patterns
177
+ if (patterns.length) {
178
+ // List all files
179
+ let scan = dir => Fs.readdirSync(dir).reduce((result, f) => {
180
+ let resource = Path.join(dir, f);
181
+ if (f.startsWith('.')) return result;
182
+ return result.concat(Fs.statSync(resource).isDirectory() ? scan(resource) : '/' + Path.relative(dirPublic, resource));
183
+ }, []);
184
+ let files = scan(dirPublic);
185
+ // Resolve patterns from files
186
+ workerConfig[strategy] = patterns.reduce((all, pattern) => {
187
+ let matchedFiles = files.filter(file => pattern.test(file, 'http://localhost'));
188
+ if (matchedFiles.length) return all.concat(matchedFiles);
189
+ throw new Error(`The pattern "${pattern.pattern.pattern.pathname}" didn't match any files.`);
190
+ }, urls);
191
+ }
192
+ }
193
+ }
194
+ declareStart.call(cx, gen, dirWorker, dirPublic, workerConfig, workerRouting);
195
+ await bundle.call(cx, gen, Path.join(dirPublic, workerroot, clientConfig.service_worker.filename));
196
+ // ------------------
197
+ // Recurse
198
+ workerGraphCallback && workerGraphCallback(workerroot, subworkerroots);
199
+ if (cx.flags.recursive) {
200
+ while (subworkerroots.length) {
201
+ await generateWorker(subworkerroots.shift());
202
+ }
203
+ }
204
+ };
205
+ // -----------
206
+ // Generate now...
207
+ let sparootsFile = Path.join(dirPublic, 'sparoots.json');
208
+ if (clientConfig.spa_routing !== false) {
209
+ const sparoots = [];
210
+ await generateClient('/', root => sparoots.push(root));
211
+ Fs.writeFileSync(sparootsFile, JSON.stringify(sparoots, null, 4));
212
+ } else {
213
+ await generateClient();
214
+ Fs.existsSync(sparootsFile) && Fs.unlinkSync(sparootsFile);
215
+ }
216
+ if (clientConfig.service_worker.filename) {
217
+ await generateWorker('/');
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Compile routes.
223
+ *
224
+ * @param object gen
225
+ * @param string routesDir
226
+ * @param string targetPublic
227
+ * @param object paramsObj
228
+ * @param object routing
229
+ *
230
+ * @return Object
231
+ */
232
+ function declareStart(gen, routesDir, targetDir, paramsObj, routing) {
233
+ const cx = this || {};
234
+ // ------------------
235
+ // >> Routes mapping
236
+ gen.code.push(`// >> Routes`);
237
+ declareRoutesObj.call(cx, gen, routesDir, targetDir, 'layout', routing);
238
+ gen.code.push(``);
239
+ // ------------------
240
+ // >> Params
241
+ gen.code.push(`// >> Params`);
242
+ declareParamsObj.call(cx, gen, { ...paramsObj, routing }, 'params');
243
+ gen.code.push(``);
244
+ // ------------------
245
+ // >> Startup
246
+ gen.code.push(`// >> Startup`);
247
+ gen.code.push(`webqit.app = await start.call({ layout, params })`);
248
+ }
249
+
250
+ /**
251
+ * Compile routes.
252
+ *
253
+ * @param object gen
254
+ * @param string routesDir
255
+ * @param string targetDir
256
+ * @param string varName
257
+ * @param object routing
258
+ *
259
+ * @return void
260
+ */
261
+ function declareRoutesObj(gen, routesDir, targetDir, varName, routing) {
262
+ const cx = this || {};
263
+ let _routesDir = Path.join(routesDir, routing.root),
264
+ _targetDir = Path.join(targetDir, routing.root);
265
+ cx.logger && cx.logger.log(cx.logger.style.keyword(`> `) + `Declaring routes...`);
266
+ // ----------------
267
+ // Directory walker
268
+ const walk = (dir, callback) => {
269
+ Fs.readdirSync(dir).forEach(f => {
270
+ let resource = Path.join(dir, f);
271
+ let _namespace = '/' + Path.relative(routesDir, resource).replace(/\\/g, '/');
272
+ let namespace = _beforeLast(_namespace, '/index.js') || '/';
273
+ if (Fs.statSync(resource).isDirectory()) {
274
+ if (routing.subroots.includes(namespace)) return;
275
+ walk(resource, callback);
276
+ } else {
277
+ let relativePath = Path.relative(_targetDir, resource).replace(/\\/g, '/');
278
+ callback(resource, namespace, relativePath);
279
+ }
280
+ });
281
+ };
282
+ // ----------------
283
+ // >> Routes mapping
284
+ gen.code.push(`const ${varName} = {};`);
285
+ let indexCount = 0;
286
+ if (Fs.existsSync(_routesDir)) {
287
+ walk(_routesDir, (file, namespace, relativePath) => {
288
+ if (relativePath.endsWith('/index.js')) {
289
+ // Import code
290
+ let routeName = 'index' + (++ indexCount);
291
+ // IMPORTANT: we;re taking a step back here so that the parent-child relationship for
292
+ // the directories be involved
293
+ gen.imports[relativePath] = '* as ' + routeName;
294
+ // Definition code
295
+ gen.code.push(`${varName}['${namespace}'] = ${routeName};`);
296
+ // Show
297
+ 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)`));
298
+ }
299
+ });
300
+ }
301
+ if (!indexCount) {
302
+ cx.logger && cx.logger.log(cx.logger.style.comment(` (none)`));
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Compile params.
308
+ *
309
+ * @param object gen
310
+ * @param object paramsObj
311
+ * @param string varName
312
+ *
313
+ * @return void
314
+ */
315
+ function declareParamsObj(gen, paramsObj, varName = null, indentation = 0) {
316
+ const cx = this || {};
317
+ // ----------------
318
+ // Params compilation
319
+ if (varName) gen.code.push(`const ${varName} = {`);
320
+ _isArray(paramsObj)
321
+ Object.keys(paramsObj).forEach(name => {
322
+ let _name = ` ${' '.repeat(indentation)}${(_isArray(paramsObj) ? '' : (name.includes(' ') ? `'${name}'` : name) + ': ')}`;
323
+ if ([ 'boolean', 'number' ].includes(typeof paramsObj[name])) {
324
+ gen.code.push(`${_name}${paramsObj[name]},`);
325
+ } else if (_isArray(paramsObj[name])) {
326
+ gen.code.push(`${_name}[`);
327
+ declareParamsObj.call(cx, gen, paramsObj[name], null, indentation + 1);
328
+ gen.code.push(` ${' '.repeat(indentation)}],`);
329
+ } else if (_isObject(paramsObj[name])) {
330
+ gen.code.push(`${_name}{`);
331
+ declareParamsObj.call(cx, gen, paramsObj[name], null, indentation + 1);
332
+ gen.code.push(` ${' '.repeat(indentation)}},`);
333
+ } else {
334
+ gen.code.push(`${_name}'${paramsObj[name]}',`);
335
+ }
336
+ });
337
+ if (varName) gen.code.push(`};`);
338
+ }
339
+
340
+ /**
341
+ * Bundle generated file
342
+ *
343
+ * @param object gen
344
+ * @param String outfile
345
+ * @param boolean asModule
346
+ *
347
+ * @return Promise
348
+ */
349
+ async function bundle(gen, outfile, asModule = false) {
350
+ const cx = this || {};
351
+ const compression = !cx.flags.compression ? false : (
352
+ cx.flags.compression === true ? ['gz'] : cx.flags.compression.split(',').map(s => s.trim())
353
+ );
354
+ const moduleFile = `${_beforeLast(outfile, '.')}.esm.js`;
355
+
356
+ // ------------------
357
+ // >> Show waiting...
358
+ if (cx.logger) {
359
+ let waiting = cx.logger.waiting(cx.logger.f`Writing the ES module file: ${moduleFile}`);
360
+ waiting.start();
361
+ jsFile.write(gen, moduleFile, 'ES Module file');
362
+ waiting.stop();
363
+ } else {
364
+ jsFile.write(gen, moduleFile, 'ES Module file');
365
+ }
366
+
367
+ // ----------------
368
+ // >> Webpack config
369
+ const bundlingConfig = {
370
+ entryPoints: [ moduleFile ],
371
+ outfile,
372
+ bundle: true,
373
+ minify: true,
374
+ banner: { js: '/** @webqit/webflo */', },
375
+ footer: { js: '', },
376
+ format: 'esm',
377
+ };
378
+ if (!asModule) {
379
+ // Support top-level await
380
+ // See: https://github.com/evanw/esbuild/issues/253#issuecomment-826147115
381
+ bundlingConfig.banner.js += '(async () => {';
382
+ bundlingConfig.footer.js += '})();';
383
+ }
384
+
385
+ // ----------------
386
+ // The bundling process
387
+ let waiting;
388
+ if (cx.logger) {
389
+ waiting = cx.logger.waiting(`Bundling...`);
390
+ cx.logger.log(cx.logger.style.keyword(`> `) + 'Bundling...');
391
+ waiting.start();
392
+ }
393
+ // Main
394
+ await EsBuild.build(bundlingConfig);
395
+ // Compress...
396
+ let compressedFiles = [], removals = [];
397
+ if (compression) {
398
+ const contents = Fs.readFileSync(bundlingConfig.outfile);
399
+ if (compression.includes('gz')) {
400
+ const gzip = gzipSync(contents, {});
401
+ Fs.writeFileSync(bundlingConfig.outfile + '.gz', gzip);
402
+ compressedFiles.push(bundlingConfig.outfile + '.gz');
403
+ } else {
404
+ removals.push(bundlingConfig.outfile + '.gz');
405
+ }
406
+ if (compression.includes('br')) {
407
+ const brotli = brotliCompressSync(contents, {});
408
+ Fs.writeFileSync(bundlingConfig.outfile + '.br', brotli);
409
+ compressedFiles.push(bundlingConfig.outfile + '.br');
410
+ } else {
411
+ removals.push(bundlingConfig.outfile + '.br');
412
+ }
413
+ }
414
+ // Remove moduleFile build
415
+ Fs.unlinkSync(bundlingConfig.entryPoints[0]);
416
+ removals.forEach(file => Fs.existsSync(file) && Fs.unlinkSync(file));
417
+ if (waiting) waiting.stop();
418
+ // ----------------
419
+ // Stats
420
+ if (cx.logger) {
421
+ [bundlingConfig.outfile].concat(compressedFiles).forEach(file => {
422
+ let ext = '.' + _afterLast(file, '.');
423
+ cx.logger.info(cx.logger.style.comment(` [${ext}]: `) + cx.logger.style.url(file) + cx.logger.style.comment(` (${Fs.statSync(file).size / 1024} KB)`));
424
+ });
425
+ cx.logger.log('');
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Handles auto-embeds
431
+ *
432
+ * @param String targetDocumentFile
433
+ * @param Array embedList
434
+ * @param Array unembedList
435
+ *
436
+ * @return Void
437
+ */
438
+ function handleEmbeds(targetDocumentFile, embedList, unembedList) {
439
+ let targetDocument, successLevel = 0;
440
+ if (Fs.existsSync(targetDocumentFile) && (targetDocument = Fs.readFileSync(targetDocumentFile).toString()) && targetDocument.trim().startsWith('<!DOCTYPE html')) {
441
+ successLevel = 1;
442
+ let dom = new Jsdom.JSDOM(targetDocument), by = 'webflo', touched;
443
+ let embed = (src, after) => {
444
+ src = src.replace(/\\/g, '/');
445
+ let embedded = dom.window.document.querySelector(`script[src="${src}"]`);
446
+ if (!embedded) {
447
+ embedded = dom.window.document.createElement('script');
448
+ embedded.setAttribute('type', 'module');
449
+ embedded.setAttribute('src', src);
450
+ embedded.setAttribute('by', by);
451
+ if (after) {
452
+ after.after(embedded, `\n\t\t`);
453
+ } else {
454
+ dom.window.document.head.appendChild(embedded);
455
+ }
456
+ touched = true;
457
+ }
458
+ return embedded;
459
+ };
460
+ let unembed = src => {
461
+ src = Path.join('/', src);
462
+ src = src.replace(/\\/g, '/');
463
+ let embedded = dom.window.document.querySelector(`script[src="${src}"][by="${by}"]`);
464
+ if (embedded) {
465
+ embedded.remove();
466
+ touched = true;
467
+ }
468
+ };
469
+ embedList.reduce((prev, src) => {
470
+ return embed(src, prev);
471
+ }, [ ...dom.window.document.head.querySelectorAll(`script[src]`) ].pop() || dom.window.document.querySelector(`script`));
472
+ unembedList.forEach(src => {
473
+ unembed(src);
474
+ });
475
+ if (touched) {
476
+ Fs.writeFileSync(targetDocumentFile, dom.serialize());
477
+ successLevel = 2;
478
+ }
479
+ }
480
+ return successLevel;
472
481
  }