@warlock.js/web 5.2.0 → 5.2.1

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 (201) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/esm/build/contribution.d.mts +1 -1
  3. package/esm/build/contribution.mjs +1 -1
  4. package/esm/build/contribution.mjs.map +1 -1
  5. package/esm/build/discover-pages.mjs +1 -1
  6. package/esm/build/discover-pages.mjs.map +1 -1
  7. package/esm/build/generate-client-registry.mjs +1 -1
  8. package/esm/build/generate-client-registry.mjs.map +1 -1
  9. package/esm/build/generate-pages-barrel.mjs +1 -1
  10. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  11. package/esm/build/page-default-export.mjs +1 -1
  12. package/esm/build/page-default-export.mjs.map +1 -1
  13. package/esm/build/page-routes-manifest.mjs +1 -1
  14. package/esm/build/page-routes-manifest.mjs.map +1 -1
  15. package/esm/build/public-files.mjs +1 -1
  16. package/esm/build/public-files.mjs.map +1 -1
  17. package/esm/build/read-route-exports.mjs +1 -1
  18. package/esm/build/read-route-exports.mjs.map +1 -1
  19. package/esm/client/build-hydrated-tree.mjs +1 -1
  20. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  21. package/esm/client/hydrate-page.mjs +1 -1
  22. package/esm/client/hydrate-page.mjs.map +1 -1
  23. package/esm/client/navigation/current-route.d.mts +1 -1
  24. package/esm/client/navigation/current-route.mjs +1 -1
  25. package/esm/client/navigation/current-route.mjs.map +1 -1
  26. package/esm/client/navigation/fetch-page-data.mjs +1 -1
  27. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  28. package/esm/client/navigation/navigation-root.mjs +1 -1
  29. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  30. package/esm/client/navigation/prefetch.mjs +1 -1
  31. package/esm/client/navigation/prefetch.mjs.map +1 -1
  32. package/esm/client/navigation/refresh.d.mts +1 -1
  33. package/esm/client/navigation/refresh.mjs +1 -1
  34. package/esm/client/navigation/refresh.mjs.map +1 -1
  35. package/esm/client/navigation/scroll-to-fragment.mjs +1 -1
  36. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
  37. package/esm/client/navigation/verbs.d.mts +1 -1
  38. package/esm/client/navigation/verbs.mjs +1 -1
  39. package/esm/client/navigation/verbs.mjs.map +1 -1
  40. package/esm/client/runtime/manifest.d.mts +1 -1
  41. package/esm/client/runtime/manifest.mjs +1 -1
  42. package/esm/client/runtime/manifest.mjs.map +1 -1
  43. package/esm/client/runtime/matcher.d.mts +1 -1
  44. package/esm/client/runtime/matcher.mjs +1 -1
  45. package/esm/client/runtime/matcher.mjs.map +1 -1
  46. package/esm/client/runtime/types.d.mts +1 -1
  47. package/esm/components/default-app.mjs +1 -1
  48. package/esm/components/default-app.mjs.map +1 -1
  49. package/esm/components/document-context.d.mts +1 -1
  50. package/esm/components/document-context.mjs +1 -1
  51. package/esm/components/document-context.mjs.map +1 -1
  52. package/esm/components/head.d.mts +1 -1
  53. package/esm/components/head.mjs +1 -1
  54. package/esm/components/head.mjs.map +1 -1
  55. package/esm/components/link.d.mts +1 -1
  56. package/esm/components/link.mjs +1 -1
  57. package/esm/components/link.mjs.map +1 -1
  58. package/esm/components/scripts.d.mts +1 -1
  59. package/esm/components/scripts.mjs +1 -1
  60. package/esm/components/scripts.mjs.map +1 -1
  61. package/esm/context.d.mts +1 -1
  62. package/esm/hydration/index.mjs +1 -1
  63. package/esm/hydration/index.mjs.map +1 -1
  64. package/esm/hydration-payload.mjs +1 -1
  65. package/esm/hydration-payload.mjs.map +1 -1
  66. package/esm/index.d.mts +1 -1
  67. package/esm/loaders.d.mts +1 -1
  68. package/esm/metadata.d.mts +1 -1
  69. package/esm/metadata.mjs +1 -1
  70. package/esm/metadata.mjs.map +1 -1
  71. package/esm/props.d.mts +1 -1
  72. package/esm/route.d.mts +1 -1
  73. package/esm/routing/compose-route-path.d.mts +1 -1
  74. package/esm/routing/compose-route-path.mjs +1 -1
  75. package/esm/routing/compose-route-path.mjs.map +1 -1
  76. package/esm/routing/data-request.mjs +1 -1
  77. package/esm/routing/data-request.mjs.map +1 -1
  78. package/esm/routing/filesystem-route.mjs +1 -1
  79. package/esm/routing/filesystem-route.mjs.map +1 -1
  80. package/esm/routing/layout-policy.mjs +1 -1
  81. package/esm/routing/layout-policy.mjs.map +1 -1
  82. package/esm/routing/navigator.mjs +1 -1
  83. package/esm/routing/navigator.mjs.map +1 -1
  84. package/esm/routing/query-string.d.mts +1 -1
  85. package/esm/routing/query-string.mjs +1 -1
  86. package/esm/routing/query-string.mjs.map +1 -1
  87. package/esm/routing/route-identity.mjs +1 -1
  88. package/esm/routing/route-identity.mjs.map +1 -1
  89. package/esm/routing/route-table.d.mts +1 -1
  90. package/esm/routing/route-table.mjs +1 -1
  91. package/esm/routing/route-table.mjs.map +1 -1
  92. package/esm/routing/router-events.d.mts +1 -1
  93. package/esm/routing/router-events.mjs +1 -1
  94. package/esm/routing/router-events.mjs.map +1 -1
  95. package/esm/routing/url-fragment.mjs +1 -1
  96. package/esm/routing/url-fragment.mjs.map +1 -1
  97. package/esm/runtime/register-modules.d.mts +1 -1
  98. package/esm/runtime/register-modules.mjs +1 -1
  99. package/esm/runtime/register-modules.mjs.map +1 -1
  100. package/esm/server/build-hydration-payload.mjs +1 -1
  101. package/esm/server/build-hydration-payload.mjs.map +1 -1
  102. package/esm/server/client-asset-url-prefix.d.mts +1 -1
  103. package/esm/server/client-asset-url-prefix.mjs +1 -1
  104. package/esm/server/client-asset-url-prefix.mjs.map +1 -1
  105. package/esm/server/create-page-module-loader.d.mts +1 -1
  106. package/esm/server/create-page-module-loader.mjs +1 -1
  107. package/esm/server/create-page-module-loader.mjs.map +1 -1
  108. package/esm/server/create-page-route-handler.d.mts +1 -1
  109. package/esm/server/create-page-route-handler.mjs +1 -1
  110. package/esm/server/create-page-route-handler.mjs.map +1 -1
  111. package/esm/server/dev-server.mjs +1 -1
  112. package/esm/server/dev-server.mjs.map +1 -1
  113. package/esm/server/error-page.d.mts +1 -1
  114. package/esm/server/error-page.mjs +1 -1
  115. package/esm/server/error-page.mjs.map +1 -1
  116. package/esm/server/execute-page-request.d.mts +1 -1
  117. package/esm/server/execute-page-request.mjs +1 -1
  118. package/esm/server/execute-page-request.mjs.map +1 -1
  119. package/esm/server/execute-page-request.types.d.mts +1 -1
  120. package/esm/server/hydration-client-url.d.mts +1 -1
  121. package/esm/server/hydration-client-url.mjs +1 -1
  122. package/esm/server/hydration-client-url.mjs.map +1 -1
  123. package/esm/server/install-page-routes-from-manifest.d.mts +1 -1
  124. package/esm/server/install-page-routes-from-manifest.mjs +1 -1
  125. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  126. package/esm/server/install-page-routes.d.mts +1 -1
  127. package/esm/server/install-page-routes.mjs +1 -1
  128. package/esm/server/install-page-routes.mjs.map +1 -1
  129. package/esm/server/install-production-page-routes.mjs +1 -1
  130. package/esm/server/install-production-page-routes.mjs.map +1 -1
  131. package/esm/server/match-page-route.mjs +1 -1
  132. package/esm/server/match-page-route.mjs.map +1 -1
  133. package/esm/server/not-found-page.d.mts +1 -1
  134. package/esm/server/not-found-page.mjs +1 -1
  135. package/esm/server/not-found-page.mjs.map +1 -1
  136. package/esm/server/page-context.d.mts +1 -1
  137. package/esm/server/page-context.mjs +1 -1
  138. package/esm/server/page-context.mjs.map +1 -1
  139. package/esm/server/page-file-change.mjs +1 -1
  140. package/esm/server/page-file-change.mjs.map +1 -1
  141. package/esm/server/page-manifest.d.mts +1 -1
  142. package/esm/server/page-manifest.mjs +1 -1
  143. package/esm/server/page-manifest.mjs.map +1 -1
  144. package/esm/server/page-render-bundle.mjs +1 -1
  145. package/esm/server/page-render-bundle.mjs.map +1 -1
  146. package/esm/server/page-route-reload.mjs +1 -1
  147. package/esm/server/page-route-reload.mjs.map +1 -1
  148. package/esm/server/register-production-public-files.mjs +1 -1
  149. package/esm/server/register-production-public-files.mjs.map +1 -1
  150. package/esm/server/render-page.d.mts +1 -1
  151. package/esm/server/render-page.mjs +1 -1
  152. package/esm/server/render-page.mjs.map +1 -1
  153. package/esm/server/resolve-page-metadata.mjs +1 -1
  154. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  155. package/esm/server/resolve-validation-data.mjs +1 -1
  156. package/esm/server/resolve-validation-data.mjs.map +1 -1
  157. package/esm/server/settle-page-response.d.mts +1 -1
  158. package/esm/server/settle-page-response.mjs +1 -1
  159. package/esm/server/settle-page-response.mjs.map +1 -1
  160. package/esm/server/stylesheet-urls.d.mts +1 -1
  161. package/esm/server/stylesheet-urls.mjs +1 -1
  162. package/esm/server/stylesheet-urls.mjs.map +1 -1
  163. package/esm/server/unregistered-pages.mjs +1 -1
  164. package/esm/server/unregistered-pages.mjs.map +1 -1
  165. package/esm/server/web-connector-factory.d.mts +1 -1
  166. package/esm/server/web-connector-factory.mjs +1 -1
  167. package/esm/server/web-connector-factory.mjs.map +1 -1
  168. package/esm/server/web-connector.d.mts +1 -1
  169. package/esm/server/web-connector.mjs +1 -1
  170. package/esm/server/web-connector.mjs.map +1 -1
  171. package/esm/shared.d.mts +1 -1
  172. package/esm/shared.mjs +1 -1
  173. package/esm/shared.mjs.map +1 -1
  174. package/esm/validation.d.mts +1 -1
  175. package/esm/vite/app-convention-aliases.mjs +1 -1
  176. package/esm/vite/app-convention-aliases.mjs.map +1 -1
  177. package/esm/vite/build-client.d.mts +1 -1
  178. package/esm/vite/build-client.mjs +1 -1
  179. package/esm/vite/build-client.mjs.map +1 -1
  180. package/esm/vite/gate-a-resolve.d.mts +1 -1
  181. package/esm/vite/gate-a-resolve.mjs +1 -1
  182. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  183. package/esm/vite/gate-b-secrets.d.mts +1 -1
  184. package/esm/vite/gate-b-secrets.mjs +1 -1
  185. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  186. package/esm/vite/gate-c-verify.d.mts +1 -1
  187. package/esm/vite/gate-c-verify.mjs +1 -1
  188. package/esm/vite/gate-c-verify.mjs.map +1 -1
  189. package/esm/vite/hydration-entries.d.mts +1 -1
  190. package/esm/vite/hydration-entries.mjs +1 -1
  191. package/esm/vite/hydration-entries.mjs.map +1 -1
  192. package/esm/vite/index.d.mts +1 -1
  193. package/esm/vite/index.mjs +1 -1
  194. package/esm/vite/index.mjs.map +1 -1
  195. package/esm/vite/page-registry-plugin.d.mts +1 -1
  196. package/esm/vite/page-registry-plugin.mjs +1 -1
  197. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  198. package/esm/vite/projection.d.mts +1 -1
  199. package/esm/vite/projection.mjs +1 -1
  200. package/esm/vite/projection.mjs.map +1 -1
  201. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"route-table.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/routing/route-table.ts"],"sourcesContent":["/**\n * The name→URL primitive, and the one route table both sides read.\n *\n * `href(name, params, query)` is the DURABLE primitive; `<Link>` is sugar over\n * it. That ordering is deliberate and ratified: a function serves emails,\n * redirects, `Location` headers and non-React callers, none of which can render\n * a component — and it keeps client-side navigation, when it lands, a behaviour\n * change rather than an API change.\n *\n * ── Why a process-global table is correct HERE ───────────────────────────────\n * Module-level mutable state is normally a defect in a server that handles\n * concurrent requests, and this codebase has a real example of that defect to\n * point at: `@mongez/react-router`'s `RouterWrapper` keeps the RENDERED PAGE\n * CONTENT in a module-level binding, so request B overwrites the tree request A\n * is about to serialize. That is why it is not on the SSR path.\n *\n * A route table is the opposite kind of value. It is derived from the file\n * system at boot, identical for every request in the process, and never written\n * during a request — the same category as the compiled route table the server's\n * own router holds. Nothing here is per-request, so there is nothing for two\n * requests to race over.\n *\n * The rule that keeps it that way: `publishRouteTable` is called at INSTALL\n * time (server) or at hydration entry (browser), and never from a loader, a\n * middleware, or a component.\n */\n\nimport { queryStringOf, type QueryStringInput } from \"./query-string\";\n\n/** The two fields `href` needs. Callers may pass richer entries; the rest is ignored. */\nexport type RouteTableEntry = {\n readonly name: string;\n readonly path: string;\n};\n\n/** What `href` accepts for a `:param` segment. Rendered with `String(value)`. */\nexport type RouteParameters = Readonly<Record<string, unknown>>;\n\n/**\n * Query values; an `undefined` value is omitted rather than serialized.\n *\n * A value may be a scalar, an array of scalars, or an object one level deep —\n * the shapes `@warlock.js/core` parses back out of the URL. Anything deeper\n * throws `UnserializableQueryValueError`; the grammar and the measurements\n * behind it are documented in query-string.ts.\n */\nexport type RouteQuery = QueryStringInput;\n\nconst PARAMETER_PATTERN = /:([A-Za-z0-9_]+)|\\*/g;\n\n/**\n * ── WHY THIS LIVES ON `globalThis` AND NOT IN A MODULE BINDING ───────────────\n *\n * A plain `let` here does not work in development, and the failure is silent\n * enough to be worth spelling out.\n *\n * In dev the process runs TWO module graphs over the same files. Route\n * installation is loaded by tsx/Node (it is CLI bootstrap and imports core\n * directly), while page and layout modules are evaluated by Vite's SSR module\n * runner — which keeps its own registry and its own instance of every module it\n * transforms, `@warlock.js/web` included. So a module-level binding written by\n * the installer is not the binding `<Link>` reads during render: the installer\n * publishes into one instance and the component finds the other one empty.\n *\n * That was measured, not theorised. With a module-level `let`, every anchor on\n * a server-rendered page threw `RouteTableNotPublishedError` with an empty\n * known-names list while installation had demonstrably run.\n *\n * `Symbol.for` resolves through the per-ISOLATE symbol registry, which both\n * graphs share because they are the same isolate. So the table is one value no\n * matter which graph reaches it first, and the module keeps its module-shaped\n * API. `publishedBy` is carried for diagnosis only.\n */\nconst ROUTE_TABLE_SLOT = Symbol.for(\"warlock.web.routeTable\");\n\ntype RouteTableSlot = {\n table: Map<string, string>;\n publishedBy: string;\n};\n\ntype RouteTableHost = typeof globalThis & {\n [ROUTE_TABLE_SLOT]?: RouteTableSlot;\n};\n\n/**\n * `undefined` means \"nobody has published yet\", which is a DIFFERENT fault from\n * \"the table is published and this name is not in it\" — an empty Map would\n * conflate them and send the reader hunting a route that was never missing.\n */\nfunction readSlot(): RouteTableSlot | undefined {\n return (globalThis as RouteTableHost)[ROUTE_TABLE_SLOT];\n}\n\nexport class RouteTableNotPublishedError extends Error {\n public constructor(public readonly routeName: string) {\n super(\n `Warlock href(${JSON.stringify(routeName)}) was called before the route table was ` +\n \"published, so no route name resolves yet. The table is published once at boot — by \" +\n \"the server when it installs page routes, and by the hydration entry before it mounts. \" +\n \"Seeing this means href() ran outside both: typically a module evaluating at import \" +\n \"time, or a unit test that renders a component without publishing a table first.\",\n );\n this.name = \"RouteTableNotPublishedError\";\n }\n}\n\nexport class UnknownRouteNameError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly knownRouteNames: readonly string[],\n ) {\n super(\n `Warlock href(${JSON.stringify(routeName)}) does not name a known route. ` +\n (knownRouteNames.length === 0\n ? \"The route table is published but empty, so no page declared a `route` discovery could see.\"\n : `The table knows: ${knownRouteNames.map(name => JSON.stringify(name)).join(\", \")}.`),\n );\n this.name = \"UnknownRouteNameError\";\n }\n}\n\nexport class MissingRouteParameterError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly parameterName: string,\n public readonly routePath: string,\n ) {\n super(\n `Warlock href(${JSON.stringify(routeName)}) is missing the parameter ` +\n `${JSON.stringify(parameterName)}, required by the route path \"${routePath}\". ` +\n \"It is not defaulted: a missing parameter would otherwise be interpolated as the \" +\n \"literal text `undefined`, producing a link that renders correctly and 404s for a \" +\n \"visitor.\",\n );\n this.name = \"MissingRouteParameterError\";\n }\n}\n\nexport class UnknownRouteParameterError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly parameterNames: readonly string[],\n public readonly routePath: string,\n ) {\n super(\n `Warlock href(${JSON.stringify(routeName)}) was given ` +\n `${parameterNames.map(name => JSON.stringify(name)).join(\", \")}, which the route path ` +\n `\"${routePath}\" does not declare. Passing an undeclared parameter is a typo often ` +\n \"enough that it is refused rather than dropped; values meant for the query string go \" +\n \"in the third argument.\",\n );\n this.name = \"UnknownRouteParameterError\";\n }\n}\n\nexport class DuplicateRouteNameError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly paths: readonly [string, string],\n ) {\n super(\n `Warlock route table: two routes both claim the name ${JSON.stringify(routeName)} — ` +\n `\"${paths[0]}\" and \"${paths[1]}\". A name resolves to exactly one URL, so one of the ` +\n \"two would silently win and every link to it would be wrong half the time.\",\n );\n this.name = \"DuplicateRouteNameError\";\n }\n}\n\n/**\n * Publish the table. WHOLESALE — the previous one is discarded, not merged\n * into.\n *\n * Merging looks harmless until a page is deleted and the dev server restarts:\n * the dead name would stay resolvable, and `<Link>` would go on rendering a URL\n * the server no longer routes. The table has to be able to shrink.\n */\nexport function publishRouteTable(\n entries: readonly RouteTableEntry[],\n publishedBy = \"unnamed\",\n): void {\n const table = new Map<string, string>();\n\n for (const entry of entries) {\n const existing = table.get(entry.name);\n\n if (existing !== undefined) {\n throw new DuplicateRouteNameError(entry.name, [existing, entry.path]);\n }\n\n table.set(entry.name, entry.path);\n }\n\n (globalThis as RouteTableHost)[ROUTE_TABLE_SLOT] = { table, publishedBy };\n}\n\n/**\n * Drop the table, returning the module to its pre-boot state.\n *\n * Exists for tests: the table is process-global, so a suite that published one\n * would otherwise leak it into every later test in the same worker and pass in\n * file order while failing under `--shuffle`.\n */\nexport function resetRouteTable(): void {\n delete (globalThis as RouteTableHost)[ROUTE_TABLE_SLOT];\n}\n\n/** The published names, for diagnostics. Empty when nothing is published. */\nexport function knownRouteNames(): readonly string[] {\n const slot = readSlot();\n\n return slot === undefined ? [] : [...slot.table.keys()];\n}\n\n/** Who published the live table, for diagnosis. `undefined` when nothing has. */\nexport function routeTablePublisher(): string | undefined {\n return readSlot()?.publishedBy;\n}\n\nfunction parameterNamesOf(routePath: string): readonly string[] {\n const names: string[] = [];\n\n for (const match of routePath.matchAll(PARAMETER_PATTERN)) {\n names.push(match[1] ?? \"*\");\n }\n\n return names;\n}\n\nfunction interpolate(\n routeName: string,\n routePath: string,\n params: RouteParameters | undefined,\n): string {\n const declared = parameterNamesOf(routePath);\n const supplied = Object.keys(params ?? {}).filter(key => params?.[key] !== undefined);\n const undeclared = supplied.filter(key => !declared.includes(key));\n\n if (undeclared.length > 0) {\n throw new UnknownRouteParameterError(routeName, undeclared, routePath);\n }\n\n return routePath.replace(PARAMETER_PATTERN, match => {\n const name = match === \"*\" ? \"*\" : match.slice(1);\n const value = params?.[name];\n\n if (value === undefined) {\n throw new MissingRouteParameterError(routeName, name, routePath);\n }\n\n return encodeURIComponent(String(value));\n });\n}\n\n/**\n * Resolve a route NAME to a URL.\n *\n * A name, never a path: a moved page changes its URL and keeps its name, so\n * every call site keeps working. That is the property the hardcoded table in\n * the previous `<Link>` could not offer, because it restated six URLs by hand\n * and silently refused every other page in the app.\n *\n * The query half is delegated to `queryStringOf`, which lives next to the\n * DECODER it has to agree with. It used to live here, and being module-private\n * meant the read half could not share it — the one-writer rule held only\n * because nobody had written the second writer yet.\n *\n * @throws {UnserializableQueryValueError} when a query value nests deeper than\n * the wire format core parses can carry.\n */\nexport function href(\n name: string,\n params?: RouteParameters,\n query?: RouteQuery,\n): string {\n const slot = readSlot();\n\n if (slot === undefined) throw new RouteTableNotPublishedError(name);\n\n const routePath = slot.table.get(name);\n\n if (routePath === undefined) {\n throw new UnknownRouteNameError(name, [...slot.table.keys()]);\n }\n\n return `${interpolate(name, routePath, params)}${queryStringOf(query)}`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"route-table.mjs","names":[],"sources":["../../../../../../../web/src/routing/route-table.ts"],"sourcesContent":["/**\n * The name→URL primitive, and the one route table both sides read.\n *\n * `href(name, params, query)` is the DURABLE primitive; `<Link>` is sugar over\n * it. That ordering is deliberate and ratified: a function serves emails,\n * redirects, `Location` headers and non-React callers, none of which can render\n * a component — and it keeps client-side navigation, when it lands, a behaviour\n * change rather than an API change.\n *\n * ── Why a process-global table is correct HERE ───────────────────────────────\n * Module-level mutable state is normally a defect in a server that handles\n * concurrent requests, and this codebase has a real example of that defect to\n * point at: `@mongez/react-router`'s `RouterWrapper` keeps the RENDERED PAGE\n * CONTENT in a module-level binding, so request B overwrites the tree request A\n * is about to serialize. That is why it is not on the SSR path.\n *\n * A route table is the opposite kind of value. It is derived from the file\n * system at boot, identical for every request in the process, and never written\n * during a request — the same category as the compiled route table the server's\n * own router holds. Nothing here is per-request, so there is nothing for two\n * requests to race over.\n *\n * The rule that keeps it that way: `publishRouteTable` is called at INSTALL\n * time (server) or at hydration entry (browser), and never from a loader, a\n * middleware, or a component.\n */\n\nimport { queryStringOf, type QueryStringInput } from \"./query-string\";\n\n/** The two fields `href` needs. Callers may pass richer entries; the rest is ignored. */\nexport type RouteTableEntry = {\n readonly name: string;\n readonly path: string;\n};\n\n/** What `href` accepts for a `:param` segment. Rendered with `String(value)`. */\nexport type RouteParameters = Readonly<Record<string, unknown>>;\n\n/**\n * Query values; an `undefined` value is omitted rather than serialized.\n *\n * A value may be a scalar, an array of scalars, or an object one level deep —\n * the shapes `@warlock.js/core` parses back out of the URL. Anything deeper\n * throws `UnserializableQueryValueError`; the grammar and the measurements\n * behind it are documented in query-string.ts.\n */\nexport type RouteQuery = QueryStringInput;\n\nconst PARAMETER_PATTERN = /:([A-Za-z0-9_]+)|\\*/g;\n\n/**\n * ── WHY THIS LIVES ON `globalThis` AND NOT IN A MODULE BINDING ───────────────\n *\n * A plain `let` here does not work in development, and the failure is silent\n * enough to be worth spelling out.\n *\n * In dev the process runs TWO module graphs over the same files. Route\n * installation is loaded by tsx/Node (it is CLI bootstrap and imports core\n * directly), while page and layout modules are evaluated by Vite's SSR module\n * runner — which keeps its own registry and its own instance of every module it\n * transforms, `@warlock.js/web` included. So a module-level binding written by\n * the installer is not the binding `<Link>` reads during render: the installer\n * publishes into one instance and the component finds the other one empty.\n *\n * That was measured, not theorised. With a module-level `let`, every anchor on\n * a server-rendered page threw `RouteTableNotPublishedError` with an empty\n * known-names list while installation had demonstrably run.\n *\n * `Symbol.for` resolves through the per-ISOLATE symbol registry, which both\n * graphs share because they are the same isolate. So the table is one value no\n * matter which graph reaches it first, and the module keeps its module-shaped\n * API. `publishedBy` is carried for diagnosis only.\n */\nconst ROUTE_TABLE_SLOT = Symbol.for(\"warlock.web.routeTable\");\n\ntype RouteTableSlot = {\n table: Map<string, string>;\n publishedBy: string;\n};\n\ntype RouteTableHost = typeof globalThis & {\n [ROUTE_TABLE_SLOT]?: RouteTableSlot;\n};\n\n/**\n * `undefined` means \"nobody has published yet\", which is a DIFFERENT fault from\n * \"the table is published and this name is not in it\" — an empty Map would\n * conflate them and send the reader hunting a route that was never missing.\n */\nfunction readSlot(): RouteTableSlot | undefined {\n return (globalThis as RouteTableHost)[ROUTE_TABLE_SLOT];\n}\n\nexport class RouteTableNotPublishedError extends Error {\n public constructor(public readonly routeName: string) {\n super(\n `Warlock href(${JSON.stringify(routeName)}) was called before the route table was ` +\n \"published, so no route name resolves yet. The table is published once at boot — by \" +\n \"the server when it installs page routes, and by the hydration entry before it mounts. \" +\n \"Seeing this means href() ran outside both: typically a module evaluating at import \" +\n \"time, or a unit test that renders a component without publishing a table first.\",\n );\n this.name = \"RouteTableNotPublishedError\";\n }\n}\n\nexport class UnknownRouteNameError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly knownRouteNames: readonly string[],\n ) {\n super(\n `Warlock href(${JSON.stringify(routeName)}) does not name a known route. ` +\n (knownRouteNames.length === 0\n ? \"The route table is published but empty, so no page declared a `route` discovery could see.\"\n : `The table knows: ${knownRouteNames.map(name => JSON.stringify(name)).join(\", \")}.`),\n );\n this.name = \"UnknownRouteNameError\";\n }\n}\n\nexport class MissingRouteParameterError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly parameterName: string,\n public readonly routePath: string,\n ) {\n super(\n `Warlock href(${JSON.stringify(routeName)}) is missing the parameter ` +\n `${JSON.stringify(parameterName)}, required by the route path \"${routePath}\". ` +\n \"It is not defaulted: a missing parameter would otherwise be interpolated as the \" +\n \"literal text `undefined`, producing a link that renders correctly and 404s for a \" +\n \"visitor.\",\n );\n this.name = \"MissingRouteParameterError\";\n }\n}\n\nexport class UnknownRouteParameterError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly parameterNames: readonly string[],\n public readonly routePath: string,\n ) {\n super(\n `Warlock href(${JSON.stringify(routeName)}) was given ` +\n `${parameterNames.map(name => JSON.stringify(name)).join(\", \")}, which the route path ` +\n `\"${routePath}\" does not declare. Passing an undeclared parameter is a typo often ` +\n \"enough that it is refused rather than dropped; values meant for the query string go \" +\n \"in the third argument.\",\n );\n this.name = \"UnknownRouteParameterError\";\n }\n}\n\nexport class DuplicateRouteNameError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly paths: readonly [string, string],\n ) {\n super(\n `Warlock route table: two routes both claim the name ${JSON.stringify(routeName)} — ` +\n `\"${paths[0]}\" and \"${paths[1]}\". A name resolves to exactly one URL, so one of the ` +\n \"two would silently win and every link to it would be wrong half the time.\",\n );\n this.name = \"DuplicateRouteNameError\";\n }\n}\n\n/**\n * Publish the table. WHOLESALE — the previous one is discarded, not merged\n * into.\n *\n * Merging looks harmless until a page is deleted and the dev server restarts:\n * the dead name would stay resolvable, and `<Link>` would go on rendering a URL\n * the server no longer routes. The table has to be able to shrink.\n */\nexport function publishRouteTable(\n entries: readonly RouteTableEntry[],\n publishedBy = \"unnamed\",\n): void {\n const table = new Map<string, string>();\n\n for (const entry of entries) {\n const existing = table.get(entry.name);\n\n if (existing !== undefined) {\n throw new DuplicateRouteNameError(entry.name, [existing, entry.path]);\n }\n\n table.set(entry.name, entry.path);\n }\n\n (globalThis as RouteTableHost)[ROUTE_TABLE_SLOT] = { table, publishedBy };\n}\n\n/**\n * Drop the table, returning the module to its pre-boot state.\n *\n * Exists for tests: the table is process-global, so a suite that published one\n * would otherwise leak it into every later test in the same worker and pass in\n * file order while failing under `--shuffle`.\n */\nexport function resetRouteTable(): void {\n delete (globalThis as RouteTableHost)[ROUTE_TABLE_SLOT];\n}\n\n/** The published names, for diagnostics. Empty when nothing is published. */\nexport function knownRouteNames(): readonly string[] {\n const slot = readSlot();\n\n return slot === undefined ? [] : [...slot.table.keys()];\n}\n\n/** Who published the live table, for diagnosis. `undefined` when nothing has. */\nexport function routeTablePublisher(): string | undefined {\n return readSlot()?.publishedBy;\n}\n\nfunction parameterNamesOf(routePath: string): readonly string[] {\n const names: string[] = [];\n\n for (const match of routePath.matchAll(PARAMETER_PATTERN)) {\n names.push(match[1] ?? \"*\");\n }\n\n return names;\n}\n\nfunction interpolate(\n routeName: string,\n routePath: string,\n params: RouteParameters | undefined,\n): string {\n const declared = parameterNamesOf(routePath);\n const supplied = Object.keys(params ?? {}).filter(key => params?.[key] !== undefined);\n const undeclared = supplied.filter(key => !declared.includes(key));\n\n if (undeclared.length > 0) {\n throw new UnknownRouteParameterError(routeName, undeclared, routePath);\n }\n\n return routePath.replace(PARAMETER_PATTERN, match => {\n const name = match === \"*\" ? \"*\" : match.slice(1);\n const value = params?.[name];\n\n if (value === undefined) {\n throw new MissingRouteParameterError(routeName, name, routePath);\n }\n\n return encodeURIComponent(String(value));\n });\n}\n\n/**\n * Resolve a route NAME to a URL.\n *\n * A name, never a path: a moved page changes its URL and keeps its name, so\n * every call site keeps working. That is the property the hardcoded table in\n * the previous `<Link>` could not offer, because it restated six URLs by hand\n * and silently refused every other page in the app.\n *\n * The query half is delegated to `queryStringOf`, which lives next to the\n * DECODER it has to agree with. It used to live here, and being module-private\n * meant the read half could not share it — the one-writer rule held only\n * because nobody had written the second writer yet.\n *\n * @throws {UnserializableQueryValueError} when a query value nests deeper than\n * the wire format core parses can carry.\n */\nexport function href(\n name: string,\n params?: RouteParameters,\n query?: RouteQuery,\n): string {\n const slot = readSlot();\n\n if (slot === undefined) throw new RouteTableNotPublishedError(name);\n\n const routePath = slot.table.get(name);\n\n if (routePath === undefined) {\n throw new UnknownRouteNameError(name, [...slot.table.keys()]);\n }\n\n return `${interpolate(name, routePath, params)}${queryStringOf(query)}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;AAyB1B,MAAM,mBAAmB,OAAO,IAAI,wBAAwB;;;;;;AAgB5D,SAAS,WAAuC;CAC9C,OAAQ,WAA8B;AACxC;AAEA,IAAa,8BAAb,cAAiD,MAAM;CAClB;CAAnC,AAAO,YAAY,AAAgB,WAAmB;EACpD,MACE,gBAAgB,KAAK,UAAU,SAAS,EAAE,oXAK5C;EAPiC;EAQjC,KAAK,OAAO;CACd;AACF;AAEA,IAAa,wBAAb,cAA2C,MAAM;CAE7B;CACA;CAFlB,AAAO,YACL,AAAgB,WAChB,AAAgB,iBAChB;EACA,MACE,gBAAgB,KAAK,UAAU,SAAS,EAAE,oCACvC,gBAAgB,WAAW,IACxB,+FACA,oBAAoB,gBAAgB,KAAI,SAAQ,KAAK,UAAU,IAAI,CAAC,EAAE,KAAK,IAAI,EAAE,GACzF;EARgB;EACA;EAQhB,KAAK,OAAO;CACd;AACF;AAEA,IAAa,6BAAb,cAAgD,MAAM;CAElC;CACA;CACA;CAHlB,AAAO,YACL,AAAgB,WAChB,AAAgB,eAChB,AAAgB,WAChB;EACA,MACE,gBAAgB,KAAK,UAAU,SAAS,EAAE,6BACrC,KAAK,UAAU,aAAa,EAAE,gCAAgC,UAAU,+KAI/E;EAVgB;EACA;EACA;EAShB,KAAK,OAAO;CACd;AACF;AAEA,IAAa,6BAAb,cAAgD,MAAM;CAElC;CACA;CACA;CAHlB,AAAO,YACL,AAAgB,WAChB,AAAgB,gBAChB,AAAgB,WAChB;EACA,MACE,gBAAgB,KAAK,UAAU,SAAS,EAAE,cACrC,eAAe,KAAI,SAAQ,KAAK,UAAU,IAAI,CAAC,EAAE,KAAK,IAAI,EAAE,0BAC3D,UAAU,+KAGlB;EAVgB;EACA;EACA;EAShB,KAAK,OAAO;CACd;AACF;AAEA,IAAa,0BAAb,cAA6C,MAAM;CAE/B;CACA;CAFlB,AAAO,YACL,AAAgB,WAChB,AAAgB,OAChB;EACA,MACE,uDAAuD,KAAK,UAAU,SAAS,EAAE,MAC3E,MAAM,GAAG,SAAS,MAAM,GAAG,+HAEnC;EAPgB;EACA;EAOhB,KAAK,OAAO;CACd;AACF;;;;;;;;;AAUA,SAAgB,kBACd,SACA,cAAc,WACR;CACN,MAAM,wBAAQ,IAAI,IAAoB;CAEtC,KAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,MAAM,IAAI,MAAM,IAAI;EAErC,IAAI,aAAa,QACf,MAAM,IAAI,wBAAwB,MAAM,MAAM,CAAC,UAAU,MAAM,IAAI,CAAC;EAGtE,MAAM,IAAI,MAAM,MAAM,MAAM,IAAI;CAClC;CAEA,AAAC,WAA8B,oBAAoB;EAAE;EAAO;CAAY;AAC1E;;AAcA,SAAgB,kBAAqC;CACnD,MAAM,OAAO,SAAS;CAEtB,OAAO,SAAS,SAAY,CAAC,IAAI,CAAC,GAAG,KAAK,MAAM,KAAK,CAAC;AACxD;AAOA,SAAS,iBAAiB,WAAsC;CAC9D,MAAM,QAAkB,CAAC;CAEzB,KAAK,MAAM,SAAS,UAAU,SAAS,iBAAiB,GACtD,MAAM,KAAK,MAAM,MAAM,GAAG;CAG5B,OAAO;AACT;AAEA,SAAS,YACP,WACA,WACA,QACQ;CACR,MAAM,WAAW,iBAAiB,SAAS;CAE3C,MAAM,aADW,OAAO,KAAK,UAAU,CAAC,CAAC,EAAE,QAAO,QAAO,SAAS,SAAS,MACjD,EAAE,QAAO,QAAO,CAAC,SAAS,SAAS,GAAG,CAAC;CAEjE,IAAI,WAAW,SAAS,GACtB,MAAM,IAAI,2BAA2B,WAAW,YAAY,SAAS;CAGvE,OAAO,UAAU,QAAQ,oBAAmB,UAAS;EACnD,MAAM,OAAO,UAAU,MAAM,MAAM,MAAM,MAAM,CAAC;EAChD,MAAM,QAAQ,SAAS;EAEvB,IAAI,UAAU,QACZ,MAAM,IAAI,2BAA2B,WAAW,MAAM,SAAS;EAGjE,OAAO,mBAAmB,OAAO,KAAK,CAAC;CACzC,CAAC;AACH;;;;;;;;;;;;;;;;;AAkBA,SAAgB,KACd,MACA,QACA,OACQ;CACR,MAAM,OAAO,SAAS;CAEtB,IAAI,SAAS,QAAW,MAAM,IAAI,4BAA4B,IAAI;CAElE,MAAM,YAAY,KAAK,MAAM,IAAI,IAAI;CAErC,IAAI,cAAc,QAChB,MAAM,IAAI,sBAAsB,MAAM,CAAC,GAAG,KAAK,MAAM,KAAK,CAAC,CAAC;CAG9D,OAAO,GAAG,YAAY,MAAM,WAAW,MAAM,IAAI,cAAc,KAAK;AACtE"}
