@warlock.js/web 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +126 -12
  3. package/esm/build/contribution.d.mts +11 -14
  4. package/esm/build/contribution.mjs +46 -58
  5. package/esm/build/contribution.mjs.map +1 -1
  6. package/esm/build/discover-pages.mjs +105 -45
  7. package/esm/build/discover-pages.mjs.map +1 -1
  8. package/esm/build/generate-client-registry.mjs +47 -13
  9. package/esm/build/generate-client-registry.mjs.map +1 -1
  10. package/esm/build/generate-pages-barrel.mjs +55 -13
  11. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  12. package/esm/build/page-default-export.mjs +48 -0
  13. package/esm/build/page-default-export.mjs.map +1 -0
  14. package/esm/build/page-routes-manifest.mjs +21 -0
  15. package/esm/build/page-routes-manifest.mjs.map +1 -0
  16. package/esm/build/public-files.mjs +45 -0
  17. package/esm/build/public-files.mjs.map +1 -0
  18. package/esm/build/read-route-exports.mjs +1 -1
  19. package/esm/build/read-route-exports.mjs.map +1 -1
  20. package/esm/client/build-hydrated-tree.mjs +40 -9
  21. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  22. package/esm/client/hydrate-page.mjs +1 -1
  23. package/esm/client/hydrate-page.mjs.map +1 -1
  24. package/esm/client/navigation/current-route.d.mts +1 -1
  25. package/esm/client/navigation/current-route.mjs +1 -1
  26. package/esm/client/navigation/current-route.mjs.map +1 -1
  27. package/esm/client/navigation/fetch-page-data.mjs +1 -1
  28. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  29. package/esm/client/navigation/navigation-root.mjs +1 -1
  30. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  31. package/esm/client/navigation/prefetch.mjs +1 -1
  32. package/esm/client/navigation/prefetch.mjs.map +1 -1
  33. package/esm/client/navigation/refresh.d.mts +1 -1
  34. package/esm/client/navigation/refresh.mjs +1 -1
  35. package/esm/client/navigation/refresh.mjs.map +1 -1
  36. package/esm/client/navigation/scroll-to-fragment.mjs +1 -1
  37. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
  38. package/esm/client/navigation/verbs.d.mts +1 -1
  39. package/esm/client/navigation/verbs.mjs +1 -1
  40. package/esm/client/navigation/verbs.mjs.map +1 -1
  41. package/esm/client/runtime/index.d.mts +2 -1
  42. package/esm/client/runtime/index.mjs +2 -1
  43. package/esm/client/runtime/manifest.d.mts +1 -1
  44. package/esm/client/runtime/manifest.mjs +3 -2
  45. package/esm/client/runtime/manifest.mjs.map +1 -1
  46. package/esm/client/runtime/matcher.d.mts +13 -1
  47. package/esm/client/runtime/matcher.mjs +13 -1
  48. package/esm/client/runtime/matcher.mjs.map +1 -1
  49. package/esm/client/runtime/types.d.mts +15 -2
  50. package/esm/components/default-app.mjs +1 -1
  51. package/esm/components/default-app.mjs.map +1 -1
  52. package/esm/components/document-context.d.mts +40 -2
  53. package/esm/components/document-context.mjs +1 -1
  54. package/esm/components/document-context.mjs.map +1 -1
  55. package/esm/components/head.d.mts +1 -1
  56. package/esm/components/head.mjs +1 -1
  57. package/esm/components/head.mjs.map +1 -1
  58. package/esm/components/link.d.mts +1 -1
  59. package/esm/components/link.mjs +1 -1
  60. package/esm/components/link.mjs.map +1 -1
  61. package/esm/components/scripts.d.mts +1 -1
  62. package/esm/components/scripts.mjs +4 -2
  63. package/esm/components/scripts.mjs.map +1 -1
  64. package/esm/context.d.mts +6 -102
  65. package/esm/hydration/index.mjs +1 -1
  66. package/esm/hydration/index.mjs.map +1 -1
  67. package/esm/hydration-payload.d.mts +1 -1
  68. package/esm/hydration-payload.mjs +37 -8
  69. package/esm/hydration-payload.mjs.map +1 -1
  70. package/esm/index.d.mts +4 -3
  71. package/esm/loaders.d.mts +7 -36
  72. package/esm/metadata.d.mts +1 -1
  73. package/esm/metadata.mjs +1 -1
  74. package/esm/metadata.mjs.map +1 -1
  75. package/esm/props.d.mts +16 -8
  76. package/esm/route.d.mts +2 -17
  77. package/esm/routing/compose-route-path.d.mts +1 -1
  78. package/esm/routing/compose-route-path.mjs +1 -1
  79. package/esm/routing/compose-route-path.mjs.map +1 -1
  80. package/esm/routing/data-request.mjs +1 -1
  81. package/esm/routing/data-request.mjs.map +1 -1
  82. package/esm/routing/filesystem-route.mjs +45 -0
  83. package/esm/routing/filesystem-route.mjs.map +1 -0
  84. package/esm/routing/layout-policy.mjs +1 -1
  85. package/esm/routing/layout-policy.mjs.map +1 -1
  86. package/esm/routing/navigator.mjs +1 -1
  87. package/esm/routing/navigator.mjs.map +1 -1
  88. package/esm/routing/query-string.d.mts +1 -1
  89. package/esm/routing/query-string.mjs +1 -1
  90. package/esm/routing/query-string.mjs.map +1 -1
  91. package/esm/routing/route-identity.mjs +1 -1
  92. package/esm/routing/route-identity.mjs.map +1 -1
  93. package/esm/routing/route-table.d.mts +1 -1
  94. package/esm/routing/route-table.mjs +1 -1
  95. package/esm/routing/route-table.mjs.map +1 -1
  96. package/esm/routing/router-events.d.mts +1 -1
  97. package/esm/routing/router-events.mjs +1 -1
  98. package/esm/routing/router-events.mjs.map +1 -1
  99. package/esm/routing/url-fragment.mjs +1 -1
  100. package/esm/routing/url-fragment.mjs.map +1 -1
  101. package/esm/runtime/register-modules.d.mts +19 -0
  102. package/esm/runtime/register-modules.mjs +22 -0
  103. package/esm/runtime/register-modules.mjs.map +1 -0
  104. package/esm/server/build-hydration-payload.mjs +2 -1
  105. package/esm/server/build-hydration-payload.mjs.map +1 -1
  106. package/esm/server/client-asset-url-prefix.d.mts +1 -1
  107. package/esm/server/client-asset-url-prefix.mjs +1 -1
  108. package/esm/server/client-asset-url-prefix.mjs.map +1 -1
  109. package/esm/server/create-page-module-loader.d.mts +1 -1
  110. package/esm/server/create-page-module-loader.mjs +2 -1
  111. package/esm/server/create-page-module-loader.mjs.map +1 -1
  112. package/esm/server/create-page-route-handler.d.mts +31 -6
  113. package/esm/server/create-page-route-handler.mjs +128 -43
  114. package/esm/server/create-page-route-handler.mjs.map +1 -1
  115. package/esm/server/dev-server.mjs +2 -13
  116. package/esm/server/dev-server.mjs.map +1 -1
  117. package/esm/server/error-page.d.mts +14 -0
  118. package/esm/server/error-page.mjs +39 -0
  119. package/esm/server/error-page.mjs.map +1 -0
  120. package/esm/server/execute-page-request.d.mts +7 -4
  121. package/esm/server/execute-page-request.mjs +79 -80
  122. package/esm/server/execute-page-request.mjs.map +1 -1
  123. package/esm/server/execute-page-request.types.d.mts +33 -92
  124. package/esm/server/hydration-client-url.d.mts +1 -1
  125. package/esm/server/hydration-client-url.mjs +2 -2
  126. package/esm/server/hydration-client-url.mjs.map +1 -1
  127. package/esm/server/index.d.mts +5 -5
  128. package/esm/server/index.mjs +3 -4
  129. package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
  130. package/esm/server/install-page-routes-from-manifest.mjs +71 -10
  131. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  132. package/esm/server/install-page-routes.d.mts +32 -15
  133. package/esm/server/install-page-routes.mjs +91 -39
  134. package/esm/server/install-page-routes.mjs.map +1 -1
  135. package/esm/server/install-production-page-routes.mjs +3 -36
  136. package/esm/server/install-production-page-routes.mjs.map +1 -1
  137. package/esm/server/match-page-route.mjs +2 -2
  138. package/esm/server/match-page-route.mjs.map +1 -1
  139. package/esm/server/not-found-page.d.mts +1 -1
  140. package/esm/server/not-found-page.mjs +2 -2
  141. package/esm/server/not-found-page.mjs.map +1 -1
  142. package/esm/server/page-context.d.mts +3 -9
  143. package/esm/server/page-context.mjs +3 -16
  144. package/esm/server/page-context.mjs.map +1 -1
  145. package/esm/server/page-file-change.mjs +77 -0
  146. package/esm/server/page-file-change.mjs.map +1 -0
  147. package/esm/server/page-manifest.d.mts +25 -10
  148. package/esm/server/page-manifest.mjs +1 -1
  149. package/esm/server/page-manifest.mjs.map +1 -1
  150. package/esm/server/page-render-bundle.mjs +38 -0
  151. package/esm/server/page-render-bundle.mjs.map +1 -0
  152. package/esm/server/page-route-reload.mjs +54 -0
  153. package/esm/server/page-route-reload.mjs.map +1 -0
  154. package/esm/server/register-production-public-files.mjs +40 -0
  155. package/esm/server/register-production-public-files.mjs.map +1 -0
  156. package/esm/server/render-page.d.mts +11 -14
  157. package/esm/server/render-page.mjs +154 -28
  158. package/esm/server/render-page.mjs.map +1 -1
  159. package/esm/server/resolve-page-metadata.mjs +2 -2
  160. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  161. package/esm/server/resolve-validation-data.mjs +1 -1
  162. package/esm/server/resolve-validation-data.mjs.map +1 -1
  163. package/esm/server/settle-page-response.d.mts +58 -1
  164. package/esm/server/settle-page-response.mjs +111 -46
  165. package/esm/server/settle-page-response.mjs.map +1 -1
  166. package/esm/server/stylesheet-urls.d.mts +41 -30
  167. package/esm/server/stylesheet-urls.mjs +137 -52
  168. package/esm/server/stylesheet-urls.mjs.map +1 -1
  169. package/esm/server/unregistered-pages.mjs +42 -0
  170. package/esm/server/unregistered-pages.mjs.map +1 -0
  171. package/esm/server/web-connector-factory.d.mts +2 -3
  172. package/esm/server/web-connector-factory.mjs +7 -6
  173. package/esm/server/web-connector-factory.mjs.map +1 -1
  174. package/esm/server/web-connector.d.mts +1 -1
  175. package/esm/server/web-connector.mjs +138 -18
  176. package/esm/server/web-connector.mjs.map +1 -1
  177. package/esm/shared.d.mts +18 -3
  178. package/esm/shared.mjs +3 -7
  179. package/esm/shared.mjs.map +1 -1
  180. package/esm/validation.d.mts +1 -1
  181. package/esm/vite/app-convention-aliases.mjs +1 -1
  182. package/esm/vite/app-convention-aliases.mjs.map +1 -1
  183. package/esm/vite/build-client.d.mts +1 -1
  184. package/esm/vite/build-client.mjs +2 -2
  185. package/esm/vite/build-client.mjs.map +1 -1
  186. package/esm/vite/gate-a-resolve.d.mts +1 -1
  187. package/esm/vite/gate-a-resolve.mjs +67 -13
  188. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  189. package/esm/vite/gate-b-secrets.d.mts +1 -1
  190. package/esm/vite/gate-b-secrets.mjs +35 -17
  191. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  192. package/esm/vite/gate-c-verify.d.mts +1 -1
  193. package/esm/vite/gate-c-verify.mjs +6 -6
  194. package/esm/vite/gate-c-verify.mjs.map +1 -1
  195. package/esm/vite/hydration-entries.d.mts +1 -1
  196. package/esm/vite/hydration-entries.mjs +2 -2
  197. package/esm/vite/hydration-entries.mjs.map +1 -1
  198. package/esm/vite/index.d.mts +9 -6
  199. package/esm/vite/index.mjs +124 -11
  200. package/esm/vite/index.mjs.map +1 -1
  201. package/esm/vite/page-registry-plugin.d.mts +22 -3
  202. package/esm/vite/page-registry-plugin.mjs +48 -14
  203. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  204. package/esm/vite/projection.d.mts +2 -2
  205. package/esm/vite/projection.mjs +38 -15
  206. package/esm/vite/projection.mjs.map +1 -1
  207. package/llms-full.txt +220 -56
  208. package/llms.txt +5 -3
  209. package/package.json +3 -3
  210. package/skills/add-web-to-an-app/SKILL.md +3 -3
  211. package/skills/create-a-page/SKILL.md +117 -10
  212. package/skills/load-page-data/SKILL.md +92 -12
  213. package/skills/navigate-on-the-client/SKILL.md +1 -1
  214. package/skills/serve-styles/SKILL.md +13 -12
  215. package/skills/use-layouts/SKILL.md +14 -8
  216. package/skills/write-the-root/SKILL.md +3 -1
  217. package/esm/server/buffered-response.d.mts +0 -58
  218. package/esm/server/buffered-response.mjs +0 -115
  219. package/esm/server/buffered-response.mjs.map +0 -1
