@warlock.js/web 5.1.0 → 5.2.0

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 (219) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +126 -12
  3. package/esm/build/contribution.d.mts +11 -14
  4. package/esm/build/contribution.mjs +46 -58
  5. package/esm/build/contribution.mjs.map +1 -1
  6. package/esm/build/discover-pages.mjs +105 -45
  7. package/esm/build/discover-pages.mjs.map +1 -1
  8. package/esm/build/generate-client-registry.mjs +47 -13
  9. package/esm/build/generate-client-registry.mjs.map +1 -1
  10. package/esm/build/generate-pages-barrel.mjs +55 -13
  11. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  12. package/esm/build/page-default-export.mjs +48 -0
  13. package/esm/build/page-default-export.mjs.map +1 -0
  14. package/esm/build/page-routes-manifest.mjs +21 -0
  15. package/esm/build/page-routes-manifest.mjs.map +1 -0
  16. package/esm/build/public-files.mjs +45 -0
  17. package/esm/build/public-files.mjs.map +1 -0
  18. package/esm/build/read-route-exports.mjs +1 -1
  19. package/esm/build/read-route-exports.mjs.map +1 -1
  20. package/esm/client/build-hydrated-tree.mjs +40 -9
  21. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  22. package/esm/client/hydrate-page.mjs +1 -1
  23. package/esm/client/hydrate-page.mjs.map +1 -1
  24. package/esm/client/navigation/current-route.d.mts +1 -1
  25. package/esm/client/navigation/current-route.mjs +1 -1
  26. package/esm/client/navigation/current-route.mjs.map +1 -1
  27. package/esm/client/navigation/fetch-page-data.mjs +1 -1
  28. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  29. package/esm/client/navigation/navigation-root.mjs +1 -1
  30. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  31. package/esm/client/navigation/prefetch.mjs +1 -1
  32. package/esm/client/navigation/prefetch.mjs.map +1 -1
  33. package/esm/client/navigation/refresh.d.mts +1 -1
  34. package/esm/client/navigation/refresh.mjs +1 -1
  35. package/esm/client/navigation/refresh.mjs.map +1 -1
  36. package/esm/client/navigation/scroll-to-fragment.mjs +1 -1
  37. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
  38. package/esm/client/navigation/verbs.d.mts +1 -1
  39. package/esm/client/navigation/verbs.mjs +1 -1
  40. package/esm/client/navigation/verbs.mjs.map +1 -1
  41. package/esm/client/runtime/index.d.mts +2 -1
  42. package/esm/client/runtime/index.mjs +2 -1
  43. package/esm/client/runtime/manifest.d.mts +1 -1
  44. package/esm/client/runtime/manifest.mjs +3 -2
  45. package/esm/client/runtime/manifest.mjs.map +1 -1
  46. package/esm/client/runtime/matcher.d.mts +13 -1
  47. package/esm/client/runtime/matcher.mjs +13 -1
  48. package/esm/client/runtime/matcher.mjs.map +1 -1
  49. package/esm/client/runtime/types.d.mts +15 -2
  50. package/esm/components/default-app.mjs +1 -1
  51. package/esm/components/default-app.mjs.map +1 -1
  52. package/esm/components/document-context.d.mts +40 -2
  53. package/esm/components/document-context.mjs +1 -1
  54. package/esm/components/document-context.mjs.map +1 -1
  55. package/esm/components/head.d.mts +1 -1
  56. package/esm/components/head.mjs +1 -1
  57. package/esm/components/head.mjs.map +1 -1
  58. package/esm/components/link.d.mts +1 -1
  59. package/esm/components/link.mjs +1 -1
  60. package/esm/components/link.mjs.map +1 -1
  61. package/esm/components/scripts.d.mts +1 -1
  62. package/esm/components/scripts.mjs +4 -2
  63. package/esm/components/scripts.mjs.map +1 -1
  64. package/esm/context.d.mts +6 -102
  65. package/esm/hydration/index.mjs +1 -1
  66. package/esm/hydration/index.mjs.map +1 -1
  67. package/esm/hydration-payload.d.mts +1 -1
  68. package/esm/hydration-payload.mjs +37 -8
  69. package/esm/hydration-payload.mjs.map +1 -1
  70. package/esm/index.d.mts +4 -3
  71. package/esm/loaders.d.mts +7 -36
  72. package/esm/metadata.d.mts +1 -1
  73. package/esm/metadata.mjs +1 -1
  74. package/esm/metadata.mjs.map +1 -1
  75. package/esm/props.d.mts +16 -8
  76. package/esm/route.d.mts +2 -17
  77. package/esm/routing/compose-route-path.d.mts +1 -1
  78. package/esm/routing/compose-route-path.mjs +1 -1
  79. package/esm/routing/compose-route-path.mjs.map +1 -1
  80. package/esm/routing/data-request.mjs +1 -1
  81. package/esm/routing/data-request.mjs.map +1 -1
  82. package/esm/routing/filesystem-route.mjs +45 -0
  83. package/esm/routing/filesystem-route.mjs.map +1 -0
  84. package/esm/routing/layout-policy.mjs +1 -1
  85. package/esm/routing/layout-policy.mjs.map +1 -1
  86. package/esm/routing/navigator.mjs +1 -1
  87. package/esm/routing/navigator.mjs.map +1 -1
  88. package/esm/routing/query-string.d.mts +1 -1
  89. package/esm/routing/query-string.mjs +1 -1
  90. package/esm/routing/query-string.mjs.map +1 -1
  91. package/esm/routing/route-identity.mjs +1 -1
  92. package/esm/routing/route-identity.mjs.map +1 -1
  93. package/esm/routing/route-table.d.mts +1 -1
  94. package/esm/routing/route-table.mjs +1 -1
  95. package/esm/routing/route-table.mjs.map +1 -1
  96. package/esm/routing/router-events.d.mts +1 -1
  97. package/esm/routing/router-events.mjs +1 -1
  98. package/esm/routing/router-events.mjs.map +1 -1
  99. package/esm/routing/url-fragment.mjs +1 -1
  100. package/esm/routing/url-fragment.mjs.map +1 -1
  101. package/esm/runtime/register-modules.d.mts +19 -0
  102. package/esm/runtime/register-modules.mjs +22 -0
  103. package/esm/runtime/register-modules.mjs.map +1 -0
  104. package/esm/server/build-hydration-payload.mjs +2 -1
  105. package/esm/server/build-hydration-payload.mjs.map +1 -1
  106. package/esm/server/client-asset-url-prefix.d.mts +1 -1
  107. package/esm/server/client-asset-url-prefix.mjs +1 -1
  108. package/esm/server/client-asset-url-prefix.mjs.map +1 -1
  109. package/esm/server/create-page-module-loader.d.mts +1 -1
  110. package/esm/server/create-page-module-loader.mjs +2 -1
  111. package/esm/server/create-page-module-loader.mjs.map +1 -1
  112. package/esm/server/create-page-route-handler.d.mts +31 -6
  113. package/esm/server/create-page-route-handler.mjs +128 -43
  114. package/esm/server/create-page-route-handler.mjs.map +1 -1
  115. package/esm/server/dev-server.mjs +2 -13
  116. package/esm/server/dev-server.mjs.map +1 -1
  117. package/esm/server/error-page.d.mts +14 -0
  118. package/esm/server/error-page.mjs +39 -0
  119. package/esm/server/error-page.mjs.map +1 -0
  120. package/esm/server/execute-page-request.d.mts +7 -4
  121. package/esm/server/execute-page-request.mjs +79 -80
  122. package/esm/server/execute-page-request.mjs.map +1 -1
  123. package/esm/server/execute-page-request.types.d.mts +33 -92
  124. package/esm/server/hydration-client-url.d.mts +1 -1
  125. package/esm/server/hydration-client-url.mjs +2 -2
  126. package/esm/server/hydration-client-url.mjs.map +1 -1
  127. package/esm/server/index.d.mts +5 -5
  128. package/esm/server/index.mjs +3 -4
  129. package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
  130. package/esm/server/install-page-routes-from-manifest.mjs +71 -10
  131. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  132. package/esm/server/install-page-routes.d.mts +32 -15
  133. package/esm/server/install-page-routes.mjs +91 -39
  134. package/esm/server/install-page-routes.mjs.map +1 -1
  135. package/esm/server/install-production-page-routes.mjs +3 -36
  136. package/esm/server/install-production-page-routes.mjs.map +1 -1
  137. package/esm/server/match-page-route.mjs +2 -2
  138. package/esm/server/match-page-route.mjs.map +1 -1
  139. package/esm/server/not-found-page.d.mts +1 -1
  140. package/esm/server/not-found-page.mjs +2 -2
  141. package/esm/server/not-found-page.mjs.map +1 -1
  142. package/esm/server/page-context.d.mts +3 -9
  143. package/esm/server/page-context.mjs +3 -16
  144. package/esm/server/page-context.mjs.map +1 -1
  145. package/esm/server/page-file-change.mjs +77 -0
  146. package/esm/server/page-file-change.mjs.map +1 -0
  147. package/esm/server/page-manifest.d.mts +25 -10
  148. package/esm/server/page-manifest.mjs +1 -1
  149. package/esm/server/page-manifest.mjs.map +1 -1
  150. package/esm/server/page-render-bundle.mjs +38 -0
  151. package/esm/server/page-render-bundle.mjs.map +1 -0
  152. package/esm/server/page-route-reload.mjs +54 -0
  153. package/esm/server/page-route-reload.mjs.map +1 -0
  154. package/esm/server/register-production-public-files.mjs +40 -0
  155. package/esm/server/register-production-public-files.mjs.map +1 -0
  156. package/esm/server/render-page.d.mts +11 -14
  157. package/esm/server/render-page.mjs +154 -28
  158. package/esm/server/render-page.mjs.map +1 -1
  159. package/esm/server/resolve-page-metadata.mjs +2 -2
  160. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  161. package/esm/server/resolve-validation-data.mjs +1 -1
  162. package/esm/server/resolve-validation-data.mjs.map +1 -1
  163. package/esm/server/settle-page-response.d.mts +58 -1
  164. package/esm/server/settle-page-response.mjs +111 -46
  165. package/esm/server/settle-page-response.mjs.map +1 -1
  166. package/esm/server/stylesheet-urls.d.mts +41 -30
  167. package/esm/server/stylesheet-urls.mjs +137 -52
  168. package/esm/server/stylesheet-urls.mjs.map +1 -1
  169. package/esm/server/unregistered-pages.mjs +42 -0
  170. package/esm/server/unregistered-pages.mjs.map +1 -0
  171. package/esm/server/web-connector-factory.d.mts +2 -3
  172. package/esm/server/web-connector-factory.mjs +7 -6
  173. package/esm/server/web-connector-factory.mjs.map +1 -1
  174. package/esm/server/web-connector.d.mts +1 -1
  175. package/esm/server/web-connector.mjs +138 -18
  176. package/esm/server/web-connector.mjs.map +1 -1
  177. package/esm/shared.d.mts +18 -3
  178. package/esm/shared.mjs +3 -7
  179. package/esm/shared.mjs.map +1 -1
  180. package/esm/validation.d.mts +1 -1
  181. package/esm/vite/app-convention-aliases.mjs +1 -1
  182. package/esm/vite/app-convention-aliases.mjs.map +1 -1
  183. package/esm/vite/build-client.d.mts +1 -1
  184. package/esm/vite/build-client.mjs +2 -2
  185. package/esm/vite/build-client.mjs.map +1 -1
  186. package/esm/vite/gate-a-resolve.d.mts +1 -1
  187. package/esm/vite/gate-a-resolve.mjs +67 -13
  188. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  189. package/esm/vite/gate-b-secrets.d.mts +1 -1
  190. package/esm/vite/gate-b-secrets.mjs +35 -17
  191. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  192. package/esm/vite/gate-c-verify.d.mts +1 -1
  193. package/esm/vite/gate-c-verify.mjs +6 -6
  194. package/esm/vite/gate-c-verify.mjs.map +1 -1
  195. package/esm/vite/hydration-entries.d.mts +1 -1
  196. package/esm/vite/hydration-entries.mjs +2 -2
  197. package/esm/vite/hydration-entries.mjs.map +1 -1
  198. package/esm/vite/index.d.mts +9 -6
  199. package/esm/vite/index.mjs +124 -11
  200. package/esm/vite/index.mjs.map +1 -1
  201. package/esm/vite/page-registry-plugin.d.mts +22 -3
  202. package/esm/vite/page-registry-plugin.mjs +48 -14
  203. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  204. package/esm/vite/projection.d.mts +2 -2
  205. package/esm/vite/projection.mjs +38 -15
  206. package/esm/vite/projection.mjs.map +1 -1
  207. package/llms-full.txt +220 -56
  208. package/llms.txt +5 -3
  209. package/package.json +3 -3
  210. package/skills/add-web-to-an-app/SKILL.md +3 -3
  211. package/skills/create-a-page/SKILL.md +117 -10
  212. package/skills/load-page-data/SKILL.md +92 -12
  213. package/skills/navigate-on-the-client/SKILL.md +1 -1
  214. package/skills/serve-styles/SKILL.md +13 -12
  215. package/skills/use-layouts/SKILL.md +14 -8
  216. package/skills/write-the-root/SKILL.md +3 -1
  217. package/esm/server/buffered-response.d.mts +0 -58
  218. package/esm/server/buffered-response.mjs +0 -115
  219. package/esm/server/buffered-response.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"page-manifest.mjs","names":[],"sources":["../../../../../../../web/src/server/page-manifest.ts"],"sourcesContent":["/**\n * The build→runtime handoff table for production pages.\n *\n * The generated `<productionDir>/pages.ts` barrel — written by\n * `web/src/build/generate-pages-barrel.ts` — statically imports every page,\n * layout and the app root, then calls {@link providePageManifest} exactly\n * once. The connector reads it back at boot with {@link consumePageManifest}.\n *\n * DEPENDENCY-FREE ON PURPOSE: this module is re-exported from web's root\n * barrel (`web/src/index.ts`) so the generated barrel — which lives in the\n * consuming app's production dir — can reach it by package specifier. Adding\n * an import here would add that import to every app that loads\n * `@warlock.js/web`.\n *\n * Entries carry MODULES + FILESYSTEM FACTS ONLY — never route paths. A page's\n * route is read off its module (`route` export) at boot and composed with its\n * layout `prefix`, exactly as `install-page-routes.ts:123-129,213-221` does in\n * dev, so explicit-wins semantics live in one place for both modes.\n */\n\nexport type PageManifestLayoutEntry = {\n /** The imported layout module namespace object (default export = component, optional `prefix`). */\n module: Record<string, unknown>;\n /** App-root-relative POSIX source path, e.g. \"src/app/main/web/layout.tsx\". */\n sourceFile: string;\n};\n\nexport type PageManifestPageEntry = {\n /** The imported page module namespace object (default export = component, optional `route`). */\n module: Record<string, unknown>;\n /**\n * App-root-relative POSIX source path, e.g. \"src/app/main/web/home.page.tsx\"\n * — feeds FS-derived route/name fallback. The anchor is the APP ROOT, not the\n * repo root; the two differ the moment the app does not sit at the repo root.\n * Consumers compare these ids VERBATIM — the barrel that wrote them is the\n * authority on their form, and re-resolving or normalising one on the way in\n * turns a lookup miss into a silent mis-match.\n */\n sourceFile: string;\n /** Layout chain, outermost first. */\n layouts: readonly PageManifestLayoutEntry[];\n};\n\nexport type PageManifest = {\n /**\n * Where the client bundle was written, as an APP-ROOT-RELATIVE POSIX path\n * (`\"dist/client\"`). Resolved against the process cwd at boot, the same\n * anchor `sourceFile` uses.\n *\n * BAKED AT BUILD TIME because the runtime has no other way to learn it. The\n * directory is `<build.outdir>/client`, and `build.outdir` is configurable —\n * but it lives in `warlock.config.ts`, a BUILD-TIME file that is never\n * shipped and cannot be loaded by the plain `node dist/app.js` process\n * `warlock start` spawns. Reading it at runtime through\n * `resolveBuildConfig()` is what used to throw\n * `WarlockConfig not loaded` during connector boot, taking the whole server\n * down before it could listen.\n *\n * OPTIONAL for the same reason `app` is: a zero-page build skips the client\n * bundle entirely, so there is no directory to name. Present whenever\n * `pages` is non-empty.\n */\n clientDir?: string;\n /**\n * The application root component every page renders inside.\n *\n * OPTIONAL because it is only meaningful when pages exist: a build that\n * discovered zero pages provides `{ pages: [] }` and has no root to wrap\n * anything in. The build enforces the other direction — pages without an\n * app root fail generation — so `app` absent means `pages` is empty. Callers\n * must still branch on `pages.length` before reading `app`: the invariant is\n * the generator's, and a consumer that assumes it without checking turns a\n * malformed manifest into a crash deep in rendering instead of a named error.\n */\n app?: { module: Record<string, unknown>; sourceFile: string };\n /**\n * The page table. EMPTY IS A REAL STATE, not a missing one: an empty array\n * means \"this bundle was built with web, and it has no pages\", which is what\n * separates it from {@link consumePageManifest} returning `undefined`.\n */\n pages: readonly PageManifestPageEntry[];\n};\n\nlet providedManifest: PageManifest | undefined;\n\n/**\n * Called exactly once by the generated production/pages.ts barrel.\n *\n * A second call throws: two barrels in one process means either a stale\n * generated file survived into the bundle or the build ran twice, and the\n * second table silently replacing the first is precisely the drift this\n * contract exists to make impossible.\n */\nexport function providePageManifest(manifest: PageManifest): void {\n if (providedManifest !== undefined) {\n throw new Error(\n \"providePageManifest: a page manifest was already provided \" +\n \"(web/src/server/page-manifest.ts). The generated production/pages.ts barrel \" +\n \"must run exactly once per process.\",\n );\n }\n\n providedManifest = manifest;\n}\n\n/**\n * Connector-side read (WebConnector.boot, prod mode).\n *\n * `undefined` = no barrel ran → the app was not built with web. A manifest\n * whose `pages` is empty is the OTHER answer — built with web, no pages — and\n * the two are deliberately distinguishable here rather than collapsed into one\n * falsy result. The registry itself never throws on absence — absence is a\n * fact, not an error; the connector branches on MODE (prod: boot error; dev:\n * normal) so `undefined` never means two things at one call site.\n */\nexport function consumePageManifest(): PageManifest | undefined {\n return providedManifest;\n}\n"],"mappings":";AAmFA,IAAI;;;;;;;;;AAUJ,SAAgB,oBAAoB,UAA8B;CAChE,IAAI,qBAAqB,QACvB,MAAM,IAAI,MACR,0KAGF;CAGF,mBAAmB;AACrB;;;;;;;;;;;AAYA,SAAgB,sBAAgD;CAC9D,OAAO;AACT"}
