@webqit/webflo 0.20.4 → 0.20.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/package.json +9 -15
  2. package/src/build-pi/esbuild-plugin-uselive-transform.js +42 -0
  3. package/src/build-pi/index.js +7 -5
  4. package/src/init-pi/index.js +1 -1
  5. package/src/init-pi/templates/pwa/.gitignore +6 -0
  6. package/src/init-pi/templates/pwa/.webqit/webflo/client.json +15 -0
  7. package/src/init-pi/templates/pwa/.webqit/webflo/layout.json +7 -0
  8. package/src/init-pi/templates/pwa/public/manifest.json +2 -2
  9. package/src/init-pi/templates/web/.gitignore +6 -0
  10. package/src/init-pi/templates/web/.webqit/webflo/client.json +12 -0
  11. package/src/init-pi/templates/web/.webqit/webflo/layout.json +7 -0
  12. package/src/runtime-pi/WebfloRuntime.js +23 -14
  13. package/src/runtime-pi/apis.js +1 -1
  14. package/src/runtime-pi/webflo-client/DeviceCapabilities.js +1 -1
  15. package/src/runtime-pi/webflo-client/WebfloClient.js +3 -2
  16. package/src/runtime-pi/webflo-client/WebfloRootClient1.js +8 -4
  17. package/src/runtime-pi/webflo-client/WebfloRootClient2.js +1 -1
  18. package/src/runtime-pi/webflo-client/WebfloSubClient.js +1 -1
  19. package/src/runtime-pi/webflo-client/bootstrap.js +1 -0
  20. package/src/runtime-pi/webflo-fetch/LiveResponse.js +34 -27
  21. package/src/runtime-pi/webflo-fetch/index.js +15 -14
  22. package/src/runtime-pi/webflo-messaging/wq-message-port.js +1 -1
  23. package/src/runtime-pi/webflo-routing/HttpEvent.js +8 -6
  24. package/src/runtime-pi/webflo-routing/WebfloRouter.js +12 -7
  25. package/src/runtime-pi/webflo-server/WebfloServer.js +57 -22
  26. package/src/runtime-pi/webflo-server/webflo-devmode.js +11 -7
  27. package/src/runtime-pi/webflo-url/Url.js +1 -1
  28. package/src/runtime-pi/webflo-url/xURL.js +1 -1
  29. package/src/runtime-pi/webflo-worker/bootstrap.js +1 -0
  30. package/src/build-pi/esbuild-plugin-livejs-transform.js +0 -35
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "vanila-javascript"
13
13
  ],
14
14
  "homepage": "https://webqit.io/tooling/webflo",
15
- "version": "0.20.4",
15
+ "version": "0.20.5",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
@@ -42,25 +42,15 @@
42
42
  "webflo-certbot-http-cleanup-hook": "src/services-pi/cert/http-cleanup-hook.js"
43
43
  },
44
44
  "dependencies": {
45
- "@linked-db/linked-ql": "^0.30.13",
46
45
  "@octokit/webhooks": "^7.15.1",
47
46
  "@webqit/backpack": "^0.1.12",
48
- "@webqit/oohtml-ssr": "^2.1.1",
49
- "@webqit/quantum-js": "^4.6.3",
47
+ "@webqit/oohtml-ssr": "^2.2.1",
48
+ "@webqit/use-live": "^0.5.41",
50
49
  "@webqit/util": "^0.8.11",
51
50
  "dotenv": "^16.4.7",
52
- "fs-extra": "^11.3.0",
53
- "i": "^0.3.7",
54
- "ioredis": "^5.5.0",
55
- "jsdom": "^21.1.1",
56
- "markdown-it-mathjax3": "^4.3.2",
57
51
  "mime-types": "^2.1.33",
58
- "npm": "^11.4.0",
59
- "pg": "^8.13.3",
60
52
  "simple-git": "^2.20.1",
61
- "urlpattern-polyfill": "^4.0.3",
62
- "vitepress-plugin-mermaid": "^2.0.17",
63
- "web-push": "^3.6.7"
53
+ "urlpattern-polyfill": "^4.0.3"
64
54
  },
65
55
  "devDependencies": {
66
56
  "chai": "^4.3.6",
@@ -68,9 +58,13 @@
68
58
  "coveralls": "^3.1.1",
69
59
  "esbuild": "^0.14.38",
70
60
  "fast-glob": "^3.3.3",
61
+ "jsdom": "^27.0.1",
62
+ "markdown-it-mathjax3": "^4.3.2",
71
63
  "mocha": "^10.0.0",
72
64
  "mocha-lcov-reporter": "^1.3.0",
73
- "vitepress": "^1.6.4"
65
+ "vitepress": "^1.6.4",
66
+ "vitepress-plugin-mermaid": "^2.0.17",
67
+ "web-push": "^3.6.7"
74
68
  },
75
69
  "author": "Oxford Harrison <oxharris.dev@gmail.com>",
