@webqit/webflo 0.20.4-next.2 → 0.20.4-next.4

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 (56) hide show
  1. package/package.json +13 -34
  2. package/site/docs/concepts/realtime.md +45 -44
  3. package/site/docs/getting-started.md +40 -40
  4. package/src/{Context.js → CLIContext.js} +9 -8
  5. package/src/build-pi/esbuild-plugin-uselive-transform.js +42 -0
  6. package/src/{runtime-pi/webflo-client/webflo-codegen.js → build-pi/index.js} +148 -142
  7. package/src/index.js +3 -1
  8. package/src/init-pi/index.js +7 -4
  9. package/src/init-pi/templates/pwa/.gitignore +6 -0
  10. package/src/init-pi/templates/pwa/.webqit/webflo/client.json +15 -0
  11. package/src/init-pi/templates/pwa/.webqit/webflo/layout.json +7 -0
  12. package/src/init-pi/templates/pwa/package.json +2 -2
  13. package/src/init-pi/templates/pwa/public/manifest.json +2 -2
  14. package/src/init-pi/templates/web/.gitignore +6 -0
  15. package/src/init-pi/templates/web/.webqit/webflo/client.json +12 -0
  16. package/src/init-pi/templates/web/.webqit/webflo/layout.json +7 -0
  17. package/src/init-pi/templates/web/package.json +2 -2
  18. package/src/runtime-pi/AppBootstrap.js +38 -0
  19. package/src/runtime-pi/WebfloRuntime.js +68 -56
  20. package/src/runtime-pi/apis.js +9 -0
  21. package/src/runtime-pi/index.js +2 -4
  22. package/src/runtime-pi/webflo-client/DeviceCapabilities.js +1 -1
  23. package/src/runtime-pi/webflo-client/WebfloClient.js +33 -36
  24. package/src/runtime-pi/webflo-client/WebfloRootClient1.js +23 -17
  25. package/src/runtime-pi/webflo-client/WebfloRootClient2.js +1 -1
  26. package/src/runtime-pi/webflo-client/WebfloSubClient.js +14 -14
  27. package/src/runtime-pi/webflo-client/bootstrap.js +38 -0
  28. package/src/runtime-pi/webflo-client/index.js +2 -8
  29. package/src/runtime-pi/webflo-client/webflo-devmode.js +3 -3
  30. package/src/runtime-pi/webflo-fetch/LiveResponse.js +154 -116
  31. package/src/runtime-pi/webflo-fetch/index.js +436 -5
  32. package/src/runtime-pi/webflo-messaging/wq-message-port.js +1 -1
  33. package/src/runtime-pi/webflo-routing/HttpCookies.js +1 -1
  34. package/src/runtime-pi/webflo-routing/HttpEvent.js +12 -11
  35. package/src/runtime-pi/webflo-routing/HttpUser.js +7 -7
  36. package/src/runtime-pi/webflo-routing/WebfloRouter.js +12 -7
  37. package/src/runtime-pi/webflo-server/ServerSideCookies.js +3 -1
  38. package/src/runtime-pi/webflo-server/ServerSideSession.js +2 -1
  39. package/src/runtime-pi/webflo-server/WebfloServer.js +138 -200
  40. package/src/runtime-pi/webflo-server/bootstrap.js +59 -0
  41. package/src/runtime-pi/webflo-server/index.js +2 -6
  42. package/src/runtime-pi/webflo-server/webflo-devmode.js +24 -31
  43. package/src/runtime-pi/webflo-url/Url.js +1 -1
  44. package/src/runtime-pi/webflo-url/xURL.js +1 -1
  45. package/src/runtime-pi/webflo-worker/WebfloWorker.js +11 -15
  46. package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +2 -1
  47. package/src/runtime-pi/webflo-worker/bootstrap.js +39 -0
  48. package/src/runtime-pi/webflo-worker/index.js +3 -7
  49. package/src/webflo-cli.js +1 -2
  50. package/src/runtime-pi/webflo-fetch/cookies.js +0 -10
  51. package/src/runtime-pi/webflo-fetch/fetch.js +0 -16
  52. package/src/runtime-pi/webflo-fetch/formdata.js +0 -54
  53. package/src/runtime-pi/webflo-fetch/headers.js +0 -151
  54. package/src/runtime-pi/webflo-fetch/message.js +0 -49
  55. package/src/runtime-pi/webflo-fetch/request.js +0 -62
  56. package/src/runtime-pi/webflo-fetch/response.js +0 -110
@@ -7,9 +7,10 @@ import WebSocket from 'ws';
7
7
  import Mime from 'mime-types';
8
8
  import crypto from 'crypto';
9
9
  import 'dotenv/config';
10
+ import $glob from 'fast-glob';
11
+ import EsBuild from 'esbuild';
10
12
  import { Readable } from 'stream';
