@webqit/webflo 0.10.5 → 0.11.2

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 (36) hide show
  1. package/README.md +1082 -323
  2. package/package.json +2 -2
  3. package/src/config-pi/runtime/Client.js +7 -10
  4. package/src/config-pi/runtime/client/Worker.js +30 -12
  5. package/src/runtime-pi/Router.js +1 -1
  6. package/src/runtime-pi/client/Runtime.js +98 -49
  7. package/src/runtime-pi/client/RuntimeClient.js +12 -40
  8. package/src/runtime-pi/client/Workport.js +163 -0
  9. package/src/runtime-pi/client/generate.js +71 -37
  10. package/src/runtime-pi/client/worker/Worker.js +57 -23
  11. package/src/runtime-pi/client/worker/Workport.js +80 -0
  12. package/src/runtime-pi/server/Runtime.js +22 -8
  13. package/src/runtime-pi/server/RuntimeClient.js +6 -6
  14. package/src/runtime-pi/util.js +2 -2
  15. package/test/site/package.json +9 -0
  16. package/test/site/public/bundle.html +3 -0
  17. package/test/site/public/bundle.html.json +1 -0
  18. package/test/site/public/bundle.js +1 -1
  19. package/test/site/public/bundle.js.gz +0 -0
  20. package/test/site/public/bundle.webflo.js +8 -8
  21. package/test/site/public/bundle.webflo.js.gz +0 -0
  22. package/test/site/public/index.html +5 -5
  23. package/test/site/public/index1.html +35 -0
  24. package/test/site/public/page-2/bundle.js +1 -1
  25. package/test/site/public/page-2/bundle.js.gz +0 -0
  26. package/test/site/public/page-2/index.html +3 -4
  27. package/test/site/public/page-3/logo-130x130.png +0 -0
  28. package/test/site/public/page-4/subpage/bundle.js +1 -1
  29. package/test/site/public/page-4/subpage/bundle.js.gz +0 -0
  30. package/test/site/public/sparoots.json +5 -0
  31. package/test/site/public/worker.js +1 -1
  32. package/test/site/public/worker.js.gz +0 -0
  33. package/test/site/server/index.js +14 -6
  34. package/docker/Dockerfile +0 -26
  35. package/docker/README.md +0 -77
  36. package/src/runtime-pi/client/WorkerComm.js +0 -102
