@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.
- package/package.json +5 -20
- package/site/.vitepress/config.ts +1 -1
- package/site/docs/concepts/realtime.md +57 -53
- package/site/docs/concepts/{request-response.md → requests-responses.md} +1 -1
- package/site/docs/concepts/state.md +1 -1
- package/site/docs/getting-started.md +40 -40
- package/src/{Context.js → CLIContext.js} +9 -8
- package/src/build-pi/esbuild-plugin-livejs-transform.js +35 -0
- package/src/{runtime-pi/webflo-client/webflo-codegen.js → build-pi/index.js} +145 -141
- package/src/index.js +3 -1
- package/src/init-pi/index.js +6 -3
- package/src/init-pi/templates/pwa/package.json +2 -2
- package/src/init-pi/templates/web/package.json +2 -2
- package/src/runtime-pi/AppBootstrap.js +38 -0
- package/src/runtime-pi/WebfloRuntime.js +50 -47
- package/src/runtime-pi/apis.js +9 -0
- package/src/runtime-pi/index.js +2 -4
- package/src/runtime-pi/webflo-client/WebfloClient.js +31 -35
- package/src/runtime-pi/webflo-client/WebfloRootClient1.js +16 -14
- package/src/runtime-pi/webflo-client/WebfloSubClient.js +13 -13
- package/src/runtime-pi/webflo-client/bootstrap.js +37 -0
- package/src/runtime-pi/webflo-client/index.js +2 -8
- package/src/runtime-pi/webflo-client/webflo-devmode.js +3 -3
- package/src/runtime-pi/webflo-fetch/LiveResponse.js +127 -96
- package/src/runtime-pi/webflo-fetch/index.js +435 -5
- package/src/runtime-pi/webflo-routing/HttpCookies.js +1 -1
- package/src/runtime-pi/webflo-routing/HttpEvent.js +5 -6
- package/src/runtime-pi/webflo-routing/HttpUser.js +7 -7
- package/src/runtime-pi/webflo-server/ServerSideCookies.js +3 -1
- package/src/runtime-pi/webflo-server/ServerSideSession.js +2 -1
- package/src/runtime-pi/webflo-server/WebfloServer.js +98 -195
- package/src/runtime-pi/webflo-server/bootstrap.js +59 -0
- package/src/runtime-pi/webflo-server/index.js +2 -6
- package/src/runtime-pi/webflo-server/webflo-devmode.js +13 -24
- package/src/runtime-pi/webflo-worker/WebfloWorker.js +11 -15
- package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +2 -1
- package/src/runtime-pi/webflo-worker/bootstrap.js +38 -0
- package/src/runtime-pi/webflo-worker/index.js +3 -7
- package/src/webflo-cli.js +1 -2
- package/src/runtime-pi/webflo-fetch/cookies.js +0 -10
- package/src/runtime-pi/webflo-fetch/fetch.js +0 -16
- package/src/runtime-pi/webflo-fetch/formdata.js +0 -54
- package/src/runtime-pi/webflo-fetch/headers.js +0 -151
- package/src/runtime-pi/webflo-fetch/message.js +0 -49
- package/src/runtime-pi/webflo-fetch/request.js +0 -62
- 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(
|
|
51
|
-
return new this(
|
|
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
|
-
#
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
89
|
+
LOGGER.info(desc);
|
|
147
90
|
}
|
|
148
91
|
}
|
|
149
|
-
return instanceController;
|
|
150
|
-
}
|
|
151
92
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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
|
|
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.#
|
|
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 =
|
|
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
|
|
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',
|
|
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 {
|
|
507
|
+
const { RUNTIME_LAYOUT, LAYOUT } = this.config;
|
|
613
508
|
const scopeObj = {};
|
|
614
509
|
if (FLAGS['dev']) {
|
|
615
|
-
if (httpEvent.url.pathname === '/@
|
|
616
|
-
const filename = httpEvent.url.searchParams.get('src')
|
|
510
|
+
if (httpEvent.url.pathname === '/@hmr') {
|
|
511
|
+
const filename = httpEvent.url.searchParams.get('src')?.split('?')[0] || '';
|
|
617
512
|
if (filename.endsWith('.js')) {
|
|
618
|
-
|
|
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
|
|
623
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
722
|
-
scopeObj.clientRequestRealtime = scopeObj.client.createRequestRealtime(scopeObj.
|
|
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
|
|
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
|
|
632
|
+
store: this.createStorage(`${scopeObj.url.host}/user:${scopeObj.clientID}`, scopeObj.sessionTTL),
|
|
732
633
|
request: scopeObj.request,
|
|
733
|
-
|
|
634
|
+
client: scopeObj.clientRequestRealtime,
|
|
734
635
|
session: scopeObj.session,
|
|
735
636
|
});
|
|
736
637
|
scopeObj.httpEvent = this.createHttpEvent({
|
|
737
638
|
request: scopeObj.request,
|
|
738
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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 ||
|
|
908
|
-
const
|
|
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(`${
|
|
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(
|
|
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
|
-
}
|