@webqit/webflo 0.20.4-next.1 → 0.20.4-next.3

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 (46) hide show
  1. package/package.json +5 -20
  2. package/site/.vitepress/config.ts +1 -1
  3. package/site/docs/concepts/realtime.md +57 -53
  4. package/site/docs/concepts/{request-response.md → requests-responses.md} +1 -1
  5. package/site/docs/concepts/state.md +1 -1
  6. package/site/docs/getting-started.md +40 -40
  7. package/src/{Context.js → CLIContext.js} +9 -8
  8. package/src/build-pi/esbuild-plugin-livejs-transform.js +35 -0
  9. package/src/{runtime-pi/webflo-client/webflo-codegen.js → build-pi/index.js} +145 -141
  10. package/src/index.js +3 -1
  11. package/src/init-pi/index.js +6 -3
  12. package/src/init-pi/templates/pwa/package.json +2 -2
  13. package/src/init-pi/templates/web/package.json +2 -2
  14. package/src/runtime-pi/AppBootstrap.js +38 -0
  15. package/src/runtime-pi/WebfloRuntime.js +50 -47
  16. package/src/runtime-pi/apis.js +9 -0
  17. package/src/runtime-pi/index.js +2 -4
  18. package/src/runtime-pi/webflo-client/WebfloClient.js +31 -35
  19. package/src/runtime-pi/webflo-client/WebfloRootClient1.js +16 -14
  20. package/src/runtime-pi/webflo-client/WebfloSubClient.js +13 -13
  21. package/src/runtime-pi/webflo-client/bootstrap.js +37 -0
  22. package/src/runtime-pi/webflo-client/index.js +2 -8
  23. package/src/runtime-pi/webflo-client/webflo-devmode.js +3 -3
  24. package/src/runtime-pi/webflo-fetch/LiveResponse.js +127 -96
  25. package/src/runtime-pi/webflo-fetch/index.js +435 -5
  26. package/src/runtime-pi/webflo-routing/HttpCookies.js +1 -1
  27. package/src/runtime-pi/webflo-routing/HttpEvent.js +5 -6
  28. package/src/runtime-pi/webflo-routing/HttpUser.js +7 -7
  29. package/src/runtime-pi/webflo-server/ServerSideCookies.js +3 -1
  30. package/src/runtime-pi/webflo-server/ServerSideSession.js +2 -1
  31. package/src/runtime-pi/webflo-server/WebfloServer.js +98 -195
  32. package/src/runtime-pi/webflo-server/bootstrap.js +59 -0
  33. package/src/runtime-pi/webflo-server/index.js +2 -6
  34. package/src/runtime-pi/webflo-server/webflo-devmode.js +13 -24
  35. package/src/runtime-pi/webflo-worker/WebfloWorker.js +11 -15
  36. package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +2 -1
  37. package/src/runtime-pi/webflo-worker/bootstrap.js +38 -0
  38. package/src/runtime-pi/webflo-worker/index.js +3 -7
  39. package/src/webflo-cli.js +1 -2
  40. package/src/runtime-pi/webflo-fetch/cookies.js +0 -10
  41. package/src/runtime-pi/webflo-fetch/fetch.js +0 -16
  42. package/src/runtime-pi/webflo-fetch/formdata.js +0 -54
  43. package/src/runtime-pi/webflo-fetch/headers.js +0 -151
  44. package/src/runtime-pi/webflo-fetch/message.js +0 -49
  45. package/src/runtime-pi/webflo-fetch/request.js +0 -62
  46. 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 { LiveJSTransform } from '../../build-pi/esbuild-plugin-livejs-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;
@@ -78,59 +59,21 @@ export class WebfloServer extends WebfloRuntime {
78
59
  async initialize() {
79
60
  const instanceController = await super.initialize();
80
61
  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();
62
+
63
+ // ----------
64
+ // Initialize routes
113
65
  if (FLAGS['dev']) {
114
66
  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
67
  } else {
128
- LOGGER?.info(`> Server not running! No port specified.`);
68
+ await this.buildRoutes();
129
69
  }
130
- // -----------------
70
+
71
+ // ----------
72
+ // Show proxies
73
+ const { PROXY } = this.config;
131
74
  if (PROXY.entries.length) {
132
75
  // Show active proxies
133
- LOGGER?.info(`> Reverse proxies active.`);
76
+ LOGGER.info(`> Reverse proxies active.`);
134
77
  for (const proxy of PROXY.entries) {
135
78
  let desc = `> ${proxy.hostnames.join('|')} >>> ${proxy.port || proxy.path}`;
136
79
  // Start a proxy recursively?
@@ -143,101 +86,52 @@ export class WebfloServer extends WebfloRuntime {
143
86
  shell: true // for Windows compatibility
144
87
  });
145
88
  }
146
- LOGGER?.info(desc);
89
+ LOGGER.info(desc);
147
90
  }
148
91
  }
