@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.
Files changed (51) hide show
  1. package/README.md +1490 -52
  2. package/bundle.html.json +1665 -0
  3. package/package.json +2 -2
  4. package/src/Context.js +1 -1
  5. package/src/config-pi/runtime/Client.js +37 -9
  6. package/src/config-pi/runtime/Server.js +24 -8
  7. package/src/config-pi/runtime/client/Worker.js +30 -12
  8. package/src/runtime-pi/Router.js +1 -1
  9. package/src/runtime-pi/client/Runtime.js +116 -62
  10. package/src/runtime-pi/client/RuntimeClient.js +28 -43
  11. package/src/runtime-pi/client/Workport.js +163 -0
  12. package/src/runtime-pi/client/generate.js +282 -74
  13. package/src/runtime-pi/client/{generate.oohtml.js → oohtml/full.js} +0 -0
  14. package/src/runtime-pi/client/oohtml/namespacing.js +7 -0
  15. package/src/runtime-pi/client/oohtml/scripting.js +8 -0
  16. package/src/runtime-pi/client/oohtml/templating.js +8 -0
  17. package/src/runtime-pi/client/worker/Worker.js +58 -24
  18. package/src/runtime-pi/client/worker/Workport.js +80 -0
  19. package/src/runtime-pi/server/Router.js +2 -2
  20. package/src/runtime-pi/server/Runtime.js +30 -11
  21. package/src/runtime-pi/server/RuntimeClient.js +24 -14
  22. package/src/runtime-pi/util.js +2 -2
  23. package/test/site/package.json +9 -0
  24. package/test/site/public/bundle.html +6 -0
  25. package/test/site/public/bundle.html.json +4 -0
  26. package/test/site/public/bundle.js +2 -0
  27. package/test/site/public/bundle.js.gz +0 -0
  28. package/test/site/public/bundle.webflo.js +15 -0
  29. package/test/site/public/bundle.webflo.js.gz +0 -0
  30. package/test/site/public/index.html +30 -0
  31. package/test/site/public/index1.html +35 -0
  32. package/test/site/public/page-2/bundle.html +5 -0
  33. package/test/site/public/page-2/bundle.html.json +1 -0
  34. package/test/site/public/page-2/bundle.js +2 -0
  35. package/test/site/public/page-2/bundle.js.gz +0 -0
  36. package/test/site/public/page-2/index.html +46 -0
  37. package/test/site/public/page-2/logo-130x130.png +0 -0
  38. package/test/site/public/page-2/main.html +3 -0
  39. package/test/site/public/page-3/logo-130x130.png +0 -0
  40. package/test/site/public/page-4/subpage/bundle.html +0 -0
  41. package/test/site/public/page-4/subpage/bundle.html.json +1 -0
  42. package/test/site/public/page-4/subpage/bundle.js +2 -0
  43. package/test/site/public/page-4/subpage/bundle.js.gz +0 -0
  44. package/test/site/public/page-4/subpage/index.html +31 -0
  45. package/test/site/public/sparoots.json +5 -0
  46. package/test/site/public/worker.js +3 -0
  47. package/test/site/public/worker.js.gz +0 -0
  48. package/test/site/server/index.js +16 -0
  49. package/docker/Dockerfile +0 -26
  50. package/docker/README.md +0 -77
  51. package/src/runtime-pi/client/WorkerComm.js +0 -102
@@ -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
  * ---------------------------
@@ -43,7 +44,7 @@ export default class Worker {
43
44
  // Add files to cache
44
45
  evt.waitUntil( self.caches.open(this.cx.params.cache_name).then(cache => {
45
46
  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 && !c.endsWith('/') && !urlPattern(c, self.origin).isPattern());
47
+ const cache_only_urls = (this.cx.params.cache_only_urls || []).map(c => c.trim()).filter(c => c && !urlPattern(c, self.origin).isPattern());
47
48
  return cache.addAll(cache_only_urls);
48
49
  }) );
49
50
  }