1
+ {"version":3,"file":"page-manifest.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/page-manifest.ts"],"sourcesContent":["/**\n * The build→runtime handoff table for production pages.\n *\n * The generated `<productionDir>/pages.ts` barrel — written by\n * `web/src/build/generate-pages-barrel.ts` — statically imports every page,\n * layout and the app root, then calls {@link providePageManifest} exactly\n * once. The connector reads it back at boot with {@link consumePageManifest}.\n *\n * DEPENDENCY-FREE ON PURPOSE: this module is re-exported from web's root\n * barrel (`web/src/index.ts`) so the generated barrel — which lives in the\n * consuming app's production dir — can reach it by package specifier. Adding\n * an import here would add that import to every app that loads\n * `@warlock.js/web`.\n *\n * Entries carry MODULES + FILESYSTEM FACTS ONLY — never route paths. A page's\n * route is read off its module (`route` export) at boot and, when present,\n * composed with its layout `prefix`; when absent, the path is instead derived\n * from the page's own `sourceFile` and its layouts' prefixes, exactly as\n * `install-page-routes.ts:377-382` does in dev and `discover-pages.ts:925-935`\n * does at build — so explicit-wins semantics live in one place for all three.\n */\n\nexport type PageManifestLayoutEntry = {\n /** The imported layout module namespace object (default export = component, optional `prefix`). */\n module: Record<string, unknown>;\n /** App-root-relative POSIX source path, e.g. \"src/web/layout.tsx\". */\n sourceFile: string;\n};\n\nexport type PageManifestPageEntry = {\n /** The imported page module namespace object (default export = component, optional `route`). */\n module: Record<string, unknown>;\n /**\n * App-root-relative POSIX source path, e.g. \"src/web/home.page.tsx\"\n * — feeds FS-derived route/name fallback. The anchor is the APP ROOT, not the\n * repo root; the two differ the moment the app does not sit at the repo root.\n * Consumers compare these ids VERBATIM — the barrel that wrote them is the\n * authority on their form, and re-resolving or normalising one on the way in\n * turns a lookup miss into a silent mis-match.\n */\n sourceFile: string;\n /** Layout chain, outermost first. */\n layouts: readonly PageManifestLayoutEntry[];\n};\n\n/** The optional application error boundary, kept outside the routable page table. */\nexport type PageManifestErrorPageEntry = {\n module: Record<string, unknown>;\n sourceFile: string;\n};\n\nexport type PageManifest = {\n /**\n * Where the client bundle was written, as an APP-ROOT-RELATIVE POSIX path\n * (`\"dist/client\"`). Resolved against the process cwd at boot, the same\n * anchor `sourceFile` uses.\n *\n * BAKED AT BUILD TIME because the runtime has no other way to learn it. The\n * directory is `<build.outdir>/client`, and `build.outdir` is configurable —\n * but it lives in `warlock.config.ts`, a BUILD-TIME file that is never\n * shipped and cannot be loaded by the plain `node dist/app.js` process\n * `warlock start` spawns. Reading it at runtime through\n * `resolveBuildConfig()` is what used to throw\n * `WarlockConfig not loaded` during connector boot, taking the whole server\n * down before it could listen.\n *\n * OPTIONAL only when the build has neither pages nor public files. A\n * zero-page build skips the hydration bundle, but still names this directory\n * when it copied app-owned `public/` files below it. Present whenever\n * `pages` or `publicFiles` is non-empty.\n */\n clientDir?: string;\n /**\n * App-public file paths copied below `<clientDir>/public`, relative and\n * POSIX. Production registers these exact URLs; it never mounts the client\n * root and therefore never exposes `.vite/manifest.json`.\n */\n publicFiles?: readonly string[];\n /**\n * The application root component every page renders inside.\n *\n * OPTIONAL because it is only meaningful when pages exist: a build that\n * discovered zero pages provides `{ pages: [] }` and has no root to wrap\n * anything in. The build enforces the other direction — pages without an\n * app root fail generation — so `app` absent means `pages` is empty. Callers\n * must still branch on `pages.length` before reading `app`: the invariant is\n * the generator's, and a consumer that assumes it without checking turns a\n * malformed manifest into a crash deep in rendering instead of a named error.\n */\n app?: { module: Record<string, unknown>; sourceFile: string };\n /** An application-owned error boundary. It has no route name or path. */\n errorPage?: PageManifestErrorPageEntry;\n /**\n * The page table. EMPTY IS A REAL STATE, not a missing one: an empty array\n * means \"this bundle was built with web, and it has no pages\", which is what\n * separates it from {@link consumePageManifest} returning `undefined`.\n */\n pages: readonly PageManifestPageEntry[];\n};\n\nlet providedManifest: PageManifest | undefined;\n\n/**\n * Called exactly once by the generated production/pages.ts barrel.\n *\n * A second call throws: two barrels in one process means either a stale\n * generated file survived into the bundle or the build ran twice, and the\n * second table silently replacing the first is precisely the drift this\n * contract exists to make impossible.\n */\nexport function providePageManifest(manifest: PageManifest): void {\n if (providedManifest !== undefined) {\n throw new Error(\n \"providePageManifest: a page manifest was already provided \" +\n \"(web/src/server/page-manifest.ts). The generated production/pages.ts barrel \" +\n \"must run exactly once per process.\",\n );\n }\n\n providedManifest = manifest;\n}\n\n/**\n * Connector-side read (WebConnector.boot, prod mode).\n *\n * `undefined` = no barrel ran → the app was not built with web. A manifest\n * whose `pages` is empty is the OTHER answer — built with web, no pages — and\n * the two are deliberately distinguishable here rather than collapsed into one\n * falsy result. The registry itself never throws on absence — absence is a\n * fact, not an error; the connector branches on MODE (prod: boot error; dev:\n * normal) so `undefined` never means two things at one call site.\n */\nexport function consumePageManifest(): PageManifest | undefined {\n return providedManifest;\n}\n"],"mappings":";AAoGA,IAAI;;;;;;;;;AAUJ,SAAgB,oBAAoB,UAA8B;CAChE,IAAI,qBAAqB,QACvB,MAAM,IAAI,MACR,0KAGF;CAGF,mBAAmB;AACrB;;;;;;;;;;;AAYA,SAAgB,sBAAgD;CAC9D,OAAO;AACT"}
@@ -0,0 +1,38 @@
1
+ //#region web/src/server/page-render-bundle.ts
2
+ /**
3
+ * The internal, non-serializable marker `renderPageFailure` (render-page.ts)
4
+ * sets on its bundle AND on the document payload it hands to
5
+ * `DocumentContext` — never on anything `finishRender` produces.
6
+ *
7
+ * `renderPageFailure` runs when a module-load or registration throw happens
8
+ * BEFORE any page/layout/app triple exists — there is no trustworthy server
9
+ * composition for the browser to hydrate against, unlike a normal app-owned
10
+ * `error.page.tsx` reached through `finishRender`, which renders inside a
11
+ * real triple and stays hydratable. Marking both the bundle (read by
12
+ * `create-page-route-handler.ts`'s catch path to skip the hydration module)
13
+ * and the payload (read by `<Scripts/>` to skip `__WARLOCK_DATA__`) lets both
14
+ * call sites agree from the SAME flag instead of two independent guesses.
15
+ *
16
+ * A `Symbol`, non-enumerable, and never part of any exported type: nothing
17
+ * that serializes or spreads these objects (`JSON.stringify`, `{ ...x }`) can
18
+ * observe or carry it forward — only `isNonHydrating` reads it, and only
19
+ * `markNonHydrating` writes it.
20
+ */
21
+ const NON_HYDRATING = Symbol("warlock.web/non-hydrating");
22
+ /** Set only by `renderPageFailure` — see the module doc above. */
23
+ function markNonHydrating(target) {
24
+ Object.defineProperty(target, NON_HYDRATING, {
25
+ value: true,
26
+ enumerable: false,
27
+ configurable: true
28
+ });
29
+ return target;
30
+ }
31
+ /** Read by `create-page-route-handler.ts` and `<Scripts/>` — see the module doc above. */
32
+ function isNonHydrating(target) {
33
+ return target !== void 0 && target[NON_HYDRATING] === true;
34
+ }
35
+
36
+ //#endregion
37
+ export { isNonHydrating, markNonHydrating };
38
+ //# sourceMappingURL=page-render-bundle.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-render-bundle.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/page-render-bundle.ts"],"sourcesContent":["/**\n * The internal, non-serializable marker `renderPageFailure` (render-page.ts)\n * sets on its bundle AND on the document payload it hands to\n * `DocumentContext` — never on anything `finishRender` produces.\n *\n * `renderPageFailure` runs when a module-load or registration throw happens\n * BEFORE any page/layout/app triple exists — there is no trustworthy server\n * composition for the browser to hydrate against, unlike a normal app-owned\n * `error.page.tsx` reached through `finishRender`, which renders inside a\n * real triple and stays hydratable. Marking both the bundle (read by\n * `create-page-route-handler.ts`'s catch path to skip the hydration module)\n * and the payload (read by `<Scripts/>` to skip `__WARLOCK_DATA__`) lets both\n * call sites agree from the SAME flag instead of two independent guesses.\n *\n * A `Symbol`, non-enumerable, and never part of any exported type: nothing\n * that serializes or spreads these objects (`JSON.stringify`, `{ ...x }`) can\n * observe or carry it forward — only `isNonHydrating` reads it, and only\n * `markNonHydrating` writes it.\n */\nconst NON_HYDRATING = Symbol(\"warlock.web/non-hydrating\");\n\ntype MarkedNonHydrating = {\n readonly [NON_HYDRATING]?: true;\n};\n\n/** Set only by `renderPageFailure` — see the module doc above. */\nexport function markNonHydrating<T extends object>(target: T): T {\n Object.defineProperty(target, NON_HYDRATING, {\n value: true,\n enumerable: false,\n configurable: true,\n });\n\n return target;\n}\n\n/** Read by `create-page-route-handler.ts` and `<Scripts/>` — see the module doc above. */\nexport function isNonHydrating(target: object | undefined): boolean {\n return target !== undefined && (target as MarkedNonHydrating)[NON_HYDRATING] === true;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,MAAM,gBAAgB,OAAO,2BAA2B;;AAOxD,SAAgB,iBAAmC,QAAc;CAC/D,OAAO,eAAe,QAAQ,eAAe;EAC3C,OAAO;EACP,YAAY;EACZ,cAAc;CAChB,CAAC;CAED,OAAO;AACT;;AAGA,SAAgB,eAAe,QAAqC;CAClE,OAAO,WAAW,UAAc,OAA8B,mBAAmB;AACnF"}
@@ -0,0 +1,54 @@
1
+ import { isNotFoundPageFile } from "./not-found-page.mjs";
2
+ import { resolvePageRouteIdentity } from "./install-page-routes.mjs";
3
+ import { isErrorPageFilePath } from "./page-file-change.mjs";
4
+ import path from "node:path";
5
+
6
+ //#region web/src/server/page-route-reload.ts
7
+ function pathKey(file) {
8
+ const normalized = path.resolve(file).replace(/\\/g, "/");
9
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
10
+ }
11
+ /**
12
+ * Every real page file currently represented in the route table. This includes
13
+ * a custom `404.page.tsx`, which is intentionally absent from the public page
14
+ * table because it has no linkable URL of its own.
15
+ */
16
+ function registeredPageFiles(routes, appSrcRoot) {
17
+ const sourceRoot = path.dirname(appSrcRoot);
18
+ return [...new Set(routes.filter((route) => route.isPage && route.sourceFile && !route.sourceFile.startsWith("\0")).map((route) => path.resolve(sourceRoot, route.sourceFile)))];
19
+ }
20
+ /** Exact route-source owners replaced by a live page-table transaction. */
21
+ function pageRouteSourceFiles(routes) {
22
+ return [...new Set(routes.filter((route) => route.isPage && route.sourceFile).map((route) => route.sourceFile))];
23
+ }
24
+ /**
25
+ * Inspect only in-place page edits. Vite's SSR graph is invalidated explicitly
26
+ * before evaluation because core's file watcher and Vite's watcher have no
27
+ * ordering contract. Component-only edits compare equal and stay in Fast
28
+ * Refresh; route/name changes request an atomic table replacement.
29
+ */
30
+ async function pageRoutesNeedReplacement(changes, options) {
31
+ let replace = changes.added.length > 0 || changes.removed.length > 0;
32
+ const installedByFile = new Map(options.installedPages.map((page) => [pathKey(page.file), page]));
33
+ for (const file of changes.inspectionNeeded) {
34
+ if (isErrorPageFilePath(file)) return true;
35
+ options.vite.environments.ssr.moduleGraph.onFileChange(file);
36
+ const pageModule = await options.vite.ssrLoadModule(file);
37
+ if (isNotFoundPageFile(file)) {
38
+ if (pageModule.route !== void 0) replace = true;
39
+ continue;
40
+ }
41
+ const installed = installedByFile.get(pathKey(file));
42
+ if (installed === void 0) {
43
+ replace = true;
44
+ continue;
45
+ }
46
+ const next = resolvePageRouteIdentity(pageModule.route, file, options.appSrcRoot);
47
+ if (next.declaredPath !== installed.declaredPath || next.name !== installed.name) replace = true;
48
+ }
49
+ return replace;
50
+ }
51
+
52
+ //#endregion
53
+ export { pageRouteSourceFiles, pageRoutesNeedReplacement, registeredPageFiles };
54
+ //# sourceMappingURL=page-route-reload.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-route-reload.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/page-route-reload.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Router } from \"@warlock.js/core\";\nimport type { ViteDevServer } from \"vite\";\nimport type { InstalledPageRoute, PageModuleShape } from \"./install-page-routes\";\nimport { resolvePageRouteIdentity } from \"./install-page-routes\";\nimport { isNotFoundPageFile } from \"./not-found-page\";\nimport type { PageFileChanges } from \"./page-file-change\";\nimport { isErrorPageFilePath } from \"./page-file-change\";\n\nfunction pathKey(file: string): string {\n const normalized = path.resolve(file).replace(/\\\\/g, \"/\");\n\n return process.platform === \"win32\" ? normalized.toLowerCase() : normalized;\n}\n\n/**\n * Every real page file currently represented in the route table. This includes\n * a custom `404.page.tsx`, which is intentionally absent from the public page\n * table because it has no linkable URL of its own.\n */\nexport function registeredPageFiles(\n routes: ReturnType<Router[\"list\"]>,\n appSrcRoot: string,\n): string[] {\n const sourceRoot = path.dirname(appSrcRoot);\n\n return [\n ...new Set(\n routes\n .filter((route) => route.isPage && route.sourceFile && !route.sourceFile.startsWith(\"\\0\"))\n .map((route) => path.resolve(sourceRoot, route.sourceFile)),\n ),\n ];\n}\n\n/** Exact route-source owners replaced by a live page-table transaction. */\nexport function pageRouteSourceFiles(routes: ReturnType<Router[\"list\"]>): string[] {\n return [\n ...new Set(\n routes\n .filter((route) => route.isPage && route.sourceFile)\n .map((route) => route.sourceFile),\n ),\n ];\n}\n\n/**\n * Inspect only in-place page edits. Vite's SSR graph is invalidated explicitly\n * before evaluation because core's file watcher and Vite's watcher have no\n * ordering contract. Component-only edits compare equal and stay in Fast\n * Refresh; route/name changes request an atomic table replacement.\n */\nexport async function pageRoutesNeedReplacement(\n changes: PageFileChanges,\n options: {\n vite: ViteDevServer;\n appSrcRoot: string;\n installedPages: readonly InstalledPageRoute[];\n },\n): Promise<boolean> {\n let replace = changes.added.length > 0 || changes.removed.length > 0;\n\n const installedByFile = new Map(\n options.installedPages.map((page) => [pathKey(page.file), page] as const),\n );\n\n for (const file of changes.inspectionNeeded) {\n // An error boundary has no installed router route to compare against. Its\n // namespace is projected into every client composition, so any edit must\n // replace the generated table; do not load it here just to inspect it.\n if (isErrorPageFilePath(file)) return true;\n\n options.vite.environments.ssr.moduleGraph.onFileChange(file);\n const pageModule = (await options.vite.ssrLoadModule(file)) as PageModuleShape;\n\n // A valid 404 page has no route identity. Its component body remains Vite\n // HMR territory; adding an illegal route export must still enter the\n // transaction so the installer can reject it without touching live routes.\n if (isNotFoundPageFile(file)) {\n if (pageModule.route !== undefined) replace = true;\n continue;\n }\n\n const installed = installedByFile.get(pathKey(file));\n if (installed === undefined) {\n replace = true;\n continue;\n }\n\n const next = resolvePageRouteIdentity(pageModule.route, file, options.appSrcRoot);\n if (next.declaredPath !== installed.declaredPath || next.name !== installed.name) {\n replace = true;\n }\n }\n\n return replace;\n}\n"],"mappings":";;;;;;AASA,SAAS,QAAQ,MAAsB;CACrC,MAAM,aAAa,KAAK,QAAQ,IAAI,EAAE,QAAQ,OAAO,GAAG;CAExD,OAAO,QAAQ,aAAa,UAAU,WAAW,YAAY,IAAI;AACnE;;;;;;AAOA,SAAgB,oBACd,QACA,YACU;CACV,MAAM,aAAa,KAAK,QAAQ,UAAU;CAE1C,OAAO,CACL,GAAG,IAAI,IACL,OACG,QAAQ,UAAU,MAAM,UAAU,MAAM,cAAc,CAAC,MAAM,WAAW,WAAW,IAAI,CAAC,EACxF,KAAK,UAAU,KAAK,QAAQ,YAAY,MAAM,UAAU,CAAC,CAC9D,CACF;AACF;;AAGA,SAAgB,qBAAqB,QAA8C;CACjF,OAAO,CACL,GAAG,IAAI,IACL,OACG,QAAQ,UAAU,MAAM,UAAU,MAAM,UAAU,EAClD,KAAK,UAAU,MAAM,UAAU,CACpC,CACF;AACF;;;;;;;AAQA,eAAsB,0BACpB,SACA,SAKkB;CAClB,IAAI,UAAU,QAAQ,MAAM,SAAS,KAAK,QAAQ,QAAQ,SAAS;CAEnE,MAAM,kBAAkB,IAAI,IAC1B,QAAQ,eAAe,KAAK,SAAS,CAAC,QAAQ,KAAK,IAAI,GAAG,IAAI,CAAU,CAC1E;CAEA,KAAK,MAAM,QAAQ,QAAQ,kBAAkB;EAI3C,IAAI,oBAAoB,IAAI,GAAG,OAAO;EAEtC,QAAQ,KAAK,aAAa,IAAI,YAAY,aAAa,IAAI;EAC3D,MAAM,aAAc,MAAM,QAAQ,KAAK,cAAc,IAAI;EAKzD,IAAI,mBAAmB,IAAI,GAAG;GAC5B,IAAI,WAAW,UAAU,QAAW,UAAU;GAC9C;EACF;EAEA,MAAM,YAAY,gBAAgB,IAAI,QAAQ,IAAI,CAAC;EACnD,IAAI,cAAc,QAAW;GAC3B,UAAU;GACV;EACF;EAEA,MAAM,OAAO,yBAAyB,WAAW,OAAO,MAAM,QAAQ,UAAU;EAChF,IAAI,KAAK,iBAAiB,UAAU,gBAAgB,KAAK,SAAS,UAAU,MAC1E,UAAU;CAEd;CAEA,OAAO;AACT"}
@@ -0,0 +1,40 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ //#region web/src/server/register-production-public-files.ts
5
+ var InvalidProductionPublicFileError = class extends Error {
6
+ constructor(publicFile) {
7
+ super(`Cannot register production public file ${JSON.stringify(publicFile)}: the build manifest must contain a non-empty relative POSIX path with no traversal segments.`);
8
+ this.name = "InvalidProductionPublicFileError";
9
+ }
10
+ };
11
+ var MissingProductionPublicFileError = class extends Error {
12
+ constructor(publicFile, absoluteFile) {
13
+ super(`Cannot register production public file ${JSON.stringify(publicFile)}: the successful build manifest recorded it, but ${JSON.stringify(absoluteFile)} is missing or not a file. Run \`warlock build\` again to replace the incomplete artifact.`);
14
+ this.name = "MissingProductionPublicFileError";
15
+ }
16
+ };
17
+ function assertRelativePublicFile(publicFile) {
18
+ const segments = publicFile.split("/");
19
+ if (publicFile.length === 0 || publicFile.startsWith("/") || publicFile.includes("\\") || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) throw new InvalidProductionPublicFileError(publicFile);
20
+ return segments;
21
+ }
22
+ /** Register the exact app-public files recorded by the successful build. */
23
+ function registerProductionPublicFiles(router, clientDir, publicFiles) {
24
+ const publicRoot = path.join(clientDir, "public");
25
+ for (const publicFile of publicFiles) {
26
+ const absoluteFile = path.join(publicRoot, ...assertRelativePublicFile(publicFile));
27
+ let stat;
28
+ try {
29
+ stat = fs.statSync(absoluteFile);
30
+ } catch {
31
+ throw new MissingProductionPublicFileError(publicFile, absoluteFile);
32
+ }
33
+ if (!stat.isFile()) throw new MissingProductionPublicFileError(publicFile, absoluteFile);
34
+ router.file(`/${publicFile}`, absoluteFile);
35
+ }
36
+ }
37
+
38
+ //#endregion
39
+ export { registerProductionPublicFiles };
40
+ //# sourceMappingURL=register-production-public-files.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-production-public-files.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/register-production-public-files.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { Router } from \"@warlock.js/core\";\n\nexport class InvalidProductionPublicFileError extends Error {\n public constructor(publicFile: string) {\n super(\n `Cannot register production public file ${JSON.stringify(publicFile)}: ` +\n \"the build manifest must contain a non-empty relative POSIX path with no \" +\n \"traversal segments.\",\n );\n this.name = \"InvalidProductionPublicFileError\";\n }\n}\n\nexport class MissingProductionPublicFileError extends Error {\n public constructor(publicFile: string, absoluteFile: string) {\n super(\n `Cannot register production public file ${JSON.stringify(publicFile)}: ` +\n \"the successful build manifest recorded it, but \" +\n `${JSON.stringify(absoluteFile)} is missing or not a file. ` +\n \"Run `warlock build` again to replace the incomplete artifact.\",\n );\n this.name = \"MissingProductionPublicFileError\";\n }\n}\n\nfunction assertRelativePublicFile(publicFile: string): string[] {\n const segments = publicFile.split(\"/\");\n\n if (\n publicFile.length === 0 ||\n publicFile.startsWith(\"/\") ||\n publicFile.includes(\"\\\\\") ||\n segments.some((segment) => segment.length === 0 || segment === \".\" || segment === \"..\")\n ) {\n throw new InvalidProductionPublicFileError(publicFile);\n }\n\n return segments;\n}\n\n/** Register the exact app-public files recorded by the successful build. */\nexport function registerProductionPublicFiles(\n router: Router,\n clientDir: string,\n publicFiles: readonly string[],\n): void {\n const publicRoot = path.join(clientDir, \"public\");\n\n for (const publicFile of publicFiles) {\n const absoluteFile = path.join(publicRoot, ...assertRelativePublicFile(publicFile));\n\n let stat: fs.Stats;\n\n try {\n stat = fs.statSync(absoluteFile);\n } catch {\n throw new MissingProductionPublicFileError(publicFile, absoluteFile);\n }\n\n if (!stat.isFile()) {\n throw new MissingProductionPublicFileError(publicFile, absoluteFile);\n }\n\n router.file(`/${publicFile}`, absoluteFile);\n }\n}\n"],"mappings":";;;;AAIA,IAAa,mCAAb,cAAsD,MAAM;CAC1D,AAAO,YAAY,YAAoB;EACrC,MACE,0CAA0C,KAAK,UAAU,UAAU,EAAE,8FAGvE;EACA,KAAK,OAAO;CACd;AACF;AAEA,IAAa,mCAAb,cAAsD,MAAM;CAC1D,AAAO,YAAY,YAAoB,cAAsB;EAC3D,MACE,0CAA0C,KAAK,UAAU,UAAU,EAAE,mDAEhE,KAAK,UAAU,YAAY,EAAE,2FAEpC;EACA,KAAK,OAAO;CACd;AACF;AAEA,SAAS,yBAAyB,YAA8B;CAC9D,MAAM,WAAW,WAAW,MAAM,GAAG;CAErC,IACE,WAAW,WAAW,KACtB,WAAW,WAAW,GAAG,KACzB,WAAW,SAAS,IAAI,KACxB,SAAS,MAAM,YAAY,QAAQ,WAAW,KAAK,YAAY,OAAO,YAAY,IAAI,GAEtF,MAAM,IAAI,iCAAiC,UAAU;CAGvD,OAAO;AACT;;AAGA,SAAgB,8BACd,QACA,WACA,aACM;CACN,MAAM,aAAa,KAAK,KAAK,WAAW,QAAQ;CAEhD,KAAK,MAAM,cAAc,aAAa;EACpC,MAAM,eAAe,KAAK,KAAK,YAAY,GAAG,yBAAyB,UAAU,CAAC;EAElF,IAAI;EAEJ,IAAI;GACF,OAAO,GAAG,SAAS,YAAY;EACjC,QAAQ;GACN,MAAM,IAAI,iCAAiC,YAAY,YAAY;EACrE;EAEA,IAAI,CAAC,KAAK,OAAO,GACf,MAAM,IAAI,iCAAiC,YAAY,YAAY;EAGrE,OAAO,KAAK,IAAI,cAAc,YAAY;CAC5C;AACF"}
@@ -1,11 +1,14 @@
1
1
  import { PAYLOAD_SCRIPT_ID, escapePayload } from "../components/document-context.mjs";
