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
@@ -1,126 +1,171 @@
1
- /**
2
- * Build-time SSG for dynamic routes. After the client bundle is written, this loads each dynamic
3
- * route that exports `generateStaticParams`, enumerates its concrete URLs, runs the route's
4
- * `generateMetadata` per URL, and bakes a `<url>/index.html` (so JS-less crawlers get per-page tags)
5
- * plus a `sitemap.xml` entry. Opt-in: a route without `generateStaticParams` is untouched, and the
6
- * whole pass is skipped when no such route exists or `seo` is unconfigured. Build-only.
7
- *
8
- * Runs from `build()` (not the prerender Vite plugin) so it can reuse `createViteConfig` without the
9
- * `vite.ts` <-> `prerender.ts` import cycle; it spins up a short-lived SSR server to load route source.
10
- */
11
- import fs from 'node:fs';
12
- import path from 'node:path';
13
-
14
- import { createServer } from 'vite';
15
-
16
- import { type ResolvedToilConfig } from './config.js';
17
- import { scanRoutes } from './routes.js';
18
- import { injectSeoHtml, routeSeo, sitemapXml } from './seo.js';
19
- import { createViteConfig } from './vite.js';
20
-
21
- type StaticParams = Record<string, string | string[]>;
22
-
23
- interface RouteModule {
24
- generateStaticParams?: () => StaticParams[] | Promise<StaticParams[]>;
25
- generateMetadata?: (args: {
26
- params: StaticParams;
27
- searchParams: URLSearchParams;
28
- data: unknown;
29
- }) => unknown;
30
- loader?: (args: { params: StaticParams; searchParams: URLSearchParams }) => unknown;
31
- metadata?: Record<string, unknown>;
32
- }
33
-
34
- /** Substitutes `:param` / `*catch-all` segments in a route pattern with concrete param values. */
35
- export function fillPattern(pattern: string, params: StaticParams): string {
36
- return pattern.replace(/[:*]([A-Za-z0-9_]+)/g, (_m, name: string) => {
37
- const value = params[name] as string | string[] | undefined;
38
- if (Array.isArray(value)) return value.join('/');
39
- return value ?? '';
40
- });
41
- }
42
-
43
- /** Coerces an unknown module export to a typed Metadata-ish record, or null. */
44
- function asMetadata(value: unknown): Record<string, unknown> | null {
45
- return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : null;
46
- }
47
-
48
- /**
49
- * Pre-renders every dynamic route that opts in via `generateStaticParams`. Bakes per-URL HTML into
50
- * `outDir` and rewrites `sitemap.xml` with the generated URLs. Returns the list of generated URLs.
51
- */
52
- export async function prerenderStaticParams(cfg: ResolvedToilConfig): Promise<string[]> {
53
- if (!cfg.seo) return [];
54
- const outDir = path.resolve(cfg.root, cfg.outDir);
55
- // Prefer the clean shell stashed by the prerender plugin (no per-route SEO baked in); fall back
56
- // to the built index.html.
57
- const stashed = path.join(cfg.toilDir, 'shell.html');
58
- const shellPath = fs.existsSync(stashed) ? stashed : path.join(outDir, 'index.html');
59
- if (!fs.existsSync(shellPath)) return [];
60
-
61
- const allRoutes = scanRoutes(cfg.routesAbsDir);
62
- const dynamic = allRoutes.filter(
63
- (r) => r.slot === undefined && !r.intercept && /[:*]/.test(r.pattern),
64
- );
65
- if (dynamic.length === 0) return [];
66
-
67
- const shell = fs.readFileSync(shellPath, 'utf8');
68
- const server = await createServer({
69
- ...(await createViteConfig(cfg)),
70
- server: { middlewareMode: true, hmr: false },
71
- appType: 'custom',
72
- logLevel: 'silent',
73
- });
74
-
75
- const generated: string[] = [];
76
- const warn = (msg: string): void => {
77
- process.stderr.write(` toil: SSG ${msg}\n`);
78
- };
79
- try {
80
- for (const route of dynamic) {
81
- let mod: RouteModule;
82
- try {
83
- mod = (await server.ssrLoadModule(route.file)) as RouteModule;
84
- } catch (err) {
85
- warn(`skipped ${route.pattern} (${err instanceof Error ? err.message : String(err)})`);
86
- continue;
87
- }
88
- if (typeof mod.generateStaticParams !== 'function') continue;
89
- const paramSets = await mod.generateStaticParams();
90
- for (const params of paramSets) {
91
- const url = fillPattern(route.pattern, params);
92
- let metadata: Record<string, unknown> | null = null;
93
- try {
94
- if (typeof mod.generateMetadata === 'function') {
95
- const searchParams = new URLSearchParams();
96
- const data =
97
- typeof mod.loader === 'function'
98
- ? await mod.loader({ params, searchParams })
99
- : undefined;
100
- metadata = asMetadata(await mod.generateMetadata({ params, searchParams, data }));
101
- } else if (mod.metadata) {
102
- metadata = asMetadata(mod.metadata);
103
- }
104
- } catch (err) {
105
- warn(`metadata failed for ${url} (${err instanceof Error ? err.message : String(err)})`);
106
- }
107
- const html = injectSeoHtml(shell, routeSeo(cfg.seo, metadata, url));
108
- const target = path.join(outDir, url.replace(/^\//, ''), 'index.html');
109
- fs.mkdirSync(path.dirname(target), { recursive: true });
110
- fs.writeFileSync(target, html);
111
- generated.push(url);
112
- }
113
- }
114
- } finally {
115
- await server.close();
116
- }
117
-
118
- if (generated.length > 0) {
119
- const sitemap = sitemapXml(cfg.seo, allRoutes, generated);
120
- if (sitemap) fs.writeFileSync(path.join(outDir, 'sitemap.xml'), sitemap);
121
- process.stdout.write(
122
- ` ✓ prerendered ${String(generated.length)} dynamic route${generated.length === 1 ? '' : 's'}\n`,
123
- );
124
- }
125
- return generated;
126
- }
1
+ /**
2
+ * Build-time SSG for dynamic routes. After the client bundle is written, this loads each dynamic
3
+ * route that exports `generateStaticParams`, enumerates its concrete URLs, runs the route's
4
+ * `generateMetadata` per URL, and bakes a `<url>/index.html` (so JS-less crawlers get per-page tags)
5
+ * plus a `sitemap.xml` entry. Opt-in: a route without `generateStaticParams` is untouched, and the
6
+ * whole pass is skipped when no such route exists or `seo` is unconfigured. Build-only.
7
+ *
8
+ * Runs from `build()` (not the prerender Vite plugin) so it can reuse `createViteConfig` without the
9
+ * `vite.ts` <-> `prerender.ts` import cycle; it spins up a short-lived SSR server to load route source.
10
+ */
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+
14
+ import { createServer } from 'vite';
15
+
16
+ import { type ResolvedToilConfig } from './config.js';
17
+ import { extractStaticMetadata, loadTypeScript } from './prerender.js';
18
+ import { scanRoutes } from './routes.js';
19
+ import { injectSeoHtml, joinUrl, llmsTxt, routeSeo, sitemapXml, type LlmsPage } from './seo.js';
20
+ import { createViteConfig } from './vite.js';
21
+
22
+ /** Reads a string field off a metadata record, or undefined. */
23
+ function metaString(meta: Record<string, unknown> | null, key: string): string | undefined {
24
+ const value = meta?.[key];
25
+ return typeof value === 'string' ? value : undefined;
26
+ }
27
+
28
+ type StaticParams = Record<string, string | string[]>;
29
+
30
+ interface RouteModule {
31
+ generateStaticParams?: () => StaticParams[] | Promise<StaticParams[]>;
32
+ generateMetadata?: (args: {
33
+ params: StaticParams;
34
+ searchParams: URLSearchParams;
35
+ data: unknown;
36
+ }) => unknown;
37
+ loader?: (args: { params: StaticParams; searchParams: URLSearchParams }) => unknown;
38
+ metadata?: Record<string, unknown>;
39
+ }
40
+
41
+ /** Substitutes `:param` / `*catch-all` segments in a route pattern with concrete param values. */
42
+ export function fillPattern(pattern: string, params: StaticParams): string {
43
+ return pattern.replace(/[:*]([A-Za-z0-9_]+)/g, (_m, name: string) => {
44
+ const value = params[name] as string | string[] | undefined;
45
+ if (Array.isArray(value)) return value.join('/');
46
+ return value ?? '';
47
+ });
48
+ }
49
+
50
+ /** Coerces an unknown module export to a typed Metadata-ish record, or null. */
51
+ function asMetadata(value: unknown): Record<string, unknown> | null {
52
+ return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : null;
53
+ }
54
+
55
+ /**
56
+ * Pre-renders every dynamic route that opts in via `generateStaticParams`. Bakes per-URL HTML into
57
+ * `outDir` and rewrites `sitemap.xml` with the generated URLs. Returns the list of generated URLs.
58
+ */
59
+ export async function prerenderStaticParams(cfg: ResolvedToilConfig): Promise<string[]> {
60
+ if (!cfg.seo) return [];
61
+ const outDir = path.resolve(cfg.root, cfg.outDir);
62
+ // Prefer the clean shell stashed by the prerender plugin (no per-route SEO baked in); fall back
63
+ // to the built index.html.
64
+ const stashed = path.join(cfg.toilDir, 'shell.html');
65
+ const shellPath = fs.existsSync(stashed) ? stashed : path.join(outDir, 'index.html');
66
+ if (!fs.existsSync(shellPath)) return [];
67
+
68
+ const allRoutes = scanRoutes(cfg.routesAbsDir);
69
+ const dynamic = allRoutes.filter(
70
+ (r) => r.slot === undefined && !r.intercept && /[:*]/.test(r.pattern),
71
+ );
72
+ if (dynamic.length === 0) return [];
73
+
74
+ const shell = fs.readFileSync(shellPath, 'utf8');
75
+ const server = await createServer({
76
+ ...(await createViteConfig(cfg)),
77
+ server: { middlewareMode: true, hmr: false },
78
+ appType: 'custom',
79
+ logLevel: 'silent',
80
+ });
81
+
82
+ const baseUrl = cfg.seo.url;
83
+ const generated: string[] = [];
84
+ const dynamicPages: LlmsPage[] = [];
85
+ const warn = (msg: string): void => {
86
+ process.stderr.write(` toil: SSG ${msg}\n`);
87
+ };
88
+ try {
89
+ for (const route of dynamic) {
90
+ let mod: RouteModule;
91
+ try {
92
+ mod = (await server.ssrLoadModule(route.file)) as RouteModule;
93
+ } catch (err) {
94
+ warn(`skipped ${route.pattern} (${err instanceof Error ? err.message : String(err)})`);
95
+ continue;
96
+ }
97
+ if (typeof mod.generateStaticParams !== 'function') continue;
98
+ const paramSets = await mod.generateStaticParams();
99
+ for (const params of paramSets) {
100
+ const url = fillPattern(route.pattern, params);
101
+ // Containment guard: a param value with `..`/separators could make `url` resolve the
102
+ // output path outside `outDir`. Resolve the target up front and skip anything that
103
+ // escapes, so a (possibly externally-derived) param can't clobber files elsewhere.
104
+ const target = path.join(outDir, url.replace(/^\//, ''), 'index.html');
105
+ const outRoot = path.resolve(outDir);
106
+ const absTarget = path.resolve(target);
107
+ if (absTarget !== outRoot && !absTarget.startsWith(outRoot + path.sep)) {
108
+ warn(`skipped ${route.pattern}: params escape outDir (${url})`);
109
+ continue;
110
+ }
111
+ let metadata: Record<string, unknown> | null = null;
112
+ try {
113
+ if (typeof mod.generateMetadata === 'function') {
114
+ const searchParams = new URLSearchParams();
115
+ const data =
116
+ typeof mod.loader === 'function'
117
+ ? await mod.loader({ params, searchParams })
118
+ : undefined;
119
+ metadata = asMetadata(await mod.generateMetadata({ params, searchParams, data }));
120
+ } else if (mod.metadata) {
121
+ metadata = asMetadata(mod.metadata);
122
+ }
123
+ } catch (err) {
124
+ warn(`metadata failed for ${url} (${err instanceof Error ? err.message : String(err)})`);
125
+ }
126
+ const html = injectSeoHtml(shell, routeSeo(cfg.seo, metadata, url));
127
+ fs.mkdirSync(path.dirname(target), { recursive: true });
128
+ fs.writeFileSync(target, html);
129
+ generated.push(url);
130
+ dynamicPages.push({
131
+ title: metaString(metadata, 'title') ?? url,
132
+ url: baseUrl !== undefined ? joinUrl(baseUrl, url) : url,
133
+ description: metaString(metadata, 'description'),
134
+ });
135
+ }
136
+ }
137
+ } finally {
138
+ await server.close();
139
+ }
140
+
141
+ if (generated.length > 0) {
142
+ const sitemap = sitemapXml(cfg.seo, allRoutes, generated);
143
+ if (sitemap) fs.writeFileSync(path.join(outDir, 'sitemap.xml'), sitemap);
144
+ process.stdout.write(
145
+ ` ✓ prerendered ${String(generated.length)} dynamic route${generated.length === 1 ? '' : 's'}\n`,
146
+ );
147
+ }
148
+
149
+ // Rewrite llms.txt with the full page index: every static route's resolved title/description plus
150
+ // the enumerated dynamic pages, so AI crawlers get the whole site, not just the static paths.
151
+ if (baseUrl !== undefined) {
152
+ const ts = await loadTypeScript(cfg.root);
153
+ const staticPages: LlmsPage[] = allRoutes
154
+ .filter((r) => r.slot === undefined && !r.intercept && !/[:*]/.test(r.pattern))
155
+ .map((r): LlmsPage => {
156
+ const meta = ts ? extractStaticMetadata(ts, r.file) : null;
157
+ return {
158
+ title: metaString(meta, 'title') ?? (r.pattern === '/' ? 'Home' : r.pattern),
159
+ url: joinUrl(baseUrl, r.pattern),
160
+ description: metaString(meta, 'description'),
161
+ };
162
+ });
163
+ const pages = [...staticPages, ...dynamicPages];
164
+ if (pages.length > 0) {
165
+ const llms = llmsTxt(cfg.seo, allRoutes, pages);
166
+ if (llms) fs.writeFileSync(path.join(outDir, 'llms.txt'), llms);
167
+ }
168
+ }
169
+
170
+ return generated;
171
+ }
@@ -1,3 +1,4 @@
1
+ import fs from 'node:fs';
1
2
  import { createRequire } from 'node:module';
2
3
  import path from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
@@ -41,6 +42,36 @@ const IMAGE_EXT = /^(png|jpe?g|svg|gif|tiff|bmp|ico|webp|avif)$/i;
41
42
  /** Font extensions routed to `fonts/`. */
42
43
  const FONT_EXT = /^(woff|woff2|eot|ttf|otf)$/i;
43
44
 
45
+ /**
46
+ * Resolves bare `shared/*` imports to the project's `shared/` folder (replacing a plain alias so it
47
+ * can run early), and fails `shared/server` with an actionable message when the module has not been
48
+ * generated yet (it comes from the server build, which must run first), instead of Vite's opaque
49
+ * `UNLOADABLE_DEPENDENCY`.
50
+ */
51
+ function sharedResolverPlugin(cfg: ResolvedToilConfig): PluginOption {
52
+ const sharedDir = path.join(cfg.root, 'shared');
53
+ return {
54
+ name: 'toiljs:shared-resolver',
55
+ enforce: 'pre',
56
+ resolveId(source: string) {
57
+ if (source !== 'shared' && !source.startsWith('shared/')) return null;
58
+ const rel = source === 'shared' ? 'index' : source.slice('shared/'.length);
59
+ for (const ext of ['', '.ts', '.tsx', '.js', '.jsx', '.mjs']) {
60
+ const candidate = path.join(sharedDir, rel + ext);
61
+ if (fs.existsSync(candidate)) return candidate;
62
+ }
63
+ if (source === 'shared/server') {
64
+ throw new Error(
65
+ 'toiljs: "shared/server" is generated by the server build but is missing. ' +
66
+ 'Run the server build first (it emits shared/server.ts from your @data/@remote code): ' +
67
+ '`npm run build:server` (toilscript --target release --rpcModule shared/server.ts).',
68
+ );
69
+ }
70
+ return null;
71
+ },
72
+ };
73
+ }
74
+
44
75
  /** Routes a built asset to a typed sub-folder (`images/`, `fonts/`, `css/`, else `assets/`). */
45
76
  function assetFileName(name: string): string {
46
77
  const ext = name.split('.').pop() ?? '';
@@ -116,12 +147,15 @@ export async function createViteConfig(cfg: ResolvedToilConfig): Promise<InlineC
116
147
  cfg.fonts ? fontPreloadPlugin(cfg) : undefined,
117
148
  nodePolyfills({ globals: { Buffer: true, global: true, process: true } }),
118
149
  react(),
150
+ sharedResolverPlugin(cfg),
119
151
  toilPlugin(cfg),
120
152
  ],
121
153
  resolve: {
122
154
  alias: {
123
155
  'toiljs/client': cfg.runtimePath,
124
156
  'toiljs/routes': path.join(cfg.toilDir, 'routes.ts'),
157
+ // `shared/*` is resolved by sharedResolverPlugin (above) so a missing generated
158
+ // shared/server.ts gives an actionable error instead of an opaque load failure.
125
159
  ...polyfillShimAliases,
126
160
  },
127
161
  dedupe: ['react', 'react-dom'],
package/src/io/FastMap.ts CHANGED
@@ -1,127 +1,151 @@
1
- export type PropertyExtendedKey = PropertyKey | bigint;
2
-
3
- /**
4
- * Like Record, but supports bigint keys (which JS auto-converts to strings).
5
- * Reflects actual JavaScript behavior where obj[123n] becomes obj["123"].
6
- */
7
- export type FastRecord<V> = {
8
- [key: string]: V;
9
- };
10
-
11
- export type IndexKey = string | number;
12
-
13
- export class FastMap<K extends PropertyExtendedKey, V> implements Disposable {
14
- protected _keys: K[] = [];
15
- protected _values: FastRecord<V> = {};
16
-
17
- constructor(iterable?: ReadonlyArray<readonly [K, V]> | null | FastMap<K, V>) {
18
- if (iterable instanceof FastMap) {
19
- this.setAll(iterable);
20
- } else {
21
- if (iterable) {
22
- for (const [key, value] of iterable) {
23
- this.set(key, value);
24
- }
25
- }
26
- }
27
- }
28
-
29
- public get size(): number {
30
- return this._keys.length;
31
- }
32
-
33
- public setAll(map: FastMap<K, V>): void {
34
- this._keys = [...map._keys];
35
- this._values = { ...map._values };
36
- }
37
-
38
- public addAll(map: FastMap<K, V>): void {
39
- for (const [key, value] of map.entries()) {
40
- this.set(key, value);
41
- }
42
- }
43
-
44
- public *keys(): IterableIterator<K> {
45
- yield* this._keys;
46
- }
47
-
48
- public *values(): IterableIterator<V> {
49
- for (const key of this._keys) {
50
- yield this._values[key as IndexKey] as V;
51
- }
52
- }
53
-
54
- public *entries(): IterableIterator<[K, V]> {
55
- for (const key of this._keys) {
56
- yield [key, this._values[key as IndexKey] as V];
57
- }
58
- }
59
-
60
- public set(key: K, value: V): this {
61
- if (!this.has(key)) {
62
- this._keys.push(key);
63
- }
64
-
65
- this._values[key as IndexKey] = value;
66
-
67
- return this;
68
- }
69
-
70
- public indexOf(key: K): number {
71
- if (!this.has(key)) {
72
- return -1;
73
- }
74
-
75
- for (let i = 0; i < this._keys.length; i++) {
76
- if (this._keys[i] === key) {
77
- return i;
78
- }
79
- }
80
-
81
- throw new Error('Key not found, this should not happen.');
82
- }
83
-
84
- public get(key: K): V | undefined {
85
- return this._values[key as IndexKey];
86
- }
87
-
88
- public has(key: K): boolean {
89
- return Object.prototype.hasOwnProperty.call(this._values, key as IndexKey);
90
- }
91
-
92
- public delete(key: K): boolean {
93
- if (!this.has(key)) {
94
- return false;
95
- }
96
-
97
- const index = this.indexOf(key);
98
- this._keys.splice(index, 1);
99
-
100
- delete this._values[key as IndexKey];
101
- return true;
102
- }
103
-
104
- public clear(): void {
105
- this._keys = [];
106
- this._values = {};
107
- }
108
-
109
- public [Symbol.dispose](): void {
110
- this.clear();
111
- }
112
-
113
- public forEach(
114
- callback: (value: V, key: K, map: FastMap<K, V>) => void,
115
- thisArg?: unknown,
116
- ): void {
117
- for (const key of this._keys) {
118
- callback.call(thisArg, this._values[key as IndexKey] as V, key, this);
119
- }
120
- }
121
-
122
- *[Symbol.iterator](): IterableIterator<[K, V]> {
123
- for (const key of this._keys) {
124
- yield [key, this._values[key as IndexKey] as V];
125
- }
126
- }
127
- }
1
+ /** A key usable with {@link FastMap}: any `PropertyKey` plus `bigint`. */
2
+ export type PropertyExtendedKey = PropertyKey | bigint;
3
+
4
+ /**
5
+ * Like Record, but supports bigint keys (which JS auto-converts to strings).
6
+ * Reflects actual JavaScript behavior where obj[123n] becomes obj["123"].
7
+ */
8
+ export type FastRecord<V> = {
9
+ [key: string]: V;
10
+ };
11
+
12
+ /** The string/number form a key takes once used to index the backing object. */
13
+ export type IndexKey = string | number;
14
+
15
+ /**
16
+ * An insertion-ordered map backed by a key array plus a plain object, supporting
17
+ * `bigint` keys (coerced to their string form, like native property access). Exposed
18
+ * to the client as a global (no import). Implements `Disposable`, so a `using` binding
19
+ * clears it on scope exit.
20
+ */
21
+ export class FastMap<K extends PropertyExtendedKey, V> implements Disposable {
22
+ protected _keys: K[] = [];
23
+ protected _values: FastRecord<V> = {};
24
+
25
+ /** @param iterable - initial entries, or another FastMap to copy. */
26
+ constructor(iterable?: ReadonlyArray<readonly [K, V]> | null | FastMap<K, V>) {
27
+ if (iterable instanceof FastMap) {
28
+ this.setAll(iterable);
29
+ } else {
30
+ if (iterable) {
31
+ for (const [key, value] of iterable) {
32
+ this.set(key, value);
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ /** Number of entries. */
39
+ public get size(): number {
40
+ return this._keys.length;
41
+ }
42
+
43
+ /** Replaces all entries with a copy of `map`'s entries. */
44
+ public setAll(map: FastMap<K, V>): void {
45
+ this._keys = [...map._keys];
46
+ this._values = { ...map._values };
47
+ }
48
+
49
+ /** Merges `map`'s entries into this one (existing keys are overwritten). */
50
+ public addAll(map: FastMap<K, V>): void {
51
+ for (const [key, value] of map.entries()) {
52
+ this.set(key, value);
53
+ }
54
+ }
55
+
56
+ /** Iterates the keys in insertion order. */
57
+ public *keys(): IterableIterator<K> {
58
+ yield* this._keys;
59
+ }
60
+
61
+ /** Iterates the values in key-insertion order. */
62
+ public *values(): IterableIterator<V> {
63
+ for (const key of this._keys) {
64
+ yield this._values[key as IndexKey] as V;
65
+ }
66
+ }
67
+
68
+ /** Iterates `[key, value]` pairs in insertion order. */
69
+ public *entries(): IterableIterator<[K, V]> {
70
+ for (const key of this._keys) {
71
+ yield [key, this._values[key as IndexKey] as V];
72
+ }
73
+ }
74
+
75
+ /** Sets `key` to `value` (appending the key if new), and returns `this` for chaining. */
76
+ public set(key: K, value: V): this {
77
+ if (!this.has(key)) {
78
+ this._keys.push(key);
79
+ }
80
+
81
+ this._values[key as IndexKey] = value;
82
+
83
+ return this;
84
+ }
85
+
86
+ /** Returns the insertion index of `key`, or -1 if absent. */
87
+ public indexOf(key: K): number {
88
+ if (!this.has(key)) {
89
+ return -1;
90
+ }
91
+
92
+ for (let i = 0; i < this._keys.length; i++) {
93
+ if (this._keys[i] === key) {
94
+ return i;
95
+ }
96
+ }
97
+
98
+ throw new Error('Key not found, this should not happen.');
99
+ }
100
+
101
+ /** Returns the value for `key`, or `undefined` if absent. */
102
+ public get(key: K): V | undefined {
103
+ return this._values[key as IndexKey];
104
+ }
105
+
106
+ /** Whether `key` is present. */
107
+ public has(key: K): boolean {
108
+ return Object.prototype.hasOwnProperty.call(this._values, key as IndexKey);
109
+ }
110
+
111
+ /** Removes `key`; returns true if it was present. */
112
+ public delete(key: K): boolean {
113
+ if (!this.has(key)) {
114
+ return false;
115
+ }
116
+
117
+ const index = this.indexOf(key);
118
+ this._keys.splice(index, 1);
119
+
120
+ delete this._values[key as IndexKey];
121
+ return true;
122
+ }
123
+
124
+ /** Removes all entries. */
125
+ public clear(): void {
126
+ this._keys = [];
127
+ this._values = {};
128
+ }
129
+
130
+ /** `Disposable` hook: clears the map (so `using m = new FastMap()` frees it on scope exit). */
131
+ public [Symbol.dispose](): void {
132
+ this.clear();
133
+ }
134
+
135
+ /** Calls `callback(value, key, map)` for each entry in insertion order. */
136
+ public forEach(
137
+ callback: (value: V, key: K, map: FastMap<K, V>) => void,
138
+ thisArg?: unknown,
139
+ ): void {
140
+ for (const key of this._keys) {
141
+ callback.call(thisArg, this._values[key as IndexKey] as V, key, this);
142
+ }
143
+ }
144
+
145
+ /** Default iterator: `[key, value]` pairs in insertion order. */
146
+ *[Symbol.iterator](): IterableIterator<[K, V]> {
147
+ for (const key of this._keys) {
148
+ yield [key, this._values[key as IndexKey] as V];
149
+ }
150
+ }
151
+ }