toiljs 0.0.115 → 0.0.117

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 (326) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +16 -7
  3. package/TYPESCRIPT_LAW.md +1946 -2073
  4. package/build/backend/.tsbuildinfo +1 -1
  5. package/build/cli/.tsbuildinfo +1 -1
  6. package/build/cli/index.js +2375 -258
  7. package/build/client/.tsbuildinfo +1 -1
  8. package/build/client/dev/devtools.js +1 -1
  9. package/build/client/dev/error-overlay.js +1 -1
  10. package/build/client/routing/mount.js +1 -1
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/auth-emails.js +7 -3
  13. package/build/compiler/emails.js +1 -1
  14. package/build/compiler/index.d.ts +1 -0
  15. package/build/compiler/index.js +25 -4
  16. package/build/compiler/pages.js +2 -3
  17. package/build/compiler/prerender.d.ts +3 -8
  18. package/build/compiler/prerender.js +88 -127
  19. package/build/compiler/ssg.js +3 -4
  20. package/build/compiler/template-build.js +3 -4
  21. package/build/compiler/toil-docs.generated.js +6 -5
  22. package/build/compiler/vite.js +3 -6
  23. package/build/devserver/.tsbuildinfo +1 -1
  24. package/build/devserver/db/database.js +7 -3
  25. package/build/devserver/db/types.js +1 -1
  26. package/build/devserver/production.js +4 -3
  27. package/build/devserver/runtime/crypto.d.ts +1 -1
  28. package/build/io/.tsbuildinfo +1 -1
  29. package/build/logger/.tsbuildinfo +1 -1
  30. package/build/shared/.tsbuildinfo +1 -1
  31. package/examples/basic/client/routes/analytics.tsx +7 -5
  32. package/examples/basic/server/tsconfig.json +1 -3
  33. package/package.json +45 -44
  34. package/presets/no-uint8array-tostring.js +120 -153
  35. package/presets/oxlint.d.ts +5 -0
  36. package/presets/oxlint.js +224 -0
  37. package/server/runtime/tsconfig.json +1 -3
  38. package/tsconfig.json +2 -11
  39. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  40. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
  41. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  42. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  43. package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
  44. package/.github/changelog-config.json +0 -45
  45. package/.github/dependabot.yml +0 -27
  46. package/.github/workflows/ci.yml +0 -222
  47. package/.vscode/settings.json +0 -10
  48. package/docs/README.md +0 -107
  49. package/docs/auth/README.md +0 -104
  50. package/docs/auth/configuration.md +0 -105
  51. package/docs/auth/emails.md +0 -92
  52. package/docs/auth/extending.md +0 -202
  53. package/docs/auth/how-it-works.md +0 -138
  54. package/docs/auth/usage.md +0 -188
  55. package/docs/backend/README.md +0 -143
  56. package/docs/backend/data.md +0 -351
  57. package/docs/backend/rest.md +0 -402
  58. package/docs/backend/rpc.md +0 -226
  59. package/docs/background/README.md +0 -114
  60. package/docs/background/daemons.md +0 -276
  61. package/docs/background/derive.md +0 -179
  62. package/docs/cli/README.md +0 -379
  63. package/docs/concepts/ai-guide.md +0 -132
  64. package/docs/concepts/config.md +0 -416
  65. package/docs/concepts/decorators.md +0 -127
  66. package/docs/concepts/security.md +0 -108
  67. package/docs/concepts/tiers.md +0 -166
  68. package/docs/concepts/types.md +0 -216
  69. package/docs/database/README.md +0 -143
  70. package/docs/database/capacity.md +0 -350
  71. package/docs/database/counters.md +0 -174
  72. package/docs/database/documents.md +0 -277
  73. package/docs/database/events.md +0 -307
  74. package/docs/database/membership.md +0 -246
  75. package/docs/database/setup.md +0 -155
  76. package/docs/database/unique.md +0 -216
  77. package/docs/database/views.md +0 -246
  78. package/docs/frontend/README.md +0 -104
  79. package/docs/frontend/components.md +0 -286
  80. package/docs/frontend/data-fetching.md +0 -260
  81. package/docs/frontend/images.md +0 -148
  82. package/docs/frontend/metadata.md +0 -344
  83. package/docs/frontend/navigation.md +0 -296
  84. package/docs/frontend/rendering.md +0 -236
  85. package/docs/frontend/routing.md +0 -346
  86. package/docs/frontend/scripts.md +0 -118
  87. package/docs/frontend/search.md +0 -191
  88. package/docs/frontend/styling.md +0 -147
  89. package/docs/frontend/toil-global.md +0 -216
  90. package/docs/getting-started/README.md +0 -81
  91. package/docs/getting-started/create-project.md +0 -131
  92. package/docs/getting-started/deploy.md +0 -101
  93. package/docs/getting-started/first-app.md +0 -215
  94. package/docs/getting-started/installation.md +0 -124
  95. package/docs/getting-started/migrating.md +0 -125
  96. package/docs/getting-started/project-structure.md +0 -163
  97. package/docs/introduction/README.md +0 -66
  98. package/docs/introduction/design-principles.md +0 -81
  99. package/docs/introduction/distributed.md +0 -91
  100. package/docs/introduction/how-it-works.md +0 -121
  101. package/docs/introduction/hyperscale.md +0 -70
  102. package/docs/introduction/modern-stack.md +0 -85
  103. package/docs/introduction/vs-other-frameworks.md +0 -64
  104. package/docs/introduction/why-toil.md +0 -99
  105. package/docs/llms.txt +0 -95
  106. package/docs/realtime/README.md +0 -102
  107. package/docs/realtime/channels.md +0 -211
  108. package/docs/realtime/streams.md +0 -369
  109. package/docs/services/README.md +0 -60
  110. package/docs/services/analytics.md +0 -279
  111. package/docs/services/caching.md +0 -175
  112. package/docs/services/cookies.md +0 -235
  113. package/docs/services/crypto.md +0 -209
  114. package/docs/services/email.md +0 -289
  115. package/docs/services/environment.md +0 -141
  116. package/docs/services/ratelimit.md +0 -174
  117. package/docs/services/time.md +0 -85
  118. package/eslint.config.js +0 -57
  119. package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
  120. package/presets/eslint.js +0 -88
  121. package/src/backend/index.ts +0 -204
  122. package/src/cli/configure.ts +0 -363
  123. package/src/cli/create.ts +0 -924
  124. package/src/cli/db.ts +0 -158
  125. package/src/cli/diagnostics.ts +0 -665
  126. package/src/cli/doctor.ts +0 -982
  127. package/src/cli/features.ts +0 -170
  128. package/src/cli/index.ts +0 -326
  129. package/src/cli/notify.ts +0 -120
  130. package/src/cli/proc.ts +0 -56
  131. package/src/cli/ui.ts +0 -169
  132. package/src/cli/update.ts +0 -248
  133. package/src/cli/updates.ts +0 -92
  134. package/src/cli/validate.ts +0 -31
  135. package/src/cli/version-check.ts +0 -125
  136. package/src/client/auth.ts +0 -869
  137. package/src/client/channel/channel.ts +0 -146
  138. package/src/client/components/Form.tsx +0 -65
  139. package/src/client/components/Image.tsx +0 -160
  140. package/src/client/components/Script.tsx +0 -113
  141. package/src/client/components/Slot.tsx +0 -21
  142. package/src/client/dev/devtools.tsx +0 -1089
  143. package/src/client/dev/error-overlay.tsx +0 -225
  144. package/src/client/errors.ts +0 -11
  145. package/src/client/head/head.ts +0 -182
  146. package/src/client/head/metadata.ts +0 -110
  147. package/src/client/index.ts +0 -130
  148. package/src/client/navigation/Link.tsx +0 -108
  149. package/src/client/navigation/NavLink.tsx +0 -86
  150. package/src/client/navigation/navigation.ts +0 -235
  151. package/src/client/navigation/prefetch.ts +0 -169
  152. package/src/client/navigation/scroll.ts +0 -53
  153. package/src/client/routing/Router.tsx +0 -216
  154. package/src/client/routing/action.ts +0 -122
  155. package/src/client/routing/error-boundary.tsx +0 -43
  156. package/src/client/routing/hooks.ts +0 -140
  157. package/src/client/routing/lazy.ts +0 -77
  158. package/src/client/routing/loader.ts +0 -381
  159. package/src/client/routing/match.ts +0 -47
  160. package/src/client/routing/mount.tsx +0 -90
  161. package/src/client/routing/params-context.ts +0 -10
  162. package/src/client/routing/slot-context.ts +0 -7
  163. package/src/client/rpc.ts +0 -97
  164. package/src/client/search/search.ts +0 -189
  165. package/src/client/search/use-page-search.ts +0 -73
  166. package/src/client/ssr/markers.tsx +0 -172
  167. package/src/client/stream/client.ts +0 -237
  168. package/src/client/types.ts +0 -73
  169. package/src/compiler/auth-emails.ts +0 -312
  170. package/src/compiler/config.ts +0 -369
  171. package/src/compiler/docs.ts +0 -127
  172. package/src/compiler/email-preview.ts +0 -314
  173. package/src/compiler/emails.ts +0 -404
  174. package/src/compiler/fonts.ts +0 -89
  175. package/src/compiler/generate.ts +0 -471
  176. package/src/compiler/image-blur.ts +0 -42
  177. package/src/compiler/image-report.ts +0 -88
  178. package/src/compiler/index.ts +0 -1362
  179. package/src/compiler/pages.ts +0 -49
  180. package/src/compiler/plugin.ts +0 -391
  181. package/src/compiler/prerender.ts +0 -263
  182. package/src/compiler/routes.ts +0 -170
  183. package/src/compiler/seo.ts +0 -415
  184. package/src/compiler/sri.ts +0 -144
  185. package/src/compiler/ssg.ts +0 -177
  186. package/src/compiler/ssr-codegen.ts +0 -85
  187. package/src/compiler/template-build.ts +0 -676
  188. package/src/compiler/template.ts +0 -262
  189. package/src/compiler/toil-docs.generated.ts +0 -77
  190. package/src/compiler/vite.ts +0 -245
  191. package/src/devserver/analytics/index.ts +0 -317
  192. package/src/devserver/config/dotenv.ts +0 -102
  193. package/src/devserver/config/env.ts +0 -30
  194. package/src/devserver/config/ratelimit.ts +0 -123
  195. package/src/devserver/daemon/catalog.ts +0 -120
  196. package/src/devserver/daemon/cron.ts +0 -87
  197. package/src/devserver/daemon/host.ts +0 -134
  198. package/src/devserver/daemon/index.ts +0 -369
  199. package/src/devserver/daemon/runtime.ts +0 -48
  200. package/src/devserver/db/catalog.ts +0 -109
  201. package/src/devserver/db/database.ts +0 -1834
  202. package/src/devserver/db/derives.ts +0 -121
  203. package/src/devserver/db/index.ts +0 -20
  204. package/src/devserver/db/routeKinds.ts +0 -191
  205. package/src/devserver/db/types.ts +0 -168
  206. package/src/devserver/email/caps.ts +0 -0
  207. package/src/devserver/email/config.ts +0 -141
  208. package/src/devserver/email/console.ts +0 -348
  209. package/src/devserver/email/index.ts +0 -155
  210. package/src/devserver/email/providers.ts +0 -131
  211. package/src/devserver/email/status.ts +0 -23
  212. package/src/devserver/email/validate.ts +0 -37
  213. package/src/devserver/email/wire.ts +0 -55
  214. package/src/devserver/http/cache.ts +0 -0
  215. package/src/devserver/http/envelope.ts +0 -157
  216. package/src/devserver/http/proxy.ts +0 -172
  217. package/src/devserver/http/runtime.ts +0 -387
  218. package/src/devserver/index.ts +0 -43
  219. package/src/devserver/production-ipc.ts +0 -63
  220. package/src/devserver/production-worker.ts +0 -83
  221. package/src/devserver/production.ts +0 -718
  222. package/src/devserver/runtime/crypto.ts +0 -587
  223. package/src/devserver/runtime/host.ts +0 -427
  224. package/src/devserver/runtime/module.ts +0 -442
  225. package/src/devserver/server.ts +0 -251
  226. package/src/devserver/ssr.ts +0 -208
  227. package/src/devserver/stream/catalog.ts +0 -106
  228. package/src/devserver/stream/host.ts +0 -42
  229. package/src/devserver/stream/index.ts +0 -301
  230. package/src/devserver/stream/manager.ts +0 -167
  231. package/src/devserver/stream/router.ts +0 -101
  232. package/src/devserver/stream/wire.ts +0 -58
  233. package/src/devserver/stream/ws.ts +0 -78
  234. package/src/devserver/wasm/sections.ts +0 -59
  235. package/src/devserver/wasm/surface.ts +0 -115
  236. package/src/io/FastMap.ts +0 -151
  237. package/src/io/FastSet.ts +0 -113
  238. package/src/io/codec.ts +0 -362
  239. package/src/io/index.ts +0 -10
  240. package/src/io/lengths.ts +0 -14
  241. package/src/io/types.ts +0 -19
  242. package/src/logger/index.ts +0 -22
  243. package/src/shared/index.ts +0 -46
  244. package/test/analytics-dev.test.ts +0 -113
  245. package/test/assembly/aspect-shim.ts +0 -33
  246. package/test/assembly/cookie.spec.ts +0 -303
  247. package/test/assembly/example.spec.ts +0 -22
  248. package/test/assembly/ssr.spec.ts +0 -95
  249. package/test/assembly.test.ts +0 -59
  250. package/test/auth-emails.test.ts +0 -118
  251. package/test/built-ssr.test.ts +0 -148
  252. package/test/channel.test.ts +0 -21
  253. package/test/config-host-port.test.ts +0 -59
  254. package/test/configure.test.ts +0 -94
  255. package/test/daemon-build.test.ts +0 -278
  256. package/test/daemon-catalog.test.ts +0 -349
  257. package/test/daemon-emulation.test.ts +0 -256
  258. package/test/db.test.ts +0 -0
  259. package/test/devserver-database.test.ts +0 -1102
  260. package/test/devserver-email.test.ts +0 -306
  261. package/test/devserver-pqauth.test.ts +0 -93
  262. package/test/devserver-secrets.test.ts +0 -63
  263. package/test/devserver.test.ts +0 -259
  264. package/test/doctor.test.ts +0 -298
  265. package/test/dom/Image.test.tsx +0 -144
  266. package/test/dom/Link.test.tsx +0 -67
  267. package/test/dom/NavLink.test.tsx +0 -37
  268. package/test/dom/Script.test.tsx +0 -48
  269. package/test/dom/action.test.tsx +0 -146
  270. package/test/dom/error-overlay.test.tsx +0 -44
  271. package/test/dom/loader.test.tsx +0 -121
  272. package/test/dom/navigation.test.ts +0 -59
  273. package/test/dom/revalidate.test.tsx +0 -38
  274. package/test/dom/route-head.test.tsx +0 -56
  275. package/test/dom/router-loading.test.tsx +0 -44
  276. package/test/dom/scroll.test.ts +0 -56
  277. package/test/dom/slot.test.tsx +0 -131
  278. package/test/dom/use-metadata.test.tsx +0 -58
  279. package/test/dom/view-transitions.test.tsx +0 -53
  280. package/test/dynamic-routing.test.ts +0 -132
  281. package/test/email-console.test.ts +0 -86
  282. package/test/email-preview.test.ts +0 -73
  283. package/test/emails.test.ts +0 -58
  284. package/test/errors.test.ts +0 -21
  285. package/test/example-guestbook.test.ts +0 -123
  286. package/test/features.test.ts +0 -149
  287. package/test/fixtures/bignum-wire/spec.ts +0 -25
  288. package/test/fixtures/daemon-app.ts +0 -65
  289. package/test/fixtures/stream-echo.ts +0 -26
  290. package/test/fixtures/stream-gate.ts +0 -28
  291. package/test/fixtures/stream-trap.ts +0 -18
  292. package/test/fixtures/stream-typed.ts +0 -41
  293. package/test/fonts.test.ts +0 -28
  294. package/test/global-setup.ts +0 -17
  295. package/test/head.test.ts +0 -42
  296. package/test/image-blur.test.ts +0 -50
  297. package/test/io.test.ts +0 -117
  298. package/test/metadata.test.ts +0 -40
  299. package/test/navlink.test.ts +0 -28
  300. package/test/pages.test.ts +0 -105
  301. package/test/placeholder.test.ts +0 -9
  302. package/test/pqauth-e2e.test.ts +0 -216
  303. package/test/pqauth-email.test.ts +0 -801
  304. package/test/prerender.test.ts +0 -116
  305. package/test/prettier-plugin.test.ts +0 -46
  306. package/test/routes.test.ts +0 -76
  307. package/test/rpc-bignum-wire.test.ts +0 -164
  308. package/test/rpc-dispatch.test.ts +0 -132
  309. package/test/rpc-kinds.test.ts +0 -18
  310. package/test/rpc.test.ts +0 -66
  311. package/test/search.test.ts +0 -114
  312. package/test/seo.test.ts +0 -175
  313. package/test/slot-layouts.test.ts +0 -69
  314. package/test/sri.test.ts +0 -129
  315. package/test/ssg.test.ts +0 -36
  316. package/test/ssr-hydration.test.tsx +0 -122
  317. package/test/ssr-render.test.ts +0 -197
  318. package/test/ssr-template.test.tsx +0 -482
  319. package/test/stream-emulation.test.ts +0 -479
  320. package/test/ui.test.ts +0 -40
  321. package/test/update.test.ts +0 -58
  322. package/test/validate.test.ts +0 -42
  323. package/test/version-check.test.ts +0 -130
  324. package/tests/data-parity/generated-parity.ts +0 -99
  325. package/tests/data-parity/parity.ts +0 -80
  326. package/tests/data-parity/spec.ts +0 -46