@@ -1,4 +1,4 @@
1
- //#region web/src/routing/router-events.d.ts
1
+ //#region ../web/src/routing/router-events.d.ts
2
2
  /**
3
3
  * The navigation lifecycle emitter — what a progress bar subscribes to.
4
4
  *
@@ -1,4 +1,4 @@
1
- //#region web/src/routing/router-events.ts
1
+ //#region ../web/src/routing/router-events.ts
2
2
  /**
3
3
  * One event's registrations and its delivery loop. A `Set` because insertion
4
4
  * order is preserved (listeners fire in subscription order) and removal is by
@@ -1 +1 @@
1
- {"version":3,"file":"router-events.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/routing/router-events.ts"],"sourcesContent":["/**\n * The navigation lifecycle emitter — what a progress bar subscribes to.\n *\n * A client navigation is a fetch followed by a tree swap\n * (`client/navigation/navigation-root.tsx`). Nothing about that is visible to\n * the user while it is in flight, which is the whole reason this module\n * exists: a progress bar, an analytics hook or a scroll restorer needs to know\n * that a navigation STARTED, that it FINISHED, and that it FAILED — and none\n * of them should have to reach into the navigation runtime to find out.\n *\n * So the runtime announces, and anyone may listen. The listeners are strangers\n * to each other and to the runtime; that is the point.\n *\n * ## This module ships the emitter only\n *\n * Nothing here is wired into the navigation runtime yet — `navigation-root.tsx`\n * calls the `emit*` methods in a later change. Until then this is a complete,\n * self-contained emitter with no callers, which is why it has no dependency on\n * anything in `client/`.\n *\n * ## What it deliberately does NOT do\n *\n * It carries URLs as OPAQUE STRINGS. There is no matching, no parsing, no\n * \"which route is this\" — Warlock's server router is the only matcher (canon\n * 9c8f878b), and an emitter that started parsing paths would be a second one.\n * A listener that wants a route name gets it from the payload the navigation\n * produced, not from this module.\n *\n * DIRECTORY CONTRACT — see `route-identity.ts`: nothing in `web/src/routing/`\n * may import `node:fs`, `node:path`, `vite` or `fastify`. This module also\n * touches no DOM global, which is what makes it importable from a server\n * render (see below).\n *\n * ## Named for its ancestor\n *\n * `routerEvents` is the name `@mongez/react-router` used\n * (`@mongez/react-router/src/events.ts`), and the subscription ergonomics are\n * deliberately familiar: `routerEvents.onNavigating(callback)` returns\n * something you call to stop listening. The implementation is NOT ported —\n * MRR's delegates to a global `@mongez/events` bus keyed by string\n * (`\"router.navigating\"`), which reaches its router singleton and gives up\n * per-event payload typing in the process. Here the events are the object's\n * own methods, so each one carries its own payload type and a typo is a\n * compile error rather than a listener that never fires.\n *\n * MRR returns an `EventSubscription` object; this returns the unsubscribe\n * FUNCTION itself, because that is what a React `useEffect` cleanup wants to\n * be handed:\n *\n * ```ts\n * useEffect(() => routerEvents.onNavigating(() => setLoading(true)), []);\n * ```\n */\n\n/**\n * How the navigation will be written to browser history.\n *\n * `\"replace\"` covers both an explicit `<Link replace>` and a Back/Forward\n * press — the runtime replaces in both cases, because the history entry\n * already exists. A listener that wants to stay quiet during Back/Forward\n * cannot tell the two apart from here, and does not need to: what it actually\n * cares about is that no new entry is being pushed.\n */\nexport type NavigationMode = \"push\" | \"replace\";\n\n/** Emitted when a navigation begins — before anything has been fetched. */\nexport type NavigationStartPayload = {\n /** The URL the navigation was requested for, verbatim and unparsed. */\n url: string;\n mode: NavigationMode;\n};\n\n/** Emitted when a navigation has completed and the new page is on screen. */\nexport type NavigationEndPayload = {\n /** The URL the navigation was requested for — the same string {@link NavigationStartPayload} carried. */\n url: string;\n /**\n * The URL the page data actually came from, which is what landed in the\n * address bar. It differs from `url` whenever the server redirected — a\n * page that requires auth answers from `/login` (see\n * `client/navigation/fetch-page-data.ts`).\n */\n resolvedUrl: string;\n mode: NavigationMode;\n};\n\n/**\n * Emitted when a navigation could not complete.\n *\n * A failed client navigation is not a dead end — the runtime degrades to a\n * full browser load — so a listener should treat this as \"the in-flight\n * navigation is over\", not as an error to render. A progress bar hides on it.\n */\nexport type NavigationErrorPayload = {\n /** The URL the navigation was requested for. */\n url: string;\n mode: NavigationMode;\n /**\n * What went wrong. `unknown` rather than `Error` because a caught value has\n * no such guarantee — narrow it before reading `.message`.\n */\n error: unknown;\n};\n\n/** A listener for one navigation event. Its return value is ignored. */\nexport type RouterEventListener<Payload> = (payload: Payload) => void;\n\n/** Call it to stop listening. Calling it more than once is a no-op. */\nexport type RouterEventUnsubscribe = () => void;\n\n/**\n * The navigation lifecycle surface: three events, each with its own payload\n * type, each subscribable and emittable.\n *\n * The `emit*` half belongs to the navigation runtime. Nothing stops other code\n * from calling it, and nothing needs to — an emitter that lies about\n * navigations is a bug in whoever called it, not a boundary worth policing.\n */\nexport type RouterEvents = {\n /** Subscribe to the start of every navigation. @returns the unsubscribe function. */\n onNavigating: (listener: RouterEventListener<NavigationStartPayload>) => RouterEventUnsubscribe;\n /** Subscribe to every navigation that completed. @returns the unsubscribe function. */\n onNavigated: (listener: RouterEventListener<NavigationEndPayload>) => RouterEventUnsubscribe;\n /** Subscribe to every navigation that failed. @returns the unsubscribe function. */\n onNavigationError: (\n listener: RouterEventListener<NavigationErrorPayload>,\n ) => RouterEventUnsubscribe;\n /** Announce that a navigation has begun. Called by the navigation runtime. */\n emitNavigating: (payload: NavigationStartPayload) => void;\n /** Announce that a navigation has completed. Called by the navigation runtime. */\n emitNavigated: (payload: NavigationEndPayload) => void;\n /** Announce that a navigation has failed. Called by the navigation runtime. */\n emitNavigationError: (payload: NavigationErrorPayload) => void;\n};\n\n/**\n * One registration. An OBJECT rather than the callback itself, so that\n * subscribing the same function twice is two independent registrations — a\n * registry keyed by the callback would collapse them, and then one component's\n * cleanup would silently deafen another's.\n */\ntype Registration<Payload> = { listener: RouterEventListener<Payload> };\n\ntype Signal<Payload> = {\n subscribe: (listener: RouterEventListener<Payload>) => RouterEventUnsubscribe;\n emit: (payload: Payload) => void;\n};\n\n/**\n * One event's registrations and its delivery loop. A `Set` because insertion\n * order is preserved (listeners fire in subscription order) and removal is by\n * identity, which is exactly what an unsubscribe closure holds.\n */\nfunction createSignal<Payload>(eventName: string): Signal<Payload> {\n const registrations = new Set<Registration<Payload>>();\n\n return {\n subscribe: listener => {\n const registration: Registration<Payload> = { listener };\n\n registrations.add(registration);\n\n // `Set.delete` on an absent member is a no-op, so this is idempotent for\n // free — which matters because React StrictMode runs an effect's cleanup\n // twice in development.\n return () => {\n registrations.delete(registration);\n };\n },\n emit: payload => {\n /*\n A SNAPSHOT, not the live set. Listeners subscribe and unsubscribe from\n inside other listeners — a progress bar that hides itself, a one-shot\n analytics hook — and mutating the collection being iterated is how an\n emitter starts skipping listeners. Iterating a copy also fixes the\n cohort: a listener added during this emit belongs to the next\n navigation, not to the one already in flight.\n\n Removals during the emit are still honoured (the deleted-check below),\n because a listener that has just unsubscribed has said it no longer\n wants this event, and delivering it anyway is the bug that outlives the\n component.\n */\n for (const registration of [...registrations]) {\n if (!registrations.has(registration)) continue;\n\n try {\n registration.listener(payload);\n } catch (error) {\n /*\n THE POINT OF THE TRY. Everything downstream of an emit — the\n history entry, the tree swap — happens after this loop returns, so\n a listener that throws would otherwise take the navigation down\n with it. One broken progress bar is a broken progress bar; it is\n not a stuck page.\n\n Reported rather than swallowed: a listener failing silently on\n every navigation is worse than noisy. The listener STAYS\n subscribed, because one bad emit is not consent to deregister\n something the component still owns and will still try to clean up.\n */\n console.error(`Warlock routerEvents: a ${eventName} listener threw:`, error);\n }\n }\n },\n };\n}\n\n/**\n * Builds an independent emitter with no listeners.\n *\n * This is what makes the module TESTABLE without cross-test bleed, and it is\n * the escape hatch for anything that needs a private lifecycle bus. The\n * shared {@link routerEvents} is one of these, created once.\n */\nexport function createRouterEvents(): RouterEvents {\n const navigating = createSignal<NavigationStartPayload>(\"navigating\");\n const navigated = createSignal<NavigationEndPayload>(\"navigated\");\n const navigationError = createSignal<NavigationErrorPayload>(\"navigation-error\");\n\n return {\n onNavigating: navigating.subscribe,\n onNavigated: navigated.subscribe,\n onNavigationError: navigationError.subscribe,\n emitNavigating: navigating.emit,\n emitNavigated: navigated.emit,\n emitNavigationError: navigationError.emit,\n };\n}\n\n/**\n * The shared navigation lifecycle emitter — the one a progress bar subscribes\n * to and the one the navigation runtime emits on.\n *\n * ## Why a module-level singleton is safe here, when module-level state is a known SSR hazard\n *\n * Canon records MRR's `RouterWrapper.tsx:63` as the cautionary case: module\n * state that holds RENDER state is shared by every concurrent request on the\n * server, so one visitor's page leaks into another's. That hazard is about\n * *what* is held, not about module scope itself — `routing/navigator.ts` in\n * this same directory already holds a module-level registration for the same\n * reason.\n *\n * This object holds ONLY listener registrations. No current route, no payload,\n * no request-scoped anything — nothing a render reads and nothing a response\n * is built from. Two concurrent SSR requests observe the same empty listener\n * sets and neither can learn a thing about the other.\n *\n * It is also safe to IMPORT on the server: constructing it touches no DOM\n * global, no `window`, no `history`, and performs no work beyond allocating\n * three empty sets. On the server nothing subscribes and nothing emits, so it\n * simply sits there — which is the correct server behaviour for a progress\n * bar.\n *\n * A singleton (rather than a context or a factory at the call site) is what\n * lets a progress bar living anywhere in the tree — or outside React\n * entirely — hear a navigation without being handed a bus by every component\n * between it and the root. That plumbing is the reason MRR made this global\n * too. Use {@link createRouterEvents} when you want an isolated one.\n */\nexport const routerEvents: RouterEvents = createRouterEvents();\n"],"mappings":";;;;;;AAyJA,SAAS,aAAsB,WAAoC;CACjE,MAAM,gCAAgB,IAAI,IAA2B;CAErD,OAAO;EACL,YAAW,aAAY;GACrB,MAAM,eAAsC,EAAE,SAAS;GAEvD,cAAc,IAAI,YAAY;GAK9B,aAAa;IACX,cAAc,OAAO,YAAY;GACnC;EACF;EACA,OAAM,YAAW;GAcf,KAAK,MAAM,gBAAgB,CAAC,GAAG,aAAa,GAAG;IAC7C,IAAI,CAAC,cAAc,IAAI,YAAY,GAAG;IAEtC,IAAI;KACF,aAAa,SAAS,OAAO;IAC/B,SAAS,OAAO;KAad,QAAQ,MAAM,2BAA2B,UAAU,mBAAmB,KAAK;IAC7E;GACF;EACF;CACF;AACF;;;;;;;;AASA,SAAgB,qBAAmC;CACjD,MAAM,aAAa,aAAqC,YAAY;CACpE,MAAM,YAAY,aAAmC,WAAW;CAChE,MAAM,kBAAkB,aAAqC,kBAAkB;CAE/E,OAAO;EACL,cAAc,WAAW;EACzB,aAAa,UAAU;EACvB,mBAAmB,gBAAgB;EACnC,gBAAgB,WAAW;EAC3B,eAAe,UAAU;EACzB,qBAAqB,gBAAgB;CACvC;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAa,eAA6B,mBAAmB"}
1
+ {"version":3,"file":"router-events.mjs","names":[],"sources":["../../../../../../../web/src/routing/router-events.ts"],"sourcesContent":["/**\n * The navigation lifecycle emitter — what a progress bar subscribes to.\n *\n * A client navigation is a fetch followed by a tree swap\n * (`client/navigation/navigation-root.tsx`). Nothing about that is visible to\n * the user while it is in flight, which is the whole reason this module\n * exists: a progress bar, an analytics hook or a scroll restorer needs to know\n * that a navigation STARTED, that it FINISHED, and that it FAILED — and none\n * of them should have to reach into the navigation runtime to find out.\n *\n * So the runtime announces, and anyone may listen. The listeners are strangers\n * to each other and to the runtime; that is the point.\n *\n * ## This module ships the emitter only\n *\n * Nothing here is wired into the navigation runtime yet — `navigation-root.tsx`\n * calls the `emit*` methods in a later change. Until then this is a complete,\n * self-contained emitter with no callers, which is why it has no dependency on\n * anything in `client/`.\n *\n * ## What it deliberately does NOT do\n *\n * It carries URLs as OPAQUE STRINGS. There is no matching, no parsing, no\n * \"which route is this\" — Warlock's server router is the only matcher (canon\n * 9c8f878b), and an emitter that started parsing paths would be a second one.\n * A listener that wants a route name gets it from the payload the navigation\n * produced, not from this module.\n *\n * DIRECTORY CONTRACT — see `route-identity.ts`: nothing in `web/src/routing/`\n * may import `node:fs`, `node:path`, `vite` or `fastify`. This module also\n * touches no DOM global, which is what makes it importable from a server\n * render (see below).\n *\n * ## Named for its ancestor\n *\n * `routerEvents` is the name `@mongez/react-router` used\n * (`@mongez/react-router/src/events.ts`), and the subscription ergonomics are\n * deliberately familiar: `routerEvents.onNavigating(callback)` returns\n * something you call to stop listening. The implementation is NOT ported —\n * MRR's delegates to a global `@mongez/events` bus keyed by string\n * (`\"router.navigating\"`), which reaches its router singleton and gives up\n * per-event payload typing in the process. Here the events are the object's\n * own methods, so each one carries its own payload type and a typo is a\n * compile error rather than a listener that never fires.\n *\n * MRR returns an `EventSubscription` object; this returns the unsubscribe\n * FUNCTION itself, because that is what a React `useEffect` cleanup wants to\n * be handed:\n *\n * ```ts\n * useEffect(() => routerEvents.onNavigating(() => setLoading(true)), []);\n * ```\n */\n\n/**\n * How the navigation will be written to browser history.\n *\n * `\"replace\"` covers both an explicit `<Link replace>` and a Back/Forward\n * press — the runtime replaces in both cases, because the history entry\n * already exists. A listener that wants to stay quiet during Back/Forward\n * cannot tell the two apart from here, and does not need to: what it actually\n * cares about is that no new entry is being pushed.\n */\nexport type NavigationMode = \"push\" | \"replace\";\n\n/** Emitted when a navigation begins — before anything has been fetched. */\nexport type NavigationStartPayload = {\n /** The URL the navigation was requested for, verbatim and unparsed. */\n url: string;\n mode: NavigationMode;\n};\n\n/** Emitted when a navigation has completed and the new page is on screen. */\nexport type NavigationEndPayload = {\n /** The URL the navigation was requested for — the same string {@link NavigationStartPayload} carried. */\n url: string;\n /**\n * The URL the page data actually came from, which is what landed in the\n * address bar. It differs from `url` whenever the server redirected — a\n * page that requires auth answers from `/login` (see\n * `client/navigation/fetch-page-data.ts`).\n */\n resolvedUrl: string;\n mode: NavigationMode;\n};\n\n/**\n * Emitted when a navigation could not complete.\n *\n * A failed client navigation is not a dead end — the runtime degrades to a\n * full browser load — so a listener should treat this as \"the in-flight\n * navigation is over\", not as an error to render. A progress bar hides on it.\n */\nexport type NavigationErrorPayload = {\n /** The URL the navigation was requested for. */\n url: string;\n mode: NavigationMode;\n /**\n * What went wrong. `unknown` rather than `Error` because a caught value has\n * no such guarantee — narrow it before reading `.message`.\n */\n error: unknown;\n};\n\n/** A listener for one navigation event. Its return value is ignored. */\nexport type RouterEventListener<Payload> = (payload: Payload) => void;\n\n/** Call it to stop listening. Calling it more than once is a no-op. */\nexport type RouterEventUnsubscribe = () => void;\n\n/**\n * The navigation lifecycle surface: three events, each with its own payload\n * type, each subscribable and emittable.\n *\n * The `emit*` half belongs to the navigation runtime. Nothing stops other code\n * from calling it, and nothing needs to — an emitter that lies about\n * navigations is a bug in whoever called it, not a boundary worth policing.\n */\nexport type RouterEvents = {\n /** Subscribe to the start of every navigation. @returns the unsubscribe function. */\n onNavigating: (listener: RouterEventListener<NavigationStartPayload>) => RouterEventUnsubscribe;\n /** Subscribe to every navigation that completed. @returns the unsubscribe function. */\n onNavigated: (listener: RouterEventListener<NavigationEndPayload>) => RouterEventUnsubscribe;\n /** Subscribe to every navigation that failed. @returns the unsubscribe function. */\n onNavigationError: (\n listener: RouterEventListener<NavigationErrorPayload>,\n ) => RouterEventUnsubscribe;\n /** Announce that a navigation has begun. Called by the navigation runtime. */\n emitNavigating: (payload: NavigationStartPayload) => void;\n /** Announce that a navigation has completed. Called by the navigation runtime. */\n emitNavigated: (payload: NavigationEndPayload) => void;\n /** Announce that a navigation has failed. Called by the navigation runtime. */\n emitNavigationError: (payload: NavigationErrorPayload) => void;\n};\n\n/**\n * One registration. An OBJECT rather than the callback itself, so that\n * subscribing the same function twice is two independent registrations — a\n * registry keyed by the callback would collapse them, and then one component's\n * cleanup would silently deafen another's.\n */\ntype Registration<Payload> = { listener: RouterEventListener<Payload> };\n\ntype Signal<Payload> = {\n subscribe: (listener: RouterEventListener<Payload>) => RouterEventUnsubscribe;\n emit: (payload: Payload) => void;\n};\n\n/**\n * One event's registrations and its delivery loop. A `Set` because insertion\n * order is preserved (listeners fire in subscription order) and removal is by\n * identity, which is exactly what an unsubscribe closure holds.\n */\nfunction createSignal<Payload>(eventName: string): Signal<Payload> {\n const registrations = new Set<Registration<Payload>>();\n\n return {\n subscribe: listener => {\n const registration: Registration<Payload> = { listener };\n\n registrations.add(registration);\n\n // `Set.delete` on an absent member is a no-op, so this is idempotent for\n // free — which matters because React StrictMode runs an effect's cleanup\n // twice in development.\n return () => {\n registrations.delete(registration);\n };\n },\n emit: payload => {\n /*\n A SNAPSHOT, not the live set. Listeners subscribe and unsubscribe from\n inside other listeners — a progress bar that hides itself, a one-shot\n analytics hook — and mutating the collection being iterated is how an\n emitter starts skipping listeners. Iterating a copy also fixes the\n cohort: a listener added during this emit belongs to the next\n navigation, not to the one already in flight.\n\n Removals during the emit are still honoured (the deleted-check below),\n because a listener that has just unsubscribed has said it no longer\n wants this event, and delivering it anyway is the bug that outlives the\n component.\n */\n for (const registration of [...registrations]) {\n if (!registrations.has(registration)) continue;\n\n try {\n registration.listener(payload);\n } catch (error) {\n /*\n THE POINT OF THE TRY. Everything downstream of an emit — the\n history entry, the tree swap — happens after this loop returns, so\n a listener that throws would otherwise take the navigation down\n with it. One broken progress bar is a broken progress bar; it is\n not a stuck page.\n\n Reported rather than swallowed: a listener failing silently on\n every navigation is worse than noisy. The listener STAYS\n subscribed, because one bad emit is not consent to deregister\n something the component still owns and will still try to clean up.\n */\n console.error(`Warlock routerEvents: a ${eventName} listener threw:`, error);\n }\n }\n },\n };\n}\n\n/**\n * Builds an independent emitter with no listeners.\n *\n * This is what makes the module TESTABLE without cross-test bleed, and it is\n * the escape hatch for anything that needs a private lifecycle bus. The\n * shared {@link routerEvents} is one of these, created once.\n */\nexport function createRouterEvents(): RouterEvents {\n const navigating = createSignal<NavigationStartPayload>(\"navigating\");\n const navigated = createSignal<NavigationEndPayload>(\"navigated\");\n const navigationError = createSignal<NavigationErrorPayload>(\"navigation-error\");\n\n return {\n onNavigating: navigating.subscribe,\n onNavigated: navigated.subscribe,\n onNavigationError: navigationError.subscribe,\n emitNavigating: navigating.emit,\n emitNavigated: navigated.emit,\n emitNavigationError: navigationError.emit,\n };\n}\n\n/**\n * The shared navigation lifecycle emitter — the one a progress bar subscribes\n * to and the one the navigation runtime emits on.\n *\n * ## Why a module-level singleton is safe here, when module-level state is a known SSR hazard\n *\n * Canon records MRR's `RouterWrapper.tsx:63` as the cautionary case: module\n * state that holds RENDER state is shared by every concurrent request on the\n * server, so one visitor's page leaks into another's. That hazard is about\n * *what* is held, not about module scope itself — `routing/navigator.ts` in\n * this same directory already holds a module-level registration for the same\n * reason.\n *\n * This object holds ONLY listener registrations. No current route, no payload,\n * no request-scoped anything — nothing a render reads and nothing a response\n * is built from. Two concurrent SSR requests observe the same empty listener\n * sets and neither can learn a thing about the other.\n *\n * It is also safe to IMPORT on the server: constructing it touches no DOM\n * global, no `window`, no `history`, and performs no work beyond allocating\n * three empty sets. On the server nothing subscribes and nothing emits, so it\n * simply sits there — which is the correct server behaviour for a progress\n * bar.\n *\n * A singleton (rather than a context or a factory at the call site) is what\n * lets a progress bar living anywhere in the tree — or outside React\n * entirely — hear a navigation without being handed a bus by every component\n * between it and the root. That plumbing is the reason MRR made this global\n * too. Use {@link createRouterEvents} when you want an isolated one.\n */\nexport const routerEvents: RouterEvents = createRouterEvents();\n"],"mappings":";;;;;;AAyJA,SAAS,aAAsB,WAAoC;CACjE,MAAM,gCAAgB,IAAI,IAA2B;CAErD,OAAO;EACL,YAAW,aAAY;GACrB,MAAM,eAAsC,EAAE,SAAS;GAEvD,cAAc,IAAI,YAAY;GAK9B,aAAa;IACX,cAAc,OAAO,YAAY;GACnC;EACF;EACA,OAAM,YAAW;GAcf,KAAK,MAAM,gBAAgB,CAAC,GAAG,aAAa,GAAG;IAC7C,IAAI,CAAC,cAAc,IAAI,YAAY,GAAG;IAEtC,IAAI;KACF,aAAa,SAAS,OAAO;IAC/B,SAAS,OAAO;KAad,QAAQ,MAAM,2BAA2B,UAAU,mBAAmB,KAAK;IAC7E;GACF;EACF;CACF;AACF;;;;;;;;AASA,SAAgB,qBAAmC;CACjD,MAAM,aAAa,aAAqC,YAAY;CACpE,MAAM,YAAY,aAAmC,WAAW;CAChE,MAAM,kBAAkB,aAAqC,kBAAkB;CAE/E,OAAO;EACL,cAAc,WAAW;EACzB,aAAa,UAAU;EACvB,mBAAmB,gBAAgB;EACnC,gBAAgB,WAAW;EAC3B,eAAe,UAAU;EACzB,qBAAqB,gBAAgB;CACvC;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAa,eAA6B,mBAAmB"}
@@ -1,4 +1,4 @@
1
- //#region web/src/routing/url-fragment.ts
1
+ //#region ../web/src/routing/url-fragment.ts
2
2
  /**
3
3
  * The URL FRAGMENT, kept as a first-class part of a navigation.
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"url-fragment.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/routing/url-fragment.ts"],"sourcesContent":["/**\n * The URL FRAGMENT, kept as a first-class part of a navigation.\n *\n * ## The defect this file exists for\n *\n * A client navigation asks the server for page data with `fetch`, and a\n * fragment is a CLIENT-SIDE construct: it is never sent, and `response.url` —\n * the URL history is written from, because it reflects any redirect that was\n * followed — therefore never carries one. So `<Link href=\"/docs#install\">`\n * pushed `/docs` and the fragment the author wrote was silently gone from the\n * address bar: not shareable, not bookmarkable, not restorable on reload.\n *\n * Everything here is pure string work over `URL`, deliberately: the navigation\n * runtime is the only place that may touch `window`, and these answers have to\n * be provable in a suite with no browser.\n *\n * ## The redirect rule, which is the browser's rule\n *\n * When a request is redirected and the `Location` carries no fragment of its\n * own, the ORIGINAL fragment is carried onto the destination (RFC 7231 §7.1.2).\n * A `Location` that DOES name one wins outright. {@link withFragmentFrom} is\n * that rule and nothing else, so a client navigation through a redirect lands\n * where a full page load would have landed.\n */\n\n/**\n * The fragment of `url`, WITHOUT its leading `#`.\n *\n * `undefined` means there was no `#` at all, and `\"\"` means there was one with\n * nothing after it. The two are kept apart because they are different requests:\n * `/docs` says nothing about a fragment, `/docs#` says \"no target\" — and only\n * the second should end up written to the address bar as `#`.\n *\n * Plain string work rather than `new URL()`: this is asked of RELATIVE URLs\n * (`/docs#install`, `#install`), which `URL` cannot parse without a base, and\n * the base is `window`'s — not available on the server, and not this module's\n * to reach for.\n */\nexport function fragmentOf(url: string): string | undefined {\n const index = url.indexOf(\"#\");\n\n return index === -1 ? undefined : url.slice(index + 1);\n}\n\n/**\n * `url` with everything from its `#` onward removed.\n */\nexport function withoutFragment(url: string): string {\n const index = url.indexOf(\"#\");\n\n return index === -1 ? url : url.slice(0, index);\n}\n\n/**\n * Carry the fragment the caller ASKED for onto the URL the response came from.\n *\n * The resolved URL wins when it names a fragment itself — a redirect that says\n * `Location: /docs/v5#moved` meant it. Otherwise the requested fragment rides\n * along, which for the overwhelmingly common case (no redirect) simply puts\n * back what `fetch` dropped.\n */\nexport function withFragmentFrom(resolvedUrl: string, requestedUrl: string): string {\n if (fragmentOf(resolvedUrl) !== undefined) return resolvedUrl;\n\n const fragment = fragmentOf(requestedUrl);\n\n return fragment === undefined ? resolvedUrl : `${resolvedUrl}#${fragment}`;\n}\n\n/**\n * The `id` a fragment names, decoded.\n *\n * A fragment travels PERCENT-ENCODED — `#a%20b`, and every non-ASCII id is\n * encoded by the browser the moment it reaches the address bar — while the\n * `id` attribute in the document holds the decoded characters. Looking up the\n * raw fragment therefore misses every id with a space or a non-Latin letter,\n * which is a silent no-scroll indistinguishable from the bug this fixes.\n *\n * A malformed escape (`#100%`) is NOT an error here: `decodeURIComponent`\n * throws on it, and a throw during a navigation would cost the page for the\n * sake of a scroll. The raw text is returned instead, which is exactly what an\n * `id=\"100%\"` in the document would match.\n */\nexport function fragmentTargetId(fragment: string): string {\n try {\n return decodeURIComponent(fragment);\n } catch {\n return fragment;\n }\n}\n\n/**\n * The fragment of a destination that is THIS page with a fragment on it —\n * `#section`, or the current path written out in full with one appended.\n *\n * `undefined` means \"not that\": a different page, no fragment, or a URL that\n * cannot be resolved at all. The caller navigates as it otherwise would.\n *\n * Such a click must NOT run a client navigation. Re-fetching the page the user\n * is already looking at would throw away its DOM and every piece of state in\n * it — a scrolled container, an open menu, a playing video — to arrive at the\n * same page, and the round trip means the jump does not happen until the\n * network answers. The browser does not do that for a plain `<a href=\"#x\">`\n * and neither do we: the URL is updated and the page scrolls, in that order.\n *\n * The comparison is on origin, path and QUERY: `?page=2#top` from `?page=1#top`\n * is a real navigation to different data that happens to share a fragment.\n *\n * An EMPTY fragment (`/here#`) does not qualify — there is no target to scroll\n * to and nothing to distinguish it from a plain re-navigation to the same page.\n */\nexport function samePageFragment(url: string, currentHref: string): string | undefined {\n let destination: URL;\n let current: URL;\n\n try {\n destination = new URL(url, currentHref);\n current = new URL(currentHref);\n } catch {\n return undefined;\n }\n\n const fragment = destination.hash.slice(1);\n\n if (fragment === \"\") return undefined;\n\n const samePage =\n destination.origin === current.origin &&\n destination.pathname === current.pathname &&\n destination.search === current.search;\n\n return samePage ? fragment : undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,WAAW,KAAiC;CAC1D,MAAM,QAAQ,IAAI,QAAQ,GAAG;CAE7B,OAAO,UAAU,KAAK,SAAY,IAAI,MAAM,QAAQ,CAAC;AACvD;;;;AAKA,SAAgB,gBAAgB,KAAqB;CACnD,MAAM,QAAQ,IAAI,QAAQ,GAAG;CAE7B,OAAO,UAAU,KAAK,MAAM,IAAI,MAAM,GAAG,KAAK;AAChD;;;;;;;;;AAUA,SAAgB,iBAAiB,aAAqB,cAA8B;CAClF,IAAI,WAAW,WAAW,MAAM,QAAW,OAAO;CAElD,MAAM,WAAW,WAAW,YAAY;CAExC,OAAO,aAAa,SAAY,cAAc,GAAG,YAAY,GAAG;AAClE;;;;;;;;;;;;;;;AAgBA,SAAgB,iBAAiB,UAA0B;CACzD,IAAI;EACF,OAAO,mBAAmB,QAAQ;CACpC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,iBAAiB,KAAa,aAAyC;CACrF,IAAI;CACJ,IAAI;CAEJ,IAAI;EACF,cAAc,IAAI,IAAI,KAAK,WAAW;EACtC,UAAU,IAAI,IAAI,WAAW;CAC/B,QAAQ;EACN;CACF;CAEA,MAAM,WAAW,YAAY,KAAK,MAAM,CAAC;CAEzC,IAAI,aAAa,IAAI,OAAO;CAO5B,OAJE,YAAY,WAAW,QAAQ,UAC/B,YAAY,aAAa,QAAQ,YACjC,YAAY,WAAW,QAAQ,SAEf,WAAW;AAC/B"}
1
+ {"version":3,"file":"url-fragment.mjs","names":[],"sources":["../../../../../../../web/src/routing/url-fragment.ts"],"sourcesContent":["/**\n * The URL FRAGMENT, kept as a first-class part of a navigation.\n *\n * ## The defect this file exists for\n *\n * A client navigation asks the server for page data with `fetch`, and a\n * fragment is a CLIENT-SIDE construct: it is never sent, and `response.url` —\n * the URL history is written from, because it reflects any redirect that was\n * followed — therefore never carries one. So `<Link href=\"/docs#install\">`\n * pushed `/docs` and the fragment the author wrote was silently gone from the\n * address bar: not shareable, not bookmarkable, not restorable on reload.\n *\n * Everything here is pure string work over `URL`, deliberately: the navigation\n * runtime is the only place that may touch `window`, and these answers have to\n * be provable in a suite with no browser.\n *\n * ## The redirect rule, which is the browser's rule\n *\n * When a request is redirected and the `Location` carries no fragment of its\n * own, the ORIGINAL fragment is carried onto the destination (RFC 7231 §7.1.2).\n * A `Location` that DOES name one wins outright. {@link withFragmentFrom} is\n * that rule and nothing else, so a client navigation through a redirect lands\n * where a full page load would have landed.\n */\n\n/**\n * The fragment of `url`, WITHOUT its leading `#`.\n *\n * `undefined` means there was no `#` at all, and `\"\"` means there was one with\n * nothing after it. The two are kept apart because they are different requests:\n * `/docs` says nothing about a fragment, `/docs#` says \"no target\" — and only\n * the second should end up written to the address bar as `#`.\n *\n * Plain string work rather than `new URL()`: this is asked of RELATIVE URLs\n * (`/docs#install`, `#install`), which `URL` cannot parse without a base, and\n * the base is `window`'s — not available on the server, and not this module's\n * to reach for.\n */\nexport function fragmentOf(url: string): string | undefined {\n const index = url.indexOf(\"#\");\n\n return index === -1 ? undefined : url.slice(index + 1);\n}\n\n/**\n * `url` with everything from its `#` onward removed.\n */\nexport function withoutFragment(url: string): string {\n const index = url.indexOf(\"#\");\n\n return index === -1 ? url : url.slice(0, index);\n}\n\n/**\n * Carry the fragment the caller ASKED for onto the URL the response came from.\n *\n * The resolved URL wins when it names a fragment itself — a redirect that says\n * `Location: /docs/v5#moved` meant it. Otherwise the requested fragment rides\n * along, which for the overwhelmingly common case (no redirect) simply puts\n * back what `fetch` dropped.\n */\nexport function withFragmentFrom(resolvedUrl: string, requestedUrl: string): string {\n if (fragmentOf(resolvedUrl) !== undefined) return resolvedUrl;\n\n const fragment = fragmentOf(requestedUrl);\n\n return fragment === undefined ? resolvedUrl : `${resolvedUrl}#${fragment}`;\n}\n\n/**\n * The `id` a fragment names, decoded.\n *\n * A fragment travels PERCENT-ENCODED — `#a%20b`, and every non-ASCII id is\n * encoded by the browser the moment it reaches the address bar — while the\n * `id` attribute in the document holds the decoded characters. Looking up the\n * raw fragment therefore misses every id with a space or a non-Latin letter,\n * which is a silent no-scroll indistinguishable from the bug this fixes.\n *\n * A malformed escape (`#100%`) is NOT an error here: `decodeURIComponent`\n * throws on it, and a throw during a navigation would cost the page for the\n * sake of a scroll. The raw text is returned instead, which is exactly what an\n * `id=\"100%\"` in the document would match.\n */\nexport function fragmentTargetId(fragment: string): string {\n try {\n return decodeURIComponent(fragment);\n } catch {\n return fragment;\n }\n}\n\n/**\n * The fragment of a destination that is THIS page with a fragment on it —\n * `#section`, or the current path written out in full with one appended.\n *\n * `undefined` means \"not that\": a different page, no fragment, or a URL that\n * cannot be resolved at all. The caller navigates as it otherwise would.\n *\n * Such a click must NOT run a client navigation. Re-fetching the page the user\n * is already looking at would throw away its DOM and every piece of state in\n * it — a scrolled container, an open menu, a playing video — to arrive at the\n * same page, and the round trip means the jump does not happen until the\n * network answers. The browser does not do that for a plain `<a href=\"#x\">`\n * and neither do we: the URL is updated and the page scrolls, in that order.\n *\n * The comparison is on origin, path and QUERY: `?page=2#top` from `?page=1#top`\n * is a real navigation to different data that happens to share a fragment.\n *\n * An EMPTY fragment (`/here#`) does not qualify — there is no target to scroll\n * to and nothing to distinguish it from a plain re-navigation to the same page.\n */\nexport function samePageFragment(url: string, currentHref: string): string | undefined {\n let destination: URL;\n let current: URL;\n\n try {\n destination = new URL(url, currentHref);\n current = new URL(currentHref);\n } catch {\n return undefined;\n }\n\n const fragment = destination.hash.slice(1);\n\n if (fragment === \"\") return undefined;\n\n const samePage =\n destination.origin === current.origin &&\n destination.pathname === current.pathname &&\n destination.search === current.search;\n\n return samePage ? fragment : undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,WAAW,KAAiC;CAC1D,MAAM,QAAQ,IAAI,QAAQ,GAAG;CAE7B,OAAO,UAAU,KAAK,SAAY,IAAI,MAAM,QAAQ,CAAC;AACvD;;;;AAKA,SAAgB,gBAAgB,KAAqB;CACnD,MAAM,QAAQ,IAAI,QAAQ,GAAG;CAE7B,OAAO,UAAU,KAAK,MAAM,IAAI,MAAM,GAAG,KAAK;AAChD;;;;;;;;;AAUA,SAAgB,iBAAiB,aAAqB,cAA8B;CAClF,IAAI,WAAW,WAAW,MAAM,QAAW,OAAO;CAElD,MAAM,WAAW,WAAW,YAAY;CAExC,OAAO,aAAa,SAAY,cAAc,GAAG,YAAY,GAAG;AAClE;;;;;;;;;;;;;;;AAgBA,SAAgB,iBAAiB,UAA0B;CACzD,IAAI;EACF,OAAO,mBAAmB,QAAQ;CACpC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,iBAAiB,KAAa,aAAyC;CACrF,IAAI;CACJ,IAAI;CAEJ,IAAI;EACF,cAAc,IAAI,IAAI,KAAK,WAAW;EACtC,UAAU,IAAI,IAAI,WAAW;CAC/B,QAAQ;EACN;CACF;CAEA,MAAM,WAAW,YAAY,KAAK,MAAM,CAAC;CAEzC,IAAI,aAAa,IAAI,OAAO;CAO5B,OAJE,YAAY,WAAW,QAAQ,UAC/B,YAAY,aAAa,QAAQ,YACjC,YAAY,WAAW,QAAQ,SAEf,WAAW;AAC/B"}
@@ -1,4 +1,4 @@
1
- //#region web/src/runtime/register-modules.d.ts
1
+ //#region ../web/src/runtime/register-modules.d.ts
2
2
  /**
3
3
  * The universal portion of a page or layout module namespace.
4
4
  *
@@ -1,4 +1,4 @@
1
- //#region web/src/runtime/register-modules.ts
1
+ //#region ../web/src/runtime/register-modules.ts
2
2
  const registeredModules = /* @__PURE__ */ new WeakSet();
3
3
  function isThenable(value) {
4
4
  return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
@@ -1 +1 @@
1
- {"version":3,"file":"register-modules.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/runtime/register-modules.ts"],"sourcesContent":["/**\n * The universal portion of a page or layout module namespace.\n *\n * `register` deliberately has no arguments and must finish synchronously: it\n * runs while both the server and browser evaluate the same module graph.\n */\nexport type RegisterableModuleNamespace = {\n readonly register?: () => unknown;\n};\n\nconst registeredModules = new WeakSet<RegisterableModuleNamespace>();\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n value !== null &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n typeof (value as { then?: unknown }).then === \"function\"\n );\n}\n\n/**\n * Run each module's optional universal registration hook at most once for\n * that module namespace instance. A replacement namespace from HMR naturally\n * has a new identity and is therefore registered again.\n */\nexport function registerModules(modules: readonly RegisterableModuleNamespace[]): void {\n for (const module of modules) {\n if (registeredModules.has(module)) continue;\n\n const result = module.register?.();\n\n if (isThenable(result)) {\n throw new Error(\n \"Warlock register() hooks must be synchronous and must not return a Promise or thenable.\",\n );\n }\n\n // A throwing hook (including one that returned a thenable above) is not\n // recorded, so a later route composition still exposes and can retry it.\n registeredModules.add(module);\n }\n}\n"],"mappings":";AAUA,MAAM,oCAAoB,IAAI,QAAqC;AAEnE,SAAS,WAAW,OAA+C;CACjE,OACE,UAAU,SACT,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,OAAQ,MAA6B,SAAS;AAElD;;;;;;AAOA,SAAgB,gBAAgB,SAAuD;CACrF,KAAK,MAAM,UAAU,SAAS;EAC5B,IAAI,kBAAkB,IAAI,MAAM,GAAG;EAEnC,MAAM,SAAS,OAAO,WAAW;EAEjC,IAAI,WAAW,MAAM,GACnB,MAAM,IAAI,MACR,yFACF;EAKF,kBAAkB,IAAI,MAAM;CAC9B;AACF"}
1
+ {"version":3,"file":"register-modules.mjs","names":[],"sources":["../../../../../../../web/src/runtime/register-modules.ts"],"sourcesContent":["/**\n * The universal portion of a page or layout module namespace.\n *\n * `register` deliberately has no arguments and must finish synchronously: it\n * runs while both the server and browser evaluate the same module graph.\n */\nexport type RegisterableModuleNamespace = {\n readonly register?: () => unknown;\n};\n\nconst registeredModules = new WeakSet<RegisterableModuleNamespace>();\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n value !== null &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n typeof (value as { then?: unknown }).then === \"function\"\n );\n}\n\n/**\n * Run each module's optional universal registration hook at most once for\n * that module namespace instance. A replacement namespace from HMR naturally\n * has a new identity and is therefore registered again.\n */\nexport function registerModules(modules: readonly RegisterableModuleNamespace[]): void {\n for (const module of modules) {\n if (registeredModules.has(module)) continue;\n\n const result = module.register?.();\n\n if (isThenable(result)) {\n throw new Error(\n \"Warlock register() hooks must be synchronous and must not return a Promise or thenable.\",\n );\n }\n\n // A throwing hook (including one that returned a thenable above) is not\n // recorded, so a later route composition still exposes and can retry it.\n registeredModules.add(module);\n }\n}\n"],"mappings":";AAUA,MAAM,oCAAoB,IAAI,QAAqC;AAEnE,SAAS,WAAW,OAA+C;CACjE,OACE,UAAU,SACT,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,OAAQ,MAA6B,SAAS;AAElD;;;;;;AAOA,SAAgB,gBAAgB,SAAuD;CACrF,KAAK,MAAM,UAAU,SAAS;EAC5B,IAAI,kBAAkB,IAAI,MAAM,GAAG;EAEnC,MAAM,SAAS,OAAO,WAAW;EAEjC,IAAI,WAAW,MAAM,GACnB,MAAM,IAAI,MACR,yFACF;EAKF,kBAAkB,IAAI,MAAM;CAC9B;AACF"}
@@ -1,4 +1,4 @@
1
- //#region web/src/server/build-hydration-payload.ts
1
+ //#region ../web/src/server/build-hydration-payload.ts
2
2
  /**
3
3
  * Levels without a loader resolve to `undefined`, but the hydration contract
4
4
  * requires every data key to be PRESENT.
@@ -1 +1 @@
1
- {"version":3,"file":"build-hydration-payload.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/build-hydration-payload.ts"],"sourcesContent":["/**\n * The ONE place the hydration payload's shape is decided.\n *\n * Two callers need the identical object and must never drift:\n *\n * `render-page.ts` embeds it in the document as `#__WARLOCK_DATA__`, which\n * is what a FULL page load hands the browser.\n * the `_loader` route returns it as JSON, which is what a CLIENT navigation\n * fetches instead of re-rendering the document.\n *\n * Drift between those two is not a cosmetic bug: the browser builds the same\n * React tree from either source, so a key present on one path and absent on the\n * other produces a page that works when you land on it and breaks when you\n * navigate to it — or the reverse, which is worse, because the first visit is\n * the one everybody tests.\n *\n * Extracted rather than duplicated for exactly that reason. It was previously\n * assembled inline inside the renderer, where the loader route could not reach\n * it without copying five lines that would then be free to diverge.\n */\nimport type { HydrationDocumentPayloadSource } from \"../components/document-context\";\nimport type { PageDataBundle } from \"./execute-page-request\";\n\n/**\n * Levels without a loader resolve to `undefined`, but the hydration contract\n * requires every data key to be PRESENT.\n *\n * An intentional `null` is preserved — a loader that returned `null` said\n * something, and flattening it would erase that. Only \"no data at all\" becomes\n * an empty object.\n */\nfunction serializableData(data: unknown): unknown {\n return data === undefined ? {} : data;\n}\n\nexport function buildHydrationPayload(bundle: PageDataBundle): HydrationDocumentPayloadSource {\n return {\n appData: serializableData(bundle.appData),\n layoutData: serializableData(bundle.layoutData),\n pageData: serializableData(bundle.pageData),\n shared: serializableData(bundle.shared),\n // The server's own match, carried for the same reason `name` is: the params\n // are an ANSWER the router already gave, and re-deriving them in the\n // browser from `location.pathname` would be a second matcher disagreeing\n // with the server about the request it is hydrating. `{}` for a route with\n // no dynamic segments — a real answer, not a missing one.\n params: bundle.route.params,\n // Spread, so \"the page produced no metadata\" is the SAME shape here and on\n // the wire. `metadata: undefined` would be a key in the in-process object\n // and no key at all after `JSON.stringify` — one type, two payload shapes,\n // which is precisely the drift this file exists to prevent. Carried whole:\n // `<Head/>` renders every member of `MetadataOutput`, so anything narrowed\n // out here is a tag the first request has and a navigation does not.\n ...(bundle.metadata === undefined ? {} : { metadata: bundle.metadata }),\n ...(bundle.errorPage === undefined ? {} : { errorPage: bundle.errorPage }),\n // The matched entry's own name, carried untransformed from stage 1\n // (`bundle.route.name` IS `matched.entry.name`, execute-page-request.ts).\n // The browser reads it to look up the page the server resolved rather than\n // re-matching the pathname — a second matcher can disagree with the server\n // about the very request it is hydrating, and on a client navigation it\n // would be disagreeing about a request the server already answered.\n name: bundle.route.name,\n };\n}\n"],"mappings":";;;;;;;;;AA+BA,SAAS,iBAAiB,MAAwB;CAChD,OAAO,SAAS,SAAY,CAAC,IAAI;AACnC;AAEA,SAAgB,sBAAsB,QAAwD;CAC5F,OAAO;EACL,SAAS,iBAAiB,OAAO,OAAO;EACxC,YAAY,iBAAiB,OAAO,UAAU;EAC9C,UAAU,iBAAiB,OAAO,QAAQ;EAC1C,QAAQ,iBAAiB,OAAO,MAAM;EAMtC,QAAQ,OAAO,MAAM;EAOrB,GAAI,OAAO,aAAa,SAAY,CAAC,IAAI,EAAE,UAAU,OAAO,SAAS;EACrE,GAAI,OAAO,cAAc,SAAY,CAAC,IAAI,EAAE,WAAW,OAAO,UAAU;EAOxE,MAAM,OAAO,MAAM;CACrB;AACF"}
1
+ {"version":3,"file":"build-hydration-payload.mjs","names":[],"sources":["../../../../../../../web/src/server/build-hydration-payload.ts"],"sourcesContent":["/**\n * The ONE place the hydration payload's shape is decided.\n *\n * Two callers need the identical object and must never drift:\n *\n * `render-page.ts` embeds it in the document as `#__WARLOCK_DATA__`, which\n * is what a FULL page load hands the browser.\n * the `_loader` route returns it as JSON, which is what a CLIENT navigation\n * fetches instead of re-rendering the document.\n *\n * Drift between those two is not a cosmetic bug: the browser builds the same\n * React tree from either source, so a key present on one path and absent on the\n * other produces a page that works when you land on it and breaks when you\n * navigate to it — or the reverse, which is worse, because the first visit is\n * the one everybody tests.\n *\n * Extracted rather than duplicated for exactly that reason. It was previously\n * assembled inline inside the renderer, where the loader route could not reach\n * it without copying five lines that would then be free to diverge.\n */\nimport type { HydrationDocumentPayloadSource } from \"../components/document-context\";\nimport type { PageDataBundle } from \"./execute-page-request\";\n\n/**\n * Levels without a loader resolve to `undefined`, but the hydration contract\n * requires every data key to be PRESENT.\n *\n * An intentional `null` is preserved — a loader that returned `null` said\n * something, and flattening it would erase that. Only \"no data at all\" becomes\n * an empty object.\n */\nfunction serializableData(data: unknown): unknown {\n return data === undefined ? {} : data;\n}\n\nexport function buildHydrationPayload(bundle: PageDataBundle): HydrationDocumentPayloadSource {\n return {\n appData: serializableData(bundle.appData),\n layoutData: serializableData(bundle.layoutData),\n pageData: serializableData(bundle.pageData),\n shared: serializableData(bundle.shared),\n // The server's own match, carried for the same reason `name` is: the params\n // are an ANSWER the router already gave, and re-deriving them in the\n // browser from `location.pathname` would be a second matcher disagreeing\n // with the server about the request it is hydrating. `{}` for a route with\n // no dynamic segments — a real answer, not a missing one.\n params: bundle.route.params,\n // Spread, so \"the page produced no metadata\" is the SAME shape here and on\n // the wire. `metadata: undefined` would be a key in the in-process object\n // and no key at all after `JSON.stringify` — one type, two payload shapes,\n // which is precisely the drift this file exists to prevent. Carried whole:\n // `<Head/>` renders every member of `MetadataOutput`, so anything narrowed\n // out here is a tag the first request has and a navigation does not.\n ...(bundle.metadata === undefined ? {} : { metadata: bundle.metadata }),\n ...(bundle.errorPage === undefined ? {} : { errorPage: bundle.errorPage }),\n // The matched entry's own name, carried untransformed from stage 1\n // (`bundle.route.name` IS `matched.entry.name`, execute-page-request.ts).\n // The browser reads it to look up the page the server resolved rather than\n // re-matching the pathname — a second matcher can disagree with the server\n // about the very request it is hydrating, and on a client navigation it\n // would be disagreeing about a request the server already answered.\n name: bundle.route.name,\n };\n}\n"],"mappings":";;;;;;;;;AA+BA,SAAS,iBAAiB,MAAwB;CAChD,OAAO,SAAS,SAAY,CAAC,IAAI;AACnC;AAEA,SAAgB,sBAAsB,QAAwD;CAC5F,OAAO;EACL,SAAS,iBAAiB,OAAO,OAAO;EACxC,YAAY,iBAAiB,OAAO,UAAU;EAC9C,UAAU,iBAAiB,OAAO,QAAQ;EAC1C,QAAQ,iBAAiB,OAAO,MAAM;EAMtC,QAAQ,OAAO,MAAM;EAOrB,GAAI,OAAO,aAAa,SAAY,CAAC,IAAI,EAAE,UAAU,OAAO,SAAS;EACrE,GAAI,OAAO,cAAc,SAAY,CAAC,IAAI,EAAE,WAAW,OAAO,UAAU;EAOxE,MAAM,OAAO,MAAM;CACrB;AACF"}
@@ -1,4 +1,4 @@
1
- //#region web/src/server/client-asset-url-prefix.d.ts
1
+ //#region ../web/src/server/client-asset-url-prefix.d.ts
2
2
  /**
3
3
  * The SINGLE authority for where built client assets are served.
4
4
  *
@@ -1,4 +1,4 @@
1
- //#region web/src/server/client-asset-url-prefix.ts
1
+ //#region ../web/src/server/client-asset-url-prefix.ts
2
2
  /**
3
3
  * The SINGLE authority for where built client assets are served.
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"client-asset-url-prefix.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/client-asset-url-prefix.ts"],"sourcesContent":["/**\n * The SINGLE authority for where built client assets are served.\n *\n * Two halves must agree and only one of them may own the literal:\n * `resolveHydrationClientUrl` GUARANTEES every URL it returns starts with this\n * prefix, and the production static-file route MUST mount `<clientDir>/assets`\n * at exactly this prefix by importing THIS symbol. A second `\"/assets\"`\n * literal anywhere else is a drift bug: the two copies can be edited apart,\n * and the failure mode is a silent 404 on the hydration script — the page\n * renders, never hydrates, and nothing throws.\n *\n * WHY THE `assets` SUBDIRECTORY AND NOT THE CLIENT ROOT: the client build dir\n * also contains `.vite/manifest.json`, the build→runtime handoff artifact,\n * which must NEVER be publicly served. Mounting the client root would expose\n * it. Mounting only the `assets` subdirectory keeps the manifest unreachable\n * while still serving every hashed artifact the manifest points at.\n */\nexport const CLIENT_ASSET_URL_PREFIX = \"/assets\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,MAAa,0BAA0B"}
1
+ {"version":3,"file":"client-asset-url-prefix.mjs","names":[],"sources":["../../../../../../../web/src/server/client-asset-url-prefix.ts"],"sourcesContent":["/**\n * The SINGLE authority for where built client assets are served.\n *\n * Two halves must agree and only one of them may own the literal:\n * `resolveHydrationClientUrl` GUARANTEES every URL it returns starts with this\n * prefix, and the production static-file route MUST mount `<clientDir>/assets`\n * at exactly this prefix by importing THIS symbol. A second `\"/assets\"`\n * literal anywhere else is a drift bug: the two copies can be edited apart,\n * and the failure mode is a silent 404 on the hydration script — the page\n * renders, never hydrates, and nothing throws.\n *\n * WHY THE `assets` SUBDIRECTORY AND NOT THE CLIENT ROOT: the client build dir\n * also contains `.vite/manifest.json`, the build→runtime handoff artifact,\n * which must NEVER be publicly served. Mounting the client root would expose\n * it. Mounting only the `assets` subdirectory keeps the manifest unreachable\n * while still serving every hashed artifact the manifest points at.\n */\nexport const CLIENT_ASSET_URL_PREFIX = \"/assets\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,MAAa,0BAA0B"}
@@ -1,7 +1,7 @@
1
1
  import { PageManifest } from "./page-manifest.mjs";
2
2
  import { PageModuleLoader } from "./create-page-route-handler.mjs";
3
3
 
4
- //#region web/src/server/create-page-module-loader.d.ts
4
+ //#region ../web/src/server/create-page-module-loader.d.ts
5
5
  /**
6
6
  * Asked for a module this build does not contain.
7
7
  *
@@ -1,4 +1,4 @@
1
- //#region web/src/server/create-page-module-loader.ts
1
+ //#region ../web/src/server/create-page-module-loader.ts
2
2
  /**
3
3
  * Asked for a module this build does not contain.
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"create-page-module-loader.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/create-page-module-loader.ts"],"sourcesContent":["/**\n * The production answer to \"how do I load a page module\".\n *\n * `createPageRouteHandler` takes loading a module as an INPUT\n * ({@link PageModuleLoader}) precisely so the two runtimes can answer it\n * differently. Dev hands it `moduleId => vite.ssrLoadModule(moduleId)`;\n * production has no Vite and nothing to evaluate — the generated `pages.ts`\n * barrel already imported every page, layout and the app root statically, and\n * {@link PageManifest} is the table those namespace objects arrived in. So the\n * production loader does not load anything: it is a LOOKUP over modules that\n * are already in memory, and this file is that lookup and nothing else.\n */\nimport type { PageModuleLoader } from \"./create-page-route-handler\";\nimport type { PageManifest } from \"./page-manifest\";\n\n/**\n * Asked for a module this build does not contain.\n *\n * Hard failure, deliberately. Every alternative — returning `undefined`, an\n * empty namespace, or falling back to a dynamic import — turns a build that\n * shipped the wrong module table into a page that renders blank, or into a\n * production process reaching for source files that are not deployed. The id\n * is in the message because the whole diagnosis is \"which id, and why is it\n * not in the table\"; the count is there because zero entries means the build\n * discovered no pages at all, which is a different fault from a mismatch.\n */\nexport class PageModuleNotInManifestError extends Error {\n public constructor(moduleId: string, knownIdCount: number) {\n super(\n `Cannot serve a page module: \"${moduleId}\" is not in this build's page manifest ` +\n `(${knownIdCount} module(s) available). Module ids are the manifest's \\`sourceFile\\` ` +\n \"values — app-root-relative POSIX paths, extension included — and must match exactly. \" +\n \"Re-run `warlock build` so the generated `pages.ts` barrel matches what is being served.\",\n );\n this.name = \"PageModuleNotInManifestError\";\n }\n}\n\n/**\n * Build the loader for ONE manifest.\n *\n * Ids are compared by exact string equality against the `sourceFile` each\n * entry carries — no lowercasing, no separator swapping, no extension\n * stripping. Both sides of that comparison come from the same generator\n * (`generatePagesBarrel` writes every `sourceFile` through one\n * app-root-relative POSIX derivation), so any spelling difference is a real\n * disagreement about which file is meant, and a normalizer would only hide it.\n *\n * A layout shared by several pages appears once per chain it belongs to; the\n * generator emits one identifier per layout file, so every occurrence of an id\n * carries the same namespace object and re-registering it is a no-op.\n */\nexport function createPageModuleLoader(manifest: PageManifest): PageModuleLoader {\n const modulesById = new Map<string, Record<string, unknown>>();\n\n if (manifest.app) {\n modulesById.set(manifest.app.sourceFile, manifest.app.module);\n }\n\n if (manifest.errorPage) {\n modulesById.set(manifest.errorPage.sourceFile, manifest.errorPage.module);\n }\n\n for (const page of manifest.pages) {\n modulesById.set(page.sourceFile, page.module);\n\n for (const layout of page.layouts) {\n modulesById.set(layout.sourceFile, layout.module);\n }\n }\n\n // `async` so an unknown id surfaces as a rejection rather than a synchronous\n // throw: the handler awaits every module through one `Promise.all`, and a\n // loader that fails both ways depending on the reason is a loader callers\n // have to guard twice.\n return async (moduleId: string) => {\n const module = modulesById.get(moduleId);\n\n if (module === undefined) {\n throw new PageModuleNotInManifestError(moduleId, modulesById.size);\n }\n\n return module;\n };\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,IAAa,+BAAb,cAAkD,MAAM;CACtD,AAAO,YAAY,UAAkB,cAAsB;EACzD,MACE,gCAAgC,SAAS,0CACnC,aAAa,qPAGrB;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;AAgBA,SAAgB,uBAAuB,UAA0C;CAC/E,MAAM,8BAAc,IAAI,IAAqC;CAE7D,IAAI,SAAS,KACX,YAAY,IAAI,SAAS,IAAI,YAAY,SAAS,IAAI,MAAM;CAG9D,IAAI,SAAS,WACX,YAAY,IAAI,SAAS,UAAU,YAAY,SAAS,UAAU,MAAM;CAG1E,KAAK,MAAM,QAAQ,SAAS,OAAO;EACjC,YAAY,IAAI,KAAK,YAAY,KAAK,MAAM;EAE5C,KAAK,MAAM,UAAU,KAAK,SACxB,YAAY,IAAI,OAAO,YAAY,OAAO,MAAM;CAEpD;CAMA,OAAO,OAAO,aAAqB;EACjC,MAAM,SAAS,YAAY,IAAI,QAAQ;EAEvC,IAAI,WAAW,QACb,MAAM,IAAI,6BAA6B,UAAU,YAAY,IAAI;EAGnE,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"create-page-module-loader.mjs","names":[],"sources":["../../../../../../../web/src/server/create-page-module-loader.ts"],"sourcesContent":["/**\n * The production answer to \"how do I load a page module\".\n *\n * `createPageRouteHandler` takes loading a module as an INPUT\n * ({@link PageModuleLoader}) precisely so the two runtimes can answer it\n * differently. Dev hands it `moduleId => vite.ssrLoadModule(moduleId)`;\n * production has no Vite and nothing to evaluate — the generated `pages.ts`\n * barrel already imported every page, layout and the app root statically, and\n * {@link PageManifest} is the table those namespace objects arrived in. So the\n * production loader does not load anything: it is a LOOKUP over modules that\n * are already in memory, and this file is that lookup and nothing else.\n */\nimport type { PageModuleLoader } from \"./create-page-route-handler\";\nimport type { PageManifest } from \"./page-manifest\";\n\n/**\n * Asked for a module this build does not contain.\n *\n * Hard failure, deliberately. Every alternative — returning `undefined`, an\n * empty namespace, or falling back to a dynamic import — turns a build that\n * shipped the wrong module table into a page that renders blank, or into a\n * production process reaching for source files that are not deployed. The id\n * is in the message because the whole diagnosis is \"which id, and why is it\n * not in the table\"; the count is there because zero entries means the build\n * discovered no pages at all, which is a different fault from a mismatch.\n */\nexport class PageModuleNotInManifestError extends Error {\n public constructor(moduleId: string, knownIdCount: number) {\n super(\n `Cannot serve a page module: \"${moduleId}\" is not in this build's page manifest ` +\n `(${knownIdCount} module(s) available). Module ids are the manifest's \\`sourceFile\\` ` +\n \"values — app-root-relative POSIX paths, extension included — and must match exactly. \" +\n \"Re-run `warlock build` so the generated `pages.ts` barrel matches what is being served.\",\n );\n this.name = \"PageModuleNotInManifestError\";\n }\n}\n\n/**\n * Build the loader for ONE manifest.\n *\n * Ids are compared by exact string equality against the `sourceFile` each\n * entry carries — no lowercasing, no separator swapping, no extension\n * stripping. Both sides of that comparison come from the same generator\n * (`generatePagesBarrel` writes every `sourceFile` through one\n * app-root-relative POSIX derivation), so any spelling difference is a real\n * disagreement about which file is meant, and a normalizer would only hide it.\n *\n * A layout shared by several pages appears once per chain it belongs to; the\n * generator emits one identifier per layout file, so every occurrence of an id\n * carries the same namespace object and re-registering it is a no-op.\n */\nexport function createPageModuleLoader(manifest: PageManifest): PageModuleLoader {\n const modulesById = new Map<string, Record<string, unknown>>();\n\n if (manifest.app) {\n modulesById.set(manifest.app.sourceFile, manifest.app.module);\n }\n\n if (manifest.errorPage) {\n modulesById.set(manifest.errorPage.sourceFile, manifest.errorPage.module);\n }\n\n for (const page of manifest.pages) {\n modulesById.set(page.sourceFile, page.module);\n\n for (const layout of page.layouts) {\n modulesById.set(layout.sourceFile, layout.module);\n }\n }\n\n // `async` so an unknown id surfaces as a rejection rather than a synchronous\n // throw: the handler awaits every module through one `Promise.all`, and a\n // loader that fails both ways depending on the reason is a loader callers\n // have to guard twice.\n return async (moduleId: string) => {\n const module = modulesById.get(moduleId);\n\n if (module === undefined) {\n throw new PageModuleNotInManifestError(moduleId, modulesById.size);\n }\n\n return module;\n };\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,IAAa,+BAAb,cAAkD,MAAM;CACtD,AAAO,YAAY,UAAkB,cAAsB;EACzD,MACE,gCAAgC,SAAS,0CACnC,aAAa,qPAGrB;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;AAgBA,SAAgB,uBAAuB,UAA0C;CAC/E,MAAM,8BAAc,IAAI,IAAqC;CAE7D,IAAI,SAAS,KACX,YAAY,IAAI,SAAS,IAAI,YAAY,SAAS,IAAI,MAAM;CAG9D,IAAI,SAAS,WACX,YAAY,IAAI,SAAS,UAAU,YAAY,SAAS,UAAU,MAAM;CAG1E,KAAK,MAAM,QAAQ,SAAS,OAAO;EACjC,YAAY,IAAI,KAAK,YAAY,KAAK,MAAM;EAE5C,KAAK,MAAM,UAAU,KAAK,SACxB,YAAY,IAAI,OAAO,YAAY,OAAO,MAAM;CAEpD;CAMA,OAAO,OAAO,aAAqB;EACjC,MAAM,SAAS,YAAY,IAAI,QAAQ;EAEvC,IAAI,WAAW,QACb,MAAM,IAAI,6BAA6B,UAAU,YAAY,IAAI;EAGnE,OAAO;CACT;AACF"}
@@ -3,7 +3,7 @@ import { BufferedCookie } from "./settle-page-response.mjs";
3
3
  import { ErrorPageModuleLoader } from "./error-page.mjs";
