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
package/src/cli/doctor.ts CHANGED
@@ -23,9 +23,12 @@ import {
23
23
  checkNode,
24
24
  checkPackageManager,
25
25
  checkPeer,
26
+ checkPrettierPlugin,
26
27
  checkRelativeAssets,
28
+ checkRestDispatch,
27
29
  checkRootElement,
28
30
  checkRoutesPresent,
31
+ checkRpcWiring,
29
32
  checkSeoUrl,
30
33
  checkServerEntry,
31
34
  type CheckStatus,
@@ -36,6 +39,10 @@ import {
36
39
  checkWasmBuilt,
37
40
  findRelativeAssets,
38
41
  hasFailures,
42
+ type RestFacts,
43
+ type RpcFacts,
44
+ RPC_TOILSCRIPT_MIN,
45
+ satisfiesMin,
39
46
  type SourceFile,
40
47
  summarize,
41
48
  } from './diagnostics.js';
@@ -53,6 +60,8 @@ export interface DoctorOptions {
53
60
  readonly cwd: string;
54
61
  /** Emit machine-readable JSON instead of the human report. */
55
62
  readonly json?: boolean;
63
+ /** Auto-fix what can be fixed in place (currently the typed-RPC wiring). */
64
+ readonly fix?: boolean;
56
65
  }
57
66
 
58
67
  /** Parses a JSON file into a plain object, or null on any error / non-object. */
@@ -83,6 +92,336 @@ function readFile(file: string): string | null {
83
92
  }
84
93
  }
85
94
 
