@webqit/webflo 0.10.3 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +1509 -3
  2. package/bundle.html.json +1665 -0
  3. package/package.json +3 -3
  4. package/src/Context.js +8 -4
  5. package/src/config-pi/deployment/Env.js +2 -2
  6. package/src/config-pi/deployment/Layout.js +2 -2
  7. package/src/config-pi/deployment/Origins.js +2 -2
  8. package/src/config-pi/deployment/Virtualization.js +2 -2
  9. package/src/config-pi/runtime/Client.js +39 -11
  10. package/src/config-pi/runtime/Server.js +26 -10
  11. package/src/config-pi/runtime/client/Worker.js +32 -14
  12. package/src/config-pi/runtime/server/Headers.js +2 -2
  13. package/src/config-pi/runtime/server/Redirects.js +2 -2
  14. package/src/config-pi/static/Manifest.js +2 -2
  15. package/src/config-pi/static/Ssg.js +2 -2
  16. package/src/runtime-pi/Router.js +1 -1
  17. package/src/runtime-pi/client/Runtime.js +116 -62
  18. package/src/runtime-pi/client/RuntimeClient.js +28 -43
  19. package/src/runtime-pi/client/Workport.js +163 -0
  20. package/src/runtime-pi/client/generate.js +285 -77
  21. package/src/runtime-pi/client/{generate.oohtml.js → oohtml/full.js} +0 -0
  22. package/src/runtime-pi/client/oohtml/namespacing.js +7 -0
  23. package/src/runtime-pi/client/oohtml/scripting.js +8 -0
  24. package/src/runtime-pi/client/oohtml/templating.js +8 -0
  25. package/src/runtime-pi/client/worker/Worker.js +58 -24
  26. package/src/runtime-pi/client/worker/Workport.js +80 -0
  27. package/src/runtime-pi/server/Router.js +2 -2
  28. package/src/runtime-pi/server/Runtime.js +30 -11
  29. package/src/runtime-pi/server/RuntimeClient.js +24 -14
  30. package/src/runtime-pi/util.js +2 -2
  31. package/src/webflo.js +7 -9
  32. package/test/site/package.json +9 -0
  33. package/test/site/public/bundle.html +6 -0
  34. package/test/site/public/bundle.html.json +4 -0
  35. package/test/site/public/bundle.js +2 -0
  36. package/test/site/public/bundle.js.gz +0 -0
  37. package/test/site/public/bundle.webflo.js +15 -0
  38. package/test/site/public/bundle.webflo.js.gz +0 -0
  39. package/test/site/public/index.html +30 -0
  40. package/test/site/public/index1.html +35 -0
  41. package/test/site/public/page-2/bundle.html +5 -0
  42. package/test/site/public/page-2/bundle.html.json +1 -0
  43. package/test/site/public/page-2/bundle.js +2 -0
  44. package/test/site/public/page-2/bundle.js.gz +0 -0
  45. package/test/site/public/page-2/index.html +46 -0
  46. package/test/site/public/page-2/logo-130x130.png +0 -0
  47. package/test/site/public/page-2/main.html +3 -0
  48. package/test/site/public/page-3/logo-130x130.png +0 -0
  49. package/test/site/public/page-4/subpage/bundle.html +0 -0
  50. package/test/site/public/page-4/subpage/bundle.html.json +1 -0
  51. package/test/site/public/page-4/subpage/bundle.js +2 -0
  52. package/test/site/public/page-4/subpage/bundle.js.gz +0 -0
  53. package/test/site/public/page-4/subpage/index.html +31 -0
  54. package/test/site/public/sparoots.json +5 -0
  55. package/test/site/public/worker.js +3 -0
  56. package/test/site/public/worker.js.gz +0 -0
  57. package/test/site/server/index.js +16 -0
  58. package/src/Cli.js +0 -131
  59. package/src/Configurator.js +0 -97
  60. package/src/runtime-pi/client/WorkerComm.js +0 -102
@@ -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;
package/src/webflo.js CHANGED
@@ -5,23 +5,21 @@
5
5
  */
6
6
  import Url from 'url';
7
7
  import Path from 'path';
8
- import { read as readJsonFile } from '@webqit/backpack/src/dotfiles/DotJson.js';
9
- import logger from '@webqit/backpack/src/cli/Ui.js';
8
+ import { jsonFile } from '@webqit/backpack/src/dotfile/index.js';
9
+ import { Logger, Cli } from '@webqit/backpack';
10
10
  import * as WebfloPI from './index.js';
11
- import Context from './Context.js';
12
- import Cli from './Cli.js';
13
11
 
14
12
  const dirSelf = Path.dirname(Url.fileURLToPath(import.meta.url));
15
- const webfloJson = readJsonFile(Path.join(dirSelf, '../package.json'));
16
- const appJson = readJsonFile('./package.json');
13
+ const webfloJson = jsonFile.read(Path.join(dirSelf, '../package.json'));
14
+ const appJson = jsonFile.read('./package.json');
17
15
 
18
16
  /**
19
17
  * @cx
20
18
  */
21
- const cx = Context.create({
22
- webflo: { title: webfloJson.title, version: webfloJson.version },
19
+ const cx = WebfloPI.Context.create({
20
+ meta: { title: webfloJson.title, version: webfloJson.version },
23
21
  app: { title: appJson.title, version: appJson.version },
24
- logger,
22
+ logger: Logger,
25
23
  config: WebfloPI.config,
26
24
  middlewares: [ WebfloPI.deployment.origins.webhook, ],
27
25
  });
@@ -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