package/esm/context.d.mts CHANGED
@@ -1,107 +1,11 @@
1
1
  import { SharedContext } from "./index.mjs";
2
+ import { HttpContext, Request } from "@warlock.js/core";
2
3
 
3
- //#region ../web/src/context.d.ts
4
- /**
5
- * The brand behind loader short-circuits.
6
- *
7
- * Core's `Response#notFound()` / `#redirect()` return an unbranded
8
- * `Response`/`this` (core/src/http/response.ts:862-866, 1028-1034). The page
9
- * contract needs those returns to NOT union into `PageProps["data"]`
10
- * (v5/app/src/app/products/web/product-details.page.tsx:45-57), so web brands
11
- * them on ITS OWN response surface — the one a loader's context carries —
12
- * without touching core. `LoaderData` excludes exactly this brand.
13
- */
14
- declare const loaderShortCircuit: unique symbol;
15
- interface LoaderShortCircuit {
16
- readonly [loaderShortCircuit]: true;
17
- }
18
- /**
19
- * The request as a loader sees it. Structural and deliberately minimal for M1:
20
- * exactly the members the v5/app loaders exercise, typed by the page's own
21
- * `validation` and `route` generics.
22
- *
23
- * TODO(Stream A.6 — core ctx migration): once core ships
24
- * `HttpContext { request, response }` and the declared `request.user` /
25
- * `request.locals` / `request.nonce`, this narrows to a refinement of core's `Request`
26
- * instead of a standalone structural type.
27
- */
28
- interface WebRequest<TValidated = Record<string, never>, TParams extends string = string> {
29
- /**
30
- * The validated inputs, typed off the page's `validation.schema` with
31
- * `Infer.Output` semantics — `.default()` fields stay required
32
- * (v5/app/src/app/auth/schema/login.schema.ts:29-38). The key-list parameter
33
- * is optional and selects a subset, mirroring core
34
- * (core/src/http/request.ts:610).
35
- */
36
- validated(inputs?: readonly (keyof TValidated | (string & {}))[]): TValidated;
37
- /**
38
- * Typed FROM THE ROUTE: `TParams` is derived from the route path literal, so
39
- * `request.input("id")` types against `path: "/:id"` and a key that is not a
40
- * route parameter is a compile error
41
- * (v5/app/src/web/__type-tests__/route-params.type-test.ts).
42
- */
43
- input(key: TParams, defaultValue?: unknown): string;
44
- /**
45
- * The authenticated user, when middleware resolved one.
46
- *
47
- * `any`, deliberately, and recorded as a deviation in the M1 report: the
48
- * reference app hands `request.user` to services typed against the APP's own
49
- * model (`navService.forUser(user: User)`,
50
- * v5/app/src/web/layouts/dashboard.layout.tsx:35) and reads `.id` with no
51
- * null-check (settings.page.tsx:65). No type this package can declare is
52
- * assignable to an app-owned model class; the real declaration has to come
53
- * from core's own `Request.user` and lands with Stream A.6.
54
- *
55
- * Optional, matching core (`Request.user?`, core/src/http/request.ts:94) —
56
- * a required member here would reject core's real `Request` at the seam.
57
- * `any` absorbs the `undefined`, so loader-side `request.user.id` reads
58
- * keep compiling (the recorded M1 deviation stands).
59
- */
60
- user?: any;
61
- }
62
- /**
63
- * The response as a loader sees it. A loader may mutate the response during
64
- * render (headers, status) and may SHORT-CIRCUIT it — the short-circuiting
65
- * methods return the branded type that never reaches `PageProps["data"]`.
66
- *
67
- * TODO(Stream A.6): same story as `WebRequest` — becomes a refinement of
68
- * core's `Response` once the ctx object lands in core.
69
- */
70
- interface WebResponse {
71
- /** Buffered, committed in tree order after every loader settles. */
72
- header(key: string, value: string): WebResponse;
73
- /** The not-found page's loader's real output is the status (not-found.page.tsx:25). */
74
- setStatusCode(statusCode: number): WebResponse;
75
- /** A short-circuit, not a value — never unions into `data`. */
76
- redirect(url: string, statusCode?: number): LoaderShortCircuit;
77
- /** 301 flavour of the same short-circuit. */
78
- permanentRedirect(url: string): LoaderShortCircuit;
79
- /** A 404 is an ANSWER, not an incident (product-details.page.tsx:45-57). */
80
- notFound(body?: unknown): LoaderShortCircuit;
81
- }
82
- /**
83
- * The one context object every handler receives.
84
- *
85
- * TODO(Stream A.6): core is to OWN `HttpContext { request, response }` and the
86
- * re-parameterised `Middleware<TContext extends HttpContext>`. Core today has
87
- * no `HttpContext` and its `Middleware` is positional `(request, response)`
88
- * (core/src/router/types.ts:17), so importing the type from
89
- * `@warlock.js/core` cannot resolve to the v5 shape yet — this structural
90
- * declaration stands in until core declares it.
91
- */
92
- interface HttpContext {
93
- request: WebRequest;
94
- response: WebResponse;
95
- }
96
- /**
97
- * The web layer's context: `HttpContext` plus the per-request payload. The
98
- * keys are framework-owned and the list is closed — `request`, `response`,
99
- * `shared`. App data goes on `request.locals` (private) or `shared` (public),
100
- * never onto the context itself.
101
- */
102
- interface PageContext extends HttpContext {
4
+ //#region web/src/context.d.ts
5
+ type HttpContext$1<TRequest extends Request = Request> = HttpContext<TRequest>;
6
+ type PageContext<TRequest extends Request = Request> = HttpContext<TRequest> & {
103
7
  shared: SharedContext;
104
- }
8
+ };
105
9
  //#endregion
106
- export { LoaderShortCircuit, PageContext, WebRequest, WebResponse };
10
+ export { HttpContext$1 as HttpContext, PageContext };
107
11
  //# sourceMappingURL=context.d.mts.map
@@ -5,7 +5,7 @@ import { NavigationRoot } from "../client/navigation/navigation-root.mjs";
5
5
  import { createElement } from "react";
6
6
  import { pages } from "virtual:warlock/pages";
7
7
 
8
- //#region ../web/src/hydration/index.ts
8
+ //#region web/src/hydration/index.ts
9
9
  /**
10
10
  * The client entry: registry in, hydration out. No page names, no app paths.
11
11
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../web/src/hydration/index.ts"],"sourcesContent":["/**\n * The client entry: registry in, hydration out. No page names, no app paths.\n *\n * The page graph arrives from the Vite virtual module the build plugin serves,\n * so this file works for ANY page in ANY app. The previous version imported one\n * page and one layout by relative path into the monorepo's reference app, which\n * both hydrated every URL as the home page and made `@warlock.js/web`\n * uninstallable anywhere outside this checkout. Neither a page name nor an app\n * path appears here now, and nothing about the composition lives here either -\n * that is `buildHydratedTree`, which takes the registry as an argument and is\n * therefore testable without a bundler.\n */\nimport { createElement } from \"react\";\nimport { pages } from \"virtual:warlock/pages\";\nimport { buildHydratedTree } from \"../client/build-hydrated-tree\";\nimport { hydratePage } from \"../client/hydrate-page\";\nimport { NavigationRoot } from \"../client/navigation/navigation-root\";\nimport { publishRouteTable } from \"../routing/route-table\";\n\n/*\n BEFORE the mount, not after: `<Link>` resolves its URL through the route table\n during render, and the first render is the hydration render. Publishing\n afterwards would make every anchor in the initial tree throw.\n\n The registry entries already carry `name` and `path` - the same pair the\n server registered its routes from, out of the same discovery result - so the\n browser's table cannot drift from the server's without the two being built\n from different page graphs, which hydration already refuses.\n*/\npublishRouteTable(pages, \"hydration client entry\");\n\n/*\n The hydrated tree is wrapped in `NavigationRoot` so the page can be REPLACED\n later without a document load. The first render is still exactly the tree the\n server produced - `NavigationRoot` renders `initialTree` verbatim and adds no\n markup of its own - so hydration still matches the server byte for byte, and\n the wrapper only starts to matter on the first navigation.\n\n NO MRR HISTORY BRIDGE IS INSTALLED HERE YET, and that is a pending decision\n rather than an oversight. This runtime drives `window.history` itself and is\n complete without MRR. Handing history to `@mongez/react-router` as well means\n `@warlock.js/web` importing it, which puts MRR in the bundle of EVERY app that\n uses this package - a packaging choice (dependency vs. peer vs. app-level\n opt-in) with consequences for apps that never navigate through MRR. The bridge\n itself is written and documented in\n `conversations/2026-08-24-production-ssr-session.md`; it lands the moment that\n choice is made.\n*/\nhydratePage(async (payload) => {\n const tree = await buildHydratedTree(pages, payload);\n\n return createElement(NavigationRoot, {\n pages,\n initialPayload: payload,\n initialTree: tree,\n buildTree: buildHydratedTree,\n });\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA6BA,kBAAkB,OAAO,wBAAwB;AAmBjD,YAAY,OAAO,YAAY;CAG7B,OAAO,cAAc,gBAAgB;EACnC;EACA,gBAAgB;EAChB,aAAa,MALI,kBAAkB,OAAO,OAAO;EAMjD,WAAW;CACb,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/hydration/index.ts"],"sourcesContent":["/**\n * The client entry: registry in, hydration out. No page names, no app paths.\n *\n * The page graph arrives from the Vite virtual module the build plugin serves,\n * so this file works for ANY page in ANY app. The previous version imported one\n * page and one layout by relative path into the monorepo's reference app, which\n * both hydrated every URL as the home page and made `@warlock.js/web`\n * uninstallable anywhere outside this checkout. Neither a page name nor an app\n * path appears here now, and nothing about the composition lives here either -\n * that is `buildHydratedTree`, which takes the registry as an argument and is\n * therefore testable without a bundler.\n */\nimport { createElement } from \"react\";\nimport { pages } from \"virtual:warlock/pages\";\nimport { buildHydratedTree } from \"../client/build-hydrated-tree\";\nimport { hydratePage } from \"../client/hydrate-page\";\nimport { NavigationRoot } from \"../client/navigation/navigation-root\";\nimport { publishRouteTable } from \"../routing/route-table\";\n\n/*\n BEFORE the mount, not after: `<Link>` resolves its URL through the route table\n during render, and the first render is the hydration render. Publishing\n afterwards would make every anchor in the initial tree throw.\n\n The registry entries already carry `name` and `path` - the same pair the\n server registered its routes from, out of the same discovery result - so the\n browser's table cannot drift from the server's without the two being built\n from different page graphs, which hydration already refuses.\n*/\npublishRouteTable(pages, \"hydration client entry\");\n\n/*\n The hydrated tree is wrapped in `NavigationRoot` so the page can be REPLACED\n later without a document load. The first render is still exactly the tree the\n server produced - `NavigationRoot` renders `initialTree` verbatim and adds no\n markup of its own - so hydration still matches the server byte for byte, and\n the wrapper only starts to matter on the first navigation.\n\n NO MRR HISTORY BRIDGE IS INSTALLED HERE YET, and that is a pending decision\n rather than an oversight. This runtime drives `window.history` itself and is\n complete without MRR. Handing history to `@mongez/react-router` as well means\n `@warlock.js/web` importing it, which puts MRR in the bundle of EVERY app that\n uses this package - a packaging choice (dependency vs. peer vs. app-level\n opt-in) with consequences for apps that never navigate through MRR. The bridge\n itself is written and documented in\n `conversations/2026-08-24-production-ssr-session.md`; it lands the moment that\n choice is made.\n*/\nhydratePage(async (payload) => {\n const tree = await buildHydratedTree(pages, payload);\n\n return createElement(NavigationRoot, {\n pages,\n initialPayload: payload,\n initialTree: tree,\n buildTree: buildHydratedTree,\n });\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA6BA,kBAAkB,OAAO,wBAAwB;AAmBjD,YAAY,OAAO,YAAY;CAG7B,OAAO,cAAc,gBAAgB;EACnC;EACA,gBAAgB;EAChB,aAAa,MALI,kBAAkB,OAAO,OAAO;EAMjD,WAAW;CACb,CAAC;AACH,CAAC"}
@@ -1 +1 @@
1
- export { };
1
+ import { ErrorPageProps, SerializedErrorPageProps, SerializedPageError } from "./components/document-context.mjs";
@@ -1,6 +1,6 @@
1
1
  import { PAYLOAD_SCRIPT_ID } from "./components/document-context.mjs";
