toiljs 0.0.59 → 0.0.61
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/.github/workflows/ci.yml +31 -0
- package/CHANGELOG.md +15 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +311 -118
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/index.d.ts +1 -1
- package/build/client/index.js +1 -1
- package/build/client/routing/mount.js +12 -1
- package/build/client/ssr/markers.d.ts +1 -0
- package/build/client/ssr/markers.js +3 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +21 -0
- package/build/compiler/config.js +35 -0
- package/build/compiler/docs.d.ts +2 -1
- package/build/compiler/docs.js +33 -304
- package/build/compiler/index.d.ts +13 -0
- package/build/compiler/index.js +113 -21
- package/build/compiler/template-build.d.ts +21 -1
- package/build/compiler/template-build.js +110 -26
- package/build/compiler/toil-docs.generated.d.ts +1 -0
- package/build/compiler/toil-docs.generated.js +20 -0
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/catalog.d.ts +26 -0
- package/build/devserver/daemon/catalog.js +48 -0
- package/build/devserver/daemon/cron.d.ts +4 -0
- package/build/devserver/daemon/cron.js +50 -0
- package/build/devserver/daemon/host.d.ts +37 -0
- package/build/devserver/daemon/host.js +94 -0
- package/build/devserver/daemon/index.d.ts +34 -0
- package/build/devserver/daemon/index.js +241 -0
- package/build/devserver/db/catalog.d.ts +2 -0
- package/build/devserver/db/catalog.js +80 -0
- package/build/devserver/db/database.d.ts +80 -0
- package/build/devserver/db/database.js +1032 -0
- package/build/devserver/db/index.d.ts +3 -0
- package/build/devserver/db/index.js +3 -0
- package/build/devserver/db/routeKinds.d.ts +8 -0
- package/build/devserver/db/routeKinds.js +139 -0
- package/build/devserver/db/types.d.ts +121 -0
- package/build/devserver/db/types.js +52 -0
- package/build/devserver/email/index.js +1 -1
- package/build/devserver/index.d.ts +19 -24
- package/build/devserver/index.js +11 -165
- package/build/devserver/mstore/store.d.ts +18 -0
- package/build/devserver/mstore/store.js +82 -0
- package/build/devserver/{host.d.ts → runtime/host.d.ts} +7 -1
- package/build/devserver/{host.js → runtime/host.js} +51 -7
- package/build/devserver/{module.d.ts → runtime/module.d.ts} +2 -1
- package/build/devserver/{module.js → runtime/module.js} +34 -1
- package/build/devserver/server.d.ts +23 -0
- package/build/devserver/server.js +223 -0
- package/build/devserver/ssr.d.ts +25 -0
- package/build/devserver/ssr.js +114 -0
- package/build/devserver/wasm/sections.d.ts +2 -0
- package/build/devserver/wasm/sections.js +42 -0
- package/build/devserver/wasm/surface.d.ts +18 -0
- package/build/devserver/wasm/surface.js +41 -0
- package/docs/README.md +4 -4
- package/docs/auth-todo.md +6 -6
- package/docs/caching.md +5 -5
- package/docs/cli.md +15 -0
- package/docs/client.md +40 -0
- package/docs/crypto.md +4 -4
- package/docs/data.md +6 -6
- package/docs/email.md +28 -28
- package/docs/environment.md +10 -10
- package/docs/index.md +26 -0
- package/docs/ratelimit.md +10 -10
- package/docs/routing.md +2 -2
- package/docs/server.md +61 -0
- package/docs/ssr.md +561 -113
- package/docs/styling.md +22 -0
- package/docs/time.md +3 -3
- package/eslint.config.js +10 -1
- package/examples/basic/client/components/Header.tsx +3 -0
- package/examples/basic/client/routes/features/actions.tsx +0 -2
- package/examples/basic/client/routes/hello.tsx +89 -19
- package/examples/basic/client/styles/main.css +48 -0
- package/examples/basic/server/SsrHelloRender.ts +97 -0
- package/examples/basic/server/main.ts +5 -0
- package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
- package/examples/basic/server/streams/Echo.ts +49 -0
- package/package.json +12 -10
- package/scripts/gen-toil-docs.mjs +96 -0
- package/server/runtime/time.ts +3 -3
- package/src/cli/create.ts +40 -3
- package/src/cli/db.ts +158 -0
- package/src/cli/diagnostics.ts +19 -0
- package/src/cli/doctor.ts +20 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/update.ts +58 -0
- package/src/client/index.ts +1 -1
- package/src/client/routing/mount.tsx +18 -2
- package/src/client/ssr/markers.tsx +22 -0
- package/src/compiler/config.ts +88 -2
- package/src/compiler/docs.ts +47 -308
- package/src/compiler/index.ts +236 -32
- package/src/compiler/ssr-codegen.ts +1 -1
- package/src/compiler/template-build.ts +247 -46
- package/src/compiler/toil-docs.generated.ts +26 -0
- package/src/devserver/daemon/catalog.ts +120 -0
- package/src/devserver/daemon/cron.ts +87 -0
- package/src/devserver/daemon/host.ts +224 -0
- package/src/devserver/daemon/index.ts +349 -0
- package/src/devserver/db/catalog.ts +108 -0
- package/src/devserver/db/database.ts +1633 -0
- package/src/devserver/db/index.ts +18 -0
- package/src/devserver/db/routeKinds.ts +147 -0
- package/src/devserver/db/types.ts +139 -0
- package/src/devserver/email/index.ts +1 -1
- package/src/devserver/index.ts +31 -287
- package/src/devserver/mstore/store.ts +121 -0
- package/src/devserver/{host.ts → runtime/host.ts} +98 -7
- package/src/devserver/{module.ts → runtime/module.ts} +47 -1
- package/src/devserver/server.ts +393 -0
- package/src/devserver/ssr.ts +166 -0
- package/src/devserver/wasm/sections.ts +59 -0
- package/src/devserver/wasm/surface.ts +88 -0
- package/test/daemon-build.test.ts +198 -0
- package/test/daemon-catalog.test.ts +265 -0
- package/test/daemon-emulation.test.ts +216 -0
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +510 -14
- package/test/devserver-pqauth.test.ts +1 -1
- package/test/devserver-secrets.test.ts +5 -1
- package/test/doctor.test.ts +13 -0
- package/test/email-preview.test.ts +6 -1
- package/test/example-guestbook.test.ts +43 -1
- package/test/fixtures/daemon-app.ts +56 -0
- package/test/global-setup.ts +17 -0
- package/test/pqauth-e2e.test.ts +1 -1
- package/test/ssr-render.test.ts +94 -27
- package/test/ssr-template.test.tsx +44 -1
- package/vitest.config.ts +3 -0
- package/build/devserver/database.d.ts +0 -8
- package/build/devserver/database.js +0 -418
- package/src/devserver/database.ts +0 -618
- /package/build/devserver/{dotenv.d.ts → config/dotenv.d.ts} +0 -0
- /package/build/devserver/{dotenv.js → config/dotenv.js} +0 -0
- /package/build/devserver/{env.d.ts → config/env.d.ts} +0 -0
- /package/build/devserver/{env.js → config/env.js} +0 -0
- /package/build/devserver/{ratelimit.d.ts → config/ratelimit.d.ts} +0 -0
- /package/build/devserver/{ratelimit.js → config/ratelimit.js} +0 -0
- /package/build/devserver/{cache.d.ts → http/cache.d.ts} +0 -0
- /package/build/devserver/{cache.js → http/cache.js} +0 -0
- /package/build/devserver/{envelope.d.ts → http/envelope.d.ts} +0 -0
- /package/build/devserver/{envelope.js → http/envelope.js} +0 -0
- /package/build/devserver/{proxy.d.ts → http/proxy.d.ts} +0 -0
- /package/build/devserver/{proxy.js → http/proxy.js} +0 -0
- /package/build/devserver/{crypto.d.ts → runtime/crypto.d.ts} +0 -0
- /package/build/devserver/{crypto.js → runtime/crypto.js} +0 -0
- /package/src/devserver/{dotenv.ts → config/dotenv.ts} +0 -0
- /package/src/devserver/{env.ts → config/env.ts} +0 -0
- /package/src/devserver/{ratelimit.ts → config/ratelimit.ts} +0 -0
- /package/src/devserver/{cache.ts → http/cache.ts} +0 -0
- /package/src/devserver/{envelope.ts → http/envelope.ts} +0 -0
- /package/src/devserver/{proxy.ts → http/proxy.ts} +0 -0
- /package/src/devserver/{crypto.ts → runtime/crypto.ts} +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { devEnvGet, devEnvGetSecure } from '../config/env.js';
|
|
2
|
+
import { ratelimitCheck } from '../config/ratelimit.js';
|
|
3
|
+
import { buildDatabaseImports, freshDbState } from '../db/index.js';
|
|
4
|
+
import { EmailStatus, getEmailService } from '../email/index.js';
|
|
5
|
+
import { parseEmailBlob } from '../email/wire.js';
|
|
1
6
|
import { buildCryptoImports, freshCryptoState } from './crypto.js';
|
|
2
|
-
import { buildDatabaseImports, freshDbState } from './database.js';
|
|
3
|
-
import { EmailStatus, getEmailService } from './email/index.js';
|
|
4
|
-
import { parseEmailBlob } from './email/wire.js';
|
|
5
|
-
import { devEnvGet, devEnvGetSecure } from './env.js';
|
|
6
|
-
import { ratelimitCheck } from './ratelimit.js';
|
|
7
7
|
const MAX_TOTAL_HEADERS_BYTES = 64 * 1024;
|
|
8
8
|
const MAX_HEADER_NAME_LEN = 256;
|
|
9
9
|
const MAX_HEADER_VALUE_LEN = 8192;
|
|
@@ -32,12 +32,21 @@ function mem(ref) {
|
|
|
32
32
|
throw new Error('host import called before memory was bound');
|
|
33
33
|
return Buffer.from(ref.memory.buffer);
|
|
34
34
|
}
|
|
35
|
-
function readBytes(ref, ptr, len) {
|
|
35
|
+
export function readBytes(ref, ptr, len) {
|
|
36
36
|
const m = mem(ref);
|
|
37
37
|
if (ptr < 0 || len < 0 || ptr + len > m.length)
|
|
38
38
|
throw new Error(`host import read out of bounds: ptr=${String(ptr)} len=${String(len)}`);
|
|
39
39
|
return m.subarray(ptr, ptr + len);
|
|
40
40
|
}
|
|
41
|
+
export function writeBytesOut(ref, bytes, outPtr, outCap) {
|
|
42
|
+
if (bytes.length > outCap)
|
|
43
|
+
return -1;
|
|
44
|
+
const m = mem(ref);
|
|
45
|
+
if (outPtr < 0 || outPtr + bytes.length > m.length)
|
|
46
|
+
throw new Error('host import write out of bounds');
|
|
47
|
+
bytes.copy(m, outPtr);
|
|
48
|
+
return bytes.length;
|
|
49
|
+
}
|
|
41
50
|
function readGuestString(ref, ptr) {
|
|
42
51
|
if (ptr === 0)
|
|
43
52
|
return '';
|
|
@@ -80,6 +89,41 @@ function envLookup(ref, keyPtr, keyLen, outPtr, outCap, secure) {
|
|
|
80
89
|
bytes.copy(m, outPtr);
|
|
81
90
|
return bytes.length;
|
|
82
91
|
}
|
|
92
|
+
export function buildEnvImports(ref, _state) {
|
|
93
|
+
return {
|
|
94
|
+
abort: (msgPtr, filePtr, line, col) => {
|
|
95
|
+
throw new WasmAbortError(readGuestString(ref, msgPtr), readGuestString(ref, filePtr), line, col);
|
|
96
|
+
},
|
|
97
|
+
env_get: (keyPtr, keyLen, outPtr, outCap) => envLookup(ref, keyPtr, keyLen, outPtr, outCap, false),
|
|
98
|
+
env_get_secure: (keyPtr, keyLen, outPtr, outCap) => envLookup(ref, keyPtr, keyLen, outPtr, outCap, true),
|
|
99
|
+
thread_spawn: (_startArg) => -1,
|
|
100
|
+
'Date.now': () => BigInt(Date.now()),
|
|
101
|
+
email_send: (reqPtr, reqLen) => {
|
|
102
|
+
const raw = readBytes(ref, reqPtr, reqLen);
|
|
103
|
+
const svc = getEmailService();
|
|
104
|
+
if (svc === null) {
|
|
105
|
+
const to = parseEmailBlob(raw)?.to ?? '<unparsed>';
|
|
106
|
+
process.stdout.write(` ✉ dev email_send -> ${to} (no email config; not sent)\n`);
|
|
107
|
+
return EmailStatus.Sent;
|
|
108
|
+
}
|
|
109
|
+
const { status, parsed } = svc.prepare(raw);
|
|
110
|
+
if (parsed === null) {
|
|
111
|
+
process.stdout.write(` ✉ dev email_send -> ${EmailStatus[status]}\n`);
|
|
112
|
+
return status;
|
|
113
|
+
}
|
|
114
|
+
void svc
|
|
115
|
+
.deliver(parsed)
|
|
116
|
+
.then((s) => {
|
|
117
|
+
const label = s === EmailStatus.Sent ? 'sent' : EmailStatus[s];
|
|
118
|
+
process.stdout.write(` ✉ dev email_send -> ${parsed.to} (${label})\n`);
|
|
119
|
+
})
|
|
120
|
+
.catch((e) => {
|
|
121
|
+
process.stdout.write(` ✉ dev email_send -> ${parsed.to} (error: ${String(e)})\n`);
|
|
122
|
+
});
|
|
123
|
+
return EmailStatus.Sent;
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
83
127
|
export function buildHostImports(ref, state) {
|
|
84
128
|
return {
|
|
85
129
|
env: {
|
|
@@ -157,7 +201,7 @@ export function buildHostImports(ref, state) {
|
|
|
157
201
|
env_get: (keyPtr, keyLen, outPtr, outCap) => envLookup(ref, keyPtr, keyLen, outPtr, outCap, false),
|
|
158
202
|
env_get_secure: (keyPtr, keyLen, outPtr, outCap) => envLookup(ref, keyPtr, keyLen, outPtr, outCap, true),
|
|
159
203
|
thread_spawn: (_startArg) => -1,
|
|
160
|
-
'Date.now': () => Date.now(),
|
|
204
|
+
'Date.now': () => BigInt(Date.now()),
|
|
161
205
|
...buildCryptoImports(ref, state.crypto),
|
|
162
206
|
...buildDatabaseImports(ref, state.db),
|
|
163
207
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type EnvelopeRequest } from '
|
|
1
|
+
import { type EnvelopeRequest } from '../http/envelope.js';
|
|
2
2
|
export { WasmAbortError } from './host.js';
|
|
3
3
|
export declare const UNHANDLED_HEADER = "x-toil-unhandled";
|
|
4
4
|
export interface WasmDispatchResult {
|
|
@@ -12,6 +12,7 @@ export declare class WasmServerModule {
|
|
|
12
12
|
private readonly wasmPath;
|
|
13
13
|
private module;
|
|
14
14
|
private loadedMtimeMs;
|
|
15
|
+
private routeKinds;
|
|
15
16
|
constructor(wasmPath: string);
|
|
16
17
|
get available(): boolean;
|
|
17
18
|
refresh(): boolean;
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { DbFunctionKind, persistDb, setDbCatalog } from '../db/index.js';
|
|
3
|
+
import { parseRouteKinds, routeKindForRequest } from '../db/routeKinds.js';
|
|
4
|
+
import { decodeResponseEnvelope, encodeRequestEnvelope, unpackHandleResult, } from '../http/envelope.js';
|
|
3
5
|
import { buildHostImports, freshDispatchState } from './host.js';
|
|
4
6
|
export { WasmAbortError } from './host.js';
|
|
5
7
|
export const UNHANDLED_HEADER = 'x-toil-unhandled';
|
|
6
8
|
const WASM_PAGE = 65536;
|
|
9
|
+
function dbKindForHttpMethod(method) {
|
|
10
|
+
switch (method.toUpperCase()) {
|
|
11
|
+
case 'GET':
|
|
12
|
+
case 'HEAD':
|
|
13
|
+
case 'OPTIONS':
|
|
14
|
+
return DbFunctionKind.Query;
|
|
15
|
+
case 'POST':
|
|
16
|
+
case 'PUT':
|
|
17
|
+
case 'PATCH':
|
|
18
|
+
case 'DELETE':
|
|
19
|
+
default:
|
|
20
|
+
return DbFunctionKind.Action;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
7
23
|
const PROVIDED_IMPORTS = new Set([
|
|
8
24
|
'abort',
|
|
9
25
|
'set_status',
|
|
@@ -50,6 +66,8 @@ const PROVIDED_IMPORTS = new Set([
|
|
|
50
66
|
'data.counter_get',
|
|
51
67
|
'data.counter_add',
|
|
52
68
|
'data.append',
|
|
69
|
+
'data.append_once',
|
|
70
|
+
'data.enqueue',
|
|
53
71
|
'data.latest',
|
|
54
72
|
'data.capacity_set_total',
|
|
55
73
|
'data.capacity_available',
|
|
@@ -57,11 +75,14 @@ const PROVIDED_IMPORTS = new Set([
|
|
|
57
75
|
'data.capacity_confirm',
|
|
58
76
|
'data.capacity_cancel',
|
|
59
77
|
'data.take_result',
|
|
78
|
+
'data.result_schema_version',
|
|
79
|
+
'data.write_allowed',
|
|
60
80
|
]);
|
|
61
81
|
export class WasmServerModule {
|
|
62
82
|
wasmPath;
|
|
63
83
|
module = null;
|
|
64
84
|
loadedMtimeMs = -1;
|
|
85
|
+
routeKinds = [];
|
|
65
86
|
constructor(wasmPath) {
|
|
66
87
|
this.wasmPath = wasmPath;
|
|
67
88
|
}
|
|
@@ -75,6 +96,7 @@ export class WasmServerModule {
|
|
|
75
96
|
}
|
|
76
97
|
catch {
|
|
77
98
|
this.module = null;
|
|
99
|
+
this.routeKinds = [];
|
|
78
100
|
this.loadedMtimeMs = -1;
|
|
79
101
|
return false;
|
|
80
102
|
}
|
|
@@ -84,6 +106,8 @@ export class WasmServerModule {
|
|
|
84
106
|
const module = new WebAssembly.Module(bytes);
|
|
85
107
|
this.assertImportSurface(module);
|
|
86
108
|
this.assertExportSurface(module);
|
|
109
|
+
setDbCatalog(bytes);
|
|
110
|
+
this.routeKinds = parseRouteKinds(bytes);
|
|
87
111
|
this.module = module;
|
|
88
112
|
this.loadedMtimeMs = mtimeMs;
|
|
89
113
|
return true;
|
|
@@ -95,6 +119,13 @@ export class WasmServerModule {
|
|
|
95
119
|
const ref = { memory: null };
|
|
96
120
|
const state = freshDispatchState();
|
|
97
121
|
state.clientIp = req.clientIp ?? '';
|
|
122
|
+
const routeKind = routeKindForRequest(this.routeKinds, req.method, req.path);
|
|
123
|
+
state.db.functionKind =
|
|
124
|
+
this.routeKinds.length === 0
|
|
125
|
+
? DbFunctionKind.Job
|
|
126
|
+
: routeKind === DbFunctionKind.Query
|
|
127
|
+
? DbFunctionKind.Query
|
|
128
|
+
: dbKindForHttpMethod(req.method);
|
|
98
129
|
const instance = new WebAssembly.Instance(this.module, buildHostImports(ref, state));
|
|
99
130
|
const exports = instance.exports;
|
|
100
131
|
ref.memory = exports.memory;
|
|
@@ -110,6 +141,7 @@ export class WasmServerModule {
|
|
|
110
141
|
const headers = [...resp.headers, ...state.headers];
|
|
111
142
|
const status = state.status ?? resp.status;
|
|
112
143
|
const unhandled = headers.some(([n]) => n.toLowerCase() === UNHANDLED_HEADER);
|
|
144
|
+
persistDb();
|
|
113
145
|
return {
|
|
114
146
|
status,
|
|
115
147
|
headers: headers.filter(([n]) => n.toLowerCase() !== UNHANDLED_HEADER),
|
|
@@ -125,6 +157,7 @@ export class WasmServerModule {
|
|
|
125
157
|
const ref = { memory: null };
|
|
126
158
|
const state = freshDispatchState();
|
|
127
159
|
state.clientIp = req.clientIp ?? '';
|
|
160
|
+
state.db.functionKind = DbFunctionKind.Query;
|
|
128
161
|
const instance = new WebAssembly.Instance(this.module, buildHostImports(ref, state));
|
|
129
162
|
const exports = instance.exports;
|
|
130
163
|
if (typeof exports.render !== 'function')
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EmailBackendConfig } from 'toiljs/shared';
|
|
2
|
+
import type { ResolvedDaemonConfig } from './daemon/host.js';
|
|
3
|
+
import { type ViteTarget } from './http/proxy.js';
|
|
4
|
+
import { type DevSsrTemplate } from './ssr.js';
|
|
5
|
+
export interface DevServerOptions {
|
|
6
|
+
readonly root: string;
|
|
7
|
+
readonly port: number;
|
|
8
|
+
readonly host?: string;
|
|
9
|
+
readonly wasmFile: string;
|
|
10
|
+
readonly coldWasmFile?: string;
|
|
11
|
+
readonly nodeMode?: string;
|
|
12
|
+
readonly daemon?: ResolvedDaemonConfig;
|
|
13
|
+
readonly vite: ViteTarget;
|
|
14
|
+
readonly maxBodyLength?: number;
|
|
15
|
+
readonly email?: EmailBackendConfig;
|
|
16
|
+
readonly ssrTemplates?: readonly DevSsrTemplate[];
|
|
17
|
+
}
|
|
18
|
+
export interface RunningDevServer {
|
|
19
|
+
readonly port: number;
|
|
20
|
+
readonly host: string;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare function startDevServer(options: DevServerOptions): Promise<RunningDevServer>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Server } from '@dacely/hyper-express';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { DaemonHost, daemonEmulationEnabled } from './daemon/index.js';
|
|
6
|
+
import { configureDbPersistence } from './db/index.js';
|
|
7
|
+
import { initEmailService } from './email/index.js';
|
|
8
|
+
import { applyCacheRule, lookupCache } from './http/cache.js';
|
|
9
|
+
import { METHOD_CODES } from './http/envelope.js';
|
|
10
|
+
import { proxyToVite, wireWebsocketProxy } from './http/proxy.js';
|
|
11
|
+
import { WasmServerModule } from './runtime/module.js';
|
|
12
|
+
import { assembleSsr, buildSsrRoutes, pathnameOf, } from './ssr.js';
|
|
13
|
+
const DEFAULT_MAX_BODY_LENGTH = 1024 * 1024 * 8;
|
|
14
|
+
const VITE_PREFIXES = ['/@', '/node_modules/', '/__toil/'];
|
|
15
|
+
const MIME = {
|
|
16
|
+
'.html': 'text/html; charset=utf-8',
|
|
17
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
18
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
19
|
+
'.css': 'text/css; charset=utf-8',
|
|
20
|
+
'.json': 'application/json; charset=utf-8',
|
|
21
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
22
|
+
'.svg': 'image/svg+xml',
|
|
23
|
+
'.png': 'image/png',
|
|
24
|
+
'.jpg': 'image/jpeg',
|
|
25
|
+
'.jpeg': 'image/jpeg',
|
|
26
|
+
'.webp': 'image/webp',
|
|
27
|
+
'.avif': 'image/avif',
|
|
28
|
+
'.gif': 'image/gif',
|
|
29
|
+
'.ico': 'image/x-icon',
|
|
30
|
+
'.wasm': 'application/wasm',
|
|
31
|
+
'.woff2': 'font/woff2',
|
|
32
|
+
};
|
|
33
|
+
function isViteInternal(url) {
|
|
34
|
+
return VITE_PREFIXES.some((p) => url.startsWith(p));
|
|
35
|
+
}
|
|
36
|
+
function resolveSendfile(root, file) {
|
|
37
|
+
const resolved = path.resolve(root, file);
|
|
38
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep))
|
|
39
|
+
return null;
|
|
40
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile())
|
|
41
|
+
return null;
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
async function toEnvelopeRequest(request) {
|
|
45
|
+
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
46
|
+
const body = hasBody ? new Uint8Array(await request.buffer()) : new Uint8Array(0);
|
|
47
|
+
const xff = request.headers['x-forwarded-for'];
|
|
48
|
+
const clientIp = typeof xff === 'string' && xff.length > 0 ? xff.split(',')[0].trim() : '127.0.0.1';
|
|
49
|
+
return {
|
|
50
|
+
method: request.method,
|
|
51
|
+
path: request.url,
|
|
52
|
+
headers: Object.entries(request.headers),
|
|
53
|
+
body,
|
|
54
|
+
clientIp,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function sendWasmResponse(response, root, result) {
|
|
58
|
+
response.status(result.status);
|
|
59
|
+
let hasContentType = false;
|
|
60
|
+
for (const [name, value] of result.headers) {
|
|
61
|
+
if (name.toLowerCase() === 'content-type')
|
|
62
|
+
hasContentType = true;
|
|
63
|
+
response.header(name, value);
|
|
64
|
+
}
|
|
65
|
+
response.header('server', 'toil-dev');
|
|
66
|
+
if (result.sendfile !== null) {
|
|
67
|
+
const file = resolveSendfile(root, result.sendfile);
|
|
68
|
+
if (file === null) {
|
|
69
|
+
response.status(404).send('not found\n');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!hasContentType) {
|
|
73
|
+
response.header('content-type', MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream');
|
|
74
|
+
}
|
|
75
|
+
response.sendFile(file);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!hasContentType)
|
|
79
|
+
response.header('content-type', 'text/plain; charset=utf-8');
|
|
80
|
+
response.send(Buffer.from(result.body.buffer, result.body.byteOffset, result.body.length));
|
|
81
|
+
}
|
|
82
|
+
function sendSsr(response, out, headOnly) {
|
|
83
|
+
response.status(out.status);
|
|
84
|
+
let hasContentType = false;
|
|
85
|
+
for (const [name, value] of out.headers) {
|
|
86
|
+
if (name.toLowerCase() === 'content-type')
|
|
87
|
+
hasContentType = true;
|
|
88
|
+
response.header(name, value);
|
|
89
|
+
}
|
|
90
|
+
if (!hasContentType)
|
|
91
|
+
response.header('content-type', 'text/html; charset=utf-8');
|
|
92
|
+
response.header('server', 'toil-dev');
|
|
93
|
+
if (headOnly) {
|
|
94
|
+
response.send('');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
response.send(Buffer.from(out.html.buffer, out.html.byteOffset, out.html.length));
|
|
98
|
+
}
|
|
99
|
+
export async function startDevServer(options) {
|
|
100
|
+
const host = options.host ?? '127.0.0.1';
|
|
101
|
+
const root = path.resolve(options.root);
|
|
102
|
+
const emailInit = initEmailService(root, options.email);
|
|
103
|
+
if (emailInit.service !== null) {
|
|
104
|
+
process.stdout.write(pc.dim(` ✉ email enabled: ${emailInit.note}`) + '\n');
|
|
105
|
+
}
|
|
106
|
+
else if (emailInit.note !== null) {
|
|
107
|
+
process.stdout.write(pc.yellow(' ! ') + pc.dim(`email off: ${emailInit.note}`) + '\n');
|
|
108
|
+
}
|
|
109
|
+
const module = new WasmServerModule(options.wasmFile);
|
|
110
|
+
const ssrRoutes = buildSsrRoutes(options.ssrTemplates ?? []);
|
|
111
|
+
if (ssrRoutes.length > 0) {
|
|
112
|
+
process.stdout.write(pc.dim(` edge SSR: ${String(ssrRoutes.length)} route(s) served server-side`) + '\n');
|
|
113
|
+
}
|
|
114
|
+
configureDbPersistence(path.join(root, '.toil', 'devdata.json'));
|
|
115
|
+
let warnedMissing = false;
|
|
116
|
+
let loadedOnce = false;
|
|
117
|
+
const refresh = () => {
|
|
118
|
+
try {
|
|
119
|
+
if (module.refresh() && loadedOnce) {
|
|
120
|
+
process.stdout.write(pc.green(' ✓ ') + pc.dim('server module reloaded') + '\n');
|
|
121
|
+
}
|
|
122
|
+
loadedOnce ||= module.available;
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
process.stdout.write(pc.red(` ✗ server wasm failed to load: ${String(e)}`) + '\n');
|
|
126
|
+
}
|
|
127
|
+
if (!module.available && !warnedMissing) {
|
|
128
|
+
warnedMissing = true;
|
|
129
|
+
process.stdout.write(pc.yellow(' ! ') +
|
|
130
|
+
pc.dim(`server wasm not found at ${options.wasmFile}; serving client only`) +
|
|
131
|
+
'\n');
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
refresh();
|
|
135
|
+
const app = new Server({
|
|
136
|
+
max_body_length: options.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH,
|
|
137
|
+
max_body_buffer: 1024 * 32,
|
|
138
|
+
fast_abort: true,
|
|
139
|
+
});
|
|
140
|
+
app.set_error_handler((_request, response, error) => {
|
|
141
|
+
if (response.completed)
|
|
142
|
+
return;
|
|
143
|
+
response.atomic(() => {
|
|
144
|
+
response.status(500).send(`internal error: ${error.message}\n`);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
wireWebsocketProxy(app, options.vite);
|
|
148
|
+
const nodeMode = options.nodeMode ?? 'all';
|
|
149
|
+
let daemonHost = null;
|
|
150
|
+
let daemonTimer = null;
|
|
151
|
+
if (options.coldWasmFile !== undefined && daemonEmulationEnabled(nodeMode) && options.daemon) {
|
|
152
|
+
daemonHost = new DaemonHost(options.coldWasmFile, options.daemon, nodeMode);
|
|
153
|
+
const pollDaemon = () => {
|
|
154
|
+
try {
|
|
155
|
+
daemonHost?.refresh();
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
process.stdout.write(pc.red(` ✗ daemon reload failed: ${String(e)}`) + '\n');
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
pollDaemon();
|
|
162
|
+
daemonTimer = setInterval(pollDaemon, 500);
|
|
163
|
+
daemonTimer.unref?.();
|
|
164
|
+
}
|
|
165
|
+
app.any('/*', async (request, response) => {
|
|
166
|
+
response.removeHeader('uWebSockets');
|
|
167
|
+
const dispatchable = !isViteInternal(request.url) && METHOD_CODES[request.method] !== undefined;
|
|
168
|
+
if (dispatchable)
|
|
169
|
+
refresh();
|
|
170
|
+
if (dispatchable && module.available) {
|
|
171
|
+
const envelopeReq = await toEnvelopeRequest(request);
|
|
172
|
+
const cacheHost = request.headers.host ?? 'dev';
|
|
173
|
+
const hasAuth = request.headers.cookie !== undefined || request.headers.authorization !== undefined;
|
|
174
|
+
const cached = lookupCache(cacheHost, request.method, request.url, envelopeReq.body);
|
|
175
|
+
if (cached !== null) {
|
|
176
|
+
sendWasmResponse(response, root, cached);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const result = module.dispatch(envelopeReq);
|
|
181
|
+
if (!result.unhandled) {
|
|
182
|
+
const finalized = applyCacheRule(cacheHost, request.method, request.url, envelopeReq.body, hasAuth, result);
|
|
183
|
+
sendWasmResponse(response, root, finalized);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
process.stdout.write(pc.red(` ✗ ${request.method} ${request.path} server error: ${String(e)}`) +
|
|
189
|
+
'\n');
|
|
190
|
+
response.status(500).send('internal error\n');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if ((request.method === 'GET' || request.method === 'HEAD') &&
|
|
194
|
+
ssrRoutes.length > 0) {
|
|
195
|
+
const route = ssrRoutes.find((r) => r.test(pathnameOf(request.url)));
|
|
196
|
+
if (route) {
|
|
197
|
+
try {
|
|
198
|
+
const out = assembleSsr(route, module.dispatchRender(envelopeReq));
|
|
199
|
+
if (out !== null) {
|
|
200
|
+
sendSsr(response, out, request.method === 'HEAD');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
process.stdout.write(pc.red(` ✗ SSR ${request.path}: ${String(e)}`) + '\n');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
await proxyToVite(request, response, options.vite);
|
|
211
|
+
});
|
|
212
|
+
await app.listen(options.port, host);
|
|
213
|
+
return {
|
|
214
|
+
port: options.port,
|
|
215
|
+
host,
|
|
216
|
+
close: async () => {
|
|
217
|
+
if (daemonTimer !== null)
|
|
218
|
+
clearInterval(daemonTimer);
|
|
219
|
+
daemonHost?.close();
|
|
220
|
+
await app.shutdown();
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface DevSsrTemplate {
|
|
2
|
+
pattern: string;
|
|
3
|
+
name: string;
|
|
4
|
+
tmpl: Uint8Array;
|
|
5
|
+
entries: {
|
|
6
|
+
id: number;
|
|
7
|
+
offset: number;
|
|
8
|
+
}[];
|
|
9
|
+
}
|
|
10
|
+
export interface SsrRoute {
|
|
11
|
+
test: (pathname: string) => boolean;
|
|
12
|
+
tmpl: Uint8Array;
|
|
13
|
+
entries: {
|
|
14
|
+
id: number;
|
|
15
|
+
offset: number;
|
|
16
|
+
}[];
|
|
17
|
+
}
|
|
18
|
+
export declare function pathnameOf(url: string): string;
|
|
19
|
+
export declare function buildSsrRoutes(templates: readonly DevSsrTemplate[]): SsrRoute[];
|
|
20
|
+
export interface SsrResult {
|
|
21
|
+
status: number;
|
|
22
|
+
headers: [string, string][];
|
|
23
|
+
html: Uint8Array;
|
|
24
|
+
}
|
|
25
|
+
export declare function assembleSsr(route: SsrRoute, envelope: Uint8Array): SsrResult | null;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export function pathnameOf(url) {
|
|
2
|
+
const q = url.indexOf('?');
|
|
3
|
+
return q < 0 ? url : url.slice(0, q);
|
|
4
|
+
}
|
|
5
|
+
function patternToTest(pattern) {
|
|
6
|
+
const norm = (p) => (p.length > 1 && p.endsWith('/') ? p.slice(0, -1) : p);
|
|
7
|
+
let re = '';
|
|
8
|
+
let i = 0;
|
|
9
|
+
while (i < pattern.length) {
|
|
10
|
+
const ch = pattern[i];
|
|
11
|
+
if (ch === ':') {
|
|
12
|
+
i++;
|
|
13
|
+
while (i < pattern.length && /[A-Za-z0-9_]/.test(pattern[i]))
|
|
14
|
+
i++;
|
|
15
|
+
re += '[^/]+';
|
|
16
|
+
}
|
|
17
|
+
else if (ch === '*') {
|
|
18
|
+
re += '.*';
|
|
19
|
+
i++;
|
|
20
|
+
}
|
|
21
|
+
else if (ch === '[') {
|
|
22
|
+
const end = pattern.indexOf(']', i);
|
|
23
|
+
const inner = end < 0 ? '' : pattern.slice(i + 1, end);
|
|
24
|
+
re += inner.startsWith('...') ? '.*' : '[^/]+';
|
|
25
|
+
i = end < 0 ? pattern.length : end + 1;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
re += ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
29
|
+
i++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const compiled = new RegExp(`^${re}$`);
|
|
33
|
+
return (pathname) => compiled.test(norm(pathname));
|
|
34
|
+
}
|
|
35
|
+
export function buildSsrRoutes(templates) {
|
|
36
|
+
return templates.map((t) => ({ test: patternToTest(t.pattern), tmpl: t.tmpl, entries: t.entries }));
|
|
37
|
+
}
|
|
38
|
+
function decodeValues(buf) {
|
|
39
|
+
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
40
|
+
let o = 0;
|
|
41
|
+
const need = (n) => o + n <= buf.byteLength;
|
|
42
|
+
try {
|
|
43
|
+
if (!need(2 + 32 + 2))
|
|
44
|
+
return null;
|
|
45
|
+
const status = dv.getUint16(o, true);
|
|
46
|
+
o += 2;
|
|
47
|
+
o += 32;
|
|
48
|
+
const nHeaders = dv.getUint16(o, true);
|
|
49
|
+
o += 2;
|
|
50
|
+
const headers = [];
|
|
51
|
+
const dec = new TextDecoder();
|
|
52
|
+
for (let i = 0; i < nHeaders; i++) {
|
|
53
|
+
if (!need(4))
|
|
54
|
+
return null;
|
|
55
|
+
const nameLen = dv.getUint16(o, true);
|
|
56
|
+
const valLen = dv.getUint16(o + 2, true);
|
|
57
|
+
o += 4;
|
|
58
|
+
if (!need(nameLen + valLen))
|
|
59
|
+
return null;
|
|
60
|
+
const name = dec.decode(buf.subarray(o, o + nameLen));
|
|
61
|
+
o += nameLen;
|
|
62
|
+
const val = dec.decode(buf.subarray(o, o + valLen));
|
|
63
|
+
o += valLen;
|
|
64
|
+
headers.push([name, val]);
|
|
65
|
+
}
|
|
66
|
+
if (!need(2))
|
|
67
|
+
return null;
|
|
68
|
+
const nSlots = dv.getUint16(o, true);
|
|
69
|
+
o += 2;
|
|
70
|
+
const values = new Map();
|
|
71
|
+
for (let i = 0; i < nSlots; i++) {
|
|
72
|
+
if (!need(2 + 1 + 4))
|
|
73
|
+
return null;
|
|
74
|
+
const id = dv.getUint16(o, true);
|
|
75
|
+
o += 2;
|
|
76
|
+
o += 1;
|
|
77
|
+
const len = dv.getUint32(o, true);
|
|
78
|
+
o += 4;
|
|
79
|
+
if (!need(len))
|
|
80
|
+
return null;
|
|
81
|
+
values.set(id, buf.subarray(o, o + len));
|
|
82
|
+
o += len;
|
|
83
|
+
}
|
|
84
|
+
return { status, headers, values };
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function splice(tmpl, inserts) {
|
|
91
|
+
const parts = [];
|
|
92
|
+
let prev = 0;
|
|
93
|
+
for (const ins of inserts) {
|
|
94
|
+
if (ins.offset > prev)
|
|
95
|
+
parts.push(tmpl.subarray(prev, ins.offset));
|
|
96
|
+
if (ins.value.length > 0)
|
|
97
|
+
parts.push(ins.value);
|
|
98
|
+
prev = ins.offset;
|
|
99
|
+
}
|
|
100
|
+
if (tmpl.length > prev)
|
|
101
|
+
parts.push(tmpl.subarray(prev));
|
|
102
|
+
return Buffer.concat(parts.map((p) => Buffer.from(p.buffer, p.byteOffset, p.byteLength)));
|
|
103
|
+
}
|
|
104
|
+
export function assembleSsr(route, envelope) {
|
|
105
|
+
const decoded = decodeValues(envelope);
|
|
106
|
+
if (decoded === null)
|
|
107
|
+
return null;
|
|
108
|
+
if (decoded.status >= 500 || decoded.values.size === 0)
|
|
109
|
+
return null;
|
|
110
|
+
const inserts = route.entries
|
|
111
|
+
.map((e) => ({ offset: e.offset, value: decoded.values.get(e.id) ?? new Uint8Array(0) }))
|
|
112
|
+
.sort((a, b) => a.offset - b.offset);
|
|
113
|
+
return { status: decoded.status, headers: decoded.headers, html: splice(route.tmpl, inserts) };
|
|
114
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function leb(buf, pos) {
|
|
2
|
+
let result = 0;
|
|
3
|
+
let shift = 0;
|
|
4
|
+
let p = pos;
|
|
5
|
+
for (;;) {
|
|
6
|
+
if (p >= buf.length)
|
|
7
|
+
throw new RangeError('leb128 past end of buffer');
|
|
8
|
+
const b = buf[p++];
|
|
9
|
+
result |= (b & 0x7f) << shift;
|
|
10
|
+
if ((b & 0x80) === 0)
|
|
11
|
+
break;
|
|
12
|
+
shift += 7;
|
|
13
|
+
if (shift > 35)
|
|
14
|
+
throw new RangeError('leb128 too long');
|
|
15
|
+
}
|
|
16
|
+
return [result >>> 0, p];
|
|
17
|
+
}
|
|
18
|
+
export function customSection(wasm, want) {
|
|
19
|
+
if (wasm.length < 8 ||
|
|
20
|
+
wasm[0] !== 0x00 ||
|
|
21
|
+
wasm[1] !== 0x61 ||
|
|
22
|
+
wasm[2] !== 0x73 ||
|
|
23
|
+
wasm[3] !== 0x6d)
|
|
24
|
+
return null;
|
|
25
|
+
let pos = 8;
|
|
26
|
+
while (pos < wasm.length) {
|
|
27
|
+
const id = wasm[pos++];
|
|
28
|
+
let size;
|
|
29
|
+
[size, pos] = leb(wasm, pos);
|
|
30
|
+
const end = pos + size;
|
|
31
|
+
if (end > wasm.length || end < pos)
|
|
32
|
+
return null;
|
|
33
|
+
if (id === 0) {
|
|
34
|
+
const [nameLen, namePos] = leb(wasm, pos);
|
|
35
|
+
if (namePos + nameLen <= end &&
|
|
36
|
+
wasm.toString('latin1', namePos, namePos + nameLen) === want)
|
|
37
|
+
return wasm.subarray(namePos + nameLen, end);
|
|
38
|
+
}
|
|
39
|
+
pos = end;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SurfaceFlags {
|
|
2
|
+
readonly rest: boolean;
|
|
3
|
+
readonly stream: boolean;
|
|
4
|
+
readonly daemon: boolean;
|
|
5
|
+
readonly scheduled: boolean;
|
|
6
|
+
readonly database: boolean;
|
|
7
|
+
readonly render: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface Surface {
|
|
10
|
+
readonly targetMode: 'hot' | 'cold';
|
|
11
|
+
readonly flags: SurfaceFlags;
|
|
12
|
+
readonly abiVersion: number;
|
|
13
|
+
readonly buildId: string;
|
|
14
|
+
readonly fingerprint: number;
|
|
15
|
+
readonly dataCoherenceHash: number;
|
|
16
|
+
readonly pairCoherenceHash: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function parseSurface(wasm: Buffer): Surface | 'absent' | 'invalid';
|