@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.
- package/package.json +13 -34
- package/site/docs/concepts/realtime.md +45 -44
- package/site/docs/getting-started.md +40 -40
- package/src/{Context.js → CLIContext.js} +9 -8
- package/src/build-pi/esbuild-plugin-uselive-transform.js +42 -0
- package/src/{runtime-pi/webflo-client/webflo-codegen.js → build-pi/index.js} +148 -142
- package/src/index.js +3 -1
- package/src/init-pi/index.js +7 -4
- package/src/init-pi/templates/pwa/.gitignore +6 -0
- package/src/init-pi/templates/pwa/.webqit/webflo/client.json +15 -0
- package/src/init-pi/templates/pwa/.webqit/webflo/layout.json +7 -0
- package/src/init-pi/templates/pwa/package.json +2 -2
- package/src/init-pi/templates/pwa/public/manifest.json +2 -2
- package/src/init-pi/templates/web/.gitignore +6 -0
- package/src/init-pi/templates/web/.webqit/webflo/client.json +12 -0
- package/src/init-pi/templates/web/.webqit/webflo/layout.json +7 -0
- package/src/init-pi/templates/web/package.json +2 -2
- package/src/runtime-pi/AppBootstrap.js +38 -0
- package/src/runtime-pi/WebfloRuntime.js +68 -56
- package/src/runtime-pi/apis.js +9 -0
- package/src/runtime-pi/index.js +2 -4
- package/src/runtime-pi/webflo-client/DeviceCapabilities.js +1 -1
- package/src/runtime-pi/webflo-client/WebfloClient.js +33 -36
- package/src/runtime-pi/webflo-client/WebfloRootClient1.js +23 -17
- package/src/runtime-pi/webflo-client/WebfloRootClient2.js +1 -1
- package/src/runtime-pi/webflo-client/WebfloSubClient.js +14 -14
- package/src/runtime-pi/webflo-client/bootstrap.js +38 -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 +154 -116
- package/src/runtime-pi/webflo-fetch/index.js +436 -5
- package/src/runtime-pi/webflo-messaging/wq-message-port.js +1 -1
- package/src/runtime-pi/webflo-routing/HttpCookies.js +1 -1
- package/src/runtime-pi/webflo-routing/HttpEvent.js +12 -11
- package/src/runtime-pi/webflo-routing/HttpUser.js +7 -7
- package/src/runtime-pi/webflo-routing/WebfloRouter.js +12 -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 +138 -200
- 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 +24 -31
- package/src/runtime-pi/webflo-url/Url.js +1 -1
- package/src/runtime-pi/webflo-url/xURL.js +1 -1
- 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 +39 -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 { 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(
|
|
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;
|
|
@@ -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
|
-
|
|
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();
|
|
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
|
-
|
|
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
|
|
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
|
|
97
|
+
LOGGER.info(desc);
|
|
147
98
|
}
|
|
148
99
|
}
|
|
149
|
-
return instanceController;
|
|
150
|
-
}
|
|
151
100
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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
|
|
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.#
|
|
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 =
|
|
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
|
|
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',
|
|
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 {
|
|
541
|
+
const { RUNTIME_LAYOUT, LAYOUT } = this.config;
|
|
613
542
|
const scopeObj = {};
|
|
614
543
|
if (FLAGS['dev']) {
|
|
615
|
-
if (httpEvent.url.pathname === '/@
|
|
616
|
-
const filename = httpEvent.url.searchParams.get('src')
|
|
544
|
+
if (httpEvent.url.pathname === '/@hmr') {
|
|
545
|
+
const filename = httpEvent.url.searchParams.get('src')?.split('?')[0] || '';
|
|
617
546
|
if (filename.endsWith('.js')) {
|
|
618
|
-
|
|
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
|
|
623
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
722
|
-
scopeObj.clientRequestRealtime = scopeObj.client.createRequestRealtime(scopeObj.
|
|
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
|
|
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
|
|
667
|
+
store: this.createStorage(`${scopeObj.url.host}/user:${scopeObj.clientID}`, scopeObj.sessionTTL),
|
|
732
668
|
request: scopeObj.request,
|
|
733
|
-
|
|
669
|
+
client: scopeObj.clientRequestRealtime,
|
|
734
670
|
session: scopeObj.session,
|
|
735
671
|
});
|
|
736
672
|
scopeObj.httpEvent = this.createHttpEvent({
|
|
737
673
|
request: scopeObj.request,
|
|
738
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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 ||
|
|
908
|
-
const
|
|
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(`${
|
|
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(
|
|
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
|
-
}
|