2
2
 
3
- //#region ../web/src/hydration-payload.ts
3
+ //#region web/src/hydration-payload.ts
4
4
  /**
5
5
  * Exported so a payload-shape assertion can be written against the contract
6
6
  * itself. A spec that hardcodes its own copy of this list silently becomes a
@@ -22,11 +22,11 @@ function malformedPayload() {
22
22
  /**
23
23
  * The keys that are allowed to be ABSENT but not allowed to be wrong.
24
24
  *
25
- * `metadata` and `params` are optional because the server is right not to
25
+ * `metadata`, `params` and `errorPage` are optional because the server is right
26
26
  * always produce them — a page with no `metadata` export resolves none, and a
27
- * payload written by a build that predates these keys carries neither. Failing
28
- * a whole page over an absent accessor would turn a cosmetic gap into a blank
29
- * screen, so absence is accepted and the readers default it.
27
+ * older payload carries none of these additions. Failing a whole page over an
28
+ * absent accessor would turn a compatible payload into a blank screen, so
29
+ * absence is accepted.
30
30
  *
31
31
  * Present-but-not-an-object is a different claim entirely: it means something
32
32
  * produced a payload with these names meaning something else, and every reader
@@ -34,10 +34,36 @@ function malformedPayload() {
34
34
  * rule the required keys live by, so it throws. Arrays included — `typeof []`
35
35
  * is `"object"`, and an array of params is not params.
36
36
  */
37
- const OPTIONAL_OBJECT_PAYLOAD_KEYS = ["metadata", "params"];
37
+ const OPTIONAL_OBJECT_PAYLOAD_KEYS = [
38
+ "metadata",
39
+ "params",
40
+ "errorPage"
41
+ ];
38
42
  function isPlainObject(value) {
39
43
  return typeof value === "object" && value !== null && !Array.isArray(value);
40
44
  }
45
+ function hasExactStringKeys(value, required, optional = []) {
46
+ const allowed = new Set([...required, ...optional]);
47
+ const keys = Reflect.ownKeys(value);
48
+ return required.every((key) => Object.prototype.hasOwnProperty.call(value, key)) && keys.every((key) => typeof key === "string" && allowed.has(key));
49
+ }
50
+ /**
51
+ * Validate the explicit serialization boundary, not an `Error` instance.
52
+ * `JSON.stringify(new Error("boom"))` is normally `{}` because its useful
53
+ * fields are non-enumerable; accepting that would hydrate an error page with a
54
+ * different contract from the one the server rendered.
55
+ */
56
+ function requireErrorPagePayload(value) {
57
+ if (!isPlainObject(value)) malformedPayload();
58
+ const errorPage = value;
59
+ if (!hasExactStringKeys(errorPage, ["error", "status"])) malformedPayload();
60
+ if (!isPlainObject(errorPage.error)) malformedPayload();
61
+ const error = errorPage.error;
62
+ if (!hasExactStringKeys(error, ["name", "message"], ["stack"])) malformedPayload();
63
+ if (typeof error.name !== "string" || typeof error.message !== "string") malformedPayload();
64
+ if (error.stack !== void 0 && typeof error.stack !== "string") malformedPayload();
65
+ if (typeof errorPage.status !== "number" || !Number.isInteger(errorPage.status) || errorPage.status < 500 || errorPage.status > 599) malformedPayload();
66
+ }
41
67
  function requireHydrationPayload(value) {
42
68
  if (!isPlainObject(value)) malformedPayload();
43
69
  for (const key of REQUIRED_PAYLOAD_KEYS) if (!Object.prototype.hasOwnProperty.call(value, key)) malformedPayload();
@@ -45,15 +71,18 @@ function requireHydrationPayload(value) {
45
71
  const optional = value[key];
46
72
  if (optional !== void 0 && !isPlainObject(optional)) malformedPayload();
47
73
  }
74
+ const errorPage = value.errorPage;
75
+ if (errorPage !== void 0) requireErrorPagePayload(errorPage);
48
76
  return value;
49
77
  }
50
78
  /**
51
79
  * Read the fixed payload script without changing the server-rendered root.
52
80
  *
53
81
  * Extra fields are ignored. The gate owns the FIVE required keys — absent or
54
- * malformed, both throw — plus a shape check on the two optional ones
82
+ * malformed, both throw — plus a shape check on the three optional ones
55
83
  * ({@link OPTIONAL_OBJECT_PAYLOAD_KEYS}); it deliberately does not require
56
- * those to be present.
84
+ * those to be present. `errorPage`, when present, is additionally validated as
85
+ * one atomic `{ error, status }` selection with a serialized error and a 5xx.
57
86
  */
58
87
  function readHydrationPayload(documentNode) {
59
88
  const element = documentNode.getElementById(PAYLOAD_SCRIPT_ID);
@@ -1 +1 @@
1
- {"version":3,"file":"hydration-payload.mjs","names":[],"sources":["../../../../../../web/src/hydration-payload.ts"],"sourcesContent":["import {\n PAYLOAD_SCRIPT_ID,\n type HydrationDocumentPayloadSource,\n} from \"./components/document-context\";\n\nexport type { HydrationDocumentPayloadSource } from \"./components/document-context\";\n\n/**\n * Exported so a payload-shape assertion can be written against the contract\n * itself. A spec that hardcodes its own copy of this list silently becomes a\n * claim about a PAST revision — that is exactly how the rev. 3 keys landed with\n * two specs still asserting the rev. 2 shape.\n */\nexport const REQUIRED_PAYLOAD_KEYS = [\n \"appData\",\n \"layoutData\",\n \"pageData\",\n \"shared\",\n \"name\",\n] as const;\n\nconst ABSENT_PAYLOAD_MESSAGE =\n `Warlock hydration payload is absent: #${PAYLOAD_SCRIPT_ID}, owned by ` +\n \"web/src/components/document-context.ts, was not found.\";\nconst MALFORMED_PAYLOAD_MESSAGE =\n `Warlock hydration payload was found at #${PAYLOAD_SCRIPT_ID} but could not be read.`;\n\nfunction malformedPayload(): never {\n throw new Error(MALFORMED_PAYLOAD_MESSAGE);\n}\n\n/**\n * The keys that are allowed to be ABSENT but not allowed to be wrong.\n *\n * `metadata` and `params` are optional because the server is right not to\n * always produce them — a page with no `metadata` export resolves none, and a\n * payload written by a build that predates these keys carries neither. Failing\n * a whole page over an absent accessor would turn a cosmetic gap into a blank\n * screen, so absence is accepted and the readers default it.\n *\n * Present-but-not-an-object is a different claim entirely: it means something\n * produced a payload with these names meaning something else, and every reader\n * downstream would then be indexing a string. That is MALFORMED under the same\n * rule the required keys live by, so it throws. Arrays included — `typeof []`\n * is `\"object\"`, and an array of params is not params.\n */\nexport const OPTIONAL_OBJECT_PAYLOAD_KEYS = [\"metadata\", \"params\"] as const;\n\nfunction isPlainObject(value: unknown): boolean {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction requireHydrationPayload(value: unknown): HydrationDocumentPayloadSource {\n if (!isPlainObject(value)) malformedPayload();\n\n for (const key of REQUIRED_PAYLOAD_KEYS) {\n if (!Object.prototype.hasOwnProperty.call(value, key)) malformedPayload();\n }\n\n for (const key of OPTIONAL_OBJECT_PAYLOAD_KEYS) {\n const optional = (value as Record<string, unknown>)[key];\n\n if (optional !== undefined && !isPlainObject(optional)) malformedPayload();\n }\n\n return value as HydrationDocumentPayloadSource;\n}\n\n/**\n * Read the fixed payload script without changing the server-rendered root.\n *\n * Extra fields are ignored. The gate owns the FIVE required keys — absent or\n * malformed, both throw — plus a shape check on the two optional ones\n * ({@link OPTIONAL_OBJECT_PAYLOAD_KEYS}); it deliberately does not require\n * those to be present.\n */\nexport function readHydrationPayload(documentNode: Document): HydrationDocumentPayloadSource {\n const element = documentNode.getElementById(PAYLOAD_SCRIPT_ID);\n\n if (element === null) throw new Error(ABSENT_PAYLOAD_MESSAGE);\n\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(element.textContent ?? \"\");\n } catch {\n malformedPayload();\n }\n\n return requireHydrationPayload(parsed);\n}\n"],"mappings":";;;;;;;;;AAaA,MAAa,wBAAwB;CACnC;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,yBACJ,yCAAyC,kBAAkB;AAE7D,MAAM,4BACJ,2CAA2C,kBAAkB;AAE/D,SAAS,mBAA0B;CACjC,MAAM,IAAI,MAAM,yBAAyB;AAC3C;;;;;;;;;;;;;;;;AAiBA,MAAa,+BAA+B,CAAC,YAAY,QAAQ;AAEjE,SAAS,cAAc,OAAyB;CAC9C,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,wBAAwB,OAAgD;CAC/E,IAAI,CAAC,cAAc,KAAK,GAAG,iBAAiB;CAE5C,KAAK,MAAM,OAAO,uBAChB,IAAI,CAAC,OAAO,UAAU,eAAe,KAAK,OAAO,GAAG,GAAG,iBAAiB;CAG1E,KAAK,MAAM,OAAO,8BAA8B;EAC9C,MAAM,WAAY,MAAkC;EAEpD,IAAI,aAAa,UAAa,CAAC,cAAc,QAAQ,GAAG,iBAAiB;CAC3E;CAEA,OAAO;AACT;;;;;;;;;AAUA,SAAgB,qBAAqB,cAAwD;CAC3F,MAAM,UAAU,aAAa,eAAe,iBAAiB;CAE7D,IAAI,YAAY,MAAM,MAAM,IAAI,MAAM,sBAAsB;CAE5D,IAAI;CAEJ,IAAI;EACF,SAAS,KAAK,MAAM,QAAQ,eAAe,EAAE;CAC/C,QAAQ;EACN,iBAAiB;CACnB;CAEA,OAAO,wBAAwB,MAAM;AACvC"}
1
+ {"version":3,"file":"hydration-payload.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/hydration-payload.ts"],"sourcesContent":["import {\n PAYLOAD_SCRIPT_ID,\n type HydrationDocumentPayloadSource,\n} from \"./components/document-context\";\n\nexport type { HydrationDocumentPayloadSource } from \"./components/document-context\";\nexport type {\n ErrorPageProps,\n SerializedErrorPageProps,\n SerializedPageError,\n} from \"./components/document-context\";\n\n/**\n * Exported so a payload-shape assertion can be written against the contract\n * itself. A spec that hardcodes its own copy of this list silently becomes a\n * claim about a PAST revision — that is exactly how the rev. 3 keys landed with\n * two specs still asserting the rev. 2 shape.\n */\nexport const REQUIRED_PAYLOAD_KEYS = [\n \"appData\",\n \"layoutData\",\n \"pageData\",\n \"shared\",\n \"name\",\n] as const;\n\nconst ABSENT_PAYLOAD_MESSAGE =\n `Warlock hydration payload is absent: #${PAYLOAD_SCRIPT_ID}, owned by ` +\n \"web/src/components/document-context.ts, was not found.\";\nconst MALFORMED_PAYLOAD_MESSAGE =\n `Warlock hydration payload was found at #${PAYLOAD_SCRIPT_ID} but could not be read.`;\n\nfunction malformedPayload(): never {\n throw new Error(MALFORMED_PAYLOAD_MESSAGE);\n}\n\n/**\n * The keys that are allowed to be ABSENT but not allowed to be wrong.\n *\n * `metadata`, `params` and `errorPage` are optional because the server is right\n * always produce them — a page with no `metadata` export resolves none, and a\n * older payload carries none of these additions. Failing a whole page over an\n * absent accessor would turn a compatible payload into a blank screen, so\n * absence is accepted.\n *\n * Present-but-not-an-object is a different claim entirely: it means something\n * produced a payload with these names meaning something else, and every reader\n * downstream would then be indexing a string. That is MALFORMED under the same\n * rule the required keys live by, so it throws. Arrays included — `typeof []`\n * is `\"object\"`, and an array of params is not params.\n */\nexport const OPTIONAL_OBJECT_PAYLOAD_KEYS = [\"metadata\", \"params\", \"errorPage\"] as const;\n\nfunction isPlainObject(value: unknown): boolean {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasExactStringKeys(\n value: Record<PropertyKey, unknown>,\n required: readonly string[],\n optional: readonly string[] = [],\n): boolean {\n const allowed = new Set([...required, ...optional]);\n const keys = Reflect.ownKeys(value);\n\n return (\n required.every((key) => Object.prototype.hasOwnProperty.call(value, key)) &&\n keys.every((key) => typeof key === \"string\" && allowed.has(key))\n );\n}\n\n/**\n * Validate the explicit serialization boundary, not an `Error` instance.\n * `JSON.stringify(new Error(\"boom\"))` is normally `{}` because its useful\n * fields are non-enumerable; accepting that would hydrate an error page with a\n * different contract from the one the server rendered.\n */\nfunction requireErrorPagePayload(value: unknown): void {\n if (!isPlainObject(value)) malformedPayload();\n\n const errorPage = value as Record<PropertyKey, unknown>;\n\n if (!hasExactStringKeys(errorPage, [\"error\", \"status\"])) malformedPayload();\n if (!isPlainObject(errorPage.error)) malformedPayload();\n\n const error = errorPage.error as Record<PropertyKey, unknown>;\n\n if (!hasExactStringKeys(error, [\"name\", \"message\"], [\"stack\"])) malformedPayload();\n if (typeof error.name !== \"string\" || typeof error.message !== \"string\") {\n malformedPayload();\n }\n if (error.stack !== undefined && typeof error.stack !== \"string\") malformedPayload();\n\n if (\n typeof errorPage.status !== \"number\" ||\n !Number.isInteger(errorPage.status) ||\n errorPage.status < 500 ||\n errorPage.status > 599\n ) {\n malformedPayload();\n }\n}\n\nfunction requireHydrationPayload(value: unknown): HydrationDocumentPayloadSource {\n if (!isPlainObject(value)) malformedPayload();\n\n for (const key of REQUIRED_PAYLOAD_KEYS) {\n if (!Object.prototype.hasOwnProperty.call(value, key)) malformedPayload();\n }\n\n for (const key of OPTIONAL_OBJECT_PAYLOAD_KEYS) {\n const optional = (value as Record<string, unknown>)[key];\n\n if (optional !== undefined && !isPlainObject(optional)) malformedPayload();\n }\n\n const errorPage = (value as Record<string, unknown>).errorPage;\n if (errorPage !== undefined) requireErrorPagePayload(errorPage);\n\n return value as HydrationDocumentPayloadSource;\n}\n\n/**\n * Read the fixed payload script without changing the server-rendered root.\n *\n * Extra fields are ignored. The gate owns the FIVE required keys — absent or\n * malformed, both throw — plus a shape check on the three optional ones\n * ({@link OPTIONAL_OBJECT_PAYLOAD_KEYS}); it deliberately does not require\n * those to be present. `errorPage`, when present, is additionally validated as\n * one atomic `{ error, status }` selection with a serialized error and a 5xx.\n */\nexport function readHydrationPayload(documentNode: Document): HydrationDocumentPayloadSource {\n const element = documentNode.getElementById(PAYLOAD_SCRIPT_ID);\n\n if (element === null) throw new Error(ABSENT_PAYLOAD_MESSAGE);\n\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(element.textContent ?? \"\");\n } catch {\n malformedPayload();\n }\n\n return requireHydrationPayload(parsed);\n}\n"],"mappings":";;;;;;;;;AAkBA,MAAa,wBAAwB;CACnC;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,yBACJ,yCAAyC,kBAAkB;AAE7D,MAAM,4BACJ,2CAA2C,kBAAkB;AAE/D,SAAS,mBAA0B;CACjC,MAAM,IAAI,MAAM,yBAAyB;AAC3C;;;;;;;;;;;;;;;;AAiBA,MAAa,+BAA+B;CAAC;CAAY;CAAU;AAAW;AAE9E,SAAS,cAAc,OAAyB;CAC9C,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,mBACP,OACA,UACA,WAA8B,CAAC,GACtB;CACT,MAAM,UAAU,IAAI,IAAI,CAAC,GAAG,UAAU,GAAG,QAAQ,CAAC;CAClD,MAAM,OAAO,QAAQ,QAAQ,KAAK;CAElC,OACE,SAAS,OAAO,QAAQ,OAAO,UAAU,eAAe,KAAK,OAAO,GAAG,CAAC,KACxE,KAAK,OAAO,QAAQ,OAAO,QAAQ,YAAY,QAAQ,IAAI,GAAG,CAAC;AAEnE;;;;;;;AAQA,SAAS,wBAAwB,OAAsB;CACrD,IAAI,CAAC,cAAc,KAAK,GAAG,iBAAiB;CAE5C,MAAM,YAAY;CAElB,IAAI,CAAC,mBAAmB,WAAW,CAAC,SAAS,QAAQ,CAAC,GAAG,iBAAiB;CAC1E,IAAI,CAAC,cAAc,UAAU,KAAK,GAAG,iBAAiB;CAEtD,MAAM,QAAQ,UAAU;CAExB,IAAI,CAAC,mBAAmB,OAAO,CAAC,QAAQ,SAAS,GAAG,CAAC,OAAO,CAAC,GAAG,iBAAiB;CACjF,IAAI,OAAO,MAAM,SAAS,YAAY,OAAO,MAAM,YAAY,UAC7D,iBAAiB;CAEnB,IAAI,MAAM,UAAU,UAAa,OAAO,MAAM,UAAU,UAAU,iBAAiB;CAEnF,IACE,OAAO,UAAU,WAAW,YAC5B,CAAC,OAAO,UAAU,UAAU,MAAM,KAClC,UAAU,SAAS,OACnB,UAAU,SAAS,KAEnB,iBAAiB;AAErB;AAEA,SAAS,wBAAwB,OAAgD;CAC/E,IAAI,CAAC,cAAc,KAAK,GAAG,iBAAiB;CAE5C,KAAK,MAAM,OAAO,uBAChB,IAAI,CAAC,OAAO,UAAU,eAAe,KAAK,OAAO,GAAG,GAAG,iBAAiB;CAG1E,KAAK,MAAM,OAAO,8BAA8B;EAC9C,MAAM,WAAY,MAAkC;EAEpD,IAAI,aAAa,UAAa,CAAC,cAAc,QAAQ,GAAG,iBAAiB;CAC3E;CAEA,MAAM,YAAa,MAAkC;CACrD,IAAI,cAAc,QAAW,wBAAwB,SAAS;CAE9D,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,qBAAqB,cAAwD;CAC3F,MAAM,UAAU,aAAa,eAAe,iBAAiB;CAE7D,IAAI,YAAY,MAAM,MAAM,IAAI,MAAM,sBAAsB;CAE5D,IAAI;CAEJ,IAAI;EACF,SAAS,KAAK,MAAM,QAAQ,eAAe,EAAE;CAC/C,QAAQ;EACN,iBAAiB;CACnB;CAEA,OAAO,wBAAwB,MAAM;AACvC"}
package/esm/index.d.mts CHANGED
@@ -1,6 +1,8 @@
1
+ import { HttpContext, PageContext } from "./context.mjs";
1
2
  import { AppLoader, LayoutLoader, PageLoader } from "./loaders.mjs";
2
3
  import { AppProps, LayoutProps, PageProps } from "./props.mjs";
3
4
  import { PageMetadata } from "./metadata.mjs";
5
+ import { ErrorPageProps, SerializedErrorPageProps, SerializedPageError } from "./components/document-context.mjs";
4
6
  import { shared, useShared } from "./shared.mjs";
5
7
  import { QueryStringInput, QueryStringLeaf, QueryStringNested, QueryStringObject, QueryStringOptions, QueryStringValue, RepeatedKeyStrategy, UnserializableQueryValueError, queryString, queryStringOf, resetQueryStringOptions, setQueryStringOptions } from "./routing/query-string.mjs";
6
8
  import { RouteParameters, RouteQuery, href } from "./routing/route-table.mjs";
@@ -11,9 +13,8 @@ import { MatchedRoute, currentRoute, previousRoute } from "./client/navigation/c
11
13
  import { refresh } from "./client/navigation/refresh.mjs";
12
14
  import { Head } from "./components/head.mjs";
13
15
  import { Scripts } from "./components/scripts.mjs";
14
- import { PageContext } from "./context.mjs";
15
16
 
16
- //#region ../web/src/index.d.ts
17
+ //#region web/src/index.d.ts
17
18
  /**
18
19
  * THE AUDIT SURFACE — everything the browser receives, declared by the app.
19
20
  *
@@ -31,5 +32,5 @@ import { PageContext } from "./context.mjs";
31
32
  */
32
33
  interface SharedContext {}
33
34
  //#endregion
34
- export { type AppLoader, type AppProps, Head, type LayoutLoader, type LayoutProps, Link, type MatchedRoute, type NavigationEndPayload, type NavigationErrorPayload, type NavigationStartPayload, type PageContext, type PageLoader, type PageMetadata, type PageProps, type QueryStringInput, type QueryStringLeaf, type QueryStringNested, type QueryStringObject, type QueryStringOptions, type QueryStringValue, type RepeatedKeyStrategy, type RouteParameters, type RouteQuery, Scripts, SharedContext, UnserializableQueryValueError, createRouterEvents, currentRoute, getHash, href, navigateBack, navigateTo, previousRoute, queryString, queryStringOf, refresh, resetQueryStringOptions, routerEvents, setQueryStringOptions, shared, useShared };
35
+ export { type AppLoader, type AppProps, type ErrorPageProps, Head, type HttpContext, type LayoutLoader, type LayoutProps, Link, type MatchedRoute, type NavigationEndPayload, type NavigationErrorPayload, type NavigationStartPayload, type PageContext, type PageLoader, type PageMetadata, type PageProps, type QueryStringInput, type QueryStringLeaf, type QueryStringNested, type QueryStringObject, type QueryStringOptions, type QueryStringValue, type RepeatedKeyStrategy, type RouteParameters, type RouteQuery, Scripts, type SerializedErrorPageProps, type SerializedPageError, SharedContext, UnserializableQueryValueError, createRouterEvents, currentRoute, getHash, href, navigateBack, navigateTo, previousRoute, queryString, queryStringOf, refresh, resetQueryStringOptions, routerEvents, setQueryStringOptions, shared, useShared };
35
36
  //# sourceMappingURL=index.d.mts.map
package/esm/loaders.d.mts CHANGED
@@ -1,46 +1,17 @@
1
- import { RouteDeclaration, RouteParamsOf } from "./route.mjs";
1
+ import { PageContext } from "./context.mjs";
2
+ import { RouteDeclaration } from "./route.mjs";
2
3
  import { PageValidation, ValidatedOutput } from "./validation.mjs";
3
4
  import { SharedContext } from "./index.mjs";
4
- import { PageContext, WebRequest, WebResponse } from "./context.mjs";
5
+ import { Request, Response } from "@warlock.js/core";
5
6
 
6
- //#region ../web/src/loaders.d.ts
7
- /**
8
- * The context a PAGE loader receives, narrowed by the page's own sibling
9
- * exports: `TValidation` types `request.validated()` off the schema,
10
- * `TRoute` types `request.input()` off the path literal. A sibling export
11
- * cannot contextually type an already-authored function — the generics are
12
- * the mechanism that links them (product-details.page.tsx:20-41).
13
- */
14
- type PageLoaderContext<TValidation extends PageValidation | undefined, TRoute extends RouteDeclaration | undefined> = {
15
- request: WebRequest<ValidatedOutput<TValidation>, RouteParamsOf<TRoute>>;
16
- response: WebResponse;
7
+ //#region web/src/loaders.d.ts
8
+ type PageLoaderContext<TValidation extends PageValidation | undefined, _TRoute extends RouteDeclaration | undefined> = {
9
+ request: Request<ValidatedOutput<TValidation>>;
10
+ response: Response;
17
11
  shared: SharedContext;
18
12
  };
19
- /**
20
- * The page controller's contract. Used with `satisfies`, NEVER `:` — an
21
- * annotation replaces the inferred type and `PageProps<typeof loader>` loses
22
- * `data` (v5/app README §A). The return is `unknown` because `satisfies`
23
- * preserves whatever the loader actually returns; the short-circuit brand and
24
- * the props types do the narrowing on the way out.
25
- *
26
- * `TValidation` may be `undefined` — a page with no `validation` export
27
- * (product-details.page.tsx:69). `TRoute` may be the config object or a bare
28
- * string literal (contact-us.page.tsx:27).
29
- */
30
13
  type PageLoader<TValidation extends PageValidation | undefined = undefined, TRoute extends RouteDeclaration | undefined = undefined> = (context: PageLoaderContext<TValidation, TRoute>) => unknown;
31
- /**
32
- * A layout's controller. Bare — no generics — in every v5/app use
33
- * (products/web/layout.tsx:53-58, dashboard.layout.tsx:34-36): a layout has
34
- * no `validation`/`route` exports to link, and the contract is the annotated
35
- * type, not a parameter list (`async () => …` is still a controller with the
36
- * full context available).
37
- */
38
14
  type LayoutLoader = (context: PageContext) => unknown;
39
- /**
40
- * The application root's controller (root.tsx:56-58). Same shape as
41
- * `LayoutLoader`; a separate name because the two revalidate and nest under
42
- * different rules, and conflating them would make that drift invisible.
43
- */
44
15
  type AppLoader = (context: PageContext) => unknown;
45
16
  //#endregion
46
17
  export { AppLoader, LayoutLoader, PageLoader };
@@ -1,7 +1,7 @@
1
1
  import { LoaderData, LoaderFunction } from "./props.mjs";
2
2
  import { SharedContext } from "./index.mjs";
3
3
 
4
- //#region ../web/src/metadata.d.ts
4
+ //#region web/src/metadata.d.ts
5
5
  /**
6
6
  * What the `metadata` export may produce. The pipeline injects it into
7
7
  * `<head>` before the first byte; `<Head />` only decides placement.
package/esm/metadata.mjs CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../web/src/metadata.ts
1
+ //#region web/src/metadata.ts
2
2
  /**
3
3
  * The SAME key set as {@link MetadataOutput}, as a value.
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"metadata.mjs","names":[],"sources":["../../../../../../web/src/metadata.ts"],"sourcesContent":["import type { SharedContext } from \"./index\";\r\nimport type { LoaderData, LoaderFunction } from \"./props\";\r\n\r\n/**\r\n * What the `metadata` export may produce. The pipeline injects it into\r\n * `<head>` before the first byte; `<Head />` only decides placement.\r\n *\r\n * **Every member here is a member something READS.** The two renderers are\r\n * `components/head.ts:28-70` (the SSR'd document) and\r\n * `client/navigation/navigation-root.tsx:105-123` (the same head, rewritten\r\n * after a client navigation), and they agree key for key. This type is the list\r\n * of those keys and deliberately not one entry longer: a field the type promises\r\n * and no renderer consumes is the same silence as an unknown key — the page is\r\n * served without it and nothing says so.\r\n *\r\n * Adding a member is therefore a two-file change by construction. Add it here\r\n * and {@link METADATA_KEYS} stops matching, which is a compile error\r\n * ({@link MetadataKeysAreExact}); make it match and the build gate in\r\n * `build/discover-pages.ts` accepts the key — but until a renderer emits a tag\r\n * for it, the key still does nothing. Write the renderer.\r\n */\r\nexport type MetadataOutput = {\r\n title?: string;\r\n description?: string;\r\n keywords?: string | readonly string[];\r\n canonical?: string;\r\n robots?: string;\r\n /**\r\n * `og:title`/`og:description` fall back to the top-level `title`/\r\n * `description` when `openGraph` is present but the member is absent —\r\n * no other member has a fallback.\r\n */\r\n openGraph?: {\r\n title?: string;\r\n description?: string;\r\n image?: string;\r\n url?: string;\r\n type?: string;\r\n };\r\n twitter?: {\r\n card?: string;\r\n title?: string;\r\n description?: string;\r\n image?: string;\r\n };\r\n};\r\n\r\n/**\r\n * The SAME key set as {@link MetadataOutput}, as a value.\r\n *\r\n * It exists because the defect this guards is one a type cannot reach.\r\n * `export const metadata = { tittle: \"x\" }` — no annotation — is a well-typed\r\n * program: TypeScript infers `{ tittle: string }` and has nothing to compare it\r\n * against, so the page is served with no title and the build says nothing. The\r\n * only place left to catch it is where pages are DISCOVERED, and discovery\r\n * refuses to run application code — it parses. A parser cannot ask a type what\r\n * its keys are, so the keys have to exist at runtime too.\r\n *\r\n * Two lists of one thing is exactly the drift this codebase refuses elsewhere,\r\n * so they are not two lists: {@link MetadataKeysAreExact} makes any\r\n * disagreement a compile error, in either direction. Add a key to the type\r\n * alone and this file stops compiling; add it here alone, likewise.\r\n */\r\nexport const METADATA_KEYS = [\r\n \"title\",\r\n \"description\",\r\n \"keywords\",\r\n \"canonical\",\r\n \"robots\",\r\n \"openGraph\",\r\n \"twitter\",\r\n] as const;\r\n\r\n/** The members of `openGraph`, on the same terms as {@link METADATA_KEYS}. */\r\nexport const OPEN_GRAPH_KEYS = [\"title\", \"description\", \"image\", \"url\", \"type\"] as const;\r\n\r\n/** The members of `twitter`, on the same terms as {@link METADATA_KEYS}. */\r\nexport const TWITTER_KEYS = [\"card\", \"title\", \"description\", \"image\"] as const;\r\n\r\n/**\r\n * Instantiates only when `Difference` is empty. When it is not, the compiler\r\n * names the offending key in the error — \"Type '\\\"tittle\\\"' does not satisfy\r\n * the constraint 'never'\" — which is the whole message a drift needs.\r\n *\r\n * A mutual `extends` (`Exactly<A extends B, B extends A>`) says the same thing\r\n * more directly and TypeScript rejects it as a circular constraint (TS2313), so\r\n * the sets are compared by difference instead, once in each direction. BOTH\r\n * directions matter: a list that has fallen behind the type makes the build gate\r\n * reject a key it should accept, and a list that has run ahead makes it accept\r\n * one nothing renders.\r\n */\r\ntype NoDifference<Difference extends never> = Difference;\r\n\r\n/**\r\n * The drift guards, written out per key set rather than through one generic\r\n * helper: a `SameKeys<List, Keys>` alias would apply `NoDifference` to an\r\n * UNRESOLVED `Exclude<List, Keys>`, which the compiler cannot show is empty and\r\n * so rejects at the declaration (TS2344) whatever the real key sets are. Passed\r\n * concrete types, it resolves and checks the thing it is meant to check.\r\n *\r\n * Exported so they are not \"unused\", and named so a failure reads as what it\r\n * is: the key list and the type have diverged.\r\n */\r\nexport type MetadataKeysAreExact = [\r\n NoDifference<Exclude<(typeof METADATA_KEYS)[number], keyof MetadataOutput>>,\r\n NoDifference<Exclude<keyof MetadataOutput, (typeof METADATA_KEYS)[number]>>,\r\n];\r\n\r\ntype OpenGraphKey = keyof NonNullable<MetadataOutput[\"openGraph\"]>;\r\n\r\nexport type OpenGraphKeysAreExact = [\r\n NoDifference<Exclude<(typeof OPEN_GRAPH_KEYS)[number], OpenGraphKey>>,\r\n NoDifference<Exclude<OpenGraphKey, (typeof OPEN_GRAPH_KEYS)[number]>>,\r\n];\r\n\r\ntype TwitterKey = keyof NonNullable<MetadataOutput[\"twitter\"]>;\r\n\r\nexport type TwitterKeysAreExact = [\r\n NoDifference<Exclude<(typeof TWITTER_KEYS)[number], TwitterKey>>,\r\n NoDifference<Exclude<TwitterKey, (typeof TWITTER_KEYS)[number]>>,\r\n];\r\n\r\n/**\r\n * Static object OR a function of the loader's data (products.page.tsx:81-84,\r\n * product-details.page.tsx:71-74). The function form runs server-side, after\r\n * the loader, with the same `data` the component will receive — which is why\r\n * it can describe the page instead of guessing at it.\r\n *\r\n * **`data` is always present, and that is now true rather than merely\r\n * declared.** The function form runs only when the loader resolved; when it\r\n * rejected, the framework emits `ERROR_PAGE_METADATA` and this never runs\r\n * (`server/resolve-page-metadata.ts`, which explains why at length). An earlier\r\n * revision passed `{ data: undefined, error }` on the boundary path while\r\n * declaring `data` non-optional — every page that read `data` unguarded then\r\n * threw a `TypeError` that replaced the loader's real error.\r\n */\r\nexport type PageMetadata<TLoader extends LoaderFunction | undefined = undefined> =\r\n | MetadataOutput\r\n | ((context: {\r\n data: LoaderData<TLoader>;\r\n shared: Readonly<SharedContext>;\r\n }) => MetadataOutput);\r\n"],"mappings":";;;;;;;;;;;;;;;;;AA+DA,MAAa,gBAAgB;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,MAAa,kBAAkB;CAAC;CAAS;CAAe;CAAS;CAAO;AAAM;;AAG9E,MAAa,eAAe;CAAC;CAAQ;CAAS;CAAe;AAAO"}
1
+ {"version":3,"file":"metadata.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/metadata.ts"],"sourcesContent":["import type { SharedContext } from \"./index\";\r\nimport type { LoaderData, LoaderFunction } from \"./props\";\r\n\r\n/**\r\n * What the `metadata` export may produce. The pipeline injects it into\r\n * `<head>` before the first byte; `<Head />` only decides placement.\r\n *\r\n * **Every member here is a member something READS.** The two renderers are\r\n * `components/head.ts:28-70` (the SSR'd document) and\r\n * `client/navigation/navigation-root.tsx:105-123` (the same head, rewritten\r\n * after a client navigation), and they agree key for key. This type is the list\r\n * of those keys and deliberately not one entry longer: a field the type promises\r\n * and no renderer consumes is the same silence as an unknown key — the page is\r\n * served without it and nothing says so.\r\n *\r\n * Adding a member is therefore a two-file change by construction. Add it here\r\n * and {@link METADATA_KEYS} stops matching, which is a compile error\r\n * ({@link MetadataKeysAreExact}); make it match and the build gate in\r\n * `build/discover-pages.ts` accepts the key — but until a renderer emits a tag\r\n * for it, the key still does nothing. Write the renderer.\r\n */\r\nexport type MetadataOutput = {\r\n title?: string;\r\n description?: string;\r\n keywords?: string | readonly string[];\r\n canonical?: string;\r\n robots?: string;\r\n /**\r\n * `og:title`/`og:description` fall back to the top-level `title`/\r\n * `description` when `openGraph` is present but the member is absent —\r\n * no other member has a fallback.\r\n */\r\n openGraph?: {\r\n title?: string;\r\n description?: string;\r\n image?: string;\r\n url?: string;\r\n type?: string;\r\n };\r\n twitter?: {\r\n card?: string;\r\n title?: string;\r\n description?: string;\r\n image?: string;\r\n };\r\n};\r\n\r\n/**\r\n * The SAME key set as {@link MetadataOutput}, as a value.\r\n *\r\n * It exists because the defect this guards is one a type cannot reach.\r\n * `export const metadata = { tittle: \"x\" }` — no annotation — is a well-typed\r\n * program: TypeScript infers `{ tittle: string }` and has nothing to compare it\r\n * against, so the page is served with no title and the build says nothing. The\r\n * only place left to catch it is where pages are DISCOVERED, and discovery\r\n * refuses to run application code — it parses. A parser cannot ask a type what\r\n * its keys are, so the keys have to exist at runtime too.\r\n *\r\n * Two lists of one thing is exactly the drift this codebase refuses elsewhere,\r\n * so they are not two lists: {@link MetadataKeysAreExact} makes any\r\n * disagreement a compile error, in either direction. Add a key to the type\r\n * alone and this file stops compiling; add it here alone, likewise.\r\n */\r\nexport const METADATA_KEYS = [\r\n \"title\",\r\n \"description\",\r\n \"keywords\",\r\n \"canonical\",\r\n \"robots\",\r\n \"openGraph\",\r\n \"twitter\",\r\n] as const;\r\n\r\n/** The members of `openGraph`, on the same terms as {@link METADATA_KEYS}. */\r\nexport const OPEN_GRAPH_KEYS = [\"title\", \"description\", \"image\", \"url\", \"type\"] as const;\r\n\r\n/** The members of `twitter`, on the same terms as {@link METADATA_KEYS}. */\r\nexport const TWITTER_KEYS = [\"card\", \"title\", \"description\", \"image\"] as const;\r\n\r\n/**\r\n * Instantiates only when `Difference` is empty. When it is not, the compiler\r\n * names the offending key in the error — \"Type '\\\"tittle\\\"' does not satisfy\r\n * the constraint 'never'\" — which is the whole message a drift needs.\r\n *\r\n * A mutual `extends` (`Exactly<A extends B, B extends A>`) says the same thing\r\n * more directly and TypeScript rejects it as a circular constraint (TS2313), so\r\n * the sets are compared by difference instead, once in each direction. BOTH\r\n * directions matter: a list that has fallen behind the type makes the build gate\r\n * reject a key it should accept, and a list that has run ahead makes it accept\r\n * one nothing renders.\r\n */\r\ntype NoDifference<Difference extends never> = Difference;\r\n\r\n/**\r\n * The drift guards, written out per key set rather than through one generic\r\n * helper: a `SameKeys<List, Keys>` alias would apply `NoDifference` to an\r\n * UNRESOLVED `Exclude<List, Keys>`, which the compiler cannot show is empty and\r\n * so rejects at the declaration (TS2344) whatever the real key sets are. Passed\r\n * concrete types, it resolves and checks the thing it is meant to check.\r\n *\r\n * Exported so they are not \"unused\", and named so a failure reads as what it\r\n * is: the key list and the type have diverged.\r\n */\r\nexport type MetadataKeysAreExact = [\r\n NoDifference<Exclude<(typeof METADATA_KEYS)[number], keyof MetadataOutput>>,\r\n NoDifference<Exclude<keyof MetadataOutput, (typeof METADATA_KEYS)[number]>>,\r\n];\r\n\r\ntype OpenGraphKey = keyof NonNullable<MetadataOutput[\"openGraph\"]>;\r\n\r\nexport type OpenGraphKeysAreExact = [\r\n NoDifference<Exclude<(typeof OPEN_GRAPH_KEYS)[number], OpenGraphKey>>,\r\n NoDifference<Exclude<OpenGraphKey, (typeof OPEN_GRAPH_KEYS)[number]>>,\r\n];\r\n\r\ntype TwitterKey = keyof NonNullable<MetadataOutput[\"twitter\"]>;\r\n\r\nexport type TwitterKeysAreExact = [\r\n NoDifference<Exclude<(typeof TWITTER_KEYS)[number], TwitterKey>>,\r\n NoDifference<Exclude<TwitterKey, (typeof TWITTER_KEYS)[number]>>,\r\n];\r\n\r\n/**\r\n * Static object OR a function of the loader's data (products.page.tsx:81-84,\r\n * product-details.page.tsx:71-74). The function form runs server-side, after\r\n * the loader, with the same `data` the component will receive — which is why\r\n * it can describe the page instead of guessing at it.\r\n *\r\n * **`data` is always present, and that is now true rather than merely\r\n * declared.** The function form runs only when the loader resolved; when it\r\n * rejected, the framework emits `ERROR_PAGE_METADATA` and this never runs\r\n * (`server/resolve-page-metadata.ts`, which explains why at length). An earlier\r\n * revision passed `{ data: undefined, error }` on the boundary path while\r\n * declaring `data` non-optional — every page that read `data` unguarded then\r\n * threw a `TypeError` that replaced the loader's real error.\r\n */\r\nexport type PageMetadata<TLoader extends LoaderFunction | undefined = undefined> =\r\n | MetadataOutput\r\n | ((context: {\r\n data: LoaderData<TLoader>;\r\n shared: Readonly<SharedContext>;\r\n }) => MetadataOutput);\r\n"],"mappings":";;;;;;;;;;;;;;;;;AA+DA,MAAa,gBAAgB;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,MAAa,kBAAkB;CAAC;CAAS;CAAe;CAAS;CAAO;AAAM;;AAG9E,MAAa,eAAe;CAAC;CAAQ;CAAS;CAAe;AAAO"}
package/esm/props.d.mts CHANGED
@@ -1,17 +1,15 @@
1
1
  import { SharedContext } from "./index.mjs";
2
- import { LoaderShortCircuit } from "./context.mjs";
3
2
  import { ReactNode } from "react";
3
+ import { Response } from "@warlock.js/core";
4
4
 
5
- //#region ../web/src/props.d.ts
5
+ //#region web/src/props.d.ts
6
6
  /** Any loader authored with `satisfies` — the concrete function type. */
7
7
  type LoaderFunction = (...args: any[]) => unknown;
8
8
  /**
9
- * The loader's literal return shape MINUS the branded short-circuits:
10
- * `response.notFound()` / `response.redirect()` never union into `data`
11
- * (product-details.page.tsx:45-57, login.page.tsx:71-84). No loader → no
12
- * data.
9
+ * The loader's literal return shape minus core Response. Returning that exact
10
+ * class is terminal; every other value is data.
13
11
  */