149
- return instanceController;
150
- }
151
92
 
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' });
93
+ // ----------
94
+ // Start serving
95
+ this.control();
96
+
97
+ // ----------
98
+ // Show server details
99
+ if (this.#servers.size) {
100
+ LOGGER.info(`> Server running! (${APP_META.title || ''}) ✅`);
101
+ for (let [proto, def] of this.#servers) {
102
+ LOGGER.info(`> ${proto.toUpperCase()} / ${def.hostnames.concat('').join(`:${def.port} / `)}`);
184
103
  }
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
104
  } 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
- }
105
+ LOGGER.info(`> No servers running!`);
234
106
  }
107
+
235
108
  return instanceController;
236
109
  }
237
110
 
111
+ async buildRoutes({ client = false, worker = false, ...options } = {}) {
112
+ 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 })
114
+ .then((files) => files.map((file) => file.replace(/\\/g, '/')));
115
+ const entryNames = routeDirs.length === 1 ? `${Path.relative(process.cwd(), routeDirs[0])}/[dir]/[name]` : `[dir]/[name]`;
116
+ const bundlingConfig = {
117
+ entryPoints,
118
+ outdir: this.config.RUNTIME_DIR,
119
+ entryNames,
120
+ bundle: true,
121
+ format: 'esm',
122
+ minify: false,
123
+ sourcemap: false,
124
+ platform: 'browser', // optional but good for clarity
125
+ treeShaking: true, // Important optimization
126
+ plugins: [ LiveJSTransform() ],
127
+ ...options,
128
+ };
129
+ return await EsBuild.build(bundlingConfig);
130
+ }
131
+
238
132
  async enterDevMode() {
239
133
  const { appMeta, flags: FLAGS } = this.cx;
240
- this.#hmrRegistry = WebfloHMR.manage(this, {
134
+ this.#hmr = WebfloHMR.manage(this, {
241
135
  appMeta,
242
136
  buildScripts: {
243
137
  ['build:html']: FLAGS['build:html'] ?? true,
@@ -246,7 +140,7 @@ export class WebfloServer extends WebfloRuntime {
246
140
  },
247
141
  buildSensitivity: parseInt(FLAGS['build-sensitivity'] || 0),
248
142
  });
249
- await this.#hmrRegistry.buildJS(true);
143
+ await this.#hmr.buildRoutes(true);
250
144
  if (FLAGS['open']) {
251
145
  for (let [proto, def] of this.#servers) {
252
146
  const url = `${proto}://${def.hostnames.find((h) => h !== '*') || 'localhost'}:${def.port}`;
@@ -259,7 +153,7 @@ export class WebfloServer extends WebfloRuntime {
259
153
  const { flags: FLAGS } = this.cx;
260
154
  const { SERVER, PROXY } = this.config;
261
155
  const instanceController = super.control();
262
- // ---------------
156
+
263
157
  if (!FLAGS['test-only'] && !FLAGS['https-only'] && SERVER.port) {
264
158
  const httpServer = Http.createServer((request, response) => this.handleNodeHttpRequest(request, response));
265
159
  httpServer.listen(FLAGS['port'] || SERVER.port);
@@ -273,7 +167,7 @@ export class WebfloServer extends WebfloRuntime {
273
167
  this.handleNodeWsRequest(wss, request, socket, head);
274
168
  });
275
169
  }
276
- // ---------------
170
+
277
171
  if (!FLAGS['test-only'] && !FLAGS['http-only'] && SERVER.https.port) {
278
172
  const httpsServer = Https.createServer((request, response) => this.handleNodeHttpRequest(request, response));
279
173
  httpsServer.listen(SERVER.https.port);
@@ -304,21 +198,22 @@ export class WebfloServer extends WebfloRuntime {
304
198
  this.handleNodeWsRequest(wss, request, socket, head);
305
199
  });
306
200
  }
307
- // ---------------
201
+
308
202
  const wss = new WebSocket.Server({ noServer: true });
309
- // -----------------
203
+
310
204
  process.on('uncaughtException', (err) => {
311
205
  console.error('Uncaught Exception:', err);
312
206
  });
313
207
  process.on('unhandledRejection', (reason, promise) => {
314
208
  console.log('Unhandled Rejection', reason, promise);
315
209
  });
210
+
316
211
  return instanceController;
317
212
  }