2
- import { BufferedCookie } from "./buffered-response.mjs";
2
+ import { BufferedCookie } from "./settle-page-response.mjs";
3
3
  import { ExecutePageRequestOptions, PageDataBundle, PageRouteEntry } from "./execute-page-request.types.mjs";
4
- //#region ../web/src/server/render-page.d.ts
4
+ import { ErrorPageModuleLoader } from "./error-page.mjs";
5
+ import { Request, Response } from "@warlock.js/core";
6
+
7
+ //#region web/src/server/render-page.d.ts
5
8
  /**
6
9
  * Pipeline stages 9–10: RENDER the page tree from the
7
10
  * data bundle stages 1–8 produced, then return finalized { html, status,
8
- * headers, cookies }. Stage 10 happens at the CALL SITE in two halves —
11
+ * headers }. Stage 10 happens at the CALL SITE in two halves —
9
12
  * 10a the caller applies status + headers (the single live-response write,
10
13
  * after render, before anything flushes), 10b it flushes
11
14
  * the document. Nothing in this module writes the live response. It never
@@ -39,7 +42,8 @@ type RenderPageOptions = {
39
42
  */
40
43
  as?: unknown; /** Per-call overrides of the connected registry (tests, mostly). */
41
44
  routes?: readonly PageRouteEntry[];