14
- type LoaderData<TLoader> = TLoader extends LoaderFunction ? Exclude<Awaited<ReturnType<TLoader>>, LoaderShortCircuit> : undefined;
12
+ type LoaderData<TLoader> = TLoader extends LoaderFunction ? Exclude<Awaited<ReturnType<TLoader>>, Response> : undefined;
15
13
  /**
16
14
  * What the pipeline hands a page component: its own loader's data plus the
17
15
  * per-request payload. Never `request` or `response` — the component also
@@ -20,6 +18,7 @@ type LoaderData<TLoader> = TLoader extends LoaderFunction ? Exclude<Awaited<Retu
20
18
  type PageProps<TLoader extends LoaderFunction | undefined = undefined> = {
21
19
  data: LoaderData<TLoader>;
22
20
  shared: Readonly<SharedContext>;
21
+ params: Readonly<Record<string, string>>;
23
22
  };
24
23
  /**
25
24
  * A layout additionally receives the subtree it wraps. Usable bare —
@@ -37,6 +36,15 @@ type AppProps<TLoader extends LoaderFunction | undefined = undefined> = {
37
36
  shared: Readonly<SharedContext>;
38
37
  children: ReactNode;
39
38
  };
39
+ /**
40
+ * Props passed only while the server renders an application `error.page.tsx`.
41
+ * The thrown value intentionally remains intact here; its browser counterpart
42
+ * is normalized at the hydration boundary.
43
+ */
44
+ type ServerErrorPageProps = {
45
+ error: unknown;
46
+ status: number;
47
+ };
40
48
  //#endregion
41
- export { AppProps, LayoutProps, LoaderData, LoaderFunction, PageProps };
49
+ export { AppProps, LayoutProps, LoaderData, LoaderFunction, PageProps, ServerErrorPageProps };
42
50
  //# sourceMappingURL=props.d.mts.map
package/esm/route.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../web/src/route.d.ts
1
+ //#region web/src/route.d.ts
2
2
  /**
3
3
  * The `route` export's accepted shapes: the configured object
4
4
  * (`{ path, name } as const` — the `as const` is what keeps `path` a literal
@@ -10,21 +10,6 @@ type RouteDeclaration = string | {
10
10
  readonly path: string;
11
11
  readonly name?: string;
12
12
  };
13
- type RoutePath<TRoute> = TRoute extends string ? TRoute : TRoute extends {
14
- readonly path: infer TPath extends string;
15
- } ? TPath : never;
16
- /**
17
- * Parameter names out of a path literal: `"/:id"` → `"id"`,
18
- * `"/a/:x/:y"` → `"x" | "y"`, `"/"` and `"*"` → `never` (so `request.input()`
19
- * is uncallable where no parameter exists to read).
20
- */
21
- type PathParams<TPath extends string> = TPath extends `${string}:${infer TRest}` ? TRest extends `${infer TParam}/${infer TTail}` ? TParam | PathParams<`/${TTail}`> : TRest : never;
22
- /**
23
- * `undefined` (no route generic supplied) keeps `input()` loose rather than
24
- * uncallable — the contract only narrows when the loader links its route via
25
- * `satisfies PageLoader<…, typeof route>`.
26
- */
27
- type RouteParamsOf<TRoute> = [TRoute] extends [undefined] ? string : PathParams<RoutePath<TRoute>>;
28
13
  //#endregion
29
- export { RouteDeclaration, RouteParamsOf };
14
+ export { RouteDeclaration };
30
15
  //# sourceMappingURL=route.d.mts.map
