toiljs 0.0.15 → 0.0.19
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/.babelrc +13 -13
- package/.gitattributes +2 -2
- package/.github/ISSUE_TEMPLATE/bug_report.md +38 -38
- package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -90
- package/.github/ISSUE_TEMPLATE/config.yml +8 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +43 -43
- package/.github/changelog-config.json +45 -45
- package/.github/dependabot.yml +27 -27
- package/.github/workflows/ci.yml +191 -191
- package/.prettierrc.json +11 -11
- package/.vscode/settings.json +9 -9
- package/CHANGELOG.md +116 -5
- package/LICENSE +187 -187
- package/README.md +524 -315
- package/as-pect.asconfig.json +34 -34
- package/as-pect.config.js +65 -65
- package/assets/logo.svg +36 -36
- package/build/backend/.tsbuildinfo +1 -1
- package/build/backend/index.d.ts +1 -0
- package/build/backend/index.js +20 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +1320 -696
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.d.ts +6 -0
- package/build/client/dev/devtools.js +479 -0
- package/build/client/dev/error-overlay.d.ts +9 -0
- package/build/client/dev/error-overlay.js +19 -4
- package/build/client/errors.d.ts +1 -0
- package/build/client/errors.js +3 -0
- package/build/client/index.d.ts +2 -0
- package/build/client/index.js +2 -0
- package/build/client/navigation/prefetch.d.ts +1 -0
- package/build/client/navigation/prefetch.js +35 -0
- package/build/client/routing/Router.js +1 -1
- package/build/client/routing/hooks.js +6 -2
- package/build/client/routing/loader.d.ts +23 -0
- package/build/client/routing/loader.js +53 -7
- package/build/client/routing/mount.js +4 -3
- package/build/client/rpc.d.ts +1 -0
- package/build/client/rpc.js +37 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +16 -0
- package/build/compiler/config.js +9 -0
- package/build/compiler/docs.js +78 -21
- package/build/compiler/generate.js +5 -4
- package/build/compiler/index.d.ts +3 -2
- package/build/compiler/index.js +2 -2
- package/build/compiler/plugin.js +228 -0
- package/build/compiler/prerender.d.ts +1 -0
- package/build/compiler/prerender.js +1 -1
- package/build/compiler/seo.d.ts +1 -1
- package/build/compiler/seo.js +20 -5
- package/build/compiler/ssg.js +39 -2
- package/build/compiler/vite.js +25 -0
- package/build/io/.tsbuildinfo +1 -1
- package/build/io/codec.d.ts +54 -0
- package/build/io/codec.js +143 -0
- package/build/io/index.d.ts +1 -2
- package/build/io/index.js +1 -2
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/eslint.config.js +48 -48
- package/examples/basic/client/404.tsx +11 -11
- package/examples/basic/client/components/.gitkeep +1 -1
- package/examples/basic/client/global-error.tsx +13 -13
- package/examples/basic/client/layout.tsx +25 -25
- package/examples/basic/client/public/images/.gitkeep +1 -1
- package/examples/basic/client/public/images/logo.svg +36 -36
- package/examples/basic/client/public/robots.txt +2 -2
- package/examples/basic/client/routes/docs/[...slug].tsx +12 -12
- package/examples/basic/client/routes/features/error/error.tsx +16 -16
- package/examples/basic/client/routes/features/index.tsx +1 -1
- package/examples/basic/client/routes/features/template/b.tsx +14 -14
- package/examples/basic/client/routes/files/[[...slug]].tsx +21 -21
- package/examples/basic/client/routes/gallery/layout.tsx +13 -13
- package/examples/basic/client/routes/io.tsx +23 -24
- package/examples/basic/client/routes/loader-demo/loading.tsx +13 -13
- package/examples/basic/client/routes/rest.tsx +74 -0
- package/examples/basic/client/routes/rpc.tsx +43 -0
- package/examples/basic/client/routes/search.tsx +61 -61
- package/examples/basic/client/toil.tsx +5 -5
- package/package.json +167 -148
- package/presets/eslint.js +88 -88
- package/presets/no-uint8array-tostring.js +200 -200
- package/presets/prettier-plugin.js +51 -0
- package/presets/prettier.json +19 -18
- package/presets/tsconfig.json +37 -37
- package/server/runtime/README.md +97 -0
- package/server/runtime/abort/abort.ts +27 -0
- package/server/runtime/env/Server.ts +61 -0
- package/server/runtime/envelope.ts +191 -0
- package/server/runtime/exports/index.ts +52 -0
- package/server/runtime/handlers/ToilHandler.ts +34 -0
- package/server/runtime/index.ts +26 -0
- package/server/runtime/lang/Potential.ts +5 -0
- package/server/runtime/memory.ts +81 -0
- package/server/runtime/request.ts +55 -0
- package/server/runtime/response.ts +86 -0
- package/server/runtime/rest/Rest.ts +39 -0
- package/server/runtime/rest/RestHandler.ts +20 -0
- package/server/runtime/rest/RouteContext.ts +82 -0
- package/server/runtime/rest/match.ts +48 -0
- package/server/runtime/tsconfig.json +7 -0
- package/src/backend/index.ts +202 -160
- package/src/cli/create.ts +15 -5
- package/src/cli/diagnostics.ts +81 -0
- package/src/cli/doctor.ts +384 -7
- package/src/cli/index.ts +11 -2
- package/src/cli/proc.ts +50 -50
- package/src/cli/updates.ts +69 -69
- package/src/cli/validate.ts +31 -31
- package/src/client/channel/channel.ts +146 -146
- package/src/client/components/Form.tsx +65 -65
- package/src/client/components/Script.tsx +113 -113
- package/src/client/components/Slot.tsx +21 -21
- package/src/client/dev/devtools.tsx +1018 -0
- package/src/client/dev/error-overlay.tsx +30 -4
- package/src/client/errors.ts +11 -0
- package/src/client/head/head.ts +167 -167
- package/src/client/head/metadata.ts +112 -112
- package/src/client/index.ts +91 -89
- package/src/client/navigation/NavLink.tsx +86 -86
- package/src/client/navigation/navigation.ts +235 -235
- package/src/client/navigation/prefetch.ts +169 -130
- package/src/client/navigation/scroll.ts +53 -53
- package/src/client/routing/Router.tsx +8 -2
- package/src/client/routing/action.ts +122 -122
- package/src/client/routing/error-boundary.tsx +43 -43
- package/src/client/routing/hooks.ts +21 -6
- package/src/client/routing/loader.ts +325 -235
- package/src/client/routing/match.ts +47 -47
- package/src/client/routing/mount.tsx +54 -52
- package/src/client/routing/params-context.ts +10 -10
- package/src/client/routing/slot-context.ts +7 -7
- package/src/client/rpc.ts +64 -0
- package/src/client/search/search.ts +189 -189
- package/src/client/search/use-page-search.ts +73 -73
- package/src/client/types.ts +73 -73
- package/src/compiler/config.ts +221 -182
- package/src/compiler/docs.ts +285 -228
- package/src/compiler/generate.ts +395 -394
- package/src/compiler/index.ts +66 -57
- package/src/compiler/pages.ts +70 -70
- package/src/compiler/plugin.ts +258 -2
- package/src/compiler/prerender.ts +156 -156
- package/src/compiler/seo.ts +417 -390
- package/src/compiler/ssg.ts +171 -126
- package/src/compiler/vite.ts +34 -0
- package/src/io/FastMap.ts +151 -127
- package/src/io/FastSet.ts +15 -1
- package/src/io/codec.ts +217 -0
- package/src/io/index.ts +10 -11
- package/src/io/lengths.ts +14 -14
- package/src/io/types.ts +19 -18
- package/src/logger/index.ts +22 -22
- package/src/shared/index.ts +10 -10
- package/std/client/index.d.ts +15 -15
- package/std/client/package.json +3 -3
- package/test/assembly/example.spec.ts +17 -7
- package/test/channel.test.ts +21 -21
- package/test/doctor.test.ts +65 -0
- package/test/dom/Link.test.tsx +47 -47
- package/test/dom/NavLink.test.tsx +37 -37
- package/test/dom/error-overlay.test.tsx +44 -44
- package/test/dom/loader.test.tsx +121 -121
- package/test/dom/navigation.test.ts +59 -59
- package/test/dom/revalidate.test.tsx +38 -38
- package/test/dom/route-head.test.tsx +78 -78
- package/test/dom/router-loading.test.tsx +44 -44
- package/test/dom/scroll.test.ts +56 -56
- package/test/dom/use-metadata.test.tsx +58 -58
- package/test/errors.test.ts +21 -0
- package/test/io.test.ts +117 -93
- package/test/navlink.test.ts +28 -28
- package/test/placeholder.test.ts +9 -9
- package/test/prettier-plugin.test.ts +46 -0
- package/test/routes.test.ts +76 -76
- package/test/rpc.test.ts +50 -0
- package/test/seo.test.ts +175 -164
- package/test/slot-layouts.test.ts +69 -69
- package/test/ssg.test.ts +36 -36
- package/test/update.test.ts +44 -44
- package/test/validate.test.ts +42 -42
- package/tests/data-parity/generated-parity.ts +99 -0
- package/tests/data-parity/parity.ts +80 -0
- package/tests/data-parity/spec.ts +46 -0
- package/toil-routes.d.ts +7 -0
- package/tsconfig.backend.json +13 -13
- package/tsconfig.base.json +35 -35
- package/tsconfig.cli.json +13 -13
- package/tsconfig.client.json +14 -14
- package/tsconfig.compiler.json +13 -13
- package/tsconfig.io.json +12 -12
- package/tsconfig.json +22 -22
- package/tsconfig.logger.json +12 -12
- package/tsconfig.server.json +10 -10
- package/tsconfig.shared.json +12 -12
- package/vitest.config.ts +26 -26
- package/.idea/codeStyles/Project.xml +0 -54
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.idea/inspectionProfiles/Project_Default.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/prettier.xml +0 -7
- package/.idea/toiljs.iml +0 -8
- package/.idea/vcs.xml +0 -6
- package/.toil/entry.tsx +0 -9
- package/.toil/index.html +0 -12
- package/.toil/routes.ts +0 -9
- package/build/cli/configure.d.ts +0 -16
- package/build/cli/configure.js +0 -272
- package/build/cli/create.d.ts +0 -16
- package/build/cli/create.js +0 -420
- package/build/cli/diagnostics.d.ts +0 -55
- package/build/cli/diagnostics.js +0 -333
- package/build/cli/doctor.d.ts +0 -6
- package/build/cli/doctor.js +0 -249
- package/build/cli/features.d.ts +0 -25
- package/build/cli/features.js +0 -107
- package/build/cli/index.d.ts +0 -2
- package/build/cli/proc.d.ts +0 -6
- package/build/cli/proc.js +0 -31
- package/build/cli/ui.d.ts +0 -9
- package/build/cli/ui.js +0 -75
- package/build/cli/update.d.ts +0 -7
- package/build/cli/update.js +0 -117
- package/build/cli/updates.d.ts +0 -10
- package/build/cli/updates.js +0 -45
- package/build/cli/validate.d.ts +0 -4
- package/build/cli/validate.js +0 -19
- package/build/client/Link.d.ts +0 -8
- package/build/client/Link.js +0 -44
- package/build/client/NavLink.d.ts +0 -14
- package/build/client/NavLink.js +0 -37
- package/build/client/Router.d.ts +0 -7
- package/build/client/Router.js +0 -55
- package/build/client/channel.d.ts +0 -23
- package/build/client/channel.js +0 -94
- package/build/client/error-boundary.d.ts +0 -16
- package/build/client/error-boundary.js +0 -19
- package/build/client/head.d.ts +0 -26
- package/build/client/head.js +0 -87
- package/build/client/hooks.d.ts +0 -17
- package/build/client/hooks.js +0 -48
- package/build/client/lazy.d.ts +0 -16
- package/build/client/lazy.js +0 -53
- package/build/client/match.d.ts +0 -2
- package/build/client/match.js +0 -32
- package/build/client/mount.d.ts +0 -2
- package/build/client/mount.js +0 -13
- package/build/client/navigation.d.ts +0 -13
- package/build/client/navigation.js +0 -97
- package/build/client/params-context.d.ts +0 -2
- package/build/client/params-context.js +0 -2
- package/build/client/prefetch.d.ts +0 -11
- package/build/client/prefetch.js +0 -100
- package/build/client/runtime.d.ts +0 -31
- package/build/client/runtime.js +0 -112
- package/build/client/scroll.d.ts +0 -8
- package/build/client/scroll.js +0 -36
- package/build/io/BinaryReader.d.ts +0 -44
- package/build/io/BinaryReader.js +0 -244
- package/build/io/BinaryWriter.d.ts +0 -44
- package/build/io/BinaryWriter.js +0 -297
- package/build/server/release.wasm +0 -0
- package/build/server/release.wat +0 -9
- package/src/io/BinaryReader.ts +0 -340
- package/src/io/BinaryWriter.ts +0 -385
- package/src/server/index.ts +0 -10
- package/src/server/main.ts +0 -13
- package/src/server/tsconfig.json +0 -4
- package/toil-env.d.ts +0 -16
- package/toilconfig.json +0 -30
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request context handed to a `@route` method. Carries the captured path
|
|
3
|
+
* params (`/todos/:id`), the parsed query string, and the raw `Request`. The
|
|
4
|
+
* compiler builds one of these for you and injects it into any route method
|
|
5
|
+
* that declares a `RouteContext` parameter.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Request } from '../request';
|
|
9
|
+
|
|
10
|
+
export class RouteContext {
|
|
11
|
+
/** The raw incoming request (method, path, headers, body). */
|
|
12
|
+
request: Request;
|
|
13
|
+
|
|
14
|
+
// Parallel arrays rather than a Map: a route has a handful of params, the
|
|
15
|
+
// linear scan is cheaper than hashing, and it keeps the codec-free runtime small.
|
|
16
|
+
private paramKeys: Array<string>;
|
|
17
|
+
private paramVals: Array<string>;
|
|
18
|
+
private queryKeys: Array<string> | null = null;
|
|
19
|
+
private queryVals: Array<string> | null = null;
|
|
20
|
+
|
|
21
|
+
constructor(request: Request, paramKeys: Array<string>, paramVals: Array<string>) {
|
|
22
|
+
this.request = request;
|
|
23
|
+
this.paramKeys = paramKeys;
|
|
24
|
+
this.paramVals = paramVals;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A captured path parameter (`/todos/:id` gives `param("id")`), or "" if absent. */
|
|
28
|
+
param(name: string): string {
|
|
29
|
+
for (let i = 0; i < this.paramKeys.length; i++) {
|
|
30
|
+
if (this.paramKeys[i] == name) return this.paramVals[i];
|
|
31
|
+
}
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A query-string value (`?q=hi` gives `query("q")`), or "" if absent. Not URL-decoded in v1. */
|
|
36
|
+
query(name: string): string {
|
|
37
|
+
this.ensureQuery();
|
|
38
|
+
const keys = this.queryKeys!;
|
|
39
|
+
const vals = this.queryVals!;
|
|
40
|
+
for (let i = 0; i < keys.length; i++) {
|
|
41
|
+
if (keys[i] == name) return vals[i];
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Case-insensitive request header, or null. Delegates to `Request.header`. */
|
|
47
|
+
header(name: string): string | null {
|
|
48
|
+
return this.request.header(name);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The raw request body decoded as UTF-8 text (used by the JSON stream codec). */
|
|
52
|
+
text(): string {
|
|
53
|
+
const body = this.request.body;
|
|
54
|
+
if (body.length == 0) return '';
|
|
55
|
+
return String.UTF8.decodeUnsafe(body.dataStart, body.byteLength);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private ensureQuery(): void {
|
|
59
|
+
if (this.queryKeys != null) return;
|
|
60
|
+
const keys = new Array<string>();
|
|
61
|
+
const vals = new Array<string>();
|
|
62
|
+
const path = this.request.path;
|
|
63
|
+
const q = path.indexOf('?');
|
|
64
|
+
if (q >= 0 && q + 1 < path.length) {
|
|
65
|
+
const pairs = path.substring(q + 1).split('&');
|
|
66
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
67
|
+
const pair = pairs[i];
|
|
68
|
+
if (pair.length == 0) continue;
|
|
69
|
+
const eq = pair.indexOf('=');
|
|
70
|
+
if (eq < 0) {
|
|
71
|
+
keys.push(pair);
|
|
72
|
+
vals.push('');
|
|
73
|
+
} else {
|
|
74
|
+
keys.push(pair.substring(0, eq));
|
|
75
|
+
vals.push(pair.substring(eq + 1));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
this.queryKeys = keys;
|
|
80
|
+
this.queryVals = vals;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time route patterns (`/api/todos/:id`) matched against a request path
|
|
3
|
+
* at runtime, capturing `:params`. The compiler emits one `matchRoute(...)` call
|
|
4
|
+
* per route inside a controller's injected `__tryRoute`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Request } from '../request';
|
|
8
|
+
import { RouteContext } from './RouteContext';
|
|
9
|
+
|
|
10
|
+
const COLON: i32 = 0x3a; // ':'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Match `pattern` against `req.path`. Static segments must be equal; a `:name`
|
|
14
|
+
* segment captures the corresponding path segment. The query string is ignored
|
|
15
|
+
* for matching. Returns a populated `RouteContext` on a match, `null` on a miss.
|
|
16
|
+
*/
|
|
17
|
+
export function matchRoute(pattern: string, req: Request): RouteContext | null {
|
|
18
|
+
let path = req.path;
|
|
19
|
+
const q = path.indexOf('?');
|
|
20
|
+
if (q >= 0) path = path.substring(0, q);
|
|
21
|
+
|
|
22
|
+
const pat = splitSegments(pattern);
|
|
23
|
+
const act = splitSegments(path);
|
|
24
|
+
if (pat.length != act.length) return null;
|
|
25
|
+
|
|
26
|
+
const keys = new Array<string>();
|
|
27
|
+
const vals = new Array<string>();
|
|
28
|
+
for (let i = 0; i < pat.length; i++) {
|
|
29
|
+
const seg = pat[i];
|
|
30
|
+
if (seg.length > 0 && seg.charCodeAt(0) == COLON) {
|
|
31
|
+
keys.push(seg.substring(1));
|
|
32
|
+
vals.push(act[i]);
|
|
33
|
+
} else if (seg != act[i]) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return new RouteContext(req, keys, vals);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Split a path on `/`, dropping empty segments (so leading/trailing slashes don't matter). */
|
|
41
|
+
function splitSegments(path: string): Array<string> {
|
|
42
|
+
const out = new Array<string>();
|
|
43
|
+
const parts = path.split('/');
|
|
44
|
+
for (let i = 0; i < parts.length; i++) {
|
|
45
|
+
if (parts[i].length > 0) out.push(parts[i]);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
package/src/backend/index.ts
CHANGED
|
@@ -1,160 +1,202 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* toiljs backend, the self-host / dev server, built on @btc-vision/hyper-express (uWebSockets.js)
|
|
3
|
-
* for very high throughput. It serves the built client (static assets + SPA fallback) and exposes
|
|
4
|
-
* a WebSocket channel for realtime / live updates.
|
|
5
|
-
*
|
|
6
|
-
* This is the Node "server" that hosts the app on a local machine; it is distinct from the
|
|
7
|
-
* toilscript WASM
|
|
8
|
-
*/
|
|
9
|
-
import fs from 'node:fs';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
Server,
|
|
14
|
-
type MiddlewareNext,
|
|
15
|
-
type Request,
|
|
16
|
-
type Response,
|
|
17
|
-
type Websocket,
|
|
18
|
-
} from '@btc-vision/hyper-express';
|
|
19
|
-
|
|
20
|
-
const DEFAULT_MAX_BODY_LENGTH = 1024 * 1024 * 8;
|
|
21
|
-
const MAX_BODY_BUFFER = 1024 * 32;
|
|
22
|
-
const HTTP_IDLE_TIMEOUT = 60;
|
|
23
|
-
const HTTP_RESPONSE_TIMEOUT = 120;
|
|
24
|
-
|
|
25
|
-
const WS_MAX_PAYLOAD_LENGTH = 1024 * 1024;
|
|
26
|
-
const WS_IDLE_TIMEOUT = 120;
|
|
27
|
-
const WS_MAX_BACKPRESSURE = 1024 * 1024 * 2;
|
|
28
|
-
|
|
29
|
-
const CORS_METHODS = 'GET, POST, OPTIONS, PUT, PATCH, DELETE';
|
|
30
|
-
const CORS_HEADERS = 'X-Requested-With, content-type';
|
|
31
|
-
|
|
32
|
-
/** Options for {@link startBackend}. */
|
|
33
|
-
export interface BackendOptions {
|
|
34
|
-
/** Directory to serve (the built client `outDir`, e.g. `dist`). */
|
|
35
|
-
readonly root: string;
|
|
36
|
-
/** Listening port. Default `3000`. */
|
|
37
|
-
readonly port?: number;
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
readonly
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
readonly
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
1
|
+
/**
|
|
2
|
+
* toiljs backend, the self-host / dev server, built on @btc-vision/hyper-express (uWebSockets.js)
|
|
3
|
+
* for very high throughput. It serves the built client (static assets + SPA fallback) and exposes
|
|
4
|
+
* a WebSocket channel for realtime / live updates.
|
|
5
|
+
*
|
|
6
|
+
* This is the Node "server" that hosts the app on a local machine; it is distinct from the
|
|
7
|
+
* toilscript WASM runtime in `server/runtime` (the `toiljs/server/runtime` library export).
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
Server,
|
|
14
|
+
type MiddlewareNext,
|
|
15
|
+
type Request,
|
|
16
|
+
type Response,
|
|
17
|
+
type Websocket,
|
|
18
|
+
} from '@btc-vision/hyper-express';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_MAX_BODY_LENGTH = 1024 * 1024 * 8;
|
|
21
|
+
const MAX_BODY_BUFFER = 1024 * 32;
|
|
22
|
+
const HTTP_IDLE_TIMEOUT = 60;
|
|
23
|
+
const HTTP_RESPONSE_TIMEOUT = 120;
|
|
24
|
+
|
|
25
|
+
const WS_MAX_PAYLOAD_LENGTH = 1024 * 1024;
|
|
26
|
+
const WS_IDLE_TIMEOUT = 120;
|
|
27
|
+
const WS_MAX_BACKPRESSURE = 1024 * 1024 * 2;
|
|
28
|
+
|
|
29
|
+
const CORS_METHODS = 'GET, POST, OPTIONS, PUT, PATCH, DELETE';
|
|
30
|
+
const CORS_HEADERS = 'X-Requested-With, content-type';
|
|
31
|
+
|
|
32
|
+
/** Options for {@link startBackend}. */
|
|
33
|
+
export interface BackendOptions {
|
|
34
|
+
/** Directory to serve (the built client `outDir`, e.g. `dist`). */
|
|
35
|
+
readonly root: string;
|
|
36
|
+
/** Listening port. Default `3000`. */
|
|
37
|
+
readonly port?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Bind host. Default `127.0.0.1` (loopback only). Pass `0.0.0.0` (or a specific interface) to
|
|
40
|
+
* expose the server on the network; do so deliberately, since the WebSocket channel relays
|
|
41
|
+
* messages between all connected clients.
|
|
42
|
+
*/
|
|
43
|
+
readonly host?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Extra origins allowed to open the WebSocket channel, in addition to the server's own origin.
|
|
46
|
+
* Cross-origin WebSocket handshakes from other origins are rejected (prevents cross-site
|
|
47
|
+
* WebSocket hijacking). Example: `['https://app.example.com']`.
|
|
48
|
+
*/
|
|
49
|
+
readonly allowedOrigins?: readonly string[];
|
|
50
|
+
/** WebSocket channel path. Default `/_toil`. */
|
|
51
|
+
readonly wsPath?: string;
|
|
52
|
+
/** Send permissive CORS headers + handle preflight. Default `true`. */
|
|
53
|
+
readonly cors?: boolean;
|
|
54
|
+
/** Max request body length in bytes. Default 8 MB. */
|
|
55
|
+
readonly maxBodyLength?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A running backend instance. */
|
|
59
|
+
export interface RunningBackend {
|
|
60
|
+
readonly port: number;
|
|
61
|
+
readonly host: string;
|
|
62
|
+
readonly wsPath: string;
|
|
63
|
+
/** Sends a message to every connected WebSocket client. */
|
|
64
|
+
broadcast(message: string): void;
|
|
65
|
+
/** Number of currently-connected WebSocket clients. */
|
|
66
|
+
clientCount(): number;
|
|
67
|
+
/** Gracefully shuts the server down. */
|
|
68
|
+
close(): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether a WebSocket upgrade from `origin` may connect. A missing `Origin` (non-browser clients
|
|
73
|
+
* like curl or server-to-server) is allowed; a browser `Origin` must match the host the server was
|
|
74
|
+
* reached at (same-origin) or be in the explicit allowlist. This blocks cross-site WebSocket
|
|
75
|
+
* hijacking, where a page the victim visits opens a socket to this server from their browser.
|
|
76
|
+
*/
|
|
77
|
+
function isWsOriginAllowed(
|
|
78
|
+
origin: string | undefined,
|
|
79
|
+
hostHeader: string | undefined,
|
|
80
|
+
allowed: readonly string[] | undefined,
|
|
81
|
+
): boolean {
|
|
82
|
+
if (!origin) return true;
|
|
83
|
+
if (allowed?.includes(origin)) return true;
|
|
84
|
+
try {
|
|
85
|
+
return new URL(origin).host === hostHeader;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Resolves a request path to a file inside `root`, guarding against path traversal. */
|
|
92
|
+
function resolveStaticFile(root: string, requestPath: string): string | null {
|
|
93
|
+
const decoded = decodeURIComponent(requestPath);
|
|
94
|
+
const resolved = path.join(root, decoded);
|
|
95
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
|
|
96
|
+
if (decoded === '/' || decoded === '') return null;
|
|
97
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) return resolved;
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Starts the hyper-express server serving `root` with an SPA fallback to `index.html`,
|
|
103
|
+
* plus a WebSocket channel at `wsPath`. Resolves once the server is listening.
|
|
104
|
+
*/
|
|
105
|
+
export async function startBackend(options: BackendOptions): Promise<RunningBackend> {
|
|
106
|
+
const port = options.port ?? 3000;
|
|
107
|
+
const host = options.host ?? '127.0.0.1';
|
|
108
|
+
const wsPath = options.wsPath ?? '/_toil';
|
|
109
|
+
const cors = options.cors ?? true;
|
|
110
|
+
const root = path.resolve(options.root);
|
|
111
|
+
const indexHtml = path.join(root, 'index.html');
|
|
112
|
+
|
|
113
|
+
const app = new Server({
|
|
114
|
+
max_body_length: options.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH,
|
|
115
|
+
max_body_buffer: MAX_BODY_BUFFER,
|
|
116
|
+
fast_abort: true,
|
|
117
|
+
idle_timeout: HTTP_IDLE_TIMEOUT,
|
|
118
|
+
response_timeout: HTTP_RESPONSE_TIMEOUT,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const clients = new Set<Websocket>();
|
|
122
|
+
|
|
123
|
+
app.set_error_handler((_request: Request, response: Response, _error: Error) => {
|
|
124
|
+
if (response.completed) return;
|
|
125
|
+
response.atomic(() => {
|
|
126
|
+
response.status(500).json({ error: 'Internal server error.' });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (cors) {
|
|
131
|
+
app.use((request: Request, response: Response, next: MiddlewareNext) => {
|
|
132
|
+
if (request.method !== 'OPTIONS') {
|
|
133
|
+
response.setHeader('Access-Control-Allow-Origin', '*');
|
|
134
|
+
response.setHeader('Access-Control-Allow-Methods', CORS_METHODS);
|
|
135
|
+
response.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
|
|
136
|
+
}
|
|
137
|
+
response.removeHeader('uWebSockets');
|
|
138
|
+
next();
|
|
139
|
+
});
|
|
140
|
+
app.options('/*', (_request: Request, response: Response) => {
|
|
141
|
+
response.setHeader('Access-Control-Allow-Origin', '*');
|
|
142
|
+
response.setHeader('Access-Control-Allow-Methods', CORS_METHODS);
|
|
143
|
+
response.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
|
|
144
|
+
response.setHeader('Access-Control-Max-Age', '86400');
|
|
145
|
+
response.status(204).send();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
app.ws(
|
|
150
|
+
wsPath,
|
|
151
|
+
{
|
|
152
|
+
message_type: 'String',
|
|
153
|
+
max_payload_length: WS_MAX_PAYLOAD_LENGTH,
|
|
154
|
+
idle_timeout: WS_IDLE_TIMEOUT,
|
|
155
|
+
max_backpressure: WS_MAX_BACKPRESSURE,
|
|
156
|
+
},
|
|
157
|
+
(ws) => {
|
|
158
|
+
clients.add(ws);
|
|
159
|
+
ws.send(JSON.stringify({ type: 'connected', clients: clients.size }));
|
|
160
|
+
ws.on('message', (message: string) => {
|
|
161
|
+
for (const client of clients) client.send(message);
|
|
162
|
+
});
|
|
163
|
+
ws.on('drain', () => {});
|
|
164
|
+
ws.on('close', () => {
|
|
165
|
+
clients.delete(ws);
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Gate the WebSocket upgrade on the request Origin, so a cross-origin page in a victim's browser
|
|
171
|
+
// cannot hijack the channel (CSWSH). Registered AFTER `app.ws` so it overrides that route's
|
|
172
|
+
// default upgrade handler (hyper-express links it to the companion ws route). Same-origin and
|
|
173
|
+
// non-browser clients pass; others get 403.
|
|
174
|
+
app.upgrade(wsPath, (request: Request, response: Response) => {
|
|
175
|
+
if (!isWsOriginAllowed(request.headers.origin, request.headers.host, options.allowedOrigins)) {
|
|
176
|
+
response.status(403).send();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
response.upgrade({});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
app.get('/*', (request: Request, response: Response) => {
|
|
183
|
+
if (response.completed) return;
|
|
184
|
+
const file = resolveStaticFile(root, request.path);
|
|
185
|
+
response.sendFile(file ?? indexHtml);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await app.listen(port, host);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
port,
|
|
192
|
+
host,
|
|
193
|
+
wsPath,
|
|
194
|
+
broadcast: (message: string): void => {
|
|
195
|
+
for (const client of clients) client.send(message);
|
|
196
|
+
},
|
|
197
|
+
clientCount: (): number => clients.size,
|
|
198
|
+
close: async (): Promise<void> => {
|
|
199
|
+
await app.shutdown();
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
package/src/cli/create.ts
CHANGED
|
@@ -114,7 +114,7 @@ function scaffold(
|
|
|
114
114
|
'@types/react-dom': '^19.2.3',
|
|
115
115
|
eslint: '^10.2.0',
|
|
116
116
|
prettier: '^3.8.1',
|
|
117
|
-
toilscript: '^0.1.
|
|
117
|
+
toilscript: '^0.1.11',
|
|
118
118
|
typescript: '^6.0.3',
|
|
119
119
|
};
|
|
120
120
|
for (const dep of requiredPackages(features).sort()) {
|
|
@@ -126,9 +126,9 @@ function scaffold(
|
|
|
126
126
|
type: 'module',
|
|
127
127
|
scripts: {
|
|
128
128
|
dev: 'toiljs dev',
|
|
129
|
-
build: '
|
|
129
|
+
build: 'toilscript --target release --rpcModule shared/server.ts && toiljs build',
|
|
130
130
|
'build:client': 'toiljs build',
|
|
131
|
-
'build:server': 'toilscript --target release',
|
|
131
|
+
'build:server': 'toilscript --target release --rpcModule shared/server.ts',
|
|
132
132
|
lint: 'eslint client',
|
|
133
133
|
typecheck: 'tsc --noEmit',
|
|
134
134
|
format: 'prettier --write "client/**/*.{ts,tsx,css,scss,less}" "client/public/**/*.html"',
|
|
@@ -152,10 +152,20 @@ function scaffold(
|
|
|
152
152
|
' },\n' +
|
|
153
153
|
'});\n',
|
|
154
154
|
'tsconfig.json':
|
|
155
|
-
'{\n
|
|
155
|
+
'{\n' +
|
|
156
|
+
' "extends": "toiljs/tsconfig",\n' +
|
|
157
|
+
' "compilerOptions": {\n' +
|
|
158
|
+
' "paths": { "shared/*": ["./shared/*"] }\n' +
|
|
159
|
+
' },\n' +
|
|
160
|
+
' "include": ["client", "shared", "toil-env.d.ts", "toil-routes.d.ts"]\n' +
|
|
161
|
+
'}\n',
|
|
156
162
|
'eslint.config.js': "import toiljs from 'toiljs/eslint';\n\nexport default toiljs;\n",
|
|
157
163
|
'.prettierrc': '"toiljs/prettier"\n',
|
|
158
|
-
'.
|
|
164
|
+
// Generated files don't need formatting. (toilscript server decorators like @main /
|
|
165
|
+
// @remote-on-functions are handled by the toiljs/prettier-plugin, so server/ is not ignored.)
|
|
166
|
+
'.prettierignore':
|
|
167
|
+
'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\n',
|
|
168
|
+
'.gitignore': 'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\n',
|
|
159
169
|
// Use the project's pinned TypeScript (node_modules) instead of VS Code's bundled version.
|
|
160
170
|
'.vscode/settings.json':
|
|
161
171
|
JSON.stringify({ 'typescript.tsdk': 'node_modules/typescript/lib' }, null, 4) + '\n',
|
package/src/cli/diagnostics.ts
CHANGED
|
@@ -400,6 +400,87 @@ export function checkWasmBuilt(exists: boolean): Check {
|
|
|
400
400
|
};
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
// --- Typed RPC (@data / @remote / @service) -------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
/** Minimum toilscript: the @rest/@route HTTP layer + RPC codegen + hardened decoders + editor decls. */
|
|
406
|
+
export const RPC_TOILSCRIPT_MIN = '0.1.11';
|
|
407
|
+
|
|
408
|
+
/** Whether each piece of the typed-RPC wiring is in place (computed in `doctor.ts`). */
|
|
409
|
+
export interface RpcFacts {
|
|
410
|
+
/** `build:server` runs toilscript with `--rpcModule`. */
|
|
411
|
+
readonly buildServerWired: boolean;
|
|
412
|
+
/** tsconfig includes `shared` and has the `shared/*` path alias. */
|
|
413
|
+
readonly tsconfigWired: boolean;
|
|
414
|
+
/** `.gitignore` ignores the generated `shared/server.ts`. */
|
|
415
|
+
readonly gitignoreWired: boolean;
|
|
416
|
+
/** The declared toilscript range is at least {@link RPC_TOILSCRIPT_MIN}. */
|
|
417
|
+
readonly toilscriptOk: boolean;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* One check for the typed-RPC setup (`@data`/`@remote` -> generated `Server`). Warns (does not fail)
|
|
422
|
+
* when an existing project predates the feature, and points at the one-command upgrade.
|
|
423
|
+
*/
|
|
424
|
+
export function checkRpcWiring(f: RpcFacts): Check {
|
|
425
|
+
const missing: string[] = [];
|
|
426
|
+
if (!f.toilscriptOk) missing.push(`toilscript >=${RPC_TOILSCRIPT_MIN}`);
|
|
427
|
+
if (!f.buildServerWired) missing.push('build:server --rpcModule');
|
|
428
|
+
if (!f.tsconfigWired) missing.push('tsconfig shared/ + alias');
|
|
429
|
+
if (!f.gitignoreWired) missing.push('.gitignore shared/server.ts');
|
|
430
|
+
if (missing.length === 0) {
|
|
431
|
+
return { id: 'rpc-wiring', label: 'typed RPC wiring', status: 'pass' };
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
id: 'rpc-wiring',
|
|
435
|
+
label: 'typed RPC wiring',
|
|
436
|
+
status: 'warn',
|
|
437
|
+
detail: `missing: ${missing.join(', ')}`,
|
|
438
|
+
fix: 'Run `toiljs doctor --fix` to wire @data/@remote RPC (build:server, tsconfig, .gitignore, toilscript).',
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Whether the project's prettier setup pulls in the toilscript plugin (`toiljs/prettier-plugin`,
|
|
444
|
+
* or the `toiljs/prettier` shareable that bundles it). Without it, prettier throws on the server's
|
|
445
|
+
* native function decorators (`@main`, `@remote function ...`).
|
|
446
|
+
*/
|
|
447
|
+
export function checkPrettierPlugin(present: boolean): Check {
|
|
448
|
+
return present
|
|
449
|
+
? { id: 'prettier-plugin', label: 'prettier toilscript plugin', status: 'pass' }
|
|
450
|
+
: {
|
|
451
|
+
id: 'prettier-plugin',
|
|
452
|
+
label: 'prettier toilscript plugin',
|
|
453
|
+
status: 'warn',
|
|
454
|
+
detail: 'prettier will fail on @main / @remote-on-function in server code',
|
|
455
|
+
fix: 'Run `toiljs doctor --fix` to add toiljs/prettier-plugin to your prettier config.',
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export interface RestFacts {
|
|
460
|
+
/** The server declares at least one `@rest` controller. */
|
|
461
|
+
readonly hasControllers: boolean;
|
|
462
|
+
/** Some server file dispatches them: a `Rest.dispatch(` call, or a `RestHandler`. */
|
|
463
|
+
readonly dispatched: boolean;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Guards the easy-to-miss wiring step for the HTTP layer: a `@rest` controller self-registers,
|
|
468
|
+
* but its routes are only served if a handler calls `Rest.dispatch(req)` (or the project uses
|
|
469
|
+
* `RestHandler`). Without that, the routes silently 404 - a confusing footgun, so we warn.
|
|
470
|
+
*/
|
|
471
|
+
export function checkRestDispatch(f: RestFacts): Check {
|
|
472
|
+
if (!f.hasControllers || f.dispatched) {
|
|
473
|
+
return { id: 'rest-dispatch', label: 'REST dispatch wiring', status: 'pass' };
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
id: 'rest-dispatch',
|
|
477
|
+
label: 'REST dispatch wiring',
|
|
478
|
+
status: 'warn',
|
|
479
|
+
detail: '@rest controllers found, but nothing calls Rest.dispatch(req) - their routes will not be served',
|
|
480
|
+
fix: 'In your handler add `const hit = Rest.dispatch(req); if (hit != null) return hit;`, or set `Server.handler = () => new RestHandler()`.',
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
403
484
|
// --- Summary --------------------------------------------------------------------------------------
|
|
404
485
|
|
|
405
486
|
export function summarize(groups: readonly CheckGroup[]): DoctorSummary {
|