42
- createHttp?: ExecutePageRequestOptions["createHttp"];
45
+ createHttp?: ExecutePageRequestOptions["createHttp"]; /** Loaded only after the ordinary boundary chain has been exhausted. */
46
+ loadErrorPage?: ErrorPageModuleLoader;
43
47
  };
44
48
  /**
45
49
  * `renderPageRequest` takes the URL itself, so `params`/`query` (the
@@ -50,14 +54,7 @@ type RenderPageRequestOptions = Omit<RenderPageOptions, "params" | "query">;
50
54
  type RenderedPage = {
51
55
  /** The full document ("" when the pipeline short-circuited before render). */html: string;
52
56
  status: number; /** Committed response headers, lowercased key → value. */
53
- headers: Record<string, string>;
54
- /**
55
- * Committed cookies in commit order, attribute-faithful: each entry carries
56
- * the loader's raw value (pre-serialization) AND its options
57
- * (`httpOnly`/`secure`/`sameSite`/`path`/`expires`/…). Never flattened to a
58
- * name→value map — a map cannot express the attributes, and a Set-Cookie
59
- * built without them is a security defect, not a convenience.
60
- */
57
+ headers: Record<string, string>; /** Committed response cookies, in commit order — stage 7's `bundle.commit.cookies`. */
61
58
  cookies: BufferedCookie[];
