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.
Files changed (217) 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 +5 -5
  14. package/LICENSE +187 -187
  15. package/README.md +339 -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/cli/.tsbuildinfo +1 -1
  21. package/build/cli/index.js +0 -0
  22. package/build/client/.tsbuildinfo +1 -1
  23. package/build/client/dev/devtools.d.ts +6 -0
  24. package/build/client/dev/devtools.js +442 -0
  25. package/build/client/dev/error-overlay.d.ts +9 -0
  26. package/build/client/dev/error-overlay.js +19 -4
  27. package/build/client/navigation/prefetch.d.ts +1 -0
  28. package/build/client/navigation/prefetch.js +35 -0
  29. package/build/client/routing/Router.js +1 -1
  30. package/build/client/routing/hooks.js +6 -2
  31. package/build/client/routing/loader.d.ts +23 -0
  32. package/build/client/routing/loader.js +53 -7
  33. package/build/client/routing/mount.js +4 -3
  34. package/build/compiler/.tsbuildinfo +1 -1
  35. package/build/compiler/config.d.ts +16 -0
  36. package/build/compiler/config.js +7 -0
  37. package/build/compiler/docs.js +16 -16
  38. package/build/compiler/index.d.ts +2 -2
  39. package/build/compiler/index.js +1 -1
  40. package/build/compiler/plugin.js +156 -0
  41. package/build/compiler/prerender.d.ts +1 -0
  42. package/build/compiler/prerender.js +1 -1
  43. package/build/compiler/seo.d.ts +1 -1
  44. package/build/compiler/seo.js +5 -4
  45. package/build/compiler/ssg.js +32 -1
  46. package/build/io/.tsbuildinfo +1 -1
  47. package/build/logger/.tsbuildinfo +1 -1
  48. package/build/shared/.tsbuildinfo +1 -1
  49. package/eslint.config.js +48 -48
  50. package/examples/basic/client/404.tsx +11 -11
  51. package/examples/basic/client/components/.gitkeep +1 -1
  52. package/examples/basic/client/global-error.tsx +13 -13
  53. package/examples/basic/client/layout.tsx +25 -25
  54. package/examples/basic/client/public/images/.gitkeep +1 -1
  55. package/examples/basic/client/public/images/logo.svg +36 -36
  56. package/examples/basic/client/public/robots.txt +2 -2
  57. package/examples/basic/client/routes/docs/[...slug].tsx +12 -12
  58. package/examples/basic/client/routes/features/error/error.tsx +16 -16
  59. package/examples/basic/client/routes/features/template/b.tsx +14 -14
  60. package/examples/basic/client/routes/files/[[...slug]].tsx +21 -21
  61. package/examples/basic/client/routes/gallery/layout.tsx +13 -13
  62. package/examples/basic/client/routes/io.tsx +24 -24
  63. package/examples/basic/client/routes/loader-demo/loading.tsx +13 -13
  64. package/examples/basic/client/routes/search.tsx +61 -61
  65. package/examples/basic/client/toil.tsx +5 -5
  66. package/package.json +155 -148
  67. package/presets/eslint.js +88 -88
  68. package/presets/no-uint8array-tostring.js +200 -200
  69. package/presets/prettier.json +18 -18
  70. package/presets/tsconfig.json +37 -37
  71. package/src/backend/index.ts +160 -160
  72. package/src/cli/proc.ts +50 -50
  73. package/src/cli/updates.ts +69 -69
  74. package/src/cli/validate.ts +31 -31
  75. package/src/client/channel/channel.ts +146 -146
  76. package/src/client/components/Form.tsx +65 -65
  77. package/src/client/components/Script.tsx +113 -113
  78. package/src/client/components/Slot.tsx +21 -21
  79. package/src/client/dev/devtools.tsx +973 -0
  80. package/src/client/dev/error-overlay.tsx +30 -4
  81. package/src/client/head/head.ts +167 -167
  82. package/src/client/head/metadata.ts +112 -112
  83. package/src/client/index.ts +89 -89
  84. package/src/client/navigation/NavLink.tsx +86 -86
  85. package/src/client/navigation/navigation.ts +235 -235
  86. package/src/client/navigation/prefetch.ts +169 -130
  87. package/src/client/navigation/scroll.ts +53 -53
  88. package/src/client/routing/Router.tsx +8 -2
  89. package/src/client/routing/action.ts +122 -122
  90. package/src/client/routing/error-boundary.tsx +43 -43
  91. package/src/client/routing/hooks.ts +21 -6
  92. package/src/client/routing/loader.ts +325 -235
  93. package/src/client/routing/match.ts +47 -47
  94. package/src/client/routing/mount.tsx +54 -52
  95. package/src/client/routing/params-context.ts +10 -10
  96. package/src/client/routing/slot-context.ts +7 -7
  97. package/src/client/search/search.ts +189 -189
  98. package/src/client/search/use-page-search.ts +73 -73
  99. package/src/client/types.ts +73 -73
  100. package/src/compiler/config.ts +219 -182
  101. package/src/compiler/docs.ts +228 -228
  102. package/src/compiler/generate.ts +394 -394
  103. package/src/compiler/index.ts +64 -57
  104. package/src/compiler/pages.ts +70 -70
  105. package/src/compiler/plugin.ts +170 -2
  106. package/src/compiler/prerender.ts +156 -156
  107. package/src/compiler/seo.ts +397 -390
  108. package/src/compiler/ssg.ts +162 -126
  109. package/src/io/BinaryReader.ts +340 -340
  110. package/src/io/BinaryWriter.ts +385 -385
  111. package/src/io/FastMap.ts +127 -127
  112. package/src/io/index.ts +11 -11
  113. package/src/io/lengths.ts +14 -14
  114. package/src/io/types.ts +18 -18
  115. package/src/logger/index.ts +22 -22
  116. package/src/server/index.ts +10 -10
  117. package/src/server/main.ts +13 -13
  118. package/src/server/tsconfig.json +4 -4
  119. package/src/shared/index.ts +10 -10
  120. package/std/client/index.d.ts +15 -15
  121. package/std/client/package.json +3 -3
  122. package/test/assembly/example.spec.ts +7 -7
  123. package/test/channel.test.ts +21 -21
  124. package/test/dom/Link.test.tsx +47 -47
  125. package/test/dom/NavLink.test.tsx +37 -37
  126. package/test/dom/error-overlay.test.tsx +44 -44
  127. package/test/dom/loader.test.tsx +121 -121
  128. package/test/dom/navigation.test.ts +59 -59
  129. package/test/dom/revalidate.test.tsx +38 -38
  130. package/test/dom/route-head.test.tsx +78 -78
  131. package/test/dom/router-loading.test.tsx +44 -44
  132. package/test/dom/scroll.test.ts +56 -56
  133. package/test/dom/use-metadata.test.tsx +58 -58
  134. package/test/io.test.ts +93 -93
  135. package/test/navlink.test.ts +28 -28
  136. package/test/placeholder.test.ts +9 -9
  137. package/test/routes.test.ts +76 -76
  138. package/test/seo.test.ts +175 -164
  139. package/test/slot-layouts.test.ts +69 -69
  140. package/test/ssg.test.ts +36 -36
  141. package/test/update.test.ts +44 -44
  142. package/test/validate.test.ts +42 -42
  143. package/toil-routes.d.ts +7 -0
  144. package/toilconfig.json +30 -30
  145. package/tsconfig.backend.json +13 -13
  146. package/tsconfig.base.json +35 -35
  147. package/tsconfig.cli.json +13 -13
  148. package/tsconfig.client.json +14 -14
  149. package/tsconfig.compiler.json +13 -13
  150. package/tsconfig.io.json +12 -12
  151. package/tsconfig.json +22 -22
  152. package/tsconfig.logger.json +12 -12
  153. package/tsconfig.server.json +10 -10
  154. package/tsconfig.shared.json +12 -12
  155. package/vitest.config.ts +26 -26
  156. package/.idea/codeStyles/Project.xml +0 -54
  157. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  158. package/.idea/inspectionProfiles/Project_Default.xml +0 -6
  159. package/.idea/modules.xml +0 -8
  160. package/.idea/prettier.xml +0 -7
  161. package/.idea/toiljs.iml +0 -8
  162. package/.idea/vcs.xml +0 -6
  163. package/.toil/entry.tsx +0 -9
  164. package/.toil/index.html +0 -12
  165. package/.toil/routes.ts +0 -9
  166. package/build/cli/configure.d.ts +0 -16
  167. package/build/cli/configure.js +0 -272
  168. package/build/cli/create.d.ts +0 -16
  169. package/build/cli/create.js +0 -420
  170. package/build/cli/diagnostics.d.ts +0 -55
  171. package/build/cli/diagnostics.js +0 -333
  172. package/build/cli/doctor.d.ts +0 -6
  173. package/build/cli/doctor.js +0 -249
  174. package/build/cli/features.d.ts +0 -25
  175. package/build/cli/features.js +0 -107
  176. package/build/cli/index.d.ts +0 -2
  177. package/build/cli/proc.d.ts +0 -6
  178. package/build/cli/proc.js +0 -31
  179. package/build/cli/ui.d.ts +0 -9
  180. package/build/cli/ui.js +0 -75
  181. package/build/cli/update.d.ts +0 -7
  182. package/build/cli/update.js +0 -117
  183. package/build/cli/updates.d.ts +0 -10
  184. package/build/cli/updates.js +0 -45
  185. package/build/cli/validate.d.ts +0 -4
  186. package/build/cli/validate.js +0 -19
  187. package/build/client/Link.d.ts +0 -8
  188. package/build/client/Link.js +0 -44
  189. package/build/client/NavLink.d.ts +0 -14
  190. package/build/client/NavLink.js +0 -37
  191. package/build/client/Router.d.ts +0 -7
  192. package/build/client/Router.js +0 -55
  193. package/build/client/channel.d.ts +0 -23
  194. package/build/client/channel.js +0 -94
  195. package/build/client/error-boundary.d.ts +0 -16
  196. package/build/client/error-boundary.js +0 -19
  197. package/build/client/head.d.ts +0 -26
  198. package/build/client/head.js +0 -87
  199. package/build/client/hooks.d.ts +0 -17
  200. package/build/client/hooks.js +0 -48
  201. package/build/client/lazy.d.ts +0 -16
  202. package/build/client/lazy.js +0 -53
  203. package/build/client/match.d.ts +0 -2
  204. package/build/client/match.js +0 -32
  205. package/build/client/mount.d.ts +0 -2
  206. package/build/client/mount.js +0 -13
  207. package/build/client/navigation.d.ts +0 -13
  208. package/build/client/navigation.js +0 -97
  209. package/build/client/params-context.d.ts +0 -2
  210. package/build/client/params-context.js +0 -2
  211. package/build/client/prefetch.d.ts +0 -11
  212. package/build/client/prefetch.js +0 -100
  213. package/build/client/runtime.d.ts +0 -31
  214. package/build/client/runtime.js +0 -112
  215. package/build/client/scroll.d.ts +0 -8
  216. package/build/client/scroll.js +0 -36
  217. package/toil-env.d.ts +0 -16
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }