@warlock.js/web 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +126 -12
  3. package/esm/build/contribution.d.mts +11 -14
  4. package/esm/build/contribution.mjs +46 -58
  5. package/esm/build/contribution.mjs.map +1 -1
  6. package/esm/build/discover-pages.mjs +105 -45
  7. package/esm/build/discover-pages.mjs.map +1 -1
  8. package/esm/build/generate-client-registry.mjs +47 -13
  9. package/esm/build/generate-client-registry.mjs.map +1 -1
  10. package/esm/build/generate-pages-barrel.mjs +55 -13
  11. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  12. package/esm/build/page-default-export.mjs +48 -0
  13. package/esm/build/page-default-export.mjs.map +1 -0
  14. package/esm/build/page-routes-manifest.mjs +21 -0
  15. package/esm/build/page-routes-manifest.mjs.map +1 -0
  16. package/esm/build/public-files.mjs +45 -0
  17. package/esm/build/public-files.mjs.map +1 -0
  18. package/esm/build/read-route-exports.mjs +1 -1
  19. package/esm/build/read-route-exports.mjs.map +1 -1
  20. package/esm/client/build-hydrated-tree.mjs +40 -9
  21. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  22. package/esm/client/hydrate-page.mjs +1 -1
  23. package/esm/client/hydrate-page.mjs.map +1 -1
  24. package/esm/client/navigation/current-route.d.mts +1 -1
  25. package/esm/client/navigation/current-route.mjs +1 -1
  26. package/esm/client/navigation/current-route.mjs.map +1 -1
  27. package/esm/client/navigation/fetch-page-data.mjs +1 -1
  28. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  29. package/esm/client/navigation/navigation-root.mjs +1 -1
  30. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  31. package/esm/client/navigation/prefetch.mjs +1 -1
  32. package/esm/client/navigation/prefetch.mjs.map +1 -1
  33. package/esm/client/navigation/refresh.d.mts +1 -1
  34. package/esm/client/navigation/refresh.mjs +1 -1
  35. package/esm/client/navigation/refresh.mjs.map +1 -1
  36. package/esm/client/navigation/scroll-to-fragment.mjs +1 -1
  37. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
  38. package/esm/client/navigation/verbs.d.mts +1 -1
  39. package/esm/client/navigation/verbs.mjs +1 -1
  40. package/esm/client/navigation/verbs.mjs.map +1 -1
  41. package/esm/client/runtime/index.d.mts +2 -1
  42. package/esm/client/runtime/index.mjs +2 -1
  43. package/esm/client/runtime/manifest.d.mts +1 -1
  44. package/esm/client/runtime/manifest.mjs +3 -2
  45. package/esm/client/runtime/manifest.mjs.map +1 -1
  46. package/esm/client/runtime/matcher.d.mts +13 -1
  47. package/esm/client/runtime/matcher.mjs +13 -1
  48. package/esm/client/runtime/matcher.mjs.map +1 -1
  49. package/esm/client/runtime/types.d.mts +15 -2
  50. package/esm/components/default-app.mjs +1 -1
  51. package/esm/components/default-app.mjs.map +1 -1
  52. package/esm/components/document-context.d.mts +40 -2
  53. package/esm/components/document-context.mjs +1 -1
  54. package/esm/components/document-context.mjs.map +1 -1
  55. package/esm/components/head.d.mts +1 -1
  56. package/esm/components/head.mjs +1 -1
  57. package/esm/components/head.mjs.map +1 -1
  58. package/esm/components/link.d.mts +1 -1
  59. package/esm/components/link.mjs +1 -1
  60. package/esm/components/link.mjs.map +1 -1
  61. package/esm/components/scripts.d.mts +1 -1
  62. package/esm/components/scripts.mjs +4 -2
  63. package/esm/components/scripts.mjs.map +1 -1
  64. package/esm/context.d.mts +6 -102
  65. package/esm/hydration/index.mjs +1 -1
  66. package/esm/hydration/index.mjs.map +1 -1
  67. package/esm/hydration-payload.d.mts +1 -1
  68. package/esm/hydration-payload.mjs +37 -8
  69. package/esm/hydration-payload.mjs.map +1 -1
  70. package/esm/index.d.mts +4 -3
  71. package/esm/loaders.d.mts +7 -36
  72. package/esm/metadata.d.mts +1 -1
  73. package/esm/metadata.mjs +1 -1
  74. package/esm/metadata.mjs.map +1 -1
  75. package/esm/props.d.mts +16 -8
  76. package/esm/route.d.mts +2 -17
  77. package/esm/routing/compose-route-path.d.mts +1 -1
  78. package/esm/routing/compose-route-path.mjs +1 -1
  79. package/esm/routing/compose-route-path.mjs.map +1 -1
  80. package/esm/routing/data-request.mjs +1 -1
  81. package/esm/routing/data-request.mjs.map +1 -1
  82. package/esm/routing/filesystem-route.mjs +45 -0
  83. package/esm/routing/filesystem-route.mjs.map +1 -0
  84. package/esm/routing/layout-policy.mjs +1 -1
  85. package/esm/routing/layout-policy.mjs.map +1 -1
  86. package/esm/routing/navigator.mjs +1 -1
  87. package/esm/routing/navigator.mjs.map +1 -1
  88. package/esm/routing/query-string.d.mts +1 -1
  89. package/esm/routing/query-string.mjs +1 -1
  90. package/esm/routing/query-string.mjs.map +1 -1
  91. package/esm/routing/route-identity.mjs +1 -1
  92. package/esm/routing/route-identity.mjs.map +1 -1
  93. package/esm/routing/route-table.d.mts +1 -1
  94. package/esm/routing/route-table.mjs +1 -1
  95. package/esm/routing/route-table.mjs.map +1 -1
  96. package/esm/routing/router-events.d.mts +1 -1
  97. package/esm/routing/router-events.mjs +1 -1
  98. package/esm/routing/router-events.mjs.map +1 -1
  99. package/esm/routing/url-fragment.mjs +1 -1
  100. package/esm/routing/url-fragment.mjs.map +1 -1
  101. package/esm/runtime/register-modules.d.mts +19 -0
  102. package/esm/runtime/register-modules.mjs +22 -0
  103. package/esm/runtime/register-modules.mjs.map +1 -0
  104. package/esm/server/build-hydration-payload.mjs +2 -1
  105. package/esm/server/build-hydration-payload.mjs.map +1 -1
  106. package/esm/server/client-asset-url-prefix.d.mts +1 -1
  107. package/esm/server/client-asset-url-prefix.mjs +1 -1
  108. package/esm/server/client-asset-url-prefix.mjs.map +1 -1
  109. package/esm/server/create-page-module-loader.d.mts +1 -1
  110. package/esm/server/create-page-module-loader.mjs +2 -1
  111. package/esm/server/create-page-module-loader.mjs.map +1 -1
  112. package/esm/server/create-page-route-handler.d.mts +31 -6
  113. package/esm/server/create-page-route-handler.mjs +128 -43
  114. package/esm/server/create-page-route-handler.mjs.map +1 -1
  115. package/esm/server/dev-server.mjs +2 -13
  116. package/esm/server/dev-server.mjs.map +1 -1
  117. package/esm/server/error-page.d.mts +14 -0
  118. package/esm/server/error-page.mjs +39 -0
  119. package/esm/server/error-page.mjs.map +1 -0
  120. package/esm/server/execute-page-request.d.mts +7 -4
  121. package/esm/server/execute-page-request.mjs +79 -80
  122. package/esm/server/execute-page-request.mjs.map +1 -1
  123. package/esm/server/execute-page-request.types.d.mts +33 -92
  124. package/esm/server/hydration-client-url.d.mts +1 -1
  125. package/esm/server/hydration-client-url.mjs +2 -2
  126. package/esm/server/hydration-client-url.mjs.map +1 -1
  127. package/esm/server/index.d.mts +5 -5
  128. package/esm/server/index.mjs +3 -4
  129. package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
  130. package/esm/server/install-page-routes-from-manifest.mjs +71 -10
  131. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  132. package/esm/server/install-page-routes.d.mts +32 -15
  133. package/esm/server/install-page-routes.mjs +91 -39
  134. package/esm/server/install-page-routes.mjs.map +1 -1
  135. package/esm/server/install-production-page-routes.mjs +3 -36
  136. package/esm/server/install-production-page-routes.mjs.map +1 -1
  137. package/esm/server/match-page-route.mjs +2 -2
  138. package/esm/server/match-page-route.mjs.map +1 -1
  139. package/esm/server/not-found-page.d.mts +1 -1
  140. package/esm/server/not-found-page.mjs +2 -2
  141. package/esm/server/not-found-page.mjs.map +1 -1
  142. package/esm/server/page-context.d.mts +3 -9
  143. package/esm/server/page-context.mjs +3 -16
  144. package/esm/server/page-context.mjs.map +1 -1
  145. package/esm/server/page-file-change.mjs +77 -0
  146. package/esm/server/page-file-change.mjs.map +1 -0
  147. package/esm/server/page-manifest.d.mts +25 -10
  148. package/esm/server/page-manifest.mjs +1 -1
  149. package/esm/server/page-manifest.mjs.map +1 -1
  150. package/esm/server/page-render-bundle.mjs +38 -0
  151. package/esm/server/page-render-bundle.mjs.map +1 -0
  152. package/esm/server/page-route-reload.mjs +54 -0
  153. package/esm/server/page-route-reload.mjs.map +1 -0
  154. package/esm/server/register-production-public-files.mjs +40 -0
  155. package/esm/server/register-production-public-files.mjs.map +1 -0
  156. package/esm/server/render-page.d.mts +11 -14
  157. package/esm/server/render-page.mjs +154 -28
  158. package/esm/server/render-page.mjs.map +1 -1
  159. package/esm/server/resolve-page-metadata.mjs +2 -2
  160. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  161. package/esm/server/resolve-validation-data.mjs +1 -1
  162. package/esm/server/resolve-validation-data.mjs.map +1 -1
  163. package/esm/server/settle-page-response.d.mts +58 -1
  164. package/esm/server/settle-page-response.mjs +111 -46
  165. package/esm/server/settle-page-response.mjs.map +1 -1
  166. package/esm/server/stylesheet-urls.d.mts +41 -30
  167. package/esm/server/stylesheet-urls.mjs +137 -52
  168. package/esm/server/stylesheet-urls.mjs.map +1 -1
  169. package/esm/server/unregistered-pages.mjs +42 -0
  170. package/esm/server/unregistered-pages.mjs.map +1 -0
  171. package/esm/server/web-connector-factory.d.mts +2 -3
  172. package/esm/server/web-connector-factory.mjs +7 -6
  173. package/esm/server/web-connector-factory.mjs.map +1 -1
  174. package/esm/server/web-connector.d.mts +1 -1
  175. package/esm/server/web-connector.mjs +138 -18
  176. package/esm/server/web-connector.mjs.map +1 -1
  177. package/esm/shared.d.mts +18 -3
  178. package/esm/shared.mjs +3 -7
  179. package/esm/shared.mjs.map +1 -1
  180. package/esm/validation.d.mts +1 -1
  181. package/esm/vite/app-convention-aliases.mjs +1 -1
  182. package/esm/vite/app-convention-aliases.mjs.map +1 -1
  183. package/esm/vite/build-client.d.mts +1 -1
  184. package/esm/vite/build-client.mjs +2 -2
  185. package/esm/vite/build-client.mjs.map +1 -1
  186. package/esm/vite/gate-a-resolve.d.mts +1 -1
  187. package/esm/vite/gate-a-resolve.mjs +67 -13
  188. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  189. package/esm/vite/gate-b-secrets.d.mts +1 -1
  190. package/esm/vite/gate-b-secrets.mjs +35 -17
  191. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  192. package/esm/vite/gate-c-verify.d.mts +1 -1
  193. package/esm/vite/gate-c-verify.mjs +6 -6
  194. package/esm/vite/gate-c-verify.mjs.map +1 -1
  195. package/esm/vite/hydration-entries.d.mts +1 -1
  196. package/esm/vite/hydration-entries.mjs +2 -2
  197. package/esm/vite/hydration-entries.mjs.map +1 -1
  198. package/esm/vite/index.d.mts +9 -6
  199. package/esm/vite/index.mjs +124 -11
  200. package/esm/vite/index.mjs.map +1 -1
  201. package/esm/vite/page-registry-plugin.d.mts +22 -3
  202. package/esm/vite/page-registry-plugin.mjs +48 -14
  203. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  204. package/esm/vite/projection.d.mts +2 -2
  205. package/esm/vite/projection.mjs +38 -15
  206. package/esm/vite/projection.mjs.map +1 -1
  207. package/llms-full.txt +220 -56
  208. package/llms.txt +5 -3
  209. package/package.json +3 -3
  210. package/skills/add-web-to-an-app/SKILL.md +3 -3
  211. package/skills/create-a-page/SKILL.md +117 -10
  212. package/skills/load-page-data/SKILL.md +92 -12
  213. package/skills/navigate-on-the-client/SKILL.md +1 -1
  214. package/skills/serve-styles/SKILL.md +13 -12
  215. package/skills/use-layouts/SKILL.md +14 -8
  216. package/skills/write-the-root/SKILL.md +3 -1
  217. package/esm/server/buffered-response.d.mts +0 -58
  218. package/esm/server/buffered-response.mjs +0 -115
  219. package/esm/server/buffered-response.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { currentNavigator } from "../../routing/navigator.mjs";
2
2
 
3
- //#region ../web/src/client/navigation/verbs.ts
3
+ //#region web/src/client/navigation/verbs.ts
4
4
  /**
5
5
  * The history verbs, under the names MRR spells them
6
6
  * (`@mongez/react-router` — `src/utilities.tsx`).
@@ -1 +1 @@
1
- {"version":3,"file":"verbs.mjs","names":[],"sources":["../../../../../../../../web/src/client/navigation/verbs.ts"],"sourcesContent":["import { currentNavigator } from \"../../routing/navigator\";\n\n/**\n * The history verbs, under the names MRR spells them\n * (`@mongez/react-router` — `src/utilities.tsx`).\n *\n * A developer moving between a Mongez CSR app and a Warlock SSR app should not\n * relearn \"go there\" and \"go back\", so the names and call shapes are MRR's.\n * What is NOT MRR's is the mechanism: MRR's `navigateTo` reaches its router\n * singleton, and that singleton is the client-side matcher Warlock\n * deliberately does not have — the SERVER router is the only matcher\n * (canon `9c8f878b`). So `navigateTo` here delegates to the navigator the\n * client runtime registered, and knows nothing about paths beyond passing one\n * along.\n *\n * ## Why nothing in this file throws\n *\n * All three are importable from universal modules, which means all three can\n * be CALLED with no browser and no runtime: during the server render, and in\n * the gap between first paint and hydration. Each therefore has a defined\n * \"did nothing\" answer — `false`, a no-op, `\"\"` — instead of an exception.\n * Throwing here would cost the page rather than the navigation.\n */\n\n/**\n * Navigate to a path client-side.\n *\n * @param path where to go — passed through untouched; matching is the server's\n * job, so this is not parsed, resolved or prefixed here.\n * @param options `replace: true` swaps the current history entry instead of\n * pushing a new one, so Back skips it.\n * @returns `true` if the client runtime accepted the navigation. `false` means\n * nothing handled it — either no runtime is connected yet (server render, or\n * pre-hydration) or the runtime declined the URL. A `false` caller that needs\n * the user to arrive anyway should fall back to a real browser navigation.\n */\nexport function navigateTo(path: string, options?: { replace?: boolean }): boolean {\n const navigator = currentNavigator();\n\n if (!navigator) return false;\n\n return navigator(path, options);\n}\n\n/**\n * Go back one entry in browser history.\n *\n * This is the browser's own Back, not a re-navigation to a remembered URL, so\n * it restores scroll and forward history the way the button does. No-op\n * without a `window`.\n */\nexport function navigateBack(): void {\n if (typeof window === \"undefined\") return;\n\n window.history.back();\n}\n\n/**\n * @returns the current url's hash WITHOUT its leading `#` — MRR's shape, so\n * `getHash() === \"reviews\"` for `/products#reviews`. `\"\"` when there is no\n * hash, and `\"\"` rather than `undefined` when there is no `window`, so callers\n * can treat the result as a string unconditionally.\n */\nexport function getHash(): string {\n if (typeof window === \"undefined\") return \"\";\n\n return window.location.hash.replace(\"#\", \"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,WAAW,MAAc,SAA0C;CACjF,MAAM,YAAY,iBAAiB;CAEnC,IAAI,CAAC,WAAW,OAAO;CAEvB,OAAO,UAAU,MAAM,OAAO;AAChC;;;;;;;;AASA,SAAgB,eAAqB;CACnC,IAAI,OAAO,WAAW,aAAa;CAEnC,OAAO,QAAQ,KAAK;AACtB;;;;;;;AAQA,SAAgB,UAAkB;CAChC,IAAI,OAAO,WAAW,aAAa,OAAO;CAE1C,OAAO,OAAO,SAAS,KAAK,QAAQ,KAAK,EAAE;AAC7C"}
1
+ {"version":3,"file":"verbs.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/client/navigation/verbs.ts"],"sourcesContent":["import { currentNavigator } from \"../../routing/navigator\";\n\n/**\n * The history verbs, under the names MRR spells them\n * (`@mongez/react-router` — `src/utilities.tsx`).\n *\n * A developer moving between a Mongez CSR app and a Warlock SSR app should not\n * relearn \"go there\" and \"go back\", so the names and call shapes are MRR's.\n * What is NOT MRR's is the mechanism: MRR's `navigateTo` reaches its router\n * singleton, and that singleton is the client-side matcher Warlock\n * deliberately does not have — the SERVER router is the only matcher\n * (canon `9c8f878b`). So `navigateTo` here delegates to the navigator the\n * client runtime registered, and knows nothing about paths beyond passing one\n * along.\n *\n * ## Why nothing in this file throws\n *\n * All three are importable from universal modules, which means all three can\n * be CALLED with no browser and no runtime: during the server render, and in\n * the gap between first paint and hydration. Each therefore has a defined\n * \"did nothing\" answer — `false`, a no-op, `\"\"` — instead of an exception.\n * Throwing here would cost the page rather than the navigation.\n */\n\n/**\n * Navigate to a path client-side.\n *\n * @param path where to go — passed through untouched; matching is the server's\n * job, so this is not parsed, resolved or prefixed here.\n * @param options `replace: true` swaps the current history entry instead of\n * pushing a new one, so Back skips it.\n * @returns `true` if the client runtime accepted the navigation. `false` means\n * nothing handled it — either no runtime is connected yet (server render, or\n * pre-hydration) or the runtime declined the URL. A `false` caller that needs\n * the user to arrive anyway should fall back to a real browser navigation.\n */\nexport function navigateTo(path: string, options?: { replace?: boolean }): boolean {\n const navigator = currentNavigator();\n\n if (!navigator) return false;\n\n return navigator(path, options);\n}\n\n/**\n * Go back one entry in browser history.\n *\n * This is the browser's own Back, not a re-navigation to a remembered URL, so\n * it restores scroll and forward history the way the button does. No-op\n * without a `window`.\n */\nexport function navigateBack(): void {\n if (typeof window === \"undefined\") return;\n\n window.history.back();\n}\n\n/**\n * @returns the current url's hash WITHOUT its leading `#` — MRR's shape, so\n * `getHash() === \"reviews\"` for `/products#reviews`. `\"\"` when there is no\n * hash, and `\"\"` rather than `undefined` when there is no `window`, so callers\n * can treat the result as a string unconditionally.\n */\nexport function getHash(): string {\n if (typeof window === \"undefined\") return \"\";\n\n return window.location.hash.replace(\"#\", \"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,WAAW,MAAc,SAA0C;CACjF,MAAM,YAAY,iBAAiB;CAEnC,IAAI,CAAC,WAAW,OAAO;CAEvB,OAAO,UAAU,MAAM,OAAO;AAChC;;;;;;;;AASA,SAAgB,eAAqB;CACnC,IAAI,OAAO,WAAW,aAAa;CAEnC,OAAO,QAAQ,KAAK;AACtB;;;;;;;AAQA,SAAgB,UAAkB;CAChC,IAAI,OAAO,WAAW,aAAa,OAAO;CAE1C,OAAO,OAAO,SAAS,KAAK,QAAQ,KAAK,EAAE;AAC7C"}
@@ -1,4 +1,5 @@
1
+ import { registerModules } from "../../runtime/register-modules.mjs";
1
2
  import { ClientPageEntry, ClientProjectedModule, ClientRouteComposition, ClientRouteLoad, ClientRouteMatch, ClientRouteParams } from "./types.mjs";
2
3
  import { loadClientRouteComposition, validateClientRouteManifest } from "./manifest.mjs";
3
4
  import { matchClientRoute } from "./matcher.mjs";
4
- export { type ClientPageEntry, type ClientProjectedModule, type ClientRouteComposition, type ClientRouteLoad, type ClientRouteMatch, type ClientRouteParams, loadClientRouteComposition, matchClientRoute, validateClientRouteManifest };
5
+ export { type ClientPageEntry, type ClientProjectedModule, type ClientRouteComposition, type ClientRouteLoad, type ClientRouteMatch, type ClientRouteParams, loadClientRouteComposition, matchClientRoute, registerModules, validateClientRouteManifest };
@@ -1,4 +1,5 @@
1
+ import { registerModules } from "../../runtime/register-modules.mjs";
1
2
  import { loadClientRouteComposition, validateClientRouteManifest } from "./manifest.mjs";
2
3
  import { matchClientRoute } from "./matcher.mjs";
3
4
 
4
- export { loadClientRouteComposition, matchClientRoute, validateClientRouteManifest };
5
+ export { loadClientRouteComposition, matchClientRoute, registerModules, validateClientRouteManifest };
@@ -1,6 +1,6 @@
1
1
  import { ClientPageEntry, ClientRouteComposition } from "./types.mjs";
2
2
 
3
- //#region ../web/src/client/runtime/manifest.d.ts
3
+ //#region web/src/client/runtime/manifest.d.ts
4
4
  declare function validateClientRouteManifest(input: unknown): readonly ClientPageEntry[];
5
5
  declare function loadClientRouteComposition(entry: ClientPageEntry): Promise<ClientRouteComposition>;
6
6
  //#endregion
@@ -1,4 +1,4 @@
1
- //#region ../web/src/client/runtime/manifest.ts
1
+ //#region web/src/client/runtime/manifest.ts
2
2
  const ENTRY_KEYS = [
3
3
  "type",
4
4
  "name",
@@ -6,7 +6,7 @@ const ENTRY_KEYS = [
6
6
  "load"
7
7
  ];
8
8
  const COMPOSITION_REQUIRED_KEYS = ["Page", "layouts"];
9
- const COMPOSITION_OPTIONAL_KEYS = ["App"];
9
+ const COMPOSITION_OPTIONAL_KEYS = ["App", "ErrorPage"];
10
10
  function isNonArrayObject(value) {
11
11
  return typeof value === "object" && value !== null && !Array.isArray(value);
12
12
  }
@@ -53,6 +53,7 @@ function validateComposition(input) {
53
53
  validateProjectedModule(layout, `${label} layout at index ${index}`);
54
54
  });
55
55
  if (Object.prototype.hasOwnProperty.call(input, "App")) validateProjectedModule(input.App, `${label} App`);
56
+ if (Object.prototype.hasOwnProperty.call(input, "ErrorPage")) validateProjectedModule(input.ErrorPage, `${label} ErrorPage`);
56
57
  return input;
57
58
  }
58
59
  function validateClientRouteManifest(input) {
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.mjs","names":[],"sources":["../../../../../../../../web/src/client/runtime/manifest.ts"],"sourcesContent":["import type {\n ClientPageEntry,\n ClientProjectedModule,\n ClientRouteComposition,\n} from \"./types\";\n\nconst ENTRY_KEYS = [\"type\", \"name\", \"path\", \"load\"] as const;\nconst COMPOSITION_REQUIRED_KEYS = [\"Page\", \"layouts\"] as const;\nconst COMPOSITION_OPTIONAL_KEYS = [\"App\"] as const;\n\ntype DataRecord = Record<PropertyKey, unknown>;\n\nfunction isNonArrayObject(value: unknown): value is DataRecord {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction printableValue(value: unknown): string {\n if (typeof value === \"string\") return JSON.stringify(value);\n\n try {\n return String(value);\n } catch {\n return \"<unprintable>\";\n }\n}\n\nfunction assertExactDataKeys(\n value: DataRecord,\n requiredKeys: readonly string[],\n optionalKeys: readonly string[],\n label: string,\n): void {\n const allowedKeys = new Set([...requiredKeys, ...optionalKeys]);\n const descriptors = Object.getOwnPropertyDescriptors(value);\n const ownKeys = Reflect.ownKeys(value);\n\n for (const key of ownKeys) {\n if (typeof key !== \"string\" || !allowedKeys.has(key)) {\n throw new TypeError(`${label} has unexpected own key ${JSON.stringify(String(key))}.`);\n }\n }\n\n for (const key of requiredKeys) {\n if (!Object.prototype.hasOwnProperty.call(descriptors, key)) {\n throw new TypeError(`${label} is missing own key ${JSON.stringify(key)}.`);\n }\n }\n\n for (const key of ownKeys) {\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n if (descriptor && !(\"value\" in descriptor)) {\n throw new TypeError(\n `${label} key ${JSON.stringify(String(key))} must be an own data property.`,\n );\n }\n }\n}\n\nfunction validateEntry(input: unknown, index: number): ClientPageEntry {\n const label = `Client route manifest entry at index ${index}`;\n\n if (!isNonArrayObject(input)) {\n throw new TypeError(`${label} must be a non-array object.`);\n }\n\n assertExactDataKeys(input, ENTRY_KEYS, [], label);\n\n if (input.type !== \"page\") {\n throw new TypeError(\n `${label} has unknown type ${printableValue(input.type)}; expected \"page\".`,\n );\n }\n\n if (typeof input.name !== \"string\" || input.name.trim().length === 0) {\n throw new TypeError(`${label} name must be a non-empty string.`);\n }\n\n if (typeof input.path !== \"string\" || input.path.trim().length === 0) {\n throw new TypeError(`${label} path must be a non-empty string.`);\n }\n\n if (typeof input.load !== \"function\") {\n throw new TypeError(`${label} load must be callable.`);\n }\n\n return input as ClientPageEntry;\n}\n\nfunction validateProjectedModule(input: unknown, label: string): ClientProjectedModule {\n if (!isNonArrayObject(input)) {\n throw new TypeError(`${label} must be a non-array module object.`);\n }\n\n return input;\n}\n\nfunction validateComposition(input: unknown): ClientRouteComposition {\n const label = \"Loaded client route composition\";\n\n if (!isNonArrayObject(input)) {\n throw new TypeError(`${label} must be a non-array object.`);\n }\n\n assertExactDataKeys(\n input,\n COMPOSITION_REQUIRED_KEYS,\n COMPOSITION_OPTIONAL_KEYS,\n label,\n );\n validateProjectedModule(input.Page, `${label} Page`);\n\n if (!Array.isArray(input.layouts)) {\n throw new TypeError(`${label} layouts must be an array.`);\n }\n\n input.layouts.forEach((layout, index) => {\n validateProjectedModule(layout, `${label} layout at index ${index}`);\n });\n\n if (Object.prototype.hasOwnProperty.call(input, \"App\")) {\n validateProjectedModule(input.App, `${label} App`);\n }\n\n return input as ClientRouteComposition;\n}\n\nexport function validateClientRouteManifest(input: unknown): readonly ClientPageEntry[] {\n if (!Array.isArray(input)) {\n throw new TypeError(\"Client route manifest must be an array.\");\n }\n\n const names = new Set<string>();\n const paths = new Set<string>();\n\n return input.map((candidate, index) => {\n const entry = validateEntry(candidate, index);\n\n if (names.has(entry.name)) {\n throw new TypeError(\n `Client route manifest has duplicate name ${JSON.stringify(entry.name)}.`,\n );\n }\n\n if (paths.has(entry.path)) {\n throw new TypeError(\n `Client route manifest has duplicate path ${JSON.stringify(entry.path)}.`,\n );\n }\n\n names.add(entry.name);\n paths.add(entry.path);\n return entry;\n });\n}\n\nexport async function loadClientRouteComposition(\n entry: ClientPageEntry,\n): Promise<ClientRouteComposition> {\n const loaded = await entry.load();\n return validateComposition(loaded);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"manifest.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/client/runtime/manifest.ts"],"sourcesContent":["import type {\n ClientPageEntry,\n ClientProjectedModule,\n ClientRouteComposition,\n} from \"./types\";\n\nconst ENTRY_KEYS = [\"type\", \"name\", \"path\", \"load\"] as const;\nconst COMPOSITION_REQUIRED_KEYS = [\"Page\", \"layouts\"] as const;\nconst COMPOSITION_OPTIONAL_KEYS = [\"App\", \"ErrorPage\"] as const;\n\ntype DataRecord = Record<PropertyKey, unknown>;\n\nfunction isNonArrayObject(value: unknown): value is DataRecord {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction printableValue(value: unknown): string {\n if (typeof value === \"string\") return JSON.stringify(value);\n\n try {\n return String(value);\n } catch {\n return \"<unprintable>\";\n }\n}\n\nfunction assertExactDataKeys(\n value: DataRecord,\n requiredKeys: readonly string[],\n optionalKeys: readonly string[],\n label: string,\n): void {\n const allowedKeys = new Set([...requiredKeys, ...optionalKeys]);\n const descriptors = Object.getOwnPropertyDescriptors(value);\n const ownKeys = Reflect.ownKeys(value);\n\n for (const key of ownKeys) {\n if (typeof key !== \"string\" || !allowedKeys.has(key)) {\n throw new TypeError(`${label} has unexpected own key ${JSON.stringify(String(key))}.`);\n }\n }\n\n for (const key of requiredKeys) {\n if (!Object.prototype.hasOwnProperty.call(descriptors, key)) {\n throw new TypeError(`${label} is missing own key ${JSON.stringify(key)}.`);\n }\n }\n\n for (const key of ownKeys) {\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n if (descriptor && !(\"value\" in descriptor)) {\n throw new TypeError(\n `${label} key ${JSON.stringify(String(key))} must be an own data property.`,\n );\n }\n }\n}\n\nfunction validateEntry(input: unknown, index: number): ClientPageEntry {\n const label = `Client route manifest entry at index ${index}`;\n\n if (!isNonArrayObject(input)) {\n throw new TypeError(`${label} must be a non-array object.`);\n }\n\n assertExactDataKeys(input, ENTRY_KEYS, [], label);\n\n if (input.type !== \"page\") {\n throw new TypeError(\n `${label} has unknown type ${printableValue(input.type)}; expected \"page\".`,\n );\n }\n\n if (typeof input.name !== \"string\" || input.name.trim().length === 0) {\n throw new TypeError(`${label} name must be a non-empty string.`);\n }\n\n if (typeof input.path !== \"string\" || input.path.trim().length === 0) {\n throw new TypeError(`${label} path must be a non-empty string.`);\n }\n\n if (typeof input.load !== \"function\") {\n throw new TypeError(`${label} load must be callable.`);\n }\n\n return input as ClientPageEntry;\n}\n\nfunction validateProjectedModule(input: unknown, label: string): ClientProjectedModule {\n if (!isNonArrayObject(input)) {\n throw new TypeError(`${label} must be a non-array module object.`);\n }\n\n return input;\n}\n\nfunction validateComposition(input: unknown): ClientRouteComposition {\n const label = \"Loaded client route composition\";\n\n if (!isNonArrayObject(input)) {\n throw new TypeError(`${label} must be a non-array object.`);\n }\n\n assertExactDataKeys(\n input,\n COMPOSITION_REQUIRED_KEYS,\n COMPOSITION_OPTIONAL_KEYS,\n label,\n );\n validateProjectedModule(input.Page, `${label} Page`);\n\n if (!Array.isArray(input.layouts)) {\n throw new TypeError(`${label} layouts must be an array.`);\n }\n\n input.layouts.forEach((layout, index) => {\n validateProjectedModule(layout, `${label} layout at index ${index}`);\n });\n\n if (Object.prototype.hasOwnProperty.call(input, \"App\")) {\n validateProjectedModule(input.App, `${label} App`);\n }\n\n if (Object.prototype.hasOwnProperty.call(input, \"ErrorPage\")) {\n validateProjectedModule(input.ErrorPage, `${label} ErrorPage`);\n }\n\n return input as ClientRouteComposition;\n}\n\nexport function validateClientRouteManifest(input: unknown): readonly ClientPageEntry[] {\n if (!Array.isArray(input)) {\n throw new TypeError(\"Client route manifest must be an array.\");\n }\n\n const names = new Set<string>();\n const paths = new Set<string>();\n\n return input.map((candidate, index) => {\n const entry = validateEntry(candidate, index);\n\n if (names.has(entry.name)) {\n throw new TypeError(\n `Client route manifest has duplicate name ${JSON.stringify(entry.name)}.`,\n );\n }\n\n if (paths.has(entry.path)) {\n throw new TypeError(\n `Client route manifest has duplicate path ${JSON.stringify(entry.path)}.`,\n );\n }\n\n names.add(entry.name);\n paths.add(entry.path);\n return entry;\n });\n}\n\nexport async function loadClientRouteComposition(\n entry: ClientPageEntry,\n): Promise<ClientRouteComposition> {\n const loaded = await entry.load();\n return validateComposition(loaded);\n}\n"],"mappings":"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"}
@@ -1,6 +1,18 @@
1
1
  import { ClientPageEntry, ClientRouteMatch } from "./types.mjs";