11
13
  import { spawn } from 'child_process';
12
- import { Observer } from '@webqit/quantum-js';
13
14
  import { _from as _arrFrom, _any } from '@webqit/util/arr/index.js';
14
15
  import { _isEmpty, _isObject } from '@webqit/util/js/index.js';
15
16
  import { _each } from '@webqit/util/obj/index.js';
@@ -21,18 +22,9 @@ import { ServerSideCookies } from './ServerSideCookies.js';
21
22
  import { ServerSideSession } from './ServerSideSession.js';
22
23
  import { HttpEvent } from '../webflo-routing/HttpEvent.js';
23
24
  import { HttpUser } from '../webflo-routing/HttpUser.js';
25
+ import { response as responseShim, headers as headersShim } from '../webflo-fetch/index.js';
26
+ import { UseLiveTransform } from '../../build-pi/esbuild-plugin-uselive-transform.js';
24
27
  import { createWindow } from '@webqit/oohtml-ssr';
25
- import {
26
- readServerConfig,
27
- readHeadersConfig,
28
- readRedirectsConfig,
29
- readLayoutConfig,
30
- readEnvConfig,
31
- readProxyConfig,
32
- readWorkerConfig,
33
- scanRoots,
34
- scanRouteHandlers,
35
- } from '../../deployment-pi/util.js';
36
28
  import { _wq } from '../../util.js';
37
29
  import '../webflo-fetch/index.js';
38
30
  import '../webflo-url/index.js';