62
59
  /**
63
60
  * The PAGE loader's data — `data.product.name` reads as the dx story
@@ -72,7 +69,7 @@ type RenderedPage = {
72
69
  */
73
70
  bundle: PageDataBundle | undefined;
74
71
  };
75
- declare function renderPage(routeName: string, options?: RenderPageOptions): Promise<RenderedPage>;
72
+ declare function renderPage(routeName: string, options?: RenderPageOptions): Promise<RenderedPage | Response>;
76
73
  /**
77
74
  * The URL-based sibling of `renderPage` — the production render surface: a
78
75
  * real HTTP server has a URL, not a route name. The url goes STRAIGHT to
@@ -83,7 +80,7 @@ declare function renderPage(routeName: string, options?: RenderPageOptions): Pro
83
80
  * URL matching no route is a legitimate 404, and a server must ANSWER it —
84
81
  * `{ html: "", status: 404 }` with an undefined `bundle` (see RenderedPage).
85
82
  */
86
- declare function renderPageRequest(url: string, options?: RenderPageRequestOptions): Promise<RenderedPage>;
83
+ declare function renderPageRequest(url: string, options?: RenderPageRequestOptions): Promise<RenderedPage | Response>;
87
84
  //#endregion
88
85
  export { PageRoutesRegistry, RenderPageOptions, RenderPageRequestOptions, RenderedPage, connectPageRoutes, renderPage, renderPageRequest };