2
2
 
3
- //#region ../web/src/client/runtime/matcher.d.ts
3
+ //#region web/src/client/runtime/matcher.d.ts
4
+ /**
5
+ * @deprecated Do not adopt for new code. This client-side matcher duplicates the
6
+ * route grammar the server already evaluates, and divergence between the two is
7
+ * silent (wrong page, not an error). It is superseded by navigation consuming the
8
+ * server-returned page composition/page swap: the client requests loader data and
9
+ * the matched page's identity rides back on that same response.
10
+ *
11
+ * Delete only after the server-answered page swap is proven working in production
12
+ * use — not before. Deleting earlier leaves neither implementation in place.
13
+ * Removing this export (and the `@warlock.js/web/client/runtime` re-export) is a
14
+ * breaking change to a published subpath and must be announced as one.
15
+ */
4
16
  declare function matchClientRoute(entries: readonly ClientPageEntry[], pathname: string): ClientRouteMatch | null;
5
17
  //#endregion
6
18
  export { matchClientRoute };
@@ -1,4 +1,4 @@
1
- //#region ../web/src/client/runtime/matcher.ts
1
+ //#region web/src/client/runtime/matcher.ts
2
2
  const PARAMETER_NAME = /^[A-Za-z0-9_]+$/;
3
3
  const REGEXP_SPECIAL = /[.*+?^${}()|[\]\\]/g;
4
4
  function escapeRegExp(value) {
@@ -144,6 +144,18 @@ function compileRoutes(entries) {
144
144
  return route;
145
145
  }).sort(compareSpecificity);
146
146
  }