@@ -47,26 +39,15 @@ export class WebfloServer extends WebfloRuntime {
47
39
 
48
40
  static get HttpUser() { return HttpUser; }
49
41
 
50
- static create(cx) {
51
- return new this(this.Context.create(cx));
42
+ static create(bootstrap) {
43
+ return new this(bootstrap);
52
44
  }
53
45
 
54
- #config;
55
- get config() { return this.#config; }
56
-
57
- #routes;
58
- get routes() { return this.#routes; }
59
-
60
- #renderFileCache = new Map;
61
-
62
- #sdk = {};
63
- get sdk() { return this.#sdk; }
64
-
65
46
  #servers = new Map;
47
+ #clients = new Clients;
48
+ #hmr;
66
49
 
67
- #clients;
68
-
69
- #hmrRegistry;
50
+ #renderFileCache = new Map;
70
51
 
71
52
  env(key) {
72
53
  const { ENV } = this.config;
@@ -76,61 +57,31 @@ export class WebfloServer extends WebfloRuntime {
76
57
  }
77
58
 
78
59
  async initialize() {
79
- const instanceController = await super.initialize();
80
60
  const { appMeta: APP_META, flags: FLAGS, logger: LOGGER, } = this.cx;
81
- this.#config = {
82
- LAYOUT: await readLayoutConfig(this.cx),
83
- ENV: await readEnvConfig(this.cx),
84
- SERVER: await readServerConfig(this.cx),
85
- HEADERS: await readHeadersConfig(this.cx),
86
- REDIRECTS: await readRedirectsConfig(this.cx),
87
- PROXY: await readProxyConfig(this.cx),
88
- WORKER: await readWorkerConfig(this.cx),
89
- };
90
- const { PROXY } = this.config;
91
- if (FLAGS['dev']) {
92
- this.#config.DEV_DIR = Path.join(process.cwd(), '.webqit/webflo/@dev');
93
- this.#config.DEV_LAYOUT = { ...this.config.LAYOUT };
94
- for (const name of ['CLIENT_DIR', 'WORKER_DIR', 'SERVER_DIR', 'VIEWS_DIR', 'PUBLIC_DIR']) {
95
- const originalDir = Path.relative(process.cwd(), this.config.LAYOUT[name]);
96
- this.config.DEV_LAYOUT[name] = `${this.#config.DEV_DIR}/${originalDir}`;
97
- }
98
- }
99
- // -----------------
100
- this.#routes = {};
101
- const spaRoots = Fs.existsSync(this.config.LAYOUT.PUBLIC_DIR) ? scanRoots(this.config.LAYOUT.PUBLIC_DIR, 'index.html') : [];
102
- const serverRoots = PROXY.entries.map((proxy) => proxy.path?.replace(/^\.\//, '')).filter((p) => p);
103
- scanRouteHandlers(this.#config.DEV_LAYOUT || this.#config.LAYOUT, 'server', (file, route) => {
104
- this.routes[route] = file;
105
- }, ''/*offset*/, serverRoots);
106
- Object.defineProperty(this.#routes, '$root', { value: '' });
107
- Object.defineProperty(this.#routes, '$sparoots', { value: spaRoots });
108
- Object.defineProperty(this.#routes, '$serverroots', { value: serverRoots });
109
- // -----------------
110
- await this.setupCapabilities();
111
- this.#clients = new Clients;
112
- this.control();
61
+
62
+ // ----------
63
+ // Initialize routes
113
64
  if (FLAGS['dev']) {
114
65
  await this.enterDevMode();
115
- }
116
- // -----------------
117
- if (this.#servers.size) {
118
- // Show server details
119
- LOGGER?.info(`> Server running! (${APP_META.title || ''}) ✅`);
120
- for (let [proto, def] of this.#servers) {
121
- LOGGER?.info(`> ${proto.toUpperCase()} / ${def.hostnames.concat('').join(`:${def.port} / `)}`);
122
- }
123
- // Show capabilities
124
- LOGGER?.info(``);
125
- LOGGER?.info(`Capabilities: ${Object.keys(this.#sdk).join(', ')}`);
126
- LOGGER?.info(``);
127
66
  } else {
128
- LOGGER?.info(`> Server not running! No port specified.`);
67
+ await this.buildRoutes({ server: true });
68
+ await this.bundleAssetsIfPending(true);
129
69
  }
130
- // -----------------
70
+
71
+ // ----------
72
+ // Call default-init
73
+ const instanceController = await super.initialize();
74
+
75
+ // ----------
76
+ // Start serving
77
+ this.control();
78
+
79
+ // ----------
80
+ // Show proxies
81
+ const { PROXY } = this.config;
131
82
  if (PROXY.entries.length) {
132
83
  // Show active proxies
133
- LOGGER?.info(`> Reverse proxies active.`);
84
+ LOGGER.info(`> Reverse proxies active.`);
134
85
  for (const proxy of PROXY.entries) {
135
86
  let desc = `> ${proxy.hostnames.join('|')} >>> ${proxy.port || proxy.path}`;
136
87
  // Start a proxy recursively?
@@ -143,101 +94,48 @@ export class WebfloServer extends WebfloRuntime {
143
94
  shell: true // for Windows compatibility
144
95
  });
145
96
  }
146
- LOGGER?.info(desc);
97
+ LOGGER.info(desc);
147
98
  }
148
99
  }
149
- return instanceController;
150
- }
151
100
 
152
- async setupCapabilities() {
153
- const instanceController = await super.setupCapabilities();
154
- const { SERVER } = this.config;
155
- this.#sdk.Observer = Observer;
156
- // 1. Database capabilities?
157
- if (SERVER.capabilities?.database) {
158
- if (SERVER.capabilities.database_dialect !== 'postgres') {
159
- throw new Error(`Only postgres supported for now for database dialect`);
160
- }
161
- if (this.env('DATABASE_URL')) {
162
- const { SQLClient } = await import('@linked-db/linked-ql/sql');
163
- const { default: pg } = await import('pg');
164
- // Obtain pg client
165
- const pgClient = new pg.Pool({
166
- connectionString: this.env('DATABASE_URL')
167
- });
168
- await (async function connect() {
169
- pgClient.on('error', (e) => {
170
- console.log('PG Error', e);
171
- });
172
- pgClient.on('end', (e) => {
173
- console.log('PG End', e);
174
- });
175
- pgClient.on('notice', (e) => {
176
- console.log('PG Notice', e);
177
- });
178
- await pgClient.connect();
179
- })();
180
- this.#sdk.db = new SQLClient(pgClient, { dialect: 'postgres' });
181
- } else {
182
- //const { ODBClient } = await import('@linked-db/linked-ql/odb');
183
- //this.#sdk.db = new ODBClient({ dialect: 'postgres' });
101
+ // ----------
102
+ // Show server details
103
+ if (this.#servers.size) {
104
+ LOGGER.info(`> Server running! (${APP_META.title || ''}) ✅`);
105
+ for (let [proto, def] of this.#servers) {
106
+ LOGGER.info(`> ${proto.toUpperCase()} / ${def.hostnames.concat('').join(`:${def.port} / `)}`);
184
107
  }
185
- }
186
- // 2. Storage capabilities?
187
- if (SERVER.capabilities?.redis) {
188
- const { Redis } = await import('ioredis');
189
- const redis = this.env('REDIS_URL')
190
- ? new Redis(this.env('REDIS_URL'))
191
- : new Redis;
192
- this.#sdk.redis = redis;
193
- this.#sdk.storage = (namespace, ttl = null) => ({
194
- async has(key) { return await redis.hexists(namespace, key); },
195
- async get(key) {
196
- const value = await redis.hget(namespace, key);
197
- return typeof value === 'undefined' ? value : JSON.parse(value);
198
- },
199
- async set(key, value) {
200
- const returnValue = await redis.hset(namespace, key, JSON.stringify(value));
201
- if (!this.ttlApplied && ttl) {
202
- await redis.expire(namespace, ttl);
203
- this.ttlApplied = true;
204
- }
205
- return returnValue;
206
- },
207
- async delete(key) { return await redis.hdel(namespace, key); },
208
- async clear() { return await redis.del(namespace); },
209
- async keys() { return await redis.hkeys(namespace); },
210
- async values() { return (await redis.hvals(namespace) || []).map((value) => typeof value === 'undefined' ? value : JSON.parse(value)); },
211
- async entries() { return Object.entries(await redis.hgetall(namespace) || {}).map(([key, value]) => [key, typeof value === 'undefined' ? value : JSON.parse(value)]); },
212
- get size() { return redis.hlen(namespace); },
213
- });
214
108
  } else {
215
- const inmemSessionRegistry = new Map;
216
- this.#sdk.storage = (namespace) => {
217
- if (!inmemSessionRegistry.has(namespace)) {
218
- inmemSessionRegistry.set(namespace, new Map);
219
- }
220
- return inmemSessionRegistry.get(namespace);
221
- };
222
- }
223
- // 3. webpush capabilities?
224
- if (SERVER.capabilities?.webpush) {
225
- const { default: webpush } = await import('web-push');
226
- this.#sdk.webpush = webpush;
227
- if (this.env('VAPID_PUBLIC_KEY') && this.env('VAPID_PRIVATE_KEY')) {
228
- webpush.setVapidDetails(
229
- SERVER.capabilities.vapid_subject,
230
- this.env('VAPID_PUBLIC_KEY'),
231
- this.env('VAPID_PRIVATE_KEY')
232
- );
233
- }
109
+ LOGGER.info(`> No servers running!`);
234
110
  }
111
+
235
112
  return instanceController;
236
113
  }
237
114
 
115
+ async buildRoutes({ client = false, worker = false, server = false, ...options } = {}) {
116
+ const routeDirs = [...new Set([this.config.LAYOUT.CLIENT_DIR, this.config.LAYOUT.WORKER_DIR, this.config.LAYOUT.SERVER_DIR])];
117
+ const entryPoints = await $glob(routeDirs.map((d) => `${d}/**/handler{${client ? ',.client' : ''}${worker ? ',.worker' : ''}${server ? ',.server' : ''}}.js`), { absolute: true })
118
+ .then((files) => files.map((file) => file.replace(/\\/g, '/')));
119
+ const initFiles = await $glob(`${process.cwd()}/init.server.js`);
120
+ const bundlingConfig = {
121
+ entryPoints: entryPoints.concat(initFiles),
122
+ outdir: this.config.RUNTIME_DIR,
123
+ outbase: process.cwd(),
124
+ format: 'esm',
125
+ platform: server ? 'node' : 'browser',
126
+ bundle: server ? false : true,
127
+ minify: server ? false : true,
128
+ sourcemap: false,
129
+ treeShaking: true,
130
+ plugins: [UseLiveTransform()],
131
+ ...options,
132
+ };
133
+ return await EsBuild.build(bundlingConfig);
134
+ }
135
+
238
136
  async enterDevMode() {
239
137
  const { appMeta, flags: FLAGS } = this.cx;
240
- this.#hmrRegistry = WebfloHMR.manage(this, {
138
+ this.#hmr = WebfloHMR.manage(this, {
241
139
  appMeta,
242
140
  buildScripts: {
243
141
  ['build:html']: FLAGS['build:html'] ?? true,
@@ -246,7 +144,8 @@ export class WebfloServer extends WebfloRuntime {
246
144
  },
247
145
  buildSensitivity: parseInt(FLAGS['build-sensitivity'] || 0),
248
146
  });
249
- await this.#hmrRegistry.buildJS(true);
147
+ await this.#hmr.buildRoutes(true);
148
+ await this.#hmr.bundleAssetsIfPending(true);
250
149
  if (FLAGS['open']) {
251
150
  for (let [proto, def] of this.#servers) {
252
151
  const url = `${proto}://${def.hostnames.find((h) => h !== '*') || 'localhost'}:${def.port}`;
@@ -255,11 +154,40 @@ export class WebfloServer extends WebfloRuntime {
255
154
  }
256
155
  }
257
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
+
258
186
  control() {
259
187
  const { flags: FLAGS } = this.cx;
260
188
  const { SERVER, PROXY } = this.config;
261
189
  const instanceController = super.control();
262
- // ---------------
190
+
263
191
  if (!FLAGS['test-only'] && !FLAGS['https-only'] && SERVER.port) {
264
192
  const httpServer = Http.createServer((request, response) => this.handleNodeHttpRequest(request, response));
265
193
  httpServer.listen(FLAGS['port'] || SERVER.port);
@@ -273,7 +201,7 @@ export class WebfloServer extends WebfloRuntime {
273
201
  this.handleNodeWsRequest(wss, request, socket, head);
274
202
  });
275
203
  }
276
- // ---------------
204
+
277
205
  if (!FLAGS['test-only'] && !FLAGS['http-only'] && SERVER.https.port) {
278
206
  const httpsServer = Https.createServer((request, response) => this.handleNodeHttpRequest(request, response));
279
207
  httpsServer.listen(SERVER.https.port);
@@ -304,21 +232,22 @@ export class WebfloServer extends WebfloRuntime {
304
232
  this.handleNodeWsRequest(wss, request, socket, head);
305
233
  });
306
234
  }
307
- // ---------------
235
+
308
236
  const wss = new WebSocket.Server({ noServer: true });
309
- // -----------------
237
+
310
238
  process.on('uncaughtException', (err) => {
311
239
  console.error('Uncaught Exception:', err);
312
240
  });
313
241
  process.on('unhandledRejection', (reason, promise) => {
314
242
  console.log('Unhandled Rejection', reason, promise);
315
243
  });
244
+
316
245
  return instanceController;
317
246
  }
318
247
 
319
248
  identifyIncoming(request, autoGenerateID = false) {
320
249
  const secret = this.env('SESSION_KEY');
321
- let clientID = request.headers.get('Cookie', true).find((c) => c.name === '__sessid')?.value;
250
+ let clientID = headersShim.get.value.call(request.headers, 'Cookie', true).find((c) => c.name === '__sessid')?.value;
322
251
  if (clientID?.includes('.')) {
323
252
  if (secret) {
324
253
  const [rand, signature] = clientID.split('.');
@@ -465,7 +394,7 @@ export class WebfloServer extends WebfloRuntime {
465
394
  if (requestURL.searchParams.get('rel') === 'hmr') {
466
395
  wss.handleUpgrade(nodeRequest, socket, head, (ws) => {
467
396
  wss.emit('connection', ws, nodeRequest);
468
- this.#hmrRegistry.clients.add(ws);
397
+ this.#hmr.clients.add(ws);
469
398
  });
470
399
  }
471
400
  if (requestURL.searchParams.get('rel') === 'background-messaging') {
@@ -497,7 +426,7 @@ export class WebfloServer extends WebfloRuntime {
497
426
  if (existing) nodeResponse.setHeader(name, [].concat(existing).concat(value));
498
427
  else nodeResponse.setHeader(name, value);
499
428
  }
500
- nodeResponse.statusCode = response.status;
429
+ nodeResponse.statusCode = responseShim.prototype.status.get.call(response);
501
430
  nodeResponse.statusMessage = response.statusText;
502
431
  if (response.body instanceof Readable) {
503
432
  response.body.pipe(nodeResponse);
@@ -571,7 +500,7 @@ export class WebfloServer extends WebfloRuntime {
571
500
  }
572
501
 
573
502
  writeRedirectHeaders(httpEvent, response) {
574
- const $sparoots = this.#routes.$sparoots;
503
+ const $sparoots = this.bootstrap.$sparoots;
575
504
  const xRedirectPolicy = httpEvent.request.headers.get('X-Redirect-Policy');
576
505
  const xRedirectCode = httpEvent.request.headers.get('X-Redirect-Code') || 300;
577
506
  const destinationURL = new URL(response.headers.get('Location'), httpEvent.url.origin);
@@ -584,7 +513,7 @@ export class WebfloServer extends WebfloRuntime {
584
513
  isSameSpaRedirect = matchRoot(destinationURL.pathname) === matchRoot(httpEvent.url.pathname);
585
514
  }
586
515
  if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (!isSameSpaRedirect && xRedirectPolicy === 'manual-when-cross-spa')) {
587
- response.headers.set('X-Redirect-Code', response.status);
516
+ response.headers.set('X-Redirect-Code', responseShim.prototype.status.get.call(response));
588
517
  response.headers.set('Access-Control-Allow-Origin', '*');
589
518
  response.headers.set('Cache-Control', 'no-store');
590
519
  const responseMeta = _wq(response, 'meta');
@@ -609,18 +538,23 @@ export class WebfloServer extends WebfloRuntime {
609
538
 
610
539
  async localFetch(httpEvent) {
611
540
  const { flags: FLAGS } = this.cx;
612
- const { DEV_LAYOUT, LAYOUT } = this.config;
541
+ const { RUNTIME_LAYOUT, LAYOUT } = this.config;
613
542
  const scopeObj = {};
614
543
  if (FLAGS['dev']) {
615
- if (httpEvent.url.pathname === '/@dev') {
616
- const filename = httpEvent.url.searchParams.get('src').split('?')[0];
544
+ if (httpEvent.url.pathname === '/@hmr') {
545
+ const filename = httpEvent.url.searchParams.get('src')?.split('?')[0] || '';
617
546
  if (filename.endsWith('.js')) {
618
- scopeObj.filename = Path.join(DEV_LAYOUT.PUBLIC_DIR, filename);
547
+ // This is purely a route handler source request from HMR
548
+ scopeObj.filename = Path.join(RUNTIME_LAYOUT.PUBLIC_DIR, filename);
619
549
  } else {
550
+ // This is a static asset (HTML) request from HMR
620
551
  scopeObj.filename = Path.join(LAYOUT.PUBLIC_DIR, filename);
621
552
  }
622
- } else if (this.#hmrRegistry.options.buildSensitivity === 2) {
623
- await this.#hmrRegistry.bundleAssetsIfPending();
553
+ } else {
554
+ if (this.#hmr.options.buildSensitivity === 1) {
555
+ // This is a static asset request in dev mode but NOT from HMR
556
+ await this.#hmr.bundleAssetsIfPending();
557
+ }
624
558
  scopeObj.filename = Path.join(LAYOUT.PUBLIC_DIR, httpEvent.url.pathname.split('?')[0]);
625
559
  }
626
560
  } else {
@@ -628,6 +562,10 @@ export class WebfloServer extends WebfloRuntime {
628
562
  }
629
563
  scopeObj.ext = Path.parse(scopeObj.filename).ext;
630
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
+ }
631
569
  const responseMeta = _wq(response, 'meta');
632
570
  responseMeta.set('filename', scopeObj.filename);
633
571
  responseMeta.set('static', true);
@@ -676,7 +614,8 @@ export class WebfloServer extends WebfloRuntime {
676
614
  // Range support
677
615
  const readStream = (params = {}) => Fs.createReadStream(scopeObj.filename, { ...params });
678
616
  scopeObj.response = this.createStreamingResponse(httpEvent, readStream, scopeObj.stats);
679
- if (scopeObj.response.status === 416) return finalizeResponse(scopeObj.response);
617
+ const statusCode = responseShim.prototype.status.get.call(scopeObj.response);
618
+ if (statusCode === 416) return finalizeResponse(scopeObj.response);
680
619
  // ------ If we get here, it means we're good ------
681
620
  if (scopeObj.enc) {
682
621
  scopeObj.response.headers.set('Content-Encoding', scopeObj.enc);
@@ -696,10 +635,7 @@ export class WebfloServer extends WebfloRuntime {
696
635
  scopeObj.response.headers.set('X-Frame-Options', 'SAMEORIGIN');
697
636
  // 5. Partial content support
698
637
  scopeObj.response.headers.set('Accept-Ranges', 'bytes');
699
- // 6. Qualify Service-Worker responses
700
- if (httpEvent.request.headers.get('Service-Worker') === 'script') {
701
- scopeObj.response.headers.set('Service-Worker-Allowed', this.#config.WORKER.scope || '/');
702
- }
638
+
703
639
  return finalizeResponse(scopeObj.response);
704
640
  }
705
641
 
@@ -718,35 +654,34 @@ export class WebfloServer extends WebfloRuntime {
718
654
  });
719
655
  scopeObj.clientID = this.identifyIncoming(scopeObj.request, true);
720
656
  scopeObj.client = this.#clients.getClient(scopeObj.clientID, true);
721
- scopeObj.realtimePortID = crypto.randomUUID();
722
- scopeObj.clientRequestRealtime = scopeObj.client.createRequestRealtime(scopeObj.realtimePortID, scopeObj.request.url);
657
+ scopeObj.clientPortID = crypto.randomUUID();
658
+ scopeObj.clientRequestRealtime = scopeObj.client.createRequestRealtime(scopeObj.clientPortID, scopeObj.request.url);
723
659
  scopeObj.sessionTTL = this.env('SESSION_TTL') || 2592000/*30days*/;
724
660
  scopeObj.session = this.createHttpSession({
725
- store: this.#sdk.storage?.(`${scopeObj.url.host}/session:${scopeObj.clientID}`, scopeObj.sessionTTL),
661
+ store: this.createStorage(`${scopeObj.url.host}/session:${scopeObj.clientID}`, scopeObj.sessionTTL),
726
662
  request: scopeObj.request,
727
663
  sessionID: scopeObj.clientID,
728
664
  ttl: scopeObj.sessionTTL
729
665
  });
730
666
  scopeObj.user = this.createHttpUser({
731
- store: this.#sdk.storage?.(`${scopeObj.url.host}/user:${scopeObj.clientID}`, scopeObj.sessionTTL),
667
+ store: this.createStorage(`${scopeObj.url.host}/user:${scopeObj.clientID}`, scopeObj.sessionTTL),
732
668
  request: scopeObj.request,
733
- realtime: scopeObj.clientRequestRealtime,
669
+ client: scopeObj.clientRequestRealtime,
734
670
  session: scopeObj.session,
735
671
  });
736
672
  scopeObj.httpEvent = this.createHttpEvent({
737
673
  request: scopeObj.request,
738
- realtime: scopeObj.clientRequestRealtime,
674
+ client: scopeObj.clientRequestRealtime,
739
675
  cookies: scopeObj.cookies,
740
676
  session: scopeObj.session,
741
677
  user: scopeObj.user,
742
678
  detail: scopeObj.detail,
743
- sdk: this.#sdk,
744
679
  });
745
680
  // Dispatch for response
746
681
  scopeObj.response = await this.dispatchNavigationEvent({
747
682
  httpEvent: scopeObj.httpEvent,
748
683
  crossLayerFetch: (event) => this.localFetch(event),
749
- responseRealtime: `ws:${scopeObj.httpEvent.realtime.portID}?rel=background-messaging`
684
+ clientPortB: `ws:${scopeObj.httpEvent.client.portID}?rel=background-messaging`
750
685
  });
751
686
  // Reponse handlers
752
687
  if (FLAGS['dev']) {
@@ -765,16 +700,17 @@ export class WebfloServer extends WebfloRuntime {
765
700
  }
766
701
 
767
702
  async satisfyRequestFormat(httpEvent, response) {
768
- if (response.status === 206 || response.status === 416) {
703
+ const statusCode = responseShim.prototype.status.get.call(response);
704
+ if (statusCode === 206 || statusCode === 416) {
769
705
  // If the response is a partial content, we don't need to do anything else
770
706
  return response;
771
707
  }
772
708
  // Satisfy "Accept" header
773
- const requestAccept = httpEvent.request.headers.get('Accept', true);
709
+ const requestAccept = headersShim.get.value.call(httpEvent.request.headers, 'Accept', true);
774
710
  const asHTML = requestAccept?.match('text/html');
775
711
  const asIs = requestAccept?.match(response.headers.get('Content-Type'));
776
712
  const responseMeta = _wq(response, 'meta');
777
- if (requestAccept && asHTML >= asIs && !responseMeta.get('static')) {
713
+ if (requestAccept && asHTML > asIs && !responseMeta.get('static')) {
778
714
  response = await this.render(httpEvent, response);
779
715
  } else if (requestAccept && response.headers.get('Content-Type') && !asIs) {
780
716
  return new Response(response.body, { status: 406, statusText: 'Not Acceptable', headers: response.headers });
@@ -785,7 +721,7 @@ export class WebfloServer extends WebfloRuntime {
785
721
  response.headers.append('Vary', 'Accept');
786
722
  }
787
723
  // Satisfy "Range" header
788
- const requestRange = httpEvent.request.headers.get('Range', true);
724
+ const requestRange = headersShim.get.value.call(httpEvent.request.headers, 'Range', true);
789
725
  if (requestRange.length && response.headers.get('Content-Length')) {
790
726
  const stats = {
791
727
  size: parseInt(response.headers.get('Content-Length')),
@@ -827,7 +763,7 @@ export class WebfloServer extends WebfloRuntime {
827
763
  if (document.readyState === 'complete') return res(1);
828
764
  document.addEventListener('load', res);
829
765
  });
830
- const data = await response.parse();
766
+ const data = await responseShim.prototype.parse.value.call(response);
831
767
  if (window.webqit?.oohtml?.config) {
832
768
  // Await rendering engine
833
769
  if (window.webqit?.$qCompilerWorker) {
@@ -879,7 +815,7 @@ export class WebfloServer extends WebfloRuntime {
879
815
  const rendering = window.toString();
880
816
  document.documentElement.remove();
881
817
  document.writeln('');
882
- try { window.close(); } catch (e) {}
818
+ try { window.close(); } catch (e) { }
883
819
  return rendering;
884
820
  });
885
821
  // Validate rendering
@@ -887,9 +823,10 @@ export class WebfloServer extends WebfloRuntime {
887
823
  throw new Error('render() must return a string response or an object that implements toString()..');
888
824
  }
889
825
  // Convert back to response
826
+ const statusCode = responseShim.prototype.status.get.call(response);
890
827
  scopeObj.response = new Response(scopeObj.rendering, {
891
828
  headers: response.headers,
892
- status: response.status,
829
+ status: statusCode,
893
830
  statusText: response.statusText,
894
831
  });
895
832
  scopeObj.response.headers.set('Content-Type', 'text/html');
@@ -902,10 +839,11 @@ export class WebfloServer extends WebfloRuntime {
902
839
  const log = [];
903
840
  // ---------------
904
841
  const style = LOGGER.style || { keyword: (str) => str, comment: (str) => str, url: (str) => str, val: (str) => str, err: (str) => str, };
905
- const errorCode = response.status >= 400 && response.status < 500 ? response.status : 0;
842
+ const statusCode = responseShim.prototype.status.get.call(response);
843
+ const errorCode = statusCode >= 400 && statusCode < 500 ? statusCode : 0;
906
844
  const xRedirectCode = response.headers.get('X-Redirect-Code');
907
- const isRedirect = (xRedirectCode || response.status + '').startsWith('3') && (xRedirectCode || response.status) !== 304;
908
- const statusCode = xRedirectCode && `${xRedirectCode} (${response.status})` || response.status;
845
+ const isRedirect = (xRedirectCode || statusCode + '').startsWith('3') && (xRedirectCode || statusCode) !== 304;
846
+ const _statusCode = xRedirectCode && `${xRedirectCode} (${statusCode})` || statusCode;
909
847
  const responseMeta = _wq(response, 'meta');
910
848
  // ---------------
911
849
  log.push(`[${style.comment((new Date).toUTCString())}]`);
@@ -917,7 +855,7 @@ export class WebfloServer extends WebfloRuntime {
917
855
  if (contentInfo.length) log.push(`(${style.comment(contentInfo.join('; '))})`);
918
856
  if (response.headers.get('Content-Encoding')) log.push(`(${style.comment(response.headers.get('Content-Encoding'))})`);
919
857
  if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
920
- else log.push(style.val(`${statusCode} ${response.statusText}`));
858
+ else log.push(style.val(`${_statusCode} ${response.statusText}`));
921
859
  if (isRedirect) log.push(`- ${style.url(response.headers.get('Location'))}`);
922
860
  return log.join(' ');
923
861
  }
@@ -0,0 +1,59 @@
1
+ import Fs from 'fs';
2
+ import Path from 'path';
3
+ import {
4
+ readServerConfig,
5
+ readHeadersConfig,
6
+ readRedirectsConfig,
7
+ readLayoutConfig,
8
+ readEnvConfig,
9
+ readProxyConfig,
10
+ readWorkerConfig,
11
+ scanRoots,
12
+ scanRouteHandlers,
13
+ } from '../../deployment-pi/util.js';
14
+ import { start as _start } from './index.js';
15
+
16
+ export async function bootstrap(cx, offset = '', runtimeMode = false) {
17
+ const $init = Fs.existsSync('./init.server.js')
18
+ ? Path.resolve('./init.server.js')
19
+ : null;
20
+ const config = {
21
+ LAYOUT: await readLayoutConfig(cx),
22
+ ENV: await readEnvConfig(cx),
23
+ SERVER: await readServerConfig(cx),
24
+ HEADERS: await readHeadersConfig(cx),
25
+ REDIRECTS: await readRedirectsConfig(cx),
26
+ PROXY: await readProxyConfig(cx),
27
+ WORKER: await readWorkerConfig(cx),
28
+ };
29
+ config.RUNTIME_LAYOUT = { ...config.LAYOUT };
30
+ config.RUNTIME_DIR = Path.join(process.cwd(), '.webqit/webflo/@runtime');
31
+ if (runtimeMode) {
32
+ for (const name of ['CLIENT_DIR', 'WORKER_DIR', 'SERVER_DIR', 'VIEWS_DIR', 'PUBLIC_DIR']) {
33
+ const originalDir = Path.relative(process.cwd(), config.LAYOUT[name]);
34
+ config.RUNTIME_LAYOUT[name] = `${config.RUNTIME_DIR}/${originalDir}`;
35
+ }
36
+ }
37
+ const routes = {};
38
+ const { PROXY } = config;
39
+ const $roots = PROXY.entries.map((proxy) => proxy.path?.replace(/^\.\//, '')).filter((p) => p);
40
+ const $sparoots = Fs.existsSync(config.LAYOUT.PUBLIC_DIR) ? scanRoots(config.LAYOUT.PUBLIC_DIR, 'index.html') : [];
41
+ const cwd = cx.CWD || process.cwd();
42
+ scanRouteHandlers(config.LAYOUT, 'server', (file, route) => {
43
+ routes[route] = runtimeMode
44
+ ? Path.join(config.RUNTIME_DIR, Path.relative(cwd, file))
45
+ : file;
46
+ }, offset, $roots);
47
+ const outdir = Path.join(config.RUNTIME_DIR, offset);
48
+ return { $init, config, routes, $roots, $sparoots, outdir, offset };
49
+ }
50
+
51
+ export async function start() {
52
+ const cx = this || {};
53
+ const { $init, ...$bootstrap } = await bootstrap(cx, '', true);
54
+
55
+ let init = null;
56
+ if ($init) init = await import($init);
57
+
58
+ return _start({ init, cx, ...$bootstrap });
59
+ }
@@ -1,11 +1,7 @@
1
1
  import { WebfloServer } from './WebfloServer.js';
2
2
 
3
- export async function start() {
4
- const instance = WebfloServer.create(this || {});
3
+ export async function start(bootstrap) {
4
+ const instance = WebfloServer.create(bootstrap);
5
5
  await instance.initialize();
6
6
  return instance;
7
7
  }
8
-
9
- export {
10
- WebfloServer
11
- }