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.
Files changed (273) hide show
  1. package/.babelrc +13 -13
  2. package/.gitattributes +2 -2
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +38 -38
  4. package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -90
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
  7. package/.github/PULL_REQUEST_TEMPLATE.md +43 -43
  8. package/.github/changelog-config.json +45 -45
  9. package/.github/dependabot.yml +27 -27
  10. package/.github/workflows/ci.yml +191 -191
  11. package/.prettierrc.json +11 -11
  12. package/.vscode/settings.json +9 -9
  13. package/CHANGELOG.md +116 -5
  14. package/LICENSE +187 -187
  15. package/README.md +524 -315
  16. package/as-pect.asconfig.json +34 -34
  17. package/as-pect.config.js +65 -65
  18. package/assets/logo.svg +36 -36
  19. package/build/backend/.tsbuildinfo +1 -1
  20. package/build/backend/index.d.ts +1 -0
  21. package/build/backend/index.js +20 -1
  22. package/build/cli/.tsbuildinfo +1 -1
  23. package/build/cli/index.js +1320 -696
  24. package/build/client/.tsbuildinfo +1 -1
  25. package/build/client/dev/devtools.d.ts +6 -0
  26. package/build/client/dev/devtools.js +479 -0
  27. package/build/client/dev/error-overlay.d.ts +9 -0
  28. package/build/client/dev/error-overlay.js +19 -4
  29. package/build/client/errors.d.ts +1 -0
  30. package/build/client/errors.js +3 -0
  31. package/build/client/index.d.ts +2 -0
  32. package/build/client/index.js +2 -0
  33. package/build/client/navigation/prefetch.d.ts +1 -0
  34. package/build/client/navigation/prefetch.js +35 -0
  35. package/build/client/routing/Router.js +1 -1
  36. package/build/client/routing/hooks.js +6 -2
  37. package/build/client/routing/loader.d.ts +23 -0
  38. package/build/client/routing/loader.js +53 -7
  39. package/build/client/routing/mount.js +4 -3
  40. package/build/client/rpc.d.ts +1 -0
  41. package/build/client/rpc.js +37 -0
  42. package/build/compiler/.tsbuildinfo +1 -1
  43. package/build/compiler/config.d.ts +16 -0
  44. package/build/compiler/config.js +9 -0
  45. package/build/compiler/docs.js +78 -21
  46. package/build/compiler/generate.js +5 -4
  47. package/build/compiler/index.d.ts +3 -2
  48. package/build/compiler/index.js +2 -2
  49. package/build/compiler/plugin.js +228 -0
  50. package/build/compiler/prerender.d.ts +1 -0
  51. package/build/compiler/prerender.js +1 -1
  52. package/build/compiler/seo.d.ts +1 -1
  53. package/build/compiler/seo.js +20 -5
  54. package/build/compiler/ssg.js +39 -2
  55. package/build/compiler/vite.js +25 -0
  56. package/build/io/.tsbuildinfo +1 -1
  57. package/build/io/codec.d.ts +54 -0
  58. package/build/io/codec.js +143 -0
  59. package/build/io/index.d.ts +1 -2
  60. package/build/io/index.js +1 -2
  61. package/build/logger/.tsbuildinfo +1 -1
  62. package/build/shared/.tsbuildinfo +1 -1
  63. package/eslint.config.js +48 -48
  64. package/examples/basic/client/404.tsx +11 -11
  65. package/examples/basic/client/components/.gitkeep +1 -1
  66. package/examples/basic/client/global-error.tsx +13 -13
  67. package/examples/basic/client/layout.tsx +25 -25
  68. package/examples/basic/client/public/images/.gitkeep +1 -1
  69. package/examples/basic/client/public/images/logo.svg +36 -36
  70. package/examples/basic/client/public/robots.txt +2 -2
  71. package/examples/basic/client/routes/docs/[...slug].tsx +12 -12
  72. package/examples/basic/client/routes/features/error/error.tsx +16 -16
  73. package/examples/basic/client/routes/features/index.tsx +1 -1
  74. package/examples/basic/client/routes/features/template/b.tsx +14 -14
  75. package/examples/basic/client/routes/files/[[...slug]].tsx +21 -21
  76. package/examples/basic/client/routes/gallery/layout.tsx +13 -13
  77. package/examples/basic/client/routes/io.tsx +23 -24
  78. package/examples/basic/client/routes/loader-demo/loading.tsx +13 -13
  79. package/examples/basic/client/routes/rest.tsx +74 -0
  80. package/examples/basic/client/routes/rpc.tsx +43 -0
  81. package/examples/basic/client/routes/search.tsx +61 -61
  82. package/examples/basic/client/toil.tsx +5 -5
  83. package/package.json +167 -148
  84. package/presets/eslint.js +88 -88
  85. package/presets/no-uint8array-tostring.js +200 -200
  86. package/presets/prettier-plugin.js +51 -0
  87. package/presets/prettier.json +19 -18
  88. package/presets/tsconfig.json +37 -37
  89. package/server/runtime/README.md +97 -0
  90. package/server/runtime/abort/abort.ts +27 -0
  91. package/server/runtime/env/Server.ts +61 -0
  92. package/server/runtime/envelope.ts +191 -0
  93. package/server/runtime/exports/index.ts +52 -0
  94. package/server/runtime/handlers/ToilHandler.ts +34 -0
  95. package/server/runtime/index.ts +26 -0
  96. package/server/runtime/lang/Potential.ts +5 -0
  97. package/server/runtime/memory.ts +81 -0
  98. package/server/runtime/request.ts +55 -0
  99. package/server/runtime/response.ts +86 -0
  100. package/server/runtime/rest/Rest.ts +39 -0
  101. package/server/runtime/rest/RestHandler.ts +20 -0
  102. package/server/runtime/rest/RouteContext.ts +82 -0
  103. package/server/runtime/rest/match.ts +48 -0
  104. package/server/runtime/tsconfig.json +7 -0
  105. package/src/backend/index.ts +202 -160
  106. package/src/cli/create.ts +15 -5
  107. package/src/cli/diagnostics.ts +81 -0
  108. package/src/cli/doctor.ts +384 -7
  109. package/src/cli/index.ts +11 -2
  110. package/src/cli/proc.ts +50 -50
  111. package/src/cli/updates.ts +69 -69
  112. package/src/cli/validate.ts +31 -31
  113. package/src/client/channel/channel.ts +146 -146
  114. package/src/client/components/Form.tsx +65 -65
  115. package/src/client/components/Script.tsx +113 -113
  116. package/src/client/components/Slot.tsx +21 -21
  117. package/src/client/dev/devtools.tsx +1018 -0
  118. package/src/client/dev/error-overlay.tsx +30 -4
  119. package/src/client/errors.ts +11 -0
  120. package/src/client/head/head.ts +167 -167
  121. package/src/client/head/metadata.ts +112 -112
  122. package/src/client/index.ts +91 -89
  123. package/src/client/navigation/NavLink.tsx +86 -86
  124. package/src/client/navigation/navigation.ts +235 -235
  125. package/src/client/navigation/prefetch.ts +169 -130
  126. package/src/client/navigation/scroll.ts +53 -53
  127. package/src/client/routing/Router.tsx +8 -2
  128. package/src/client/routing/action.ts +122 -122
  129. package/src/client/routing/error-boundary.tsx +43 -43
  130. package/src/client/routing/hooks.ts +21 -6
  131. package/src/client/routing/loader.ts +325 -235
  132. package/src/client/routing/match.ts +47 -47
  133. package/src/client/routing/mount.tsx +54 -52
  134. package/src/client/routing/params-context.ts +10 -10
  135. package/src/client/routing/slot-context.ts +7 -7
  136. package/src/client/rpc.ts +64 -0
  137. package/src/client/search/search.ts +189 -189
  138. package/src/client/search/use-page-search.ts +73 -73
  139. package/src/client/types.ts +73 -73
  140. package/src/compiler/config.ts +221 -182
  141. package/src/compiler/docs.ts +285 -228
  142. package/src/compiler/generate.ts +395 -394
  143. package/src/compiler/index.ts +66 -57
  144. package/src/compiler/pages.ts +70 -70
  145. package/src/compiler/plugin.ts +258 -2
  146. package/src/compiler/prerender.ts +156 -156
  147. package/src/compiler/seo.ts +417 -390
  148. package/src/compiler/ssg.ts +171 -126
  149. package/src/compiler/vite.ts +34 -0
  150. package/src/io/FastMap.ts +151 -127
  151. package/src/io/FastSet.ts +15 -1
  152. package/src/io/codec.ts +217 -0
  153. package/src/io/index.ts +10 -11
  154. package/src/io/lengths.ts +14 -14
  155. package/src/io/types.ts +19 -18
  156. package/src/logger/index.ts +22 -22
  157. package/src/shared/index.ts +10 -10
  158. package/std/client/index.d.ts +15 -15
  159. package/std/client/package.json +3 -3
  160. package/test/assembly/example.spec.ts +17 -7
  161. package/test/channel.test.ts +21 -21
  162. package/test/doctor.test.ts +65 -0
  163. package/test/dom/Link.test.tsx +47 -47
  164. package/test/dom/NavLink.test.tsx +37 -37
  165. package/test/dom/error-overlay.test.tsx +44 -44
  166. package/test/dom/loader.test.tsx +121 -121
  167. package/test/dom/navigation.test.ts +59 -59
  168. package/test/dom/revalidate.test.tsx +38 -38
  169. package/test/dom/route-head.test.tsx +78 -78
  170. package/test/dom/router-loading.test.tsx +44 -44
  171. package/test/dom/scroll.test.ts +56 -56
  172. package/test/dom/use-metadata.test.tsx +58 -58
  173. package/test/errors.test.ts +21 -0
  174. package/test/io.test.ts +117 -93
  175. package/test/navlink.test.ts +28 -28
  176. package/test/placeholder.test.ts +9 -9
  177. package/test/prettier-plugin.test.ts +46 -0
  178. package/test/routes.test.ts +76 -76
  179. package/test/rpc.test.ts +50 -0
  180. package/test/seo.test.ts +175 -164
  181. package/test/slot-layouts.test.ts +69 -69
  182. package/test/ssg.test.ts +36 -36
  183. package/test/update.test.ts +44 -44
  184. package/test/validate.test.ts +42 -42
  185. package/tests/data-parity/generated-parity.ts +99 -0
  186. package/tests/data-parity/parity.ts +80 -0
  187. package/tests/data-parity/spec.ts +46 -0
  188. package/toil-routes.d.ts +7 -0
  189. package/tsconfig.backend.json +13 -13
  190. package/tsconfig.base.json +35 -35
  191. package/tsconfig.cli.json +13 -13
  192. package/tsconfig.client.json +14 -14
  193. package/tsconfig.compiler.json +13 -13
  194. package/tsconfig.io.json +12 -12
  195. package/tsconfig.json +22 -22
  196. package/tsconfig.logger.json +12 -12
  197. package/tsconfig.server.json +10 -10
  198. package/tsconfig.shared.json +12 -12
  199. package/vitest.config.ts +26 -26
  200. package/.idea/codeStyles/Project.xml +0 -54
  201. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  202. package/.idea/inspectionProfiles/Project_Default.xml +0 -6
  203. package/.idea/modules.xml +0 -8
  204. package/.idea/prettier.xml +0 -7
  205. package/.idea/toiljs.iml +0 -8
  206. package/.idea/vcs.xml +0 -6
  207. package/.toil/entry.tsx +0 -9
  208. package/.toil/index.html +0 -12
  209. package/.toil/routes.ts +0 -9
  210. package/build/cli/configure.d.ts +0 -16
  211. package/build/cli/configure.js +0 -272
  212. package/build/cli/create.d.ts +0 -16
  213. package/build/cli/create.js +0 -420
  214. package/build/cli/diagnostics.d.ts +0 -55
  215. package/build/cli/diagnostics.js +0 -333
  216. package/build/cli/doctor.d.ts +0 -6
  217. package/build/cli/doctor.js +0 -249
  218. package/build/cli/features.d.ts +0 -25
  219. package/build/cli/features.js +0 -107
  220. package/build/cli/index.d.ts +0 -2
  221. package/build/cli/proc.d.ts +0 -6
  222. package/build/cli/proc.js +0 -31
  223. package/build/cli/ui.d.ts +0 -9
  224. package/build/cli/ui.js +0 -75
  225. package/build/cli/update.d.ts +0 -7
  226. package/build/cli/update.js +0 -117
  227. package/build/cli/updates.d.ts +0 -10
  228. package/build/cli/updates.js +0 -45
  229. package/build/cli/validate.d.ts +0 -4
  230. package/build/cli/validate.js +0 -19
  231. package/build/client/Link.d.ts +0 -8
  232. package/build/client/Link.js +0 -44
  233. package/build/client/NavLink.d.ts +0 -14
  234. package/build/client/NavLink.js +0 -37
  235. package/build/client/Router.d.ts +0 -7
  236. package/build/client/Router.js +0 -55
  237. package/build/client/channel.d.ts +0 -23
  238. package/build/client/channel.js +0 -94
  239. package/build/client/error-boundary.d.ts +0 -16
  240. package/build/client/error-boundary.js +0 -19
  241. package/build/client/head.d.ts +0 -26
  242. package/build/client/head.js +0 -87
  243. package/build/client/hooks.d.ts +0 -17
  244. package/build/client/hooks.js +0 -48
  245. package/build/client/lazy.d.ts +0 -16
  246. package/build/client/lazy.js +0 -53
  247. package/build/client/match.d.ts +0 -2
  248. package/build/client/match.js +0 -32
  249. package/build/client/mount.d.ts +0 -2
  250. package/build/client/mount.js +0 -13
  251. package/build/client/navigation.d.ts +0 -13
  252. package/build/client/navigation.js +0 -97
  253. package/build/client/params-context.d.ts +0 -2
  254. package/build/client/params-context.js +0 -2
  255. package/build/client/prefetch.d.ts +0 -11
  256. package/build/client/prefetch.js +0 -100
  257. package/build/client/runtime.d.ts +0 -31
  258. package/build/client/runtime.js +0 -112
  259. package/build/client/scroll.d.ts +0 -8
  260. package/build/client/scroll.js +0 -36
  261. package/build/io/BinaryReader.d.ts +0 -44
  262. package/build/io/BinaryReader.js +0 -244
  263. package/build/io/BinaryWriter.d.ts +0 -44
  264. package/build/io/BinaryWriter.js +0 -297
  265. package/build/server/release.wasm +0 -0
  266. package/build/server/release.wat +0 -9
  267. package/src/io/BinaryReader.ts +0 -340
  268. package/src/io/BinaryWriter.ts +0 -385
  269. package/src/server/index.ts +0 -10
  270. package/src/server/main.ts +0 -13
  271. package/src/server/tsconfig.json +0 -4
  272. package/toil-env.d.ts +0 -16
  273. 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
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "toilscript/std/assembly.json",
3
+ "compilerOptions": {
4
+ "plugins": [{ "name": "toilscript/std/ts-plugin.cjs" }]
5
+ },
6
+ "include": ["./**/*.ts"]
7
+ }
@@ -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 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 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.2',
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: 'toiljs build && toilscript --target release',
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 "extends": "toiljs/tsconfig",\n "include": ["client", "toil-env.d.ts", "toil-routes.d.ts"]\n}\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
- '.gitignore': 'node_modules\nbuild\n.toil\ntoil-env.d.ts\ntoil-routes.d.ts\n',
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',
@@ -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 {