89
86
  //# sourceMappingURL=render-page.d.mts.map
@@ -1,11 +1,26 @@
1
1
  import { DocumentContext, PAYLOAD_SCRIPT_ID, escapePayload } from "../components/document-context.mjs";
2
+ import { markNonHydrating } from "./page-render-bundle.mjs";
3
+ import { registerModules } from "../runtime/register-modules.mjs";
2
4
  import DefaultApp from "../components/default-app.mjs";
3
5
  import { buildHydrationPayload } from "./build-hydration-payload.mjs";
6
+ import { ERROR_PAGE_METADATA } from "./resolve-page-metadata.mjs";
7
+ import { hydrationErrorPageProps, resolveErrorPageMetadata } from "./error-page.mjs";
4
8
  import { buildErrorRecord, designateBoundary } from "./settle-page-response.mjs";
5
9
  import { executePageRequest } from "./execute-page-request.mjs";
6
10
  import { createElement } from "react";
11
+ import { Response } from "@warlock.js/core";
7
12
 
8
- //#region ../web/src/server/render-page.ts
13
+ //#region web/src/server/render-page.ts
14
+ /** Reads the stage 7 commit into the lowercased header map `RenderedPage` carries. */
15
+ function committedHeaders(bundle) {
16
+ const headers = {};
17
+ for (const header of bundle.commit?.headers ?? []) headers[header.key.toLowerCase()] = header.value;
18
+ return headers;
19
+ }
20
+ /** Reads the stage 7 commit into the cookie list `RenderedPage` carries. */
21
+ function committedCookies(bundle) {
22
+ return bundle.commit?.cookies ?? [];
23
+ }
9
24
  let pageRoutesRegistry;