4
4
  import { HttpContext, Response } from "@warlock.js/core";
5
5
 
6
- //#region web/src/server/create-page-route-handler.d.ts
6
+ //#region ../web/src/server/create-page-route-handler.d.ts
7
7
  /**
8
8
  * How the handler obtains a page/layout/app module, by the same id
9
9
  * (`appFile`/`layoutFile`/`pageFile`) the caller registered it under. In dev
@@ -5,7 +5,7 @@ import { buildHydrationPayload } from "./build-hydration-payload.mjs";
5
5
  import { renderPageFailure, renderPageRequest } from "./render-page.mjs";
6
6
  import { Response } from "@warlock.js/core";
7
7
 
8
- //#region web/src/server/create-page-route-handler.ts
8
+ //#region ../web/src/server/create-page-route-handler.ts
9
9
  /**
10
10
  * The page handler, as a named seam.
11
11
  *
@@ -1 +1 @@
1
- {"version":3,"file":"create-page-route-handler.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/create-page-route-handler.ts"],"sourcesContent":["/**\r\n * The page handler, as a named seam.\r\n *\r\n * This is the request handler `installPageRoutes` used to inline into its\r\n * `router.get(...)` call (`install-page-routes.ts:236-275` before this\r\n * extraction; the pre-extraction copy is `scratchpad/install-page-routes.ts.orig`).\r\n * The behaviour is unchanged, byte for byte — what changes is that it is now\r\n * a named, exported, independently constructible function instead of a closure\r\n * over eight ambient bindings of `installPageRoutes`.\r\n *\r\n * WHY IT TAKES `loadModule` AND NOT A `ViteDevServer`: loading a module is the\r\n * only capability the handler ever needed, and the two runtimes answer it\r\n * differently — dev goes through Vite's SSR graph\r\n * (`vite.ssrLoadModule`, `install-page-routes.ts:207`), production reads the\r\n * already-built page manifest (`page-manifest.ts`). Taking \"how to load a\r\n * module\" as an INPUT is what lets the same handler serve both, and what lets\r\n * a test construct it with a plain async function — no Vite, no dev server, no\r\n * `app/` directory on disk.\r\n *\r\n * Scope: this file creates a seam and nothing else. It does not implement\r\n * `type: \"page\"` routing, HTML error pages, or any other new capability.\r\n */\r\nimport { Response, type HttpContext } from \"@warlock.js/core\";\r\n\r\nimport {\r\n DATA_RESPONSE_CONTENT_TYPE,\r\n isDataRequest,\r\n WARLOCK_DATA_REQUEST_HEADER,\r\n} from \"../routing/data-request\";\r\nimport {\r\n registerModules,\r\n type RegisterableModuleNamespace,\r\n} from \"../runtime/register-modules\";\r\nimport { buildHydrationPayload } from \"./build-hydration-payload\";\r\nimport type { BufferedCookie, PageRouteEntry, PageTripleModule } from \"./execute-page-request\";\r\nimport { isNonHydrating } from \"./page-render-bundle\";\r\nimport { renderPageFailure, renderPageRequest, type RenderedPage } from \"./render-page\";\r\nimport type { ErrorPageModuleLoader } from \"./error-page\";\r\n\r\n/**\r\n * Replay ONE committed cookie through core's own `Response.cookie()` — the\r\n * same serializer every ordinary controller's cookie goes through, so there\r\n * is nothing here for a second implementation to drift from. The one-liner\r\n * `dev-server.ts` wires as the production default; passed in (`applyBufferedCookie`\r\n * option, below) rather than imported so this file stays free of anything\r\n * Vite-shaped.\r\n */\r\nfunction defaultApplyBufferedCookie(response: Response, cookie: BufferedCookie): void {\r\n response.cookie(cookie.name, cookie.value as never, cookie.options ?? {});\r\n}\r\n\r\n/**\r\n * Stage 10a — apply the stage 7 commit (headers, then cookies) to the LIVE\r\n * response, once, before either terminal write (10b: `html()` or `send()`).\r\n * Both the document and data representations of a page route go through this\r\n * so a client navigation never drops a `Set-Cookie` a full load would have\r\n * kept (`create-page-route-handler.spec.ts` — \"applies committed cookies and\r\n * headers exactly as the document path does\").\r\n */\r\nfunction applyCommit(\r\n response: Response,\r\n rendered: Pick<RenderedPage, \"headers\" | \"cookies\">,\r\n applyBufferedCookie: (response: Response, cookie: BufferedCookie) => void,\r\n): void {\r\n response.headers(rendered.headers ?? {});\r\n\r\n for (const cookie of rendered.cookies ?? []) {\r\n applyBufferedCookie(response, cookie);\r\n }\r\n}\r\n\r\n/**\r\n * How the handler obtains a page/layout/app module, by the same id\r\n * (`appFile`/`layoutFile`/`pageFile`) the caller registered it under. In dev\r\n * this is `moduleId => vite.ssrLoadModule(moduleId)`; the connector already\r\n * owns the dev/prod split, so the handler never learns which one it got.\r\n */\r\nexport type PageModuleLoader = (moduleId: string) => Promise<unknown>;\r\n\r\nexport type PageRouteHandlerOptions = {\r\n /** The composed, registered route path — `composeRoutePath`'s output. */\r\n path: string;\r\n /** The resolved route name; shared namespace with API routes. */\r\n name: string;\r\n /** The single global app-root file, e.g. `<appSrcRoot>/web/root.tsx`. */\r\n appFile: string;\r\n /** The page module's id. */\r\n pageFile: string;\r\n /** The page's own-directory `layout.tsx`, when it has one. */\r\n layoutFile?: string | undefined;\r\n loadModule: PageModuleLoader;\r\n /** Optional lazy application `error.page.tsx` loader. Never called on success. */\r\n loadErrorPage?: ErrorPageModuleLoader;\r\n /**\r\n * Load the REAL layout module namespaces, outermost first, for universal\r\n * registration. This stays separate from `loadModule(layoutFile)` because\r\n * dev may answer that id with a synthetic wrapper whose middleware is the\r\n * composition of several layouts. That wrapper is a render-pipeline detail,\r\n * not a module identity, and must never enter `registerModules`' WeakSet.\r\n */\r\n loadRegistrationLayouts?: () => Promise<readonly RegisterableModuleNamespace[]>;\r\n /** Browser module appended after the server-rendered document. */\r\n hydrationClientModuleUrl?: string;\r\n /**\r\n * Stylesheet URLs for this page, emitted into `<head>` so the FIRST paint is\r\n * styled. Absent or empty means the application has no CSS — it never means\r\n * a stylesheet failed to resolve, which is the build's job to report.\r\n */\r\n stylesheetUrls?: readonly string[];\r\n /** Same helper `dev-server.ts` exports — passed in, never imported. */\r\n /**\r\n * The pattern stage 1 matches `request.path` against, when it differs from\r\n * the REGISTERED path. Defaults to `path`, which is right for every route\r\n * whose URL is its own.\r\n *\r\n * Exactly one route needs it: the not-found page, registered on the catch-all\r\n * `*`. `matchRoute` compares segment by segment (`./match-page-route.ts`) and\r\n * has no wildcard token, so a route registered as `*` matches NOTHING — the\r\n * pipeline reports no match and `renderPageRequest` answers `{ html: \"\",\r\n * status: 404 }`. Correct status, empty document: a 404 page that never\r\n * renders its own body. Handing it `requestPath => requestPath` makes the\r\n * requested URL the route's pattern for that one request, so the match is\r\n * trivially true and the page renders for the URL the visitor actually asked\r\n * for.\r\n */\r\n matchPath?: (requestPath: string) => string;\r\n /**\r\n * The status this route answers with when the pipeline settles on a plain\r\n * `200` — the not-found route's `404`, and nothing else uses it.\r\n *\r\n * Applied ONLY to `200`, never as a blanket override: a `200` from this\r\n * pipeline means \"the document rendered and nobody objected\", which for this\r\n * route is precisely the not-found case. Any other settled status is a real\r\n * outcome that the page or the boundary decided — a 500 from a failed render,\r\n * a redirect — and overwriting it would report a broken page as a missing one.\r\n */\r\n statusForRenderedOk?: number;\n /**\n * Exclude the page module's loader from the request triple while preserving\n * the real namespace for `register()` and rendering. Used only by the\n * catch-all 404 page: a missing URL must not run application data work or\n * turn a simple miss into a second failure path.\n */\n skipPageLoader?: boolean;\n /**\r\n * Replays one committed cookie through core's `Response.cookie()`. Defaults\r\n * to doing exactly that (`defaultApplyBufferedCookie`, above); injectable so\r\n * a caller with a different `Response` shape (or a test) can observe/replace\r\n * the call.\r\n */\r\n applyBufferedCookie?: (response: Response, cookie: BufferedCookie) => void;\r\n};\r\n\r\nexport type PageRouteHandler = (context: HttpContext) => Promise<void | Response>;\r\n\r\nfunction escapeHtmlAttribute(value: string): string {\r\n return value.replace(/[&<>\"']/g, (character) => {\r\n switch (character) {\r\n case \"&\":\r\n return \"&amp;\";\r\n case \"<\":\r\n return \"&lt;\";\r\n case \">\":\r\n return \"&gt;\";\r\n case '\"':\r\n return \"&quot;\";\r\n default:\r\n return \"&#39;\";\r\n }\r\n });\r\n}\r\n\r\nfunction installHydrationClientModule(\r\n html: string,\r\n moduleUrl: string | undefined,\r\n nonce: string | undefined,\r\n): string {\r\n if (moduleUrl === undefined || html === \"\") return html;\r\n\r\n const closingBodyIndex = html.lastIndexOf(\"</body>\");\r\n if (closingBodyIndex === -1) {\r\n throw new Error(\r\n \"installPageRoutes: cannot install the hydration client module because the rendered document has no closing </body> tag.\",\r\n );\r\n }\r\n\r\n const nonceAttribute = nonce === undefined ? \"\" : ` nonce=\"${escapeHtmlAttribute(nonce)}\"`;\r\n const script = `<script type=\"module\"${nonceAttribute} src=\"${escapeHtmlAttribute(moduleUrl)}\"></script>`;\r\n return `${html.slice(0, closingBodyIndex)}${script}${html.slice(closingBodyIndex)}`;\r\n}\r\n\r\n/**\r\n * Put the page's stylesheets in `<head>`, so the first paint is styled.\r\n *\r\n * Without this the document carries no CSS at all. The stylesheet reaches the\r\n * browser only because the CLIENT bundle imports it, which means it is applied\r\n * by JavaScript after the module graph loads — the page renders unstyled first\r\n * and restyles a moment later. Correct markup, wrong-looking page, and nothing\r\n * in the console to explain it.\r\n *\r\n * A `<link>` in `<head>` is render-blocking, which is exactly what is wanted\r\n * here: the browser holds the first paint until the CSS is in, so there is no\r\n * flash rather than a faster ugly one.\r\n *\r\n * Inserted before `</head>` rather than after `<head>` so an application's own\r\n * `<link>`/`<style>` in the root document still comes FIRST and can be\r\n * overridden by these — matching how the framework's tags are documented to\r\n * behave, and keeping cascade order predictable.\r\n */\r\nfunction installStylesheets(html: string, stylesheetUrls: readonly string[]): string {\r\n if (stylesheetUrls.length === 0 || html === \"\") return html;\r\n\r\n const closingHeadIndex = html.lastIndexOf(\"</head>\");\r\n\r\n // No `<head>` is not an error the way a missing `</body>` is: a root that\r\n // renders no head is unusual but legal, and losing the stylesheet is a\r\n // cosmetic failure where losing hydration is a broken page. Silently\r\n // dropping it would be the wrong trade the other way, though — so the\r\n // document is left exactly as rendered and the caller's own missing-`</body>`\r\n // check remains the loud one.\r\n if (closingHeadIndex === -1) return html;\r\n\r\n const links = stylesheetUrls\r\n .map((url) => `<link rel=\"stylesheet\" href=\"${escapeHtmlAttribute(url)}\">`)\r\n .join(\"\");\r\n\r\n return `${html.slice(0, closingHeadIndex)}${links}${html.slice(closingHeadIndex)}`;\r\n}\r\n\r\n/**\r\n * Build the handler for ONE page route. Per request it loads the App + layout\r\n * + page triple (concurrently, in that order), renders the URL through\r\n * `renderPageRequest`, splices in the hydration module, and flushes the\r\n * document.\r\n *\r\n * No try/catch, deliberately: loader/render throws are already absorbed by the\r\n * pipeline's boundary machinery inside `renderPageRequest`, and anything that\r\n * escapes (a module-load or register failure, the missing-`</body>` throw\r\n * above) belongs to the router's error path — which is exactly where it went\r\n * before.\r\n */\r\nexport function createPageRouteHandler(options: PageRouteHandlerOptions): PageRouteHandler {\r\n const {\r\n path,\r\n name,\r\n appFile,\r\n pageFile,\r\n layoutFile,\r\n loadModule,\r\n loadErrorPage,\r\n loadRegistrationLayouts,\r\n hydrationClientModuleUrl,\r\n stylesheetUrls,\r\n matchPath,\n statusForRenderedOk,\n skipPageLoader = false,\n applyBufferedCookie = defaultApplyBufferedCookie,\n } = options;\n\r\n return async ({ request, response }: HttpContext) => {\r\n const wantsData = isDataRequest(request.header(WARLOCK_DATA_REQUEST_HEADER, undefined));\r\n\r\n try {\r\n const [appModule, layoutModule, ownPageModule, registrationLayouts] = await Promise.all([\r\n loadModule(appFile),\r\n layoutFile ? loadModule(layoutFile) : Promise.resolve({}),\r\n loadModule(pageFile),\r\n loadRegistrationLayouts?.() ?? Promise.resolve([]),\r\n ]);\r\n\r\n // Registration is the first lifecycle action after all module namespaces\r\n // have loaded and before `renderPageRequest` can run middleware, loaders or\r\n // render. App/page are already their real namespaces. Layouts deliberately\r\n // come from the separate raw chain above, never from `layoutModule`, which\r\n // may be the synthetic composed middleware wrapper used by dev.\r\n registerModules([\r\n appModule as RegisterableModuleNamespace,\r\n ...registrationLayouts,\r\n ownPageModule as RegisterableModuleNamespace,\r\n ]);\r\n\r\n const pageModule = ownPageModule as PageTripleModule;\n const triple: PageRouteEntry[\"triple\"] = {\n app: appModule as PageTripleModule,\n layout: layoutModule as PageTripleModule,\n // Registration above deliberately receives the REAL namespace. Only the\n // pipeline view is projected: spreading preserves the component,\n // metadata, middleware and boundary exports while making a custom 404's\n // loader uncallable.\n page: skipPageLoader\n ? {\n ...pageModule,\n // Vite and native ESM loaders hand us module namespace objects,\n // whose export descriptors are not an object-spread contract.\n // Keep the rendering export explicitly while hiding only loader.\n default: pageModule.default,\n loader: undefined,\n }\n : pageModule,\n };\n\r\n const requestUrl = request.path;\n const [requestPathname] = requestUrl.split(\"?\");\n const routes: PageRouteEntry[] = [\n { path: matchPath === undefined ? path : matchPath(requestPathname), name, triple },\n ];\n\r\n // A DATA request runs everything above and below this line identically —\r\n // it is the same route, the same match and the same pipeline — and differs\r\n // only in what gets written at the end. Decided here, before the render, so\r\n // the branch is visibly about REPRESENTATION and not about behaviour.\r\n const rendered = await renderPageRequest(requestUrl, {\n routes,\r\n createHttp: () => ({ request, response }),\r\n loadErrorPage,\r\n });\r\n\r\n if (rendered instanceof Response) return rendered;\r\n\r\n // See `statusForRenderedOk`: a settled 200 is the only status this route is\r\n // allowed to restate, and both the document and the data branch below must\r\n // restate it the same way — a client navigation that received 200 with a\r\n // not-found payload would push the URL into history as a real page.\r\n const status =\r\n rendered.status === 200 && statusForRenderedOk !== undefined\r\n ? statusForRenderedOk\r\n : rendered.status;\r\n\r\n // Stage 10a: the stage 7 commit (headers, then cookies), applied ONCE,\r\n // identically for the document and the data representation — see\r\n // `applyCommit`.\r\n applyCommit(response, rendered, applyBufferedCookie);\r\n\r\n if (wantsData) {\r\n // So a shared cache can never serve a document to a client that asked for\r\n // JSON, or the reverse. See `data-request.ts` on why this stays even\r\n // while page responses are `no-store`.\r\n response.header(\"Vary\", WARLOCK_DATA_REQUEST_HEADER);\r\n\r\n // `bundle` is absent on exactly one path: nothing matched, so no pipeline\r\n // ran and there is no payload to build. Fastify already matched this\r\n // route to get here, so reaching it means `request.path` did not satisfy\r\n // the entry's own pattern — answered as the 404 it is, rather than\r\n // synthesising an empty payload the client would try to render as a page.\r\n if (rendered.bundle === undefined) {\r\n response.setContentType(DATA_RESPONSE_CONTENT_TYPE);\r\n await response.send(JSON.stringify({ error: \"not_found\" }), status);\r\n\r\n return;\r\n }\r\n\r\n // SERIALIZED HERE, and handed over as a STRING on purpose.\r\n //\r\n // `response.send(object)` runs the body through core's `Response.parse`,\r\n // which recurses the object, calls `toJSON()` on anything that has one\r\n // (assigning `request` onto it as it goes) and rebuilds arrays. That is\r\n // the right behaviour for a controller returning Resources; it is the\r\n // wrong behaviour here, because the DOCUMENT path serializes this exact\r\n // object with a plain `JSON.stringify` into `#__WARLOCK_DATA__`. Routing\r\n // one path through a transformer and not the other is precisely the\r\n // drift `build-hydration-payload.ts` exists to prevent — the browser\r\n // would build one tree on a page load and a different one on a\r\n // navigation to the same URL.\r\n //\r\n // A string body also bypasses `parseBody()` entirely, so the content type\r\n // has to be declared rather than inferred from an object body.\r\n response.setContentType(DATA_RESPONSE_CONTENT_TYPE);\r\n await response.send(JSON.stringify(buildHydrationPayload(rendered.bundle)), status);\r\n\r\n return;\r\n }\r\n\r\n // Stylesheets first: they go in `<head>`, the hydration module goes before\r\n // `</body>`, and doing the head work on the already-rendered string keeps\r\n // both splices in one place rather than threading CSS through the React\r\n // render just to reach the same bytes.\r\n const styled = installStylesheets(rendered.html, stylesheetUrls ?? []);\r\n\r\n const html = installHydrationClientModule(\r\n styled,\r\n hydrationClientModuleUrl,\r\n hydrationClientModuleUrl === undefined ? undefined : request.nonce,\r\n );\r\n\r\n await response.html(html, status);\r\n } catch (thrown) {\r\n // This is outside the page pipeline: loading/registering a module can\r\n // fail before a triple exists for its authored boundaries to handle.\r\n // Reuse this request/response pair so headers, nonce and response\r\n // ownership remain exactly the same as the ordinary path.\r\n //\r\n // Nested try/catch, deliberately: this block's own job is to render a\r\n // NICER answer for `thrown` — it must never let a failure IN THAT\r\n // ATTEMPT (`renderPageFailure` itself throwing, or misbehaving) replace\r\n // `thrown` with a less useful error. If rendering the failure page\r\n // fails too, the original throw escapes exactly as it would have with\r\n // no try/catch at all (the file header's stated contract) — the\r\n // router's own error path is still the answer, just one throw later.\r\n try {\r\n const rendered = await renderPageFailure({\r\n name,\r\n path: request.path,\r\n request,\r\n response,\r\n thrown,\r\n loadErrorPage,\r\n });\r\n\r\n applyCommit(response, rendered, applyBufferedCookie);\r\n\r\n if (wantsData) {\r\n response.header(\"Vary\", WARLOCK_DATA_REQUEST_HEADER);\r\n response.setContentType(DATA_RESPONSE_CONTENT_TYPE);\r\n await response.send(JSON.stringify(buildHydrationPayload(rendered.bundle!)), 500);\r\n return;\r\n }\r\n\r\n const styled = installStylesheets(rendered.html, stylesheetUrls ?? []);\r\n\r\n // `renderPageFailure` marks its bundle non-hydrating (page-render-bundle.ts):\r\n // there is no triple, so there is nothing on the client the hydration\r\n // module could attach to. Injecting it anyway would ship a script that\r\n // hydrates against a composition the server never trusted.\r\n const html = isNonHydrating(rendered.bundle)\r\n ? styled\r\n : installHydrationClientModule(\r\n styled,\r\n hydrationClientModuleUrl,\r\n hydrationClientModuleUrl === undefined ? undefined : request.nonce,\r\n );\r\n await response.html(html, 500);\r\n } catch {\r\n throw thrown;\r\n }\r\n }\r\n };\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"create-page-route-handler.mjs","names":[],"sources":["../../../../../../../web/src/server/create-page-route-handler.ts"],"sourcesContent":["/**\r\n * The page handler, as a named seam.\r\n *\r\n * This is the request handler `installPageRoutes` used to inline into its\r\n * `router.get(...)` call (`install-page-routes.ts:236-275` before this\r\n * extraction; the pre-extraction copy is `scratchpad/install-page-routes.ts.orig`).\r\n * The behaviour is unchanged, byte for byte — what changes is that it is now\r\n * a named, exported, independently constructible function instead of a closure\r\n * over eight ambient bindings of `installPageRoutes`.\r\n *\r\n * WHY IT TAKES `loadModule` AND NOT A `ViteDevServer`: loading a module is the\r\n * only capability the handler ever needed, and the two runtimes answer it\r\n * differently — dev goes through Vite's SSR graph\r\n * (`vite.ssrLoadModule`, `install-page-routes.ts:207`), production reads the\r\n * already-built page manifest (`page-manifest.ts`). Taking \"how to load a\r\n * module\" as an INPUT is what lets the same handler serve both, and what lets\r\n * a test construct it with a plain async function — no Vite, no dev server, no\r\n * `app/` directory on disk.\r\n *\r\n * Scope: this file creates a seam and nothing else. It does not implement\r\n * `type: \"page\"` routing, HTML error pages, or any other new capability.\r\n */\r\nimport { Response, type HttpContext } from \"@warlock.js/core\";\r\n\r\nimport {\r\n DATA_RESPONSE_CONTENT_TYPE,\r\n isDataRequest,\r\n WARLOCK_DATA_REQUEST_HEADER,\r\n} from \"../routing/data-request\";\r\nimport {\r\n registerModules,\r\n type RegisterableModuleNamespace,\r\n} from \"../runtime/register-modules\";\r\nimport { buildHydrationPayload } from \"./build-hydration-payload\";\r\nimport type { BufferedCookie, PageRouteEntry, PageTripleModule } from \"./execute-page-request\";\r\nimport { isNonHydrating } from \"./page-render-bundle\";\r\nimport { renderPageFailure, renderPageRequest, type RenderedPage } from \"./render-page\";\r\nimport type { ErrorPageModuleLoader } from \"./error-page\";\r\n\r\n/**\r\n * Replay ONE committed cookie through core's own `Response.cookie()` — the\r\n * same serializer every ordinary controller's cookie goes through, so there\r\n * is nothing here for a second implementation to drift from. The one-liner\r\n * `dev-server.ts` wires as the production default; passed in (`applyBufferedCookie`\r\n * option, below) rather than imported so this file stays free of anything\r\n * Vite-shaped.\r\n */\r\nfunction defaultApplyBufferedCookie(response: Response, cookie: BufferedCookie): void {\r\n response.cookie(cookie.name, cookie.value as never, cookie.options ?? {});\r\n}\r\n\r\n/**\r\n * Stage 10a — apply the stage 7 commit (headers, then cookies) to the LIVE\r\n * response, once, before either terminal write (10b: `html()` or `send()`).\r\n * Both the document and data representations of a page route go through this\r\n * so a client navigation never drops a `Set-Cookie` a full load would have\r\n * kept (`create-page-route-handler.spec.ts` — \"applies committed cookies and\r\n * headers exactly as the document path does\").\r\n */\r\nfunction applyCommit(\r\n response: Response,\r\n rendered: Pick<RenderedPage, \"headers\" | \"cookies\">,\r\n applyBufferedCookie: (response: Response, cookie: BufferedCookie) => void,\r\n): void {\r\n response.headers(rendered.headers ?? {});\r\n\r\n for (const cookie of rendered.cookies ?? []) {\r\n applyBufferedCookie(response, cookie);\r\n }\r\n}\r\n\r\n/**\r\n * How the handler obtains a page/layout/app module, by the same id\r\n * (`appFile`/`layoutFile`/`pageFile`) the caller registered it under. In dev\r\n * this is `moduleId => vite.ssrLoadModule(moduleId)`; the connector already\r\n * owns the dev/prod split, so the handler never learns which one it got.\r\n */\r\nexport type PageModuleLoader = (moduleId: string) => Promise<unknown>;\r\n\r\nexport type PageRouteHandlerOptions = {\r\n /** The composed, registered route path — `composeRoutePath`'s output. */\r\n path: string;\r\n /** The resolved route name; shared namespace with API routes. */\r\n name: string;\r\n /** The single global app-root file, e.g. `<appSrcRoot>/web/root.tsx`. */\r\n appFile: string;\r\n /** The page module's id. */\r\n pageFile: string;\r\n /** The page's own-directory `layout.tsx`, when it has one. */\r\n layoutFile?: string | undefined;\r\n loadModule: PageModuleLoader;\r\n /** Optional lazy application `error.page.tsx` loader. Never called on success. */\r\n loadErrorPage?: ErrorPageModuleLoader;\r\n /**\r\n * Load the REAL layout module namespaces, outermost first, for universal\r\n * registration. This stays separate from `loadModule(layoutFile)` because\r\n * dev may answer that id with a synthetic wrapper whose middleware is the\r\n * composition of several layouts. That wrapper is a render-pipeline detail,\r\n * not a module identity, and must never enter `registerModules`' WeakSet.\r\n */\r\n loadRegistrationLayouts?: () => Promise<readonly RegisterableModuleNamespace[]>;\r\n /** Browser module appended after the server-rendered document. */\r\n hydrationClientModuleUrl?: string;\r\n /**\r\n * Stylesheet URLs for this page, emitted into `<head>` so the FIRST paint is\r\n * styled. Absent or empty means the application has no CSS — it never means\r\n * a stylesheet failed to resolve, which is the build's job to report.\r\n */\r\n stylesheetUrls?: readonly string[];\r\n /** Same helper `dev-server.ts` exports — passed in, never imported. */\r\n /**\r\n * The pattern stage 1 matches `request.path` against, when it differs from\r\n * the REGISTERED path. Defaults to `path`, which is right for every route\r\n * whose URL is its own.\r\n *\r\n * Exactly one route needs it: the not-found page, registered on the catch-all\r\n * `*`. `matchRoute` compares segment by segment (`./match-page-route.ts`) and\r\n * has no wildcard token, so a route registered as `*` matches NOTHING — the\r\n * pipeline reports no match and `renderPageRequest` answers `{ html: \"\",\r\n * status: 404 }`. Correct status, empty document: a 404 page that never\r\n * renders its own body. Handing it `requestPath => requestPath` makes the\r\n * requested URL the route's pattern for that one request, so the match is\r\n * trivially true and the page renders for the URL the visitor actually asked\r\n * for.\r\n */\r\n matchPath?: (requestPath: string) => string;\r\n /**\r\n * The status this route answers with when the pipeline settles on a plain\r\n * `200` — the not-found route's `404`, and nothing else uses it.\r\n *\r\n * Applied ONLY to `200`, never as a blanket override: a `200` from this\r\n * pipeline means \"the document rendered and nobody objected\", which for this\r\n * route is precisely the not-found case. Any other settled status is a real\r\n * outcome that the page or the boundary decided — a 500 from a failed render,\r\n * a redirect — and overwriting it would report a broken page as a missing one.\r\n */\r\n statusForRenderedOk?: number;\n /**\n * Exclude the page module's loader from the request triple while preserving\n * the real namespace for `register()` and rendering. Used only by the\n * catch-all 404 page: a missing URL must not run application data work or\n * turn a simple miss into a second failure path.\n */\n skipPageLoader?: boolean;\n /**\r\n * Replays one committed cookie through core's `Response.cookie()`. Defaults\r\n * to doing exactly that (`defaultApplyBufferedCookie`, above); injectable so\r\n * a caller with a different `Response` shape (or a test) can observe/replace\r\n * the call.\r\n */\r\n applyBufferedCookie?: (response: Response, cookie: BufferedCookie) => void;\r\n};\r\n\r\nexport type PageRouteHandler = (context: HttpContext) => Promise<void | Response>;\r\n\r\nfunction escapeHtmlAttribute(value: string): string {\r\n return value.replace(/[&<>\"']/g, (character) => {\r\n switch (character) {\r\n case \"&\":\r\n return \"&amp;\";\r\n case \"<\":\r\n return \"&lt;\";\r\n case \">\":\r\n return \"&gt;\";\r\n case '\"':\r\n return \"&quot;\";\r\n default:\r\n return \"&#39;\";\r\n }\r\n });\r\n}\r\n\r\nfunction installHydrationClientModule(\r\n html: string,\r\n moduleUrl: string | undefined,\r\n nonce: string | undefined,\r\n): string {\r\n if (moduleUrl === undefined || html === \"\") return html;\r\n\r\n const closingBodyIndex = html.lastIndexOf(\"</body>\");\r\n if (closingBodyIndex === -1) {\r\n throw new Error(\r\n \"installPageRoutes: cannot install the hydration client module because the rendered document has no closing </body> tag.\",\r\n );\r\n }\r\n\r\n const nonceAttribute = nonce === undefined ? \"\" : ` nonce=\"${escapeHtmlAttribute(nonce)}\"`;\r\n const script = `<script type=\"module\"${nonceAttribute} src=\"${escapeHtmlAttribute(moduleUrl)}\"></script>`;\r\n return `${html.slice(0, closingBodyIndex)}${script}${html.slice(closingBodyIndex)}`;\r\n}\r\n\r\n/**\r\n * Put the page's stylesheets in `<head>`, so the first paint is styled.\r\n *\r\n * Without this the document carries no CSS at all. The stylesheet reaches the\r\n * browser only because the CLIENT bundle imports it, which means it is applied\r\n * by JavaScript after the module graph loads — the page renders unstyled first\r\n * and restyles a moment later. Correct markup, wrong-looking page, and nothing\r\n * in the console to explain it.\r\n *\r\n * A `<link>` in `<head>` is render-blocking, which is exactly what is wanted\r\n * here: the browser holds the first paint until the CSS is in, so there is no\r\n * flash rather than a faster ugly one.\r\n *\r\n * Inserted before `</head>` rather than after `<head>` so an application's own\r\n * `<link>`/`<style>` in the root document still comes FIRST and can be\r\n * overridden by these — matching how the framework's tags are documented to\r\n * behave, and keeping cascade order predictable.\r\n */\r\nfunction installStylesheets(html: string, stylesheetUrls: readonly string[]): string {\r\n if (stylesheetUrls.length === 0 || html === \"\") return html;\r\n\r\n const closingHeadIndex = html.lastIndexOf(\"</head>\");\r\n\r\n // No `<head>` is not an error the way a missing `</body>` is: a root that\r\n // renders no head is unusual but legal, and losing the stylesheet is a\r\n // cosmetic failure where losing hydration is a broken page. Silently\r\n // dropping it would be the wrong trade the other way, though — so the\r\n // document is left exactly as rendered and the caller's own missing-`</body>`\r\n // check remains the loud one.\r\n if (closingHeadIndex === -1) return html;\r\n\r\n const links = stylesheetUrls\r\n .map((url) => `<link rel=\"stylesheet\" href=\"${escapeHtmlAttribute(url)}\">`)\r\n .join(\"\");\r\n\r\n return `${html.slice(0, closingHeadIndex)}${links}${html.slice(closingHeadIndex)}`;\r\n}\r\n\r\n/**\r\n * Build the handler for ONE page route. Per request it loads the App + layout\r\n * + page triple (concurrently, in that order), renders the URL through\r\n * `renderPageRequest`, splices in the hydration module, and flushes the\r\n * document.\r\n *\r\n * No try/catch, deliberately: loader/render throws are already absorbed by the\r\n * pipeline's boundary machinery inside `renderPageRequest`, and anything that\r\n * escapes (a module-load or register failure, the missing-`</body>` throw\r\n * above) belongs to the router's error path — which is exactly where it went\r\n * before.\r\n */\r\nexport function createPageRouteHandler(options: PageRouteHandlerOptions): PageRouteHandler {\r\n const {\r\n path,\r\n name,\r\n appFile,\r\n pageFile,\r\n layoutFile,\r\n loadModule,\r\n loadErrorPage,\r\n loadRegistrationLayouts,\r\n hydrationClientModuleUrl,\r\n stylesheetUrls,\r\n matchPath,\n statusForRenderedOk,\n skipPageLoader = false,\n applyBufferedCookie = defaultApplyBufferedCookie,\n } = options;\n\r\n return async ({ request, response }: HttpContext) => {\r\n const wantsData = isDataRequest(request.header(WARLOCK_DATA_REQUEST_HEADER, undefined));\r\n\r\n try {\r\n const [appModule, layoutModule, ownPageModule, registrationLayouts] = await Promise.all([\r\n loadModule(appFile),\r\n layoutFile ? loadModule(layoutFile) : Promise.resolve({}),\r\n loadModule(pageFile),\r\n loadRegistrationLayouts?.() ?? Promise.resolve([]),\r\n ]);\r\n\r\n // Registration is the first lifecycle action after all module namespaces\r\n // have loaded and before `renderPageRequest` can run middleware, loaders or\r\n // render. App/page are already their real namespaces. Layouts deliberately\r\n // come from the separate raw chain above, never from `layoutModule`, which\r\n // may be the synthetic composed middleware wrapper used by dev.\r\n registerModules([\r\n appModule as RegisterableModuleNamespace,\r\n ...registrationLayouts,\r\n ownPageModule as RegisterableModuleNamespace,\r\n ]);\r\n\r\n const pageModule = ownPageModule as PageTripleModule;\n const triple: PageRouteEntry[\"triple\"] = {\n app: appModule as PageTripleModule,\n layout: layoutModule as PageTripleModule,\n // Registration above deliberately receives the REAL namespace. Only the\n // pipeline view is projected: spreading preserves the component,\n // metadata, middleware and boundary exports while making a custom 404's\n // loader uncallable.\n page: skipPageLoader\n ? {\n ...pageModule,\n // Vite and native ESM loaders hand us module namespace objects,\n // whose export descriptors are not an object-spread contract.\n // Keep the rendering export explicitly while hiding only loader.\n default: pageModule.default,\n loader: undefined,\n }\n : pageModule,\n };\n\r\n const requestUrl = request.path;\n const [requestPathname] = requestUrl.split(\"?\");\n const routes: PageRouteEntry[] = [\n { path: matchPath === undefined ? path : matchPath(requestPathname), name, triple },\n ];\n\r\n // A DATA request runs everything above and below this line identically —\r\n // it is the same route, the same match and the same pipeline — and differs\r\n // only in what gets written at the end. Decided here, before the render, so\r\n // the branch is visibly about REPRESENTATION and not about behaviour.\r\n const rendered = await renderPageRequest(requestUrl, {\n routes,\r\n createHttp: () => ({ request, response }),\r\n loadErrorPage,\r\n });\r\n\r\n if (rendered instanceof Response) return rendered;\r\n\r\n // See `statusForRenderedOk`: a settled 200 is the only status this route is\r\n // allowed to restate, and both the document and the data branch below must\r\n // restate it the same way — a client navigation that received 200 with a\r\n // not-found payload would push the URL into history as a real page.\r\n const status =\r\n rendered.status === 200 && statusForRenderedOk !== undefined\r\n ? statusForRenderedOk\r\n : rendered.status;\r\n\r\n // Stage 10a: the stage 7 commit (headers, then cookies), applied ONCE,\r\n // identically for the document and the data representation — see\r\n // `applyCommit`.\r\n applyCommit(response, rendered, applyBufferedCookie);\r\n\r\n if (wantsData) {\r\n // So a shared cache can never serve a document to a client that asked for\r\n // JSON, or the reverse. See `data-request.ts` on why this stays even\r\n // while page responses are `no-store`.\r\n response.header(\"Vary\", WARLOCK_DATA_REQUEST_HEADER);\r\n\r\n // `bundle` is absent on exactly one path: nothing matched, so no pipeline\r\n // ran and there is no payload to build. Fastify already matched this\r\n // route to get here, so reaching it means `request.path` did not satisfy\r\n // the entry's own pattern — answered as the 404 it is, rather than\r\n // synthesising an empty payload the client would try to render as a page.\r\n if (rendered.bundle === undefined) {\r\n response.setContentType(DATA_RESPONSE_CONTENT_TYPE);\r\n await response.send(JSON.stringify({ error: \"not_found\" }), status);\r\n\r\n return;\r\n }\r\n\r\n // SERIALIZED HERE, and handed over as a STRING on purpose.\r\n //\r\n // `response.send(object)` runs the body through core's `Response.parse`,\r\n // which recurses the object, calls `toJSON()` on anything that has one\r\n // (assigning `request` onto it as it goes) and rebuilds arrays. That is\r\n // the right behaviour for a controller returning Resources; it is the\r\n // wrong behaviour here, because the DOCUMENT path serializes this exact\r\n // object with a plain `JSON.stringify` into `#__WARLOCK_DATA__`. Routing\r\n // one path through a transformer and not the other is precisely the\r\n // drift `build-hydration-payload.ts` exists to prevent — the browser\r\n // would build one tree on a page load and a different one on a\r\n // navigation to the same URL.\r\n //\r\n // A string body also bypasses `parseBody()` entirely, so the content type\r\n // has to be declared rather than inferred from an object body.\r\n response.setContentType(DATA_RESPONSE_CONTENT_TYPE);\r\n await response.send(JSON.stringify(buildHydrationPayload(rendered.bundle)), status);\r\n\r\n return;\r\n }\r\n\r\n // Stylesheets first: they go in `<head>`, the hydration module goes before\r\n // `</body>`, and doing the head work on the already-rendered string keeps\r\n // both splices in one place rather than threading CSS through the React\r\n // render just to reach the same bytes.\r\n const styled = installStylesheets(rendered.html, stylesheetUrls ?? []);\r\n\r\n const html = installHydrationClientModule(\r\n styled,\r\n hydrationClientModuleUrl,\r\n hydrationClientModuleUrl === undefined ? undefined : request.nonce,\r\n );\r\n\r\n await response.html(html, status);\r\n } catch (thrown) {\r\n // This is outside the page pipeline: loading/registering a module can\r\n // fail before a triple exists for its authored boundaries to handle.\r\n // Reuse this request/response pair so headers, nonce and response\r\n // ownership remain exactly the same as the ordinary path.\r\n //\r\n // Nested try/catch, deliberately: this block's own job is to render a\r\n // NICER answer for `thrown` — it must never let a failure IN THAT\r\n // ATTEMPT (`renderPageFailure` itself throwing, or misbehaving) replace\r\n // `thrown` with a less useful error. If rendering the failure page\r\n // fails too, the original throw escapes exactly as it would have with\r\n // no try/catch at all (the file header's stated contract) — the\r\n // router's own error path is still the answer, just one throw later.\r\n try {\r\n const rendered = await renderPageFailure({\r\n name,\r\n path: request.path,\r\n request,\r\n response,\r\n thrown,\r\n loadErrorPage,\r\n });\r\n\r\n applyCommit(response, rendered, applyBufferedCookie);\r\n\r\n if (wantsData) {\r\n response.header(\"Vary\", WARLOCK_DATA_REQUEST_HEADER);\r\n response.setContentType(DATA_RESPONSE_CONTENT_TYPE);\r\n await response.send(JSON.stringify(buildHydrationPayload(rendered.bundle!)), 500);\r\n return;\r\n }\r\n\r\n const styled = installStylesheets(rendered.html, stylesheetUrls ?? []);\r\n\r\n // `renderPageFailure` marks its bundle non-hydrating (page-render-bundle.ts):\r\n // there is no triple, so there is nothing on the client the hydration\r\n // module could attach to. Injecting it anyway would ship a script that\r\n // hydrates against a composition the server never trusted.\r\n const html = isNonHydrating(rendered.bundle)\r\n ? styled\r\n : installHydrationClientModule(\r\n styled,\r\n hydrationClientModuleUrl,\r\n hydrationClientModuleUrl === undefined ? undefined : request.nonce,\r\n );\r\n await response.html(html, 500);\r\n } catch {\r\n throw thrown;\r\n }\r\n }\r\n };\r\n}\r\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { stripVTControlCharacters } from "node:util";
2
2
 