@@ -42,28 +42,29 @@ export async function generate() {
42
42
  const dirSelf = Path.dirname(Url.fileURLToPath(import.meta.url)).replace(/\\/g, '/');
43
43
  // -----------
44
44
  // Scan Subdocuments
45
- const scanSubscopes = scope => {
46
- let dir = Path.join(dirPublic, scope), passes = 0;
47
- return [ Fs.readdirSync(dir).reduce((scopes, f) => {
45
+ const scanSubroots = (sparoot, rootFileName) => {
46
+ let dir = Path.join(dirPublic, sparoot), passes = 0;
47
+ return [ Fs.readdirSync(dir).reduce((sparoots, f) => {
48
48
  let resource = Path.join(dir, f);
49
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);
50
+ let subsparoot = Path.join(sparoot, f);
51
+ if (Fs.existsSync(Path.join(resource, rootFileName))) {
52
+ return sparoots.concat(subsparoot);
53
53
  }
54
54
  passes ++;
55
- return scopes.concat(scanSubscopes(subscope)[ 0 ]);
55
+ return sparoots.concat(scanSubroots(subsparoot, rootFileName)[ 0 ]);
56
56
  }
57
- return scopes;
57
+ return sparoots;
58
58
  }, []), passes ];
59
59
  };
60
60
  // -----------
61
61
  // Generate client build
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),
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),
67
68
  outfileWebflo = _beforeLast(clientConfig.bundle_filename, '.js') + '.webflo.js';
68
69
  let gen = { imports: {}, code: [], };
69
70
  // ------------------
@@ -88,7 +89,7 @@ export async function generate() {
88
89
  // ------------------
89
90
  if (!codeSplitting) {
90
91
  initWebflo(gen);
91
- } else if (scope === '/') {
92
+ } else if (sparoot === '/') {
92
93
  if (cx.logger) {
93
94
  cx.logger.log(cx.logger.style.keyword(`-----------------`));
94
95
  cx.logger.log(`Base Build`);
@@ -100,22 +101,22 @@ export async function generate() {
100
101
  // ------------------
101
102
  if (cx.logger) {
102
103
  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(`Client Build ` + cx.logger.style.comment(`(sparoot:${sparoot}; is-split:${codeSplitting})`));
104
105
  cx.logger.log(cx.logger.style.keyword(`-----------------`));
105
106
  }
106
107
  gen.code.push(`const { start } = WebQit.Webflo`);
107
108
  // ------------------
108
109
  // Bundle
109
- declareStart.call(cx, gen, dirClient, dirPublic, clientConfig, routing);
110
+ declareStart.call(cx, gen, dirClient, dirPublic, clientConfig, spaRouting);
110
111
  await bundle.call(cx, gen, Path.join(dirPublic, outfileMain), true/* asModule */);
111
112
  // ------------------
112
113
  // Embed/unembed
113
- let targetDocumentFile = Path.join(dirPublic, scope, 'index.html'),
114
+ let targetDocumentFile = Path.join(dirPublic, sparoot, 'index.html'),
114
115
  outfileWebfloPublic = Path.join(clientConfig.public_base_url, outfileWebflo),
115
116
  outfileMainPublic = Path.join(clientConfig.public_base_url, outfileMain),
116
117
  embedList = [],
117
118
  unembedList = [];
118
- if (cx.flags['auto-embeds']) {
119
+ if (cx.flags['auto-embed']) {
119
120
  if (codeSplitting) {
120
121
  embedList.push(outfileWebfloPublic);
121
122
  } else {
@@ -128,21 +129,23 @@ export async function generate() {
128
129
  handleEmbeds(targetDocumentFile, embedList, unembedList);
129
130
  // ------------------
130
131
  // Recurse
132
+ spaGraphCallback && spaGraphCallback(sparoot, subsparoots);
131
133
  if (cx.flags.recursive) {
132
- while (subscopes.length) {
133
- await generateClient(subscopes.shift());
134
+ while (subsparoots.length) {
135
+ await generateClient(subsparoots.shift(), spaGraphCallback);
134
136
  }
135
137
  }
136
138
  };
137
139
  // -----------
138
140
  // Generate worker build
139
- const generateWorker = async function(scope) {
140
- let subscopes = [];
141
- let routing = { scope, subscopes };
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 };
142
145
  let gen = { imports: {}, code: [], };
143
146
  if (cx.logger) {
144
147
  cx.logger.log(cx.logger.style.comment(`-----------------`));
145
- cx.logger.log(`Worker Build - scope:${scope}`);
148
+ cx.logger.log(`Worker Build - workerroot:${workerroot}`);
146
149
  cx.logger.log(cx.logger.style.comment(`-----------------`));
147
150
  }
148
151
  // ------------------
@@ -152,22 +155,53 @@ export async function generate() {
152
155
  // ------------------
153
156
  // Bundle
154
157
  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
- }, []);
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
+ }
159
182
  }
160
- declareStart.call(cx, gen, dirWorker, dirPublic, workerConfig, routing);
161
- await bundle.call(cx, gen, Path.join(dirPublic, scope, clientConfig.worker_filename));
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);
162
188
  if (cx.flags.recursive) {
163
- while (subscopes.length) {
164
- await generateWorker(subscopes.shift());
189
+ while (subworkerroots.length) {
190
+ await generateWorker(subworkerroots.shift());
165
191
  }
166
192
  }
167
193
  };
168
194
  // -----------
169
195
  // Generate now...
170
- await generateClient('/');
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
+ }
171
205
  if (clientConfig.service_worker_support) {
172
206
  await generateWorker('/');
173
207
  }
@@ -215,8 +249,8 @@ function declareStart(gen, routesDir, targetDir, paramsObj, routing) {
215
249
  */
216
250
  function declareRoutesObj(gen, routesDir, targetDir, varName, routing) {
217
251
  const cx = this || {};
218
- let _routesDir = Path.join(routesDir, routing.scope),
219
- _targetDir = Path.join(targetDir, routing.scope);
252
+ let _routesDir = Path.join(routesDir, routing.root),
253
+ _targetDir = Path.join(targetDir, routing.root);
220
254
  cx.logger && cx.logger.log(cx.logger.style.keyword(`> `) + `Declaring routes...`);
221
255
  // ----------------
222
256
  // Directory walker
@@ -225,7 +259,7 @@ function declareRoutesObj(gen, routesDir, targetDir, varName, routing) {
225
259
  let resource = Path.join(dir, f);
226
260
  let namespace = _beforeLast('/' + Path.relative(routesDir, resource), '/index.js') || '/';
227
261
  if (Fs.statSync(resource).isDirectory()) {
228
- if (routing.subscopes.includes(namespace)) return;
262
+ if (routing.subroots.includes(namespace)) return;
229
263
  walk(resource, callback);
230
264
  } else {
231
265
  let relativePath = Path.relative(_targetDir, resource);
@@ -368,7 +402,7 @@ async function bundle(gen, outfile, asModule = false) {
368
402
  }
369
403
  // Remove moduleFile build
370
404
  Fs.unlinkSync(bundlingConfig.entryPoints[0]);
371
- removals.forEach(file => Fs.unlinkSync(file));
405
+ removals.forEach(file => Fs.existsSync(file) && Fs.unlinkSync(file));
372
406
  if (waiting) waiting.stop();
373
407
  // ----------------
374
408
  // Stats
@@ -5,6 +5,7 @@
5
5
  import { _any } from '@webqit/util/arr/index.js';
6
6
  import { HttpEvent, Request, Response, Observer } from '../Runtime.js';
7
7
  import { urlPattern } from '../../util.js';
8
+ import Workport from './Workport.js';
8
9
 
9
10
  /**
10
11
  * ---------------------------
@@ -71,11 +72,13 @@ export default class Worker {
71
72
  });
72
73
 
73
74
  // -------------
74
- // ONFETCH
75
+ // ONFETCH
75
76
  self.addEventListener('fetch', event => {
76
77
  // URL schemes that might arrive here but not supported; e.g.: chrome-extension://
77
78
  if (!event.request.url.startsWith('http')) return;
78
79
  event.respondWith((async (req, evt) => {
80
+ let requestingClient = await self.clients.get(event.clientId);
81
+ this.workport.setCurrentClient(requestingClient);
79
82
  const requestInit = [
80
83
  'method', 'headers', 'mode', 'credentials', 'cache', 'redirect', 'referrer', 'integrity',
81
84
  ].reduce((init, prop) => ({ [prop]: req[prop], ...init }), {});
@@ -93,6 +96,33 @@ export default class Worker {
93
96
  })(event.request, event));
94
97
  });
95
98
 
99
+ // -------------
100
+ // Workport
101
+ let workport = new Workport();
102
+ Observer.set(this, 'workport', workport);
103
+ workport.messaging.listen(async evt => {
104
+ let responsePort = evt.ports[0];
105
+ let client = this.clients.get('*');
106
+ let response = client.alert && await client.alert(evt);
107
+ if (responsePort) {
108
+ if (response instanceof Promise) {
109
+ response.then(data => {
110
+ responsePort.postMessage(data);
111
+ });
112
+ } else {
113
+ responsePort.postMessage(response);
114
+ }
115
+ }
116
+ });
117
+ workport.notifications.listen(async evt => {
118
+ let client = this.clients.get('*');
119
+ client.alert && await client.alert(evt);
120
+ });
121
+ workport.push.listen(async evt => {
122
+ let client = this.clients.get('*');
123
+ client.alert && await client.alert(evt);
124
+ });
125
+
96
126
  // ---------------
97
127
  Observer.set(this, 'location', {});
98
128
  Observer.set(this, 'network', {});
@@ -163,22 +193,26 @@ export default class Worker {
163
193
  }
164
194
  const matchUrl = (patterns, url) => _any((patterns || []).map(p => p.trim()).filter(p => p), p => urlPattern(p, self.origin).test(url));
165
195
  const execFetch = () => {
166
- if (matchUrl(this.cx.params.cache_only_urls, request.url)) {
167
- Observer.set(this.network, 'strategy', 'cache-only');
168
- return this.cacheFetch(request, { networkFallback: false, cacheRefresh: false });
196
+ // network_first_urls
197
+ if (!this.cx.params.default_fetching_strategy || this.cx.params.default_fetching_strategy === 'network-first' || matchUrl(this.cx.params.network_first_urls, request.url)) {
198
+ Observer.set(this.network, 'strategy', 'network-first');
199
+ return this.networkFetch(request, { cacheFallback: true, cacheRefresh: true });
200
+ }
201
+ // cache_first_urls
202
+ if (this.cx.params.default_fetching_strategy === 'cache-first' || matchUrl(this.cx.params.cache_first_urls, request.url)) {
203
+ Observer.set(this.network, 'strategy', 'cache-first');
204
+ return this.cacheFetch(request, { networkFallback: true, cacheRefresh: true });
169
205
  }
170
206
  // network_only_urls
171
- if (matchUrl(this.cx.params.network_only_urls, request.url)) {
207
+ if (this.cx.params.default_fetching_strategy === 'network-only' || matchUrl(this.cx.params.network_only_urls, request.url)) {
172
208
  Observer.set(this.network, 'strategy', 'network-only');
173
209
  return this.networkFetch(request, { cacheFallback: false, cacheRefresh: false });
174
210
  }
175
- // cache_first_urls
176
- if (matchUrl(this.cx.params.cache_first_urls, request.url)) {
177
- Observer.set(this.network, 'strategy', 'cache-first');
178
- return this.cacheFetch(request, { networkFallback: true, cacheRefresh: true });
211
+ // cache_only_urls
212
+ if (this.cx.params.default_fetching_strategy === 'cache-only' || matchUrl(this.cx.params.cache_only_urls, request.url)) {
213
+ Observer.set(this.network, 'strategy', 'cache-only');
214
+ return this.cacheFetch(request, { networkFallback: false, cacheRefresh: false });
179
215
  }
180
- Observer.set(this.network, 'strategy', 'network-first');
181
- return this.networkFetch(request, { cacheFallback: true, cacheRefresh: true });
182
216
  };
183
217
  let response = execFetch(request);
184
218
  // This catch() is NOT intended to handle failure of the fetch
@@ -187,18 +221,6 @@ export default class Worker {
187
221
  return response.then(_response => Response.compat(_response));
188
222
  }
189
223
 
190
- // Caching strategy: cache_first
191
- cacheFetch(request, params = {}) {
192
- return this.getRequestCache(request).then(cache => cache.match(request).then(response => {
193
- // Nothing cache, use network
194
- if (!response && params.networkFallback) return this.networkFetch(request, { ...params, cacheFallback: false });
195
- // Note: fetch, but for refreshing purposes only... not the returned response
196
- if (response && params.cacheRefresh) this.networkFetch(request, { ...params, justRefreshing: true });
197
- Observer.set(this.network, 'cache', true);
198
- return response;
199
- }));
200
- }
201
-
202
224
  // Caching strategy: network_first
203
225
  networkFetch(request, params = {}) {
204
226
  if (!params.cacheFallback) {
@@ -215,6 +237,18 @@ export default class Worker {
215
237
  }));
216
238
  }
217
239
 
240
+ // Caching strategy: cache_first
241
+ cacheFetch(request, params = {}) {
242
+ return this.getRequestCache(request).then(cache => cache.match(request).then(response => {
243
+ // Nothing cache, use network
244
+ if (!response && params.networkFallback) return this.networkFetch(request, { ...params, cacheFallback: false });
245
+ // Note: fetch, but for refreshing purposes only... not the returned response
246
+ if (response && params.cacheRefresh) this.networkFetch(request, { ...params, justRefreshing: true });
247
+ Observer.set(this.network, 'cache', true);
248
+ return response;
249
+ }));
250
+ }
251
+
218
252
  // Caches response
219
253
  refreshCache(request, response) {
220
254
  // Check if we received a valid response
@@ -0,0 +1,80 @@
1
+
2
+ export default class Workport {
3
+
4
+ constructor() {
5
+ // --------
6
+ // Post messaging
7
+ // --------
8
+ this.messaging = {
9
+ post: (message, client = this.client) => {
10
+ if (!client) throw new Error(`No client for this operation.`);
11
+ client.postMessage(message);
12
+ return this.post;
13
+ },
14
+ listen: (callback, client = this.client) => {
15
+ if (!client) {
16
+ self.addEventListener('message', evt => {
17
+ this.client = evt.source;
18
+ callback(evt);
19
+ });
20
+ return this.post;
21
+ }
22
+ client.addEventListener('message', callback);
23
+ return this.post;
24
+ },
25
+ request: (message, client = this.client) => {
26
+ if (!client) throw new Error(`No client for this operation.`);
27
+ return new Promise(res => {
28
+ let messageChannel = new MessageChannel();
29
+ client.postMessage(message, [ messageChannel.port2 ]);
30
+ messageChannel.port1.onmessage = e => res(e.data);
31
+ });
32
+ },
33
+ channel(channelId) {
34
+ if (!this.channels.has(channelId)) { this.channels.set(channelId, new BroadcastChannel(channel)); }
35
+ let channel = this.channels.get(channelId);
36
+ return {
37
+ broadcast: message => channel.postMessage(message),
38
+ listen: callback => channel.addEventListener('message', callback),
39
+ };
40
+ },
41
+ channels: new Map,
42
+ };
43
+
44
+ // --------
45
+ // Notifications
46
+ // --------
47
+ this.notifications = {
48
+ fire: (title, params = {}) => {
49
+ return new Promise((res, rej) => {
50
+ if (!(self.Notification && self.Notification.permission === 'granted')) {
51
+ return rej(self.Notification && self.Notification.permission);
52
+ }
53
+ notification.addEventListener('error', rej);
54
+ let notification = new self.Notification(title, params);
55
+ notification.addEventListener('click', res);
56
+ notification.addEventListener('close', res);
57
+ });
58
+ },
59
+ listen: callback => {
60
+ self.addEventListener('notificationclick', callback);
61
+ return this.notifications;
62
+ },
63
+ };
64
+
65
+ // --------
66
+ // Push Notifications
67
+ // --------
68
+ this.push = {
69
+ listen: callback => {
70
+ self.addEventListener('push', callback);
71
+ return this.post;
72
+ },
73
+ };
74
+ }
75
+
76
+ setCurrentClient(client) {
77
+ this.client = client;
78
+ }
79
+
80
+ }
@@ -307,19 +307,25 @@ export default class Runtime {
307
307
  return this.getSession(_context, httpEvent, id, options, callback);
308
308
  });
309
309
  // Response
310
- let client = this.clients.get('*');
310
+ let client = this.clients.get('*'), response, finalResponse;
311
311
  if (this.cx.server.shared) {
312
312
  client = this.clients.get(url.hostname);
313
313
  }
314
- let response = await client.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
315
- let finalResponse = await this.handleResponse(httpEvent, response, autoHeaders.filter(header => header.type === 'response'));
314
+ try {
315
+ response = await client.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
316
+ finalResponse = await this.handleResponse(_context, httpEvent, response, autoHeaders.filter(header => header.type === 'response'));
317
+ } catch(e) {
318
+ finalResponse = new Response(null, { status: 500, statusText: e.message });
319
+ console.error(e);
320
+ }
316
321
  // Logging
317
322
  if (this.cx.logger) {
318
323
  let log = this.generateLog(httpEvent, finalResponse);
319
324
  this.cx.logger.log(log);
320
325
  }
326
+ // ------------
321
327
  // Return value
322
- return finalResponse;
328
+ return finalResponse;
323
329
  }
324
330
 
325
331
  // Generates request object
@@ -378,7 +384,7 @@ export default class Runtime {
378
384
  }
379
385
 
380
386
  // Handles response object
381
- async handleResponse(e, response, autoHeaders = []) {
387
+ async handleResponse(cx, e, response, autoHeaders = []) {
382
388
  if (!(response instanceof Response)) { response = new Response(response); }
383
389
  Observer.set(this.network, 'remote', false);
384
390
  Observer.set(this.network, 'error', null);
@@ -401,14 +407,22 @@ export default class Runtime {
401
407
  if (response.headers.redirect) {
402
408
  let xRedirectPolicy = e.request.headers.get('X-Redirect-Policy');
403
409
  let xRedirectCode = e.request.headers.get('X-Redirect-Code') || 300;
404
- let isSameOriginRedirect = (new whatwag.URL(response.headers.location, e.url.origin)).origin === e.url.origin;
405
- if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (isSameOriginRedirect && xRedirectPolicy === 'manual-when-same-origin')) {
406
- response.attrs.status = xRedirectCode;
410
+ let destinationUrl = new whatwag.URL(response.headers.location, e.url.origin);
411
+ let isSameOriginRedirect = destinationUrl.origin === e.url.origin;
412
+ let isSameSpaRedirect, sparootsFile = Path.join(cx.CWD, cx.layout.PUBLIC_DIR, 'sparoots.json');
413
+ if (isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-spa' && Fs.existsSync(sparootsFile)) {
414
+ // Longest-first sorting
415
+ let sparoots = _arrFrom(JSON.parse(Fs.readFileSync(sparootsFile))).sort((a, b) => a.length > b.length ? -1 : 1);
416
+ let matchRoot = path => sparoots.reduce((prev, root) => prev || (`${path}/`.startsWith(`${root}/`) && root), null);
417
+ isSameSpaRedirect = matchRoot(destinationUrl.pathname) === matchRoot(e.url.pathname);
418
+ }
419
+ if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (!isSameSpaRedirect && xRedirectPolicy === 'manual-when-cross-spa')) {
407
420
  response.headers.json({
408
421
  'X-Redirect-Code': response.status,
409
422
  'Access-Control-Allow-Origin': '*',
410
423
  'Cache-Control': 'no-store',
411
424
  });
425
+ response.attrs.status = xRedirectCode;
412
426
  }
413
427
  return response;
414
428
  }
@@ -79,10 +79,10 @@ export default class RuntimeClient {
79
79
  pathnameSplit.pop();
80
80
  }
81
81
  const instanceParams = QueryString.stringify({
82
- SOURCE: renderFile,
83
- URL: httpEvent.url.href,
84
- ROOT: this.cx.CWD,
85
- OOHTML_LEVEL: this.cx.server.oohtml_support,
82
+ file: renderFile,
83
+ url: httpEvent.url.href,
84
+ root: this.cx.CWD,
85
+ oohtml_level: this.cx.server.oohtml_support,
86
86
  });
87
87
  const { window } = await import('@webqit/oohtml-ssr/instance.js?' + instanceParams);
88
88
  // --------
@@ -99,10 +99,10 @@ export default class RuntimeClient {
99
99
  env: 'server',
100
100
  }, { update: true });
101
101
  }
102
- window.document.setState({ page: data, url: httpEvent.url }, { update: 'merge' });
102
+ window.document.setState({ data, url: httpEvent.url }, { update: 'merge' });
103
103
  }
104
104
  if (window.document.templates) {
105
- window.document.body.setAttribute('template', 'page/' + httpEvent.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
105
+ window.document.body.setAttribute('template', 'routes/' + httpEvent.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
106
106
  }
107
107
  await new Promise(res => setTimeout(res, 10));
108
108
  return window;
@@ -135,9 +135,9 @@ export const path = {
135
135
  export const urlPattern = (pattern, baseUrl = null) => ({
136
136
  pattern: new URLPattern(pattern, baseUrl),
137
137
  isPattern() {
138
- return Object.keys(this.pattern.keys).some(compName => this.pattern.keys[compName].length);
138
+ return Object.keys(this.pattern.keys || {}).some(compName => this.pattern.keys[compName].length);
139
139
  },
140
- test(...args) { this.pattern.test(...args) },
140
+ test(...args) { return this.pattern.test(...args) },
141
141
  exec(...args) {
142
142
  let components = this.pattern.exec(...args);
143
143
  if (!components) return;
@@ -0,0 +1,9 @@
1
+ {
2
+ "title": "Webflo Test",
3
+ "type": "module",
4
+ "scripts": {
5
+ "generate": "webflo generate::client --recursive --compression --auto-embed",
6
+ "bundle": "cd public && oohtml bundle --recursive --auto-embed=routes",
7
+ "build": "npm run generate && npm run bundle"
8
+ }
9
+ }
@@ -1,3 +1,6 @@
1
1
 
2
+ <template name="page-3">
3
+ <img src="/page-3/logo-130x130.png" exportgroup="logo-130x130.png" />
4
+ </template>
2
5
  <template name="page-4">
3
6
  </template>
@@ -1,3 +1,4 @@
1
1
  {
2
+ "page-3": {},
2
3
  "page-4": {}
3
4
  }
@@ -1,2 +1,2 @@
1
1
  /** @webqit/webflo */
2
- var{start:e}=WebQit.Webflo,s={},r={bundle_filename:"bundle.js",public_base_url:"/",address_bar_synchrony:"standard",oohtml_support:"full",service_worker_support:!0,worker_scope:"/",worker_filename:"worker.js",routing:{scope:"/",subscopes:["/page-2","/page-4/subpage"],passes:1}};e.call({layout:s,params:r});
2
+ var{start:e}=WebQit.Webflo,r={},o={bundle_filename:"bundle.js",public_base_url:"/",spa_routing:!0,oohtml_support:"full",service_worker_support:!0,worker_scope:"/",worker_filename:"worker.js",routing:{root:"/",subroots:["/page-2","/page-4/subpage"],targets:2}};e.call({layout:r,params:o});
Binary file