10
25
  /**
11
26
  * Boot-time wiring so `renderPage(name, options)` can resolve a route NAME
@@ -46,6 +61,11 @@ function buildUrl(entry, params, query) {
46
61
  function FrameworkRootBoundary() {
47
62
  return createElement("main", { role: "alert" }, "Something went wrong.");
48
63
  }
64
+ function errorPageElement(module, props) {
65
+ const ErrorPage = module.default;
66
+ if (!ErrorPage) throw new Error("The application error.page.tsx module has no default export.");
67
+ return createElement(ErrorPage, props);
68
+ }
49
69
  const DATA_KEYS = {
50
70
  app: "appData",
51
71
  layout: "layoutData",
@@ -63,7 +83,7 @@ const DATA_KEYS = {
63
83
  * with no DOM.
64
84
  */
65
85
  function buildPageElement(triple, bundle) {
66
- return wrapRootward(triple, bundle, "page", buildLeaf(triple.page, bundle, "page"));
86
+ return wrapRootward(triple, bundle, "page", buildLeaf(triple.page, bundle));
67
87
  }
68
88
  /**
69
89
  * The error path renders the DESIGNATED boundary in place of the level it
@@ -81,12 +101,13 @@ function buildBoundaryElement(triple, bundle, record) {
81
101
  const wrapped = wrapRootward(triple, bundle, boundary.boundaryLevel, element);
82
102
  return boundary.boundaryLevel === "app" ? createElement(DefaultApp, { children: wrapped }) : wrapped;
83
103
  }
84
- function buildLeaf(module, bundle, level) {
104
+ function buildLeaf(module, bundle) {
85
105
  const Component = module.default;
86
106
  if (!Component) return null;
87
107
  return createElement(Component, {
88
- data: bundle[DATA_KEYS[level]],
89
- shared: bundle.shared
108
+ data: bundle.pageData,
109
+ shared: bundle.shared,
110
+ params: bundle.route.params
90
111
  });
91
112
  }
92
113
  function wrapRootward(triple, bundle, from, leaf) {
@@ -131,20 +152,12 @@ function capturingCreateHttp(registry, as) {
131
152
  createHttp(match) {
132
153
  state.match = match;
133
154
  state.captured = registry.createHttp(match);
134
- if (as !== void 0) state.captured.request.user = as;
155
+ if (as != null) state.captured.request.user = as;
135
156
  return state.captured;
136
157
  }
137
158
  };
138
159
  }
139
- /**
140
- * Structural read of the two core `Request` fields the slots need. Neither
141
- * `nonce` nor `locale` is declared on `PipelineRequest`/`WebRequest` (the
142
- * loader-facing facade only declares `validated`/`input`/`user`,
143
- * context.ts:30-68) — a narrow typed intersection at the one call site that
144
- * needs it, the same pattern `execute-page-request.ts` uses for its own
145
- * `(response as PipelineResponse & { statusCode?: number })` read
146
- * (execute-page-request.ts:517).
147
- */
160
+ /** Reads document slots directly from core's Request. */
148
161
  function documentSlotsFrom(captured) {
149
162
  const request = captured?.request;
150
163
  return {
@@ -152,13 +165,12 @@ function documentSlotsFrom(captured) {
152
165
  lang: request?.locale
153
166
  };
154
167
  }
155
- async function finishRender(triple, bundle, documentSlots) {
156
- const headers = {};
157
- for (const header of bundle.commit?.headers ?? []) headers[header.key.toLowerCase()] = header.value;
158
- const cookies = bundle.commit?.cookies ?? [];
168
+ async function finishRender(triple, bundle, documentSlots, response, loadErrorPage) {
169
+ const headers = committedHeaders(bundle);
170
+ const cookies = committedCookies(bundle);
159
171
  if (bundle.shortCircuit) return {
160
172
  html: "",
161
- status: bundle.shortCircuit.stage === "validation" ? bundle.shortCircuit.status : bundle.shortCircuit.stage === "loaders" ? bundle.shortCircuit.statusCode : bundle.shortCircuit.statusCode ?? 200,
173
+ status: bundle.shortCircuit.stage === "validation" ? bundle.shortCircuit.status : bundle.shortCircuit.statusCode ?? 200,
162
174
  headers,
163
175
  cookies,
164
176
  data: bundle.pageData,
@@ -166,7 +178,7 @@ async function finishRender(triple, bundle, documentSlots) {
166
178
  };
167
179
  if (headers["cache-control"] === void 0) headers["cache-control"] = "private";
168
180
  const { renderToString } = await import("react-dom/server");
169
- const documentValue = {
181
+ let documentValue = {
170
182
  metadata: bundle.metadata,
171
183
  payload: buildHydrationPayload(bundle),
172
184
  nonce: documentSlots.nonce,
@@ -177,20 +189,58 @@ async function finishRender(triple, bundle, documentSlots) {
177
189
  children: element
178
190
  }));
179
191
  let currentError = bundle.error;
180
- let renderTimeThrow = false;
181
192
  let body;
193
+ const renderFrameworkRoot = () => renderWithContext(createElement(DefaultApp, { children: createElement(FrameworkRootBoundary, {}) }));
194
+ const renderFrameworkAfterErrorPageFailure = () => {
195
+ bundle.errorPage = void 0;
196
+ bundle.metadata = ERROR_PAGE_METADATA;
197
+ documentValue = {
198
+ ...documentValue,
199
+ metadata: bundle.metadata,
200
+ payload: buildHydrationPayload(bundle)
201
+ };
202
+ return renderFrameworkRoot();
203
+ };
204
+ const renderErrorPage = async (thrown, serializableError = thrown) => {
205
+ if (!loadErrorPage) return void 0;
206
+ const props = {
207
+ error: thrown,
208
+ status: 500
209
+ };
210
+ const module = await loadErrorPage();
211
+ registerModules([module]);
212
+ bundle.errorPage = hydrationErrorPageProps(props, serializableError);
213
+ bundle.metadata = resolveErrorPageMetadata(module, props);
214
+ documentValue = {
215
+ ...documentValue,
216
+ metadata: bundle.metadata,
217
+ payload: buildHydrationPayload(bundle)
218
+ };
219
+ return renderWithContext(wrapRootward(triple, bundle, "page", errorPageElement(module, props)));
220
+ };
182
221
  for (;;) try {
222
+ if (currentError?.boundary.boundaryLevel === "app" && !triple.app.ErrorBoundary) {
223
+ try {
224
+ body = await renderErrorPage(currentError.originalError ?? currentError.error, currentError.error) ?? renderFrameworkRoot();
225
+ } catch {
226
+ body = renderFrameworkAfterErrorPageFailure();
227
+ }
228
+ break;
229
+ }
183
230
  body = renderWithContext(currentError ? buildBoundaryElement(triple, bundle, currentError) : buildPageElement(triple, bundle));
184
231
  break;
185
232
  } catch (thrown) {
186
- renderTimeThrow = true;
187
233
  if (currentError?.boundary.boundaryLevel === "app") {
188
- body = renderWithContext(createElement(DefaultApp, { children: createElement(FrameworkRootBoundary, {}) }));
234
+ try {
235
+ body = await renderErrorPage(thrown) ?? renderFrameworkRoot();
236
+ } catch {
237
+ body = renderFrameworkAfterErrorPageFailure();
238
+ }
189
239
  break;
190
240
  }
191
241
  currentError = buildErrorRecord(thrown, designateBoundary(currentError?.boundary.boundaryLevel === "layout" ? "app" : currentError ? "layout" : "page", triple));
192
242
  }
193
- const status = renderTimeThrow ? currentError.boundary.boundaryLevel === "app" ? 500 : bundle.commit?.statusCode ?? 200 : bundle.error ? bundle.commit?.statusCode ?? 500 : bundle.commit?.statusCode ?? 200;
243
+ const status = currentError ? 500 : bundle.commit?.statusCode ?? 200;
194
244
  return {
195
245
  html: emitDocument(body),
196
246
  status,
@@ -200,6 +250,82 @@ async function finishRender(triple, bundle, documentSlots) {
200
250
  bundle
201
251
  };
202
252
  }
253
+ /**
254
+ * Render failures that happen before the page pipeline has a triple (notably a
255
+ * module-load or registration throw). This deliberately owns one terminal
256
+ * attempt: an error-page failure falls straight to FrameworkRootBoundary.
257
+ *
258
+ * There is no triple yet, so there is no trustworthy server composition for
259
+ * the browser to hydrate against — every response this function produces is
260
+ * marked `markNonHydrating` (page-render-bundle.ts), on both the bundle and
261
+ * the document payload, whether or not it managed to render the app's own
262
+ * `error.page.tsx`. A normal app error page reached through `finishRender`
263
+ * renders inside a real triple and stays hydratable; this path never does.
264
+ */
265
+ async function renderPageFailure(options) {
266
+ const { request, response, name, path, thrown, loadErrorPage } = options;
267
+ const bundle = markNonHydrating({ route: {
268
+ name,
269
+ path,
270
+ params: {},
271
+ query: {}
272
+ } });
273
+ const headers = { "cache-control": "private" };
274
+ const { renderToString } = await import("react-dom/server");
275
+ const slots = documentSlotsFrom({
276
+ request,
277
+ response
278
+ });
279
+ const frameworkPayload = markNonHydrating(buildHydrationPayload(bundle));
280
+ let value = {
281
+ metadata: void 0,
282
+ payload: frameworkPayload,
283
+ nonce: slots.nonce,
284
+ lang: slots.lang
285
+ };
286
+ const renderWithContext = (element) => renderToString(createElement(DocumentContext.Provider, {
287
+ value,
288
+ children: element
289
+ }));
290
+ let body;
291
+ try {
292
+ if (!loadErrorPage) throw new Error("No application error page is configured.");
293
+ const props = {
294
+ error: thrown,
295
+ status: 500
296
+ };
297
+ const module = await loadErrorPage();
298
+ registerModules([module]);
299
+ const errorPage = hydrationErrorPageProps(props);
300
+ bundle.errorPage = errorPage;
301
+ value = {
302
+ ...value,
303
+ metadata: resolveErrorPageMetadata(module, props),
304
+ payload: markNonHydrating({
305
+ ...frameworkPayload,
306
+ errorPage
307
+ })
308
+ };
309
+ body = renderWithContext(createElement(DefaultApp, { children: errorPageElement(module, props) }));
310
+ } catch {
311
+ bundle.errorPage = void 0;
312
+ bundle.metadata = ERROR_PAGE_METADATA;
313
+ value = {
314
+ ...value,
315
+ metadata: bundle.metadata,
316
+ payload: markNonHydrating(buildHydrationPayload(bundle))
317
+ };
318
+ body = renderWithContext(createElement(DefaultApp, { children: createElement(FrameworkRootBoundary, {}) }));
319
+ }
320
+ return {
321
+ html: emitDocument(body),
322
+ status: 500,
323
+ headers,
324
+ cookies: [],
325
+ data: void 0,
326
+ bundle
327
+ };
328
+ }
203
329
  async function renderPage(routeName, options = {}) {
204
330
  const registry = requireRegistry(options);
205
331
  const entry = registry.routes.find((candidate) => candidate.name === routeName);
@@ -213,7 +339,7 @@ async function renderPage(routeName, options = {}) {
213
339
  url,
214
340
  routes: registry.routes,
215
341
  createHttp,
216
- finish: (bundle) => finishRender(entry.triple, bundle, documentSlotsFrom(state.captured))
342
+ finish: (bundle) => finishRender(entry.triple, bundle, documentSlotsFrom(state.captured), state.captured.response, options.loadErrorPage)
217
343
  });
218
344
  if (!rendered) throw new Error(`renderPage("${routeName}"): the built URL "${url}" did not match stage 1 (web/src/server/render-page.ts). The name resolved but the matcher disagreed — that is a manifest bug, not a caller bug.`);
219
345
  return rendered;
@@ -235,7 +361,7 @@ async function renderPageRequest(url, options = {}) {
235
361
  url,
236
362
  routes: registry.routes,
237
363
  createHttp,
238
- finish: (bundle) => finishRender(state.match.entry.triple, bundle, documentSlotsFrom(state.captured))
364
+ finish: (bundle) => finishRender(state.match.entry.triple, bundle, documentSlotsFrom(state.captured), state.captured.response, options.loadErrorPage)
239
365
  });
240
366
  if (!rendered) return {
241
367
  html: "",
@@ -249,5 +375,5 @@ async function renderPageRequest(url, options = {}) {
249
375
  }
250
376
 
251
377
  //#endregion
252
- export { connectPageRoutes, renderPage, renderPageRequest };
378
+ export { connectPageRoutes, renderPage, renderPageFailure, renderPageRequest };
253
379
  //# sourceMappingURL=render-page.mjs.map