318
213
 
319
214
  identifyIncoming(request, autoGenerateID = false) {
320
215
  const secret = this.env('SESSION_KEY');
321
- let clientID = request.headers.get('Cookie', true).find((c) => c.name === '__sessid')?.value;
216
+ let clientID = headersShim.get.value.call(request.headers, 'Cookie', true).find((c) => c.name === '__sessid')?.value;
322
217
  if (clientID?.includes('.')) {
323
218
  if (secret) {
324
219
  const [rand, signature] = clientID.split('.');
@@ -465,7 +360,7 @@ export class WebfloServer extends WebfloRuntime {
465
360
  if (requestURL.searchParams.get('rel') === 'hmr') {
466
361
  wss.handleUpgrade(nodeRequest, socket, head, (ws) => {
467
362
  wss.emit('connection', ws, nodeRequest);
468
- this.#hmrRegistry.clients.add(ws);
363
+ this.#hmr.clients.add(ws);
469
364
  });
470
365
  }
471
366
  if (requestURL.searchParams.get('rel') === 'background-messaging') {
@@ -497,7 +392,7 @@ export class WebfloServer extends WebfloRuntime {
497
392
  if (existing) nodeResponse.setHeader(name, [].concat(existing).concat(value));
498
393
  else nodeResponse.setHeader(name, value);
499
394
  }
500
- nodeResponse.statusCode = response.status;
395
+ nodeResponse.statusCode = responseShim.prototype.status.get.call(response);
501
396
  nodeResponse.statusMessage = response.statusText;
502
397
  if (response.body instanceof Readable) {
503
398
  response.body.pipe(nodeResponse);
@@ -571,7 +466,7 @@ export class WebfloServer extends WebfloRuntime {
571
466
  }
572
467
 
573
468
  writeRedirectHeaders(httpEvent, response) {
574
- const $sparoots = this.#routes.$sparoots;
469
+ const $sparoots = this.bootstrap.$sparoots;
575
470
  const xRedirectPolicy = httpEvent.request.headers.get('X-Redirect-Policy');
576
471
  const xRedirectCode = httpEvent.request.headers.get('X-Redirect-Code') || 300;
577
472
  const destinationURL = new URL(response.headers.get('Location'), httpEvent.url.origin);
@@ -584,7 +479,7 @@ export class WebfloServer extends WebfloRuntime {
584
479
  isSameSpaRedirect = matchRoot(destinationURL.pathname) === matchRoot(httpEvent.url.pathname);
585
480
  }
586
481
  if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (!isSameSpaRedirect && xRedirectPolicy === 'manual-when-cross-spa')) {
587
- response.headers.set('X-Redirect-Code', response.status);
482
+ response.headers.set('X-Redirect-Code', responseShim.prototype.status.get.call(response));
588
483
  response.headers.set('Access-Control-Allow-Origin', '*');
589
484
  response.headers.set('Cache-Control', 'no-store');
590
485
  const responseMeta = _wq(response, 'meta');
@@ -609,18 +504,23 @@ export class WebfloServer extends WebfloRuntime {
609
504
 
610
505
  async localFetch(httpEvent) {
611
506
  const { flags: FLAGS } = this.cx;
612
- const { DEV_LAYOUT, LAYOUT } = this.config;
507
+ const { RUNTIME_LAYOUT, LAYOUT } = this.config;
613
508
  const scopeObj = {};
614
509
  if (FLAGS['dev']) {
615
- if (httpEvent.url.pathname === '/@dev') {
616
- const filename = httpEvent.url.searchParams.get('src').split('?')[0];
510
+ if (httpEvent.url.pathname === '/@hmr') {
511
+ const filename = httpEvent.url.searchParams.get('src')?.split('?')[0] || '';
617
512
  if (filename.endsWith('.js')) {
618
- scopeObj.filename = Path.join(DEV_LAYOUT.PUBLIC_DIR, filename);
513
+ // This is purely a route handler source request from HMR
514
+ scopeObj.filename = Path.join(RUNTIME_LAYOUT.PUBLIC_DIR, filename);
619
515
  } else {
516
+ // This is a static asset (HTML) request from HMR
620
517
  scopeObj.filename = Path.join(LAYOUT.PUBLIC_DIR, filename);
621
518
  }
622
- } else if (this.#hmrRegistry.options.buildSensitivity === 2) {
623
- await this.#hmrRegistry.bundleAssetsIfPending();
519
+ } else {
520
+ if (this.#hmr.options.buildSensitivity === 1) {
521
+ // This is a static asset request in dev mode but NOT from HMR
522
+ await this.#hmr.bundleAssetsIfPending();
523
+ }
624
524
  scopeObj.filename = Path.join(LAYOUT.PUBLIC_DIR, httpEvent.url.pathname.split('?')[0]);
625
525
  }
626
526
  } else {
@@ -676,7 +576,8 @@ export class WebfloServer extends WebfloRuntime {
676
576
  // Range support
677
577
  const readStream = (params = {}) => Fs.createReadStream(scopeObj.filename, { ...params });
678
578
  scopeObj.response = this.createStreamingResponse(httpEvent, readStream, scopeObj.stats);
679
- if (scopeObj.response.status === 416) return finalizeResponse(scopeObj.response);
579
+ const statusCode = responseShim.prototype.status.get.call(scopeObj.response);
580
+ if (statusCode === 416) return finalizeResponse(scopeObj.response);
680
581
  // ------ If we get here, it means we're good ------
681
582
  if (scopeObj.enc) {
682
583
  scopeObj.response.headers.set('Content-Encoding', scopeObj.enc);
@@ -698,7 +599,7 @@ export class WebfloServer extends WebfloRuntime {
698
599
  scopeObj.response.headers.set('Accept-Ranges', 'bytes');
699
600
  // 6. Qualify Service-Worker responses
700
601
  if (httpEvent.request.headers.get('Service-Worker') === 'script') {
701
- scopeObj.response.headers.set('Service-Worker-Allowed', this.#config.WORKER.scope || '/');
602
+ scopeObj.response.headers.set('Service-Worker-Allowed', this.config.WORKER.scope || '/');
702
603
  }
703
604
  return finalizeResponse(scopeObj.response);
704
605
  }
@@ -718,35 +619,34 @@ export class WebfloServer extends WebfloRuntime {
718
619
  });
719
620
  scopeObj.clientID = this.identifyIncoming(scopeObj.request, true);
720
621
  scopeObj.client = this.#clients.getClient(scopeObj.clientID, true);
721
- scopeObj.realtimePortID = crypto.randomUUID();
722
- scopeObj.clientRequestRealtime = scopeObj.client.createRequestRealtime(scopeObj.realtimePortID, scopeObj.request.url);
622
+ scopeObj.clientPortID = crypto.randomUUID();
623
+ scopeObj.clientRequestRealtime = scopeObj.client.createRequestRealtime(scopeObj.clientPortID, scopeObj.request.url);
723
624
  scopeObj.sessionTTL = this.env('SESSION_TTL') || 2592000/*30days*/;
724
625
  scopeObj.session = this.createHttpSession({
725
- store: this.#sdk.storage?.(`${scopeObj.url.host}/session:${scopeObj.clientID}`, scopeObj.sessionTTL),
626
+ store: this.createStorage(`${scopeObj.url.host}/session:${scopeObj.clientID}`, scopeObj.sessionTTL),
726
627
  request: scopeObj.request,
727
628
  sessionID: scopeObj.clientID,
728
629
  ttl: scopeObj.sessionTTL
729
630
  });
730
631
  scopeObj.user = this.createHttpUser({
731
- store: this.#sdk.storage?.(`${scopeObj.url.host}/user:${scopeObj.clientID}`, scopeObj.sessionTTL),
632
+ store: this.createStorage(`${scopeObj.url.host}/user:${scopeObj.clientID}`, scopeObj.sessionTTL),
732
633
  request: scopeObj.request,
733
- realtime: scopeObj.clientRequestRealtime,
634
+ client: scopeObj.clientRequestRealtime,
734
635
  session: scopeObj.session,
735
636
  });
736
637
  scopeObj.httpEvent = this.createHttpEvent({
737
638
  request: scopeObj.request,
738
- realtime: scopeObj.clientRequestRealtime,
639
+ client: scopeObj.clientRequestRealtime,
739
640
  cookies: scopeObj.cookies,
740
641
  session: scopeObj.session,
741
642
  user: scopeObj.user,
742
643
  detail: scopeObj.detail,
743
- sdk: this.#sdk,
744
644
  });
745
645
  // Dispatch for response
746
646
  scopeObj.response = await this.dispatchNavigationEvent({
747
647
  httpEvent: scopeObj.httpEvent,
748
648
  crossLayerFetch: (event) => this.localFetch(event),
749
- responseRealtime: `ws:${scopeObj.httpEvent.realtime.portID}?rel=background-messaging`
649
+ clientPortB: `ws:${scopeObj.httpEvent.client.portID}?rel=background-messaging`
750
650
  });
751
651
  // Reponse handlers
752
652
  if (FLAGS['dev']) {
@@ -765,12 +665,13 @@ export class WebfloServer extends WebfloRuntime {
765
665
  }
766
666
 
767
667
  async satisfyRequestFormat(httpEvent, response) {
768
- if (response.status === 206 || response.status === 416) {
668
+ const statusCode = responseShim.prototype.status.get.call(response);
669
+ if (statusCode === 206 || statusCode === 416) {
769
670
  // If the response is a partial content, we don't need to do anything else
770
671
  return response;
771
672
  }
772
673
  // Satisfy "Accept" header
773
- const requestAccept = httpEvent.request.headers.get('Accept', true);
674
+ const requestAccept = headersShim.get.value.call(httpEvent.request.headers, 'Accept', true);
774
675
  const asHTML = requestAccept?.match('text/html');
775
676
  const asIs = requestAccept?.match(response.headers.get('Content-Type'));
776
677
  const responseMeta = _wq(response, 'meta');
@@ -785,7 +686,7 @@ export class WebfloServer extends WebfloRuntime {
785
686
  response.headers.append('Vary', 'Accept');
786
687
  }
787
688
  // Satisfy "Range" header
788
- const requestRange = httpEvent.request.headers.get('Range', true);
689
+ const requestRange = headersShim.get.value.call(httpEvent.request.headers, 'Range', true);
789
690
  if (requestRange.length && response.headers.get('Content-Length')) {
790
691
  const stats = {
791
692
  size: parseInt(response.headers.get('Content-Length')),
@@ -827,7 +728,7 @@ export class WebfloServer extends WebfloRuntime {
827
728
  if (document.readyState === 'complete') return res(1);
828
729
  document.addEventListener('load', res);
829
730
  });
830
- const data = await response.parse();
731
+ const data = await responseShim.prototype.parse.value.call(response);
831
732
  if (window.webqit?.oohtml?.config) {
832
733
  // Await rendering engine
833
734
  if (window.webqit?.$qCompilerWorker) {
@@ -879,7 +780,7 @@ export class WebfloServer extends WebfloRuntime {
879
780
  const rendering = window.toString();
880
781
  document.documentElement.remove();
881
782
  document.writeln('');
882
- try { window.close(); } catch (e) {}
783
+ try { window.close(); } catch (e) { }
883
784
  return rendering;
884
785
  });
885
786
  // Validate rendering
@@ -887,9 +788,10 @@ export class WebfloServer extends WebfloRuntime {
887
788
  throw new Error('render() must return a string response or an object that implements toString()..');
888
789
  }
889
790
  // Convert back to response
791
+ const statusCode = responseShim.prototype.status.get.call(response);
890
792
  scopeObj.response = new Response(scopeObj.rendering, {
891
793
  headers: response.headers,
892
- status: response.status,
794
+ status: statusCode,
893
795
  statusText: response.statusText,
894
796
  });
895
797
  scopeObj.response.headers.set('Content-Type', 'text/html');
@@ -902,10 +804,11 @@ export class WebfloServer extends WebfloRuntime {
902
804
  const log = [];
903
805
  // ---------------
904
806
  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;
807
+ const statusCode = responseShim.prototype.status.get.call(response);
808
+ const errorCode = statusCode >= 400 && statusCode < 500 ? statusCode : 0;
906
809
  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;
810
+ const isRedirect = (xRedirectCode || statusCode + '').startsWith('3') && (xRedirectCode || statusCode) !== 304;
811
+ const _statusCode = xRedirectCode && `${xRedirectCode} (${statusCode})` || statusCode;
909
812
  const responseMeta = _wq(response, 'meta');
910
813
  // ---------------
911
814
  log.push(`[${style.comment((new Date).toUTCString())}]`);
@@ -917,7 +820,7 @@ export class WebfloServer extends WebfloRuntime {
917
820
  if (contentInfo.length) log.push(`(${style.comment(contentInfo.join('; '))})`);
918
821
  if (response.headers.get('Content-Encoding')) log.push(`(${style.comment(response.headers.get('Content-Encoding'))})`);
919
822
  if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
920
- else log.push(style.val(`${statusCode} ${response.statusText}`));
823
+ else log.push(style.val(`${_statusCode} ${response.statusText}`));
921
824
  if (isRedirect) log.push(`- ${style.url(response.headers.get('Location'))}`);
922
825
  return log.join(' ');
923
826
  }
@@ -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
- }