76
70
  "maintainers": [
@@ -0,0 +1,42 @@
1
+ import Fs from 'fs/promises';
2
+ import { parse, compile, matchPrologDirective, serialize } from '@webqit/use-live';
3
+
4
+ export function UseLiveTransform() {
5
+ return {
6
+ name: 'uselive-transform',
7
+ setup(build) {
8
+ build.onLoad({ filter: /\.(js|mjs|ts|jsx|tsx)$/ }, async (args) => {
9
+ const code = await Fs.readFile(args.path, 'utf8');
10
+
11
+ // Super dirty detection
12
+ if (matchPrologDirective(code)) {
13
+ // Actual check...
14
+
15
+ let ast;
16
+ try { ast = parse(code, parserParams); } catch (e) { console.error(args.path, '\nUseLive transform error:', e); }
17
+
18
+ if (ast?.isLiveProgram || ast?.hasLiveFunctions) {
19
+ const result = await compile(parserParams.sourceType+'-file', ast, {
20
+ liveMode: ast.isLiveProgram, // Regarding top-level
21
+ fileName: args.path,
22
+ });
23
+ return { contents: serialize(result), loader: 'js' };
24
+ }
25
+ }
26
+
27
+ return { contents: code, loader: 'default' };
28
+ });
29
+ }
30
+ };
31
+ }
32
+
33
+ export const parserParams = {
34
+ ecmaVersion: 'latest',
35
+ sourceType: 'module',
36
+ executionMode: 'RegularProgram', // 'LiveProgram'
37
+ allowReturnOutsideFunction: true,
38
+ allowAwaitOutsideFunction: true,
39
+ allowSuperOutsideMethod: false,
40
+ preserveParens: false,
41
+ locations: true,
42
+ };
@@ -10,7 +10,7 @@ import { jsFile } from '@webqit/backpack/src/dotfile/index.js';
10
10
  import { bootstrap as serverBootstrap } from '../runtime-pi/webflo-server/bootstrap.js';
11
11
  import { bootstrap as clientBootstrap } from '../runtime-pi/webflo-client/bootstrap.js';
12
12
  import { bootstrap as workerBootstrap } from '../runtime-pi/webflo-worker/bootstrap.js';
13
- import { LiveJSTransform } from './esbuild-plugin-livejs-transform.js';
13
+ import { UseLiveTransform } from './esbuild-plugin-uselive-transform.js';
14
14
  import { CLIContext } from '../CLIContext.js';
15
15
  import '../runtime-pi/webflo-url/urlpattern.js';
16
16
 
@@ -145,14 +145,14 @@ async function bundleScript({ $context, $source, which, outfile, asModule = true
145
145
  const bundlingConfig = {
146
146
  entryPoints: [moduleFile],
147
147
  outfile,
148
- bundle: which === 'server' ? false : true,
149
- minify: true,
150
148
  format: asModule ? 'esm' : 'iife',
151
149
  platform: which === 'server' ? 'node' : 'browser', // optional but good for clarity
150
+ bundle: which === 'server' ? false : true,
151
+ minify: which === 'server' ? false : true,
152
152
  treeShaking: true, // Important optimization
153
153
  banner: { js: '/** @webqit/webflo */', },
154
154
  footer: { js: '', },
155
- plugins: [ LiveJSTransform() ],
155
+ plugins: [UseLiveTransform()],
156
156
  ...(restParams.buildParams || {})
157
157
  };
158
158
  if (!asModule) {
@@ -290,8 +290,10 @@ async function generateClientScript({ $context, bootstrap, ...restParams }) {
290
290
 
291
291
  const configExport = structuredClone({ ENV: bootstrap.config.ENV, CLIENT: bootstrap.config.CLIENT, WORKER: {} });
292
292
  if (bootstrap.config.CLIENT.capabilities?.service_worker === true) {
293
+ const outfile_workerBuild = Path.join(FLAGS.outdir || bootstrap.outdir, bootstrap.config.WORKER.filename);
294
+ const outfile_workerBuildPublic = Path.join(publicBaseUrl, Path.relative(bootstrap.config.LAYOUT.PUBLIC_DIR, outfile_workerBuild));
293
295
  configExport.WORKER = {
294
- filename: Path.join(publicBaseUrl.replace(/^\//, ''), bootstrap.config.WORKER.filename),
296
+ filename: outfile_workerBuildPublic,
295
297
  scope: bootstrap.config.WORKER.scope
296
298
  };
297
299
  }
@@ -40,7 +40,7 @@ export async function init(projectName = 'my-webflo-app', projectTitle = '', pro
40
40
  process.exit(1);
41
41
  }
42
42
 
43
- LOGGER?.log(LOGGER.style.keyword(`> `) + `Initializing your webflo app: "${projectName}" using template "${template}"...\n`);
43
+ LOGGER?.log(LOGGER.style.keyword(`> `) + `Initializing your webflo app "${projectName}" using template "${template}"...\n`);
44
44
 
45
45
  // 1. Create project dir
46
46
  await Fs2.mkdir(targetDir, { recursive: true });
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ .*
3
+ !/.github
4
+ !/.gitignore
5
+ !/.webqit
6
+ /.webqit/webflo/@runtime
@@ -0,0 +1,15 @@
1
+ {
2
+ "filename": "app.js",
3
+ "public_base_url": "/",
4
+ "copy_public_variables": true,
5
+ "spa_routing": true,
6
+ "capabilities": {
7
+ "service_worker": true,
8
+ "webpush": true,
9
+ "custom_install": true,
10
+ "exposed": [
11
+ "display-mode",
12
+ "notifications"
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "PUBLIC_DIR": "./public",
3
+ "VIEWS_DIR": "./app",
4
+ "SERVER_DIR": "./app",
5
+ "CLIENT_DIR": "./app",
6
+ "WORKER_DIR": "./app"
7
+ }
@@ -10,12 +10,12 @@
10
10
  "scope": "/",
11
11
  "icons": [
12
12
  {
13
- "src": "/assets/...",
13
+ "src": "/assets/logo.png",
14
14
  "sizes": "192x192",
15
15
  "type": "image/png"
16
16
  },
17
17
  {
18
- "src": "/assets/...",
18
+ "src": "/assets/logo.png",
19
19
  "sizes": "512x512",
20
20
  "type": "image/png",
21
21
  "purpose": "maskable"
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ .*
3
+ !/.github
4
+ !/.gitignore
5
+ !/.webqit
6
+ /.webqit/webflo/@runtime
@@ -0,0 +1,12 @@
1
+ {
2
+ "filename": "app.js",
3
+ "public_base_url": "/",
4
+ "copy_public_variables": true,
5
+ "spa_routing": true,
6
+ "capabilities": {
7
+ "service_worker": false,
8
+ "webpush": false,
9
+ "custom_install": false,
10
+ "exposed": []
11
+ }
12
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "PUBLIC_DIR": "./public",
3
+ "VIEWS_DIR": "./app",
4
+ "SERVER_DIR": "./app",
5
+ "CLIENT_DIR": "./app",
6
+ "WORKER_DIR": "./app"
7
+ }
@@ -1,5 +1,6 @@
1
1
  import { WebfloRouter } from './webflo-routing/WebfloRouter.js';
2
- import { LiveResponse, response as responseShim, headers as headersShim } from './webflo-fetch/index.js';
2
+ import { response as responseShim, headers as headersShim } from './webflo-fetch/index.js';
3
+ import { LiveResponse } from './webflo-fetch/LiveResponse.js';
3
4
  import { AppBootstrap } from './AppBootstrap.js';
4
5
  import { _wq } from '../util.js';
5
6
 
@@ -31,6 +32,14 @@ export class WebfloRuntime {
31
32
  }
32
33
 
33
34
  async initialize() {
35
+ if (this.bootstrap.init.SETUP) {
36
+ await this.bootstrap.init.SETUP(this);
37
+ }
38
+ await this.initCreateStorage();
39
+ return this.#instanceController;
40
+ }
41
+
42
+ async initCreateStorage() {
34
43
  if (!this.bootstrap.init.createStorage) {
35
44
  const inmemSessionRegistry = new Map;
36
45
  this.bootstrap.init.createStorage = (namespace) => {
@@ -55,8 +64,8 @@ export class WebfloRuntime {
55
64
  return this.#instanceController;
56
65
  }
57
66
 
58
- async createStorage(namespace, ttl) {
59
- return await this.bootstrap.init.createStorage(namespace, ttl);
67
+ createStorage(namespace, ttl) {
68
+ return this.bootstrap.init.createStorage(namespace, ttl);
60
69
  }
61
70
 
62
71
  createRequest(href, init = {}) {
@@ -90,7 +99,7 @@ export class WebfloRuntime {
90
99
  }
91
100
  // Dispatch event
92
101
  const router = new this.constructor.Router(this, httpEvent.url.pathname);
93
- await router.route(['SETUP'], httpEvent);
102
+ await router.route(['SETUP'], httpEvent.extend(false));
94
103
  // Do proper routing for respone
95
104
  const response = await new Promise(async (resolve) => {
96
105
  let autoLiveResponse, response;
@@ -112,10 +121,11 @@ export class WebfloRuntime {
112
121
  console.error(e);
113
122
  response = new Response(null, { status: 500, statusText: e.message });
114
123
  }
115
- if (!(response instanceof LiveResponse) && !(response instanceof Response)) {
116
- response = LiveResponse.test(response) === 'Default'
117
- ? responseShim.from.value(response)
118
- : await LiveResponse.from(response, { responsesOK: true });
124
+ if (!/Response/.test(LiveResponse.test(response))) {
125
+ const isLifecyleComplete = httpEvent.lifeCycleComplete();
126
+ response = LiveResponse.test(response) !== 'Default' || !isLifecyleComplete
127
+ ? await LiveResponse.from(response, { done: isLifecyleComplete })
128
+ : responseShim.from.value(response);
119
129
  }
120
130
  // Any "carry" data?
121
131
  //await this.handleCarries(httpEvent, response);
@@ -126,15 +136,14 @@ export class WebfloRuntime {
126
136
  resolve(response);
127
137
  }
128
138
  });
139
+
129
140
  // Commit data in the exact order. Reason: in how they depend on each other
130
141
  for (const storage of [httpEvent.user, httpEvent.session, httpEvent.cookies]) {
131
142
  await storage?.commit?.(response, FLAGS['dev']);
132
143
  }
133
- if (response instanceof LiveResponse && response.whileLive()) {
144
+ // Wait for any whileLive promises to resolve
145
+ if (LiveResponse.test(response) === 'LiveResponse' && response.whileLive()) {
134
146
  httpEvent.waitUntil(response.whileLive(true));
135
- } else {
136
- httpEvent.waitUntil(Promise.resolve());
137
- await null; // We need the above resolved before we move on
138
147
  }
139
148
 
140
149
  // Send the X-Background-Messaging-Port header
@@ -173,9 +182,9 @@ export class WebfloRuntime {
173
182
  });
174
183
  }
175
184
 
176
- if (!this.isClientSide && response instanceof LiveResponse) {
185
+ if (!this.isClientSide && LiveResponse.test(response) === 'LiveResponse') {
177
186
  // Must convert to Response on the server-side before returning
178
- return response.toResponse({ client: httpEvent.client });
187
+ return await response.toResponse({ client: httpEvent.client });
179
188
  }
180
189
 
181
190
  return response;
@@ -1,5 +1,5 @@
1
1
  import { LiveResponse } from './webflo-fetch/LiveResponse.js';
2
- import { Observer } from '@webqit/quantum-js';
2
+ import { Observer } from '@webqit/use-live';
3
3
  import { shim } from './webflo-fetch/index.js';
4
4
 
5
5
  export {
@@ -1,4 +1,4 @@
1
- import { Observer } from '@webqit/quantum-js';
1
+ import { Observer } from '@webqit/use-live';
2
2
 
3
3
  export class DeviceCapabilities {
4
4
 
@@ -1,9 +1,10 @@
1
1
  import { _before, _toTitle } from '@webqit/util/str/index.js';
2
2
  import { _isObject } from '@webqit/util/js/index.js';
3
- import { Observer } from '@webqit/quantum-js';
3
+ import { Observer } from '@webqit/use-live';
4
4
  import { WebfloRuntime } from '../WebfloRuntime.js';
5
5
  import { WQMessageChannel } from '../webflo-messaging/WQMessageChannel.js';
6
- import { LiveResponse, response as responseShim } from '../webflo-fetch/index.js';
6
+ import { response as responseShim } from '../webflo-fetch/index.js';
7
+ import { LiveResponse } from '../webflo-fetch/LiveResponse.js';
7
8
  import { WQStarPort } from '../webflo-messaging/WQStarPort.js';
8
9
  import { ClientSideCookies } from './ClientSideCookies.js';
9
10
  import { HttpSession } from '../webflo-routing/HttpSession.js';
@@ -1,8 +1,9 @@
1
- import { Observer } from '@webqit/quantum-js';
1
+ import { Observer } from '@webqit/use-live';
2
2
  import { WebfloClient } from './WebfloClient.js';
3
3
  import { ClientSideWorkport } from './ClientSideWorkport.js';
4
4
  import { DeviceCapabilities } from './DeviceCapabilities.js';
5
- import { LiveResponse, response as responseShim } from '../webflo-fetch/index.js';
5
+ import { response as responseShim } from '../webflo-fetch/index.js';
6
+ import { LiveResponse } from '../webflo-fetch/LiveResponse.js';
6
7
  import { WebfloHMR } from './webflo-devmode.js';
7
8
 
8
9
  export class WebfloRootClient1 extends WebfloClient {
@@ -92,8 +93,10 @@ export class WebfloRootClient1 extends WebfloClient {
92
93
  cleanups.push(() => this.#capabilities.close());
93
94
  if (this.config.CLIENT.capabilities?.service_worker) {
94
95
  const { filename, ...restServiceWorkerParams } = this.config.WORKER;
95
- this.#workport = await this.constructor.Workport.initialize(null, (this.config.CLIENT.public_base_url || '') + filename, restServiceWorkerParams);
96
- cleanups.push(() => this.#workport.close());
96
+ this.constructor.Workport.initialize(null, filename, restServiceWorkerParams).then((workport) => {
97
+ this.#workport = workport;
98
+ cleanups.push(() => this.#workport.close());
99
+ });
97
100
  }
98
101
  return instanceController;
99
102
  }
@@ -113,6 +116,7 @@ export class WebfloRootClient1 extends WebfloClient {
113
116
  this.background.addPort(backgroundPort);
114
117
  }
115
118
  if (scopeObj.response.body || backgroundPort) {
119
+
116
120
  const httpEvent = this.createHttpEvent({ request: this.createRequest(this.location.href) }, true);
117
121
  await this.render(httpEvent, scopeObj.response);
118
122
  } else {
@@ -1,4 +1,4 @@
1
- import { Observer } from '@webqit/quantum-js';
1
+ import { Observer } from '@webqit/use-live';
2
2
  import { WebfloRootClient1 } from './WebfloRootClient1.js';
3
3
 
4
4
  export class WebfloRootClient2 extends WebfloRootClient1 {
@@ -1,4 +1,4 @@
1
- import { Observer } from '@webqit/quantum-js';
1
+ import { Observer } from '@webqit/use-live';
2
2
  import { WebfloClient } from './WebfloClient.js';
3
3
  import { defineElement } from './webflo-embedded.js';
4
4
  import { Url } from '../webflo-url/Url.js';
@@ -21,6 +21,7 @@ export async function bootstrap(cx, offset = '') {
21
21
  };
22
22
  if (config.CLIENT.copy_public_variables) {
23
23
  const publicEnvPattern = /(?:^|_)PUBLIC(?:_|$)/;
24
+ config.ENV.data = config.ENV.data || {};
24
25
  for (const key in process.env) {
25
26
  if (publicEnvPattern.test(key)) {
26
27
  config.ENV.data[key] = process.env[key];
@@ -1,4 +1,4 @@
1
- import { State, Observer } from '@webqit/quantum-js';
1
+ import { Observer, LiveMode } from '@webqit/use-live';
2
2
  import { _isObject, _isTypeObject } from '@webqit/util/js/index.js';
3
3
  import { publishMutations, applyMutations } from '../webflo-messaging/wq-message-port.js';
4
4
  import { WQBroadcastChannel } from '../webflo-messaging/WQBroadcastChannel.js';
@@ -9,8 +9,11 @@ import { isTypeStream } from './util.js';
9
9
 
10
10
  export class LiveResponse extends EventTarget {
11
11
 
12
+ [Symbol.toStringTag] = 'LiveResponse';
13
+
12
14
  static test(data) {
13
- if (data instanceof LiveResponse) {
15
+ if (data instanceof LiveResponse
16
+ || data?.[Symbol.toStringTag] === 'LiveResponse') {
14
17
  return 'LiveResponse';
15
18
  }
16
19
  if (data instanceof Response) {
@@ -19,24 +22,25 @@ export class LiveResponse extends EventTarget {
19
22
  if (isGenerator(data)) {
20
23
  return 'Generator';
21
24
  }
22
- if (data instanceof State) {
23
- return 'Quantum';
25
+ if (data instanceof LiveMode
26
+ || data?.[Symbol.toStringTag] === 'LiveMode') {
27
+ return 'LiveMode';
24
28
  }
25
29
  return 'Default';
26
30
  }
27
31
 
28
32
  static async from(data, ...args) {
29
- if (data instanceof LiveResponse) {
33
+ if (this.test(data) === 'LiveResponse') {
30
34
  return data.clone(...args);
31
35
  }
32
- if (data instanceof Response) {
36
+ if (this.test(data) === 'Response') {
33
37
  return await this.fromResponse(data, ...args);
34
38
  }
35
- if (isGenerator(data)) {
39
+ if (this.test(data) === 'Generator') {
36
40
  return await this.fromGenerator(data, ...args);
37
41
  }
38
- if (data instanceof State) {
39
- return this.fromQuantum(data, ...args);
42
+ if (this.test(data) === 'LiveMode') {
43
+ return this.fromLiveMode(data, ...args);
40
44
  }
41
45
  return new this(data, ...args);
42
46
  }
@@ -45,7 +49,7 @@ export class LiveResponse extends EventTarget {
45
49
  if (!(response instanceof Response)) {
46
50
  throw new Error('Argument must be a Response instance.');
47
51
  }
48
-
52
+
49
53
  const body = await responseShim.prototype.parse.value.call(response);
50
54
 
51
55
  // Instance
@@ -108,7 +112,7 @@ export class LiveResponse extends EventTarget {
108
112
  let $$await;
109
113
 
110
114
  const $options = { done: firstFrame.done, ...options };
111
- if (value instanceof LiveResponse) {
115
+ if (this.test(value) === 'LiveResponse') {
112
116
  instance = new this;
113
117
  const responseMeta = _wq(value, 'meta');
114
118
  _wq(instance).set('meta', responseMeta);
@@ -133,15 +137,18 @@ export class LiveResponse extends EventTarget {
133
137
  return instance;
134
138
  }
135
139
 
136
- static async fromQuantum(qState, options = {}) {
137
- if (!(qState instanceof State)) {
138
- throw new Error('Argument must be a Quantum State instance.');
140
+ static async fromLiveMode(liveMode, options = {}) {
141
+ if (!this.test(liveMode) === 'LiveMode') {
142
+ throw new Error('Argument must be a UseLive LiveMode instance.');
143
+ }
144
+ const instance = new this;
145
+ await instance.replaceWith(liveMode.value, { done: false, ...options });
146
+ if (instance.#generatorType === 'Default') {
147
+ instance.#generator = liveMode;
148
+ instance.#generatorType = 'LiveMode';
139
149
  }
140
- const instance = new this(qState.value, { done: false, ...options });
141
- instance.#generator = qState;
142
- instance.#generatorType = 'Quantum';
143
150
  Observer.observe(
144
- qState,
151
+ liveMode,
145
152
  'value',
146
153
  (e) => instance.#replaceWith(e.value),
147
154
  { signal: instance.#abortController.signal }
@@ -156,8 +163,7 @@ export class LiveResponse extends EventTarget {
156
163
  }
157
164
 
158
165
  static getBackground(respone) {
159
- if (!(respone instanceof Response)
160
- && !(respone instanceof LiveResponse)) return;
166
+ if (!/Response/.test(this.test(respone) )) return;
161
167
  const responseMeta = _wq(respone, 'meta');
162
168
  if (!responseMeta.has('background_port')) {
163
169
  const value = respone.headers.get('X-Background-Messaging-Port')?.trim();
@@ -322,8 +328,9 @@ export class LiveResponse extends EventTarget {
322
328
  const execReplaceWithResponse = async (response, options) => {
323
329
  this.#generator = response;
324
330
  this.#generatorType = response instanceof Response ? 'Response' : 'LiveResponse';
331
+ const body = response instanceof Response ? await responseShim.prototype.parse.value.call(response) : response.body;
325
332
  execReplaceWith({
326
- body: response instanceof Response ? await responseShim.prototype.parse.value.call(response) : response.body,
333
+ body,
327
334
  status: responseShim.prototype.status.get.call(response),
328
335
  statusText: response.statusText,
329
336
  headers: response.headers,
@@ -332,7 +339,7 @@ export class LiveResponse extends EventTarget {
332
339
  redirected: response.redirected,
333
340
  url: response.url,
334
341
  });
335
- if (response instanceof LiveResponse) {
342
+ if (this.test(response) === 'LiveResponse') {
336
343
  response.addEventListener('replace', () => execReplaceWith(response), { signal: this.#abortController.signal });
337
344
  return await response.whileLive(true);
338
345
  }
@@ -360,7 +367,7 @@ export class LiveResponse extends EventTarget {
360
367
  };
361
368
 
362
369
  let donePromise;
363
- if (body instanceof Response || body instanceof LiveResponse) {
370
+ if (/Response/.test(body)) {
364
371
  if (frameClosure) {
365
372
  throw new Error('frameClosure unsupported for inputs of type response.');
366
373
  }
@@ -439,8 +446,8 @@ export class LiveResponse extends EventTarget {
439
446
  }));
440
447
  }
441
448
 
442
- toQuantum({ signal: abortSignal } = {}) {
443
- const state = new StateX;
449
+ toLiveMode({ signal: abortSignal } = {}) {
450
+ const state = new LiveModeX;
444
451
  const replaceHandler = () => Observer.defineProperty(state, 'value', { value: this.body, enumerable: true, configurable: true });
445
452
  this.addEventListener('replace', replaceHandler, { signal: abortSignal });
446
453
  replaceHandler();
@@ -462,7 +469,7 @@ export const isGenerator = (obj) => {
462
469
  typeof obj?.return === 'function';
463
470
  };
464
471
 
465
- class StateX extends State {
472
+ class LiveModeX extends LiveMode {
466
473
  constructor() { }
467
- dispose() { }
474
+ abort() { }
468
475
  }
@@ -1,10 +1,11 @@
1
- export { LiveResponse } from './LiveResponse.js';
2
1
  import { _isObject, _isTypeObject, _isNumeric } from '@webqit/util/js/index.js';
3
2
  import { _from as _arrFrom } from '@webqit/util/arr/index.js';
4
3
  import { _before, _after } from '@webqit/util/str/index.js';
5
4
  import { DeepURLSearchParams } from '../webflo-url/util.js';
6
5
  import { dataType } from './util.js';
7
6
  import { _wq } from '../../util.js';
7
+ import { Observer } from '@webqit/use-live';
8
+ import { LiveResponse } from './LiveResponse.js';
8
9
 
9
10
  // ----- env & globalize
10
11
 
@@ -30,6 +31,8 @@ export function shim(prefix = 'wq') {
30
31
  patch(api.prototype, prototype);
31
32
  }
32
33
  }
34
+ globalThis.LiveResponse = LiveResponse;
35
+ globalThis.Observer = Observer;
33
36
  }
34
37
 
35
38
  // ----- request
@@ -336,23 +339,23 @@ export function renderHttpMessageInit(httpMessageInit) {
336
339
  return { ..._headers, [name.toLowerCase()]: httpMessageInit.headers[name] };
337
340
  }, {});
338
341
  // Process body
339
- let body = httpMessageInit.body, type = dataType(httpMessageInit.body);
342
+ let body = httpMessageInit.body,
343
+ type = dataType(httpMessageInit.body);
344
+
340
345
  if (['Blob', 'File'].includes(type)) {
341
346
  !headers['content-type'] && (headers['content-type'] = body.type);
342
347
  !headers['content-length'] && (headers['content-length'] = body.size);
343
348
  } else if (['Uint8Array', 'Uint16Array', 'Uint32Array', 'ArrayBuffer'].includes(type)) {
344
349
  !headers['content-length'] && (headers['content-length'] = body.byteLength);
345
350
  } else if (type === 'json' && _isTypeObject(body)/*JSON object*/) {
346
- if (!headers['content-type']) {
347
- const [_body, isJsonfiable] = createFormDataFromJson(body, true/*jsonfy*/, true/*getIsJsonfiable*/);
348
- if (isJsonfiable) {
349
- body = JSON.stringify(body, (k, v) => v instanceof Error ? { ...v, message: v.message } : v);
350
- headers['content-type'] = 'application/json';
351
- headers['content-length'] = (new Blob([body])).size;
352
- } else {
353
- body = _body;
354
- type = 'FormData';
355
- }
351
+ const [_body, isJsonfiable] = createFormDataFromJson(body, true/*jsonfy*/, true/*getIsJsonfiable*/);
352
+ if (isJsonfiable) {
353
+ body = JSON.stringify(body, (k, v) => v instanceof Error ? { ...v, message: v.message } : v);
354
+ headers['content-type'] = 'application/json';
355
+ headers['content-length'] = (new Blob([body])).size;
356
+ } else {
357
+ body = _body;
358
+ type = 'FormData';
356
359
  }
357
360
  } else if (type === 'json'/*JSON string*/ && !headers['content-length']) {
358
361
  (headers['content-length'] = (body + '').length);
@@ -429,7 +432,5 @@ export function renderCookieObjToString(cookieObj) {
429
432
 
430
433
  const importUrl = new URL(import.meta.url);
431
434
  if (importUrl.searchParams.has('shim')) {
432
- globalThis.LiveResponse = LiveResponse;
433
435
  shim(importUrl.searchParams.get('shim')?.trim());
434
- console.log('Webflo Fetch APIs shimmed.');
435
436
  }
@@ -2,7 +2,7 @@ import { _isObject, _isTypeObject } from '@webqit/util/js/index.js';
2
2
  import { WQMessagePort, WQMessagePortInstanceTag } from './WQMessagePort.js';
3
3
  import { isTypeStream } from '../webflo-fetch/util.js';
4
4
  import { WQMessageEvent } from './WQMessageEvent.js';
5
- import { Observer } from '@webqit/quantum-js';
5
+ import { Observer } from '@webqit/use-live';
6
6
  import { _wq } from '../../util.js';
7
7
 
8
8
  /**
@@ -1,6 +1,6 @@
1
1
  import { _isObject } from '@webqit/util/js/index.js';
2
2
  import { _difference } from '@webqit/util/arr/index.js';
3
- import { LiveResponse } from '../webflo-fetch/index.js';
3
+ import { LiveResponse } from '../webflo-fetch/LiveResponse.js';
4
4
  import { xURL } from '../webflo-url/xURL.js';
5
5
  import { _wq } from '../../util.js';
6
6
 
@@ -45,6 +45,8 @@ export class HttpEvent {
45
45
  get state() { return { ...(this.#init.state || {}) }; }
46
46
 
47
47
  #lifecyclePromises = new Set;
48
+ get lifecyclePromises() { return this.#lifecyclePromises; }
49
+
48
50
  #lifeCycleResolve;
49
51
  #lifeCycleReject;
50
52
  #lifeCycleResolutionPromise = new Promise((resolve, reject) => {
@@ -80,12 +82,12 @@ export class HttpEvent {
80
82
  && !this.#lifecyclePromises.size;
81
83
  }
82
84
 
83
- waitUntil(promise) {
84
- return this.#extendLifecycle(promise);
85
+ async waitUntil(promise) {
86
+ return await this.#extendLifecycle(promise);
85
87
  }
86
88
 
87
89
  waitUntilNavigate() {
88
- return this.waitUntil(new Promise(() => { }));
90
+ this.waitUntil(new Promise(() => { }));
89
91
  }
90
92
 
91
93
  #internalLiveResponse = new LiveResponse(null, { done: false });
@@ -100,8 +102,8 @@ export class HttpEvent {
100
102
  }
101
103
 
102
104
  extend(init = {}) {
103
- const instance = this.constructor.create(this/*Main difference from clone*/, { ...this.#init, ...init });
104
- this.#extendLifecycle(instance.lifeCycleComplete(true));
105
+ const instance = this.constructor.create(this/*Main difference from clone*/, { ...this.#init, ...(init || {}) });
106
+ if (init !== false) this.#extendLifecycle(instance.lifeCycleComplete(true));
105
107
  return instance;
106
108
  }
107
109
 
@@ -1,8 +1,7 @@
1
- import { State } from '@webqit/quantum-js';
2
1
  import { _isFunction, _isArray, _isObject } from '@webqit/util/js/index.js';
3
2
  import { _from as _arrFrom } from '@webqit/util/arr/index.js';
3
+ import { LiveResponse } from '../webflo-fetch/LiveResponse.js';
4
4
  import { path as Path } from '../webflo-url/util.js';
5
- import { isGenerator } from '../webflo-fetch/LiveResponse.js';
6
5
 
7
6
  export class WebfloRouter {
8
7
 
@@ -170,11 +169,11 @@ export class WebfloRouter {
170
169
  const returnValue = await handler.call(thisContext, thisTick.event, $next/*next*/, $fetch/*fetch*/);
171
170
 
172
171
  // Handle cleanup on abort
173
- if (returnValue instanceof State) {
172
+ if (LiveResponse.test(returnValue) === 'LiveMode') {
174
173
  thisTick.event.signal.addEventListener('abort', () => {
175
- returnValue.dispose();
174
+ returnValue.abort();
176
175
  });
177
- } else if (isGenerator(returnValue)) {
176
+ } else if (LiveResponse.test(returnValue) === 'Generator') {
178
177
  thisTick.event.signal.addEventListener('abort', () => {
179
178
  if (typeof returnValue.return === 'function') {
180
179
  returnValue.return();
@@ -187,13 +186,19 @@ export class WebfloRouter {
187
186
  resolved = 2;
188
187
  resolve(returnValue);
189
188
  } else if (typeof returnValue !== 'undefined') {
190
- await thisTick.event.internalLiveResponse.replaceWith(returnValue, { done: true });
189
+ thisTick.event.internalLiveResponse.replaceWith(returnValue, { done: true });
191
190
  }
192
191
  });
193
192
  }
193
+ let returnValue;
194
194
  if (_default) {
195
- return await _default.call(thisContext, thisTick.event, remoteFetch);
195
+ returnValue = await _default.call(thisContext, thisTick.event, remoteFetch);
196
196
  }
197
+ try {
198
+ // IMPORTANT: Explicitly terminate the event lifecycle if nothing extends it
199
+ await thisTick.event.waitUntil();
200
+ } catch(e) {}
201
+ return returnValue;
197
202
  };
198
203
 
199
204
  return next({
@@ -23,7 +23,7 @@ import { ServerSideSession } from './ServerSideSession.js';
23
23
  import { HttpEvent } from '../webflo-routing/HttpEvent.js';
24
24
  import { HttpUser } from '../webflo-routing/HttpUser.js';
25
25
  import { response as responseShim, headers as headersShim } from '../webflo-fetch/index.js';
26
- import { LiveJSTransform } from '../../build-pi/esbuild-plugin-livejs-transform.js';
26
+ import { UseLiveTransform } from '../../build-pi/esbuild-plugin-uselive-transform.js';
27
27
  import { createWindow } from '@webqit/oohtml-ssr';
28
28
  import { _wq } from '../../util.js';
29
29
  import '../webflo-fetch/index.js';
@@ -57,7 +57,6 @@ export class WebfloServer extends WebfloRuntime {
57
57
  }
58
58
 
59
59
  async initialize() {
60
- const instanceController = await super.initialize();
61
60
  const { appMeta: APP_META, flags: FLAGS, logger: LOGGER, } = this.cx;
62
61
 
63
62
  // ----------
@@ -65,9 +64,18 @@ export class WebfloServer extends WebfloRuntime {
65
64
  if (FLAGS['dev']) {
66
65
  await this.enterDevMode();
67
66
  } else {
68
- await this.buildRoutes();
67
+ await this.buildRoutes({ server: true });
68
+ await this.bundleAssetsIfPending(true);
69
69
  }
70
70
 
71
+ // ----------
72
+ // Call default-init
73
+ const instanceController = await super.initialize();
74
+
75
+ // ----------
76
+ // Start serving
77
+ this.control();
78
+
71
79
  // ----------
72
80
  // Show proxies
73
81
  const { PROXY } = this.config;
@@ -90,10 +98,6 @@ export class WebfloServer extends WebfloRuntime {
90
98
  }
91
99
  }
92
100
 
93
- // ----------
94
- // Start serving
95
- this.control();
96
-
97
101
  // ----------
98
102
  // Show server details
99
103
  if (this.#servers.size) {
@@ -108,22 +112,22 @@ export class WebfloServer extends WebfloRuntime {
108
112
  return instanceController;
109
113
  }
110
114
 
111
- async buildRoutes({ client = false, worker = false, ...options } = {}) {
115
+ async buildRoutes({ client = false, worker = false, server = false, ...options } = {}) {
112
116
  const routeDirs = [...new Set([this.config.LAYOUT.CLIENT_DIR, this.config.LAYOUT.WORKER_DIR, this.config.LAYOUT.SERVER_DIR])];
113
- const entryPoints = await $glob(routeDirs.map((d) => `${d}/**/handler{${client ? ',.client' : ''}${worker ? ',.worker' : ''},.server}.js`), { absolute: true })
117
+ const entryPoints = await $glob(routeDirs.map((d) => `${d}/**/handler{${client ? ',.client' : ''}${worker ? ',.worker' : ''}${server ? ',.server' : ''}}.js`), { absolute: true })
114
118
  .then((files) => files.map((file) => file.replace(/\\/g, '/')));
115
- const entryNames = routeDirs.length === 1 ? `${Path.relative(process.cwd(), routeDirs[0])}/[dir]/[name]` : `[dir]/[name]`;
119
+ const initFiles = await $glob(`${process.cwd()}/init.server.js`);
116
120
  const bundlingConfig = {
117
- entryPoints,
121
+ entryPoints: entryPoints.concat(initFiles),
118
122
  outdir: this.config.RUNTIME_DIR,
119
- entryNames,
120
- bundle: true,
123
+ outbase: process.cwd(),
121
124
  format: 'esm',
122
- minify: false,
125
+ platform: server ? 'node' : 'browser',
126
+ bundle: server ? false : true,
127
+ minify: server ? false : true,
123
128
  sourcemap: false,
124
- platform: 'browser', // optional but good for clarity
125
- treeShaking: true, // Important optimization
126
- plugins: [ LiveJSTransform() ],
129
+ treeShaking: true,
130
+ plugins: [UseLiveTransform()],
127
131
  ...options,
128
132
  };
129
133
  return await EsBuild.build(bundlingConfig);
@@ -141,6 +145,7 @@ export class WebfloServer extends WebfloRuntime {
141
145
  buildSensitivity: parseInt(FLAGS['build-sensitivity'] || 0),
142
146
  });
143
147
  await this.#hmr.buildRoutes(true);
148
+ await this.#hmr.bundleAssetsIfPending(true);
144
149
  if (FLAGS['open']) {
145
150
  for (let [proto, def] of this.#servers) {
146
151
  const url = `${proto}://${def.hostnames.find((h) => h !== '*') || 'localhost'}:${def.port}`;
@@ -149,6 +154,35 @@ export class WebfloServer extends WebfloRuntime {
149
154
  }
150
155
  }
151
156
 
157
+ async initCreateStorage() {
158
+ if (this.bootstrap.init.createStorage
159
+ || !this.bootstrap.init.redis) {
160
+ return super.initCreateStorage();
161
+ }
162
+ const redis = this.bootstrap.init.redis;
163
+ this.bootstrap.init.createStorage = (namespace, ttl = null) => ({
164
+ async has(key) { return await redis.hexists(namespace, key); },
165
+ async get(key) {
166
+ const value = await redis.hget(namespace, key);
167
+ return typeof value === 'undefined' ? value : JSON.parse(value);
168
+ },
169
+ async set(key, value) {
170
+ const returnValue = await redis.hset(namespace, key, JSON.stringify(value));
171
+ if (!this.ttlApplied && ttl) {
172
+ await redis.expire(namespace, ttl);
173
+ this.ttlApplied = true;
174
+ }
175
+ return returnValue;
176
+ },
177
+ async delete(key) { return await redis.hdel(namespace, key); },
178
+ async clear() { return await redis.del(namespace); },
179
+ async keys() { return await redis.hkeys(namespace); },
180
+ async values() { return (await redis.hvals(namespace) || []).map((value) => typeof value === 'undefined' ? value : JSON.parse(value)); },
181
+ async entries() { return Object.entries(await redis.hgetall(namespace) || {}).map(([key, value]) => [key, typeof value === 'undefined' ? value : JSON.parse(value)]); },
182
+ get size() { return redis.hlen(namespace); },
183
+ });
184
+ }
185
+
152
186
  control() {
153
187
  const { flags: FLAGS } = this.cx;
154
188
  const { SERVER, PROXY } = this.config;
@@ -528,6 +562,10 @@ export class WebfloServer extends WebfloRuntime {
528
562
  }
529
563
  scopeObj.ext = Path.parse(scopeObj.filename).ext;
530
564
  const finalizeResponse = (response) => {
565
+ // Qualify Service-Worker responses
566
+ if (httpEvent.request.headers.get('Service-Worker') === 'script') {
567
+ scopeObj.response.headers.set('Service-Worker-Allowed', this.config.WORKER.scope || '/');
568
+ }
531
569
  const responseMeta = _wq(response, 'meta');
532
570
  responseMeta.set('filename', scopeObj.filename);
533
571
  responseMeta.set('static', true);
@@ -597,10 +635,7 @@ export class WebfloServer extends WebfloRuntime {
597
635
  scopeObj.response.headers.set('X-Frame-Options', 'SAMEORIGIN');
598
636
  // 5. Partial content support
599
637
  scopeObj.response.headers.set('Accept-Ranges', 'bytes');
600
- // 6. Qualify Service-Worker responses
601
- if (httpEvent.request.headers.get('Service-Worker') === 'script') {
602
- scopeObj.response.headers.set('Service-Worker-Allowed', this.config.WORKER.scope || '/');
603
- }
638
+
604
639
  return finalizeResponse(scopeObj.response);
605
640
  }
606
641
 
@@ -675,7 +710,7 @@ export class WebfloServer extends WebfloRuntime {
675
710
  const asHTML = requestAccept?.match('text/html');
676
711
  const asIs = requestAccept?.match(response.headers.get('Content-Type'));
677
712
  const responseMeta = _wq(response, 'meta');
678
- if (requestAccept && asHTML >= asIs && !responseMeta.get('static')) {
713
+ if (requestAccept && asHTML > asIs && !responseMeta.get('static')) {
679
714
  response = await this.render(httpEvent, response);
680
715
  } else if (requestAccept && response.headers.get('Content-Type') && !asIs) {
681
716
  return new Response(response.body, { status: 406, statusText: 'Not Acceptable', headers: response.headers });
@@ -182,13 +182,17 @@ export class WebfloHMR {
182
182
  const bundlingConfig = {
183
183
  client: true,
184
184
  worker: true,
185
+ server: true,
185
186
  metafile: true, // This is key
186
187
  logLevel: 'silent', // Suppress output
187
188
  incremental: true,
188
189
  };
189
190
  buildResult = await this.#app.buildRoutes(bundlingConfig);
190
191
  }
191
- } catch (e) { return false; }
192
+ } catch (e) {
193
+ //console.error(e);
194
+ return false;
195
+ }
192
196
 
193
197
  // 1. Forward dependency graph (file -> [imported files])
194
198
  const forward = {};
@@ -228,25 +232,25 @@ export class WebfloHMR {
228
232
  return true;
229
233
  }
230
234
 
231
- async bundleAssetsIfPending() {
235
+ async bundleAssetsIfPending(ohForce = false) {
232
236
  const entries = {};
233
237
 
234
- if (this.#dirtiness.clientRoutesAffected.size || this.#dirtiness.serviceWorkerAffected) {
238
+ if (this.#dirtiness.clientRoutesAffected.size || this.#dirtiness.serviceWorkerAffected || ohForce) {
235
239
  entries.js = {};
236
- entries.js.client = !!this.#dirtiness.clientRoutesAffected.size;
237
- entries.js.worker = this.#dirtiness.serviceWorkerAffected;
240
+ entries.js.client = !!this.#dirtiness.clientRoutesAffected.size || ohForce;
241
+ entries.js.worker = this.#dirtiness.serviceWorkerAffected || ohForce;
238
242
  entries.js.server = false;
239
243
  // Clear state
240
244
  this.#dirtiness.clientRoutesAffected.clear();
241
245
  this.#dirtiness.serviceWorkerAffected = false;
242
246
  }
243
247
 
244
- if (this.#dirtiness.HTMLAffected) {
248
+ if (this.#dirtiness.HTMLAffected || ohForce) {
245
249
  this.#dirtiness.HTMLAffected = false;
246
250
  entries.html = {};
247
251
  }
248
252
 
249
- if (this.#dirtiness.CSSAffected) {
253
+ if (this.#dirtiness.CSSAffected || ohForce) {
250
254
  this.#dirtiness.CSSAffected = false;
251
255
  entries.css = {};
252
256
  }
@@ -1,7 +1,7 @@
1
1
  import { _with } from '@webqit/util/obj/index.js';
2
2
  import { _isArray, _isObject, _isTypeObject, _isString, _isEmpty } from '@webqit/util/js/index.js';
3
3
  import { DeepURLSearchParams } from './util.js';
4
- import { Observer } from '@webqit/quantum-js';
4
+ import { Observer } from '@webqit/use-live';
5
5
 
6
6
  export class Url {
7
7
 
@@ -1,5 +1,5 @@
1
1
  import { _isObject } from '@webqit/util/js/index.js';
2
- import { Observer } from '@webqit/quantum-js';
2
+ import { Observer } from '@webqit/use-live';
3
3
  import { DeepURLSearchParams } from './util.js';
4
4
 
5
5
  export class xURL extends URL {
@@ -21,6 +21,7 @@ export async function bootstrap(cx, offset = '') {
21
21
  };
22
22
  if (config.CLIENT.copy_public_variables) {
23
23
  const publicEnvPattern = /(?:^|_)PUBLIC(?:_|$)/;
24
+ config.ENV.data = config.ENV.data || {};
24
25
  for (const key in process.env) {
25
26
  if (publicEnvPattern.test(key)) {
26
27
  config.ENV.data[key] = process.env[key];
@@ -1,35 +0,0 @@
1
- import Fs from 'fs/promises';
2
- //import { transformQuantum } from '@webqit/quantum-js';
3
-
4
- export function LiveJSTransform() {
5
- return {
6
- name: 'livejs-transform',
7
- setup(build) {
8
- build.onLoad({ filter: /\.(js|mjs|ts|jsx|tsx)$/ }, async (args) => {
9
- let code = await Fs.readFile(args.path, 'utf8');
10
-
11
- //console.log('LiveJS -- transform:', args);
12
-
13
- // super dirty detection
14
- if (!/\bquantum\s+function\b/.test(code) &&
15
- !/\basync\s+quantum\s+function\b/.test(code)) {
16
- return { contents: code, loader: 'default' };
17
- }
18
-
19
-
20
- console.log('LiveJS transform:', args.path);
21
-
22
- return { contents: code, loader: 'default' };
23
- const result = await transformQuantum(code, {
24
- filename: args.path,
25
- sourceMaps: true
26
- });
27
-
28
- return {
29
- contents: result.code,
30
- loader: 'js' // or 'ts' if you want esbuild TS transform after
31
- };
32
- });
33
- }
34
- };
35
- }