@@ -1,676 +0,0 @@
1
- /**
2
- * Build-time orchestration for edge SSR: render each opted-in route to a
3
- * template-with-holes and emit the artifacts the edge + guest consume.
4
- *
5
- * The deterministic core (`extractRouteTemplate`, `injectIntoShell`,
6
- * `writeTemplateArtifacts`) is unit-tested with controlled components; the
7
- * `extractTemplates` driver loads real route + layout modules through a short-
8
- * lived Vite SSR server (the same pattern as `ssg.ts`).
9
- *
10
- * A route opts in with `export const ssr = true`. Its page + layout chain are
11
- * rendered under the loader-data provider with sample data, in sentinel mode;
12
- * the result is spliced into the built shell's `#root`, stripped to a `.tmpl`,
13
- * and written alongside the binary `.slots` manifest and the generated AS
14
- * `Slot` enum + `HASH`. A route (or layout) that throws under static markup
15
- * (e.g. it uses router hooks outside the supported subset) is skipped with a
16
- * warning and falls back to pure client rendering.
17
- *
18
- * The `Slot` enum + `HASH` is also what the SERVER `render` imports, so it must
19
- * exist BEFORE the server compiles. The build therefore runs the render in two
20
- * passes: a slots PRE-PASS (`extractServerSlots`, before the server build) emits
21
- * the server-importable `<server>/_ssr/<name>.slots.ts` so toilscript can compile
22
- * the `render`; the FINAL pass (`extractTemplates`, after the Vite client build)
23
- * rewrites it against the real built shell so the `HASH` is authoritative, and
24
- * reports whether it rotated so the caller can recompile the server once. This is
25
- * what makes a clean build need ZERO hand-maintained slots.
26
- */
27
-
28
- import fs from 'node:fs';
29
- import { createRequire } from 'node:module';
30
- import path from 'node:path';
31
-
32
- import { type ComponentType, type Context, createElement, type ReactNode, Suspense } from 'react';
33
- import { renderToString } from 'react-dom/server';
34
- import { createServer } from 'vite';
35
-
36
- import { type ResolvedToilConfig } from './config.js';
37
- import { findLayout, findSpecialChain } from './generate.js';
38
- import { scanRoutes } from './routes.js';
39
- import { generateSlotsModule } from './ssr-codegen.js';
40
- import {
41
- assignSlotIds,
42
- coherenceHash,
43
- encodeSlots,
44
- type Extracted,
45
- extractFromHtml,
46
- } from './template.js';
47
- import { createViteConfig } from './vite.js';
48
- import { extractStaticMetadata, loadTypeScript } from './prerender.js';
49
- import { escapeAttr, escapeHtml, injectSeoHtml, routeSeo, type SeoConfig } from './seo.js';
50
-
51
- /** Marker element the client `mount` looks for to switch to `hydrateRoot`. */
52
- const SSR_MARKER = '<template id="__toil_ssr"></template>';
53
- const ROOT_DIV = '<div id="root"></div>';
54
-
55
- export interface RouteRenderInput {
56
- /** File-safe route name (the `<name>.tmpl` stem). */
57
- name: string;
58
- Page: ComponentType;
59
- /** Layout chain, OUTERMOST first (root layout, then nested). */
60
- layouts: ComponentType<{ children?: ReactNode }>[];
61
- /** Sample loader data provided via the loader context during the render. */
62
- loaderData: unknown;
63
- /** The client's `LoaderDataContext` (loaded via the SSR module graph so it
64
- * is the same instance the page's `useLoaderData` reads). */
65
- loaderContext: Context<unknown> | null;
66
- /** The markers' build switch (same module instance the page imports). */
67
- setSsrBuild: (on: boolean) => void;
68
- /** The built HTML shell (with hashed script tags) to splice into. */
69
- shell: string;
70
- /** React's `createElement` from the SAME instance the page imports (the Vite
71
- * SSR graph), so element creation and the components' hooks share one React.
72
- * Defaults to the compiler's own React when omitted (unit tests). Mixing two
73
- * React copies leaves the hook dispatcher null ("Cannot read properties of
74
- * null (reading 'useRef')"). */
75
- createElement?: typeof createElement;
76
- /** `renderToString` paired with {@link createElement}'s React. We use it (NOT
77
- * `renderToStaticMarkup`) because hydration needs the `<!-- -->` text-node
78
- * boundary markers it emits, so `hydrateRoot` can align "text + hole" runs. */
79
- renderToString?: typeof renderToString;
80
- /** React's `Suspense` from the SAME instance as {@link createElement}, so the
81
- * Suspense dehydration markers (`<!--$-->`) emitted match the client's. */
82
- Suspense?: typeof Suspense;
83
- /** Site SEO config. When set, this route's resolved SEO (site defaults overlaid with the
84
- * route's static `metadata`) is baked into the template `<head>`, so an SSR route serves the
85
- * same per-page title/description/og (incl og:image) a crawler gets from `<route>/index.html`. */
86
- seo?: SeoConfig | null;
87
- /** The route's static `export const metadata` (extracted at build), overlaid on the site SEO. */
88
- metadata?: Record<string, unknown> | null;
89
- /** The route pattern, used for the canonical / `og:url`. */
90
- pattern?: string;
91
- /** Drains the component-level head (`useHead`/`useTitle`/`<Head>`) collected during this route's
92
- * render, so it's baked into the SSR `<head>` too. From `toiljs/client`'s `__drainSsrHead`. */
93
- drainSsrHead?: () => SsrHead;
94
- }
95
-
96
- export interface TemplateArtifacts {
97
- name: string;
98
- tmpl: Buffer;
99
- /** Binary `.slots` manifest for the Rust host. */
100
- slotsBin: Buffer;
101
- /** Generated AssemblyScript `Slot` enum + `HASH` module source. */
102
- slotsModule: string;
103
- hash: Buffer;
104
- slotCount: number;
105
- }
106
-
107
- /** Build the route element tree, mirroring the client Router so `renderToString`
108
- * emits the SAME Suspense dehydration markers (`<!--$-->`) `hydrateRoot` expects.
109
- * The page sits under the loader-data provider inside a route `Suspense`, and EACH
110
- * layout (outermost first) gets its own `Suspense`, exactly as `renderMatched` +
111
- * `Router` wrap them. Without these markers the client's Suspense boundaries have
112
- * nothing to align to and hydration regenerates the whole tree. (The ErrorBoundary
113
- * / context wrappers the client adds emit no DOM and no markers, so they are
114
- * omitted here.) The `Suspense` component must come from the SAME React as `h`. */
115
- export function assembleRouteElement(
116
- Page: ComponentType,
117
- layouts: ComponentType<{ children?: ReactNode }>[],
118
- loaderData: unknown,
119
- loaderContext: Context<unknown> | null,
120
- h: typeof createElement = createElement,
121
- SuspenseComp: typeof Suspense = Suspense,
122
- ): ReactNode {
123
- let node: ReactNode = h(Page);
124
- if (loaderContext) {
125
- node = h(loaderContext.Provider, { value: loaderData }, node);
126
- }
127
- node = h(SuspenseComp, { fallback: null }, node); // route Suspense (mirrors RoutePage's boundary)
128
- for (let i = layouts.length - 1; i >= 0; i--) {
129
- node = h(SuspenseComp, { fallback: null }, h(layouts[i], null, node));
130
- }
131
- return node;
132
- }
133
-
134
- /** Splice the rendered route HTML into the shell's `#root` and add the SSR
135
- * marker so the client hydrates rather than client-renders. */
136
- export function injectIntoShell(shell: string, routeHtml: string): string {
137
- if (!shell.includes(ROOT_DIV)) {
138
- throw new Error('toil ssr: built shell has no empty <div id="root"></div> to splice into');
139
- }
140
- return shell.replace(ROOT_DIV, `<div id="root">${routeHtml}</div>${SSR_MARKER}`);
141
- }
142
-
143
- /**
144
- * React 19 auto-emits hoistable resource tags into `<head>` on the client (it
145
- * adds a `<link rel="preload">` for an `<img>`, and hoists `<title>` / `<meta>`),
146
- * but the string renderer emits them INLINE in the route fragment. Left in the
147
- * spliced `#root` template they would not appear in the client's hydrated `#root`
148
- * (the client puts them in `<head>`), so `hydrateRoot` reports a mismatch. Strip
149
- * them from the route fragment; the shell already carries the document head, and
150
- * the client re-adds its own resource hints. Only the fragment is stripped. */
151
- function stripHoistedResourceTags(html: string): string {
152
- return html
153
- .replace(/<link\b[^>]*>/gi, '')
154
- .replace(/<meta\b[^>]*>/gi, '')
155
- .replace(/<title\b[^>]*>[\s\S]*?<\/title>/gi, '');
156
- }
157
-
158
- /** The resolved component-level head drained from the client head manager during an SSR-build render
159
- * (structurally `ResolvedHead` from `toiljs/client`). */
160
- interface SsrHead {
161
- title?: string;
162
- meta: { name?: string; property?: string; content: string; [attr: string]: string | undefined }[];
163
- link: { rel: string; href: string; [attr: string]: string | undefined }[];
164
- }
165
-
166
- /** Render one head `<meta>`/`<link>` to an HTML tag, marked `data-toil-head` so the client head
167
- * manager owns it on hydration (it removes + re-emits `[data-toil-head]` tags on every navigation,
168
- * exactly as it does for the tags it adds at runtime — so there's no duplication or stale leak). */
169
- function headTag(tag: 'meta' | 'link', attrs: Record<string, string | undefined>): string {
170
- const pairs = Object.entries(attrs)
171
- .filter((e): e is [string, string] => e[1] !== undefined)
172
- .map(([k, v]) => `${k}="${escapeAttr(v)}"`);
173
- return ` <${tag} data-toil-head ${pairs.join(' ')} />`;
174
- }
175
-
176
- /**
177
- * Bake the route's component-level head (layout `<Head>` + a page's `useHead`/`useTitle`, collected
178
- * during render) into the document head: the title when the route's static `metadata` set none (the
179
- * component title outranks the site default), plus any `<meta>`/`<link>` the route SEO didn't already
180
- * carry — so the server HTML reflects the SAME head the client computes, not just the static metadata.
181
- */
182
- function injectComponentHead(
183
- html: string,
184
- head: SsrHead,
185
- routeMetadata: Record<string, unknown> | null,
186
- ): string {
187
- let out = html;
188
- if (head.title !== undefined && (routeMetadata === null || routeMetadata.title === undefined)) {
189
- const tag = `<title>${escapeHtml(head.title)}</title>`;
190
- out = /<title>[\s\S]*?<\/title>/i.test(out)
191
- ? out.replace(/<title>[\s\S]*?<\/title>/i, tag)
192
- : out.replace(/<\/head>/i, ` ${tag}\n </head>`);
193
- }
194
- const tags: string[] = [];
195
- for (const m of head.meta) {
196
- const key =
197
- m.name !== undefined
198
- ? `name="${m.name}"`
199
- : m.property !== undefined
200
- ? `property="${m.property}"`
201
- : null;
202
- if (key === null || out.includes(key)) continue; // already covered by the route SEO
203
- tags.push(headTag('meta', m));
204
- }
205
- for (const l of head.link) {
206
- if (out.includes(`href="${l.href}"`)) continue;
207
- tags.push(headTag('link', l));
208
- }
209
- if (tags.length > 0) {
210
- out = out.includes('</head>')
211
- ? out.replace(/<\/head>/i, `${tags.join('\n')}\n </head>`)
212
- : `${tags.join('\n')}\n${out}`;
213
- }
214
- return out;
215
- }
216
-
217
- /** Render one route to its template artifacts (pure given its inputs). */
218
- export function extractRouteTemplate(input: RouteRenderInput): TemplateArtifacts {
219
- const h = input.createElement ?? createElement;
220
- const render = input.renderToString ?? renderToString;
221
- const SuspenseComp = input.Suspense ?? Suspense;
222
- const element = assembleRouteElement(
223
- input.Page,
224
- input.layouts,
225
- input.loaderData,
226
- input.loaderContext,
227
- h,
228
- SuspenseComp,
229
- );
230
- // Clear any specs leaked from a previously-failed route render before collecting this route's.
231
- input.drainSsrHead?.();
232
- input.setSsrBuild(true);
233
- let routeHtml: string;
234
- try {
235
- routeHtml = render(element);
236
- } finally {
237
- input.setSsrBuild(false);
238
- }
239
- // The component-level head (layout <Head> + page useHead/useTitle) was collected during the render
240
- // above; drain it now, per route, so it can be baked alongside the static SEO.
241
- const componentHead: SsrHead = input.drainSsrHead?.() ?? { meta: [], link: [] };
242
- let full = injectIntoShell(input.shell, stripHoistedResourceTags(routeHtml));
243
- // Bake the route's resolved SEO into the template <head>, mirroring the static prerender
244
- // (prerender.ts / ssg.ts) so an `ssr=true` route serves the SAME per-page metadata (title,
245
- // description, canonical, og:* incl og:image, twitter, jsonLd) a crawler gets from the static
246
- // <route>/index.html — which the dynamic SSR template otherwise shadows at request time. Runs on
247
- // the FULL spliced document (injectSeoHtml's <title>/</head> regexes need the shell head, not the
248
- // stripped fragment) and BEFORE extractFromHtml so the coherence hash covers the baked head.
249
- if (input.seo) {
250
- full = injectSeoHtml(full, routeSeo(input.seo, input.metadata ?? null, input.pattern ?? '/'));
251
- }
252
- // Then add the component-level head (a layout's <Head>, a page's useHead/useTitle) that the static
253
- // metadata export + site SEO didn't already cover, so the server HTML carries the same head the
254
- // client computes.
255
- full = injectComponentHead(full, componentHead, input.metadata ?? null);
256
- const extracted: Extracted = extractFromHtml(full);
257
- const ids = assignSlotIds(extracted.slots);
258
- const hash = coherenceHash(extracted.tmpl, extracted.slots);
259
- return {
260
- name: input.name,
261
- tmpl: extracted.tmpl,
262
- slotsBin: encodeSlots(extracted.tmpl.length, hash, extracted.slots, ids),
263
- slotsModule: generateSlotsModule(input.name, extracted.slots, hash),
264
- hash,
265
- slotCount: extracted.slots.length,
266
- };
267
- }
268
-
269
- /** Write a route's `.tmpl` / `.slots` / `.slots.ts` into `ssrDir`. */
270
- export function writeTemplateArtifacts(ssrDir: string, art: TemplateArtifacts): void {
271
- fs.mkdirSync(ssrDir, { recursive: true });
272
- fs.writeFileSync(path.join(ssrDir, `${art.name}.tmpl`), art.tmpl);
273
- fs.writeFileSync(path.join(ssrDir, `${art.name}.slots`), art.slotsBin);
274
- fs.writeFileSync(path.join(ssrDir, `${art.name}.slots.ts`), art.slotsModule);
275
- }
276
-
277
- /**
278
- * The server source dir (where toilscript-compiled modules live): the dir of the first toilconfig
279
- * entry, else `<root>/server`. Mirrors the same resolution in `emails.ts` (`_emails.ts` lives here).
280
- */
281
- function serverDir(root: string): string {
282
- try {
283
- const cfg = JSON.parse(fs.readFileSync(path.join(root, 'toilconfig.json'), 'utf8')) as {
284
- entries?: unknown;
285
- };
286
- const first = Array.isArray(cfg.entries)
287
- ? cfg.entries.find((e): e is string => typeof e === 'string')
288
- : undefined;
289
- if (first) return path.dirname(path.resolve(root, first));
290
- } catch {
291
- // fall through to the default
292
- }
293
- return path.join(root, 'server');
294
- }
295
-
296
- /**
297
- * The build-owned dir that holds the GENERATED, server-importable `<name>.slots.ts` modules
298
- * (the `Slot` enum + `HASH` the guest `render` imports). It sits inside the server source tree so
299
- * toilscript compiles it, named `_ssr` to match the generated `_emails.ts` convention, and is
300
- * gitignored + regenerated every build — never hand-edited. The server `render` imports
301
- * `./_ssr/<name>.slots`.
302
- */
303
- export function serverSlotsDir(root: string): string {
304
- return path.join(serverDir(root), '_ssr');
305
- }
306
-
307
- /**
308
- * (Re)write the generated `<server>/_ssr/<name>.slots.ts` module(s) the server `render` imports.
309
- * Returns the map of `name -> module source` actually on disk afterwards, so the caller can detect
310
- * whether a later authoritative extraction changed the `HASH` and a server recompile is needed.
311
- * Only rewrites a file whose content actually changed (keeps mtimes stable for the dev watcher).
312
- */
313
- export function writeServerSlotsModules(
314
- root: string,
315
- modules: { name: string; slotsModule: string }[],
316
- ): Map<string, string> {
317
- const dir = serverSlotsDir(root);
318
- const written = new Map<string, string>();
319
- if (modules.length === 0) return written;
320
- fs.mkdirSync(dir, { recursive: true });
321
- for (const m of modules) {
322
- const file = path.join(dir, `${m.name}.slots.ts`);
323
- let prev: string | null = null;
324
- try {
325
- prev = fs.readFileSync(file, 'utf8');
326
- } catch {
327
- // first build for this route: no prior module
328
- }
329
- if (prev !== m.slotsModule) fs.writeFileSync(file, m.slotsModule);
330
- written.set(m.name, m.slotsModule);
331
- }
332
- return written;
333
- }
334
-
335
- /** A rendered SSR route plus the artifacts the build emits for it. */
336
- interface RenderedRoute {
337
- pattern: string;
338
- art: TemplateArtifacts;
339
- }
340
-
341
- /**
342
- * Spin up a short-lived Vite SSR server, render every `export const ssr = true` route under its
343
- * layout chain (with sample loader data, in sentinel mode) against `shell`, and return the
344
- * per-route artifacts. Shared by the pre-pass (`extractServerSlots`, slots only) and the final
345
- * `extractTemplates` (full artifacts). A route (or layout) that throws under static markup is
346
- * skipped with a warning and omitted from the result, so it falls back to client rendering.
347
- */
348
- async function renderSsrRoutes(cfg: ResolvedToilConfig, shell: string): Promise<RenderedRoute[]> {
349
- const routes = scanRoutes(cfg.routesAbsDir).filter((r) => r.slot === undefined && !r.intercept);
350
- if (routes.length === 0) return [];
351
-
352
- // Load TypeScript once (same as prerender.ts) to read each route's static `metadata` for the
353
- // SSR <head>. Only needed when the project configures SEO.
354
- const ts = cfg.seo ? await loadTypeScript(cfg.root) : null;
355
-
356
- const warn = (msg: string): void => {
357
- process.stderr.write(` toil: SSR ${msg}\n`);
358
- };
359
- const server = await createServer({
360
- ...(await createViteConfig(cfg)),
361
- server: { middlewareMode: true, hmr: false },
362
- appType: 'custom',
363
- logLevel: 'silent',
364
- });
365
-
366
- const client = (await server.ssrLoadModule('toiljs/client')) as unknown as {
367
- __setSsrBuild: (on: boolean) => void;
368
- __setSsrLocation: (path: string | null) => void;
369
- __drainSsrHead: () => SsrHead;
370
- LoaderDataContext: Context<unknown>;
371
- };
372
-
373
- // Install the ambient `Toil` global (+ registered pages / transitions) exactly
374
- // as the client entry does, by evaluating the generated globals module. Without
375
- // it, any layout or component using `Toil` (e.g. `<Toil.Head>`, `<Toil.Link>`)
376
- // throws "Toil is not defined" during extraction, so the route is silently
377
- // skipped and falls back to client rendering.
378
- const globalsModule = path.join(cfg.toilDir, 'globals.ts');
379
- if (fs.existsSync(globalsModule)) {
380
- await server.ssrLoadModule(globalsModule);
381
- }
382
-
383
- // Render with the SAME React the components import. Vite externalizes react
384
- // (CommonJS) for SSR and resolves it from the app root, so resolve it the same
385
- // way (from cfg.root) rather than using the compiler's own copy. Two React
386
- // copies leave the hook dispatcher null, so a layout/component hook
387
- // (`useLocation` -> `useRef`) throws. (`ssrLoadModule('react')` can't be used:
388
- // Vite's SSR runner cannot evaluate the CJS module -> "module is not defined".)
389
- const appRequire = createRequire(path.join(cfg.root, 'package.json'));
390
- const react = appRequire('react') as {
391
- createElement: typeof createElement;
392
- Suspense: typeof Suspense;
393
- };
394
- const reactDomServer = appRequire('react-dom/server') as {
395
- renderToString: typeof renderToString;
396
- };
397
-
398
- const rendered: RenderedRoute[] = [];
399
- try {
400
- for (const r of routes) {
401
- let mod: RouteModule;
402
- try {
403
- mod = (await server.ssrLoadModule(r.file)) as unknown as RouteModule;
404
- } catch (err) {
405
- warn(`skipped ${r.pattern} (${err instanceof Error ? err.message : String(err)})`);
406
- continue;
407
- }
408
- if (mod.ssr !== true) continue;
409
-
410
- try {
411
- const params = sampleParams(r.pattern);
412
- const loaderData =
413
- typeof mod.loader === 'function'
414
- ? await mod.loader({ params, searchParams: new URLSearchParams() })
415
- : undefined;
416
-
417
- const rootLayout = findLayout(cfg);
418
- const layoutFiles = [
419
- ...(rootLayout ? [rootLayout] : []),
420
- ...findSpecialChain(cfg, r.file, 'layout', false),
421
- ];
422
- const layouts: ComponentType<{ children?: ReactNode }>[] = [];
423
- for (const lf of layoutFiles) {
424
- const lm = (await server.ssrLoadModule(lf)) as unknown as {
425
- default: ComponentType<{ children?: ReactNode }>;
426
- };
427
- layouts.push(lm.default);
428
- }
429
-
430
- // The route's static `metadata` export (same static-AST read prerender.ts uses, so
431
- // the SSR head matches the static <route>/index.html exactly). generateMetadata is
432
- // dynamic and skipped here, as in prerender/ssg.
433
- const metadata = ts ? extractStaticMetadata(ts, r.file) : null;
434
-
435
- const name = routeTemplateName(r.pattern);
436
- // Tell location hooks which URL this template is for, so a NavLink's active
437
- // class / aria-current render as they will on the client at this route (else
438
- // the `/` default mismatches on hydration). Cleared in `finally`.
439
- client.__setSsrLocation(samplePath(r.pattern));
440
- const art = extractRouteTemplate({
441
- name,
442
- Page: mod.default,
443
- layouts,
444
- loaderData,
445
- loaderContext: client.LoaderDataContext,
446
- setSsrBuild: client.__setSsrBuild,
447
- drainSsrHead: client.__drainSsrHead,
448
- shell,
449
- createElement: react.createElement,
450
- renderToString: reactDomServer.renderToString,
451
- Suspense: react.Suspense,
452
- seo: cfg.seo,
453
- metadata,
454
- pattern: r.pattern,
455
- });
456
- // An imported image asset is resolved through the dev SSR server to a dev-only URL
457
- // (/@imagetools/..., /@fs/..., /src/...) that has no production path, so baking it into
458
- // a frozen .tmpl would 404 in the built app. Rather than ship a broken template, skip
459
- // the route (it client-renders, where the asset resolves correctly) and tell the author
460
- // to use a public/ string path for images on SSR routes.
461
- const devAsset = /(?:src|href)="\/(?:@imagetools|@fs|@id|src)\//.exec(
462
- art.tmpl.toString('utf8'),
463
- );
464
- if (devAsset) {
465
- warn(
466
- `skipped ${r.pattern}: an imported asset resolves to a dev-only URL ` +
467
- `("${devAsset[0]}…") with no production path — reference images on SSR routes ` +
468
- `via a public/ string path (e.g. src="/images/x.webp") — falling back to client rendering`,
469
- );
470
- continue;
471
- }
472
- rendered.push({ pattern: r.pattern, art });
473
- } catch (err) {
474
- warn(
475
- `skipped ${r.pattern} (render failed: ${
476
- err instanceof Error ? err.message : String(err)
477
- }) — falls back to client rendering`,
478
- );
479
- } finally {
480
- client.__setSsrLocation(null);
481
- }
482
- }
483
- } finally {
484
- await server.close();
485
- }
486
- return rendered;
487
- }
488
-
489
- /**
490
- * Resolve the HTML shell to splice routes into. The authoritative shell is the BUILT (post-Vite)
491
- * `index.html`, whose hashed `<script>`/`<link>` tags are part of the template and therefore the
492
- * coherence `HASH`. `preferBuilt` (the final extraction) demands it; the slots PRE-PASS (which runs
493
- * before Vite) falls back to the previous build's shell when present (so an unchanged rebuild's
494
- * pre-pass `HASH` already matches the final one and no server recompile is needed), and finally to
495
- * the un-built `.toil/index.html` template on a first clean build. Returns `null` when no shell
496
- * exists at all (no client build yet and no template), so the caller no-ops.
497
- */
498
- function resolveShell(cfg: ResolvedToilConfig, preferBuilt: boolean): string | null {
499
- const outDir = path.resolve(cfg.root, cfg.outDir);
500
- const builtIndex = path.join(outDir, 'index.html');
501
- const stashed = path.join(cfg.toilDir, 'shell.html');
502
- const templateIndex = path.join(cfg.toilDir, 'index.html');
503
- const order = preferBuilt
504
- ? [stashed, builtIndex]
505
- : // Pre-pass (before Vite): use a prior build's shell if it exists so the HASH is stable
506
- // across rebuilds; otherwise the generated (un-built) template, which still yields the
507
- // correct Slot ids (the final pass reconciles the HASH).
508
- [builtIndex, stashed, templateIndex];
509
- for (const p of order) {
510
- if (fs.existsSync(p)) return fs.readFileSync(p, 'utf8');
511
- }
512
- return null;
513
- }
514
-
515
- /**
516
- * SLOTS PRE-PASS (runs BEFORE the server build): render every `ssr = true` route to its `Slot`
517
- * enum + `HASH` and write the server-importable `<server>/_ssr/<name>.slots.ts` module(s), so the
518
- * server `render` can import them when toilscript compiles it. On a clean build no `.slots.ts`
519
- * exists yet, so this is what bootstraps it; on a rebuild it refreshes them. The `Slot` ids are
520
- * always correct (they depend only on the route's hole structure); the `HASH` is final only when a
521
- * prior build's shell is reused, so the final `extractTemplates` reconciles it (and the caller
522
- * recompiles the server if it rotated). Returns the modules written keyed by route name.
523
- */
524
- export async function extractServerSlots(cfg: ResolvedToilConfig): Promise<Map<string, string>> {
525
- const shell = resolveShell(cfg, false);
526
- if (shell === null) return new Map();
527
- const rendered = await renderSsrRoutes(cfg, shell);
528
- return writeServerSlotsModules(
529
- cfg.root,
530
- rendered.map((r) => ({ name: r.art.name, slotsModule: r.art.slotsModule })),
531
- );
532
- }
533
-
534
- /** One SSR route's in-memory dev template: the spliceable `.tmpl` plus its
535
- * top-level slot insertion points (numeric id -> byte offset), parsed from the
536
- * `.slots` manifest. Used by the dev server to serve SSR without a prod build. */
537
- export interface DevSsrTemplate {
538
- pattern: string;
539
- name: string;
540
- tmpl: Buffer;
541
- entries: { id: number; offset: number }[];
542
- }
543
-
544
- /**
545
- * Render every `ssr = true` route against the given (live, Vite-transformed) dev
546
- * `shell` into an in-memory template + slot offsets, for the dev server to splice
547
- * the guest `render` values into. Unlike {@link extractTemplates} this has NO
548
- * side effects (it writes nothing and does not touch `server/_ssr` or the host
549
- * bundle); the dev server builds together with the guest, so there is no hash to
550
- * reconcile. Returns `[]` when no route opts in.
551
- */
552
- export async function extractDevSsrTemplates(
553
- cfg: ResolvedToilConfig,
554
- shell: string,
555
- ): Promise<DevSsrTemplate[]> {
556
- const rendered = await renderSsrRoutes(cfg, shell);
557
- return rendered.map(({ pattern, art }) => {
558
- // Parse the `.slots` manifest: 46-byte header (n_slots at offset 44),
559
- // then 8-byte entries (u32 offset, u16 id, u8 kind, u8 reserved).
560
- const bin = art.slotsBin;
561
- const n = bin.readUInt16LE(44);
562
- const entries: { id: number; offset: number }[] = [];
563
- let o = 46;
564
- for (let i = 0; i < n; i++) {
565
- entries.push({ offset: bin.readUInt32LE(o), id: bin.readUInt16LE(o + 4) });
566
- o += 8;
567
- }
568
- return { pattern, name: art.name, tmpl: art.tmpl, entries };
569
- });
570
- }
571
-
572
- /** A file-safe, identifier-ish name for a route pattern (`/u/:name` -> `u_name`). */
573
- export function routeTemplateName(pattern: string): string {
574
- const n = pattern.replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
575
- return n.length > 0 ? n : 'index';
576
- }
577
-
578
- /** Synthesize a sample param set for a pattern's dynamic segments. */
579
- function sampleParams(pattern: string): Record<string, string> {
580
- const params: Record<string, string> = {};
581
- for (const m of pattern.matchAll(/[:*]+([A-Za-z0-9_]+)/g)) {
582
- params[m[1]] = 'sample';
583
- }
584
- return params;
585
- }
586
-
587
- /** The concrete pathname for a route pattern, dynamic segments filled with the same `sample`
588
- * value {@link sampleParams} uses, so location-dependent markup renders consistently. Static
589
- * routes return their pattern unchanged. */
590
- function samplePath(pattern: string): string {
591
- return pattern.replace(/[:*]+([A-Za-z0-9_]+)/g, 'sample');
592
- }
593
-
594
- interface RouteModule {
595
- default: ComponentType;
596
- ssr?: boolean;
597
- loader?: (args: { params: Record<string, string>; searchParams: URLSearchParams }) => unknown;
598
- }
599
-
600
- /** The outcome of the final {@link extractTemplates} pass. */
601
- export interface ExtractResult {
602
- /** The route patterns that produced a template. */
603
- readonly generated: string[];
604
- /**
605
- * Whether the AUTHORITATIVE `<server>/_ssr/<name>.slots.ts` module(s) just written differ from
606
- * the ones the server was already compiled against (`priorServerSlots`). True means the `HASH`
607
- * (or `Slot` ids) rotated after the server build, so the server must be recompiled to bake the
608
- * correct `HASH` (otherwise the host rejects the guest's stale hash as a deploy skew).
609
- */
610
- readonly serverSlotsChanged: boolean;
611
- }
612
-
613
- /**
614
- * FINAL pass (runs AFTER the client/Vite build): render every `export const ssr = true` route to
615
- * `<outDir>/_ssr/<name>.{tmpl,slots,slots.ts}` + a `templates.json` index, copy the `.tmpl`/`.slots`
616
- * into the edge host bundle at `hosts/<host>/_tmpl/`, AND (re)write the server-importable
617
- * `<server>/_ssr/<name>.slots.ts` module(s) — now against the real built shell, so the `HASH` is
618
- * authoritative. Skips (with a warning) any route that throws under static markup.
619
- *
620
- * `priorServerSlots` is the module map the slots PRE-PASS wrote (what the server was compiled
621
- * against); compared against the authoritative modules here to report whether a server recompile is
622
- * needed (see {@link ExtractResult.serverSlotsChanged}).
623
- */
624
- export async function extractTemplates(
625
- cfg: ResolvedToilConfig,
626
- hostName = 'edge',
627
- priorServerSlots: Map<string, string> = new Map(),
628
- ): Promise<ExtractResult> {
629
- const shell = resolveShell(cfg, true);
630
- if (shell === null) return { generated: [], serverSlotsChanged: false };
631
-
632
- const rendered = await renderSsrRoutes(cfg, shell);
633
-
634
- const outDir = path.resolve(cfg.root, cfg.outDir);
635
- const ssrDir = path.join(outDir, '_ssr');
636
- const hostsTmplDir = path.join(cfg.root, 'hosts', hostName, '_tmpl');
637
- const generated: string[] = [];
638
- const index: { route: string; name: string; hash: string }[] = [];
639
-
640
- for (const { pattern, art } of rendered) {
641
- writeTemplateArtifacts(ssrDir, art);
642
- fs.mkdirSync(hostsTmplDir, { recursive: true });
643
- fs.copyFileSync(
644
- path.join(ssrDir, `${art.name}.tmpl`),
645
- path.join(hostsTmplDir, `${art.name}.tmpl`),
646
- );
647
- fs.copyFileSync(
648
- path.join(ssrDir, `${art.name}.slots`),
649
- path.join(hostsTmplDir, `${art.name}.slots`),
650
- );
651
- index.push({ route: pattern, name: art.name, hash: art.hash.toString('hex') });
652
- generated.push(pattern);
653
- }
654
-
655
- // Write the AUTHORITATIVE server-importable slots module(s) (the real built-shell HASH) and
656
- // detect whether they changed since the pre-pass the server was compiled against.
657
- const authoritative = writeServerSlotsModules(
658
- cfg.root,
659
- rendered.map((r) => ({ name: r.art.name, slotsModule: r.art.slotsModule })),
660
- );
661
- let serverSlotsChanged = false;
662
- for (const [name, mod] of authoritative) {
663
- if (priorServerSlots.get(name) !== mod) serverSlotsChanged = true;
664
- }
665
-
666
- if (generated.length > 0) {
667
- fs.mkdirSync(ssrDir, { recursive: true });
668
- fs.writeFileSync(path.join(ssrDir, 'templates.json'), JSON.stringify(index, null, 2));
669
- process.stdout.write(
670
- ` ✓ extracted ${String(generated.length)} SSR template${
671
- generated.length === 1 ? '' : 's'
672
- }\n`,
673
- );
674
- }
675
- return { generated, serverSlotsChanged };
676
- }