95
+ function writeFile(file: string, content: string): void {
96
+ fs.writeFileSync(file, content);
97
+ }
98
+
99
+ /**
100
+ * Whether `name` is installed for the project at `root`. Tries Node resolution first (handles
101
+ * hoisting), then falls back to walking `node_modules`, since a strict `exports` map can make
102
+ * `require.resolve('<pkg>')` throw even when the package is present (the toilscript false positive).
103
+ */
104
+ function isPackageInstalled(root: string, name: string): boolean {
105
+ const require = createRequire(path.join(root, 'package.json'));
106
+ for (const id of [`${name}/package.json`, name]) {
107
+ try {
108
+ require.resolve(id);
109
+ return true;
110
+ } catch {
111
+ // try the next resolution strategy
112
+ }
113
+ }
114
+ for (let dir = root; ; ) {
115
+ if (fs.existsSync(path.join(dir, 'node_modules', name, 'package.json'))) return true;
116
+ const parent = path.dirname(dir);
117
+ if (parent === dir) return false;
118
+ dir = parent;
119
+ }
120
+ }
121
+
122
+ /** Narrows a value to a plain (non-array) object, or null. */
123
+ function asRecord(value: unknown): Record<string, unknown> | null {
124
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
125
+ ? (value as Record<string, unknown>)
126
+ : null;
127
+ }
128
+
129
+ const RPC_MODULE_FLAG = '--rpcModule shared/server.ts';
130
+ const RPC_GITIGNORE_LINE = 'shared/server.ts';
131
+ const RPC_GITIGNORE_RE = /(^|\n)\s*shared\/server\.ts\s*(\r?\n|$)/;
132
+
133
+ /**
134
+ * Whether a dependency range is an ordinary registry semver range (so a deliberate
135
+ * `latest`/`*`/`file:`/`github:`/`workspace:`/`npm:` pin is left untouched and treated
136
+ * as already-OK rather than clobbered to a version).
137
+ */
138
+ function looksLikeSemverRange(range: string): boolean {
139
+ return /^\s*[v^~>=<]*\s*\d+\.\d+/.test(range);
140
+ }
141
+
142
+ /** Reads the project and reports which parts of the typed-RPC wiring are present. */
143
+ function gatherRpcFacts(root: string): RpcFacts {
144
+ const pkg = readJsonObject(path.join(root, 'package.json'));
145
+ const scripts = pkg ? stringRecord(pkg.scripts) : {};
146
+ const deps = {
147
+ ...(pkg ? stringRecord(pkg.dependencies) : {}),
148
+ ...(pkg ? stringRecord(pkg.devDependencies) : {}),
149
+ };
150
+ const tsconfig = readJsonObject(path.join(root, 'tsconfig.json'));
151
+ const gitignore = readFile(path.join(root, '.gitignore'));
152
+
153
+ // Either the combined `build` or `build:server` carrying --rpcModule counts (the fixer writes both).
154
+ const buildServerWired = [scripts['build:server'], scripts['build']].some(
155
+ (s) => typeof s === 'string' && s.includes('--rpcModule'),
156
+ );
157
+
158
+ let tsconfigWired = false;
159
+ if (tsconfig) {
160
+ // An absent `include` compiles all files, so `shared` is covered implicitly.
161
+ const include = tsconfig.include;
162
+ const hasShared = !Array.isArray(include) || include.includes('shared');
163
+ const paths = asRecord(asRecord(tsconfig.compilerOptions)?.paths);
164
+ tsconfigWired = hasShared && paths !== null && 'shared/*' in paths;
165
+ }
166
+
167
+ const gitignoreWired = gitignore !== null && RPC_GITIGNORE_RE.test(gitignore);
168
+ const range = deps.toilscript;
169
+ // A non-semver range (file:/github:/latest/*) can't be assessed; don't flag it.
170
+ const toilscriptOk =
171
+ range == null
172
+ ? false
173
+ : looksLikeSemverRange(range)
174
+ ? satisfiesMin(range, RPC_TOILSCRIPT_MIN)
175
+ : true;
176
+
177
+ return { buildServerWired, tsconfigWired, gitignoreWired, toilscriptOk };
178
+ }
179
+
180
+ /** The server `.ts` sources, read from the directories of the toilconfig entries (capped). */
181
+ function serverSources(root: string, toilconfig: Record<string, unknown> | null): string[] {
182
+ const dirs = new Set<string>();
183
+ const entries = Array.isArray(toilconfig?.entries)
184
+ ? (toilconfig.entries as unknown[]).filter((e): e is string => typeof e === 'string')
185
+ : [];
186
+ for (const e of entries) dirs.add(path.dirname(path.resolve(root, e)));
187
+
188
+ const out: string[] = [];
189
+ const cap = 200;
190
+ const maxDepth = 16; // bounds the walk so a symlink cycle in a hostile project can't hang doctor
191
+ const visit = (current: string, depth: number): void => {
192
+ if (out.length >= cap || depth > maxDepth) return;
193
+ let listing: fs.Dirent[];
194
+ try {
195
+ listing = fs.readdirSync(current, { withFileTypes: true });
196
+ } catch {
197
+ return;
198
+ }
199
+ for (const entry of listing) {
200
+ if (out.length >= cap) break;
201
+ const full = path.join(current, entry.name);
202
+ // isDirectory() follows symlinks; the depth cap keeps a symlink cycle bounded.
203
+ if (entry.isDirectory()) {
204
+ if (entry.name !== 'node_modules') visit(full, depth + 1);
205
+ } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {
206
+ const src = readFile(full);
207
+ if (src !== null) out.push(src);
208
+ }
209
+ }
210
+ };
211
+ for (const dir of dirs) visit(dir, 0);
212
+ return out;
213
+ }
214
+
215
+ /** Scans the server sources for `@rest` controllers and whether anything dispatches them. */
216
+ function gatherRestFacts(root: string, toilconfig: Record<string, unknown> | null): RestFacts {
217
+ let hasControllers = false;
218
+ let dispatched = false;
219
+ for (const src of serverSources(root, toilconfig)) {
220
+ if (/@rest\b/.test(src)) hasControllers = true;
221
+ if (/\bRest\s*\.\s*dispatch\s*\(/.test(src) || /\bRestHandler\b/.test(src))
222
+ dispatched = true;
223
+ if (hasControllers && dispatched) break;
224
+ }
225
+ return { hasControllers, dispatched };
226
+ }
227
+
228
+ interface RpcFixResult {
229
+ /** Files written. */
230
+ readonly changed: string[];
231
+ /** Files that need a manual edit (e.g. tsconfig with comments). */
232
+ readonly skipped: string[];
233
+ }
234
+
235
+ /**
236
+ * Applies the typed-RPC wiring in place: appends `--rpcModule` to the toilscript build scripts,
237
+ * adds `shared` (+ the `shared/*` alias) to tsconfig, ignores the generated module, and lifts the
238
+ * toilscript floor. Idempotent; only writes files it actually changes.
239
+ */
240
+ function applyRpcFix(root: string): RpcFixResult {
241
+ const changed: string[] = [];
242
+ const skipped: string[] = [];
243
+
244
+ const pkgPath = path.join(root, 'package.json');
245
+ const pkgRaw = readFile(pkgPath);
246
+ const pkg = pkgRaw !== null ? readJsonObject(pkgPath) : null;
247
+ if (pkg !== null) {
248
+ let touched = false;
249
+ const scripts = asRecord(pkg.scripts) ?? {};
250
+ for (const key of ['build', 'build:server']) {
251
+ const value = scripts[key];
252
+ if (
253
+ typeof value === 'string' &&
254
+ value.includes('toilscript') &&
255
+ !value.includes('--rpcModule')
256
+ ) {
257
+ scripts[key] = `${value} ${RPC_MODULE_FLAG}`;
258
+ pkg.scripts = scripts;
259
+ touched = true;
260
+ }
261
+ }
262
+ let toilscriptDeclared = false;
263
+ for (const field of ['devDependencies', 'dependencies']) {
264
+ const bag = asRecord(pkg[field]);
265
+ const current = bag?.toilscript;
266
+ if (bag && typeof current === 'string') {
267
+ toilscriptDeclared = true;
268
+ // Only lift a real semver range that floors below the minimum; leave file:/latest/* pins alone.
269
+ if (looksLikeSemverRange(current) && !satisfiesMin(current, RPC_TOILSCRIPT_MIN)) {
270
+ bag.toilscript = `^${RPC_TOILSCRIPT_MIN}`;
271
+ touched = true;
272
+ }
273
+ }
274
+ }
275
+ if (!toilscriptDeclared) {
276
+ // A server project needs toilscript; add it so the wiring actually resolves.
277
+ const dd = asRecord(pkg.devDependencies) ?? {};
278
+ dd.toilscript = `^${RPC_TOILSCRIPT_MIN}`;
279
+ pkg.devDependencies = dd;
280
+ touched = true;
281
+ }
282
+ if (touched) {
283
+ writeFile(pkgPath, JSON.stringify(pkg, null, 4) + '\n');
284
+ changed.push('package.json');
285
+ }
286
+ } else if (pkgRaw !== null) {
287
+ skipped.push('package.json (unparseable)');
288
+ }
289
+
290
+ const tsPath = path.join(root, 'tsconfig.json');
291
+ const tsRaw = readFile(tsPath);
292
+ const tsconfig = tsRaw !== null ? readJsonObject(tsPath) : null;
293
+ if (tsconfig !== null) {
294
+ let touched = false;
295
+ // Only touch `include` if it already exists; an absent `include` compiles all files,
296
+ // and synthesizing one would narrow what TypeScript sees.
297
+ if (Array.isArray(tsconfig.include)) {
298
+ const include = [...(tsconfig.include as unknown[])];
299
+ if (!include.includes('shared')) {
300
+ const at = include.indexOf('client');
301
+ include.splice(at >= 0 ? at + 1 : include.length, 0, 'shared');
302
+ tsconfig.include = include;
303
+ touched = true;
304
+ }
305
+ }
306
+ const co = asRecord(tsconfig.compilerOptions) ?? {};
307
+ const paths = asRecord(co.paths) ?? {};
308
+ if (!('shared/*' in paths)) {
309
+ paths['shared/*'] = ['./shared/*'];
310
+ co.paths = paths;
311
+ tsconfig.compilerOptions = co;
312
+ touched = true;
313
+ }
314
+ if (touched) {
315
+ writeFile(tsPath, JSON.stringify(tsconfig, null, 4) + '\n');
316
+ changed.push('tsconfig.json');
317
+ }
318
+ } else if (tsRaw !== null) {
319
+ skipped.push('tsconfig.json (JSON with comments, add "shared" + paths by hand)');
320
+ }
321
+
322
+ const giPath = path.join(root, '.gitignore');
323
+ const giRaw = readFile(giPath);
324
+ if (giRaw === null) {
325
+ writeFile(giPath, `${RPC_GITIGNORE_LINE}\n`);
326
+ changed.push('.gitignore');
327
+ } else if (!RPC_GITIGNORE_RE.test(giRaw)) {
328
+ const sep = giRaw.length === 0 || giRaw.endsWith('\n') ? '' : '\n';
329
+ writeFile(giPath, `${giRaw}${sep}${RPC_GITIGNORE_LINE}\n`);
330
+ changed.push('.gitignore');
331
+ }
332
+
333
+ return { changed, skipped };
334
+ }
335
+
336
+ const PRETTIER_PLUGIN = 'toiljs/prettier-plugin';
337
+ const PRETTIER_MENTION = /toiljs\/prettier(-plugin)?/;
338
+ const PRETTIER_CONFIG_FILES = [
339
+ '.prettierrc',
340
+ '.prettierrc.json',
341
+ '.prettierrc.json5',
342
+ '.prettierrc.yaml',
343
+ '.prettierrc.yml',
344
+ '.prettierrc.js',
345
+ '.prettierrc.cjs',
346
+ '.prettierrc.mjs',
347
+ '.prettierrc.ts',
348
+ 'prettier.config.js',
349
+ 'prettier.config.cjs',
350
+ 'prettier.config.mjs',
351
+ 'prettier.config.ts',
352
+ ];
353
+
354
+ /** Whether any prettier config (file or package.json field) pulls in the toilscript plugin. */
355
+ function prettierPluginPresent(root: string, pkg: Record<string, unknown> | null): boolean {
356
+ if (pkg && pkg.prettier !== undefined && PRETTIER_MENTION.test(JSON.stringify(pkg.prettier))) {
357
+ return true;
358
+ }
359
+ for (const name of PRETTIER_CONFIG_FILES) {
360
+ const raw = readFile(path.join(root, name));
361
+ if (raw !== null && PRETTIER_MENTION.test(raw)) return true;
362
+ }
363
+ return false;
364
+ }
365
+
366
+ /**
367
+ * Adds `toiljs/prettier-plugin` to the project's prettier config so prettier can format the
368
+ * toilscript server. Handles the common cases (package.json `prettier`, a JSON `.prettierrc`,
369
+ * or no config at all); warns for shapes it can't safely edit (a JS config, or a string preset).
370
+ */
371
+ function applyPrettierFix(root: string, pkg: Record<string, unknown> | null): RpcFixResult {
372
+ const changed: string[] = [];
373
+ const skipped: string[] = [];
374
+ if (prettierPluginPresent(root, pkg)) return { changed, skipped };
375
+
376
+ // package.json "prettier" object.
377
+ const pkgPath = path.join(root, 'package.json');
378
+ const pkgConfig = pkg ? asRecord(pkg.prettier) : null;
379
+ if (pkgConfig !== null) {
380
+ const full = readJsonObject(pkgPath);
381
+ const target = full ? asRecord(full.prettier) : null;
382
+ if (full && target) {
383
+ target.plugins = [
384
+ ...(Array.isArray(target.plugins) ? target.plugins : []),
385
+ PRETTIER_PLUGIN,
386
+ ];
387
+ writeFile(pkgPath, JSON.stringify(full, null, 4) + '\n');
388
+ changed.push('package.json');
389
+ return { changed, skipped };
390
+ }
391
+ }
392
+
393
+ // A JSON .prettierrc / .prettierrc.json object.
394
+ for (const name of ['.prettierrc', '.prettierrc.json']) {
395
+ const filePath = path.join(root, name);
396
+ const raw = readFile(filePath);
397
+ if (raw === null) continue;
398
+ const obj = readJsonObject(filePath);
399
+ if (obj === null) {
400
+ skipped.push(`${name} (add "${PRETTIER_PLUGIN}" to plugins by hand)`);
401
+ return { changed, skipped };
402
+ }
403
+ obj.plugins = [...(Array.isArray(obj.plugins) ? obj.plugins : []), PRETTIER_PLUGIN];
404
+ writeFile(filePath, JSON.stringify(obj, null, 4) + '\n');
405
+ changed.push(name);
406
+ return { changed, skipped };
407
+ }
408
+
409
+ // A JS/TS config we can't safely edit.
410
+ const jsConfig = PRETTIER_CONFIG_FILES.find((name) => readFile(path.join(root, name)) !== null);
411
+ if (jsConfig) {
412
+ skipped.push(`${jsConfig} (add "${PRETTIER_PLUGIN}" to its plugins by hand)`);
413
+ return { changed, skipped };
414
+ }
415
+
416
+ // No config at all: create one.
417
+ writeFile(
418
+ path.join(root, '.prettierrc.json'),
419
+ JSON.stringify({ plugins: [PRETTIER_PLUGIN] }, null, 4) + '\n',
420
+ );
421
+ changed.push('.prettierrc.json');
422
+ return { changed, skipped };
423
+ }
424
+
86
425
  /** Reads the framework's own package.json (engines + peerDependencies) for the requirements. */
87
426
  function frameworkMeta(): { node: string; peers: Record<string, string> } {
88
427
  const pkgPath = path.resolve(
@@ -216,12 +555,7 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
216
555
  ? toilconfig.entries.filter((e): e is string => typeof e === 'string')
217
556
  : [];
218
557
  missingEntries = entries.filter((e) => !fs.existsSync(path.join(root, e)));
219
- try {
220
- createRequire(path.join(root, 'package.json')).resolve('toilscript');
221
- toilscriptInstalled = true;
222
- } catch {
223
- toilscriptInstalled = false;
224
- }
558
+ toilscriptInstalled = isPackageInstalled(root, 'toilscript');
225
559
  const targets =
226
560
  typeof toilconfig.targets === 'object' && toilconfig.targets !== null
227
561
  ? (toilconfig.targets as Record<string, unknown>)
@@ -248,6 +582,23 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
248
582
  const peerName = (n: string): Check => checkPeer(n, deps[n] ?? null, meta.peers[n] ?? '*');
249
583
  const peerChecks = Object.keys(meta.peers).map(peerName);
250
584
 
585
+ // Server tooling (RPC wiring + the prettier plugin): optionally fix in place, then re-read.
586
+ const rpcFix = serverPresent && opts.fix ? applyRpcFix(root) : null;
587
+ const prettierFix = serverPresent && opts.fix ? applyPrettierFix(root, projectPkg) : null;
588
+ const rpcFacts = gatherRpcFacts(root);
589
+ const restFacts = gatherRestFacts(root, toilconfig);
590
+ const prettierPresent = prettierPluginPresent(
591
+ root,
592
+ readJsonObject(path.join(root, 'package.json')),
593
+ );
594
+ const serverFix =
595
+ rpcFix || prettierFix
596
+ ? {
597
+ changed: [...(rpcFix?.changed ?? []), ...(prettierFix?.changed ?? [])],
598
+ skipped: [...(rpcFix?.skipped ?? []), ...(prettierFix?.skipped ?? [])],
599
+ }
600
+ : null;
601
+
251
602
  const groups: CheckGroup[] = [
252
603
  {
253
604
  title: 'Environment',
@@ -302,6 +653,9 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
302
653
  checkServerEntry(missingEntries),
303
654
  checkToilscriptInstalled(toilscriptInstalled),
304
655
  checkWasmBuilt(wasmExists),
656
+ checkRpcWiring(rpcFacts),
657
+ checkRestDispatch(restFacts),
658
+ checkPrettierPlugin(prettierPresent),
305
659
  ]
306
660
  : [checkToilconfig(false)],
307
661
  },
@@ -309,10 +663,33 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
309
663
 
310
664
  const summary = summarize(groups);
311
665
  if (opts.json) {
312
- process.stdout.write(JSON.stringify({ groups, summary }, null, 2) + '\n');
666
+ process.stdout.write(JSON.stringify({ groups, summary, fixed: serverFix }, null, 2) + '\n');
313
667
  } else {
314
668
  process.stdout.write('\n' + accent(' Doctor') + dim(` ${root}`) + '\n\n');
315
669
  renderHuman(groups);
670
+ if (serverFix) renderRpcFix(serverFix);
671
+ else if (opts.fix && !serverPresent) {
672
+ process.stdout.write(
673
+ ' ' + dim('--fix: no server (toilconfig.json) found, nothing to wire.') + '\n\n',
674
+ );
675
+ }
316
676
  }
317
677
  if (hasFailures(summary)) process.exitCode = 1;
318
678
  }
679
+
680
+ /** Prints the result of `--fix`, and whether a reinstall is needed (toilscript bump). */
681
+ function renderRpcFix(result: RpcFixResult): void {
682
+ const out: string[] = [];
683
+ if (result.changed.length > 0) {
684
+ out.push(' ' + success('fixed RPC wiring') + dim(` ${result.changed.join(', ')}`));
685
+ if (result.changed.includes('package.json')) {
686
+ out.push(
687
+ ' ' + dim('run your installer (npm/pnpm/yarn) if the toilscript version changed.'),
688
+ );
689
+ }
690
+ } else {
691
+ out.push(' ' + dim('RPC wiring already in place, nothing to fix.'));
692
+ }
693
+ for (const item of result.skipped) out.push(' ' + warn('skipped') + dim(` ${item}`));
694
+ process.stdout.write(out.join('\n') + '\n\n');
695
+ }
package/src/cli/index.ts CHANGED
@@ -16,6 +16,7 @@ import { accent, banner, bold, danger, dim, success, version } from './ui.js';
16
16
  interface Flags {
17
17
  root?: string;
18
18
  port?: number;
19
+ host?: string;
19
20
  name?: string;
20
21
  template?: Template;
21
22
  preprocessor?: Preprocessor;
@@ -27,6 +28,7 @@ interface Flags {
27
28
  pm?: string;
28
29
  yes?: boolean;
29
30
  json?: boolean;
31
+ fix?: boolean;
30
32
  target?: string;
31
33
  }
32
34
 
@@ -43,6 +45,9 @@ function parseArgs(argv: string[]): Flags {
43
45
  if (!Number.isNaN(port)) flags.port = port;
44
46
  break;
45
47
  }
48
+ case '--host':
49
+ flags.host = argv[++i];
50
+ break;
46
51
  case '--template':
47
52
  case '-t': {
48
53
  const t = argv[++i];
@@ -95,6 +100,9 @@ function parseArgs(argv: string[]): Flags {
95
100
  case '--json':
96
101
  flags.json = true;
97
102
  break;
103
+ case '--fix':
104
+ flags.fix = true;
105
+ break;
98
106
  case '--target':
99
107
  flags.target = argv[++i];
100
108
  break;
@@ -130,6 +138,7 @@ function printHelp(): void {
130
138
  cmd('-y, --yes', 'create: accept defaults (non-interactive)'),
131
139
  cmd('--no-install', "create: don't install dependencies"),
132
140
  cmd('--json', 'doctor: machine-readable output'),
141
+ cmd('--fix', 'doctor: auto-fix what it can (typed-RPC wiring)'),
133
142
  cmd('--target <t>', 'update: latest | minor | patch | newest | greatest'),
134
143
  cmd('-v, --version', 'print the toiljs version'),
135
144
  '',
@@ -193,7 +202,7 @@ async function main(): Promise<void> {
193
202
  case 'start': {
194
203
  banner();
195
204
  process.stdout.write(dim(' self-hosting the built app…') + '\n\n');
196
- const server = await start({ root: flags.root, port: flags.port });
205
+ const server = await start({ root: flags.root, port: flags.port, host: flags.host });
197
206
  process.stdout.write(
198
207
  accent(' ➜ ') +
199
208
  bold(`http://localhost:${String(server.port)}`) +
@@ -206,7 +215,7 @@ async function main(): Promise<void> {
206
215
  case 'doctor':
207
216
  // Skip the banner for --json so stdout stays valid JSON.
208
217
  if (!flags.json) banner();
209
- await runDoctor({ root: flags.root, cwd: process.cwd(), json: flags.json });
218
+ await runDoctor({ root: flags.root, cwd: process.cwd(), json: flags.json, fix: flags.fix });
210
219
  break;
211
220
 
212
221
  case 'update':
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
+ }