@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.
- package/README.md +750 -3
- package/package.json +3 -3
- package/src/Context.js +8 -4
- package/src/config-pi/deployment/Env.js +2 -2
- package/src/config-pi/deployment/Layout.js +2 -2
- package/src/config-pi/deployment/Origins.js +2 -2
- package/src/config-pi/deployment/Virtualization.js +2 -2
- package/src/config-pi/runtime/Client.js +45 -14
- package/src/config-pi/runtime/Server.js +26 -10
- package/src/config-pi/runtime/client/Worker.js +2 -2
- package/src/config-pi/runtime/server/Headers.js +2 -2
- package/src/config-pi/runtime/server/Redirects.js +2 -2
- package/src/config-pi/static/Manifest.js +2 -2
- package/src/config-pi/static/Ssg.js +2 -2
- package/src/runtime-pi/client/Runtime.js +28 -23
- package/src/runtime-pi/client/RuntimeClient.js +28 -15
- package/src/runtime-pi/client/generate.js +251 -77
- package/src/runtime-pi/client/{generate.oohtml.js → oohtml/full.js} +0 -0
- package/src/runtime-pi/client/oohtml/namespacing.js +7 -0
- package/src/runtime-pi/client/oohtml/scripting.js +8 -0
- package/src/runtime-pi/client/oohtml/templating.js +8 -0
- package/src/runtime-pi/client/worker/Worker.js +21 -23
- package/src/runtime-pi/server/Router.js +2 -2
- package/src/runtime-pi/server/Runtime.js +8 -3
- package/src/runtime-pi/server/RuntimeClient.js +21 -11
- package/src/webflo.js +7 -9
- package/test/site/public/bundle.html +3 -0
- package/test/site/public/bundle.html.json +3 -0
- package/test/site/public/bundle.js +2 -0
- package/test/site/public/bundle.js.gz +0 -0
- package/test/site/public/bundle.webflo.js +15 -0
- package/test/site/public/bundle.webflo.js.gz +0 -0
- package/test/site/public/index.html +30 -0
- package/test/site/public/page-2/bundle.html +5 -0
- package/test/site/public/page-2/bundle.html.json +1 -0
- package/test/site/public/page-2/bundle.js +2 -0
- package/test/site/public/page-2/bundle.js.gz +0 -0
- package/test/site/public/page-2/index.html +47 -0
- package/test/site/public/page-2/logo-130x130.png +0 -0
- package/test/site/public/page-2/main.html +3 -0
- package/test/site/public/page-4/subpage/bundle.html +0 -0
- package/test/site/public/page-4/subpage/bundle.html.json +1 -0
- package/test/site/public/page-4/subpage/bundle.js +2 -0
- package/test/site/public/page-4/subpage/bundle.js.gz +0 -0
- package/test/site/public/page-4/subpage/index.html +31 -0
- package/test/site/public/worker.js +3 -0
- package/test/site/public/worker.js.gz +0 -0
- package/test/site/server/index.js +8 -0
- package/src/Cli.js +0 -131
- 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
|
|
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
|
|
12
|
+
import { jsFile } from '@webqit/backpack/src/dotfile/index.js';
|
|
11
13
|
import { gzipSync, brotliCompressSync } from 'zlib';
|
|
12
|
-
import
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
let
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
56
|
-
* @param string
|
|
57
|
-
* @param
|
|
58
|
-
* @param
|
|
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
|
|
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
|
|
97
|
-
* @param string
|
|
98
|
-
* @param string
|
|
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
|
-
|
|
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
|
|
114
|
-
callback(resource,
|
|
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 (
|
|
123
|
-
|
|
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[
|
|
248
|
+
gen.imports[relativePath] = '* as ' + routeName;
|
|
133
249
|
// Definition code
|
|
134
|
-
|
|
135
|
-
gen.code.push(`${varName}['${routePath || '/'}'] = ${routeName};`);
|
|
250
|
+
gen.code.push(`${varName}['${namespace}'] = ${routeName};`);
|
|
136
251
|
// Show
|
|
137
|
-
cx.logger && cx.logger.log(
|
|
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(
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
}
|
|
File without changes
|
|
@@ -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 && !
|
|
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',
|
|
75
|
+
self.addEventListener('fetch', event => {
|
|
76
76
|
// URL schemes that might arrive here but not supported; e.g.: chrome-extension://
|
|
77
|
-
if (!
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
let mime = Mime.lookup(ext);
|
|
92
92
|
response = new httpEvent.Response(data, { headers: {
|
|
93
|
-
contentType:
|
|
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
|
-
|
|
249
|
-
|
|
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
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
});
|