147
+ /**
148
+ * @deprecated Do not adopt for new code. This client-side matcher duplicates the
149
+ * route grammar the server already evaluates, and divergence between the two is
150
+ * silent (wrong page, not an error). It is superseded by navigation consuming the
151
+ * server-returned page composition/page swap: the client requests loader data and
152
+ * the matched page's identity rides back on that same response.
153
+ *
154
+ * Delete only after the server-answered page swap is proven working in production
155
+ * use — not before. Deleting earlier leaves neither implementation in place.
156
+ * Removing this export (and the `@warlock.js/web/client/runtime` re-export) is a
157
+ * breaking change to a published subpath and must be announced as one.
158
+ */
147
159
  function matchClientRoute(entries, pathname) {
148
160
  const routes = compileRoutes(entries);
149
161
  const sanitized = sanitizePathname(pathname);
@@ -1 +1 @@
1
- {"version":3,"file":"matcher.mjs","names":[],"sources":["../../../../../../../../web/src/client/runtime/matcher.ts"],"sourcesContent":["import type { ClientPageEntry, ClientRouteMatch } from \"./types\";\n\ntype RouteToken =\n | { readonly type: \"static\"; readonly value: string }\n | { readonly type: \"parameter\"; readonly name: string }\n | { readonly type: \"catch-all\" };\n\ntype CompiledRoute = {\n readonly entry: ClientPageEntry;\n readonly tokens: readonly RouteToken[];\n readonly parameterNames: readonly string[];\n readonly expression: RegExp;\n readonly collisionKey: string;\n};\n\ntype SanitizedPath = {\n readonly path: string;\n readonly shouldDecodeParameters: boolean;\n};\n\nconst PARAMETER_NAME = /^[A-Za-z0-9_]+$/;\nconst REGEXP_SPECIAL = /[.*+?^${}()|[\\]\\\\]/g;\n\nfunction escapeRegExp(value: string): string {\n return value.replace(REGEXP_SPECIAL, \"\\\\$&\");\n}\n\nfunction decodeReservedCharacter(high: string, low: string): string | null {\n const pair = `${high}${low}`.toUpperCase();\n const reserved: Readonly<Record<string, string>> = {\n \"23\": \"#\",\n \"24\": \"$\",\n \"25\": \"%\",\n \"26\": \"&\",\n \"2B\": \"+\",\n \"2C\": \",\",\n \"2F\": \"/\",\n \"3A\": \":\",\n \"3B\": \";\",\n \"3D\": \"=\",\n \"3F\": \"?\",\n \"40\": \"@\",\n };\n\n return reserved[pair] ?? null;\n}\n\nfunction sanitizePathname(pathname: string): SanitizedPath | null {\n if (typeof pathname !== \"string\" || !pathname.startsWith(\"/\")) {\n throw new Error(\"Client route pathname must start with '/'\");\n }\n\n let path = pathname;\n let shouldDecode = false;\n let shouldDecodeParameters = false;\n\n for (let index = 1; index < path.length; index++) {\n if (path[index] !== \"%\") continue;\n\n const high = path[index + 1] ?? \"\";\n const low = path[index + 2] ?? \"\";\n const reserved = decodeReservedCharacter(high, low);\n\n if (reserved === null) {\n shouldDecode = true;\n continue;\n }\n\n shouldDecodeParameters = true;\n if (reserved === \"%\") {\n path = `${path.slice(0, index + 1)}25${path.slice(index + 1)}`;\n shouldDecode = true;\n index += 2;\n }\n index += 2;\n }\n\n try {\n if (shouldDecode) path = decodeURI(path);\n } catch {\n return null;\n }\n\n if (path.length > 1 && path.endsWith(\"/\")) path = path.slice(0, -1);\n\n return { path, shouldDecodeParameters };\n}\n\nfunction decodeParameter(value: string): string {\n let decoded = \"\";\n\n for (let index = 0; index < value.length; index++) {\n if (value[index] !== \"%\") {\n decoded += value[index];\n continue;\n }\n\n const reserved = decodeReservedCharacter(value[index + 1] ?? \"\", value[index + 2] ?? \"\");\n if (reserved === null) return value;\n\n decoded += reserved;\n index += 2;\n }\n\n return decoded;\n}\n\nfunction parsePattern(entry: ClientPageEntry): CompiledRoute {\n const original = entry.path;\n const isExactRootCatchAll = original === \"*\";\n if (!isExactRootCatchAll && !original.startsWith(\"/\")) {\n throw new Error(`Client route pattern '${original}' must start with '/'`);\n }\n\n const pattern = original.length > 1 && original.endsWith(\"/\")\n ? original.slice(0, -1)\n : original;\n const segments = isExactRootCatchAll\n ? [\"*\"]\n : pattern === \"/\"\n ? []\n : pattern.slice(1).split(\"/\");\n const tokens: RouteToken[] = [];\n const parameterNames: string[] = [];\n\n for (let index = 0; index < segments.length; index++) {\n const segment = segments[index];\n if (!segment) {\n throw new Error(`Client route pattern '${original}' contains an empty segment`);\n }\n\n if (segment === \"*\") {\n if (index !== segments.length - 1) {\n throw new Error(`Client route pattern '${original}' has a non-terminal catch-all`);\n }\n tokens.push({ type: \"catch-all\" });\n parameterNames.push(\"*\");\n continue;\n }\n\n if (segment.startsWith(\":\")) {\n const name = segment.slice(1);\n if (!PARAMETER_NAME.test(name)) {\n throw new Error(`Client route pattern '${original}' has an unsupported parameter segment`);\n }\n if (parameterNames.includes(name)) {\n throw new Error(`Client route pattern '${original}' repeats parameter '${name}'`);\n }\n tokens.push({ type: \"parameter\", name });\n parameterNames.push(name);\n continue;\n }\n\n if (segment.includes(\":\") || segment.includes(\"*\") || segment.includes(\"?\") || segment.includes(\"%\")) {\n throw new Error(`Client route pattern '${original}' contains unsupported syntax`);\n }\n tokens.push({ type: \"static\", value: segment });\n }\n\n let source = \"^\";\n for (let index = 0; index < tokens.length; index++) {\n const token = tokens[index];\n if (token.type === \"static\") source += `/${escapeRegExp(token.value)}`;\n if (token.type === \"parameter\") source += \"/([^/]{1,100})\";\n if (token.type === \"catch-all\") {\n source += isExactRootCatchAll ? \"(.*)\" : index === 0 ? \"/(.*)\" : \"/(.+)\";\n }\n }\n if (tokens.length === 0) source += \"/\";\n source += \"$\";\n\n const collisionKey = tokens\n .map((token) => {\n if (token.type === \"static\") return `s:${token.value.toLowerCase()}`;\n if (token.type === \"parameter\") return \"p\";\n return \"w\";\n })\n .join(\"/\");\n\n return {\n entry,\n tokens,\n parameterNames,\n expression: new RegExp(source, \"i\"),\n collisionKey,\n };\n}\n\nfunction compareSpecificity(left: CompiledRoute, right: CompiledRoute): number {\n const rank = (token: RouteToken | undefined): number => {\n if (!token || token.type === \"static\") return 3;\n if (token.type === \"parameter\") return 2;\n return 1;\n };\n\n const length = Math.max(left.tokens.length, right.tokens.length);\n for (let index = 0; index < length; index++) {\n const difference = rank(right.tokens[index]) - rank(left.tokens[index]);\n if (difference !== 0) return difference;\n }\n return 0;\n}\n\nfunction compileRoutes(entries: readonly ClientPageEntry[]): readonly CompiledRoute[] {\n const collisions = new Map<string, ClientPageEntry>();\n const routes = entries.map((entry) => {\n const route = parsePattern(entry);\n const existing = collisions.get(route.collisionKey);\n if (existing) {\n throw new Error(\n `Client route patterns '${existing.path}' and '${entry.path}' collide under server matching`,\n );\n }\n collisions.set(route.collisionKey, entry);\n return route;\n });\n\n return routes.sort(compareSpecificity);\n}\n\nexport function matchClientRoute(\n entries: readonly ClientPageEntry[],\n pathname: string,\n): ClientRouteMatch | null {\n const routes = compileRoutes(entries);\n const sanitized = sanitizePathname(pathname);\n if (!sanitized) return null;\n\n for (const route of routes) {\n const match = route.expression.exec(sanitized.path);\n if (!match) continue;\n\n const params: Record<string, string> = {};\n for (let index = 0; index < route.parameterNames.length; index++) {\n const value = match[index + 1];\n params[route.parameterNames[index]] = sanitized.shouldDecodeParameters\n ? decodeParameter(value)\n : value;\n }\n return { entry: route.entry, params };\n }\n\n return null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"matcher.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/client/runtime/matcher.ts"],"sourcesContent":["import type { ClientPageEntry, ClientRouteMatch } from \"./types\";\n\ntype RouteToken =\n | { readonly type: \"static\"; readonly value: string }\n | { readonly type: \"parameter\"; readonly name: string }\n | { readonly type: \"catch-all\" };\n\ntype CompiledRoute = {\n readonly entry: ClientPageEntry;\n readonly tokens: readonly RouteToken[];\n readonly parameterNames: readonly string[];\n readonly expression: RegExp;\n readonly collisionKey: string;\n};\n\ntype SanitizedPath = {\n readonly path: string;\n readonly shouldDecodeParameters: boolean;\n};\n\nconst PARAMETER_NAME = /^[A-Za-z0-9_]+$/;\nconst REGEXP_SPECIAL = /[.*+?^${}()|[\\]\\\\]/g;\n\nfunction escapeRegExp(value: string): string {\n return value.replace(REGEXP_SPECIAL, \"\\\\$&\");\n}\n\nfunction decodeReservedCharacter(high: string, low: string): string | null {\n const pair = `${high}${low}`.toUpperCase();\n const reserved: Readonly<Record<string, string>> = {\n \"23\": \"#\",\n \"24\": \"$\",\n \"25\": \"%\",\n \"26\": \"&\",\n \"2B\": \"+\",\n \"2C\": \",\",\n \"2F\": \"/\",\n \"3A\": \":\",\n \"3B\": \";\",\n \"3D\": \"=\",\n \"3F\": \"?\",\n \"40\": \"@\",\n };\n\n return reserved[pair] ?? null;\n}\n\nfunction sanitizePathname(pathname: string): SanitizedPath | null {\n if (typeof pathname !== \"string\" || !pathname.startsWith(\"/\")) {\n throw new Error(\"Client route pathname must start with '/'\");\n }\n\n let path = pathname;\n let shouldDecode = false;\n let shouldDecodeParameters = false;\n\n for (let index = 1; index < path.length; index++) {\n if (path[index] !== \"%\") continue;\n\n const high = path[index + 1] ?? \"\";\n const low = path[index + 2] ?? \"\";\n const reserved = decodeReservedCharacter(high, low);\n\n if (reserved === null) {\n shouldDecode = true;\n continue;\n }\n\n shouldDecodeParameters = true;\n if (reserved === \"%\") {\n path = `${path.slice(0, index + 1)}25${path.slice(index + 1)}`;\n shouldDecode = true;\n index += 2;\n }\n index += 2;\n }\n\n try {\n if (shouldDecode) path = decodeURI(path);\n } catch {\n return null;\n }\n\n if (path.length > 1 && path.endsWith(\"/\")) path = path.slice(0, -1);\n\n return { path, shouldDecodeParameters };\n}\n\nfunction decodeParameter(value: string): string {\n let decoded = \"\";\n\n for (let index = 0; index < value.length; index++) {\n if (value[index] !== \"%\") {\n decoded += value[index];\n continue;\n }\n\n const reserved = decodeReservedCharacter(value[index + 1] ?? \"\", value[index + 2] ?? \"\");\n if (reserved === null) return value;\n\n decoded += reserved;\n index += 2;\n }\n\n return decoded;\n}\n\nfunction parsePattern(entry: ClientPageEntry): CompiledRoute {\n const original = entry.path;\n const isExactRootCatchAll = original === \"*\";\n if (!isExactRootCatchAll && !original.startsWith(\"/\")) {\n throw new Error(`Client route pattern '${original}' must start with '/'`);\n }\n\n const pattern = original.length > 1 && original.endsWith(\"/\")\n ? original.slice(0, -1)\n : original;\n const segments = isExactRootCatchAll\n ? [\"*\"]\n : pattern === \"/\"\n ? []\n : pattern.slice(1).split(\"/\");\n const tokens: RouteToken[] = [];\n const parameterNames: string[] = [];\n\n for (let index = 0; index < segments.length; index++) {\n const segment = segments[index];\n if (!segment) {\n throw new Error(`Client route pattern '${original}' contains an empty segment`);\n }\n\n if (segment === \"*\") {\n if (index !== segments.length - 1) {\n throw new Error(`Client route pattern '${original}' has a non-terminal catch-all`);\n }\n tokens.push({ type: \"catch-all\" });\n parameterNames.push(\"*\");\n continue;\n }\n\n if (segment.startsWith(\":\")) {\n const name = segment.slice(1);\n if (!PARAMETER_NAME.test(name)) {\n throw new Error(`Client route pattern '${original}' has an unsupported parameter segment`);\n }\n if (parameterNames.includes(name)) {\n throw new Error(`Client route pattern '${original}' repeats parameter '${name}'`);\n }\n tokens.push({ type: \"parameter\", name });\n parameterNames.push(name);\n continue;\n }\n\n if (segment.includes(\":\") || segment.includes(\"*\") || segment.includes(\"?\") || segment.includes(\"%\")) {\n throw new Error(`Client route pattern '${original}' contains unsupported syntax`);\n }\n tokens.push({ type: \"static\", value: segment });\n }\n\n let source = \"^\";\n for (let index = 0; index < tokens.length; index++) {\n const token = tokens[index];\n if (token.type === \"static\") source += `/${escapeRegExp(token.value)}`;\n if (token.type === \"parameter\") source += \"/([^/]{1,100})\";\n if (token.type === \"catch-all\") {\n source += isExactRootCatchAll ? \"(.*)\" : index === 0 ? \"/(.*)\" : \"/(.+)\";\n }\n }\n if (tokens.length === 0) source += \"/\";\n source += \"$\";\n\n const collisionKey = tokens\n .map((token) => {\n if (token.type === \"static\") return `s:${token.value.toLowerCase()}`;\n if (token.type === \"parameter\") return \"p\";\n return \"w\";\n })\n .join(\"/\");\n\n return {\n entry,\n tokens,\n parameterNames,\n expression: new RegExp(source, \"i\"),\n collisionKey,\n };\n}\n\nfunction compareSpecificity(left: CompiledRoute, right: CompiledRoute): number {\n const rank = (token: RouteToken | undefined): number => {\n if (!token || token.type === \"static\") return 3;\n if (token.type === \"parameter\") return 2;\n return 1;\n };\n\n const length = Math.max(left.tokens.length, right.tokens.length);\n for (let index = 0; index < length; index++) {\n const difference = rank(right.tokens[index]) - rank(left.tokens[index]);\n if (difference !== 0) return difference;\n }\n return 0;\n}\n\nfunction compileRoutes(entries: readonly ClientPageEntry[]): readonly CompiledRoute[] {\n const collisions = new Map<string, ClientPageEntry>();\n const routes = entries.map((entry) => {\n const route = parsePattern(entry);\n const existing = collisions.get(route.collisionKey);\n if (existing) {\n throw new Error(\n `Client route patterns '${existing.path}' and '${entry.path}' collide under server matching`,\n );\n }\n collisions.set(route.collisionKey, entry);\n return route;\n });\n\n return routes.sort(compareSpecificity);\n}\n\n/**\n * @deprecated Do not adopt for new code. This client-side matcher duplicates the\n * route grammar the server already evaluates, and divergence between the two is\n * silent (wrong page, not an error). It is superseded by navigation consuming the\n * server-returned page composition/page swap: the client requests loader data and\n * the matched page's identity rides back on that same response.\n *\n * Delete only after the server-answered page swap is proven working in production\n * use — not before. Deleting earlier leaves neither implementation in place.\n * Removing this export (and the `@warlock.js/web/client/runtime` re-export) is a\n * breaking change to a published subpath and must be announced as one.\n */\nexport function matchClientRoute(\n entries: readonly ClientPageEntry[],\n pathname: string,\n): ClientRouteMatch | null {\n const routes = compileRoutes(entries);\n const sanitized = sanitizePathname(pathname);\n if (!sanitized) return null;\n\n for (const route of routes) {\n const match = route.expression.exec(sanitized.path);\n if (!match) continue;\n\n const params: Record<string, string> = {};\n for (let index = 0; index < route.parameterNames.length; index++) {\n const value = match[index + 1];\n params[route.parameterNames[index]] = sanitized.shouldDecodeParameters\n ? decodeParameter(value)\n : value;\n }\n return { entry: route.entry, params };\n }\n\n return null;\n}\n"],"mappings":"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"}
@@ -1,9 +1,22 @@
1
- //#region ../web/src/client/runtime/types.d.ts
2
- type ClientProjectedModule = Readonly<Record<string, unknown>>;
1
+ import { RegisterableModuleNamespace } from "../../runtime/register-modules.mjs";
2
+
3
+ //#region web/src/client/runtime/types.d.ts
4
+ /**
5
+ * A real ESM namespace returned by dynamic import(), retained so universal
6
+ * lifecycle exports and the default React component remain available together.
7
+ */
8
+ type ClientProjectedModule = Readonly<Record<string, unknown>> & RegisterableModuleNamespace;
3
9
  type ClientRouteComposition = {
4
10
  readonly Page: ClientProjectedModule;
5
11
  readonly layouts: readonly ClientProjectedModule[];
6
12
  readonly App?: ClientProjectedModule;
13
+ /**
14
+ * The app-owned error leaf projected for this route. Optional so registries
15
+ * produced before `error.page.tsx` support remain valid byte-for-shape.
16
+ * Presence does not create a route; the hydration payload decides whether
17
+ * the server rendered this module instead of `Page`.
18
+ */
19
+ readonly ErrorPage?: ClientProjectedModule;
7
20
  };
8
21
  type ClientRouteLoad = () => ClientRouteComposition | Promise<ClientRouteComposition>;
9
22
  type ClientPageEntry = {
@@ -3,7 +3,7 @@ import { Head } from "./head.mjs";
3
3
  import { Scripts } from "./scripts.mjs";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
 
6
- //#region ../web/src/components/default-app.tsx
6
+ //#region web/src/components/default-app.tsx
7
7
  /**
8
8
  * The framework's own root, used whenever an app defines no `root.tsx` (or
9
9
  * omits a default export). The framework's default App is ALSO a full
@@ -1 +1 @@
1
- {"version":3,"file":"default-app.mjs","names":[],"sources":["../../../../../../../web/src/components/default-app.tsx"],"sourcesContent":["import type { ReactElement, ReactNode } from \"react\";\nimport { useDocumentContext } from \"./document-context\";\nimport { Head } from \"./head\";\nimport { Scripts } from \"./scripts\";\n\nexport type DefaultAppProps = {\n children: ReactNode;\n};\n\n/**\n * The framework's own root, used whenever an app defines no `root.tsx` (or\n * omits a default export). The framework's default App is ALSO a full\n * document — so every page renders a complete `<html>` regardless of\n * whether the app customized its root. The app owns the document: when a\n * custom `root.tsx` exists (`v5/app/src/web/root.tsx` is that example), it is\n * fully responsible for `lang`/`dir`/`<Scripts nonce>`/providers/its own\n * `<head>` tags. Only when no custom App exists does this fallback supply\n * `lang`/`dir`/nonce itself, sourced from the framework's document-context\n * slots rather than any app-owned data.\n */\nexport default function DefaultApp({ children }: DefaultAppProps): ReactElement {\n const { lang, dir } = useDocumentContext(\"DefaultApp\");\n\n return (\n <html lang={lang} dir={dir}>\n <head>\n <Head />\n </head>\n <body>\n {/*\n The hydration mount point (documented previously as\n `render-page.ts`'s own hardcoded shell, now moved here since the\n root supplies the whole document itself). An app writing its own\n custom `root.tsx` is responsible for its own equivalent if it wants\n a stable hydration target — this default only covers the\n no-custom-root case.\n */}\n <div id=\"root\">{children}</div>\n {/* Prop-less: inherits the nonce slot via Scripts' own fallback\n (scripts.ts) rather than reading document-context twice here. */}\n <Scripts />\n </body>\n </html>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,SAAwB,WAAW,EAAE,YAA2C;CAC9E,MAAM,EAAE,MAAM,QAAQ,mBAAmB,YAAY;CAErD,OACE,qBAAC,QAAD;EAAY;EAAW;YAAvB,CACE,oBAAC,QAAD,YACE,oBAAC,MAAD,CAAO,GACH,IACN,qBAAC,QAAD,aASE,oBAAC,OAAD;GAAK,IAAG;GAAQ;EAAc,IAG9B,oBAAC,SAAD,CAAU,EACN,IACF;;AAEV"}
1
+ {"version":3,"file":"default-app.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/components/default-app.tsx"],"sourcesContent":["import type { ReactElement, ReactNode } from \"react\";\nimport { useDocumentContext } from \"./document-context\";\nimport { Head } from \"./head\";\nimport { Scripts } from \"./scripts\";\n\nexport type DefaultAppProps = {\n children: ReactNode;\n};\n\n/**\n * The framework's own root, used whenever an app defines no `root.tsx` (or\n * omits a default export). The framework's default App is ALSO a full\n * document — so every page renders a complete `<html>` regardless of\n * whether the app customized its root. The app owns the document: when a\n * custom `root.tsx` exists (`v5/app/src/web/root.tsx` is that example), it is\n * fully responsible for `lang`/`dir`/`<Scripts nonce>`/providers/its own\n * `<head>` tags. Only when no custom App exists does this fallback supply\n * `lang`/`dir`/nonce itself, sourced from the framework's document-context\n * slots rather than any app-owned data.\n */\nexport default function DefaultApp({ children }: DefaultAppProps): ReactElement {\n const { lang, dir } = useDocumentContext(\"DefaultApp\");\n\n return (\n <html lang={lang} dir={dir}>\n <head>\n <Head />\n </head>\n <body>\n {/*\n The hydration mount point (documented previously as\n `render-page.ts`'s own hardcoded shell, now moved here since the\n root supplies the whole document itself). An app writing its own\n custom `root.tsx` is responsible for its own equivalent if it wants\n a stable hydration target — this default only covers the\n no-custom-root case.\n */}\n <div id=\"root\">{children}</div>\n {/* Prop-less: inherits the nonce slot via Scripts' own fallback\n (scripts.ts) rather than reading document-context twice here. */}\n <Scripts />\n </body>\n </html>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,SAAwB,WAAW,EAAE,YAA2C;CAC9E,MAAM,EAAE,MAAM,QAAQ,mBAAmB,YAAY;CAErD,OACE,qBAAC,QAAD;EAAY;EAAW;YAAvB,CACE,oBAAC,QAAD,YACE,oBAAC,MAAD,CAAO,GACH,IACN,qBAAC,QAAD,aASE,oBAAC,OAAD;GAAK,IAAG;GAAQ;EAAc,IAG9B,oBAAC,SAAD,CAAU,EACN,IACF;;AAEV"}
@@ -1,7 +1,45 @@
1
- //#region ../web/src/components/document-context.d.ts
1
+ //#region web/src/components/document-context.d.ts
2
+ /**
3
+ * The JSON-safe error shape carried from the server document to browser
4
+ * hydration.
5
+ *
6
+ * This is deliberately NOT the original thrown object. Error prototypes,
7
+ * identity, non-enumerable fields and arbitrary custom values do not survive a
8
+ * JSON boundary reliably. The server renders `ErrorPageProps` with the
9
+ * original value, then normalizes it to this lossy representation only for the
10
+ * hydration payload. Normalization also owns disclosure: `stack` is optional
11
+ * and must be omitted or redacted when server internals are not safe to expose
12
+ * to the browser.
13
+ */
14
+ type SerializedPageError = {
15
+ readonly name: string;
16
+ readonly message: string;
17
+ readonly stack?: string;
18
+ };
19
+ /**
20
+ * Props received by the application-owned `error.page.tsx` during SSR.
21
+ *
22
+ * Deliberately preserve the thrown value here. An application can use its own
23
+ * error classes, symbols, or structured values while rendering on the server;
24
+ * this public component contract is not a JSON boundary.
25
+ */
26
+ type ErrorPageProps = {
27
+ readonly error: unknown;
28
+ readonly status: number;
29
+ };
30
+ /**
31
+ * The JSON-safe counterpart of {@link ErrorPageProps}, used only after the
32
+ * document crosses from SSR into browser hydration. Keeping this distinct
33
+ * prevents a serialized approximation from being mistaken for the original
34
+ * thrown value available to the server render.
35
+ */
36
+ type SerializedErrorPageProps = {
37
+ readonly error: SerializedPageError;
38
+ readonly status: number;
39
+ };
2
40
  declare const PAYLOAD_SCRIPT_ID = "__WARLOCK_DATA__";
3
41
  /** Escape JSON text for raw insertion into an application/json script. */
4
42
  declare function escapePayload(json: string): string;
5
43
  //#endregion
6
- export { PAYLOAD_SCRIPT_ID, escapePayload };
44
+ export { ErrorPageProps, PAYLOAD_SCRIPT_ID, SerializedErrorPageProps, SerializedPageError, escapePayload };
7
45
  //# sourceMappingURL=document-context.d.mts.map
@@ -1,6 +1,6 @@
1
1
  import { createContext, useContext } from "react";
2
2
 
3
- //#region ../web/src/components/document-context.ts
3
+ //#region web/src/components/document-context.ts
4
4
  const PAYLOAD_SCRIPT_ID = "__WARLOCK_DATA__";
5
5
  const LINE_SEPARATOR = String.fromCharCode(8232);
6
6
  const PARAGRAPH_SEPARATOR = String.fromCharCode(8233);
@@ -1 +1 @@
1
- {"version":3,"file":"document-context.mjs","names":[],"sources":["../../../../../../../web/src/components/document-context.ts"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { MetadataOutput } from \"../metadata\";\n\nexport type HydrationDocumentPayloadSource = {\n readonly appData: unknown;\n readonly layoutData: unknown;\n readonly pageData: unknown;\n readonly shared: unknown;\n /**\n * The matched page manifest entry's stable `name` — the same field the\n * manifest entry contract `{ type, name, path, load }` declares. It is on\n * the wire so the browser can look up WHICH page the server rendered\n * instead of re-matching `location.pathname` itself: re-matching is a\n * second implementation of route semantics, and it can disagree with the\n * server on the very request it is hydrating.\n */\n readonly name: string;\n /**\n * The params the SERVER matched for this request — `bundle.route.params`\n * (`server/execute-page-request.ts:288`), carried untransformed. Same reason\n * `name` is here: the browser must not re-derive them from\n * `location.pathname`, because deriving them IS a second matcher.\n *\n * OPTIONAL, and ungated on purpose — see {@link metadata} below for the rule\n * both new keys share. The server always emits it (`{}` for a route with no\n * dynamic segments), so absence means the payload came from a producer that\n * predates this key; `currentRoute()` then reports `{}` rather than failing a\n * page over an accessor.\n */\n readonly params?: Readonly<Record<string, string>>;\n /**\n * The page metadata the server resolved at stage 8, carried WHOLE — the same\n * `MetadataOutput` `<Head/>` rendered into the document on the first request.\n *\n * Why it has to be on the wire at all: `<Head/>` renders inside the App\n * level, and the App level is not part of the hydrated tree (the client\n * mounts at `#root`, which App contains). So on a client navigation there is\n * no React render that can reach `<head>` — without this key the browser\n * never learns the new page's title and the tab keeps the old one.\n *\n * OPTIONAL, deliberately: `bundle.metadata` is itself optional\n * (`server/execute-page-request.ts:296`) — a page that exports no `metadata`\n * produces none, and a loader short-circuit skips stage 8 entirely. Gating a\n * key the server is right not to produce would make `readHydrationPayload`\n * throw on a valid page. Present-but-not-an-object is still MALFORMED and\n * still throws; only ABSENT is accepted.\n */\n readonly metadata?: MetadataOutput;\n};\n\nexport const PAYLOAD_SCRIPT_ID = \"__WARLOCK_DATA__\";\n\nconst LINE_SEPARATOR = String.fromCharCode(0x2028);\nconst PARAGRAPH_SEPARATOR = String.fromCharCode(0x2029);\n\n/** Escape JSON text for raw insertion into an application/json script. */\nexport function escapePayload(json: string): string {\n return json\n .split(\"<\")\n .join(\"\\\\u003c\")\n .split(\">\")\n .join(\"\\\\u003e\")\n .split(LINE_SEPARATOR)\n .join(\"\\\\u2028\")\n .split(PARAGRAPH_SEPARATOR)\n .join(\"\\\\u2029\");\n}\n\n/**\n * What `<Head/>`/`<Scripts/>` need to render real elements instead of the\n * framework injecting them by string surgery post-render (Suki, room seq\n * 1205): the resolved page metadata and the exact payload the hydration\n * script will read back. Provided once, around the root element, before\n * `renderToString` runs (`render-page.ts`'s stage 9 — the bundle is already\n * complete by then). Universal: no server-only imports, so the client's\n * hydration entry (a later slice) can provide the same shape from the parsed\n * payload script.\n */\nexport type DocumentContextValue = {\n metadata: MetadataOutput | undefined;\n payload: HydrationDocumentPayloadSource;\n /**\n * The nonce/lang/dir SLOTS: fed by the render provider from CORE request\n * fields — request nonce, request locale — never from app-owned `shared`\n * keys, which an app can overwrite. The provider-side\n * wiring is a separate slice, so these are absent at runtime until it\n * lands; every reader must treat them as optional.\n */\n nonce?: string;\n lang?: string;\n dir?: string;\n};\n\nexport const DocumentContext = createContext<DocumentContextValue | undefined>(undefined);\n\n/**\n * Require the page pipeline's universal document state. The payload id and\n * escaping helpers remain exported above for the existing server seam.\n */\nexport function useDocumentContext(componentName: string): DocumentContextValue {\n const value = useContext(DocumentContext);\n\n if (!value) {\n throw new Error(\n `<${componentName}/> was rendered outside the page pipeline's document context ` +\n \"(web/src/components/document-context.ts). Fix: only render it inside \" +\n \"an App/Layout/Page component tree the pipeline itself renders.\",\n );\n }\n\n return value;\n}\n"],"mappings":";;;AAkDA,MAAa,oBAAoB;AAEjC,MAAM,iBAAiB,OAAO,aAAa,IAAM;AACjD,MAAM,sBAAsB,OAAO,aAAa,IAAM;;AAGtD,SAAgB,cAAc,MAAsB;CAClD,OAAO,KACJ,MAAM,GAAG,CAAC,CACV,KAAK,SAAS,CAAC,CACf,MAAM,GAAG,CAAC,CACV,KAAK,SAAS,CAAC,CACf,MAAM,cAAc,CAAC,CACrB,KAAK,SAAS,CAAC,CACf,MAAM,mBAAmB,CAAC,CAC1B,KAAK,SAAS;AACnB;AA2BA,MAAa,kBAAkB,cAAgD,MAAS;;;;;AAMxF,SAAgB,mBAAmB,eAA6C;CAC9E,MAAM,QAAQ,WAAW,eAAe;CAExC,IAAI,CAAC,OACH,MAAM,IAAI,MACR,IAAI,cAAc,iMAGpB;CAGF,OAAO;AACT"}
1
+ {"version":3,"file":"document-context.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/components/document-context.ts"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { MetadataOutput } from \"../metadata\";\n\n/**\n * The JSON-safe error shape carried from the server document to browser\n * hydration.\n *\n * This is deliberately NOT the original thrown object. Error prototypes,\n * identity, non-enumerable fields and arbitrary custom values do not survive a\n * JSON boundary reliably. The server renders `ErrorPageProps` with the\n * original value, then normalizes it to this lossy representation only for the\n * hydration payload. Normalization also owns disclosure: `stack` is optional\n * and must be omitted or redacted when server internals are not safe to expose\n * to the browser.\n */\nexport type SerializedPageError = {\n readonly name: string;\n readonly message: string;\n readonly stack?: string;\n};\n\n/**\n * Props received by the application-owned `error.page.tsx` during SSR.\n *\n * Deliberately preserve the thrown value here. An application can use its own\n * error classes, symbols, or structured values while rendering on the server;\n * this public component contract is not a JSON boundary.\n */\nexport type ErrorPageProps = {\n readonly error: unknown;\n readonly status: number;\n};\n\n/**\n * The JSON-safe counterpart of {@link ErrorPageProps}, used only after the\n * document crosses from SSR into browser hydration. Keeping this distinct\n * prevents a serialized approximation from being mistaken for the original\n * thrown value available to the server render.\n */\nexport type SerializedErrorPageProps = {\n readonly error: SerializedPageError;\n readonly status: number;\n};\n\nexport type HydrationDocumentPayloadSource = {\n readonly appData: unknown;\n readonly layoutData: unknown;\n readonly pageData: unknown;\n readonly shared: unknown;\n /**\n * The matched page manifest entry's stable `name` — the same field the\n * manifest entry contract `{ type, name, path, load }` declares. It is on\n * the wire so the browser can look up WHICH page the server rendered\n * instead of re-matching `location.pathname` itself: re-matching is a\n * second implementation of route semantics, and it can disagree with the\n * server on the very request it is hydrating.\n */\n readonly name: string;\n /**\n * The params the SERVER matched for this request — `bundle.route.params`\n * (`server/execute-page-request.ts:288`), carried untransformed. Same reason\n * `name` is here: the browser must not re-derive them from\n * `location.pathname`, because deriving them IS a second matcher.\n *\n * OPTIONAL, and ungated on purpose — see {@link metadata} below for the rule\n * both new keys share. The server always emits it (`{}` for a route with no\n * dynamic segments), so absence means the payload came from a producer that\n * predates this key; `currentRoute()` then reports `{}` rather than failing a\n * page over an accessor.\n */\n readonly params?: Readonly<Record<string, string>>;\n /**\n * The page metadata the server resolved at stage 8, carried WHOLE — the same\n * `MetadataOutput` `<Head/>` rendered into the document on the first request.\n *\n * Why it has to be on the wire at all: `<Head/>` renders inside the App\n * level, and the App level is not part of the hydrated tree (the client\n * mounts at `#root`, which App contains). So on a client navigation there is\n * no React render that can reach `<head>` — without this key the browser\n * never learns the new page's title and the tab keeps the old one.\n *\n * OPTIONAL, deliberately: `bundle.metadata` is itself optional\n * (`server/execute-page-request.ts:296`) — a page that exports no `metadata`\n * produces none, and a loader short-circuit skips stage 8 entirely. Gating a\n * key the server is right not to produce would make `readHydrationPayload`\n * throw on a valid page. Present-but-not-an-object is still MALFORMED and\n * still throws; only ABSENT is accepted.\n */\n readonly metadata?: MetadataOutput;\n /**\n * Present only when the server selected the application-owned error page for\n * this response. Atomic rather than two independently optional top-level\n * fields: a status without an error (or the reverse) cannot describe a tree\n * the browser can hydrate.\n *\n * `name` above intentionally remains the ORIGINAL matched route. This field\n * selects the `ErrorPage` module projected into that route's client\n * composition; it does not turn the error page into a second browsable route.\n */\n readonly errorPage?: SerializedErrorPageProps;\n};\n\nexport const PAYLOAD_SCRIPT_ID = \"__WARLOCK_DATA__\";\n\nconst LINE_SEPARATOR = String.fromCharCode(0x2028);\nconst PARAGRAPH_SEPARATOR = String.fromCharCode(0x2029);\n\n/** Escape JSON text for raw insertion into an application/json script. */\nexport function escapePayload(json: string): string {\n return json\n .split(\"<\")\n .join(\"\\\\u003c\")\n .split(\">\")\n .join(\"\\\\u003e\")\n .split(LINE_SEPARATOR)\n .join(\"\\\\u2028\")\n .split(PARAGRAPH_SEPARATOR)\n .join(\"\\\\u2029\");\n}\n\n/**\n * What `<Head/>`/`<Scripts/>` need to render real elements instead of the\n * framework injecting them by string surgery post-render (Suki, room seq\n * 1205): the resolved page metadata and the exact payload the hydration\n * script will read back. Provided once, around the root element, before\n * `renderToString` runs (`render-page.ts`'s stage 9 — the bundle is already\n * complete by then). Universal: no server-only imports, so the client's\n * hydration entry (a later slice) can provide the same shape from the parsed\n * payload script.\n */\nexport type DocumentContextValue = {\n metadata: MetadataOutput | undefined;\n payload: HydrationDocumentPayloadSource;\n /**\n * The nonce/lang/dir SLOTS: fed by the render provider from CORE request\n * fields — request nonce, request locale — never from app-owned `shared`\n * keys, which an app can overwrite. The provider-side\n * wiring is a separate slice, so these are absent at runtime until it\n * lands; every reader must treat them as optional.\n */\n nonce?: string;\n lang?: string;\n dir?: string;\n};\n\nexport const DocumentContext = createContext<DocumentContextValue | undefined>(undefined);\n\n/**\n * Require the page pipeline's universal document state. The payload id and\n * escaping helpers remain exported above for the existing server seam.\n */\nexport function useDocumentContext(componentName: string): DocumentContextValue {\n const value = useContext(DocumentContext);\n\n if (!value) {\n throw new Error(\n `<${componentName}/> was rendered outside the page pipeline's document context ` +\n \"(web/src/components/document-context.ts). Fix: only render it inside \" +\n \"an App/Layout/Page component tree the pipeline itself renders.\",\n );\n }\n\n return value;\n}\n"],"mappings":";;;AAsGA,MAAa,oBAAoB;AAEjC,MAAM,iBAAiB,OAAO,aAAa,IAAM;AACjD,MAAM,sBAAsB,OAAO,aAAa,IAAM;;AAGtD,SAAgB,cAAc,MAAsB;CAClD,OAAO,KACJ,MAAM,GAAG,EACT,KAAK,SAAS,EACd,MAAM,GAAG,EACT,KAAK,SAAS,EACd,MAAM,cAAc,EACpB,KAAK,SAAS,EACd,MAAM,mBAAmB,EACzB,KAAK,SAAS;AACnB;AA2BA,MAAa,kBAAkB,cAAgD,MAAS;;;;;AAMxF,SAAgB,mBAAmB,eAA6C;CAC9E,MAAM,QAAQ,WAAW,eAAe;CAExC,IAAI,CAAC,OACH,MAAM,IAAI,MACR,IAAI,cAAc,iMAGpB;CAGF,OAAO;AACT"}
@@ -1,6 +1,6 @@
1
1
  import { ReactElement } from "react";
2
2
 
3
- //#region ../web/src/components/head.d.ts
3
+ //#region web/src/components/head.d.ts
4
4
  /**
5
5
  * OPTIONAL placement override for the framework's `<head>` injection — the
6
6
  * page's `metadata` output, the stylesheet/preload tags and the
@@ -1,7 +1,7 @@
1
1
  import { useDocumentContext } from "./document-context.mjs";
2
2
  import { Fragment, createElement } from "react";
3
3
 
4
- //#region ../web/src/components/head.ts
4
+ //#region web/src/components/head.ts
5
5
  /**
6
6
  * OPTIONAL placement override for the framework's `<head>` injection — the
7
7
  * page's `metadata` output, the stylesheet/preload tags and the
@@ -1 +1 @@
1
- {"version":3,"file":"head.mjs","names":[],"sources":["../../../../../../../web/src/components/head.ts"],"sourcesContent":["import { createElement, Fragment, type ReactElement } from \"react\";\nimport { useDocumentContext } from \"./document-context\";\n\n/**\n * OPTIONAL placement override for the framework's `<head>` injection — the\n * page's `metadata` output, the stylesheet/preload tags and the\n * canonical/alternate links land here when it is present, and at the default\n * position when it is not (root.tsx:85-92). Takes no props: it says WHERE, and\n * only that.\n */\nexport function Head(): ReactElement {\n const { metadata } = useDocumentContext(\"Head\");\n\n const keywords =\n metadata?.keywords === undefined\n ? undefined\n : Array.isArray(metadata.keywords)\n ? metadata.keywords.join(\", \")\n : metadata.keywords;\n\n const openGraph = metadata?.openGraph;\n // Falls back to the top-level fields — the only two fallbacks the type declares.\n const ogTitle = openGraph?.title ?? metadata?.title;\n const ogDescription = openGraph?.description ?? metadata?.description;\n\n const twitter = metadata?.twitter;\n\n return createElement(\n Fragment,\n null,\n createElement(\"meta\", { charSet: \"utf-8\" }),\n metadata?.title !== undefined ? createElement(\"title\", null, metadata.title) : null,\n metadata?.description !== undefined\n ? createElement(\"meta\", { name: \"description\", content: metadata.description })\n : null,\n keywords !== undefined ? createElement(\"meta\", { name: \"keywords\", content: keywords }) : null,\n metadata?.canonical !== undefined\n ? createElement(\"link\", { rel: \"canonical\", href: metadata.canonical })\n : null,\n metadata?.robots !== undefined\n ? createElement(\"meta\", { name: \"robots\", content: metadata.robots })\n : null,\n openGraph !== undefined && ogTitle !== undefined\n ? createElement(\"meta\", { property: \"og:title\", content: ogTitle })\n : null,\n openGraph !== undefined && ogDescription !== undefined\n ? createElement(\"meta\", { property: \"og:description\", content: ogDescription })\n : null,\n openGraph?.image !== undefined\n ? createElement(\"meta\", { property: \"og:image\", content: openGraph.image })\n : null,\n openGraph?.url !== undefined\n ? createElement(\"meta\", { property: \"og:url\", content: openGraph.url })\n : null,\n openGraph?.type !== undefined\n ? createElement(\"meta\", { property: \"og:type\", content: openGraph.type })\n : null,\n twitter?.card !== undefined\n ? createElement(\"meta\", { name: \"twitter:card\", content: twitter.card })\n : null,\n twitter?.title !== undefined\n ? createElement(\"meta\", { name: \"twitter:title\", content: twitter.title })\n : null,\n twitter?.description !== undefined\n ? createElement(\"meta\", { name: \"twitter:description\", content: twitter.description })\n : null,\n twitter?.image !== undefined\n ? createElement(\"meta\", { name: \"twitter:image\", content: twitter.image })\n : null,\n );\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,OAAqB;CACnC,MAAM,EAAE,aAAa,mBAAmB,MAAM;CAE9C,MAAM,WACJ,UAAU,aAAa,SACnB,SACA,MAAM,QAAQ,SAAS,QAAQ,IAC7B,SAAS,SAAS,KAAK,IAAI,IAC3B,SAAS;CAEjB,MAAM,YAAY,UAAU;CAE5B,MAAM,UAAU,WAAW,SAAS,UAAU;CAC9C,MAAM,gBAAgB,WAAW,eAAe,UAAU;CAE1D,MAAM,UAAU,UAAU;CAE1B,OAAO,cACL,UACA,MACA,cAAc,QAAQ,EAAE,SAAS,QAAQ,CAAC,GAC1C,UAAU,UAAU,SAAY,cAAc,SAAS,MAAM,SAAS,KAAK,IAAI,MAC/E,UAAU,gBAAgB,SACtB,cAAc,QAAQ;EAAE,MAAM;EAAe,SAAS,SAAS;CAAY,CAAC,IAC5E,MACJ,aAAa,SAAY,cAAc,QAAQ;EAAE,MAAM;EAAY,SAAS;CAAS,CAAC,IAAI,MAC1F,UAAU,cAAc,SACpB,cAAc,QAAQ;EAAE,KAAK;EAAa,MAAM,SAAS;CAAU,CAAC,IACpE,MACJ,UAAU,WAAW,SACjB,cAAc,QAAQ;EAAE,MAAM;EAAU,SAAS,SAAS;CAAO,CAAC,IAClE,MACJ,cAAc,UAAa,YAAY,SACnC,cAAc,QAAQ;EAAE,UAAU;EAAY,SAAS;CAAQ,CAAC,IAChE,MACJ,cAAc,UAAa,kBAAkB,SACzC,cAAc,QAAQ;EAAE,UAAU;EAAkB,SAAS;CAAc,CAAC,IAC5E,MACJ,WAAW,UAAU,SACjB,cAAc,QAAQ;EAAE,UAAU;EAAY,SAAS,UAAU;CAAM,CAAC,IACxE,MACJ,WAAW,QAAQ,SACf,cAAc,QAAQ;EAAE,UAAU;EAAU,SAAS,UAAU;CAAI,CAAC,IACpE,MACJ,WAAW,SAAS,SAChB,cAAc,QAAQ;EAAE,UAAU;EAAW,SAAS,UAAU;CAAK,CAAC,IACtE,MACJ,SAAS,SAAS,SACd,cAAc,QAAQ;EAAE,MAAM;EAAgB,SAAS,QAAQ;CAAK,CAAC,IACrE,MACJ,SAAS,UAAU,SACf,cAAc,QAAQ;EAAE,MAAM;EAAiB,SAAS,QAAQ;CAAM,CAAC,IACvE,MACJ,SAAS,gBAAgB,SACrB,cAAc,QAAQ;EAAE,MAAM;EAAuB,SAAS,QAAQ;CAAY,CAAC,IACnF,MACJ,SAAS,UAAU,SACf,cAAc,QAAQ;EAAE,MAAM;EAAiB,SAAS,QAAQ;CAAM,CAAC,IACvE,IACN;AACF"}
1
+ {"version":3,"file":"head.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/components/head.ts"],"sourcesContent":["import { createElement, Fragment, type ReactElement } from \"react\";\nimport { useDocumentContext } from \"./document-context\";\n\n/**\n * OPTIONAL placement override for the framework's `<head>` injection — the\n * page's `metadata` output, the stylesheet/preload tags and the\n * canonical/alternate links land here when it is present, and at the default\n * position when it is not (root.tsx:85-92). Takes no props: it says WHERE, and\n * only that.\n */\nexport function Head(): ReactElement {\n const { metadata } = useDocumentContext(\"Head\");\n\n const keywords =\n metadata?.keywords === undefined\n ? undefined\n : Array.isArray(metadata.keywords)\n ? metadata.keywords.join(\", \")\n : metadata.keywords;\n\n const openGraph = metadata?.openGraph;\n // Falls back to the top-level fields — the only two fallbacks the type declares.\n const ogTitle = openGraph?.title ?? metadata?.title;\n const ogDescription = openGraph?.description ?? metadata?.description;\n\n const twitter = metadata?.twitter;\n\n return createElement(\n Fragment,\n null,\n createElement(\"meta\", { charSet: \"utf-8\" }),\n metadata?.title !== undefined ? createElement(\"title\", null, metadata.title) : null,\n metadata?.description !== undefined\n ? createElement(\"meta\", { name: \"description\", content: metadata.description })\n : null,\n keywords !== undefined ? createElement(\"meta\", { name: \"keywords\", content: keywords }) : null,\n metadata?.canonical !== undefined\n ? createElement(\"link\", { rel: \"canonical\", href: metadata.canonical })\n : null,\n metadata?.robots !== undefined\n ? createElement(\"meta\", { name: \"robots\", content: metadata.robots })\n : null,\n openGraph !== undefined && ogTitle !== undefined\n ? createElement(\"meta\", { property: \"og:title\", content: ogTitle })\n : null,\n openGraph !== undefined && ogDescription !== undefined\n ? createElement(\"meta\", { property: \"og:description\", content: ogDescription })\n : null,\n openGraph?.image !== undefined\n ? createElement(\"meta\", { property: \"og:image\", content: openGraph.image })\n : null,\n openGraph?.url !== undefined\n ? createElement(\"meta\", { property: \"og:url\", content: openGraph.url })\n : null,\n openGraph?.type !== undefined\n ? createElement(\"meta\", { property: \"og:type\", content: openGraph.type })\n : null,\n twitter?.card !== undefined\n ? createElement(\"meta\", { name: \"twitter:card\", content: twitter.card })\n : null,\n twitter?.title !== undefined\n ? createElement(\"meta\", { name: \"twitter:title\", content: twitter.title })\n : null,\n twitter?.description !== undefined\n ? createElement(\"meta\", { name: \"twitter:description\", content: twitter.description })\n : null,\n twitter?.image !== undefined\n ? createElement(\"meta\", { name: \"twitter:image\", content: twitter.image })\n : null,\n );\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,OAAqB;CACnC,MAAM,EAAE,aAAa,mBAAmB,MAAM;CAE9C,MAAM,WACJ,UAAU,aAAa,SACnB,SACA,MAAM,QAAQ,SAAS,QAAQ,IAC7B,SAAS,SAAS,KAAK,IAAI,IAC3B,SAAS;CAEjB,MAAM,YAAY,UAAU;CAE5B,MAAM,UAAU,WAAW,SAAS,UAAU;CAC9C,MAAM,gBAAgB,WAAW,eAAe,UAAU;CAE1D,MAAM,UAAU,UAAU;CAE1B,OAAO,cACL,UACA,MACA,cAAc,QAAQ,EAAE,SAAS,QAAQ,CAAC,GAC1C,UAAU,UAAU,SAAY,cAAc,SAAS,MAAM,SAAS,KAAK,IAAI,MAC/E,UAAU,gBAAgB,SACtB,cAAc,QAAQ;EAAE,MAAM;EAAe,SAAS,SAAS;CAAY,CAAC,IAC5E,MACJ,aAAa,SAAY,cAAc,QAAQ;EAAE,MAAM;EAAY,SAAS;CAAS,CAAC,IAAI,MAC1F,UAAU,cAAc,SACpB,cAAc,QAAQ;EAAE,KAAK;EAAa,MAAM,SAAS;CAAU,CAAC,IACpE,MACJ,UAAU,WAAW,SACjB,cAAc,QAAQ;EAAE,MAAM;EAAU,SAAS,SAAS;CAAO,CAAC,IAClE,MACJ,cAAc,UAAa,YAAY,SACnC,cAAc,QAAQ;EAAE,UAAU;EAAY,SAAS;CAAQ,CAAC,IAChE,MACJ,cAAc,UAAa,kBAAkB,SACzC,cAAc,QAAQ;EAAE,UAAU;EAAkB,SAAS;CAAc,CAAC,IAC5E,MACJ,WAAW,UAAU,SACjB,cAAc,QAAQ;EAAE,UAAU;EAAY,SAAS,UAAU;CAAM,CAAC,IACxE,MACJ,WAAW,QAAQ,SACf,cAAc,QAAQ;EAAE,UAAU;EAAU,SAAS,UAAU;CAAI,CAAC,IACpE,MACJ,WAAW,SAAS,SAChB,cAAc,QAAQ;EAAE,UAAU;EAAW,SAAS,UAAU;CAAK,CAAC,IACtE,MACJ,SAAS,SAAS,SACd,cAAc,QAAQ;EAAE,MAAM;EAAgB,SAAS,QAAQ;CAAK,CAAC,IACrE,MACJ,SAAS,UAAU,SACf,cAAc,QAAQ;EAAE,MAAM;EAAiB,SAAS,QAAQ;CAAM,CAAC,IACvE,MACJ,SAAS,gBAAgB,SACrB,cAAc,QAAQ;EAAE,MAAM;EAAuB,SAAS,QAAQ;CAAY,CAAC,IACnF,MACJ,SAAS,UAAU,SACf,cAAc,QAAQ;EAAE,MAAM;EAAiB,SAAS,QAAQ;CAAM,CAAC,IACvE,IACN;AACF"}
@@ -1,6 +1,6 @@
1
1
  import { AnchorHTMLAttributes, ComponentType, ReactElement } from "react";
2
2
 
3
- //#region ../web/src/components/link.d.ts
3
+ //#region web/src/components/link.d.ts
4
4
  /**
5
5
  * `<Link>` is SUGAR over `href()`, and deliberately thin.
6
6
  *
@@ -3,7 +3,7 @@ import { prefetchPageData } from "../client/navigation/prefetch.mjs";
3
3
  import { currentNavigator } from "../routing/navigator.mjs";
4
4
  import { createElement } from "react";
5
5
 
6
- //#region ../web/src/components/link.ts
6
+ //#region web/src/components/link.ts
7
7
  const DESTINATION_PROPS = [
8
8
  "to",
9
9
  "href",
@@ -1 +1 @@
1
- {"version":3,"file":"link.mjs","names":[],"sources":["../../../../../../../web/src/components/link.ts"],"sourcesContent":["import { createElement } from \"react\";\nimport type {\n AnchorHTMLAttributes,\n ComponentType,\n FocusEvent,\n MouseEvent,\n ReactElement,\n} from \"react\";\nimport { prefetchPageData } from \"../client/navigation/prefetch\";\nimport { currentNavigator } from \"../routing/navigator\";\nimport { href, knownRouteNames } from \"../routing/route-table\";\n\n/**\n * `<Link>` is SUGAR over `href()`, and deliberately thin.\n *\n * `href(name, params, query)` is the durable primitive — it serves emails,\n * redirects, `Location` headers and every non-React caller, none of which can\n * render a component. This file adds one thing to it: an anchor element.\n *\n * It renders a real `<a href>`. Client-side interception is a later slice of\n * the navigation runtime and lands here without changing this API, which is the\n * point of routing everything through `href` first: navigation becomes a\n * BEHAVIOUR change, not an API change.\n *\n * ── Parity with `@mongez/react-router` ───────────────────────────────────────\n * `href`, `newTab`, `email`, `tel`, `component` and `prefetch` are spelled\n * exactly as MRR spells them, so a component moved across keeps compiling.\n * `params` and `query` are ours and have no MRR equivalent: they pair with the\n * typed `href()` helper, which is what makes a route NAME — rather than a URL —\n * the thing a call site names.\n *\n * ── The semantic divergence this file bridges ────────────────────────────────\n * MRR's `to` is a PATH. Ours was a route NAME, and only a name — which meant a\n * component moved across from MRR compiled and then threw at render, because\n * `\"/products\"` is not the name of anything. The two packages disagreed about\n * what the most-used prop in either of them MEANS.\n *\n * Since 2026-08-24 (owner ruling) `to`/`href` accept BOTH, discriminated by\n * SHAPE — see {@link isLiteralUrl}. That is what makes MRR code portable, and\n * it costs nothing at a Warlock call site, because the two grammars cannot\n * collide: a route name never begins with `/` and never carries a `scheme:`.\n * The ruling RESTS on that, so this file asserts it rather than trusting it\n * ({@link RouteNameShapeCollisionError}).\n */\n\ntype AnchorProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\">;\n\n/**\n * Where the link goes. Every field is documented here once; which COMBINATIONS\n * are legal is decided by {@link LinkDestination}.\n */\ntype LinkDestinationProps = {\n /**\n * A route NAME, or a literal URL — told apart by SHAPE.\n *\n * `\"products.details\"` is a NAME and is resolved through the route table. A\n * page that moves changes its URL and keeps its name, so every call site\n * survives the move; a dead name throws at render naming the routes that do\n * exist, rather than rendering an anchor that 404s. This is the form to\n * prefer, and the only one `params` and `query` apply to.\n *\n * `\"/pricing\"`, `\"https://stripe.com\"`, `\"mailto:sales@example.com\"` and any\n * other `scheme:` are LITERAL — passed through to the element untouched, with\n * no route lookup at all. An app links out, and a route name is not a thing\n * you can have for a page that is not yours.\n */\n to?: string;\n /**\n * An alias of {@link to}, for parity with `@mongez/react-router`. Identical\n * in every respect, including which shapes it accepts.\n */\n href?: string;\n /** Renders a `mailto:` link. Not an in-app navigation. */\n email?: string;\n /** Renders a `tel:` link. Not an in-app navigation. */\n tel?: string;\n /**\n * Values for the route's `:param` segments, e.g. `{ id }` for\n * `\"/products/:id\"`. Only meaningful with a route NAME.\n */\n params?: Record<string, unknown>;\n /**\n * Query string values; an `undefined` value is omitted. Only meaningful with\n * a route NAME.\n */\n query?: Record<string, unknown>;\n};\n\n/**\n * EXACTLY ONE destination, enforced by the type.\n *\n * The alternative — a documented precedence such as \"`to` wins over `href`\" —\n * is silent by construction: the losing prop goes on compiling and goes on\n * reading like it works at the call site, and the anchor points at the wrong\n * page. Refusing the pair costs a call site one edit and can never be\n * misread. The runtime refuses it as well, because a JavaScript caller and a\n * cast both get past this.\n */\ntype LinkDestination =\n | { to: string; href?: never; email?: never; tel?: never }\n | { href: string; to?: never; email?: never; tel?: never }\n | { email: string; to?: never; href?: never; tel?: never }\n | { tel: string; to?: never; href?: never; email?: never };\n\nexport type LinkProps = AnchorProps &\n LinkDestinationProps &\n LinkDestination & {\n /**\n * Open in a new browsing context: `target=\"_blank\"` plus the `rel` that\n * stops the opened page from reaching back through `window.opener`.\n *\n * A caller's own `target`/`rel` win — this only fills in what was not said.\n */\n newTab?: boolean;\n /**\n * Render as something other than `<a>` — a tag name or a component.\n *\n * It receives the resolved `href`, the click handler and every remaining\n * prop, so a design-system anchor keeps client-side navigation as long as\n * it spreads what it is given onto the element it renders.\n *\n * `ComponentType<any>` is MRR's signature, kept verbatim: the component is\n * the caller's and its props are unknowable from here.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n component?: ComponentType<any> | string;\n /**\n * Fetch this page's data when the pointer or the keyboard reaches the link,\n * so the click that follows swaps without a round trip.\n *\n * A GUESS, and treated as one everywhere: it is never awaited, a failure is\n * silent, and the click behaves exactly as it would without it. Opt-in per\n * link rather than on by default, because every prefetch is a request the\n * user did not ask for and someone pays for the bandwidth.\n *\n * IGNORED for anything that is not an in-app navigation — an external URL,\n * `mailto:`, `tel:`, `newTab`, any explicit `target`. Prefetching those\n * would mean issuing a cross-origin request to a third party on hover,\n * which is not a thing a link component may decide to do.\n */\n prefetch?: boolean;\n };\n\nconst DESTINATION_PROPS = [\"to\", \"href\", \"email\", \"tel\"] as const;\n\nexport class AmbiguousLinkDestinationError extends Error {\n public constructor(public readonly providedProps: readonly string[]) {\n super(\n `Warlock <Link> was given ${providedProps\n .map(name => JSON.stringify(name))\n .join(\" and \")}, but a link goes to exactly one place. There is no ` +\n \"precedence between them on purpose: one of the two would silently win, and the \" +\n \"call site would go on naming a destination that never renders. Delete the one \" +\n \"you did not mean.\",\n );\n this.name = \"AmbiguousLinkDestinationError\";\n }\n}\n\nexport class MissingLinkDestinationError extends Error {\n public constructor() {\n super(\n `Warlock <Link> was given no destination. Pass exactly one of ${DESTINATION_PROPS.map(\n name => JSON.stringify(name),\n ).join(\", \")}. It is not defaulted to the current page: an anchor with an empty ` +\n \"`href` renders as a working link and reloads the page when clicked, which is a \" +\n \"harder fault to see than this message.\",\n );\n this.name = \"MissingLinkDestinationError\";\n }\n}\n\nexport class RouteArgumentsOnLiteralUrlError extends Error {\n public constructor(\n public readonly url: string,\n public readonly providedProps: readonly string[],\n ) {\n super(\n `Warlock <Link> was given ${providedProps\n .map(name => JSON.stringify(name))\n .join(\" and \")} alongside the literal URL \"${url}\". Those apply to a route NAME, ` +\n \"which is resolved through the route table; a literal URL is passed through exactly \" +\n \"as written, so they would have been dropped and the link would have pointed at an \" +\n \"unfiltered page that still looked right at the call site. Put the values in the URL, \" +\n \"or name the route.\",\n );\n this.name = \"RouteArgumentsOnLiteralUrlError\";\n }\n}\n\n/**\n * The ruling's one assumption, broken. See the module doc comment: telling a\n * literal URL from a route NAME by shape is only safe while no route is NAMED\n * like a URL, and nothing in the route pipeline validates a hand-declared\n * `route.name`. So the collision is checked at the one place it could do harm,\n * where it is a loud refusal instead of an anchor that silently points\n * somewhere else.\n */\nexport class RouteNameShapeCollisionError extends Error {\n public constructor(public readonly routeName: string) {\n super(\n `Warlock route table: a route is NAMED ${JSON.stringify(routeName)}, which is shaped ` +\n \"like a URL. <Link> tells a literal URL from a route name by shape — a destination \" +\n \"starting with `/` or carrying a `scheme:` is passed through untouched — so this name \" +\n \"can never be resolved, and every link to it would silently point at that path \" +\n \"instead. Rename the route (`route = { path, name }`) to a dotted name such as \" +\n `${JSON.stringify(routeName.replace(/^\\/+/, \"\").replace(/\\//g, \".\") || \"index\")}.`,\n );\n this.name = \"RouteNameShapeCollisionError\";\n }\n}\n\ntype Destination = {\n /** What lands on the element's `href`. */\n url: string;\n /**\n * Whether this URL is a page in THIS app — the only kind the client\n * navigation runtime may be asked about, and the only kind that may be\n * prefetched. `mailto:`, `tel:` and an external URL hand off to another\n * application or another origin entirely, so intercepting any of them would\n * break it, and speculatively fetching one would be a cross-origin request\n * the developer never asked for.\n */\n isInApp: boolean;\n};\n\n/**\n * Any RFC 3986 scheme — `https:`, `mailto:`, `tel:`, `whatsapp:`, an app's own\n * custom one. Matched generically rather than as a list of known schemes: a\n * list would silently resolve `bitcoin:...` through the route table, which is\n * the exact failure this ruling exists to remove, and it would have to grow\n * forever.\n */\nconst SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;\n\n/**\n * Whether this destination is a URL to be used as written, rather than a route\n * name to resolve.\n *\n * The whole discriminator, and deliberately the whole of it: two cheap shape\n * tests, no parsing, no matching. Anything more would be a SECOND route matcher\n * living beside the server's, which this codebase refuses everywhere it comes\n * up — a matcher that disagreed with the real one would produce links to pages\n * that do not exist.\n */\nfunction isLiteralUrl(destination: string): boolean {\n return destination.startsWith(\"/\") || SCHEME_PATTERN.test(destination);\n}\n\n/**\n * Whether a literal URL addresses THIS app.\n *\n * A path is ours. A `scheme:` is not — including `https:` to our own origin,\n * which would need `window.location` to recognise and would make the answer\n * depend on where the code is running. And `//host/path` is PROTOCOL-RELATIVE:\n * it starts with a slash and is nonetheless another origin, which is precisely\n * the case a \"starts with `/`\" test alone would hand to the navigator, where it\n * becomes a `pushState` to a foreign origin — a SecurityError — or a\n * speculative fetch of a third-party host.\n */\nfunction addressesThisApp(url: string): boolean {\n return url.startsWith(\"/\") && !url.startsWith(\"//\");\n}\n\n/**\n * Refuses the one table that would make {@link isLiteralUrl} wrong.\n *\n * Reached only for a destination already judged literal, so the cost is a scan\n * of the published names for links that were never going to hit the table\n * anyway — and zero for the route-name form, which is the common one. The right\n * permanent home for this is `publishRouteTable`, at boot, once (see the report\n * on this card).\n */\nfunction assertNotARouteName(url: string): void {\n if (knownRouteNames().includes(url)) throw new RouteNameShapeCollisionError(url);\n}\n\nconst ROUTE_ARGUMENT_PROPS = [\"params\", \"query\"] as const;\n\nfunction resolveDestination(props: LinkDestinationProps): Destination {\n const provided = DESTINATION_PROPS.filter(name => props[name] !== undefined);\n\n if (provided.length > 1) throw new AmbiguousLinkDestinationError(provided);\n\n if (provided.length === 0) throw new MissingLinkDestinationError();\n\n if (props.email !== undefined) return { url: `mailto:${props.email}`, isInApp: false };\n\n if (props.tel !== undefined) return { url: `tel:${props.tel}`, isInApp: false };\n\n const destination = (props.to ?? props.href) as string;\n\n /*\n LITERAL: `/pricing`, `https://stripe.com`, `mailto:…`, `whatsapp://…`. It\n goes to the element exactly as written and the route table is never\n consulted — there is nothing to look up, and looking anyway is what used to\n throw `UnknownRouteNameError` on every link out of the application.\n */\n if (isLiteralUrl(destination)) {\n const routeArguments = ROUTE_ARGUMENT_PROPS.filter(name => props[name] !== undefined);\n\n if (routeArguments.length > 0) {\n throw new RouteArgumentsOnLiteralUrlError(destination, routeArguments);\n }\n\n assertNotARouteName(destination);\n\n return { url: destination, isInApp: addressesThisApp(destination) };\n }\n\n /*\n A NAME, resolved against the route table published at boot from the SAME\n discovery result the server registered its routes from. The previous version\n of this file restated six URLs in a literal map, so linking to any seventh\n page in the application threw — the map was the limit on what could be\n linked, and nothing said so at the call site.\n */\n return { url: href(destination, props.params, props.query), isInApp: true };\n}\n\n/**\n * Whether this click should be left entirely to the browser.\n *\n * Every case here is a click that MEANS something other than \"go there in this\n * tab\", and intercepting any of them would take away behaviour the user\n * explicitly asked for:\n *\n * - a modifier or middle button: open in a new tab/window, or download\n * - `download`: save the resource, do not render it\n * - already prevented: something upstream in the tree handled this click\n *\n * Left button with no modifiers is the only click that means plain navigation.\n * The `target` case is decided before this, from the RESOLVED target, because\n * `newTab` sets it after the caller's props are read.\n */\nfunction isPlainLeftClick(event: MouseEvent<HTMLAnchorElement>): boolean {\n return (\n event.button === 0 &&\n !event.metaKey &&\n !event.ctrlKey &&\n !event.shiftKey &&\n !event.altKey &&\n !event.defaultPrevented\n );\n}\n\n/**\n * A target other than `_self` names ANOTHER browsing context — `_blank`, but\n * also `_parent`, `_top` and any named frame. Client navigation rewrites the\n * history of THIS one, so none of them are ours to intercept.\n */\nfunction opensAnotherContext(target: string | undefined): boolean {\n return target !== undefined && target !== \"_self\";\n}\n\nexport function Link({\n to,\n href: hrefAlias,\n email,\n tel,\n params,\n query,\n newTab,\n prefetch,\n component: Component = \"a\",\n children,\n onClick,\n ...elementProps\n}: LinkProps): ReactElement {\n const { url, isInApp } = resolveDestination({\n to,\n href: hrefAlias,\n email,\n tel,\n params,\n query,\n });\n\n const target = elementProps.target ?? (newTab === true ? \"_blank\" : undefined);\n\n // Only a DEFAULT: a caller that wrote its own `rel` (`\"me noopener\"`,\n // `\"external\"`) meant it, and overwriting it would delete a value the page\n // depends on to say something this component knows nothing about.\n const rel =\n elementProps.rel ?? (target === \"_blank\" ? \"noopener noreferrer\" : undefined);\n\n const handleClick = (event: MouseEvent<HTMLAnchorElement>): void => {\n // The caller's handler runs FIRST and unconditionally — it may be doing\n // analytics, closing a menu, or calling `preventDefault()` to veto the\n // navigation outright. Deciding before it ran would let this component\n // navigate away from a click the application had already cancelled.\n onClick?.(event);\n\n // `mailto:`, `tel:` and anything aimed at another browsing context leave\n // this page standing. The runtime is not consulted at all — asking it would\n // spend a page-data fetch on a click that was never going to navigate here.\n if (!isInApp || opensAnotherContext(target)) return;\n\n if (!isPlainLeftClick(event)) return;\n\n /*\n Asked for per click, never captured at render: the runtime registers\n itself when the hydration bundle mounts, which is AFTER the first render\n of every anchor on the page. A value read at render time would be\n `undefined` forever for exactly the links present at hydration — that is,\n all of them.\n\n Absent (server render, or before hydration) the anchor is left alone and\n does what an anchor does. That is the whole progressive-enhancement story:\n links work before this code runs, and work better after.\n */\n if (currentNavigator()?.(url) !== true) return;\n\n event.preventDefault();\n };\n\n /*\n The SAME gate the click uses, asked before any speculative request exists:\n only a destination this app would have navigated to itself may be fetched\n ahead of time. `mailto:`, `tel:`, an external URL and anything aimed at\n another browsing context are all clicks that leave this page, and none of\n them has page data to fetch.\n */\n const prefetchesOnInteraction =\n prefetch === true && isInApp && !opensAnotherContext(target);\n\n /*\n Attached ONLY when prefetching — a link without the prop keeps whatever\n handlers the caller passed, on the element, unwrapped.\n\n Hover AND focus, because a keyboard user never generates the first one and\n would otherwise be the only visitor who never gets the optimisation.\n\n Fire-and-forget by construction: `prefetchPageData` never rejects and is\n never awaited, so nothing here can delay the event or surface a failure. It\n is also safe to reach on the server — it no-ops without a browser — which is\n why this file can import it directly rather than through a `connect*` seam\n like the navigator's. The navigator needs a seam because the runtime behind\n it drags React state and the page registry into the server bundle; the\n prefetch cache is a `Map` and a `fetch` call, inert until an event fires.\n */\n const prefetchHandlers = prefetchesOnInteraction\n ? {\n onMouseEnter: (event: MouseEvent<HTMLAnchorElement>): void => {\n elementProps.onMouseEnter?.(event);\n void prefetchPageData(url);\n },\n onFocus: (event: FocusEvent<HTMLAnchorElement>): void => {\n elementProps.onFocus?.(event);\n void prefetchPageData(url);\n },\n }\n : undefined;\n\n return createElement(\n Component,\n { ...elementProps, ...prefetchHandlers, target, rel, href: url, onClick: handleClick },\n children,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"link.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/components/link.ts"],"sourcesContent":["import { createElement } from \"react\";\nimport type {\n AnchorHTMLAttributes,\n ComponentType,\n FocusEvent,\n MouseEvent,\n ReactElement,\n} from \"react\";\nimport { prefetchPageData } from \"../client/navigation/prefetch\";\nimport { currentNavigator } from \"../routing/navigator\";\nimport { href, knownRouteNames } from \"../routing/route-table\";\n\n/**\n * `<Link>` is SUGAR over `href()`, and deliberately thin.\n *\n * `href(name, params, query)` is the durable primitive — it serves emails,\n * redirects, `Location` headers and every non-React caller, none of which can\n * render a component. This file adds one thing to it: an anchor element.\n *\n * It renders a real `<a href>`. Client-side interception is a later slice of\n * the navigation runtime and lands here without changing this API, which is the\n * point of routing everything through `href` first: navigation becomes a\n * BEHAVIOUR change, not an API change.\n *\n * ── Parity with `@mongez/react-router` ───────────────────────────────────────\n * `href`, `newTab`, `email`, `tel`, `component` and `prefetch` are spelled\n * exactly as MRR spells them, so a component moved across keeps compiling.\n * `params` and `query` are ours and have no MRR equivalent: they pair with the\n * typed `href()` helper, which is what makes a route NAME — rather than a URL —\n * the thing a call site names.\n *\n * ── The semantic divergence this file bridges ────────────────────────────────\n * MRR's `to` is a PATH. Ours was a route NAME, and only a name — which meant a\n * component moved across from MRR compiled and then threw at render, because\n * `\"/products\"` is not the name of anything. The two packages disagreed about\n * what the most-used prop in either of them MEANS.\n *\n * Since 2026-08-24 (owner ruling) `to`/`href` accept BOTH, discriminated by\n * SHAPE — see {@link isLiteralUrl}. That is what makes MRR code portable, and\n * it costs nothing at a Warlock call site, because the two grammars cannot\n * collide: a route name never begins with `/` and never carries a `scheme:`.\n * The ruling RESTS on that, so this file asserts it rather than trusting it\n * ({@link RouteNameShapeCollisionError}).\n */\n\ntype AnchorProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\">;\n\n/**\n * Where the link goes. Every field is documented here once; which COMBINATIONS\n * are legal is decided by {@link LinkDestination}.\n */\ntype LinkDestinationProps = {\n /**\n * A route NAME, or a literal URL — told apart by SHAPE.\n *\n * `\"products.details\"` is a NAME and is resolved through the route table. A\n * page that moves changes its URL and keeps its name, so every call site\n * survives the move; a dead name throws at render naming the routes that do\n * exist, rather than rendering an anchor that 404s. This is the form to\n * prefer, and the only one `params` and `query` apply to.\n *\n * `\"/pricing\"`, `\"https://stripe.com\"`, `\"mailto:sales@example.com\"` and any\n * other `scheme:` are LITERAL — passed through to the element untouched, with\n * no route lookup at all. An app links out, and a route name is not a thing\n * you can have for a page that is not yours.\n */\n to?: string;\n /**\n * An alias of {@link to}, for parity with `@mongez/react-router`. Identical\n * in every respect, including which shapes it accepts.\n */\n href?: string;\n /** Renders a `mailto:` link. Not an in-app navigation. */\n email?: string;\n /** Renders a `tel:` link. Not an in-app navigation. */\n tel?: string;\n /**\n * Values for the route's `:param` segments, e.g. `{ id }` for\n * `\"/products/:id\"`. Only meaningful with a route NAME.\n */\n params?: Record<string, unknown>;\n /**\n * Query string values; an `undefined` value is omitted. Only meaningful with\n * a route NAME.\n */\n query?: Record<string, unknown>;\n};\n\n/**\n * EXACTLY ONE destination, enforced by the type.\n *\n * The alternative — a documented precedence such as \"`to` wins over `href`\" —\n * is silent by construction: the losing prop goes on compiling and goes on\n * reading like it works at the call site, and the anchor points at the wrong\n * page. Refusing the pair costs a call site one edit and can never be\n * misread. The runtime refuses it as well, because a JavaScript caller and a\n * cast both get past this.\n */\ntype LinkDestination =\n | { to: string; href?: never; email?: never; tel?: never }\n | { href: string; to?: never; email?: never; tel?: never }\n | { email: string; to?: never; href?: never; tel?: never }\n | { tel: string; to?: never; href?: never; email?: never };\n\nexport type LinkProps = AnchorProps &\n LinkDestinationProps &\n LinkDestination & {\n /**\n * Open in a new browsing context: `target=\"_blank\"` plus the `rel` that\n * stops the opened page from reaching back through `window.opener`.\n *\n * A caller's own `target`/`rel` win — this only fills in what was not said.\n */\n newTab?: boolean;\n /**\n * Render as something other than `<a>` — a tag name or a component.\n *\n * It receives the resolved `href`, the click handler and every remaining\n * prop, so a design-system anchor keeps client-side navigation as long as\n * it spreads what it is given onto the element it renders.\n *\n * `ComponentType<any>` is MRR's signature, kept verbatim: the component is\n * the caller's and its props are unknowable from here.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n component?: ComponentType<any> | string;\n /**\n * Fetch this page's data when the pointer or the keyboard reaches the link,\n * so the click that follows swaps without a round trip.\n *\n * A GUESS, and treated as one everywhere: it is never awaited, a failure is\n * silent, and the click behaves exactly as it would without it. Opt-in per\n * link rather than on by default, because every prefetch is a request the\n * user did not ask for and someone pays for the bandwidth.\n *\n * IGNORED for anything that is not an in-app navigation — an external URL,\n * `mailto:`, `tel:`, `newTab`, any explicit `target`. Prefetching those\n * would mean issuing a cross-origin request to a third party on hover,\n * which is not a thing a link component may decide to do.\n */\n prefetch?: boolean;\n };\n\nconst DESTINATION_PROPS = [\"to\", \"href\", \"email\", \"tel\"] as const;\n\nexport class AmbiguousLinkDestinationError extends Error {\n public constructor(public readonly providedProps: readonly string[]) {\n super(\n `Warlock <Link> was given ${providedProps\n .map(name => JSON.stringify(name))\n .join(\" and \")}, but a link goes to exactly one place. There is no ` +\n \"precedence between them on purpose: one of the two would silently win, and the \" +\n \"call site would go on naming a destination that never renders. Delete the one \" +\n \"you did not mean.\",\n );\n this.name = \"AmbiguousLinkDestinationError\";\n }\n}\n\nexport class MissingLinkDestinationError extends Error {\n public constructor() {\n super(\n `Warlock <Link> was given no destination. Pass exactly one of ${DESTINATION_PROPS.map(\n name => JSON.stringify(name),\n ).join(\", \")}. It is not defaulted to the current page: an anchor with an empty ` +\n \"`href` renders as a working link and reloads the page when clicked, which is a \" +\n \"harder fault to see than this message.\",\n );\n this.name = \"MissingLinkDestinationError\";\n }\n}\n\nexport class RouteArgumentsOnLiteralUrlError extends Error {\n public constructor(\n public readonly url: string,\n public readonly providedProps: readonly string[],\n ) {\n super(\n `Warlock <Link> was given ${providedProps\n .map(name => JSON.stringify(name))\n .join(\" and \")} alongside the literal URL \"${url}\". Those apply to a route NAME, ` +\n \"which is resolved through the route table; a literal URL is passed through exactly \" +\n \"as written, so they would have been dropped and the link would have pointed at an \" +\n \"unfiltered page that still looked right at the call site. Put the values in the URL, \" +\n \"or name the route.\",\n );\n this.name = \"RouteArgumentsOnLiteralUrlError\";\n }\n}\n\n/**\n * The ruling's one assumption, broken. See the module doc comment: telling a\n * literal URL from a route NAME by shape is only safe while no route is NAMED\n * like a URL, and nothing in the route pipeline validates a hand-declared\n * `route.name`. So the collision is checked at the one place it could do harm,\n * where it is a loud refusal instead of an anchor that silently points\n * somewhere else.\n */\nexport class RouteNameShapeCollisionError extends Error {\n public constructor(public readonly routeName: string) {\n super(\n `Warlock route table: a route is NAMED ${JSON.stringify(routeName)}, which is shaped ` +\n \"like a URL. <Link> tells a literal URL from a route name by shape — a destination \" +\n \"starting with `/` or carrying a `scheme:` is passed through untouched — so this name \" +\n \"can never be resolved, and every link to it would silently point at that path \" +\n \"instead. Rename the route (`route = { path, name }`) to a dotted name such as \" +\n `${JSON.stringify(routeName.replace(/^\\/+/, \"\").replace(/\\//g, \".\") || \"index\")}.`,\n );\n this.name = \"RouteNameShapeCollisionError\";\n }\n}\n\ntype Destination = {\n /** What lands on the element's `href`. */\n url: string;\n /**\n * Whether this URL is a page in THIS app — the only kind the client\n * navigation runtime may be asked about, and the only kind that may be\n * prefetched. `mailto:`, `tel:` and an external URL hand off to another\n * application or another origin entirely, so intercepting any of them would\n * break it, and speculatively fetching one would be a cross-origin request\n * the developer never asked for.\n */\n isInApp: boolean;\n};\n\n/**\n * Any RFC 3986 scheme — `https:`, `mailto:`, `tel:`, `whatsapp:`, an app's own\n * custom one. Matched generically rather than as a list of known schemes: a\n * list would silently resolve `bitcoin:...` through the route table, which is\n * the exact failure this ruling exists to remove, and it would have to grow\n * forever.\n */\nconst SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;\n\n/**\n * Whether this destination is a URL to be used as written, rather than a route\n * name to resolve.\n *\n * The whole discriminator, and deliberately the whole of it: two cheap shape\n * tests, no parsing, no matching. Anything more would be a SECOND route matcher\n * living beside the server's, which this codebase refuses everywhere it comes\n * up — a matcher that disagreed with the real one would produce links to pages\n * that do not exist.\n */\nfunction isLiteralUrl(destination: string): boolean {\n return destination.startsWith(\"/\") || SCHEME_PATTERN.test(destination);\n}\n\n/**\n * Whether a literal URL addresses THIS app.\n *\n * A path is ours. A `scheme:` is not — including `https:` to our own origin,\n * which would need `window.location` to recognise and would make the answer\n * depend on where the code is running. And `//host/path` is PROTOCOL-RELATIVE:\n * it starts with a slash and is nonetheless another origin, which is precisely\n * the case a \"starts with `/`\" test alone would hand to the navigator, where it\n * becomes a `pushState` to a foreign origin — a SecurityError — or a\n * speculative fetch of a third-party host.\n */\nfunction addressesThisApp(url: string): boolean {\n return url.startsWith(\"/\") && !url.startsWith(\"//\");\n}\n\n/**\n * Refuses the one table that would make {@link isLiteralUrl} wrong.\n *\n * Reached only for a destination already judged literal, so the cost is a scan\n * of the published names for links that were never going to hit the table\n * anyway — and zero for the route-name form, which is the common one. The right\n * permanent home for this is `publishRouteTable`, at boot, once (see the report\n * on this card).\n */\nfunction assertNotARouteName(url: string): void {\n if (knownRouteNames().includes(url)) throw new RouteNameShapeCollisionError(url);\n}\n\nconst ROUTE_ARGUMENT_PROPS = [\"params\", \"query\"] as const;\n\nfunction resolveDestination(props: LinkDestinationProps): Destination {\n const provided = DESTINATION_PROPS.filter(name => props[name] !== undefined);\n\n if (provided.length > 1) throw new AmbiguousLinkDestinationError(provided);\n\n if (provided.length === 0) throw new MissingLinkDestinationError();\n\n if (props.email !== undefined) return { url: `mailto:${props.email}`, isInApp: false };\n\n if (props.tel !== undefined) return { url: `tel:${props.tel}`, isInApp: false };\n\n const destination = (props.to ?? props.href) as string;\n\n /*\n LITERAL: `/pricing`, `https://stripe.com`, `mailto:…`, `whatsapp://…`. It\n goes to the element exactly as written and the route table is never\n consulted — there is nothing to look up, and looking anyway is what used to\n throw `UnknownRouteNameError` on every link out of the application.\n */\n if (isLiteralUrl(destination)) {\n const routeArguments = ROUTE_ARGUMENT_PROPS.filter(name => props[name] !== undefined);\n\n if (routeArguments.length > 0) {\n throw new RouteArgumentsOnLiteralUrlError(destination, routeArguments);\n }\n\n assertNotARouteName(destination);\n\n return { url: destination, isInApp: addressesThisApp(destination) };\n }\n\n /*\n A NAME, resolved against the route table published at boot from the SAME\n discovery result the server registered its routes from. The previous version\n of this file restated six URLs in a literal map, so linking to any seventh\n page in the application threw — the map was the limit on what could be\n linked, and nothing said so at the call site.\n */\n return { url: href(destination, props.params, props.query), isInApp: true };\n}\n\n/**\n * Whether this click should be left entirely to the browser.\n *\n * Every case here is a click that MEANS something other than \"go there in this\n * tab\", and intercepting any of them would take away behaviour the user\n * explicitly asked for:\n *\n * - a modifier or middle button: open in a new tab/window, or download\n * - `download`: save the resource, do not render it\n * - already prevented: something upstream in the tree handled this click\n *\n * Left button with no modifiers is the only click that means plain navigation.\n * The `target` case is decided before this, from the RESOLVED target, because\n * `newTab` sets it after the caller's props are read.\n */\nfunction isPlainLeftClick(event: MouseEvent<HTMLAnchorElement>): boolean {\n return (\n event.button === 0 &&\n !event.metaKey &&\n !event.ctrlKey &&\n !event.shiftKey &&\n !event.altKey &&\n !event.defaultPrevented\n );\n}\n\n/**\n * A target other than `_self` names ANOTHER browsing context — `_blank`, but\n * also `_parent`, `_top` and any named frame. Client navigation rewrites the\n * history of THIS one, so none of them are ours to intercept.\n */\nfunction opensAnotherContext(target: string | undefined): boolean {\n return target !== undefined && target !== \"_self\";\n}\n\nexport function Link({\n to,\n href: hrefAlias,\n email,\n tel,\n params,\n query,\n newTab,\n prefetch,\n component: Component = \"a\",\n children,\n onClick,\n ...elementProps\n}: LinkProps): ReactElement {\n const { url, isInApp } = resolveDestination({\n to,\n href: hrefAlias,\n email,\n tel,\n params,\n query,\n });\n\n const target = elementProps.target ?? (newTab === true ? \"_blank\" : undefined);\n\n // Only a DEFAULT: a caller that wrote its own `rel` (`\"me noopener\"`,\n // `\"external\"`) meant it, and overwriting it would delete a value the page\n // depends on to say something this component knows nothing about.\n const rel =\n elementProps.rel ?? (target === \"_blank\" ? \"noopener noreferrer\" : undefined);\n\n const handleClick = (event: MouseEvent<HTMLAnchorElement>): void => {\n // The caller's handler runs FIRST and unconditionally — it may be doing\n // analytics, closing a menu, or calling `preventDefault()` to veto the\n // navigation outright. Deciding before it ran would let this component\n // navigate away from a click the application had already cancelled.\n onClick?.(event);\n\n // `mailto:`, `tel:` and anything aimed at another browsing context leave\n // this page standing. The runtime is not consulted at all — asking it would\n // spend a page-data fetch on a click that was never going to navigate here.\n if (!isInApp || opensAnotherContext(target)) return;\n\n if (!isPlainLeftClick(event)) return;\n\n /*\n Asked for per click, never captured at render: the runtime registers\n itself when the hydration bundle mounts, which is AFTER the first render\n of every anchor on the page. A value read at render time would be\n `undefined` forever for exactly the links present at hydration — that is,\n all of them.\n\n Absent (server render, or before hydration) the anchor is left alone and\n does what an anchor does. That is the whole progressive-enhancement story:\n links work before this code runs, and work better after.\n */\n if (currentNavigator()?.(url) !== true) return;\n\n event.preventDefault();\n };\n\n /*\n The SAME gate the click uses, asked before any speculative request exists:\n only a destination this app would have navigated to itself may be fetched\n ahead of time. `mailto:`, `tel:`, an external URL and anything aimed at\n another browsing context are all clicks that leave this page, and none of\n them has page data to fetch.\n */\n const prefetchesOnInteraction =\n prefetch === true && isInApp && !opensAnotherContext(target);\n\n /*\n Attached ONLY when prefetching — a link without the prop keeps whatever\n handlers the caller passed, on the element, unwrapped.\n\n Hover AND focus, because a keyboard user never generates the first one and\n would otherwise be the only visitor who never gets the optimisation.\n\n Fire-and-forget by construction: `prefetchPageData` never rejects and is\n never awaited, so nothing here can delay the event or surface a failure. It\n is also safe to reach on the server — it no-ops without a browser — which is\n why this file can import it directly rather than through a `connect*` seam\n like the navigator's. The navigator needs a seam because the runtime behind\n it drags React state and the page registry into the server bundle; the\n prefetch cache is a `Map` and a `fetch` call, inert until an event fires.\n */\n const prefetchHandlers = prefetchesOnInteraction\n ? {\n onMouseEnter: (event: MouseEvent<HTMLAnchorElement>): void => {\n elementProps.onMouseEnter?.(event);\n void prefetchPageData(url);\n },\n onFocus: (event: FocusEvent<HTMLAnchorElement>): void => {\n elementProps.onFocus?.(event);\n void prefetchPageData(url);\n },\n }\n : undefined;\n\n return createElement(\n Component,\n { ...elementProps, ...prefetchHandlers, target, rel, href: url, onClick: handleClick },\n children,\n );\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { ReactElement } from "react";
2
2
 
3
- //#region ../web/src/components/scripts.d.ts
3
+ //#region web/src/components/scripts.d.ts
4
4
  type ScriptsProps = {
5
5
  /** Per-request CSP nonce for the inline payload script (root.tsx:119). */nonce?: string;
6
6
  };
@@ -1,13 +1,15 @@
1
1
  import { PAYLOAD_SCRIPT_ID, escapePayload, useDocumentContext } from "./document-context.mjs";
2
- import { createElement } from "react";
2
+ import { isNonHydrating } from "../server/page-render-bundle.mjs";
3
+ import { Fragment, createElement } from "react";
3
4
 
4
- //#region ../web/src/components/scripts.ts
5
+ //#region web/src/components/scripts.ts
5
6
  /**
6
7
  * OPTIONAL placement override for the serialized loader data and `shared`.
7
8
  * Hydration module emission is separate wiring.
8
9
  */
9
10
  function Scripts(props) {
10
11
  const { payload, nonce } = useDocumentContext("Scripts");
12
+ if (isNonHydrating(payload)) return createElement(Fragment, null);
11
13
  return createElement("script", {
12
14
  id: PAYLOAD_SCRIPT_ID,
13
15
  type: "application/json",
@@ -1 +1 @@
1
- {"version":3,"file":"scripts.mjs","names":[],"sources":["../../../../../../../web/src/components/scripts.ts"],"sourcesContent":["import { createElement, type ReactElement } from \"react\";\nimport { escapePayload, PAYLOAD_SCRIPT_ID, useDocumentContext } from \"./document-context\";\n\nexport type ScriptsProps = {\n /** Per-request CSP nonce for the inline payload script (root.tsx:119). */\n nonce?: string;\n};\n\n/**\n * OPTIONAL placement override for the serialized loader data and `shared`.\n * Hydration module emission is separate wiring.\n */\nexport function Scripts(props: ScriptsProps): ReactElement {\n const { payload, nonce } = useDocumentContext(\"Scripts\");\n\n // Explicit prop wins: v5/app's root.tsx passes `shared.nonce` today\n // (root.tsx:119) and must keep working unchanged. Only an absent prop falls\n // back to the framework's nonce slot (document-context.ts).\n const resolvedNonce = props.nonce ?? nonce;\n\n // `dangerouslySetInnerHTML`, not children: the serializer's escaped output must\n // reach the document byte-for-byte. React's default child-text escaping\n // (HTML-entity escaping) would double-process it and corrupt the JSON\n // (spike P7's escaping contract).\n return createElement(\"script\", {\n id: PAYLOAD_SCRIPT_ID,\n type: \"application/json\",\n nonce: resolvedNonce,\n dangerouslySetInnerHTML: { __html: escapePayload(JSON.stringify(payload)) },\n });\n}\n"],"mappings":";;;;;;;;AAYA,SAAgB,QAAQ,OAAmC;CACzD,MAAM,EAAE,SAAS,UAAU,mBAAmB,SAAS;CAWvD,OAAO,cAAc,UAAU;EAC7B,IAAI;EACJ,MAAM;EACN,OAToB,MAAM,SAAS;EAUnC,yBAAyB,EAAE,QAAQ,cAAc,KAAK,UAAU,OAAO,CAAC,EAAE;CAC5E,CAAC;AACH"}
1
+ {"version":3,"file":"scripts.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/components/scripts.ts"],"sourcesContent":["import { createElement, Fragment, type ReactElement } from \"react\";\nimport { escapePayload, PAYLOAD_SCRIPT_ID, useDocumentContext } from \"./document-context\";\nimport { isNonHydrating } from \"../server/page-render-bundle\";\n\nexport type ScriptsProps = {\n /** Per-request CSP nonce for the inline payload script (root.tsx:119). */\n nonce?: string;\n};\n\n/**\n * OPTIONAL placement override for the serialized loader data and `shared`.\n * Hydration module emission is separate wiring.\n */\nexport function Scripts(props: ScriptsProps): ReactElement {\n const { payload, nonce } = useDocumentContext(\"Scripts\");\n\n // `renderPageFailure` marks its payload non-hydrating (page-render-bundle.ts):\n // a module-load/registration throw happens before any triple exists, so\n // there is nothing trustworthy for a hydration script to describe. A normal\n // app `error.page.tsx` payload (from `finishRender`) is never marked and\n // keeps emitting `__WARLOCK_DATA__` below.\n if (isNonHydrating(payload)) return createElement(Fragment, null);\n\n // Explicit prop wins: v5/app's root.tsx passes `shared.nonce` today\n // (root.tsx:119) and must keep working unchanged. Only an absent prop falls\n // back to the framework's nonce slot (document-context.ts).\n const resolvedNonce = props.nonce ?? nonce;\n\n // `dangerouslySetInnerHTML`, not children: the serializer's escaped output must\n // reach the document byte-for-byte. React's default child-text escaping\n // (HTML-entity escaping) would double-process it and corrupt the JSON\n // (spike P7's escaping contract).\n return createElement(\"script\", {\n id: PAYLOAD_SCRIPT_ID,\n type: \"application/json\",\n nonce: resolvedNonce,\n dangerouslySetInnerHTML: { __html: escapePayload(JSON.stringify(payload)) },\n });\n}\n"],"mappings":";;;;;;;;;AAaA,SAAgB,QAAQ,OAAmC;CACzD,MAAM,EAAE,SAAS,UAAU,mBAAmB,SAAS;CAOvD,IAAI,eAAe,OAAO,GAAG,OAAO,cAAc,UAAU,IAAI;CAWhE,OAAO,cAAc,UAAU;EAC7B,IAAI;EACJ,MAAM;EACN,OAToB,MAAM,SAAS;EAUnC,yBAAyB,EAAE,QAAQ,cAAc,KAAK,UAAU,OAAO,CAAC,EAAE;CAC5E,CAAC;AACH"}