@@ -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
+ }
@@ -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
- const type = Mime.lookup(ext);
91
+ let mime = Mime.lookup(ext);
92
92
  response = new httpEvent.Response(data, { headers: {
93
- contentType: type === 'application/javascript' ? 'text/javascript' : type,
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
- if (this.cx.app.title && this.cx.logger) {
249
- this.cx.logger.info(`> Server running (${this.cx.app.title || ''})`);
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
  /**
@@ -302,19 +307,25 @@ export default class Runtime {
302
307
  return this.getSession(_context, httpEvent, id, options, callback);
303
308
  });
304
309
  // Response
305
- let client = this.clients.get('*');
310
+ let client = this.clients.get('*'), response, finalResponse;
306
311
  if (this.cx.server.shared) {
307
312
  client = this.clients.get(url.hostname);
308
313
  }
309
- let response = await client.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
310
- 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
+ }
311
321
  // Logging
312
322
  if (this.cx.logger) {
313
323
  let log = this.generateLog(httpEvent, finalResponse);
314
324
  this.cx.logger.log(log);
315
325
  }
326
+ // ------------
316
327
  // Return value
317
- return finalResponse;
328
+ return finalResponse;
318
329
  }
319
330
 
320
331
  // Generates request object
@@ -373,7 +384,7 @@ export default class Runtime {
373
384
  }
374
385
 
375
386
  // Handles response object
376
- async handleResponse(e, response, autoHeaders = []) {
387
+ async handleResponse(cx, e, response, autoHeaders = []) {
377
388
  if (!(response instanceof Response)) { response = new Response(response); }
378
389
  Observer.set(this.network, 'remote', false);
379
390
  Observer.set(this.network, 'error', null);
@@ -396,14 +407,22 @@ export default class Runtime {
396
407
  if (response.headers.redirect) {
397
408
  let xRedirectPolicy = e.request.headers.get('X-Redirect-Policy');
398
409
  let xRedirectCode = e.request.headers.get('X-Redirect-Code') || 300;
399
- let isSameOriginRedirect = (new whatwag.URL(response.headers.location, e.url.origin)).origin === e.url.origin;
400
- if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (isSameOriginRedirect && xRedirectPolicy === 'manual-when-same-origin')) {
401
- 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')) {
402
420
  response.headers.json({
403
421
  'X-Redirect-Code': response.status,
404
422
  'Access-Control-Allow-Origin': '*',
405
423
  'Cache-Control': 'no-store',
406
424
  });
425
+ response.attrs.status = xRedirectCode;
407
426
  }
408
427
  return response;
409
428
  }
@@ -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 window object or a string response.');
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
  });
@@ -78,22 +79,31 @@ export default class RuntimeClient {
78
79
  pathnameSplit.pop();
79
80
  }
80
81
  const instanceParams = QueryString.stringify({
81
- SOURCE: renderFile,
82
- URL: httpEvent.url.href,
83
- ROOT: this.cx.CWD,
82
+ file: renderFile,
83
+ url: httpEvent.url.href,
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
- await new Promise(res => window.WebQit.DOM.ready(res));
89
- await new Promise(res => (window.document.templatesReadyState === 'complete' && res(), window.document.addEventListener('templatesreadystatechange', res)));
90
- if (!window.document.state.env) {
91
- window.document.setState({
92
- env: 'server',
93
- }, { update: true });
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({ data, url: httpEvent.url }, { update: 'merge' });
103
+ }
104
+ if (window.document.templates) {
105
+ window.document.body.setAttribute('template', 'routes/' + 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
  });
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+
2
+ <template name="page-3">
3
+ <img src="/page-3/logo-130x130.png" exportgroup="logo-130x130.png" />
4
+ </template>
5
+ <template name="page-4">
6
+ </template>
@@ -0,0 +1,4 @@
1
+ {
2
+ "page-3": {},
3
+ "page-4": {}
4
+ }
@@ -0,0 +1,2 @@
1
+ /** @webqit/webflo */
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