@@ -1,4 +1,4 @@
1
- //#region ../web/src/routing/compose-route-path.d.ts
1
+ //#region web/src/routing/compose-route-path.d.ts
2
2
  /**
3
3
  * Route-path composition — the single, pure rule for turning a layout's
4
4
  * declared `prefix` and a page's declared `route.path` into the page's
@@ -1,4 +1,4 @@
1
- //#region ../web/src/routing/compose-route-path.ts
1
+ //#region web/src/routing/compose-route-path.ts
2
2
  /**
3
3
  * Route-path composition — the single, pure rule for turning a layout's
4
4
  * declared `prefix` and a page's declared `route.path` into the page's
@@ -1 +1 @@
1
- {"version":3,"file":"compose-route-path.mjs","names":[],"sources":["../../../../../../../web/src/routing/compose-route-path.ts"],"sourcesContent":["/**\n * Route-path composition — the single, pure rule for turning a layout's\n * declared `prefix` and a page's declared `route.path` into the page's\n * effective, registered path. Previously three hand-written copies: the dev\n * installer (`web/src/server/install-page-routes.ts`), the production\n * manifest installer (`web/src/server/install-page-routes-from-manifest.ts`)\n * and build discovery (`web/src/build/discover-pages.ts`) each carried this\n * exact rule so build and boot could not quietly disagree about it; all\n * three now delegate here instead.\n *\n * DIRECTORY CONTRACT — applies to everything in `web/src/routing/`: nothing\n * here may import `node:fs`, `node:path`, `vite`, or `fastify`. This module\n * receives canonical values and trusts nothing about them beyond the input\n * contract asserted below — it asserts rather than trusts, but it never\n * repairs.\n */\n\n/**\n * A page's effective path: its nearest layout's `prefix` followed by its own\n * declared `route.path`. A root prefix (\"/\") and a root path (\"/\") each\n * contribute nothing, so neither adds a slash of its own; every other case\n * joins on the slash `routePath` already starts with, which is what keeps a\n * double slash out of the result, and an empty concatenation is the site\n * root.\n */\nexport function composeRoutePath(layoutPrefix: string, routePath: string): string {\n const normalizedPrefix = layoutPrefix === \"/\" ? \"\" : layoutPrefix.replace(/\\/+$/, \"\");\n const normalizedRoute = routePath === \"/\" ? \"\" : routePath;\n const composed = `${normalizedPrefix}${normalizedRoute}`;\n\n return composed === \"\" ? \"/\" : composed;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,iBAAiB,cAAsB,WAA2B;CAGhF,MAAM,WAAW,GAFQ,iBAAiB,MAAM,KAAK,aAAa,QAAQ,QAAQ,EAAE,IAC5D,cAAc,MAAM,KAAK;CAGjD,OAAO,aAAa,KAAK,MAAM;AACjC"}
1
+ {"version":3,"file":"compose-route-path.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/routing/compose-route-path.ts"],"sourcesContent":["/**\n * Route-path composition — the single, pure rule for turning a layout's\n * declared `prefix` and a page's declared `route.path` into the page's\n * effective, registered path. Previously three hand-written copies: the dev\n * installer (`web/src/server/install-page-routes.ts`), the production\n * manifest installer (`web/src/server/install-page-routes-from-manifest.ts`)\n * and build discovery (`web/src/build/discover-pages.ts`) each carried this\n * exact rule so build and boot could not quietly disagree about it; all\n * three now delegate here instead.\n *\n * DIRECTORY CONTRACT — applies to everything in `web/src/routing/`: nothing\n * here may import `node:fs`, `node:path`, `vite`, or `fastify`. This module\n * receives canonical values and trusts nothing about them beyond the input\n * contract asserted below — it asserts rather than trusts, but it never\n * repairs.\n */\n\n/**\n * A page's effective path: its nearest layout's `prefix` followed by its own\n * declared `route.path`. A root prefix (\"/\") and a root path (\"/\") each\n * contribute nothing, so neither adds a slash of its own; every other case\n * joins on the slash `routePath` already starts with, which is what keeps a\n * double slash out of the result, and an empty concatenation is the site\n * root.\n */\nexport function composeRoutePath(layoutPrefix: string, routePath: string): string {\n const normalizedPrefix = layoutPrefix === \"/\" ? \"\" : layoutPrefix.replace(/\\/+$/, \"\");\n const normalizedRoute = routePath === \"/\" ? \"\" : routePath;\n const composed = `${normalizedPrefix}${normalizedRoute}`;\n\n return composed === \"\" ? \"/\" : composed;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,iBAAiB,cAAsB,WAA2B;CAGhF,MAAM,WAAW,GAFQ,iBAAiB,MAAM,KAAK,aAAa,QAAQ,QAAQ,EAAE,IAC5D,cAAc,MAAM,KAAK;CAGjD,OAAO,aAAa,KAAK,MAAM;AACjC"}
@@ -1,4 +1,4 @@
1
- //#region ../web/src/routing/data-request.ts
1
+ //#region web/src/routing/data-request.ts
2
2
  /**
3
3
  * The marker that turns a page request into a DATA request.
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"data-request.mjs","names":[],"sources":["../../../../../../../web/src/routing/data-request.ts"],"sourcesContent":["/**\n * The marker that turns a page request into a DATA request.\n *\n * A client navigation needs exactly what a full page load needs — middleware,\n * validation, loaders, redirects, cookies, the settled status — and differs in\n * one respect only: it wants the hydration payload as JSON instead of a\n * rendered document. So it is deliberately NOT a separate `/_loader` route.\n *\n * WHY NOT A SEPARATE ROUTE. A `/_loader?path=/products/42` endpoint has to\n * resolve that path to a page itself, which is a SECOND implementation of route\n * semantics living beside the server's. This codebase already refuses that\n * bargain for the browser — the hydration payload carries the route `name` so\n * the client never re-matches — and the same reasoning applies here with more\n * force: a loader endpoint that disagreed with the real route about params,\n * prefixes or which page owns a path would answer a different request than the\n * one the user navigated to. Same URL, same route, same matcher, same pipeline;\n * only the final representation differs.\n *\n * WHY A HEADER AND NOT `?_data=1`. The query string belongs to the page — it is\n * what `validation` and loaders read. Injecting a framework key into it means a\n * page with strict query validation rejects its own client navigations, and\n * every loader that echoes its query starts leaking a private flag.\n *\n * Responses to a data request must carry `Vary: <this header>` so a shared\n * cache can never hand a document to a client that asked for JSON, or the\n * reverse. Page responses are `private, no-store` today, which makes that\n * theoretical — `Vary` is what keeps it theoretical if the caching policy\n * changes.\n */\nexport const WARLOCK_DATA_REQUEST_HEADER = \"x-warlock-data\";\n\n/**\n * The value the client sends. Any non-empty value is honoured on the way in —\n * the header's PRESENCE is the signal — but the client sends this one so the\n * traffic is self-describing in a log or a network panel.\n */\nexport const WARLOCK_DATA_REQUEST_VALUE = \"1\";\n\n/**\n * Declared explicitly because the payload goes on the wire ALREADY SERIALIZED,\n * as a string, and core only auto-picks `application/json` for object bodies.\n * See the send site for why it must be a string.\n */\nexport const DATA_RESPONSE_CONTENT_TYPE = \"application/json\";\n\n/**\n * Whether a request asked for the payload rather than the document.\n *\n * Presence-based on purpose: a proxy that rewrites the value, or a client on a\n * newer version that sends something more specific, still means \"data\". Only an\n * absent or empty header means \"render the document\".\n */\nexport function isDataRequest(headerValue: string | string[] | undefined): boolean {\n const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;\n\n return typeof value === \"string\" && value.length > 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,8BAA8B;;;;;;AAc3C,MAAa,6BAA6B;;;;;;;;AAS1C,SAAgB,cAAc,aAAqD;CACjF,MAAM,QAAQ,MAAM,QAAQ,WAAW,IAAI,YAAY,KAAK;CAE5D,OAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD"}
1
+ {"version":3,"file":"data-request.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/routing/data-request.ts"],"sourcesContent":["/**\n * The marker that turns a page request into a DATA request.\n *\n * A client navigation needs exactly what a full page load needs — middleware,\n * validation, loaders, redirects, cookies, the settled status — and differs in\n * one respect only: it wants the hydration payload as JSON instead of a\n * rendered document. So it is deliberately NOT a separate `/_loader` route.\n *\n * WHY NOT A SEPARATE ROUTE. A `/_loader?path=/products/42` endpoint has to\n * resolve that path to a page itself, which is a SECOND implementation of route\n * semantics living beside the server's. This codebase already refuses that\n * bargain for the browser — the hydration payload carries the route `name` so\n * the client never re-matches — and the same reasoning applies here with more\n * force: a loader endpoint that disagreed with the real route about params,\n * prefixes or which page owns a path would answer a different request than the\n * one the user navigated to. Same URL, same route, same matcher, same pipeline;\n * only the final representation differs.\n *\n * WHY A HEADER AND NOT `?_data=1`. The query string belongs to the page — it is\n * what `validation` and loaders read. Injecting a framework key into it means a\n * page with strict query validation rejects its own client navigations, and\n * every loader that echoes its query starts leaking a private flag.\n *\n * Responses to a data request must carry `Vary: <this header>` so a shared\n * cache can never hand a document to a client that asked for JSON, or the\n * reverse. Page responses are `private, no-store` today, which makes that\n * theoretical — `Vary` is what keeps it theoretical if the caching policy\n * changes.\n */\nexport const WARLOCK_DATA_REQUEST_HEADER = \"x-warlock-data\";\n\n/**\n * The value the client sends. Any non-empty value is honoured on the way in —\n * the header's PRESENCE is the signal — but the client sends this one so the\n * traffic is self-describing in a log or a network panel.\n */\nexport const WARLOCK_DATA_REQUEST_VALUE = \"1\";\n\n/**\n * Declared explicitly because the payload goes on the wire ALREADY SERIALIZED,\n * as a string, and core only auto-picks `application/json` for object bodies.\n * See the send site for why it must be a string.\n */\nexport const DATA_RESPONSE_CONTENT_TYPE = \"application/json\";\n\n/**\n * Whether a request asked for the payload rather than the document.\n *\n * Presence-based on purpose: a proxy that rewrites the value, or a client on a\n * newer version that sends something more specific, still means \"data\". Only an\n * absent or empty header means \"render the document\".\n */\nexport function isDataRequest(headerValue: string | string[] | undefined): boolean {\n const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;\n\n return typeof value === \"string\" && value.length > 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,8BAA8B;;;;;;AAc3C,MAAa,6BAA6B;;;;;;;;AAS1C,SAAgB,cAAc,aAAqD;CACjF,MAAM,QAAQ,MAAM,QAAQ,WAAW,IAAI,YAAY,KAAK;CAE5D,OAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD"}
@@ -0,0 +1,45 @@
1
+ //#region web/src/routing/filesystem-route.ts
2
+ function isGroup(segment) {
3
+ return /^\([^/]+\)$/.test(segment);
4
+ }
5
+ function routeSegment(segment) {
6
+ const dynamic = /^\[([A-Za-z_][A-Za-z0-9_]*)\]$/.exec(segment);
7
+ return dynamic ? `:${dynamic[1]}` : segment;
8
+ }
9
+ function prefixSegments(prefix) {
10
+ return prefix.split("/").filter(Boolean);
11
+ }
12
+ function pageParts(pageFile) {
13
+ if (pageFile.includes("\\")) throw new Error(`filesystem-route: pageFile must use POSIX separators: "${pageFile}"`);
14
+ if (!pageFile.endsWith(".page.tsx")) throw new Error(`filesystem-route: pageFile must end in .page.tsx: "${pageFile}"`);
15
+ const parts = pageFile.split("/");
16
+ return {
17
+ directories: parts,
18
+ basename: parts.pop().slice(0, -9)
19
+ };
20
+ }
21
+ /** Derive the effective URL for a page with no explicit `route` export. */
22
+ function deriveFilesystemRoutePath(input) {
23
+ const { directories, basename } = pageParts(input.pageFile);
24
+ const prefixes = input.layoutPrefixes ?? {};
25
+ const segments = [...prefixSegments(prefixes[""] ?? "")];
26
+ for (let index = 0; index < directories.length; index++) {
27
+ const directory = directories[index];
28
+ const prefix = prefixes[directories.slice(0, index + 1).join("/")];
29
+ if (prefix !== void 0) segments.push(...prefixSegments(prefix));
30
+ else if (!isGroup(directory)) segments.push(routeSegment(directory));
31
+ }
32
+ if (basename !== "index") segments.push(routeSegment(basename));
33
+ return segments.length === 0 ? "/" : `/${segments.join("/")}`;
34
+ }
35
+ /** Derive the stable dotted route name from a page's filesystem identity. */
36
+ function deriveFilesystemRouteName(pageFile) {
37
+ const { directories, basename } = pageParts(pageFile);
38
+ const segments = directories.filter((segment) => !isGroup(segment)).map(routeSegment);
39
+ if (basename !== "index") segments.push(routeSegment(basename));
40
+ return segments.map((segment) => segment.replace(/^:/, "")).join(".") || "index";
41
+ }
42
+
43
+ //#endregion
44
+ export { deriveFilesystemRouteName, deriveFilesystemRoutePath };
45
+ //# sourceMappingURL=filesystem-route.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filesystem-route.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/routing/filesystem-route.ts"],"sourcesContent":["export type FilesystemRouteInput = {\n /** POSIX path relative to `src/web`, ending in `.page.tsx`. */\n pageFile: string;\n /** Layout prefixes keyed by their POSIX directory relative to `src/web`; root uses `\"\"`. */\n layoutPrefixes?: Readonly<Record<string, string>>;\n};\n\nfunction isGroup(segment: string): boolean {\n return /^\\([^/]+\\)$/.test(segment);\n}\n\nfunction routeSegment(segment: string): string {\n const dynamic = /^\\[([A-Za-z_][A-Za-z0-9_]*)\\]$/.exec(segment);\n\n return dynamic ? `:${dynamic[1]}` : segment;\n}\n\nfunction prefixSegments(prefix: string): string[] {\n return prefix.split(\"/\").filter(Boolean);\n}\n\nfunction pageParts(pageFile: string): { directories: string[]; basename: string } {\n if (pageFile.includes(\"\\\\\")) {\n throw new Error(`filesystem-route: pageFile must use POSIX separators: \"${pageFile}\"`);\n }\n\n if (!pageFile.endsWith(\".page.tsx\")) {\n throw new Error(`filesystem-route: pageFile must end in .page.tsx: \"${pageFile}\"`);\n }\n\n const parts = pageFile.split(\"/\");\n const filename = parts.pop() as string;\n\n return {\n directories: parts,\n basename: filename.slice(0, -\".page.tsx\".length),\n };\n}\n\n/** Derive the effective URL for a page with no explicit `route` export. */\nexport function deriveFilesystemRoutePath(input: FilesystemRouteInput): string {\n const { directories, basename } = pageParts(input.pageFile);\n const prefixes = input.layoutPrefixes ?? {};\n const segments = [...prefixSegments(prefixes[\"\"] ?? \"\")];\n\n for (let index = 0; index < directories.length; index++) {\n const directory = directories[index];\n const directoryPath = directories.slice(0, index + 1).join(\"/\");\n const prefix = prefixes[directoryPath];\n\n if (prefix !== undefined) {\n segments.push(...prefixSegments(prefix));\n } else if (!isGroup(directory)) {\n segments.push(routeSegment(directory));\n }\n }\n\n if (basename !== \"index\") {\n segments.push(routeSegment(basename));\n }\n\n return segments.length === 0 ? \"/\" : `/${segments.join(\"/\")}`;\n}\n\n/** Derive the stable dotted route name from a page's filesystem identity. */\nexport function deriveFilesystemRouteName(pageFile: string): string {\n const { directories, basename } = pageParts(pageFile);\n const segments = directories.filter((segment) => !isGroup(segment)).map(routeSegment);\n\n if (basename !== \"index\") {\n segments.push(routeSegment(basename));\n }\n\n return segments.map((segment) => segment.replace(/^:/, \"\")).join(\".\") || \"index\";\n}\n"],"mappings":";AAOA,SAAS,QAAQ,SAA0B;CACzC,OAAO,cAAc,KAAK,OAAO;AACnC;AAEA,SAAS,aAAa,SAAyB;CAC7C,MAAM,UAAU,iCAAiC,KAAK,OAAO;CAE7D,OAAO,UAAU,IAAI,QAAQ,OAAO;AACtC;AAEA,SAAS,eAAe,QAA0B;CAChD,OAAO,OAAO,MAAM,GAAG,EAAE,OAAO,OAAO;AACzC;AAEA,SAAS,UAAU,UAA+D;CAChF,IAAI,SAAS,SAAS,IAAI,GACxB,MAAM,IAAI,MAAM,0DAA0D,SAAS,EAAE;CAGvF,IAAI,CAAC,SAAS,SAAS,WAAW,GAChC,MAAM,IAAI,MAAM,sDAAsD,SAAS,EAAE;CAGnF,MAAM,QAAQ,SAAS,MAAM,GAAG;CAGhC,OAAO;EACL,aAAa;EACb,UAJe,MAAM,IAIJ,EAAE,MAAM,GAAG,EAAmB;CACjD;AACF;;AAGA,SAAgB,0BAA0B,OAAqC;CAC7E,MAAM,EAAE,aAAa,aAAa,UAAU,MAAM,QAAQ;CAC1D,MAAM,WAAW,MAAM,kBAAkB,CAAC;CAC1C,MAAM,WAAW,CAAC,GAAG,eAAe,SAAS,OAAO,EAAE,CAAC;CAEvD,KAAK,IAAI,QAAQ,GAAG,QAAQ,YAAY,QAAQ,SAAS;EACvD,MAAM,YAAY,YAAY;EAE9B,MAAM,SAAS,SADO,YAAY,MAAM,GAAG,QAAQ,CAAC,EAAE,KAAK,GACvB;EAEpC,IAAI,WAAW,QACb,SAAS,KAAK,GAAG,eAAe,MAAM,CAAC;OAClC,IAAI,CAAC,QAAQ,SAAS,GAC3B,SAAS,KAAK,aAAa,SAAS,CAAC;CAEzC;CAEA,IAAI,aAAa,SACf,SAAS,KAAK,aAAa,QAAQ,CAAC;CAGtC,OAAO,SAAS,WAAW,IAAI,MAAM,IAAI,SAAS,KAAK,GAAG;AAC5D;;AAGA,SAAgB,0BAA0B,UAA0B;CAClE,MAAM,EAAE,aAAa,aAAa,UAAU,QAAQ;CACpD,MAAM,WAAW,YAAY,QAAQ,YAAY,CAAC,QAAQ,OAAO,CAAC,EAAE,IAAI,YAAY;CAEpF,IAAI,aAAa,SACf,SAAS,KAAK,aAAa,QAAQ,CAAC;CAGtC,OAAO,SAAS,KAAK,YAAY,QAAQ,QAAQ,MAAM,EAAE,CAAC,EAAE,KAAK,GAAG,KAAK;AAC3E"}
@@ -1,4 +1,4 @@
1
- //#region ../web/src/routing/layout-policy.ts
1
+ //#region web/src/routing/layout-policy.ts
2
2
  /**
3
3
  * Raised when a `chain` passed to {@link selectPageLayout} contains an empty
4
4
  * layout identifier. An empty string is not a layout identifier a caller could