3
- //#region web/src/server/dev-server.ts
3
+ //#region ../web/src/server/dev-server.ts
4
4
  /**
5
5
  * Where a captured transform/resolve failure rides from Vite's connect stack to
6
6
  * the Fastify hook that mounted it.
@@ -1 +1 @@
1
- {"version":3,"file":"dev-server.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/dev-server.ts"],"sourcesContent":["/** Development-only transport for Vite transform failures. */\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { stripVTControlCharacters } from \"node:util\";\nimport type { Connect, Plugin } from \"vite\";\n\n/**\n * Where a captured transform/resolve failure rides from Vite's connect stack to\n * the Fastify hook that mounted it.\n *\n * A `Symbol.for` key on the raw `IncomingMessage` rather than a `WeakMap`\n * because the two halves live in different modules and are wired at different\n * times; the request object is the only thing they provably share, and the\n * symbol cannot collide with a Vite/Fastify/user property.\n *\n * Exported so a test can stage a captured failure without booting a vite\n * server, and so the two halves cannot drift onto two different keys.\n */\nexport const DEV_TRANSFORM_ERROR_BODY = Symbol.for(\"warlock.web.devTransformErrorBody\");\n\n/**\n * Re-exported so a dev-only caller can reach the cookie commit seam through\n * this module without a second import path into `./buffered-response`, which\n * is where the implementation actually lives.\n */\nexport { applyBufferedCookie, type BufferedCookie } from \"./buffered-response\";\n\ntype DevTransformErrorCarrier = { [DEV_TRANSFORM_ERROR_BODY]?: string };\n\n/**\n * The status a refused module now answers with.\n *\n * NOT the 404 this replaces. That 404 was never a decision about the module —\n * it is what an unmatched URL gets once vite has declined it, and which of the\n * two framework answers you see depends only on whether the app declares a\n * catch-all page: with one (v5/app does — `path: \"*\"`) the request lands in the\n * page pipeline, matches no route, and `./render-page.ts:604` returns\n * `{ html: \"\", status: 404 }` for `./create-page-route-handler.ts:147` to write\n * as an empty `text/html` body; without one it is `core/src/router/router.ts:879`.\n * Either way \"the module does not exist\" is precisely the wrong thing to tell a\n * developer whose module exists and was refused. 500 is the status VITE ITSELF writes for\n * this exact condition when it is not in middleware mode\n * (`node_modules/vite/dist/node/chunks/config.js:9528`), so this adopts that\n * convention rather than inventing a third one.\n */\nexport const DEV_TRANSFORM_ERROR_STATUS = 500;\n\n/**\n * `buildErrorMessage` as vite exports it. Declared structurally so this module\n * needs no value import of vite — vite is an optional, dev-only peer and a\n * production install does not carry it.\n */\nexport type BuildErrorMessage = (\n error: Error,\n args?: string[],\n includeStack?: boolean,\n) => string;\n\n/**\n * The dev error transport was constructed while the process is hosting a\n * PRODUCTION build. Refused by name at construction rather than degraded,\n * because everything this transport does — file paths, source frames, plugin\n * names — is exactly what a production response must never carry.\n */\nexport class DevErrorTransportInProductionError extends Error {\n public constructor() {\n super(\n \"The dev error transport was constructed with `Application.runtimeStrategy === \" +\n '\"production\"`. It exists only to put a Vite transform failure in front of a ' +\n \"developer and its response body carries absolute file paths and source frames, \" +\n \"so it must never be mounted on a production-hosted server.\",\n );\n this.name = \"DevErrorTransportInProductionError\";\n }\n}\n\n/**\n * Render a refused module's failure as the plain-text body the browser gets.\n *\n * Formatting is DELEGATED to vite's own exported `buildErrorMessage`, not\n * reimplemented: it is the same function vite uses to print the failure to the\n * terminal, so the text a developer reads in the network panel and the text\n * they read in the terminal cannot drift. Two things are added around it —\n * `error.name`, which vite's terminal path replaces with a fixed\n * \"Internal server error:\" prefix and which is the single most useful token for\n * a named gate refusal (`ProjectionAmbiguityError`), and the `cause` chain,\n * which vite does not walk.\n *\n * `stripVTControlCharacters` is not optional: `buildErrorMessage` colours its\n * output with picocolors, which is ON whenever the dev server owns a TTY, and\n * raw ANSI escapes in an HTTP body are noise. Vite strips them the same way for\n * the overlay payload (`config.js:9490-9497`).\n *\n * The stack is deliberately omitted (`includeStack: false`). A gate refusal's\n * stack points into the gate, not into the developer's code; the fields that\n * locate the problem — plugin, file, line, source frame — are what\n * `buildErrorMessage` puts there without it.\n */\nexport function formatDevTransformError(\n error: unknown,\n buildErrorMessage: BuildErrorMessage,\n): string {\n const failure =\n error instanceof Error ? error : new Error(typeof error === \"string\" ? error : String(error));\n\n const lines = [\n stripVTControlCharacters(\n buildErrorMessage(failure, [`${failure.name}: ${failure.message}`], false),\n ),\n ];\n\n // Walk the `cause` chain. A gate may wrap a parser failure, and the wrapped\n // message is usually the one naming the actual syntax that was refused.\n let cause = (failure as { cause?: unknown }).cause;\n\n while (cause instanceof Error) {\n lines.push(` Caused by: ${cause.name}: ${stripVTControlCharacters(cause.message)}`);\n cause = (cause as { cause?: unknown }).cause;\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/**\n * DEV-ONLY. Capture the transform/resolve failure that vite is about to throw\n * away, so the request that caused it can answer with it.\n *\n * WHY THIS IS A PLUGIN AND NOT A `middlewares.use(...)` CALL — this is the\n * whole defect, and it is an ordering fact, not a style choice:\n *\n * - Vite mounts its own error handler LAST, built as\n * `errorMiddleware(server, !!middlewareMode)`\n * (`node_modules/vite/dist/node/chunks/config.js:25705`).\n * - In middleware mode that `allowNext` flag is `true`, and the handler then\n * logs the error to the TERMINAL and calls `next()` — with no error\n * (`config.js:9525-9527`).\n * - connect only routes an error to a 4-arity handler while an error is in\n * flight (`config.js:10611-10626`), so `next()` clears it: every layer after\n * that point, INCLUDING the `done` callback `./web-connector.ts` hands the\n * stack, is called as if the request had simply gone unhandled. The\n * framework then answers the only way it can for a URL it does not know — a\n * 404, empty (see {@link DEV_TRANSFORM_ERROR_STATUS} for which of the two\n * produces it).\n * - Anything registered with `vite.middlewares.use(...)` after `createServer()`\n * resolves lands AFTER that handler and is therefore unreachable. A\n * `configureServer` POST hook does not: vite runs post hooks at\n * `config.js:25700`, five lines BEFORE it mounts its error handler.\n *\n * So this sits between the failure and vite's logger. It captures, then calls\n * `next(error)` and lets vite's own handler run exactly as before — the\n * terminal message and the `hot.send({ type: \"error\" })` overlay push\n * (`config.js:9511-9521`) are unchanged. This transport ADDS a reader; it\n * replaces nothing.\n *\n * @param isProductionRuntime the connector's own hosting-mode signal\n * (`./web-connector.ts:122`) — passed in rather than re-derived so there\n * is one definition of \"this process is Vite-hosted\", not two.\n */\nexport function devErrorTransportPlugin(options: {\n isProductionRuntime: () => boolean;\n buildErrorMessage: BuildErrorMessage;\n}): Plugin {\n const { isProductionRuntime, buildErrorMessage } = options;\n\n if (isProductionRuntime()) {\n throw new DevErrorTransportInProductionError();\n }\n\n const capture: Connect.ErrorHandleFunction = (error, request, _response, next) => {\n // Re-asserted per request, not just at construction: `runtimeStrategy` is\n // process state and a transport that leaks source frames is not something\n // to hold open on a boot-time reading alone. In production this layer is a\n // pass-through and vite's handler behaves exactly as it does today.\n if (isProductionRuntime()) return next(error);\n\n (request as DevTransformErrorCarrier)[DEV_TRANSFORM_ERROR_BODY] = formatDevTransformError(\n error,\n buildErrorMessage,\n );\n\n next(error);\n };\n\n return {\n name: \"warlock:dev-error-transport\",\n // Belt to the `isProductionRuntime` braces: this plugin has no business in\n // a `vite build` graph either.\n apply: \"serve\",\n configureServer(server) {\n // RETURNING a function is what makes this a POST hook — the ordering the\n // note above depends on. Mounting inline here would land the layer BEFORE\n // vite's transform middleware, where no error has been thrown yet.\n return () => {\n server.middlewares.use(capture);\n };\n },\n };\n}\n\n/**\n * Answer the request with the failure {@link devErrorTransportPlugin} captured,\n * if there was one. Returns `false` when there was not, which is the normal\n * case and means \"carry on down the framework's own path\".\n *\n * Called from the Fastify `onRequest` hook that mounts vite\n * (`./web-connector.ts:321`), in the `done` callback — i.e. at the one moment\n * where connect has finished, vite has declined to answer, and the framework is\n * about to 404. Writing to the raw `ServerResponse` rather than through Fastify\n * is what the mount already does for every response vite serves, so this stays\n * on the same side of the seam.\n */\nexport function sendCapturedDevError(request: IncomingMessage, response: ServerResponse): boolean {\n const body = (request as DevTransformErrorCarrier)[DEV_TRANSFORM_ERROR_BODY];\n\n if (typeof body !== \"string\") return false;\n\n // A middleware further down may have answered already (vite serves plenty of\n // requests itself). Never write twice; the captured body is then just dropped.\n if (response.headersSent || response.writableEnded) return false;\n\n response.statusCode = DEV_TRANSFORM_ERROR_STATUS;\n response.setHeader(\"content-type\", \"text/plain; charset=utf-8\");\n // A refusal is a fact about the CURRENT source. Caching it would survive the\n // edit that fixes it.\n response.setHeader(\"cache-control\", \"no-store\");\n response.end(body);\n\n return true;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiBA,MAAa,2BAA2B,OAAO,IAAI,mCAAmC;;;;;;;;;;;;;;;;;AA2BtF,MAAa,6BAA6B;;;;;;;AAmB1C,IAAa,qCAAb,cAAwD,MAAM;CAC5D,AAAO,cAAc;EACnB,MACE,uSAIF;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,wBACd,OACA,mBACQ;CACR,MAAM,UACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,UAAU,WAAW,QAAQ,OAAO,KAAK,CAAC;CAE9F,MAAM,QAAQ,CACZ,yBACE,kBAAkB,SAAS,CAAC,GAAG,QAAQ,KAAK,IAAI,QAAQ,SAAS,GAAG,KAAK,CAC3E,CACF;CAIA,IAAI,QAAS,QAAgC;CAE7C,OAAO,iBAAiB,OAAO;EAC7B,MAAM,KAAK,gBAAgB,MAAM,KAAK,IAAI,yBAAyB,MAAM,OAAO,GAAG;EACnF,QAAS,MAA8B;CACzC;CAEA,OAAO,GAAG,MAAM,KAAK,IAAI,EAAE;AAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAgB,wBAAwB,SAG7B;CACT,MAAM,EAAE,qBAAqB,sBAAsB;CAEnD,IAAI,oBAAoB,GACtB,MAAM,IAAI,mCAAmC;CAG/C,MAAM,WAAwC,OAAO,SAAS,WAAW,SAAS;EAKhF,IAAI,oBAAoB,GAAG,OAAO,KAAK,KAAK;EAE5C,AAAC,QAAqC,4BAA4B,wBAChE,OACA,iBACF;EAEA,KAAK,KAAK;CACZ;CAEA,OAAO;EACL,MAAM;EAGN,OAAO;EACP,gBAAgB,QAAQ;GAItB,aAAa;IACX,OAAO,YAAY,IAAI,OAAO;GAChC;EACF;CACF;AACF;;;;;;;;;;;;;AAcA,SAAgB,qBAAqB,SAA0B,UAAmC;CAChG,MAAM,OAAQ,QAAqC;CAEnD,IAAI,OAAO,SAAS,UAAU,OAAO;CAIrC,IAAI,SAAS,eAAe,SAAS,eAAe,OAAO;CAE3D,SAAS;CACT,SAAS,UAAU,gBAAgB,2BAA2B;CAG9D,SAAS,UAAU,iBAAiB,UAAU;CAC9C,SAAS,IAAI,IAAI;CAEjB,OAAO;AACT"}
1
+ {"version":3,"file":"dev-server.mjs","names":[],"sources":["../../../../../../../web/src/server/dev-server.ts"],"sourcesContent":["/** Development-only transport for Vite transform failures. */\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { stripVTControlCharacters } from \"node:util\";\nimport type { Connect, Plugin } from \"vite\";\n\n/**\n * Where a captured transform/resolve failure rides from Vite's connect stack to\n * the Fastify hook that mounted it.\n *\n * A `Symbol.for` key on the raw `IncomingMessage` rather than a `WeakMap`\n * because the two halves live in different modules and are wired at different\n * times; the request object is the only thing they provably share, and the\n * symbol cannot collide with a Vite/Fastify/user property.\n *\n * Exported so a test can stage a captured failure without booting a vite\n * server, and so the two halves cannot drift onto two different keys.\n */\nexport const DEV_TRANSFORM_ERROR_BODY = Symbol.for(\"warlock.web.devTransformErrorBody\");\n\n/**\n * Re-exported so a dev-only caller can reach the cookie commit seam through\n * this module without a second import path into `./buffered-response`, which\n * is where the implementation actually lives.\n */\nexport { applyBufferedCookie, type BufferedCookie } from \"./buffered-response\";\n\ntype DevTransformErrorCarrier = { [DEV_TRANSFORM_ERROR_BODY]?: string };\n\n/**\n * The status a refused module now answers with.\n *\n * NOT the 404 this replaces. That 404 was never a decision about the module —\n * it is what an unmatched URL gets once vite has declined it, and which of the\n * two framework answers you see depends only on whether the app declares a\n * catch-all page: with one (v5/app does — `path: \"*\"`) the request lands in the\n * page pipeline, matches no route, and `./render-page.ts:604` returns\n * `{ html: \"\", status: 404 }` for `./create-page-route-handler.ts:147` to write\n * as an empty `text/html` body; without one it is `core/src/router/router.ts:879`.\n * Either way \"the module does not exist\" is precisely the wrong thing to tell a\n * developer whose module exists and was refused. 500 is the status VITE ITSELF writes for\n * this exact condition when it is not in middleware mode\n * (`node_modules/vite/dist/node/chunks/config.js:9528`), so this adopts that\n * convention rather than inventing a third one.\n */\nexport const DEV_TRANSFORM_ERROR_STATUS = 500;\n\n/**\n * `buildErrorMessage` as vite exports it. Declared structurally so this module\n * needs no value import of vite — vite is an optional, dev-only peer and a\n * production install does not carry it.\n */\nexport type BuildErrorMessage = (\n error: Error,\n args?: string[],\n includeStack?: boolean,\n) => string;\n\n/**\n * The dev error transport was constructed while the process is hosting a\n * PRODUCTION build. Refused by name at construction rather than degraded,\n * because everything this transport does — file paths, source frames, plugin\n * names — is exactly what a production response must never carry.\n */\nexport class DevErrorTransportInProductionError extends Error {\n public constructor() {\n super(\n \"The dev error transport was constructed with `Application.runtimeStrategy === \" +\n '\"production\"`. It exists only to put a Vite transform failure in front of a ' +\n \"developer and its response body carries absolute file paths and source frames, \" +\n \"so it must never be mounted on a production-hosted server.\",\n );\n this.name = \"DevErrorTransportInProductionError\";\n }\n}\n\n/**\n * Render a refused module's failure as the plain-text body the browser gets.\n *\n * Formatting is DELEGATED to vite's own exported `buildErrorMessage`, not\n * reimplemented: it is the same function vite uses to print the failure to the\n * terminal, so the text a developer reads in the network panel and the text\n * they read in the terminal cannot drift. Two things are added around it —\n * `error.name`, which vite's terminal path replaces with a fixed\n * \"Internal server error:\" prefix and which is the single most useful token for\n * a named gate refusal (`ProjectionAmbiguityError`), and the `cause` chain,\n * which vite does not walk.\n *\n * `stripVTControlCharacters` is not optional: `buildErrorMessage` colours its\n * output with picocolors, which is ON whenever the dev server owns a TTY, and\n * raw ANSI escapes in an HTTP body are noise. Vite strips them the same way for\n * the overlay payload (`config.js:9490-9497`).\n *\n * The stack is deliberately omitted (`includeStack: false`). A gate refusal's\n * stack points into the gate, not into the developer's code; the fields that\n * locate the problem — plugin, file, line, source frame — are what\n * `buildErrorMessage` puts there without it.\n */\nexport function formatDevTransformError(\n error: unknown,\n buildErrorMessage: BuildErrorMessage,\n): string {\n const failure =\n error instanceof Error ? error : new Error(typeof error === \"string\" ? error : String(error));\n\n const lines = [\n stripVTControlCharacters(\n buildErrorMessage(failure, [`${failure.name}: ${failure.message}`], false),\n ),\n ];\n\n // Walk the `cause` chain. A gate may wrap a parser failure, and the wrapped\n // message is usually the one naming the actual syntax that was refused.\n let cause = (failure as { cause?: unknown }).cause;\n\n while (cause instanceof Error) {\n lines.push(` Caused by: ${cause.name}: ${stripVTControlCharacters(cause.message)}`);\n cause = (cause as { cause?: unknown }).cause;\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/**\n * DEV-ONLY. Capture the transform/resolve failure that vite is about to throw\n * away, so the request that caused it can answer with it.\n *\n * WHY THIS IS A PLUGIN AND NOT A `middlewares.use(...)` CALL — this is the\n * whole defect, and it is an ordering fact, not a style choice:\n *\n * - Vite mounts its own error handler LAST, built as\n * `errorMiddleware(server, !!middlewareMode)`\n * (`node_modules/vite/dist/node/chunks/config.js:25705`).\n * - In middleware mode that `allowNext` flag is `true`, and the handler then\n * logs the error to the TERMINAL and calls `next()` — with no error\n * (`config.js:9525-9527`).\n * - connect only routes an error to a 4-arity handler while an error is in\n * flight (`config.js:10611-10626`), so `next()` clears it: every layer after\n * that point, INCLUDING the `done` callback `./web-connector.ts` hands the\n * stack, is called as if the request had simply gone unhandled. The\n * framework then answers the only way it can for a URL it does not know — a\n * 404, empty (see {@link DEV_TRANSFORM_ERROR_STATUS} for which of the two\n * produces it).\n * - Anything registered with `vite.middlewares.use(...)` after `createServer()`\n * resolves lands AFTER that handler and is therefore unreachable. A\n * `configureServer` POST hook does not: vite runs post hooks at\n * `config.js:25700`, five lines BEFORE it mounts its error handler.\n *\n * So this sits between the failure and vite's logger. It captures, then calls\n * `next(error)` and lets vite's own handler run exactly as before — the\n * terminal message and the `hot.send({ type: \"error\" })` overlay push\n * (`config.js:9511-9521`) are unchanged. This transport ADDS a reader; it\n * replaces nothing.\n *\n * @param isProductionRuntime the connector's own hosting-mode signal\n * (`./web-connector.ts:122`) — passed in rather than re-derived so there\n * is one definition of \"this process is Vite-hosted\", not two.\n */\nexport function devErrorTransportPlugin(options: {\n isProductionRuntime: () => boolean;\n buildErrorMessage: BuildErrorMessage;\n}): Plugin {\n const { isProductionRuntime, buildErrorMessage } = options;\n\n if (isProductionRuntime()) {\n throw new DevErrorTransportInProductionError();\n }\n\n const capture: Connect.ErrorHandleFunction = (error, request, _response, next) => {\n // Re-asserted per request, not just at construction: `runtimeStrategy` is\n // process state and a transport that leaks source frames is not something\n // to hold open on a boot-time reading alone. In production this layer is a\n // pass-through and vite's handler behaves exactly as it does today.\n if (isProductionRuntime()) return next(error);\n\n (request as DevTransformErrorCarrier)[DEV_TRANSFORM_ERROR_BODY] = formatDevTransformError(\n error,\n buildErrorMessage,\n );\n\n next(error);\n };\n\n return {\n name: \"warlock:dev-error-transport\",\n // Belt to the `isProductionRuntime` braces: this plugin has no business in\n // a `vite build` graph either.\n apply: \"serve\",\n configureServer(server) {\n // RETURNING a function is what makes this a POST hook — the ordering the\n // note above depends on. Mounting inline here would land the layer BEFORE\n // vite's transform middleware, where no error has been thrown yet.\n return () => {\n server.middlewares.use(capture);\n };\n },\n };\n}\n\n/**\n * Answer the request with the failure {@link devErrorTransportPlugin} captured,\n * if there was one. Returns `false` when there was not, which is the normal\n * case and means \"carry on down the framework's own path\".\n *\n * Called from the Fastify `onRequest` hook that mounts vite\n * (`./web-connector.ts:321`), in the `done` callback — i.e. at the one moment\n * where connect has finished, vite has declined to answer, and the framework is\n * about to 404. Writing to the raw `ServerResponse` rather than through Fastify\n * is what the mount already does for every response vite serves, so this stays\n * on the same side of the seam.\n */\nexport function sendCapturedDevError(request: IncomingMessage, response: ServerResponse): boolean {\n const body = (request as DevTransformErrorCarrier)[DEV_TRANSFORM_ERROR_BODY];\n\n if (typeof body !== \"string\") return false;\n\n // A middleware further down may have answered already (vite serves plenty of\n // requests itself). Never write twice; the captured body is then just dropped.\n if (response.headersSent || response.writableEnded) return false;\n\n response.statusCode = DEV_TRANSFORM_ERROR_STATUS;\n response.setHeader(\"content-type\", \"text/plain; charset=utf-8\");\n // A refusal is a fact about the CURRENT source. Caching it would survive the\n // edit that fixes it.\n response.setHeader(\"cache-control\", \"no-store\");\n response.end(body);\n\n return true;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiBA,MAAa,2BAA2B,OAAO,IAAI,mCAAmC;;;;;;;;;;;;;;;;;AA2BtF,MAAa,6BAA6B;;;;;;;AAmB1C,IAAa,qCAAb,cAAwD,MAAM;CAC5D,AAAO,cAAc;EACnB,MACE,uSAIF;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,wBACd,OACA,mBACQ;CACR,MAAM,UACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,UAAU,WAAW,QAAQ,OAAO,KAAK,CAAC;CAE9F,MAAM,QAAQ,CACZ,yBACE,kBAAkB,SAAS,CAAC,GAAG,QAAQ,KAAK,IAAI,QAAQ,SAAS,GAAG,KAAK,CAC3E,CACF;CAIA,IAAI,QAAS,QAAgC;CAE7C,OAAO,iBAAiB,OAAO;EAC7B,MAAM,KAAK,gBAAgB,MAAM,KAAK,IAAI,yBAAyB,MAAM,OAAO,GAAG;EACnF,QAAS,MAA8B;CACzC;CAEA,OAAO,GAAG,MAAM,KAAK,IAAI,EAAE;AAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAgB,wBAAwB,SAG7B;CACT,MAAM,EAAE,qBAAqB,sBAAsB;CAEnD,IAAI,oBAAoB,GACtB,MAAM,IAAI,mCAAmC;CAG/C,MAAM,WAAwC,OAAO,SAAS,WAAW,SAAS;EAKhF,IAAI,oBAAoB,GAAG,OAAO,KAAK,KAAK;EAE5C,AAAC,QAAqC,4BAA4B,wBAChE,OACA,iBACF;EAEA,KAAK,KAAK;CACZ;CAEA,OAAO;EACL,MAAM;EAGN,OAAO;EACP,gBAAgB,QAAQ;GAItB,aAAa;IACX,OAAO,YAAY,IAAI,OAAO;GAChC;EACF;CACF;AACF;;;;;;;;;;;;;AAcA,SAAgB,qBAAqB,SAA0B,UAAmC;CAChG,MAAM,OAAQ,QAAqC;CAEnD,IAAI,OAAO,SAAS,UAAU,OAAO;CAIrC,IAAI,SAAS,eAAe,SAAS,eAAe,OAAO;CAE3D,SAAS;CACT,SAAS,UAAU,gBAAgB,2BAA2B;CAG9D,SAAS,UAAU,iBAAiB,UAAU;CAC9C,SAAS,IAAI,IAAI;CAEjB,OAAO;AACT"}
@@ -1,6 +1,6 @@
1
1
  import { ServerErrorPageProps } from "../props.mjs";
2
2
  import { MetadataOutput } from "../metadata.mjs";
3
- //#region web/src/server/error-page.d.ts
3
+ //#region ../web/src/server/error-page.d.ts
4
4
  /** Server-only shape of an application-owned `error.page.tsx` namespace. */
5
5
  type ErrorPageModule = {
6
6
  register?: () => unknown;
@@ -1,6 +1,6 @@
1
1
  import { ERROR_PAGE_METADATA } from "./resolve-page-metadata.mjs";
2
2
 
3
- //#region web/src/server/error-page.ts
3
+ //#region ../web/src/server/error-page.ts
4
4
  function serializePageError(thrown) {
5
5
  if (thrown instanceof Error) return {
6
6
  name: thrown.name || "Error",
@@ -1 +1 @@
1
- {"version":3,"file":"error-page.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/error-page.ts"],"sourcesContent":["import type { SerializedErrorPageProps, SerializedPageError } from \"../components/document-context\";\nimport { ERROR_PAGE_METADATA } from \"./resolve-page-metadata\";\nimport type { MetadataOutput } from \"../metadata\";\nimport type { ServerErrorPageProps } from \"../props\";\n\n/** Server-only shape of an application-owned `error.page.tsx` namespace. */\nexport type ErrorPageModule = {\n register?: () => unknown;\n default?: unknown;\n metadata?: MetadataOutput | ((props: ServerErrorPageProps) => MetadataOutput);\n};\n\n/** Deliberately lazy: normal requests never even load error.page.tsx. */\nexport type ErrorPageModuleLoader = () => Promise<ErrorPageModule>;\n\nexport function serializePageError(thrown: unknown): SerializedPageError {\n if (thrown instanceof Error) {\n return {\n name: thrown.name || \"Error\",\n message: thrown.message,\n ...(process.env.NODE_ENV !== \"production\" && typeof thrown.stack === \"string\"\n ? { stack: thrown.stack }\n : {}),\n };\n }\n\n let message: string;\n try {\n message = typeof thrown === \"string\" ? thrown : String(thrown);\n } catch {\n message = \"An unexpected error occurred.\";\n }\n\n return {\n name: \"Error\",\n message,\n };\n}\n\n/** Error-page metadata improves the safe framework default; it cannot remove noindex. */\nexport function resolveErrorPageMetadata(\n module: ErrorPageModule,\n props: ServerErrorPageProps,\n): MetadataOutput {\n const own = typeof module.metadata === \"function\" ? module.metadata(props) : module.metadata;\n\n return { ...ERROR_PAGE_METADATA, ...own, robots: own?.robots ?? ERROR_PAGE_METADATA.robots };\n}\n\nexport function hydrationErrorPageProps(\n props: ServerErrorPageProps,\n serializableError: unknown = props.error,\n): SerializedErrorPageProps {\n return { error: serializePageError(serializableError), status: props.status };\n}\n"],"mappings":";;;AAeA,SAAgB,mBAAmB,QAAsC;CACvE,IAAI,kBAAkB,OACpB,OAAO;EACL,MAAM,OAAO,QAAQ;EACrB,SAAS,OAAO;EAChB,GAAI,QAAQ,IAAI,aAAa,gBAAgB,OAAO,OAAO,UAAU,WACjE,EAAE,OAAO,OAAO,MAAM,IACtB,CAAC;CACP;CAGF,IAAI;CACJ,IAAI;EACF,UAAU,OAAO,WAAW,WAAW,SAAS,OAAO,MAAM;CAC/D,QAAQ;EACN,UAAU;CACZ;CAEA,OAAO;EACL,MAAM;EACN;CACF;AACF;;AAGA,SAAgB,yBACd,QACA,OACgB;CAChB,MAAM,MAAM,OAAO,OAAO,aAAa,aAAa,OAAO,SAAS,KAAK,IAAI,OAAO;CAEpF,OAAO;EAAE,GAAG;EAAqB,GAAG;EAAK,QAAQ,KAAK,UAAU,oBAAoB;CAAO;AAC7F;AAEA,SAAgB,wBACd,OACA,oBAA6B,MAAM,OACT;CAC1B,OAAO;EAAE,OAAO,mBAAmB,iBAAiB;EAAG,QAAQ,MAAM;CAAO;AAC9E"}
1
+ {"version":3,"file":"error-page.mjs","names":[],"sources":["../../../../../../../web/src/server/error-page.ts"],"sourcesContent":["import type { SerializedErrorPageProps, SerializedPageError } from \"../components/document-context\";\nimport { ERROR_PAGE_METADATA } from \"./resolve-page-metadata\";\nimport type { MetadataOutput } from \"../metadata\";\nimport type { ServerErrorPageProps } from \"../props\";\n\n/** Server-only shape of an application-owned `error.page.tsx` namespace. */\nexport type ErrorPageModule = {\n register?: () => unknown;\n default?: unknown;\n metadata?: MetadataOutput | ((props: ServerErrorPageProps) => MetadataOutput);\n};\n\n/** Deliberately lazy: normal requests never even load error.page.tsx. */\nexport type ErrorPageModuleLoader = () => Promise<ErrorPageModule>;\n\nexport function serializePageError(thrown: unknown): SerializedPageError {\n if (thrown instanceof Error) {\n return {\n name: thrown.name || \"Error\",\n message: thrown.message,\n ...(process.env.NODE_ENV !== \"production\" && typeof thrown.stack === \"string\"\n ? { stack: thrown.stack }\n : {}),\n };\n }\n\n let message: string;\n try {\n message = typeof thrown === \"string\" ? thrown : String(thrown);\n } catch {\n message = \"An unexpected error occurred.\";\n }\n\n return {\n name: \"Error\",\n message,\n };\n}\n\n/** Error-page metadata improves the safe framework default; it cannot remove noindex. */\nexport function resolveErrorPageMetadata(\n module: ErrorPageModule,\n props: ServerErrorPageProps,\n): MetadataOutput {\n const own = typeof module.metadata === \"function\" ? module.metadata(props) : module.metadata;\n\n return { ...ERROR_PAGE_METADATA, ...own, robots: own?.robots ?? ERROR_PAGE_METADATA.robots };\n}\n\nexport function hydrationErrorPageProps(\n props: ServerErrorPageProps,\n serializableError: unknown = props.error,\n): SerializedErrorPageProps {\n return { error: serializePageError(serializableError), status: props.status };\n}\n"],"mappings":";;;AAeA,SAAgB,mBAAmB,QAAsC;CACvE,IAAI,kBAAkB,OACpB,OAAO;EACL,MAAM,OAAO,QAAQ;EACrB,SAAS,OAAO;EAChB,GAAI,QAAQ,IAAI,aAAa,gBAAgB,OAAO,OAAO,UAAU,WACjE,EAAE,OAAO,OAAO,MAAM,IACtB,CAAC;CACP;CAGF,IAAI;CACJ,IAAI;EACF,UAAU,OAAO,WAAW,WAAW,SAAS,OAAO,MAAM;CAC/D,QAAQ;EACN,UAAU;CACZ;CAEA,OAAO;EACL,MAAM;EACN;CACF;AACF;;AAGA,SAAgB,yBACd,QACA,OACgB;CAChB,MAAM,MAAM,OAAO,OAAO,aAAa,aAAa,OAAO,SAAS,KAAK,IAAI,OAAO;CAEpF,OAAO;EAAE,GAAG;EAAqB,GAAG;EAAK,QAAQ,KAAK,UAAU,oBAAoB;CAAO;AAC7F;AAEA,SAAgB,wBACd,OACA,oBAA6B,MAAM,OACT;CAC1B,OAAO;EAAE,OAAO,mBAAmB,iBAAiB;EAAG,QAAQ,MAAM;CAAO;AAC9E"}
@@ -3,7 +3,7 @@ import { ExecutePageRequestOptions, PageBoundaryDesignation, PageContextRunner,
3
3
  import { connectPageContext } from "./page-context.mjs";
4
4
  import { Response } from "@warlock.js/core";
5
5
 
6
- //#region web/src/server/execute-page-request.d.ts
6
+ //#region ../web/src/server/execute-page-request.d.ts
7
7
  declare function executePageRequest<TResult = PageDataBundle>(options: ExecutePageRequestOptions<TResult>): Promise<TResult | Response | undefined>;
8
8
  //#endregion
9
9
  export { executePageRequest };
@@ -7,7 +7,7 @@ import { LEVEL_ORDER, buildErrorRecord, commitBuffers, createBufferedResponse, c
7
7
  import { Response } from "@warlock.js/core";
8
8
  import { v } from "@warlock.js/seal";
9
9
 
10
- //#region web/src/server/execute-page-request.ts
10
+ //#region ../web/src/server/execute-page-request.ts
11
11
  async function executePageRequest(options) {
12
12
  const runner = requireRunner();
13
13
  const [pathname, queryString] = options.url.split("?");