toiljs 0.0.15 → 0.0.16
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 +5 -5
- package/LICENSE +187 -187
- package/README.md +339 -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/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +0 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.d.ts +6 -0
- package/build/client/dev/devtools.js +442 -0
- package/build/client/dev/error-overlay.d.ts +9 -0
- package/build/client/dev/error-overlay.js +19 -4
- 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/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +16 -0
- package/build/compiler/config.js +7 -0
- package/build/compiler/docs.js +16 -16
- package/build/compiler/index.d.ts +2 -2
- package/build/compiler/index.js +1 -1
- package/build/compiler/plugin.js +156 -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 +5 -4
- package/build/compiler/ssg.js +32 -1
- package/build/io/.tsbuildinfo +1 -1
- 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/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 +24 -24
- package/examples/basic/client/routes/loader-demo/loading.tsx +13 -13
- package/examples/basic/client/routes/search.tsx +61 -61
- package/examples/basic/client/toil.tsx +5 -5
- package/package.json +155 -148
- package/presets/eslint.js +88 -88
- package/presets/no-uint8array-tostring.js +200 -200
- package/presets/prettier.json +18 -18
- package/presets/tsconfig.json +37 -37
- package/src/backend/index.ts +160 -160
- 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 +973 -0
- package/src/client/dev/error-overlay.tsx +30 -4
- package/src/client/head/head.ts +167 -167
- package/src/client/head/metadata.ts +112 -112
- package/src/client/index.ts +89 -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/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 +219 -182
- package/src/compiler/docs.ts +228 -228
- package/src/compiler/generate.ts +394 -394
- package/src/compiler/index.ts +64 -57
- package/src/compiler/pages.ts +70 -70
- package/src/compiler/plugin.ts +170 -2
- package/src/compiler/prerender.ts +156 -156
- package/src/compiler/seo.ts +397 -390
- package/src/compiler/ssg.ts +162 -126
- package/src/io/BinaryReader.ts +340 -340
- package/src/io/BinaryWriter.ts +385 -385
- package/src/io/FastMap.ts +127 -127
- package/src/io/index.ts +11 -11
- package/src/io/lengths.ts +14 -14
- package/src/io/types.ts +18 -18
- package/src/logger/index.ts +22 -22
- package/src/server/index.ts +10 -10
- package/src/server/main.ts +13 -13
- package/src/server/tsconfig.json +4 -4
- 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 +7 -7
- package/test/channel.test.ts +21 -21
- 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/io.test.ts +93 -93
- package/test/navlink.test.ts +28 -28
- package/test/placeholder.test.ts +9 -9
- package/test/routes.test.ts +76 -76
- 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/toil-routes.d.ts +7 -0
- package/toilconfig.json +30 -30
- 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/toil-env.d.ts +0 -16
package/presets/tsconfig.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
// toiljs shared client tsconfig, opinionated, strict.
|
|
2
|
-
// Extend it from your project: { "extends": "toiljs/tsconfig", "include": ["client", ".toil"] }
|
|
3
|
-
{
|
|
4
|
-
"compilerOptions": {
|
|
5
|
-
"strict": true,
|
|
6
|
-
"noImplicitAny": true,
|
|
7
|
-
"strictNullChecks": true,
|
|
8
|
-
"strictFunctionTypes": true,
|
|
9
|
-
"strictBindCallApply": true,
|
|
10
|
-
"strictPropertyInitialization": true,
|
|
11
|
-
"noImplicitThis": true,
|
|
12
|
-
"useUnknownInCatchVariables": true,
|
|
13
|
-
"alwaysStrict": true,
|
|
14
|
-
"noUnusedLocals": true,
|
|
15
|
-
"noUnusedParameters": true,
|
|
16
|
-
"exactOptionalPropertyTypes": false,
|
|
17
|
-
"noImplicitReturns": false,
|
|
18
|
-
"noFallthroughCasesInSwitch": true,
|
|
19
|
-
"noUncheckedIndexedAccess": false,
|
|
20
|
-
"noImplicitOverride": true,
|
|
21
|
-
"noPropertyAccessFromIndexSignature": false,
|
|
22
|
-
"module": "ES2020",
|
|
23
|
-
"target": "ES2020",
|
|
24
|
-
"moduleResolution": "Bundler",
|
|
25
|
-
"jsx": "react-jsx",
|
|
26
|
-
"lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
|
|
27
|
-
"isolatedModules": true,
|
|
28
|
-
"verbatimModuleSyntax": false,
|
|
29
|
-
"allowImportingTsExtensions": true,
|
|
30
|
-
"esModuleInterop": true,
|
|
31
|
-
"resolveJsonModule": true,
|
|
32
|
-
"forceConsistentCasingInFileNames": true,
|
|
33
|
-
"skipLibCheck": true,
|
|
34
|
-
"moduleDetection": "force",
|
|
35
|
-
"noEmit": true
|
|
36
|
-
}
|
|
37
|
-
}
|
|
1
|
+
// toiljs shared client tsconfig, opinionated, strict.
|
|
2
|
+
// Extend it from your project: { "extends": "toiljs/tsconfig", "include": ["client", ".toil"] }
|
|
3
|
+
{
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"strict": true,
|
|
6
|
+
"noImplicitAny": true,
|
|
7
|
+
"strictNullChecks": true,
|
|
8
|
+
"strictFunctionTypes": true,
|
|
9
|
+
"strictBindCallApply": true,
|
|
10
|
+
"strictPropertyInitialization": true,
|
|
11
|
+
"noImplicitThis": true,
|
|
12
|
+
"useUnknownInCatchVariables": true,
|
|
13
|
+
"alwaysStrict": true,
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noUnusedParameters": true,
|
|
16
|
+
"exactOptionalPropertyTypes": false,
|
|
17
|
+
"noImplicitReturns": false,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"noUncheckedIndexedAccess": false,
|
|
20
|
+
"noImplicitOverride": true,
|
|
21
|
+
"noPropertyAccessFromIndexSignature": false,
|
|
22
|
+
"module": "ES2020",
|
|
23
|
+
"target": "ES2020",
|
|
24
|
+
"moduleResolution": "Bundler",
|
|
25
|
+
"jsx": "react-jsx",
|
|
26
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
|
|
27
|
+
"isolatedModules": true,
|
|
28
|
+
"verbatimModuleSyntax": false,
|
|
29
|
+
"allowImportingTsExtensions": true,
|
|
30
|
+
"esModuleInterop": true,
|
|
31
|
+
"resolveJsonModule": true,
|
|
32
|
+
"forceConsistentCasingInFileNames": true,
|
|
33
|
+
"skipLibCheck": true,
|
|
34
|
+
"moduleDetection": "force",
|
|
35
|
+
"noEmit": true
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/backend/index.ts
CHANGED
|
@@ -1,160 +1,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 target in `src/server`.
|
|
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
|
-
/** Bind host. Default `0.0.0.0`. */
|
|
39
|
-
readonly host?: string;
|
|
40
|
-
/** WebSocket channel path. Default `/_toil`. */
|
|
41
|
-
readonly wsPath?: string;
|
|
42
|
-
/** Send permissive CORS headers + handle preflight. Default `true`. */
|
|
43
|
-
readonly cors?: boolean;
|
|
44
|
-
/** Max request body length in bytes. Default 8 MB. */
|
|
45
|
-
readonly maxBodyLength?: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** A running backend instance. */
|
|
49
|
-
export interface RunningBackend {
|
|
50
|
-
readonly port: number;
|
|
51
|
-
readonly host: string;
|
|
52
|
-
readonly wsPath: string;
|
|
53
|
-
/** Sends a message to every connected WebSocket client. */
|
|
54
|
-
broadcast(message: string): void;
|
|
55
|
-
/** Number of currently-connected WebSocket clients. */
|
|
56
|
-
clientCount(): number;
|
|
57
|
-
/** Gracefully shuts the server down. */
|
|
58
|
-
close(): Promise<void>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Resolves a request path to a file inside `root`, guarding against path traversal. */
|
|
62
|
-
function resolveStaticFile(root: string, requestPath: string): string | null {
|
|
63
|
-
const decoded = decodeURIComponent(requestPath);
|
|
64
|
-
const resolved = path.join(root, decoded);
|
|
65
|
-
if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
|
|
66
|
-
if (decoded === '/' || decoded === '') return null;
|
|
67
|
-
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) return resolved;
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Starts the hyper-express server serving `root` with an SPA fallback to `index.html`,
|
|
73
|
-
* plus a WebSocket channel at `wsPath`. Resolves once the server is listening.
|
|
74
|
-
*/
|
|
75
|
-
export async function startBackend(options: BackendOptions): Promise<RunningBackend> {
|
|
76
|
-
const port = options.port ?? 3000;
|
|
77
|
-
const host = options.host ?? '0.0.0.0';
|
|
78
|
-
const wsPath = options.wsPath ?? '/_toil';
|
|
79
|
-
const cors = options.cors ?? true;
|
|
80
|
-
const root = path.resolve(options.root);
|
|
81
|
-
const indexHtml = path.join(root, 'index.html');
|
|
82
|
-
|
|
83
|
-
const app = new Server({
|
|
84
|
-
max_body_length: options.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH,
|
|
85
|
-
max_body_buffer: MAX_BODY_BUFFER,
|
|
86
|
-
fast_abort: true,
|
|
87
|
-
idle_timeout: HTTP_IDLE_TIMEOUT,
|
|
88
|
-
response_timeout: HTTP_RESPONSE_TIMEOUT,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const clients = new Set<Websocket>();
|
|
92
|
-
|
|
93
|
-
app.set_error_handler((_request: Request, response: Response, _error: Error) => {
|
|
94
|
-
if (response.completed) return;
|
|
95
|
-
response.atomic(() => {
|
|
96
|
-
response.status(500).json({ error: 'Internal server error.' });
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
if (cors) {
|
|
101
|
-
app.use((request: Request, response: Response, next: MiddlewareNext) => {
|
|
102
|
-
if (request.method !== 'OPTIONS') {
|
|
103
|
-
response.setHeader('Access-Control-Allow-Origin', '*');
|
|
104
|
-
response.setHeader('Access-Control-Allow-Methods', CORS_METHODS);
|
|
105
|
-
response.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
|
|
106
|
-
}
|
|
107
|
-
response.removeHeader('uWebSockets');
|
|
108
|
-
next();
|
|
109
|
-
});
|
|
110
|
-
app.options('/*', (_request: Request, response: Response) => {
|
|
111
|
-
response.setHeader('Access-Control-Allow-Origin', '*');
|
|
112
|
-
response.setHeader('Access-Control-Allow-Methods', CORS_METHODS);
|
|
113
|
-
response.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
|
|
114
|
-
response.setHeader('Access-Control-Max-Age', '86400');
|
|
115
|
-
response.status(204).send();
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
app.ws(
|
|
120
|
-
wsPath,
|
|
121
|
-
{
|
|
122
|
-
message_type: 'String',
|
|
123
|
-
max_payload_length: WS_MAX_PAYLOAD_LENGTH,
|
|
124
|
-
idle_timeout: WS_IDLE_TIMEOUT,
|
|
125
|
-
max_backpressure: WS_MAX_BACKPRESSURE,
|
|
126
|
-
},
|
|
127
|
-
(ws) => {
|
|
128
|
-
clients.add(ws);
|
|
129
|
-
ws.send(JSON.stringify({ type: 'connected', clients: clients.size }));
|
|
130
|
-
ws.on('message', (message: string) => {
|
|
131
|
-
for (const client of clients) client.send(message);
|
|
132
|
-
});
|
|
133
|
-
ws.on('drain', () => {});
|
|
134
|
-
ws.on('close', () => {
|
|
135
|
-
clients.delete(ws);
|
|
136
|
-
});
|
|
137
|
-
},
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
app.get('/*', (request: Request, response: Response) => {
|
|
141
|
-
if (response.completed) return;
|
|
142
|
-
const file = resolveStaticFile(root, request.path);
|
|
143
|
-
response.sendFile(file ?? indexHtml);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
await app.listen(port, host);
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
port,
|
|
150
|
-
host,
|
|
151
|
-
wsPath,
|
|
152
|
-
broadcast: (message: string): void => {
|
|
153
|
-
for (const client of clients) client.send(message);
|
|
154
|
-
},
|
|
155
|
-
clientCount: (): number => clients.size,
|
|
156
|
-
close: async (): Promise<void> => {
|
|
157
|
-
await app.shutdown();
|
|
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 target in `src/server`.
|
|
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
|
+
/** Bind host. Default `0.0.0.0`. */
|
|
39
|
+
readonly host?: string;
|
|
40
|
+
/** WebSocket channel path. Default `/_toil`. */
|
|
41
|
+
readonly wsPath?: string;
|
|
42
|
+
/** Send permissive CORS headers + handle preflight. Default `true`. */
|
|
43
|
+
readonly cors?: boolean;
|
|
44
|
+
/** Max request body length in bytes. Default 8 MB. */
|
|
45
|
+
readonly maxBodyLength?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A running backend instance. */
|
|
49
|
+
export interface RunningBackend {
|
|
50
|
+
readonly port: number;
|
|
51
|
+
readonly host: string;
|
|
52
|
+
readonly wsPath: string;
|
|
53
|
+
/** Sends a message to every connected WebSocket client. */
|
|
54
|
+
broadcast(message: string): void;
|
|
55
|
+
/** Number of currently-connected WebSocket clients. */
|
|
56
|
+
clientCount(): number;
|
|
57
|
+
/** Gracefully shuts the server down. */
|
|
58
|
+
close(): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Resolves a request path to a file inside `root`, guarding against path traversal. */
|
|
62
|
+
function resolveStaticFile(root: string, requestPath: string): string | null {
|
|
63
|
+
const decoded = decodeURIComponent(requestPath);
|
|
64
|
+
const resolved = path.join(root, decoded);
|
|
65
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
|
|
66
|
+
if (decoded === '/' || decoded === '') return null;
|
|
67
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) return resolved;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Starts the hyper-express server serving `root` with an SPA fallback to `index.html`,
|
|
73
|
+
* plus a WebSocket channel at `wsPath`. Resolves once the server is listening.
|
|
74
|
+
*/
|
|
75
|
+
export async function startBackend(options: BackendOptions): Promise<RunningBackend> {
|
|
76
|
+
const port = options.port ?? 3000;
|
|
77
|
+
const host = options.host ?? '0.0.0.0';
|
|
78
|
+
const wsPath = options.wsPath ?? '/_toil';
|
|
79
|
+
const cors = options.cors ?? true;
|
|
80
|
+
const root = path.resolve(options.root);
|
|
81
|
+
const indexHtml = path.join(root, 'index.html');
|
|
82
|
+
|
|
83
|
+
const app = new Server({
|
|
84
|
+
max_body_length: options.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH,
|
|
85
|
+
max_body_buffer: MAX_BODY_BUFFER,
|
|
86
|
+
fast_abort: true,
|
|
87
|
+
idle_timeout: HTTP_IDLE_TIMEOUT,
|
|
88
|
+
response_timeout: HTTP_RESPONSE_TIMEOUT,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const clients = new Set<Websocket>();
|
|
92
|
+
|
|
93
|
+
app.set_error_handler((_request: Request, response: Response, _error: Error) => {
|
|
94
|
+
if (response.completed) return;
|
|
95
|
+
response.atomic(() => {
|
|
96
|
+
response.status(500).json({ error: 'Internal server error.' });
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (cors) {
|
|
101
|
+
app.use((request: Request, response: Response, next: MiddlewareNext) => {
|
|
102
|
+
if (request.method !== 'OPTIONS') {
|
|
103
|
+
response.setHeader('Access-Control-Allow-Origin', '*');
|
|
104
|
+
response.setHeader('Access-Control-Allow-Methods', CORS_METHODS);
|
|
105
|
+
response.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
|
|
106
|
+
}
|
|
107
|
+
response.removeHeader('uWebSockets');
|
|
108
|
+
next();
|
|
109
|
+
});
|
|
110
|
+
app.options('/*', (_request: Request, response: Response) => {
|
|
111
|
+
response.setHeader('Access-Control-Allow-Origin', '*');
|
|
112
|
+
response.setHeader('Access-Control-Allow-Methods', CORS_METHODS);
|
|
113
|
+
response.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
|
|
114
|
+
response.setHeader('Access-Control-Max-Age', '86400');
|
|
115
|
+
response.status(204).send();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
app.ws(
|
|
120
|
+
wsPath,
|
|
121
|
+
{
|
|
122
|
+
message_type: 'String',
|
|
123
|
+
max_payload_length: WS_MAX_PAYLOAD_LENGTH,
|
|
124
|
+
idle_timeout: WS_IDLE_TIMEOUT,
|
|
125
|
+
max_backpressure: WS_MAX_BACKPRESSURE,
|
|
126
|
+
},
|
|
127
|
+
(ws) => {
|
|
128
|
+
clients.add(ws);
|
|
129
|
+
ws.send(JSON.stringify({ type: 'connected', clients: clients.size }));
|
|
130
|
+
ws.on('message', (message: string) => {
|
|
131
|
+
for (const client of clients) client.send(message);
|
|
132
|
+
});
|
|
133
|
+
ws.on('drain', () => {});
|
|
134
|
+
ws.on('close', () => {
|
|
135
|
+
clients.delete(ws);
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
app.get('/*', (request: Request, response: Response) => {
|
|
141
|
+
if (response.completed) return;
|
|
142
|
+
const file = resolveStaticFile(root, request.path);
|
|
143
|
+
response.sendFile(file ?? indexHtml);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await app.listen(port, host);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
port,
|
|
150
|
+
host,
|
|
151
|
+
wsPath,
|
|
152
|
+
broadcast: (message: string): void => {
|
|
153
|
+
for (const client of clients) client.send(message);
|
|
154
|
+
},
|
|
155
|
+
clientCount: (): number => clients.size,
|
|
156
|
+
close: async (): Promise<void> => {
|
|
157
|
+
await app.shutdown();
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
package/src/cli/proc.ts
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Spawns `cmd args` in `cwd`, resolving on a 0 exit code and rejecting otherwise. On Windows the
|
|
5
|
-
* `npm`/`pnpm`/`yarn` shims are `.cmd` files that need a shell; passing an args array with
|
|
6
|
-
* `shell: true` is deprecated (DEP0190), so the whole command is passed as one string there
|
|
7
|
-
* (args are fixed/allowlisted, never raw user input). POSIX spawns directly.
|
|
8
|
-
*/
|
|
9
|
-
export function run(cmd: string, args: string[], cwd: string): Promise<void> {
|
|
10
|
-
return new Promise((resolve, reject) => {
|
|
11
|
-
const onWindows = process.platform === 'win32';
|
|
12
|
-
const child = onWindows
|
|
13
|
-
? spawn([cmd, ...args].join(' '), { cwd, stdio: 'ignore', shell: true })
|
|
14
|
-
: spawn(cmd, args, { cwd, stdio: 'ignore' });
|
|
15
|
-
child.on('error', reject);
|
|
16
|
-
child.on('close', (code) =>
|
|
17
|
-
code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${String(code)}`)),
|
|
18
|
-
);
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Like {@link run}, but captures stdout/stderr and resolves with them plus the exit code (it never
|
|
24
|
-
* rejects on a non-zero exit, the caller decides). Used to read JSON from tools like
|
|
25
|
-
* `npm-check-updates`. Same Windows shell handling as {@link run}; args are fixed/allowlisted.
|
|
26
|
-
*/
|
|
27
|
-
export function capture(
|
|
28
|
-
cmd: string,
|
|
29
|
-
args: string[],
|
|
30
|
-
cwd: string,
|
|
31
|
-
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
const onWindows = process.platform === 'win32';
|
|
34
|
-
const child = onWindows
|
|
35
|
-
? spawn([cmd, ...args].join(' '), { cwd, shell: true })
|
|
36
|
-
: spawn(cmd, args, { cwd });
|
|
37
|
-
let stdout = '';
|
|
38
|
-
let stderr = '';
|
|
39
|
-
child.stdout?.on('data', (d: Buffer) => {
|
|
40
|
-
stdout += d.toString();
|
|
41
|
-
});
|
|
42
|
-
child.stderr?.on('data', (d: Buffer) => {
|
|
43
|
-
stderr += d.toString();
|
|
44
|
-
});
|
|
45
|
-
child.on('error', reject);
|
|
46
|
-
child.on('close', (code) => {
|
|
47
|
-
resolve({ stdout, stderr, code: code ?? 1 });
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
}
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Spawns `cmd args` in `cwd`, resolving on a 0 exit code and rejecting otherwise. On Windows the
|
|
5
|
+
* `npm`/`pnpm`/`yarn` shims are `.cmd` files that need a shell; passing an args array with
|
|
6
|
+
* `shell: true` is deprecated (DEP0190), so the whole command is passed as one string there
|
|
7
|
+
* (args are fixed/allowlisted, never raw user input). POSIX spawns directly.
|
|
8
|
+
*/
|
|
9
|
+
export function run(cmd: string, args: string[], cwd: string): Promise<void> {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const onWindows = process.platform === 'win32';
|
|
12
|
+
const child = onWindows
|
|
13
|
+
? spawn([cmd, ...args].join(' '), { cwd, stdio: 'ignore', shell: true })
|
|
14
|
+
: spawn(cmd, args, { cwd, stdio: 'ignore' });
|
|
15
|
+
child.on('error', reject);
|
|
16
|
+
child.on('close', (code) =>
|
|
17
|
+
code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${String(code)}`)),
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Like {@link run}, but captures stdout/stderr and resolves with them plus the exit code (it never
|
|
24
|
+
* rejects on a non-zero exit, the caller decides). Used to read JSON from tools like
|
|
25
|
+
* `npm-check-updates`. Same Windows shell handling as {@link run}; args are fixed/allowlisted.
|
|
26
|
+
*/
|
|
27
|
+
export function capture(
|
|
28
|
+
cmd: string,
|
|
29
|
+
args: string[],
|
|
30
|
+
cwd: string,
|
|
31
|
+
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const onWindows = process.platform === 'win32';
|
|
34
|
+
const child = onWindows
|
|
35
|
+
? spawn([cmd, ...args].join(' '), { cwd, shell: true })
|
|
36
|
+
: spawn(cmd, args, { cwd });
|
|
37
|
+
let stdout = '';
|
|
38
|
+
let stderr = '';
|
|
39
|
+
child.stdout?.on('data', (d: Buffer) => {
|
|
40
|
+
stdout += d.toString();
|
|
41
|
+
});
|
|
42
|
+
child.stderr?.on('data', (d: Buffer) => {
|
|
43
|
+
stderr += d.toString();
|
|
44
|
+
});
|
|
45
|
+
child.on('error', reject);
|
|
46
|
+
child.on('close', (code) => {
|
|
47
|
+
resolve({ stdout, stderr, code: code ?? 1 });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
package/src/cli/updates.ts
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure helpers for `toiljs update`: parse `npm-check-updates --jsonUpgraded` output and classify the
|
|
3
|
-
* semver bump of each upgrade. IO-free so it can be unit-tested; the spawn/UI live in `update.ts`.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/** The kind of version jump an upgrade represents. */
|
|
7
|
-
export type Bump = 'major' | 'minor' | 'patch' | 'other';
|
|
8
|
-
|
|
9
|
-
/** One available upgrade: package, current range, target range, and bump kind. */
|
|
10
|
-
export interface UpdateRow {
|
|
11
|
-
readonly name: string;
|
|
12
|
-
readonly from: string;
|
|
13
|
-
readonly to: string;
|
|
14
|
-
readonly bump: Bump;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Extracts a version's leading `x.y.z` (ignoring `^`, `~`, `>=`, etc.); missing parts become 0. */
|
|
18
|
-
function parseVersion(v: string): [number, number, number] {
|
|
19
|
-
const m = /(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(v);
|
|
20
|
-
if (!m) return [0, 0, 0];
|
|
21
|
-
return [Number(m[1]), Number(m[2] ?? 0), Number(m[3] ?? 0)];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Classifies the bump from `from` to `to` (both may be ranges like `^1.2.3`). */
|
|
25
|
-
export function classifyBump(from: string, to: string): Bump {
|
|
26
|
-
const [fa, fb, fc] = parseVersion(from);
|
|
27
|
-
const [ta, tb, tc] = parseVersion(to);
|
|
28
|
-
if (ta !== fa) return 'major';
|
|
29
|
-
if (tb !== fb) return 'minor';
|
|
30
|
-
if (tc !== fc) return 'patch';
|
|
31
|
-
return 'other';
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Parses the JSON object `npm-check-updates --jsonUpgraded` prints (a `{ name: range }` map). Tolerant
|
|
36
|
-
* of leading/trailing noise (npx banners) by slicing to the outermost braces. Returns `{}` on failure.
|
|
37
|
-
*/
|
|
38
|
-
export function parseNcuJson(stdout: string): Record<string, string> {
|
|
39
|
-
const start = stdout.indexOf('{');
|
|
40
|
-
const end = stdout.lastIndexOf('}');
|
|
41
|
-
if (start === -1 || end <= start) return {};
|
|
42
|
-
try {
|
|
43
|
-
const parsed: unknown = JSON.parse(stdout.slice(start, end + 1));
|
|
44
|
-
if (typeof parsed !== 'object' || parsed === null) return {};
|
|
45
|
-
const out: Record<string, string> = {};
|
|
46
|
-
for (const [k, v] of Object.entries(parsed)) if (typeof v === 'string') out[k] = v;
|
|
47
|
-
return out;
|
|
48
|
-
} catch {
|
|
49
|
-
return {};
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const SEVERITY: Record<Bump, number> = { major: 0, minor: 1, patch: 2, other: 3 };
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Builds the upgrade rows from the ncu map and the project's current dependency ranges, sorted by
|
|
57
|
-
* bump severity (major first) then name.
|
|
58
|
-
*/
|
|
59
|
-
export function buildRows(
|
|
60
|
-
upgraded: Record<string, string>,
|
|
61
|
-
currentDeps: Record<string, string>,
|
|
62
|
-
): UpdateRow[] {
|
|
63
|
-
return Object.entries(upgraded)
|
|
64
|
-
.map(([name, to]) => {
|
|
65
|
-
const from = currentDeps[name] ?? '?';
|
|
66
|
-
return { name, from, to, bump: classifyBump(from, to) };
|
|
67
|
-
})
|
|
68
|
-
.sort((a, b) => SEVERITY[a.bump] - SEVERITY[b.bump] || a.name.localeCompare(b.name));
|
|
69
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for `toiljs update`: parse `npm-check-updates --jsonUpgraded` output and classify the
|
|
3
|
+
* semver bump of each upgrade. IO-free so it can be unit-tested; the spawn/UI live in `update.ts`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** The kind of version jump an upgrade represents. */
|
|
7
|
+
export type Bump = 'major' | 'minor' | 'patch' | 'other';
|
|
8
|
+
|
|
9
|
+
/** One available upgrade: package, current range, target range, and bump kind. */
|
|
10
|
+
export interface UpdateRow {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly from: string;
|
|
13
|
+
readonly to: string;
|
|
14
|
+
readonly bump: Bump;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Extracts a version's leading `x.y.z` (ignoring `^`, `~`, `>=`, etc.); missing parts become 0. */
|
|
18
|
+
function parseVersion(v: string): [number, number, number] {
|
|
19
|
+
const m = /(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(v);
|
|
20
|
+
if (!m) return [0, 0, 0];
|
|
21
|
+
return [Number(m[1]), Number(m[2] ?? 0), Number(m[3] ?? 0)];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Classifies the bump from `from` to `to` (both may be ranges like `^1.2.3`). */
|
|
25
|
+
export function classifyBump(from: string, to: string): Bump {
|
|
26
|
+
const [fa, fb, fc] = parseVersion(from);
|
|
27
|
+
const [ta, tb, tc] = parseVersion(to);
|
|
28
|
+
if (ta !== fa) return 'major';
|
|
29
|
+
if (tb !== fb) return 'minor';
|
|
30
|
+
if (tc !== fc) return 'patch';
|
|
31
|
+
return 'other';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parses the JSON object `npm-check-updates --jsonUpgraded` prints (a `{ name: range }` map). Tolerant
|
|
36
|
+
* of leading/trailing noise (npx banners) by slicing to the outermost braces. Returns `{}` on failure.
|
|
37
|
+
*/
|
|
38
|
+
export function parseNcuJson(stdout: string): Record<string, string> {
|
|
39
|
+
const start = stdout.indexOf('{');
|
|
40
|
+
const end = stdout.lastIndexOf('}');
|
|
41
|
+
if (start === -1 || end <= start) return {};
|
|
42
|
+
try {
|
|
43
|
+
const parsed: unknown = JSON.parse(stdout.slice(start, end + 1));
|
|
44
|
+
if (typeof parsed !== 'object' || parsed === null) return {};
|
|
45
|
+
const out: Record<string, string> = {};
|
|
46
|
+
for (const [k, v] of Object.entries(parsed)) if (typeof v === 'string') out[k] = v;
|
|
47
|
+
return out;
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const SEVERITY: Record<Bump, number> = { major: 0, minor: 1, patch: 2, other: 3 };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Builds the upgrade rows from the ncu map and the project's current dependency ranges, sorted by
|
|
57
|
+
* bump severity (major first) then name.
|
|
58
|
+
*/
|
|
59
|
+
export function buildRows(
|
|
60
|
+
upgraded: Record<string, string>,
|
|
61
|
+
currentDeps: Record<string, string>,
|
|
62
|
+
): UpdateRow[] {
|
|
63
|
+
return Object.entries(upgraded)
|
|
64
|
+
.map(([name, to]) => {
|
|
65
|
+
const from = currentDeps[name] ?? '?';
|
|
66
|
+
return { name, from, to, bump: classifyBump(from, to) };
|
|
67
|
+
})
|
|
68
|
+
.sort((a, b) => SEVERITY[a.bump] - SEVERITY[b.bump] || a.name.localeCompare(b.name));
|
|
69
|
+
}
|