@webqit/webflo 0.10.4 → 0.11.1
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 +1490 -52
- package/bundle.html.json +1665 -0
- package/package.json +2 -2
- package/src/Context.js +1 -1
- package/src/config-pi/runtime/Client.js +37 -9
- package/src/config-pi/runtime/Server.js +24 -8
- package/src/config-pi/runtime/client/Worker.js +30 -12
- package/src/runtime-pi/Router.js +1 -1
- package/src/runtime-pi/client/Runtime.js +116 -62
- package/src/runtime-pi/client/RuntimeClient.js +28 -43
- package/src/runtime-pi/client/Workport.js +163 -0
- package/src/runtime-pi/client/generate.js +282 -74
- 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 +58 -24
- package/src/runtime-pi/client/worker/Workport.js +80 -0
- package/src/runtime-pi/server/Router.js +2 -2
- package/src/runtime-pi/server/Runtime.js +30 -11
- package/src/runtime-pi/server/RuntimeClient.js +24 -14
- package/src/runtime-pi/util.js +2 -2
- package/test/site/package.json +9 -0
- package/test/site/public/bundle.html +6 -0
- package/test/site/public/bundle.html.json +4 -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/index1.html +35 -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 +46 -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-3/logo-130x130.png +0 -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/sparoots.json +5 -0
- package/test/site/public/worker.js +3 -0
- package/test/site/public/worker.js.gz +0 -0
- package/test/site/server/index.js +16 -0
- package/docker/Dockerfile +0 -26
- package/docker/README.md +0 -77
- package/src/runtime-pi/client/WorkerComm.js +0 -102
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @imports
|
|
5
|
+
*/
|
|
6
|
+
import { _isFunction, _isObject } from '@webqit/util/js/index.js';
|
|
7
|
+
import { Observer } from './Runtime.js';
|
|
8
|
+
|
|
9
|
+
export default class Workport {
|
|
10
|
+
|
|
11
|
+
constructor(file, params = {}) {
|
|
12
|
+
this.ready = navigator.serviceWorker.ready;
|
|
13
|
+
|
|
14
|
+
// --------
|
|
15
|
+
// Registration and lifecycle
|
|
16
|
+
// --------
|
|
17
|
+
this.registration = new Promise((resolve, reject) => {
|
|
18
|
+
const register = () => {
|
|
19
|
+
navigator.serviceWorker.register(file, { scope: params.scope || '/' }).then(async registration => {
|
|
20
|
+
|
|
21
|
+
// Helper that updates instance's state
|
|
22
|
+
const state = target => {
|
|
23
|
+
// instance2.state can be any of: "installing", "installed", "activating", "activated", "redundant"
|
|
24
|
+
const equivState = target.state === 'installed' ? 'waiting' :
|
|
25
|
+
(target.state === 'activating' || target.state === 'activated' ? 'active' : target.state)
|
|
26
|
+
Observer.set(this, equivState, target);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// We're always installing at first for a new service worker.
|
|
30
|
+
// An existing service would immediately be active
|
|
31
|
+
const worker = registration.active || registration.waiting || registration.installing;
|
|
32
|
+
state(worker);
|
|
33
|
+
worker.addEventListener('statechange', e => state(e.target));
|
|
34
|
+
|
|
35
|
+
// "updatefound" event - a new worker that will control
|
|
36
|
+
// this page is installing somewhere
|
|
37
|
+
registration.addEventListener('updatefound', () => {
|
|
38
|
+
// If updatefound is fired, it means that there's
|
|
39
|
+
// a new service worker being installed.
|
|
40
|
+
state(registration.installing);
|
|
41
|
+
registration.installing.addEventListener('statechange', e => state(e.target));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
resolve(registration);
|
|
45
|
+
}).catch(e => reject(e));
|
|
46
|
+
};
|
|
47
|
+
if (params.onWondowLoad) {
|
|
48
|
+
window.addEventListener('load', register);
|
|
49
|
+
} else {
|
|
50
|
+
register();
|
|
51
|
+
}
|
|
52
|
+
if (params.startMessages) {
|
|
53
|
+
navigator.serviceWorker.startMessages();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// --------
|
|
58
|
+
// Post messaging
|
|
59
|
+
// --------
|
|
60
|
+
const postSendCallback = (message, callback, onAvailability = 1) => {
|
|
61
|
+
if (this.active) {
|
|
62
|
+
if (_isFunction(message)) message = message();
|
|
63
|
+
callback(this.active, message);
|
|
64
|
+
} else if (onAvailability) {
|
|
65
|
+
// Availability Handling
|
|
66
|
+
const availabilityHandler = entry => {
|
|
67
|
+
if (_isFunction(message)) message = message();
|
|
68
|
+
callback(entry.value, message);
|
|
69
|
+
if (onAvailability !== 2) {
|
|
70
|
+
Observer.unobserve(this, 'active', availabilityHandler);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
Observer.observe(this, 'active', availabilityHandler);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
this.messaging = {
|
|
77
|
+
post: (message, onAvailability = 1) => {
|
|
78
|
+
postSendCallback(message, (active, message) => {
|
|
79
|
+
active.postMessage(message);
|
|
80
|
+
}, onAvailability);
|
|
81
|
+
return this.post;
|
|
82
|
+
},
|
|
83
|
+
listen: callback => {
|
|
84
|
+
navigator.serviceWorker.addEventListener('message', callback);
|
|
85
|
+
return this.post;
|
|
86
|
+
},
|
|
87
|
+
request: (message, onAvailability = 1) => {
|
|
88
|
+
return new Promise(res => {
|
|
89
|
+
postSendCallback(message, (active, message) => {
|
|
90
|
+
let messageChannel = new MessageChannel();
|
|
91
|
+
active.postMessage(message, [ messageChannel.port2 ]);
|
|
92
|
+
messageChannel.port1.onmessage = e => res(e.data);
|
|
93
|
+
}, onAvailability);
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
channel(channelId) {
|
|
97
|
+
if (!this.channels.has(channelId)) { this.channels.set(channelId, new BroadcastChannel(channel)); }
|
|
98
|
+
let channel = this.channels.get(channelId);
|
|
99
|
+
return {
|
|
100
|
+
broadcast: message => channel.postMessage(message),
|
|
101
|
+
listen: callback => channel.addEventListener('message', callback),
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
channels: new Map,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// --------
|
|
108
|
+
// Notifications
|
|
109
|
+
// --------
|
|
110
|
+
this.notifications = {
|
|
111
|
+
fire: (title, params = {}) => {
|
|
112
|
+
return new Promise((res, rej) => {
|
|
113
|
+
if (typeof Notification === 'undefined' || Notification.permission !== 'granted') {
|
|
114
|
+
return rej(typeof Notification !== 'undefined' && Notification && Notification.permission);
|
|
115
|
+
}
|
|
116
|
+
notification.addEventListener('error', rej);
|
|
117
|
+
let notification = new Notification(title, params);
|
|
118
|
+
notification.addEventListener('click', res);
|
|
119
|
+
notification.addEventListener('close', res);
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// --------
|
|
125
|
+
// Push notifications
|
|
126
|
+
// --------
|
|
127
|
+
this.push = {
|
|
128
|
+
getSubscription: async () => {
|
|
129
|
+
return (await this.registration).pushManager.getSubscription();
|
|
130
|
+
},
|
|
131
|
+
subscribe: async (publicKey, params = {}) => {
|
|
132
|
+
var subscription = await this.push.getSubscription();
|
|
133
|
+
return subscription ? subscription : (await this.registration).pushManager.subscribe(
|
|
134
|
+
_isObject(publicKey) ? publicKey : {
|
|
135
|
+
applicationServerKey: urlBase64ToUint8Array(publicKey),
|
|
136
|
+
...params,
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
unsubscribe: async () => {
|
|
141
|
+
var subscription = await this.push.getSubscription();
|
|
142
|
+
return !subscription ? null : subscription.unsubscribe();
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Public base64 to Uint
|
|
150
|
+
function urlBase64ToUint8Array(base64String) {
|
|
151
|
+
var padding = '='.repeat((4 - base64String.length % 4) % 4);
|
|
152
|
+
var base64 = (base64String + padding)
|
|
153
|
+
.replace(/\-/g, '+')
|
|
154
|
+
.replace(/_/g, '/');
|
|
155
|
+
|
|
156
|
+
var rawData = window.atob(base64);
|
|
157
|
+
var outputArray = new Uint8Array(rawData.length);
|
|
158
|
+
|
|
159
|
+
for (var i = 0; i < rawData.length; ++i) {
|
|
160
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
161
|
+
}
|
|
162
|
+
return outputArray;
|
|
163
|
+
}
|
|
@@ -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
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,235 @@ export async function generate() {
|
|
|
30
35
|
throw new Error(`The Layout configurator "config.deployment.Layout" is required in context.`);
|
|
31
36
|
}
|
|
32
37
|
const layoutConfig = await (new cx.config.deployment.Layout(cx)).read();
|
|
38
|
+
// -----------
|
|
33
39
|
const dirPublic = Path.resolve(cx.CWD || '', layoutConfig.PUBLIC_DIR);
|
|
40
|
+
const dirClient = Path.resolve(cx.CWD || '', layoutConfig.CLIENT_DIR);
|
|
41
|
+
const dirWorker = Path.resolve(cx.CWD || '', layoutConfig.WORKER_DIR);
|
|
34
42
|
const dirSelf = Path.dirname(Url.fileURLToPath(import.meta.url)).replace(/\\/g, '/');
|
|
35
43
|
// -----------
|
|
44
|
+
// Scan Subdocuments
|
|
45
|
+
const scanSubroots = (sparoot, rootFileName) => {
|
|
46
|
+
let dir = Path.join(dirPublic, sparoot), passes = 0;
|
|
47
|
+
return [ Fs.readdirSync(dir).reduce((sparoots, f) => {
|
|
48
|
+
let resource = Path.join(dir, f);
|
|
49
|
+
if (Fs.statSync(resource).isDirectory()) {
|
|
50
|
+
let subsparoot = Path.join(sparoot, f);
|
|
51
|
+
if (Fs.existsSync(Path.join(resource, rootFileName))) {
|
|
52
|
+
return sparoots.concat(subsparoot);
|
|
53
|
+
}
|
|
54
|
+
passes ++;
|
|
55
|
+
return sparoots.concat(scanSubroots(subsparoot, rootFileName)[ 0 ]);
|
|
56
|
+
}
|
|
57
|
+
return sparoots;
|
|
58
|
+
}, []), passes ];
|
|
59
|
+
};
|
|
60
|
+
// -----------
|
|
36
61
|
// Generate client build
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
const generateClient = async function(sparoot, spaGraphCallback = null) {
|
|
63
|
+
let [ subsparoots, targets ] = (sparoot && scanSubroots(sparoot, 'index.html')) || [ [], false ];
|
|
64
|
+
if (!sparoot) sparoot = '/';
|
|
65
|
+
let spaRouting = { root: sparoot, subroots: subsparoots, targets };
|
|
66
|
+
let codeSplitting = !!(sparoot !== '/' || subsparoots.length);
|
|
67
|
+
let outfileMain = Path.join(sparoot, clientConfig.bundle_filename),
|
|
68
|
+
outfileWebflo = _beforeLast(clientConfig.bundle_filename, '.js') + '.webflo.js';
|
|
69
|
+
let gen = { imports: {}, code: [], };
|
|
70
|
+
// ------------------
|
|
71
|
+
const initWebflo = gen => {
|
|
72
|
+
if (clientConfig.oohtml_support === 'namespacing') {
|
|
73
|
+
gen.imports[`${dirSelf}/oohtml/namespacing.js`] = null;
|
|
74
|
+
} else if (clientConfig.oohtml_support === 'scripting') {
|
|
75
|
+
gen.imports[`${dirSelf}/oohtml/scripting.js`] = null;
|
|
76
|
+
} else if (clientConfig.oohtml_support === 'templating') {
|
|
77
|
+
gen.imports[`${dirSelf}/oohtml/templating.js`] = null;
|
|
78
|
+
} else if (clientConfig.oohtml_support !== 'none') {
|
|
79
|
+
gen.imports[`${dirSelf}/oohtml/full.js`] = null;
|
|
80
|
+
}
|
|
81
|
+
gen.imports[`${dirSelf}/index.js`] = `* as Webflo`;
|
|
82
|
+
gen.code.push(``);
|
|
83
|
+
gen.code.push(`if (!globalThis.WebQit) {`);
|
|
84
|
+
gen.code.push(` globalThis.WebQit = {}`);
|
|
85
|
+
gen.code.push(`}`);
|
|
86
|
+
gen.code.push(`WebQit.Webflo = Webflo`);
|
|
87
|
+
return gen;
|
|
88
|
+
};
|
|
89
|
+
// ------------------
|
|
90
|
+
if (!codeSplitting) {
|
|
91
|
+
initWebflo(gen);
|
|
92
|
+
} else if (sparoot === '/') {
|
|
93
|
+
if (cx.logger) {
|
|
94
|
+
cx.logger.log(cx.logger.style.keyword(`-----------------`));
|
|
95
|
+
cx.logger.log(`Base Build`);
|
|
96
|
+
cx.logger.log(cx.logger.style.keyword(`-----------------`));
|
|
97
|
+
}
|
|
98
|
+
let gen1 = initWebflo({ imports: {}, code: [], });
|
|
99
|
+
await bundle.call(cx, gen1, Path.join(dirPublic, outfileWebflo), true/* asModule */);
|
|
100
|
+
}
|
|
101
|
+
// ------------------
|
|
102
|
+
if (cx.logger) {
|
|
103
|
+
cx.logger.log(cx.logger.style.keyword(`-----------------`));
|
|
104
|
+
cx.logger.log(`Client Build ` + cx.logger.style.comment(`(sparoot:${sparoot}; is-split:${codeSplitting})`));
|
|
105
|
+
cx.logger.log(cx.logger.style.keyword(`-----------------`));
|
|
106
|
+
}
|
|
107
|
+
gen.code.push(`const { start } = WebQit.Webflo`);
|
|
108
|
+
// ------------------
|
|
109
|
+
// Bundle
|
|
110
|
+
declareStart.call(cx, gen, dirClient, dirPublic, clientConfig, spaRouting);
|
|
111
|
+
await bundle.call(cx, gen, Path.join(dirPublic, outfileMain), true/* asModule */);
|
|
112
|
+
// ------------------
|
|
113
|
+
// Embed/unembed
|
|
114
|
+
let targetDocumentFile = Path.join(dirPublic, sparoot, 'index.html'),
|
|
115
|
+
outfileWebfloPublic = Path.join(clientConfig.public_base_url, outfileWebflo),
|
|
116
|
+
outfileMainPublic = Path.join(clientConfig.public_base_url, outfileMain),
|
|
117
|
+
embedList = [],
|
|
118
|
+
unembedList = [];
|
|
119
|
+
if (cx.flags['auto-embed']) {
|
|
120
|
+
if (codeSplitting) {
|
|
121
|
+
embedList.push(outfileWebfloPublic);
|
|
122
|
+
} else {
|
|
123
|
+
unembedList.push(outfileWebfloPublic);
|
|
124
|
+
}
|
|
125
|
+
embedList.push(outfileMainPublic);
|
|
126
|
+
} else {
|
|
127
|
+
unembedList.push(outfileWebfloPublic, outfileMainPublic);
|
|
128
|
+
}
|
|
129
|
+
handleEmbeds(targetDocumentFile, embedList, unembedList);
|
|
130
|
+
// ------------------
|
|
131
|
+
// Recurse
|
|
132
|
+
spaGraphCallback && spaGraphCallback(sparoot, subsparoots);
|
|
133
|
+
if (cx.flags.recursive) {
|
|
134
|
+
while (subsparoots.length) {
|
|
135
|
+
await generateClient(subsparoots.shift(), spaGraphCallback);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
43
139
|
// -----------
|
|
44
140
|
// Generate worker build
|
|
45
|
-
|
|
46
|
-
let
|
|
47
|
-
|
|
48
|
-
|
|
141
|
+
const generateWorker = async function(workerroot, workerGraphCallbak = null) {
|
|
142
|
+
let [ subworkerroots, targets ] = workerroot && scanSubroots(workerroot, 'workerroot') || [ [], false ];
|
|
143
|
+
if (!workerroot) workerroot = '/';
|
|
144
|
+
let workerRouting = { root: workerroot, subroots: subworkerroots, targets };
|
|
145
|
+
let gen = { imports: {}, code: [], };
|
|
146
|
+
if (cx.logger) {
|
|
147
|
+
cx.logger.log(cx.logger.style.comment(`-----------------`));
|
|
148
|
+
cx.logger.log(`Worker Build - workerroot:${workerroot}`);
|
|
149
|
+
cx.logger.log(cx.logger.style.comment(`-----------------`));
|
|
150
|
+
}
|
|
151
|
+
// ------------------
|
|
152
|
+
// >> Modules import
|
|
153
|
+
gen.imports[`${dirSelf}/worker/index.js`] = `{ start }`;
|
|
154
|
+
gen.code.push(``);
|
|
155
|
+
// ------------------
|
|
156
|
+
// Bundle
|
|
157
|
+
if (workerConfig.cache_only_urls.length) {
|
|
158
|
+
// Separate URLs from patterns
|
|
159
|
+
let [ urls, patterns ] = workerConfig.cache_only_urls.reduce(([ urls, patterns ], url) => {
|
|
160
|
+
let patternInstance = urlPattern(url, 'http://localhost'),
|
|
161
|
+
isPattern = patternInstance.isPattern();
|
|
162
|
+
if (isPattern && (patternInstance.pattern.pattern.hostname !== 'localhost' || patternInstance.pattern.pattern.port)) {
|
|
163
|
+
throw new Error(`Pattern URLs must have no origin part. Recieved "${url}".`);
|
|
164
|
+
}
|
|
165
|
+
return isPattern ? [ urls, patterns.concat(patternInstance) ] : [ urls.concat(url), patterns ];
|
|
166
|
+
}, [ [], [] ]);
|
|
167
|
+
// Resolve patterns
|
|
168
|
+
if (patterns.length) {
|
|
169
|
+
// List all files
|
|
170
|
+
let scan = dir => Fs.readdirSync(dir).reduce((result, f) => {
|
|
171
|
+
let resource = Path.join(dir, f);
|
|
172
|
+
return result.concat(Fs.statSync(resource).isDirectory() ? scan(resource) : '/' + Path.relative(dirPublic, resource));
|
|
173
|
+
}, []);
|
|
174
|
+
let files = scan(dirPublic);
|
|
175
|
+
// Resolve patterns from files
|
|
176
|
+
workerConfig.cache_only_urls = patterns.reduce((all, pattern) => {
|
|
177
|
+
let matchedFiles = files.filter(file => pattern.test(file, 'http://localhost'));
|
|
178
|
+
if (matchedFiles.length) return all.concat(matchedFiles);
|
|
179
|
+
throw new Error(`The pattern "${pattern.pattern.pattern.pathname}" didn't match any files.`);
|
|
180
|
+
}, urls);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
declareStart.call(cx, gen, dirWorker, dirPublic, workerConfig, workerRouting);
|
|
184
|
+
await bundle.call(cx, gen, Path.join(dirPublic, workerroot, clientConfig.worker_filename));
|
|
185
|
+
// ------------------
|
|
186
|
+
// Recurse
|
|
187
|
+
workerGraphCallbak && workerGraphCallbak(workerroot, subworkerroots);
|
|
188
|
+
if (cx.flags.recursive) {
|
|
189
|
+
while (subworkerroots.length) {
|
|
190
|
+
await generateWorker(subworkerroots.shift());
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
// -----------
|
|
195
|
+
// Generate now...
|
|
196
|
+
let sparootsFile = Path.join(dirPublic, 'sparoots.json');
|
|
197
|
+
if (clientConfig.spa_routing !== false) {
|
|
198
|
+
const sparoots = [];
|
|
199
|
+
await generateClient('/', root => sparoots.push(root));
|
|
200
|
+
Fs.writeFileSync(sparootsFile, JSON.stringify(sparoots, null, 4));
|
|
201
|
+
} else {
|
|
202
|
+
await generateClient();
|
|
203
|
+
Fs.existsSync(sparootsFile) && Fs.unlinkSync(sparootsFile);
|
|
204
|
+
}
|
|
205
|
+
if (clientConfig.service_worker_support) {
|
|
206
|
+
await generateWorker('/');
|
|
49
207
|
}
|
|
50
208
|
}
|
|
51
209
|
|
|
52
210
|
/**
|
|
53
211
|
* Compile routes.
|
|
54
212
|
*
|
|
55
|
-
* @param
|
|
56
|
-
* @param string
|
|
57
|
-
* @param
|
|
58
|
-
* @param
|
|
213
|
+
* @param object gen
|
|
214
|
+
* @param string routesDir
|
|
215
|
+
* @param string targetPublic
|
|
216
|
+
* @param object paramsObj
|
|
217
|
+
* @param object routing
|
|
59
218
|
*
|
|
60
219
|
* @return Object
|
|
61
220
|
*/
|
|
62
|
-
function
|
|
221
|
+
function declareStart(gen, routesDir, targetDir, paramsObj, routing) {
|
|
63
222
|
const cx = this || {};
|
|
64
|
-
if (cx.logger) {
|
|
65
|
-
cx.logger.log(cx.logger.style.comment(`-----------------`));
|
|
66
|
-
cx.logger.log(desc);
|
|
67
|
-
cx.logger.log(cx.logger.style.comment(`-----------------`));
|
|
68
|
-
cx.logger.log('');
|
|
69
|
-
}
|
|
70
|
-
// ------------------
|
|
71
|
-
const gen = { imports: {}, code: [], };
|
|
72
|
-
// ------------------
|
|
73
|
-
// >> Modules import
|
|
74
|
-
gen.imports[`${modulesDir}/index.js`] = `{ start }`;
|
|
75
|
-
gen.code.push(``);
|
|
76
223
|
// ------------------
|
|
77
224
|
// >> Routes mapping
|
|
78
225
|
gen.code.push(`// >> Routes`);
|
|
79
|
-
declareRoutesObj.call(cx, gen, routesDir, 'layout',
|
|
226
|
+
declareRoutesObj.call(cx, gen, routesDir, targetDir, 'layout', routing);
|
|
80
227
|
gen.code.push(``);
|
|
81
228
|
// ------------------
|
|
82
229
|
// >> Params
|
|
83
230
|
gen.code.push(`// >> Params`);
|
|
84
|
-
declareParamsObj.call(cx, gen, paramsObj, 'params');
|
|
231
|
+
declareParamsObj.call(cx, gen, { ...paramsObj, routing }, 'params');
|
|
85
232
|
gen.code.push(``);
|
|
86
233
|
// ------------------
|
|
87
234
|
// >> Startup
|
|
88
235
|
gen.code.push(`// >> Startup`);
|
|
89
236
|
gen.code.push(`start.call({ layout, params })`);
|
|
90
|
-
return gen;
|
|
91
237
|
}
|
|
92
238
|
|
|
93
239
|
/**
|
|
94
240
|
* Compile routes.
|
|
95
241
|
*
|
|
96
|
-
* @param object
|
|
97
|
-
* @param string
|
|
98
|
-
* @param string
|
|
242
|
+
* @param object gen
|
|
243
|
+
* @param string routesDir
|
|
244
|
+
* @param string targetDir
|
|
245
|
+
* @param string varName
|
|
246
|
+
* @param object routing
|
|
99
247
|
*
|
|
100
248
|
* @return void
|
|
101
249
|
*/
|
|
102
|
-
function declareRoutesObj(gen, routesDir, varName) {
|
|
250
|
+
function declareRoutesObj(gen, routesDir, targetDir, varName, routing) {
|
|
103
251
|
const cx = this || {};
|
|
104
|
-
|
|
252
|
+
let _routesDir = Path.join(routesDir, routing.root),
|
|
253
|
+
_targetDir = Path.join(targetDir, routing.root);
|
|
254
|
+
cx.logger && cx.logger.log(cx.logger.style.keyword(`> `) + `Declaring routes...`);
|
|
105
255
|
// ----------------
|
|
106
256
|
// Directory walker
|
|
107
257
|
const walk = (dir, callback) => {
|
|
108
258
|
Fs.readdirSync(dir).forEach(f => {
|
|
109
259
|
let resource = Path.join(dir, f);
|
|
260
|
+
let namespace = _beforeLast('/' + Path.relative(routesDir, resource), '/index.js') || '/';
|
|
110
261
|
if (Fs.statSync(resource).isDirectory()) {
|
|
262
|
+
if (routing.subroots.includes(namespace)) return;
|
|
111
263
|
walk(resource, callback);
|
|
112
264
|
} else {
|
|
113
|
-
let
|
|
114
|
-
callback(resource,
|
|
265
|
+
let relativePath = Path.relative(_targetDir, resource);
|
|
266
|
+
callback(resource, namespace, relativePath);
|
|
115
267
|
}
|
|
116
268
|
});
|
|
117
269
|
};
|
|
@@ -119,29 +271,25 @@ function declareRoutesObj(gen, routesDir, varName) {
|
|
|
119
271
|
// >> Routes mapping
|
|
120
272
|
gen.code.push(`const ${varName} = {};`);
|
|
121
273
|
let indexCount = 0;
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
walk(routesDir, (file, ext) => {
|
|
274
|
+
if (Fs.existsSync(_routesDir)) {
|
|
275
|
+
walk(_routesDir, (file, namespace, relativePath) => {
|
|
125
276
|
//relativePath = relativePath.replace(/\\/g, '/');
|
|
126
277
|
if (file.replace(/\\/g, '/').endsWith('/index.js')) {
|
|
127
|
-
let relativePath = Path.relative(routesDir, file).replace(/\\/g, '/');
|
|
128
278
|
// Import code
|
|
129
279
|
let routeName = 'index' + (++ indexCount);
|
|
130
280
|
// IMPORTANT: we;re taking a step back here so that the parent-child relationship for
|
|
131
281
|
// the directories be involved
|
|
132
|
-
gen.imports[
|
|
282
|
+
gen.imports[relativePath] = '* as ' + routeName;
|
|
133
283
|
// Definition code
|
|
134
|
-
|
|
135
|
-
gen.code.push(`${varName}['${routePath || '/'}'] = ${routeName};`);
|
|
284
|
+
gen.code.push(`${varName}['${namespace}'] = ${routeName};`);
|
|
136
285
|
// Show
|
|
137
|
-
cx.logger && cx.logger.log(
|
|
286
|
+
cx.logger && cx.logger.log(cx.logger.style.comment(` [${namespace}]: `) + cx.logger.style.url(relativePath) + cx.logger.style.comment(` (${Fs.statSync(file).size / 1024} KB)`));
|
|
138
287
|
}
|
|
139
288
|
});
|
|
140
289
|
}
|
|
141
290
|
if (!indexCount) {
|
|
142
|
-
cx.logger && cx.logger.log(
|
|
291
|
+
cx.logger && cx.logger.log(cx.logger.style.comment(` (none)`));
|
|
143
292
|
}
|
|
144
|
-
cx.logger && cx.logger.log(``);
|
|
145
293
|
}
|
|
146
294
|
|
|
147
295
|
/**
|
|
@@ -149,7 +297,7 @@ function declareRoutesObj(gen, routesDir, varName) {
|
|
|
149
297
|
*
|
|
150
298
|
* @param object gen
|
|
151
299
|
* @param object paramsObj
|
|
152
|
-
* @param string
|
|
300
|
+
* @param string varName
|
|
153
301
|
*
|
|
154
302
|
* @return void
|
|
155
303
|
*/
|
|
@@ -189,7 +337,9 @@ function declareParamsObj(gen, paramsObj, varName = null, indentation = 0) {
|
|
|
189
337
|
*/
|
|
190
338
|
async function bundle(gen, outfile, asModule = false) {
|
|
191
339
|
const cx = this || {};
|
|
192
|
-
const compression = cx.flags.
|
|
340
|
+
const compression = !cx.flags.compression ? false : (
|
|
341
|
+
cx.flags.compression === true ? ['gz'] : cx.flags.compression.split(',').map(s => s.trim())
|
|
342
|
+
);
|
|
193
343
|
const moduleFile = `${_beforeLast(outfile, '.')}.esm.js`;
|
|
194
344
|
|
|
195
345
|
// ------------------
|
|
@@ -199,7 +349,6 @@ async function bundle(gen, outfile, asModule = false) {
|
|
|
199
349
|
waiting.start();
|
|
200
350
|
jsFile.write(gen, moduleFile, 'ES Module file');
|
|
201
351
|
waiting.stop();
|
|
202
|
-
cx.logger.info(cx.logger.f`The module file: ${moduleFile}`);
|
|
203
352
|
} else {
|
|
204
353
|
jsFile.write(gen, moduleFile, 'ES Module file');
|
|
205
354
|
}
|
|
@@ -227,34 +376,93 @@ async function bundle(gen, outfile, asModule = false) {
|
|
|
227
376
|
let waiting;
|
|
228
377
|
if (cx.logger) {
|
|
229
378
|
waiting = cx.logger.waiting(`Bundling...`);
|
|
230
|
-
cx.logger.log('');
|
|
231
|
-
cx.logger.log('> Bundling...');
|
|
232
|
-
cx.logger.info(cx.logger.f`FROM: ${bundlingConfig.entryPoints[0]}`);
|
|
233
|
-
cx.logger.info(cx.logger.f`TO: ${bundlingConfig.outfile}`);
|
|
379
|
+
cx.logger.log(cx.logger.style.keyword(`> `) + 'Bundling...');
|
|
234
380
|
waiting.start();
|
|
235
381
|
}
|
|
236
|
-
//
|
|
382
|
+
// Main
|
|
237
383
|
await EsBuild.build(bundlingConfig);
|
|
238
|
-
if (waiting) waiting.stop();
|
|
239
|
-
// Remove moduleFile build
|
|
240
|
-
Fs.unlinkSync(bundlingConfig.entryPoints[0]);
|
|
241
|
-
|
|
242
|
-
// ----------------
|
|
243
384
|
// Compress...
|
|
385
|
+
let compressedFiles = [], removals = [];
|
|
244
386
|
if (compression) {
|
|
245
|
-
if (cx.logger) {
|
|
246
|
-
waiting = cx.logger.waiting(`Compressing...`);
|
|
247
|
-
waiting.start();
|
|
248
|
-
}
|
|
249
387
|
const contents = Fs.readFileSync(bundlingConfig.outfile);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
388
|
+
if (compression.includes('gz')) {
|
|
389
|
+
const gzip = gzipSync(contents, {});
|
|
390
|
+
Fs.writeFileSync(bundlingConfig.outfile + '.gz', gzip);
|
|
391
|
+
compressedFiles.push(bundlingConfig.outfile + '.gz');
|
|
392
|
+
} else {
|
|
393
|
+
removals.push(bundlingConfig.outfile + '.gz');
|
|
394
|
+
}
|
|
395
|
+
if (compression.includes('br')) {
|
|
396
|
+
const brotli = brotliCompressSync(contents, {});
|
|
397
|
+
Fs.writeFileSync(bundlingConfig.outfile + '.br', brotli);
|
|
398
|
+
compressedFiles.push(bundlingConfig.outfile + '.br');
|
|
399
|
+
} else {
|
|
400
|
+
removals.push(bundlingConfig.outfile + '.br');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Remove moduleFile build
|
|
404
|
+
Fs.unlinkSync(bundlingConfig.entryPoints[0]);
|
|
405
|
+
removals.forEach(file => Fs.existsSync(file) && Fs.unlinkSync(file));
|
|
406
|
+
if (waiting) waiting.stop();
|
|
407
|
+
// ----------------
|
|
408
|
+
// Stats
|
|
409
|
+
if (cx.logger) {
|
|
410
|
+
[bundlingConfig.outfile].concat(compressedFiles).forEach(file => {
|
|
411
|
+
let ext = '.' + _afterLast(file, '.');
|
|
412
|
+
cx.logger.info(cx.logger.style.comment(` [${ext}]: `) + cx.logger.style.url(file) + cx.logger.style.comment(` (${Fs.statSync(file).size / 1024} KB)`));
|
|
413
|
+
});
|
|
414
|
+
cx.logger.log('');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Handles auto-embeds
|
|
420
|
+
*
|
|
421
|
+
* @param String targetDocumentFile
|
|
422
|
+
* @param Array embedList
|
|
423
|
+
* @param Array unembedList
|
|
424
|
+
*
|
|
425
|
+
* @return Void
|
|
426
|
+
*/
|
|
427
|
+
function handleEmbeds(targetDocumentFile, embedList, unembedList) {
|
|
428
|
+
let targetDocument, successLevel = 0;
|
|
429
|
+
if (Fs.existsSync(targetDocumentFile) && (targetDocument = Fs.readFileSync(targetDocumentFile).toString()) && targetDocument.trim().startsWith('<!DOCTYPE html')) {
|
|
430
|
+
successLevel = 1;
|
|
431
|
+
let dom = new Jsdom.JSDOM(targetDocument), by = 'webflo', touched;
|
|
432
|
+
let embed = (src, before) => {
|
|
433
|
+
let embedded = dom.window.document.querySelector(`script[src="${src}"]`);
|
|
434
|
+
if (!embedded) {
|
|
435
|
+
embedded = dom.window.document.createElement('script');
|
|
436
|
+
embedded.setAttribute('type', 'module');
|
|
437
|
+
embedded.setAttribute('src', src);
|
|
438
|
+
embedded.setAttribute('by', by);
|
|
439
|
+
if (before) {
|
|
440
|
+
before.before(embedded, `\n\t\t`);
|
|
441
|
+
} else {
|
|
442
|
+
dom.window.document.head.appendChild(embedded);
|
|
443
|
+
}
|
|
444
|
+
touched = true;
|
|
445
|
+
}
|
|
446
|
+
return embedded;
|
|
447
|
+
};
|
|
448
|
+
let unembed = src => {
|
|
449
|
+
src = Path.join('/', src);
|
|
450
|
+
let embedded = dom.window.document.querySelector(`script[src="${src}"][by="${by}"]`);
|
|
451
|
+
if (embedded) {
|
|
452
|
+
embedded.remove();
|
|
453
|
+
touched = true;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
embedList.reverse().reduce((prev, src) => {
|
|
457
|
+
return embed(src, prev);
|
|
458
|
+
}, dom.window.document.querySelector(`script[src]`) || dom.window.document.querySelector(`script`));
|
|
459
|
+
unembedList.forEach(src => {
|
|
460
|
+
unembed(src);
|
|
461
|
+
});
|
|
462
|
+
if (touched) {
|
|
463
|
+
Fs.writeFileSync(targetDocumentFile, dom.serialize());
|
|
464
|
+
successLevel = 2;
|
|
258
465
|
}
|
|
259
466
|
}
|
|
467
|
+
return successLevel;
|
|
260
468
|
}
|
|
File without changes
|