@warlock.js/web 5.2.3 → 5.3.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 (94) hide show
  1. package/esm/build/contribution.mjs.map +1 -1
  2. package/esm/build/discover-pages.mjs +5 -7
  3. package/esm/build/discover-pages.mjs.map +1 -1
  4. package/esm/build/generate-client-registry.mjs.map +1 -1
  5. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  6. package/esm/build/page-default-export.mjs.map +1 -1
  7. package/esm/build/page-routes-manifest.mjs.map +1 -1
  8. package/esm/build/public-files.mjs.map +1 -1
  9. package/esm/build/read-route-exports.mjs.map +1 -1
  10. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  11. package/esm/client/hydrate-page.mjs +4 -3
  12. package/esm/client/hydrate-page.mjs.map +1 -1
  13. package/esm/client/navigation/fetch-page-data.mjs +2 -2
  14. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  15. package/esm/client/navigation/navigation-root.mjs +5 -1
  16. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  17. package/esm/client/navigation/prefetch.mjs.map +1 -1
  18. package/esm/client/runtime/manifest.mjs.map +1 -1
  19. package/esm/client/runtime/matcher.mjs.map +1 -1
  20. package/esm/components/document-context.mjs.map +1 -1
  21. package/esm/components/link.mjs.map +1 -1
  22. package/esm/hydration-payload.mjs +6 -2
  23. package/esm/hydration-payload.mjs.map +1 -1
  24. package/esm/index.d.mts +4 -2
  25. package/esm/index.mjs +2 -1
  26. package/esm/localization.d.mts +21 -0
  27. package/esm/localization.mjs +28 -0
  28. package/esm/localization.mjs.map +1 -0
  29. package/esm/routing/data-request.mjs +5 -3
  30. package/esm/routing/data-request.mjs.map +1 -1
  31. package/esm/routing/filesystem-route.mjs +36 -7
  32. package/esm/routing/filesystem-route.mjs.map +1 -1
  33. package/esm/routing/layout-policy.mjs.map +1 -1
  34. package/esm/routing/page-file-segment.mjs +66 -0
  35. package/esm/routing/page-file-segment.mjs.map +1 -0
  36. package/esm/routing/page-route-grammar.mjs +79 -0
  37. package/esm/routing/page-route-grammar.mjs.map +1 -0
  38. package/esm/routing/query-string.mjs.map +1 -1
  39. package/esm/routing/route-identity.d.mts +69 -0
  40. package/esm/routing/route-identity.mjs +100 -44
  41. package/esm/routing/route-identity.mjs.map +1 -1
  42. package/esm/routing/route-table.mjs.map +1 -1
  43. package/esm/server/build-hydration-payload.mjs +3 -2
  44. package/esm/server/build-hydration-payload.mjs.map +1 -1
  45. package/esm/server/create-page-route-handler.d.mts +34 -1
  46. package/esm/server/create-page-route-handler.mjs +35 -4
  47. package/esm/server/create-page-route-handler.mjs.map +1 -1
  48. package/esm/server/execute-page-request.mjs.map +1 -1
  49. package/esm/server/framework-default-not-found-stylesheet.mjs +102 -0
  50. package/esm/server/framework-default-not-found-stylesheet.mjs.map +1 -0
  51. package/esm/server/install-page-routes-from-manifest.mjs +13 -16
  52. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  53. package/esm/server/install-page-routes.d.mts +3 -1
  54. package/esm/server/install-page-routes.mjs +11 -11
  55. package/esm/server/install-page-routes.mjs.map +1 -1
  56. package/esm/server/match-page-route.mjs.map +1 -1
  57. package/esm/server/not-found-page.d.mts +1 -13
  58. package/esm/server/not-found-page.mjs +50 -5
  59. package/esm/server/not-found-page.mjs.map +1 -1
  60. package/esm/server/page-file-change.mjs.map +1 -1
  61. package/esm/server/page-route-reload.mjs.map +1 -1
  62. package/esm/server/register-production-public-files.mjs +16 -1
  63. package/esm/server/register-production-public-files.mjs.map +1 -1
  64. package/esm/server/render-page.d.mts +1 -8
  65. package/esm/server/render-page.mjs +24 -21
  66. package/esm/server/render-page.mjs.map +1 -1
  67. package/esm/server/response-cache-floor.mjs +79 -0
  68. package/esm/server/response-cache-floor.mjs.map +1 -0
  69. package/esm/server/set-cookie-cache-floor-hook.mjs +41 -0
  70. package/esm/server/set-cookie-cache-floor-hook.mjs.map +1 -0
  71. package/esm/server/settle-page-response.mjs.map +1 -1
  72. package/esm/server/stylesheet-urls.mjs.map +1 -1
  73. package/esm/server/unregistered-pages.mjs.map +1 -1
  74. package/esm/server/web-connector-factory.mjs.map +1 -1
  75. package/esm/server/web-connector.d.mts +1 -1
  76. package/esm/server/web-connector.mjs +23 -4
  77. package/esm/server/web-connector.mjs.map +1 -1
  78. package/esm/shared.mjs.map +1 -1
  79. package/esm/vite/build-client.mjs.map +1 -1
  80. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  81. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  82. package/esm/vite/gate-c-verify.mjs.map +1 -1
  83. package/esm/vite/hydration-entries.mjs.map +1 -1
  84. package/esm/vite/index.mjs.map +1 -1
  85. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  86. package/esm/vite/projection.mjs.map +1 -1
  87. package/llms-full.txt +62 -33
  88. package/llms.txt +1 -1
  89. package/package.json +4 -3
  90. package/skills/create-a-page/SKILL.md +59 -28
  91. package/skills/navigate-on-the-client/SKILL.md +16 -11
  92. package/skills/serve-styles/SKILL.md +2 -1
  93. package/skills/use-layouts/SKILL.md +3 -6
  94. package/skills/write-the-root/SKILL.md +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"page-routes-manifest.mjs","names":[],"sources":["../../../../../../../web/src/build/page-routes-manifest.ts"],"sourcesContent":["import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport type { PageRoutesManifest } from \"./generate-pages-barrel\";\r\n\r\nexport const PAGE_ROUTES_MANIFEST_FILE = \"page-routes.manifest.json\";\r\n\r\n/** Atomically replace the snapshot only after the rest of the build succeeds. */\r\nexport async function writePageRoutesManifest(\r\n outdir: string,\r\n manifest: PageRoutesManifest,\r\n): Promise<void> {\r\n const target = path.join(outdir, PAGE_ROUTES_MANIFEST_FILE);\r\n const temporary = path.join(\r\n outdir,\r\n `.${PAGE_ROUTES_MANIFEST_FILE}.${process.pid}.${Date.now()}.tmp`,\r\n );\r\n\r\n await fs.promises.mkdir(outdir, { recursive: true });\r\n\r\n try {\r\n await fs.promises.writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\\n`, \"utf-8\");\r\n await fs.promises.rename(temporary, target);\r\n } finally {\r\n await fs.promises.unlink(temporary).catch(() => {});\r\n }\r\n}\r\n"],"mappings":";;;;AAIA,MAAa,4BAA4B;;AAGzC,eAAsB,wBACpB,QACA,UACe;CACf,MAAM,SAAS,KAAK,KAAK,QAAQ,yBAAyB;CAC1D,MAAM,YAAY,KAAK,KACrB,QACA,IAAI,0BAA0B,GAAG,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,KAC7D;CAEA,MAAM,GAAG,SAAS,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;CAEnD,IAAI;EACF,MAAM,GAAG,SAAS,UAAU,WAAW,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE,KAAK,OAAO;EACxF,MAAM,GAAG,SAAS,OAAO,WAAW,MAAM;CAC5C,UAAU;EACR,MAAM,GAAG,SAAS,OAAO,SAAS,EAAE,YAAY,CAAC,CAAC;CACpD;AACF"}
1
+ {"version":3,"file":"page-routes-manifest.mjs","names":[],"sources":["../../../../../../../web/src/build/page-routes-manifest.ts"],"sourcesContent":["import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport type { PageRoutesManifest } from \"./generate-pages-barrel\";\r\n\r\nexport const PAGE_ROUTES_MANIFEST_FILE = \"page-routes.manifest.json\";\r\n\r\n/** Atomically replace the snapshot only after the rest of the build succeeds. */\r\nexport async function writePageRoutesManifest(\r\n outdir: string,\r\n manifest: PageRoutesManifest,\r\n): Promise<void> {\r\n const target = path.join(outdir, PAGE_ROUTES_MANIFEST_FILE);\r\n const temporary = path.join(\r\n outdir,\r\n `.${PAGE_ROUTES_MANIFEST_FILE}.${process.pid}.${Date.now()}.tmp`,\r\n );\r\n\r\n await fs.promises.mkdir(outdir, { recursive: true });\r\n\r\n try {\r\n await fs.promises.writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\\n`, \"utf-8\");\r\n await fs.promises.rename(temporary, target);\r\n } finally {\r\n await fs.promises.unlink(temporary).catch(() => {});\r\n }\r\n}\r\n"],"mappings":";;;;AAIA,MAAa,4BAA4B;;AAGzC,eAAsB,wBACpB,QACA,UACe;CACf,MAAM,SAAS,KAAK,KAAK,QAAQ,yBAAyB;CAC1D,MAAM,YAAY,KAAK,KACrB,QACA,IAAI,0BAA0B,GAAG,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,KAC7D;CAEA,MAAM,GAAG,SAAS,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;CAEnD,IAAI;EACF,MAAM,GAAG,SAAS,UAAU,WAAW,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE,KAAK,OAAO;EACxF,MAAM,GAAG,SAAS,OAAO,WAAW,MAAM;CAC5C,UAAU;EACR,MAAM,GAAG,SAAS,OAAO,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;CACpD;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"public-files.mjs","names":[],"sources":["../../../../../../../web/src/build/public-files.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * Enumerate the application-owned files Vite serves from `<appRoot>/public`\n * in development. Paths are POSIX and relative so the same list can be baked\n * into the production page manifest without carrying a machine-local root.\n */\nexport async function collectPublicFiles(publicRoot: string): Promise<string[]> {\n let root: fs.Stats;\n\n try {\n root = await fs.promises.stat(publicRoot);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n throw error;\n }\n\n if (!root.isDirectory()) return [];\n\n const files: string[] = [];\n\n async function visit(directory: string): Promise<void> {\n const entries = await fs.promises.readdir(directory, { withFileTypes: true });\n entries.sort((left, right) => left.name.localeCompare(right.name));\n\n for (const entry of entries) {\n const absolute = path.join(directory, entry.name);\n\n if (entry.isDirectory()) {\n await visit(absolute);\n } else if (entry.isFile()) {\n files.push(path.relative(publicRoot, absolute).split(path.sep).join(\"/\"));\n }\n }\n }\n\n await visit(publicRoot);\n return files;\n}\n\n/** Copy exactly the files recorded by {@link collectPublicFiles}. */\nexport async function copyPublicFiles(\n publicRoot: string,\n outputRoot: string,\n files: readonly string[],\n): Promise<void> {\n for (const file of files) {\n const segments = file.split(\"/\");\n const source = path.join(publicRoot, ...segments);\n const target = path.join(outputRoot, ...segments);\n\n await fs.promises.mkdir(path.dirname(target), { recursive: true });\n await fs.promises.copyFile(source, target);\n }\n}\n"],"mappings":";;;;;;;;;AAQA,eAAsB,mBAAmB,YAAuC;CAC9E,IAAI;CAEJ,IAAI;EACF,OAAO,MAAM,GAAG,SAAS,KAAK,UAAU;CAC1C,SAAS,OAAO;EACd,IAAK,MAAgC,SAAS,UAAU,OAAO,CAAC;EAChE,MAAM;CACR;CAEA,IAAI,CAAC,KAAK,YAAY,GAAG,OAAO,CAAC;CAEjC,MAAM,QAAkB,CAAC;CAEzB,eAAe,MAAM,WAAkC;EACrD,MAAM,UAAU,MAAM,GAAG,SAAS,QAAQ,WAAW,EAAE,eAAe,KAAK,CAAC;EAC5E,QAAQ,MAAM,MAAM,UAAU,KAAK,KAAK,cAAc,MAAM,IAAI,CAAC;EAEjE,KAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,WAAW,KAAK,KAAK,WAAW,MAAM,IAAI;GAEhD,IAAI,MAAM,YAAY,GACpB,MAAM,MAAM,QAAQ;QACf,IAAI,MAAM,OAAO,GACtB,MAAM,KAAK,KAAK,SAAS,YAAY,QAAQ,EAAE,MAAM,KAAK,GAAG,EAAE,KAAK,GAAG,CAAC;EAE5E;CACF;CAEA,MAAM,MAAM,UAAU;CACtB,OAAO;AACT;;AAGA,eAAsB,gBACpB,YACA,YACA,OACe;CACf,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,KAAK,MAAM,GAAG;EAC/B,MAAM,SAAS,KAAK,KAAK,YAAY,GAAG,QAAQ;EAChD,MAAM,SAAS,KAAK,KAAK,YAAY,GAAG,QAAQ;EAEhD,MAAM,GAAG,SAAS,MAAM,KAAK,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;EACjE,MAAM,GAAG,SAAS,SAAS,QAAQ,MAAM;CAC3C;AACF"}
1
+ {"version":3,"file":"public-files.mjs","names":[],"sources":["../../../../../../../web/src/build/public-files.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * Enumerate the application-owned files Vite serves from `<appRoot>/public`\n * in development. Paths are POSIX and relative so the same list can be baked\n * into the production page manifest without carrying a machine-local root.\n */\nexport async function collectPublicFiles(publicRoot: string): Promise<string[]> {\n let root: fs.Stats;\n\n try {\n root = await fs.promises.stat(publicRoot);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n throw error;\n }\n\n if (!root.isDirectory()) return [];\n\n const files: string[] = [];\n\n async function visit(directory: string): Promise<void> {\n const entries = await fs.promises.readdir(directory, { withFileTypes: true });\n entries.sort((left, right) => left.name.localeCompare(right.name));\n\n for (const entry of entries) {\n const absolute = path.join(directory, entry.name);\n\n if (entry.isDirectory()) {\n await visit(absolute);\n } else if (entry.isFile()) {\n files.push(path.relative(publicRoot, absolute).split(path.sep).join(\"/\"));\n }\n }\n }\n\n await visit(publicRoot);\n return files;\n}\n\n/** Copy exactly the files recorded by {@link collectPublicFiles}. */\nexport async function copyPublicFiles(\n publicRoot: string,\n outputRoot: string,\n files: readonly string[],\n): Promise<void> {\n for (const file of files) {\n const segments = file.split(\"/\");\n const source = path.join(publicRoot, ...segments);\n const target = path.join(outputRoot, ...segments);\n\n await fs.promises.mkdir(path.dirname(target), { recursive: true });\n await fs.promises.copyFile(source, target);\n }\n}\n"],"mappings":";;;;;;;;;AAQA,eAAsB,mBAAmB,YAAuC;CAC9E,IAAI;CAEJ,IAAI;EACF,OAAO,MAAM,GAAG,SAAS,KAAK,UAAU;CAC1C,SAAS,OAAO;EACd,IAAK,MAAgC,SAAS,UAAU,OAAO,CAAC;EAChE,MAAM;CACR;CAEA,IAAI,CAAC,KAAK,YAAY,GAAG,OAAO,CAAC;CAEjC,MAAM,QAAkB,CAAC;CAEzB,eAAe,MAAM,WAAkC;EACrD,MAAM,UAAU,MAAM,GAAG,SAAS,QAAQ,WAAW,EAAE,eAAe,KAAK,CAAC;EAC5E,QAAQ,MAAM,MAAM,UAAU,KAAK,KAAK,cAAc,MAAM,IAAI,CAAC;EAEjE,KAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,WAAW,KAAK,KAAK,WAAW,MAAM,IAAI;GAEhD,IAAI,MAAM,YAAY,GACpB,MAAM,MAAM,QAAQ;QACf,IAAI,MAAM,OAAO,GACtB,MAAM,KAAK,KAAK,SAAS,YAAY,QAAQ,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;EAE5E;CACF;CAEA,MAAM,MAAM,UAAU;CACtB,OAAO;AACT;;AAGA,eAAsB,gBACpB,YACA,YACA,OACe;CACf,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,KAAK,MAAM,GAAG;EAC/B,MAAM,SAAS,KAAK,KAAK,YAAY,GAAG,QAAQ;EAChD,MAAM,SAAS,KAAK,KAAK,YAAY,GAAG,QAAQ;EAEhD,MAAM,GAAG,SAAS,MAAM,KAAK,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;EACjE,MAAM,GAAG,SAAS,SAAS,QAAQ,MAAM;CAC3C;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"read-route-exports.mjs","names":[],"sources":["../../../../../../../web/src/build/read-route-exports.ts"],"sourcesContent":["/**\r\n * Reads a page's `route` export and a layout's `prefix` export STATICALLY —\r\n * by parsing the source, never by loading the module.\r\n *\r\n * The build has to know a page's declared route before anything is built, and\r\n * the only other way to learn it is to run the page: import the module, let its\r\n * top-level code execute, and read the binding. That is a different program\r\n * from the one being built, with the application's own side effects in it. So\r\n * this module parses instead, and the price of parsing is that the declaration\r\n * has to be readable without evaluation — a literal. What cannot be read is\r\n * REFUSED rather than guessed: a wrong route path that builds is worse than a\r\n * build that stops and says which file to change.\r\n *\r\n * `route` and `prefix` are names the page contract reserves, so this reads them\r\n * out of whichever file it is given and refuses a computed one wherever it\r\n * appears — a page that exports `prefix`, or a layout that exports `route`, is\r\n * using a name the framework already owns.\r\n *\r\n * Single responsibility, deliberately: this returns values or a typed\r\n * rejection and decides nothing. What a rejection costs, and when a page is\r\n * routed at all, belongs to the caller.\r\n */\r\nimport fs from \"node:fs\";\r\nimport { parse } from \"@babel/parser\";\r\n\r\n/**\r\n * The AST types are derived from `parse`'s own return type rather than imported\r\n * from `@babel/types`: the parser resolves its own copy of that package, and a\r\n * node from one copy is not assignable to the identically-shaped type from the\r\n * other. Reading the types off the function that produced the nodes cannot\r\n * disagree with it.\r\n */\r\ntype Statement = ReturnType<typeof parse>[\"program\"][\"body\"][number];\r\ntype Expression = Extract<Statement, { type: \"ExpressionStatement\" }>[\"expression\"];\r\ntype ObjectExpression = Extract<Expression, { type: \"ObjectExpression\" }>;\r\ntype ObjectProperty = Extract<ObjectExpression[\"properties\"][number], { type: \"ObjectProperty\" }>;\r\n\r\n/**\r\n * Anything that can appear where a value is expected — an expression, or one of\r\n * the destructuring patterns that are legal in an object literal's value slot\r\n * and are never a literal string.\r\n */\r\ntype ValueNode = ObjectProperty[\"value\"];\r\n\r\n/**\r\n * A declared route, normalised. The bare-string form (`route = \"/list\"`) and\r\n * the object form (`route = { path: \"/list\" }`) reach the caller identically,\r\n * because the server resolves them identically — `name` is absent exactly when\r\n * the source omitted it, which is the caller's signal to derive one.\r\n */\r\nexport type DeclaredRoute = { path: string; name?: string };\r\n\r\n/** Which export could not be read, from which file, and what was found instead. */\r\nexport type RouteExportsRejection = {\r\n sourceFile: string;\r\n exportName: \"route\" | \"prefix\";\r\n /** A sentence fragment naming the form that was found, e.g. \"its value is a function call\". */\r\n detail: string;\r\n};\r\n\r\nexport type RouteExportsReadResult =\r\n | { ok: true; route?: DeclaredRoute; prefix?: string }\r\n | { ok: false; rejection: RouteExportsRejection };\r\n\r\nconst EXAMPLES: Record<\"route\" | \"prefix\", string> = {\r\n route: 'export const route = \"/list\"; (or export const route = { path: \"/list\", name: \"shop.list\" };)',\r\n prefix: 'export const prefix = \"/shop\";',\r\n};\r\n\r\n/**\r\n * The one thing an app developer is told when a declaration cannot be read.\r\n *\r\n * It names the file, says what was found, says why a literal is required, and\r\n * shows one — because the reader of this message is someone who wrote perfectly\r\n * valid TypeScript and needs to know why the build will not take it.\r\n */\r\nexport class NonLiteralRouteExportError extends Error {\r\n public constructor(public readonly rejection: RouteExportsRejection) {\r\n const { sourceFile, exportName, detail } = rejection;\r\n\r\n super(\r\n `Cannot read the \\`${exportName}\\` export of \"${sourceFile}\": ${detail}. The build reads ` +\r\n \"route declarations without running your application code, so this value has to be \" +\r\n `written out as a literal. For example: ${EXAMPLES[exportName]}`,\r\n );\r\n\r\n this.name = \"NonLiteralRouteExportError\";\r\n }\r\n}\r\n\r\n/**\r\n * `as const`, `satisfies`, a non-null assertion and parentheses all wrap a value\r\n * without changing it, so reading through them costs nothing and refusing them\r\n * would reject declarations that are literal in every sense that matters here.\r\n */\r\nfunction unwrap(node: ValueNode): ValueNode {\r\n switch (node.type) {\r\n case \"TSAsExpression\":\r\n case \"TSSatisfiesExpression\":\r\n case \"TSNonNullExpression\":\r\n case \"TypeCastExpression\":\r\n case \"ParenthesizedExpression\":\r\n return unwrap(node.expression);\r\n default:\r\n return node;\r\n }\r\n}\r\n\r\n/** The string a node denotes, or `undefined` when that needs evaluation to know. */\r\nfunction stringLiteralOf(node: ValueNode): string | undefined {\r\n const value = unwrap(node);\r\n\r\n if (value.type === \"StringLiteral\") return value.value;\r\n\r\n // A template with no substitutions is a string spelled with backticks.\r\n if (value.type === \"TemplateLiteral\" && value.expressions.length === 0) {\r\n return value.quasis[0]?.value.cooked ?? value.quasis[0]?.value.raw;\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\n/** A sentence fragment naming what was found, for the developer-facing message. */\r\nfunction describe(node: ValueNode): string {\r\n const value = unwrap(node);\r\n\r\n switch (value.type) {\r\n case \"CallExpression\":\r\n case \"OptionalCallExpression\":\r\n case \"NewExpression\":\r\n return \"its value is a function call\";\r\n case \"Identifier\":\r\n return `its value is the variable \\`${value.name}\\``;\r\n case \"MemberExpression\":\r\n case \"OptionalMemberExpression\":\r\n return \"its value is read off another object\";\r\n case \"TemplateLiteral\":\r\n return \"its value is a template literal with an expression in it\";\r\n case \"ConditionalExpression\":\r\n return \"its value depends on a condition\";\r\n case \"BinaryExpression\":\r\n case \"LogicalExpression\":\r\n return \"its value is built by an expression\";\r\n default:\r\n return \"its value is computed rather than written out\";\r\n }\r\n}\r\n\r\ntype ObjectRead = { ok: true; route: DeclaredRoute } | { ok: false; detail: string };\r\n\r\n/**\r\n * The object form. Unknown keys are IGNORED rather than refused, matching the\r\n * server, which reads `path` and `name` and lets a page carry whatever else it\r\n * wants alongside them. A spread is not an unknown key: it can contribute\r\n * `path` itself, so an object that spreads is an object whose route this cannot\r\n * claim to have read.\r\n */\r\nfunction readRouteObject(node: ObjectExpression): ObjectRead {\r\n let routePath: string | undefined;\r\n let routeName: string | undefined;\r\n\r\n for (const property of node.properties) {\r\n if (property.type === \"SpreadElement\") {\r\n return { ok: false, detail: \"the object spreads another value into itself\" };\r\n }\r\n\r\n if (property.computed) {\r\n return { ok: false, detail: \"one of the object's keys is computed\" };\r\n }\r\n\r\n const { key } = property;\r\n const keyName =\r\n key.type === \"Identifier\" ? key.name : key.type === \"StringLiteral\" ? key.value : undefined;\r\n\r\n if (keyName !== \"path\" && keyName !== \"name\") continue;\r\n\r\n if (property.type !== \"ObjectProperty\") {\r\n return { ok: false, detail: `\\`${keyName}\\` is declared as a method` };\r\n }\r\n\r\n const value = stringLiteralOf(property.value);\r\n\r\n if (value === undefined) {\r\n return { ok: false, detail: `its \\`${keyName}\\` is not written as a string literal` };\r\n }\r\n\r\n if (keyName === \"path\") routePath = value;\r\n else routeName = value;\r\n }\r\n\r\n if (routePath === undefined) {\r\n return { ok: false, detail: \"the object does not declare a `path`\" };\r\n }\r\n\r\n return {\r\n ok: true,\r\n route: routeName === undefined ? { path: routePath } : { path: routePath, name: routeName },\r\n };\r\n}\r\n\r\n/**\r\n * Parses the source, or THROWS when it cannot be parsed at all.\r\n *\r\n * A syntax error is not a rejection, deliberately: nothing about the route\r\n * declaration is known yet, so telling the developer to write a literal would\r\n * answer a question they did not ask.\r\n */\r\nfunction parseSource(sourceFile: string, source: string) {\r\n try {\r\n return parse(source, {\r\n sourceType: \"module\",\r\n // Every file this reads is a page or a layout, i.e. `.tsx`.\r\n plugins: [\"typescript\", \"jsx\"],\r\n errorRecovery: false,\r\n });\r\n } catch (error) {\r\n throw new Error(\r\n `Cannot read the route declarations of \"${sourceFile}\": the file could not be parsed ` +\r\n `(${(error as Error).message}). Fix the syntax error and the build will continue.`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Returns the literal `route` and `prefix` this file declares.\r\n *\r\n * Absent is not a rejection: a file that declares neither is read successfully\r\n * with both fields unset, and what THAT means — a page with no public URL, a\r\n * layout that adds no prefix — is the caller's call to make.\r\n *\r\n * `source` is an override for callers that already hold the text; by default\r\n * the file is read from disk.\r\n */\r\nexport function readRouteExports(sourceFile: string, source?: string): RouteExportsReadResult {\r\n const text = source ?? fs.readFileSync(sourceFile, \"utf-8\");\r\n const ast = parseSource(sourceFile, text);\r\n\r\n const reject = (exportName: \"route\" | \"prefix\", detail: string): RouteExportsReadResult => ({\r\n ok: false,\r\n rejection: { sourceFile, exportName, detail },\r\n });\r\n\r\n let route: DeclaredRoute | undefined;\r\n let prefix: string | undefined;\r\n\r\n for (const statement of ast.program.body) {\r\n if (statement.type !== \"ExportNamedDeclaration\" || statement.exportKind === \"type\") continue;\r\n\r\n // `export { route }` hides the value behind a binding this cannot follow\r\n // without resolving scope — and following it across modules is exactly the\r\n // evaluation this reader exists to avoid.\r\n for (const specifier of statement.specifiers) {\r\n if (specifier.type !== \"ExportSpecifier\" || specifier.exportKind === \"type\") continue;\r\n\r\n const exported =\r\n specifier.exported.type === \"Identifier\"\r\n ? specifier.exported.name\r\n : specifier.exported.value;\r\n\r\n if (exported === \"route\" || exported === \"prefix\") {\r\n return reject(\r\n exported,\r\n \"it is exported through an export list rather than declared with `export const`\",\r\n );\r\n }\r\n }\r\n\r\n const { declaration } = statement;\r\n\r\n if (declaration?.type !== \"VariableDeclaration\") continue;\r\n\r\n for (const declarator of declaration.declarations) {\r\n if (declarator.id.type !== \"Identifier\") continue;\r\n\r\n const declared = declarator.id.name;\r\n\r\n if (declared !== \"route\" && declared !== \"prefix\") continue;\r\n\r\n if (declarator.init === null || declarator.init === undefined) {\r\n return reject(declared, \"it is declared without a value\");\r\n }\r\n\r\n if (declared === \"prefix\") {\r\n const value = stringLiteralOf(declarator.init);\r\n\r\n if (value === undefined) return reject(\"prefix\", describe(declarator.init));\r\n\r\n prefix = value;\r\n continue;\r\n }\r\n\r\n const value = stringLiteralOf(declarator.init);\r\n\r\n if (value !== undefined) {\r\n route = { path: value };\r\n continue;\r\n }\r\n\r\n const object = unwrap(declarator.init);\r\n\r\n if (object.type !== \"ObjectExpression\") return reject(\"route\", describe(declarator.init));\r\n\r\n const read = readRouteObject(object);\r\n\r\n if (!read.ok) return reject(\"route\", read.detail);\r\n\r\n route = read.route;\r\n }\r\n }\r\n\r\n return {\r\n ok: true,\r\n ...(route === undefined ? {} : { route }),\r\n ...(prefix === undefined ? {} : { prefix }),\r\n };\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"read-route-exports.mjs","names":[],"sources":["../../../../../../../web/src/build/read-route-exports.ts"],"sourcesContent":["/**\r\n * Reads a page's `route` export and a layout's `prefix` export STATICALLY —\r\n * by parsing the source, never by loading the module.\r\n *\r\n * The build has to know a page's declared route before anything is built, and\r\n * the only other way to learn it is to run the page: import the module, let its\r\n * top-level code execute, and read the binding. That is a different program\r\n * from the one being built, with the application's own side effects in it. So\r\n * this module parses instead, and the price of parsing is that the declaration\r\n * has to be readable without evaluation — a literal. What cannot be read is\r\n * REFUSED rather than guessed: a wrong route path that builds is worse than a\r\n * build that stops and says which file to change.\r\n *\r\n * `route` and `prefix` are names the page contract reserves, so this reads them\r\n * out of whichever file it is given and refuses a computed one wherever it\r\n * appears — a page that exports `prefix`, or a layout that exports `route`, is\r\n * using a name the framework already owns.\r\n *\r\n * Single responsibility, deliberately: this returns values or a typed\r\n * rejection and decides nothing. What a rejection costs, and when a page is\r\n * routed at all, belongs to the caller.\r\n */\r\nimport fs from \"node:fs\";\r\nimport { parse } from \"@babel/parser\";\r\n\r\n/**\r\n * The AST types are derived from `parse`'s own return type rather than imported\r\n * from `@babel/types`: the parser resolves its own copy of that package, and a\r\n * node from one copy is not assignable to the identically-shaped type from the\r\n * other. Reading the types off the function that produced the nodes cannot\r\n * disagree with it.\r\n */\r\ntype Statement = ReturnType<typeof parse>[\"program\"][\"body\"][number];\r\ntype Expression = Extract<Statement, { type: \"ExpressionStatement\" }>[\"expression\"];\r\ntype ObjectExpression = Extract<Expression, { type: \"ObjectExpression\" }>;\r\ntype ObjectProperty = Extract<ObjectExpression[\"properties\"][number], { type: \"ObjectProperty\" }>;\r\n\r\n/**\r\n * Anything that can appear where a value is expected — an expression, or one of\r\n * the destructuring patterns that are legal in an object literal's value slot\r\n * and are never a literal string.\r\n */\r\ntype ValueNode = ObjectProperty[\"value\"];\r\n\r\n/**\r\n * A declared route, normalised. The bare-string form (`route = \"/list\"`) and\r\n * the object form (`route = { path: \"/list\" }`) reach the caller identically,\r\n * because the server resolves them identically — `name` is absent exactly when\r\n * the source omitted it, which is the caller's signal to derive one.\r\n */\r\nexport type DeclaredRoute = { path: string; name?: string };\r\n\r\n/** Which export could not be read, from which file, and what was found instead. */\r\nexport type RouteExportsRejection = {\r\n sourceFile: string;\r\n exportName: \"route\" | \"prefix\";\r\n /** A sentence fragment naming the form that was found, e.g. \"its value is a function call\". */\r\n detail: string;\r\n};\r\n\r\nexport type RouteExportsReadResult =\r\n | { ok: true; route?: DeclaredRoute; prefix?: string }\r\n | { ok: false; rejection: RouteExportsRejection };\r\n\r\nconst EXAMPLES: Record<\"route\" | \"prefix\", string> = {\r\n route: 'export const route = \"/list\"; (or export const route = { path: \"/list\", name: \"shop.list\" };)',\r\n prefix: 'export const prefix = \"/shop\";',\r\n};\r\n\r\n/**\r\n * The one thing an app developer is told when a declaration cannot be read.\r\n *\r\n * It names the file, says what was found, says why a literal is required, and\r\n * shows one — because the reader of this message is someone who wrote perfectly\r\n * valid TypeScript and needs to know why the build will not take it.\r\n */\r\nexport class NonLiteralRouteExportError extends Error {\r\n public constructor(public readonly rejection: RouteExportsRejection) {\r\n const { sourceFile, exportName, detail } = rejection;\r\n\r\n super(\r\n `Cannot read the \\`${exportName}\\` export of \"${sourceFile}\": ${detail}. The build reads ` +\r\n \"route declarations without running your application code, so this value has to be \" +\r\n `written out as a literal. For example: ${EXAMPLES[exportName]}`,\r\n );\r\n\r\n this.name = \"NonLiteralRouteExportError\";\r\n }\r\n}\r\n\r\n/**\r\n * `as const`, `satisfies`, a non-null assertion and parentheses all wrap a value\r\n * without changing it, so reading through them costs nothing and refusing them\r\n * would reject declarations that are literal in every sense that matters here.\r\n */\r\nfunction unwrap(node: ValueNode): ValueNode {\r\n switch (node.type) {\r\n case \"TSAsExpression\":\r\n case \"TSSatisfiesExpression\":\r\n case \"TSNonNullExpression\":\r\n case \"TypeCastExpression\":\r\n case \"ParenthesizedExpression\":\r\n return unwrap(node.expression);\r\n default:\r\n return node;\r\n }\r\n}\r\n\r\n/** The string a node denotes, or `undefined` when that needs evaluation to know. */\r\nfunction stringLiteralOf(node: ValueNode): string | undefined {\r\n const value = unwrap(node);\r\n\r\n if (value.type === \"StringLiteral\") return value.value;\r\n\r\n // A template with no substitutions is a string spelled with backticks.\r\n if (value.type === \"TemplateLiteral\" && value.expressions.length === 0) {\r\n return value.quasis[0]?.value.cooked ?? value.quasis[0]?.value.raw;\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\n/** A sentence fragment naming what was found, for the developer-facing message. */\r\nfunction describe(node: ValueNode): string {\r\n const value = unwrap(node);\r\n\r\n switch (value.type) {\r\n case \"CallExpression\":\r\n case \"OptionalCallExpression\":\r\n case \"NewExpression\":\r\n return \"its value is a function call\";\r\n case \"Identifier\":\r\n return `its value is the variable \\`${value.name}\\``;\r\n case \"MemberExpression\":\r\n case \"OptionalMemberExpression\":\r\n return \"its value is read off another object\";\r\n case \"TemplateLiteral\":\r\n return \"its value is a template literal with an expression in it\";\r\n case \"ConditionalExpression\":\r\n return \"its value depends on a condition\";\r\n case \"BinaryExpression\":\r\n case \"LogicalExpression\":\r\n return \"its value is built by an expression\";\r\n default:\r\n return \"its value is computed rather than written out\";\r\n }\r\n}\r\n\r\ntype ObjectRead = { ok: true; route: DeclaredRoute } | { ok: false; detail: string };\r\n\r\n/**\r\n * The object form. Unknown keys are IGNORED rather than refused, matching the\r\n * server, which reads `path` and `name` and lets a page carry whatever else it\r\n * wants alongside them. A spread is not an unknown key: it can contribute\r\n * `path` itself, so an object that spreads is an object whose route this cannot\r\n * claim to have read.\r\n */\r\nfunction readRouteObject(node: ObjectExpression): ObjectRead {\r\n let routePath: string | undefined;\r\n let routeName: string | undefined;\r\n\r\n for (const property of node.properties) {\r\n if (property.type === \"SpreadElement\") {\r\n return { ok: false, detail: \"the object spreads another value into itself\" };\r\n }\r\n\r\n if (property.computed) {\r\n return { ok: false, detail: \"one of the object's keys is computed\" };\r\n }\r\n\r\n const { key } = property;\r\n const keyName =\r\n key.type === \"Identifier\" ? key.name : key.type === \"StringLiteral\" ? key.value : undefined;\r\n\r\n if (keyName !== \"path\" && keyName !== \"name\") continue;\r\n\r\n if (property.type !== \"ObjectProperty\") {\r\n return { ok: false, detail: `\\`${keyName}\\` is declared as a method` };\r\n }\r\n\r\n const value = stringLiteralOf(property.value);\r\n\r\n if (value === undefined) {\r\n return { ok: false, detail: `its \\`${keyName}\\` is not written as a string literal` };\r\n }\r\n\r\n if (keyName === \"path\") routePath = value;\r\n else routeName = value;\r\n }\r\n\r\n if (routePath === undefined) {\r\n return { ok: false, detail: \"the object does not declare a `path`\" };\r\n }\r\n\r\n return {\r\n ok: true,\r\n route: routeName === undefined ? { path: routePath } : { path: routePath, name: routeName },\r\n };\r\n}\r\n\r\n/**\r\n * Parses the source, or THROWS when it cannot be parsed at all.\r\n *\r\n * A syntax error is not a rejection, deliberately: nothing about the route\r\n * declaration is known yet, so telling the developer to write a literal would\r\n * answer a question they did not ask.\r\n */\r\nfunction parseSource(sourceFile: string, source: string) {\r\n try {\r\n return parse(source, {\r\n sourceType: \"module\",\r\n // Every file this reads is a page or a layout, i.e. `.tsx`.\r\n plugins: [\"typescript\", \"jsx\"],\r\n errorRecovery: false,\r\n });\r\n } catch (error) {\r\n throw new Error(\r\n `Cannot read the route declarations of \"${sourceFile}\": the file could not be parsed ` +\r\n `(${(error as Error).message}). Fix the syntax error and the build will continue.`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Returns the literal `route` and `prefix` this file declares.\r\n *\r\n * Absent is not a rejection: a file that declares neither is read successfully\r\n * with both fields unset, and what THAT means — a page with no public URL, a\r\n * layout that adds no prefix — is the caller's call to make.\r\n *\r\n * `source` is an override for callers that already hold the text; by default\r\n * the file is read from disk.\r\n */\r\nexport function readRouteExports(sourceFile: string, source?: string): RouteExportsReadResult {\r\n const text = source ?? fs.readFileSync(sourceFile, \"utf-8\");\r\n const ast = parseSource(sourceFile, text);\r\n\r\n const reject = (exportName: \"route\" | \"prefix\", detail: string): RouteExportsReadResult => ({\r\n ok: false,\r\n rejection: { sourceFile, exportName, detail },\r\n });\r\n\r\n let route: DeclaredRoute | undefined;\r\n let prefix: string | undefined;\r\n\r\n for (const statement of ast.program.body) {\r\n if (statement.type !== \"ExportNamedDeclaration\" || statement.exportKind === \"type\") continue;\r\n\r\n // `export { route }` hides the value behind a binding this cannot follow\r\n // without resolving scope — and following it across modules is exactly the\r\n // evaluation this reader exists to avoid.\r\n for (const specifier of statement.specifiers) {\r\n if (specifier.type !== \"ExportSpecifier\" || specifier.exportKind === \"type\") continue;\r\n\r\n const exported =\r\n specifier.exported.type === \"Identifier\"\r\n ? specifier.exported.name\r\n : specifier.exported.value;\r\n\r\n if (exported === \"route\" || exported === \"prefix\") {\r\n return reject(\r\n exported,\r\n \"it is exported through an export list rather than declared with `export const`\",\r\n );\r\n }\r\n }\r\n\r\n const { declaration } = statement;\r\n\r\n if (declaration?.type !== \"VariableDeclaration\") continue;\r\n\r\n for (const declarator of declaration.declarations) {\r\n if (declarator.id.type !== \"Identifier\") continue;\r\n\r\n const declared = declarator.id.name;\r\n\r\n if (declared !== \"route\" && declared !== \"prefix\") continue;\r\n\r\n if (declarator.init === null || declarator.init === undefined) {\r\n return reject(declared, \"it is declared without a value\");\r\n }\r\n\r\n if (declared === \"prefix\") {\r\n const value = stringLiteralOf(declarator.init);\r\n\r\n if (value === undefined) return reject(\"prefix\", describe(declarator.init));\r\n\r\n prefix = value;\r\n continue;\r\n }\r\n\r\n const value = stringLiteralOf(declarator.init);\r\n\r\n if (value !== undefined) {\r\n route = { path: value };\r\n continue;\r\n }\r\n\r\n const object = unwrap(declarator.init);\r\n\r\n if (object.type !== \"ObjectExpression\") return reject(\"route\", describe(declarator.init));\r\n\r\n const read = readRouteObject(object);\r\n\r\n if (!read.ok) return reject(\"route\", read.detail);\r\n\r\n route = read.route;\r\n }\r\n }\r\n\r\n return {\r\n ok: true,\r\n ...(route === undefined ? {} : { route }),\r\n ...(prefix === undefined ? {} : { prefix }),\r\n };\r\n}\r\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"build-hydrated-tree.mjs","names":[],"sources":["../../../../../../../web/src/client/build-hydrated-tree.ts"],"sourcesContent":["/**\n * The hydration COMPOSER: payload + page registry -> the ReactNode to mount.\n *\n * It takes the registry as an ARGUMENT and touches no browser global, which is\n * the whole point of it living apart from `index.ts`: every rule below is\n * testable with a hand-built registry, no bundler, no virtual module, no DOM.\n *\n * LOOKUP BY NAME, NEVER BY MATCH. `payload.name` is the identity of the entry\n * the SERVER matched for this exact request (document-context.ts's `name`\n * field). Re-deriving it from `location.pathname` with `matchClientRoute`\n * would be a second implementation of route semantics running against the one\n * request it is hydrating, free to disagree with the server that produced the\n * markup. `matchClientRoute` is for client-side NAVIGATION, where no server\n * answer exists yet.\n */\nimport { createElement, type ComponentType, type ReactNode } from \"react\";\nimport type {\n HydrationDocumentPayloadSource,\n SerializedErrorPageProps,\n} from \"../hydration-payload\";\nimport { registerModules } from \"../runtime/register-modules\";\nimport { loadClientRouteComposition } from \"./runtime\";\nimport type { ClientPageEntry, ClientProjectedModule } from \"./runtime/types\";\n\n/** What every composed level receives — the shape `render-page.ts` uses server-side. */\ntype HydratedLevelProps = {\n readonly data: unknown;\n readonly shared: unknown;\n readonly children?: ReactNode;\n};\n\n/** The ordinary page leaf alone receives params from the server's match. */\ntype HydratedPageProps = {\n readonly data: unknown;\n readonly shared: unknown;\n readonly params: Readonly<Record<string, string>>;\n};\n\nfunction describeKnownNames(knownPageNames: readonly string[]): string {\n if (knownPageNames.length === 0) return \"The client page registry is empty.\";\n\n return `The registry knows: ${knownPageNames.map((name) => JSON.stringify(name)).join(\", \")}.`;\n}\n\n/**\n * The THIRD hydration failure case, beside an absent and a malformed payload.\n *\n * It fails CLOSED — no default entry, no nearest-path fallback, no silent\n * no-op. A registry that quietly substitutes a page produces a browser showing\n * one page's markup running another page's code, which is precisely the defect\n * this entry point was rewritten to remove; a fallback would reintroduce it\n * wearing a recovery costume. Throwing leaves the server-rendered markup on\n * screen and un-hydrated, which is degraded but honest.\n */\nexport class UnknownHydrationPageNameError extends Error {\n public constructor(\n public readonly pageName: string,\n public readonly knownPageNames: readonly string[],\n ) {\n super(\n `Warlock hydration aborted: the payload names page ${JSON.stringify(pageName)}, which is ` +\n `not in the client page registry. ${describeKnownNames(knownPageNames)} The server ` +\n \"rendered a page this browser bundle does not carry, so the server and client were \" +\n \"built from different page graphs. To fix: rebuild the client bundle, or check that \" +\n \"the page's file still exports a `route` discovery can see.\",\n );\n this.name = \"UnknownHydrationPageNameError\";\n }\n}\n\n/**\n * The server selected an app error page, but this browser graph cannot load it.\n * Substituting the ordinary page would execute the component that already\n * failed and hydrate markup the server did not render, so this path fails\n * closed just like an unknown route name.\n */\nexport class MissingHydrationErrorPageError extends Error {\n public constructor(public readonly pageName: string) {\n super(\n `Warlock hydration aborted: the server selected error.page.tsx for route ` +\n `${JSON.stringify(pageName)}, but that route's client composition has no ErrorPage ` +\n \"module. Rebuild the client page registry so it projects the discovered error page.\",\n );\n this.name = \"MissingHydrationErrorPageError\";\n }\n}\n\nfunction findEntryByName(\n pages: readonly ClientPageEntry[],\n name: string,\n): ClientPageEntry {\n const entry = pages.find((candidate) => candidate.name === name);\n\n if (entry === undefined) {\n throw new UnknownHydrationPageNameError(\n name,\n pages.map((candidate) => candidate.name),\n );\n }\n\n return entry;\n}\n\n/**\n * A level's component, or undefined when the module exports no default.\n *\n * Undefined is NOT an error: `render-page.ts:258` and `:279` treat a missing\n * default as a passthrough server-side, and the client tree has to match the\n * markup React is hydrating against — introducing a level here that the server\n * did not render is a hydration mismatch, not a repair.\n */\nfunction componentOf<Props extends object>(\n module: ClientProjectedModule,\n): ComponentType<Props> | undefined {\n const component = module.default;\n\n return typeof component === \"function\"\n ? (component as ComponentType<Props>)\n : undefined;\n}\n\nfunction wrap(\n module: ClientProjectedModule,\n data: unknown,\n shared: unknown,\n children: ReactNode,\n): ReactNode {\n const Component = componentOf<HydratedLevelProps>(module);\n\n if (Component === undefined) return children;\n\n return createElement(Component, { data, shared, children });\n}\n\n/**\n * Compose the tree the server rendered inside `#root`: ordered layouts wrapping\n * the selected Page or ErrorPage leaf, layouts OUTERMOST FIRST as\n * `ClientRouteComposition` declares them. Ordinary levels receive\n * `{ data, shared }`; the error leaf receives the serialized `{ error, status\n * }` payload shape.\n *\n * ── THE APP LEVEL IS DELIBERATELY ABSENT, AND MUST STAY ABSENT ──────────────\n * `ClientRouteComposition.App` and `payload.appData` still exist and are still\n * carried; they are contracts owned elsewhere. They are simply not part of THIS\n * tree, because App is not part of the markup this tree hydrates against:\n *\n * - Server-side, `render-page.ts`'s `wrapRootward` wraps the page leaf in\n * `[\"layout\", \"app\"]` (`render-page.ts:274`), so the document React renders\n * is `App( Layout( Page ) )`.\n * - The app root is the level that owns `<html>`/`<body>` and renders\n * `<div id=\"root\">{children}</div>` inside the body. So App CONTAINS the\n * mount point — the markup actually inside `#root` is `Layout( Page )`.\n * - `hydrate-page.tsx` mounts at `#root` and nowhere else.\n *\n * Composing App here would therefore hydrate a whole `<html>` document inside a\n * `<div>` the server filled with a layout: a guaranteed hydration mismatch. If\n * you arrived here from the optional `App?` on the composition type and are\n * about to \"complete\" the tree with it — that would be the defect, not the\n * omission.\n *\n * `load()` is awaited exactly ONCE per hydration and its result reused for all\n * levels — the composition arrives whole, so calling it per layout would be\n * one network waterfall per level for no new information.\n */\nexport async function buildHydratedTree(\n pages: readonly ClientPageEntry[],\n payload: HydrationDocumentPayloadSource,\n): Promise<ReactNode> {\n const entry = findEntryByName(pages, payload.name);\n const composition = await loadClientRouteComposition(entry);\n const errorPageProps = payload.errorPage;\n const selectedPageModule =\n errorPageProps === undefined ? composition.Page : composition.ErrorPage;\n\n if (selectedPageModule === undefined) {\n throw new MissingHydrationErrorPageError(payload.name);\n }\n\n // Registration is the first lifecycle action after the real namespaces have\n // loaded. Keep server order: root/App, layouts outermost-to-innermost, page.\n // On the error path the selected error module replaces the ordinary Page in\n // that order; registering Page as well would run code the server did not run.\n // Component extraction and React element creation intentionally happen only\n // after every registration hook has completed synchronously.\n registerModules([\n ...(composition.App === undefined ? [] : [composition.App]),\n ...composition.layouts,\n selectedPageModule,\n ]);\n\n const { shared } = payload;\n let element: ReactNode;\n\n if (errorPageProps === undefined) {\n const Page = componentOf<HydratedPageProps>(selectedPageModule);\n element =\n Page === undefined\n ? null\n : createElement(Page, {\n data: payload.pageData,\n shared,\n params: payload.params ?? {},\n });\n } else {\n const ErrorPage = componentOf<SerializedErrorPageProps>(selectedPageModule);\n element =\n ErrorPage === undefined ? null : createElement(ErrorPage, errorPageProps);\n }\n\n // Innermost layout wraps the page, so walk the outermost-first list backwards.\n for (let index = composition.layouts.length - 1; index >= 0; index -= 1) {\n element = wrap(\n composition.layouts[index]!,\n payload.layoutData,\n shared,\n element,\n );\n }\n\n return element;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsCA,SAAS,mBAAmB,gBAA2C;CACrE,IAAI,eAAe,WAAW,GAAG,OAAO;CAExC,OAAO,uBAAuB,eAAe,KAAK,SAAS,KAAK,UAAU,IAAI,CAAC,EAAE,KAAK,IAAI,EAAE;AAC9F;;;;;;;;;;;AAYA,IAAa,gCAAb,cAAmD,MAAM;CAErC;CACA;CAFlB,AAAO,YACL,AAAgB,UAChB,AAAgB,gBAChB;EACA,MACE,qDAAqD,KAAK,UAAU,QAAQ,EAAE,8CACxC,mBAAmB,cAAc,EAAE,8OAI3E;EATgB;EACA;EAShB,KAAK,OAAO;CACd;AACF;;;;;;;AAQA,IAAa,iCAAb,cAAoD,MAAM;CACrB;CAAnC,AAAO,YAAY,AAAgB,UAAkB;EACnD,MACE,2EACK,KAAK,UAAU,QAAQ,EAAE,0IAEhC;EALiC;EAMjC,KAAK,OAAO;CACd;AACF;AAEA,SAAS,gBACP,OACA,MACiB;CACjB,MAAM,QAAQ,MAAM,MAAM,cAAc,UAAU,SAAS,IAAI;CAE/D,IAAI,UAAU,QACZ,MAAM,IAAI,8BACR,MACA,MAAM,KAAK,cAAc,UAAU,IAAI,CACzC;CAGF,OAAO;AACT;;;;;;;;;AAUA,SAAS,YACP,QACkC;CAClC,MAAM,YAAY,OAAO;CAEzB,OAAO,OAAO,cAAc,aACvB,YACD;AACN;AAEA,SAAS,KACP,QACA,MACA,QACA,UACW;CACX,MAAM,YAAY,YAAgC,MAAM;CAExD,IAAI,cAAc,QAAW,OAAO;CAEpC,OAAO,cAAc,WAAW;EAAE;EAAM;EAAQ;CAAS,CAAC;AAC5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,eAAsB,kBACpB,OACA,SACoB;CAEpB,MAAM,cAAc,MAAM,2BADZ,gBAAgB,OAAO,QAAQ,IACY,CAAC;CAC1D,MAAM,iBAAiB,QAAQ;CAC/B,MAAM,qBACJ,mBAAmB,SAAY,YAAY,OAAO,YAAY;CAEhE,IAAI,uBAAuB,QACzB,MAAM,IAAI,+BAA+B,QAAQ,IAAI;CASvD,gBAAgB;EACd,GAAI,YAAY,QAAQ,SAAY,CAAC,IAAI,CAAC,YAAY,GAAG;EACzD,GAAG,YAAY;EACf;CACF,CAAC;CAED,MAAM,EAAE,WAAW;CACnB,IAAI;CAEJ,IAAI,mBAAmB,QAAW;EAChC,MAAM,OAAO,YAA+B,kBAAkB;EAC9D,UACE,SAAS,SACL,OACA,cAAc,MAAM;GAClB,MAAM,QAAQ;GACd;GACA,QAAQ,QAAQ,UAAU,CAAC;EAC7B,CAAC;CACT,OAAO;EACL,MAAM,YAAY,YAAsC,kBAAkB;EAC1E,UACE,cAAc,SAAY,OAAO,cAAc,WAAW,cAAc;CAC5E;CAGA,KAAK,IAAI,QAAQ,YAAY,QAAQ,SAAS,GAAG,SAAS,GAAG,SAAS,GACpE,UAAU,KACR,YAAY,QAAQ,QACpB,QAAQ,YACR,QACA,OACF;CAGF,OAAO;AACT"}
1
+ {"version":3,"file":"build-hydrated-tree.mjs","names":[],"sources":["../../../../../../../web/src/client/build-hydrated-tree.ts"],"sourcesContent":["/**\n * The hydration COMPOSER: payload + page registry -> the ReactNode to mount.\n *\n * It takes the registry as an ARGUMENT and touches no browser global, which is\n * the whole point of it living apart from `index.ts`: every rule below is\n * testable with a hand-built registry, no bundler, no virtual module, no DOM.\n *\n * LOOKUP BY NAME, NEVER BY MATCH. `payload.name` is the identity of the entry\n * the SERVER matched for this exact request (document-context.ts's `name`\n * field). Re-deriving it from `location.pathname` with `matchClientRoute`\n * would be a second implementation of route semantics running against the one\n * request it is hydrating, free to disagree with the server that produced the\n * markup. `matchClientRoute` is for client-side NAVIGATION, where no server\n * answer exists yet.\n */\nimport { createElement, type ComponentType, type ReactNode } from \"react\";\nimport type {\n HydrationDocumentPayloadSource,\n SerializedErrorPageProps,\n} from \"../hydration-payload\";\nimport { registerModules } from \"../runtime/register-modules\";\nimport { loadClientRouteComposition } from \"./runtime\";\nimport type { ClientPageEntry, ClientProjectedModule } from \"./runtime/types\";\n\n/** What every composed level receives — the shape `render-page.ts` uses server-side. */\ntype HydratedLevelProps = {\n readonly data: unknown;\n readonly shared: unknown;\n readonly children?: ReactNode;\n};\n\n/** The ordinary page leaf alone receives params from the server's match. */\ntype HydratedPageProps = {\n readonly data: unknown;\n readonly shared: unknown;\n readonly params: Readonly<Record<string, string>>;\n};\n\nfunction describeKnownNames(knownPageNames: readonly string[]): string {\n if (knownPageNames.length === 0) return \"The client page registry is empty.\";\n\n return `The registry knows: ${knownPageNames.map((name) => JSON.stringify(name)).join(\", \")}.`;\n}\n\n/**\n * The THIRD hydration failure case, beside an absent and a malformed payload.\n *\n * It fails CLOSED — no default entry, no nearest-path fallback, no silent\n * no-op. A registry that quietly substitutes a page produces a browser showing\n * one page's markup running another page's code, which is precisely the defect\n * this entry point was rewritten to remove; a fallback would reintroduce it\n * wearing a recovery costume. Throwing leaves the server-rendered markup on\n * screen and un-hydrated, which is degraded but honest.\n */\nexport class UnknownHydrationPageNameError extends Error {\n public constructor(\n public readonly pageName: string,\n public readonly knownPageNames: readonly string[],\n ) {\n super(\n `Warlock hydration aborted: the payload names page ${JSON.stringify(pageName)}, which is ` +\n `not in the client page registry. ${describeKnownNames(knownPageNames)} The server ` +\n \"rendered a page this browser bundle does not carry, so the server and client were \" +\n \"built from different page graphs. To fix: rebuild the client bundle, or check that \" +\n \"the page's file still exports a `route` discovery can see.\",\n );\n this.name = \"UnknownHydrationPageNameError\";\n }\n}\n\n/**\n * The server selected an app error page, but this browser graph cannot load it.\n * Substituting the ordinary page would execute the component that already\n * failed and hydrate markup the server did not render, so this path fails\n * closed just like an unknown route name.\n */\nexport class MissingHydrationErrorPageError extends Error {\n public constructor(public readonly pageName: string) {\n super(\n `Warlock hydration aborted: the server selected error.page.tsx for route ` +\n `${JSON.stringify(pageName)}, but that route's client composition has no ErrorPage ` +\n \"module. Rebuild the client page registry so it projects the discovered error page.\",\n );\n this.name = \"MissingHydrationErrorPageError\";\n }\n}\n\nfunction findEntryByName(\n pages: readonly ClientPageEntry[],\n name: string,\n): ClientPageEntry {\n const entry = pages.find((candidate) => candidate.name === name);\n\n if (entry === undefined) {\n throw new UnknownHydrationPageNameError(\n name,\n pages.map((candidate) => candidate.name),\n );\n }\n\n return entry;\n}\n\n/**\n * A level's component, or undefined when the module exports no default.\n *\n * Undefined is NOT an error: `render-page.ts:258` and `:279` treat a missing\n * default as a passthrough server-side, and the client tree has to match the\n * markup React is hydrating against — introducing a level here that the server\n * did not render is a hydration mismatch, not a repair.\n */\nfunction componentOf<Props extends object>(\n module: ClientProjectedModule,\n): ComponentType<Props> | undefined {\n const component = module.default;\n\n return typeof component === \"function\"\n ? (component as ComponentType<Props>)\n : undefined;\n}\n\nfunction wrap(\n module: ClientProjectedModule,\n data: unknown,\n shared: unknown,\n children: ReactNode,\n): ReactNode {\n const Component = componentOf<HydratedLevelProps>(module);\n\n if (Component === undefined) return children;\n\n return createElement(Component, { data, shared, children });\n}\n\n/**\n * Compose the tree the server rendered inside `#root`: ordered layouts wrapping\n * the selected Page or ErrorPage leaf, layouts OUTERMOST FIRST as\n * `ClientRouteComposition` declares them. Ordinary levels receive\n * `{ data, shared }`; the error leaf receives the serialized `{ error, status\n * }` payload shape.\n *\n * ── THE APP LEVEL IS DELIBERATELY ABSENT, AND MUST STAY ABSENT ──────────────\n * `ClientRouteComposition.App` and `payload.appData` still exist and are still\n * carried; they are contracts owned elsewhere. They are simply not part of THIS\n * tree, because App is not part of the markup this tree hydrates against:\n *\n * - Server-side, `render-page.ts`'s `wrapRootward` wraps the page leaf in\n * `[\"layout\", \"app\"]` (`render-page.ts:274`), so the document React renders\n * is `App( Layout( Page ) )`.\n * - The app root is the level that owns `<html>`/`<body>` and renders\n * `<div id=\"root\">{children}</div>` inside the body. So App CONTAINS the\n * mount point — the markup actually inside `#root` is `Layout( Page )`.\n * - `hydrate-page.tsx` mounts at `#root` and nowhere else.\n *\n * Composing App here would therefore hydrate a whole `<html>` document inside a\n * `<div>` the server filled with a layout: a guaranteed hydration mismatch. If\n * you arrived here from the optional `App?` on the composition type and are\n * about to \"complete\" the tree with it — that would be the defect, not the\n * omission.\n *\n * `load()` is awaited exactly ONCE per hydration and its result reused for all\n * levels — the composition arrives whole, so calling it per layout would be\n * one network waterfall per level for no new information.\n */\nexport async function buildHydratedTree(\n pages: readonly ClientPageEntry[],\n payload: HydrationDocumentPayloadSource,\n): Promise<ReactNode> {\n const entry = findEntryByName(pages, payload.name);\n const composition = await loadClientRouteComposition(entry);\n const errorPageProps = payload.errorPage;\n const selectedPageModule =\n errorPageProps === undefined ? composition.Page : composition.ErrorPage;\n\n if (selectedPageModule === undefined) {\n throw new MissingHydrationErrorPageError(payload.name);\n }\n\n // Registration is the first lifecycle action after the real namespaces have\n // loaded. Keep server order: root/App, layouts outermost-to-innermost, page.\n // On the error path the selected error module replaces the ordinary Page in\n // that order; registering Page as well would run code the server did not run.\n // Component extraction and React element creation intentionally happen only\n // after every registration hook has completed synchronously.\n registerModules([\n ...(composition.App === undefined ? [] : [composition.App]),\n ...composition.layouts,\n selectedPageModule,\n ]);\n\n const { shared } = payload;\n let element: ReactNode;\n\n if (errorPageProps === undefined) {\n const Page = componentOf<HydratedPageProps>(selectedPageModule);\n element =\n Page === undefined\n ? null\n : createElement(Page, {\n data: payload.pageData,\n shared,\n params: payload.params ?? {},\n });\n } else {\n const ErrorPage = componentOf<SerializedErrorPageProps>(selectedPageModule);\n element =\n ErrorPage === undefined ? null : createElement(ErrorPage, errorPageProps);\n }\n\n // Innermost layout wraps the page, so walk the outermost-first list backwards.\n for (let index = composition.layouts.length - 1; index >= 0; index -= 1) {\n element = wrap(\n composition.layouts[index]!,\n payload.layoutData,\n shared,\n element,\n );\n }\n\n return element;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsCA,SAAS,mBAAmB,gBAA2C;CACrE,IAAI,eAAe,WAAW,GAAG,OAAO;CAExC,OAAO,uBAAuB,eAAe,KAAK,SAAS,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE;AAC9F;;;;;;;;;;;AAYA,IAAa,gCAAb,cAAmD,MAAM;CAErC;CACA;CAFlB,AAAO,YACL,AAAgB,UAChB,AAAgB,gBAChB;EACA,MACE,qDAAqD,KAAK,UAAU,QAAQ,EAAE,8CACxC,mBAAmB,cAAc,EAAE,8OAI3E;EATgB;EACA;EAShB,KAAK,OAAO;CACd;AACF;;;;;;;AAQA,IAAa,iCAAb,cAAoD,MAAM;CACrB;CAAnC,AAAO,YAAY,AAAgB,UAAkB;EACnD,MACE,2EACK,KAAK,UAAU,QAAQ,EAAE,0IAEhC;EALiC;EAMjC,KAAK,OAAO;CACd;AACF;AAEA,SAAS,gBACP,OACA,MACiB;CACjB,MAAM,QAAQ,MAAM,MAAM,cAAc,UAAU,SAAS,IAAI;CAE/D,IAAI,UAAU,QACZ,MAAM,IAAI,8BACR,MACA,MAAM,KAAK,cAAc,UAAU,IAAI,CACzC;CAGF,OAAO;AACT;;;;;;;;;AAUA,SAAS,YACP,QACkC;CAClC,MAAM,YAAY,OAAO;CAEzB,OAAO,OAAO,cAAc,aACvB,YACD;AACN;AAEA,SAAS,KACP,QACA,MACA,QACA,UACW;CACX,MAAM,YAAY,YAAgC,MAAM;CAExD,IAAI,cAAc,QAAW,OAAO;CAEpC,OAAO,cAAc,WAAW;EAAE;EAAM;EAAQ;CAAS,CAAC;AAC5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,eAAsB,kBACpB,OACA,SACoB;CAEpB,MAAM,cAAc,MAAM,2BADZ,gBAAgB,OAAO,QAAQ,IACY,CAAC;CAC1D,MAAM,iBAAiB,QAAQ;CAC/B,MAAM,qBACJ,mBAAmB,SAAY,YAAY,OAAO,YAAY;CAEhE,IAAI,uBAAuB,QACzB,MAAM,IAAI,+BAA+B,QAAQ,IAAI;CASvD,gBAAgB;EACd,GAAI,YAAY,QAAQ,SAAY,CAAC,IAAI,CAAC,YAAY,GAAG;EACzD,GAAG,YAAY;EACf;CACF,CAAC;CAED,MAAM,EAAE,WAAW;CACnB,IAAI;CAEJ,IAAI,mBAAmB,QAAW;EAChC,MAAM,OAAO,YAA+B,kBAAkB;EAC9D,UACE,SAAS,SACL,OACA,cAAc,MAAM;GAClB,MAAM,QAAQ;GACd;GACA,QAAQ,QAAQ,UAAU,CAAC;EAC7B,CAAC;CACT,OAAO;EACL,MAAM,YAAY,YAAsC,kBAAkB;EAC1E,UACE,cAAc,SAAY,OAAO,cAAc,WAAW,cAAc;CAC5E;CAGA,KAAK,IAAI,QAAQ,YAAY,QAAQ,SAAS,GAAG,SAAS,GAAG,SAAS,GACpE,UAAU,KACR,YAAY,QAAQ,QACpB,QAAQ,YACR,QACA,OACF;CAGF,OAAO;AACT"}
@@ -1,8 +1,8 @@
1
1
  import { hydrateShared } from "../shared.mjs";
2
2
  import { DocumentContext } from "../components/document-context.mjs";
3
3
  import { readHydrationPayload } from "../hydration-payload.mjs";
4
- import { hydrateRoot } from "react-dom/client";
5
4
  import { jsx } from "react/jsx-runtime";
5
+ import { hydrateRoot } from "react-dom/client";
6
6
 
7
7
  //#region ../web/src/client/hydrate-page.tsx
8
8
  /**
@@ -31,8 +31,9 @@ function reportHydrationFailure(error) {
31
31
  /**
32
32
  * The one hydration entry point. Mounts at `#root` only — the page subtree —
33
33
  * never `document`/`html`/`head`/`body` (hydration-payload-contract-2026-08-22.md
34
- * §2): `metadata`, `lang`, `dir`, `nonce` are not in the client payload and are
35
- * never touched here. `readHydrationPayload` (web/src/hydration-payload.ts,
34
+ * §2): `metadata`, `dir`, and `nonce` are not used to rebuild the mounted tree;
35
+ * the declared `locale` key is consumed by `NavigationRoot`'s provider.
36
+ * `readHydrationPayload` (web/src/hydration-payload.ts,
36
37
  * Vega's slice B) is the one place ABSENT/MALFORMED are decided, so this
37
38
  * function does not re-implement that check — reusing it is what keeps the
38
39
  * two throw messages from drifting apart at a second site. On ABSENT/MALFORMED
@@ -1 +1 @@
1
- {"version":3,"file":"hydrate-page.mjs","names":[],"sources":["../../../../../../../web/src/client/hydrate-page.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { DocumentContext, type DocumentContextValue } from \"../components/document-context\";\nimport { readHydrationPayload, type HydrationDocumentPayloadSource } from \"../hydration-payload\";\nimport { hydrateShared } from \"../shared\";\n\n/**\n * The hydration MOUNT point — a different id from the payload script's id.\n * Not exported anywhere as a named constant (`default-app.tsx:39` only\n * renders the literal `<div id=\"root\">`), so a local literal is fine here:\n * the contract's no-duplicate-literal rule is specifically about the payload\n * script id, which `readHydrationPayload` already owns exclusively\n * (hydration-payload-contract-2026-08-22.md §4).\n */\nconst MOUNT_ELEMENT_ID = \"root\";\n\n/**\n * Receives the VALIDATED payload and returns the ReactNode to hydrate. A\n * callback rather than a ready-made ReactNode: composing the real Layout(Page)\n * tree needs `payload.layoutData`/`pageData`/`shared`, which only exist after\n * `readHydrationPayload()` has already succeeded — building the tree first and\n * validating second would get the order backwards.\n *\n * A Promise is allowed because the real composer resolves the page's chunk\n * through the registry's dynamic `import()`. Returning it does NOT move the\n * payload check later: the callback is still only reached once the payload has\n * validated, and only the tree it produces is awaited.\n */\nexport type BuildHydratedTree = (\n payload: HydrationDocumentPayloadSource,\n) => ReactNode | Promise<ReactNode>;\n\nfunction isPromise(value: ReactNode | Promise<ReactNode>): value is Promise<ReactNode> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { then?: unknown }).then === \"function\"\n );\n}\n\n/**\n * The awaited path's failure sink. By the time a `buildTree` promise rejects\n * the synchronous stack is gone, so there is no caller left to throw at — and\n * an un-attached rejection is a blank console, which is the silent-failure\n * class this pipeline keeps regressing into. Reported loudly, and deliberately\n * WITHOUT touching `#root`: the server's markup is correct and visible, it is\n * only un-hydrated, so clearing it would turn a degraded page into a blank one.\n */\nfunction reportHydrationFailure(error: unknown): void {\n console.error(\n \"Warlock hydration failed after the page tree was requested. The server-rendered \" +\n \"markup is left on screen un-hydrated; #root was not cleared.\",\n error,\n );\n}\n\n/**\n * The one hydration entry point. Mounts at `#root` only — the page subtree —\n * never `document`/`html`/`head`/`body` (hydration-payload-contract-2026-08-22.md\n * §2): `metadata`, `lang`, `dir`, `nonce` are not in the client payload and are\n * never touched here. `readHydrationPayload` (web/src/hydration-payload.ts,\n * Vega's slice B) is the one place ABSENT/MALFORMED are decided, so this\n * function does not re-implement that check — reusing it is what keeps the\n * two throw messages from drifting apart at a second site. On ABSENT/MALFORMED\n * it throws before touching `#root`, so the server-rendered markup stays\n * visible; nothing is cleared or re-rendered.\n *\n * Order is load-bearing and unchanged by the async tree: payload validated,\n * shared snapshot installed, `#root` resolved and its absence thrown on — all\n * SYNCHRONOUSLY, so those three failures still reject the call itself — and\n * only then is the tree built and, if it is a promise, awaited. Nothing is\n * cleared on any failure path.\n */\nexport function hydratePage(buildTree: BuildHydratedTree): void {\n const payload = readHydrationPayload(document);\n\n hydrateShared(payload.shared);\n\n const mountElement = document.getElementById(MOUNT_ELEMENT_ID);\n\n if (mountElement === null) {\n throw new Error(\n `Warlock hydration aborted: no element with id \"${MOUNT_ELEMENT_ID}\" was found. The ` +\n 'server is expected to render <div id=\"root\"> as the hydration mount point ' +\n \"(web/src/components/default-app.tsx:39).\",\n );\n }\n\n const value: DocumentContextValue = { metadata: undefined, payload };\n\n const mount = (tree: ReactNode): void => {\n hydrateRoot(\n mountElement,\n <DocumentContext.Provider value={value}>{tree}</DocumentContext.Provider>,\n );\n };\n\n const tree = buildTree(payload);\n\n if (isPromise(tree)) {\n // `void` on an ALREADY-handled chain: the rejection handler is attached\n // here, so nothing escapes as an unhandled rejection.\n void tree.then(mount, reportHydrationFailure);\n\n return;\n }\n\n mount(tree);\n}"],"mappings":";;;;;;;;;;;;;;;AAcA,MAAM,mBAAmB;AAkBzB,SAAS,UAAU,OAAoE;CACrF,OACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAA6B,SAAS;AAElD;;;;;;;;;AAUA,SAAS,uBAAuB,OAAsB;CACpD,QAAQ,MACN,gJAEA,KACF;AACF;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,YAAY,WAAoC;CAC9D,MAAM,UAAU,qBAAqB,QAAQ;CAE7C,cAAc,QAAQ,MAAM;CAE5B,MAAM,eAAe,SAAS,eAAe,gBAAgB;CAE7D,IAAI,iBAAiB,MACnB,MAAM,IAAI,MACR,kDAAkD,iBAAiB,oIAGrE;CAGF,MAAM,QAA8B;EAAE,UAAU;EAAW;CAAQ;CAEnE,MAAM,SAAS,SAA0B;EACvC,YACE,cACA,oBAAC,gBAAgB,UAAjB;GAAiC;aAAQ;EAA+B,EAC1E;CACF;CAEA,MAAM,OAAO,UAAU,OAAO;CAE9B,IAAI,UAAU,IAAI,GAAG;EAGnB,AAAK,KAAK,KAAK,OAAO,sBAAsB;EAE5C;CACF;CAEA,MAAM,IAAI;AACZ"}
1
+ {"version":3,"file":"hydrate-page.mjs","names":[],"sources":["../../../../../../../web/src/client/hydrate-page.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { DocumentContext, type DocumentContextValue } from \"../components/document-context\";\nimport { readHydrationPayload, type HydrationDocumentPayloadSource } from \"../hydration-payload\";\nimport { hydrateShared } from \"../shared\";\n\n/**\n * The hydration MOUNT point — a different id from the payload script's id.\n * Not exported anywhere as a named constant (`default-app.tsx:39` only\n * renders the literal `<div id=\"root\">`), so a local literal is fine here:\n * the contract's no-duplicate-literal rule is specifically about the payload\n * script id, which `readHydrationPayload` already owns exclusively\n * (hydration-payload-contract-2026-08-22.md §4).\n */\nconst MOUNT_ELEMENT_ID = \"root\";\n\n/**\n * Receives the VALIDATED payload and returns the ReactNode to hydrate. A\n * callback rather than a ready-made ReactNode: composing the real Layout(Page)\n * tree needs `payload.layoutData`/`pageData`/`shared`, which only exist after\n * `readHydrationPayload()` has already succeeded — building the tree first and\n * validating second would get the order backwards.\n *\n * A Promise is allowed because the real composer resolves the page's chunk\n * through the registry's dynamic `import()`. Returning it does NOT move the\n * payload check later: the callback is still only reached once the payload has\n * validated, and only the tree it produces is awaited.\n */\nexport type BuildHydratedTree = (\n payload: HydrationDocumentPayloadSource,\n) => ReactNode | Promise<ReactNode>;\n\nfunction isPromise(value: ReactNode | Promise<ReactNode>): value is Promise<ReactNode> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { then?: unknown }).then === \"function\"\n );\n}\n\n/**\n * The awaited path's failure sink. By the time a `buildTree` promise rejects\n * the synchronous stack is gone, so there is no caller left to throw at — and\n * an un-attached rejection is a blank console, which is the silent-failure\n * class this pipeline keeps regressing into. Reported loudly, and deliberately\n * WITHOUT touching `#root`: the server's markup is correct and visible, it is\n * only un-hydrated, so clearing it would turn a degraded page into a blank one.\n */\nfunction reportHydrationFailure(error: unknown): void {\n console.error(\n \"Warlock hydration failed after the page tree was requested. The server-rendered \" +\n \"markup is left on screen un-hydrated; #root was not cleared.\",\n error,\n );\n}\n\n/**\n * The one hydration entry point. Mounts at `#root` only — the page subtree —\n * never `document`/`html`/`head`/`body` (hydration-payload-contract-2026-08-22.md\n * §2): `metadata`, `dir`, and `nonce` are not used to rebuild the mounted tree;\n * the declared `locale` key is consumed by `NavigationRoot`'s provider.\n * `readHydrationPayload` (web/src/hydration-payload.ts,\n * Vega's slice B) is the one place ABSENT/MALFORMED are decided, so this\n * function does not re-implement that check — reusing it is what keeps the\n * two throw messages from drifting apart at a second site. On ABSENT/MALFORMED\n * it throws before touching `#root`, so the server-rendered markup stays\n * visible; nothing is cleared or re-rendered.\n *\n * Order is load-bearing and unchanged by the async tree: payload validated,\n * shared snapshot installed, `#root` resolved and its absence thrown on — all\n * SYNCHRONOUSLY, so those three failures still reject the call itself — and\n * only then is the tree built and, if it is a promise, awaited. Nothing is\n * cleared on any failure path.\n */\nexport function hydratePage(buildTree: BuildHydratedTree): void {\n const payload = readHydrationPayload(document);\n\n hydrateShared(payload.shared);\n\n const mountElement = document.getElementById(MOUNT_ELEMENT_ID);\n\n if (mountElement === null) {\n throw new Error(\n `Warlock hydration aborted: no element with id \"${MOUNT_ELEMENT_ID}\" was found. The ` +\n 'server is expected to render <div id=\"root\"> as the hydration mount point ' +\n \"(web/src/components/default-app.tsx:39).\",\n );\n }\n\n const value: DocumentContextValue = { metadata: undefined, payload };\n\n const mount = (tree: ReactNode): void => {\n hydrateRoot(\n mountElement,\n <DocumentContext.Provider value={value}>{tree}</DocumentContext.Provider>,\n );\n };\n\n const tree = buildTree(payload);\n\n if (isPromise(tree)) {\n // `void` on an ALREADY-handled chain: the rejection handler is attached\n // here, so nothing escapes as an unhandled rejection.\n void tree.then(mount, reportHydrationFailure);\n\n return;\n }\n\n mount(tree);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,MAAM,mBAAmB;AAkBzB,SAAS,UAAU,OAAoE;CACrF,OACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAA6B,SAAS;AAElD;;;;;;;;;AAUA,SAAS,uBAAuB,OAAsB;CACpD,QAAQ,MACN,gJAEA,KACF;AACF;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,YAAY,WAAoC;CAC9D,MAAM,UAAU,qBAAqB,QAAQ;CAE7C,cAAc,QAAQ,MAAM;CAE5B,MAAM,eAAe,SAAS,eAAe,gBAAgB;CAE7D,IAAI,iBAAiB,MACnB,MAAM,IAAI,MACR,kDAAkD,iBAAiB,oIAGrE;CAGF,MAAM,QAA8B;EAAE,UAAU;EAAW;CAAQ;CAEnE,MAAM,SAAS,SAA0B;EACvC,YACE,cACA,oBAAC,gBAAgB,UAAjB;GAAiC;aAAQ;EAA+B,EAC1E;CACF;CAEA,MAAM,OAAO,UAAU,OAAO;CAE9B,IAAI,UAAU,IAAI,GAAG;EAGnB,AAAK,KAAK,KAAK,OAAO,sBAAsB;EAE5C;CACF;CAEA,MAAM,IAAI;AACZ"}
@@ -40,7 +40,7 @@ function isPayloadResponse(response) {
40
40
  * page-defined and may legitimately be anything, including `null`.
41
41
  */
42
42
  function isPayloadShape(value) {
43
- return typeof value === "object" && value !== null && typeof value.name === "string";
43
+ return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.locale === "string" && value.locale.length > 0;
44
44
  }
45
45
  async function fetchPageData(url) {
46
46
  let response;
@@ -83,7 +83,7 @@ async function fetchPageData(url) {
83
83
  if (!isPayloadShape(parsed)) return {
84
84
  type: "hard-navigate",
85
85
  url,
86
- reason: "payload has no route name"
86
+ reason: "payload is missing required navigation identity"
87
87
  };
88
88
  return {
89
89
  type: "payload",
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-page-data.mjs","names":[],"sources":["../../../../../../../../web/src/client/navigation/fetch-page-data.ts"],"sourcesContent":["/**\n * Ask the server for a URL's page data instead of its document.\n *\n * This is the browser half of the representation split: the same route the user\n * would have loaded, asked for as JSON via the `x-warlock-data` marker. What\n * comes back is exactly the payload a full page load embeds, so the caller can\n * rebuild the tree from it with no second code path.\n *\n * ## Every failure degrades to a REAL navigation, never to an error screen\n *\n * A client navigation is an OPTIMISATION over what the browser already does\n * perfectly well. So nothing here reports a failure to the user — it reports\n * `hard-navigate`, and the caller hands the URL back to the browser. The user\n * gets the page; they just get it the slow way.\n *\n * That is what makes the whole feature safe to add: the worst case of a bug in\n * this file is the behaviour we had before the file existed. Rendering our own\n * \"navigation failed\" state would be strictly worse than the fallback we\n * already have, and would turn every unhandled edge — an auth redirect to an\n * external IdP, a maintenance page, a proxy that strips the header, a deploy\n * that changed the payload shape mid-session — into a dead end.\n */\nimport {\n DATA_RESPONSE_CONTENT_TYPE,\n WARLOCK_DATA_REQUEST_HEADER,\n WARLOCK_DATA_REQUEST_VALUE,\n} from \"../../routing/data-request\";\nimport type { HydrationDocumentPayloadSource } from \"../../hydration-payload\";\n\nexport type PageDataResult =\n | {\n type: \"payload\";\n /**\n * The payload to rebuild the tree from.\n */\n payload: HydrationDocumentPayloadSource;\n /**\n * The URL the response actually came from — NOT the one requested. A\n * redirect is followed by `fetch` transparently, so a login-required page\n * answers from `/login`, and pushing the requested URL into history would\n * leave the address bar lying about what is on screen.\n */\n url: string;\n }\n | {\n type: \"hard-navigate\";\n url: string;\n /** Why, for a console warning — never shown to the user. */\n reason: string;\n };\n\n/**\n * Whether the body is the payload we asked for.\n *\n * Checked rather than assumed because a 200 does not mean \"this came from the\n * page pipeline\": a captive portal, an SSO interstitial or a proxy error page\n * all answer 200 with HTML. Parsing that as JSON would throw; treating a\n * successful parse of *something else* as a payload would render garbage.\n */\nfunction isPayloadResponse(response: Response): boolean {\n return (response.headers.get(\"content-type\") ?? \"\").includes(DATA_RESPONSE_CONTENT_TYPE);\n}\n\n/**\n * The shape check, kept deliberately narrow: `name` is the only field the tree\n * builder cannot proceed without — it selects the page. The data fields are\n * page-defined and may legitimately be anything, including `null`.\n */\nfunction isPayloadShape(value: unknown): value is HydrationDocumentPayloadSource {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { name?: unknown }).name === \"string\"\n );\n}\n\nexport async function fetchPageData(url: string): Promise<PageDataResult> {\n let response: Response;\n\n try {\n response = await fetch(url, {\n headers: {\n [WARLOCK_DATA_REQUEST_HEADER]: WARLOCK_DATA_REQUEST_VALUE,\n accept: DATA_RESPONSE_CONTENT_TYPE,\n },\n // Same-origin credentials so a navigation carries the session exactly as\n // a document request would. Without this a client navigation could be\n // logged out while a full load of the same URL is not.\n credentials: \"same-origin\",\n // Redirects are FOLLOWED, not intercepted: the marker header is re-sent,\n // so the destination answers with a payload too, and `response.url` tells\n // us where we ended up. Handling redirects ourselves would mean\n // re-implementing the rules the browser already has.\n redirect: \"follow\",\n });\n } catch (error) {\n // Offline, DNS, CORS, an aborted connection. The browser can render its own\n // network error far better than we can fake one.\n return { type: \"hard-navigate\", url, reason: `request failed: ${String(error)}` };\n }\n\n if (!response.ok) {\n // 404, 500, 403 — all of these have a real page the server renders. Letting\n // the browser load it gets the correct status AND the correct document,\n // rather than us inventing a client-side error state that the server's own\n // error page already covers.\n return { type: \"hard-navigate\", url, reason: `status ${response.status}` };\n }\n\n if (!isPayloadResponse(response)) {\n return {\n type: \"hard-navigate\",\n url,\n reason: `unexpected content-type \"${response.headers.get(\"content-type\") ?? \"none\"}\"`,\n };\n }\n\n let parsed: unknown;\n\n try {\n parsed = await response.json();\n } catch (error) {\n return { type: \"hard-navigate\", url, reason: `malformed JSON: ${String(error)}` };\n }\n\n if (!isPayloadShape(parsed)) {\n return { type: \"hard-navigate\", url, reason: \"payload has no route name\" };\n }\n\n // `response.url` is absolute and reflects any redirect that was followed.\n // Falling back to the requested URL keeps this working under test doubles\n // that do not set it.\n return { type: \"payload\", payload: parsed, url: response.url || url };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,SAAS,kBAAkB,UAA6B;CACtD,QAAQ,SAAS,QAAQ,IAAI,cAAc,KAAK,IAAI,SAAS,0BAA0B;AACzF;;;;;;AAOA,SAAS,eAAe,OAAyD;CAC/E,OACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAA6B,SAAS;AAElD;AAEA,eAAsB,cAAc,KAAsC;CACxE,IAAI;CAEJ,IAAI;EACF,WAAW,MAAM,MAAM,KAAK;GAC1B,SAAS;KACN;IACD,QAAQ;GACV;GAIA,aAAa;GAKb,UAAU;EACZ,CAAC;CACH,SAAS,OAAO;EAGd,OAAO;GAAE,MAAM;GAAiB;GAAK,QAAQ,mBAAmB,OAAO,KAAK;EAAI;CAClF;CAEA,IAAI,CAAC,SAAS,IAKZ,OAAO;EAAE,MAAM;EAAiB;EAAK,QAAQ,UAAU,SAAS;CAAS;CAG3E,IAAI,CAAC,kBAAkB,QAAQ,GAC7B,OAAO;EACL,MAAM;EACN;EACA,QAAQ,4BAA4B,SAAS,QAAQ,IAAI,cAAc,KAAK,OAAO;CACrF;CAGF,IAAI;CAEJ,IAAI;EACF,SAAS,MAAM,SAAS,KAAK;CAC/B,SAAS,OAAO;EACd,OAAO;GAAE,MAAM;GAAiB;GAAK,QAAQ,mBAAmB,OAAO,KAAK;EAAI;CAClF;CAEA,IAAI,CAAC,eAAe,MAAM,GACxB,OAAO;EAAE,MAAM;EAAiB;EAAK,QAAQ;CAA4B;CAM3E,OAAO;EAAE,MAAM;EAAW,SAAS;EAAQ,KAAK,SAAS,OAAO;CAAI;AACtE"}
1
+ {"version":3,"file":"fetch-page-data.mjs","names":[],"sources":["../../../../../../../../web/src/client/navigation/fetch-page-data.ts"],"sourcesContent":["/**\n * Ask the server for a URL's page data instead of its document.\n *\n * This is the browser half of the representation split: the same route the user\n * would have loaded, asked for as JSON via the `x-warlock-data` marker. What\n * comes back is exactly the payload a full page load embeds, so the caller can\n * rebuild the tree from it with no second code path.\n *\n * ## Every failure degrades to a REAL navigation, never to an error screen\n *\n * A client navigation is an OPTIMISATION over what the browser already does\n * perfectly well. So nothing here reports a failure to the user — it reports\n * `hard-navigate`, and the caller hands the URL back to the browser. The user\n * gets the page; they just get it the slow way.\n *\n * That is what makes the whole feature safe to add: the worst case of a bug in\n * this file is the behaviour we had before the file existed. Rendering our own\n * \"navigation failed\" state would be strictly worse than the fallback we\n * already have, and would turn every unhandled edge — an auth redirect to an\n * external IdP, a maintenance page, a proxy that strips the header, a deploy\n * that changed the payload shape mid-session — into a dead end.\n */\nimport {\n DATA_RESPONSE_CONTENT_TYPE,\n WARLOCK_DATA_REQUEST_HEADER,\n WARLOCK_DATA_REQUEST_VALUE,\n} from \"../../routing/data-request\";\nimport type { HydrationDocumentPayloadSource } from \"../../hydration-payload\";\n\nexport type PageDataResult =\n | {\n type: \"payload\";\n /**\n * The payload to rebuild the tree from.\n */\n payload: HydrationDocumentPayloadSource;\n /**\n * The URL the response actually came from — NOT the one requested. A\n * redirect is followed by `fetch` transparently, so a login-required page\n * answers from `/login`, and pushing the requested URL into history would\n * leave the address bar lying about what is on screen.\n */\n url: string;\n }\n | {\n type: \"hard-navigate\";\n url: string;\n /** Why, for a console warning — never shown to the user. */\n reason: string;\n };\n\n/**\n * Whether the body is the payload we asked for.\n *\n * Checked rather than assumed because a 200 does not mean \"this came from the\n * page pipeline\": a captive portal, an SSO interstitial or a proxy error page\n * all answer 200 with HTML. Parsing that as JSON would throw; treating a\n * successful parse of *something else* as a payload would render garbage.\n */\nfunction isPayloadResponse(response: Response): boolean {\n return (response.headers.get(\"content-type\") ?? \"\").includes(DATA_RESPONSE_CONTENT_TYPE);\n}\n\n/**\n * The shape check, kept deliberately narrow: `name` is the only field the tree\n * builder cannot proceed without — it selects the page. The data fields are\n * page-defined and may legitimately be anything, including `null`.\n */\nfunction isPayloadShape(value: unknown): value is HydrationDocumentPayloadSource {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { name?: unknown }).name === \"string\" &&\n typeof (value as { locale?: unknown }).locale === \"string\" &&\n (value as { locale: string }).locale.length > 0\n );\n}\n\nexport async function fetchPageData(url: string): Promise<PageDataResult> {\n let response: Response;\n\n try {\n response = await fetch(url, {\n headers: {\n [WARLOCK_DATA_REQUEST_HEADER]: WARLOCK_DATA_REQUEST_VALUE,\n accept: DATA_RESPONSE_CONTENT_TYPE,\n },\n // Same-origin credentials so a navigation carries the session exactly as\n // a document request would. Without this a client navigation could be\n // logged out while a full load of the same URL is not.\n credentials: \"same-origin\",\n // Redirects are FOLLOWED, not intercepted: the marker header is re-sent,\n // so the destination answers with a payload too, and `response.url` tells\n // us where we ended up. Handling redirects ourselves would mean\n // re-implementing the rules the browser already has.\n redirect: \"follow\",\n });\n } catch (error) {\n // Offline, DNS, CORS, an aborted connection. The browser can render its own\n // network error far better than we can fake one.\n return { type: \"hard-navigate\", url, reason: `request failed: ${String(error)}` };\n }\n\n if (!response.ok) {\n // 404, 500, 403 — all of these have a real page the server renders. Letting\n // the browser load it gets the correct status AND the correct document,\n // rather than us inventing a client-side error state that the server's own\n // error page already covers.\n return { type: \"hard-navigate\", url, reason: `status ${response.status}` };\n }\n\n if (!isPayloadResponse(response)) {\n return {\n type: \"hard-navigate\",\n url,\n reason: `unexpected content-type \"${response.headers.get(\"content-type\") ?? \"none\"}\"`,\n };\n }\n\n let parsed: unknown;\n\n try {\n parsed = await response.json();\n } catch (error) {\n return { type: \"hard-navigate\", url, reason: `malformed JSON: ${String(error)}` };\n }\n\n if (!isPayloadShape(parsed)) {\n // Deliberately recover through a full document load: malformed navigation\n // data must not crash the client when the server can still render the URL.\n return {\n type: \"hard-navigate\",\n url,\n reason: \"payload is missing required navigation identity\",\n };\n }\n\n // `response.url` is absolute and reflects any redirect that was followed.\n // Falling back to the requested URL keeps this working under test doubles\n // that do not set it.\n return { type: \"payload\", payload: parsed, url: response.url || url };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,SAAS,kBAAkB,UAA6B;CACtD,QAAQ,SAAS,QAAQ,IAAI,cAAc,KAAK,GAAE,CAAE,SAAS,0BAA0B;AACzF;;;;;;AAOA,SAAS,eAAe,OAAyD;CAC/E,OACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAA6B,SAAS,YAC9C,OAAQ,MAA+B,WAAW,YACjD,MAA6B,OAAO,SAAS;AAElD;AAEA,eAAsB,cAAc,KAAsC;CACxE,IAAI;CAEJ,IAAI;EACF,WAAW,MAAM,MAAM,KAAK;GAC1B,SAAS;KACN;IACD,QAAQ;GACV;GAIA,aAAa;GAKb,UAAU;EACZ,CAAC;CACH,SAAS,OAAO;EAGd,OAAO;GAAE,MAAM;GAAiB;GAAK,QAAQ,mBAAmB,OAAO,KAAK;EAAI;CAClF;CAEA,IAAI,CAAC,SAAS,IAKZ,OAAO;EAAE,MAAM;EAAiB;EAAK,QAAQ,UAAU,SAAS;CAAS;CAG3E,IAAI,CAAC,kBAAkB,QAAQ,GAC7B,OAAO;EACL,MAAM;EACN;EACA,QAAQ,4BAA4B,SAAS,QAAQ,IAAI,cAAc,KAAK,OAAO;CACrF;CAGF,IAAI;CAEJ,IAAI;EACF,SAAS,MAAM,SAAS,KAAK;CAC/B,SAAS,OAAO;EACd,OAAO;GAAE,MAAM;GAAiB;GAAK,QAAQ,mBAAmB,OAAO,KAAK;EAAI;CAClF;CAEA,IAAI,CAAC,eAAe,MAAM,GAGxB,OAAO;EACL,MAAM;EACN;EACA,QAAQ;CACV;CAMF,OAAO;EAAE,MAAM;EAAW,SAAS;EAAQ,KAAK,SAAS,OAAO;CAAI;AACtE"}
@@ -1,4 +1,5 @@
1
1
  import { hydrateShared } from "../../shared.mjs";
2
+ import { LocaleProvider } from "../../localization.mjs";
2
3
  import { fetchPageData } from "./fetch-page-data.mjs";
3
4
  import { takePrefetchedPageData } from "./prefetch.mjs";
4
5
  import { connectNavigator } from "../../routing/navigator.mjs";
@@ -224,7 +225,10 @@ function NavigationRoot({ pages, initialPayload, initialTree, buildTree }) {
224
225
  metadata: current.payload.metadata,
225
226
  payload: current.payload
226
227
  },
227
- children: current.tree
228
+ children: /* @__PURE__ */ jsx(LocaleProvider, {
229
+ locale: current.payload.locale,
230
+ children: current.tree
231
+ })
228
232
  });
229
233
  }
230
234
 
@@ -1 +1 @@
1
- {"version":3,"file":"navigation-root.mjs","names":[],"sources":["../../../../../../../../web/src/client/navigation/navigation-root.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from \"react\";\r\nimport { DocumentContext } from \"../../components/document-context\";\r\nimport type { HydrationDocumentPayloadSource } from \"../../hydration-payload\";\r\nimport type { MetadataOutput } from \"../../metadata\";\r\nimport { connectNavigator } from \"../../routing/navigator\";\r\nimport {\r\n fragmentOf,\r\n samePageFragment,\r\n withFragmentFrom,\r\n withoutFragment,\r\n} from \"../../routing/url-fragment\";\r\nimport { hydrateShared } from \"../../shared\";\r\nimport type { ClientPageEntry } from \"../runtime\";\r\nimport { recordCurrentRoute } from \"./current-route\";\r\nimport { fetchPageData } from \"./fetch-page-data\";\r\nimport { takePrefetchedPageData } from \"./prefetch\";\r\nimport { connectRefresher, createRefresher, type RefreshablePage } from \"./refresh\";\r\nimport { scrollToFragment } from \"./scroll-to-fragment\";\r\n\r\n/**\r\n * The component that makes a page REPLACEABLE.\r\n *\r\n * Hydration mounts a fixed tree — correct, because the first render must match\r\n * the server's markup byte for byte. Client navigation needs that same position\r\n * in the tree to be able to hold a *different* page later, which means state,\r\n * which means a component. This is that component and nothing more.\r\n *\r\n * ## Why the layout stays mounted\r\n *\r\n * The new tree is built by the same `buildHydratedTree` the server's payload\r\n * went through, so a navigation within one layout produces an element whose\r\n * layout components are the same types in the same positions. React reconciles\r\n * them rather than remounting, so layout state — an open menu, a scroll\r\n * container, a video — survives. That is the entire user-visible benefit of\r\n * client navigation, and it is a consequence of reusing the builder rather than\r\n * anything this file does.\r\n */\r\n\r\n/**\r\n * ONE tag `<head>` may hold at most one of, addressed the way the browser\r\n * already addresses it. No marker attribute: the tags this replaces were\r\n * rendered by `<Head/>` on the server and carry none, and a marker would make\r\n * the applier ignore exactly the tags it exists to correct — the first\r\n * navigation's.\r\n */\r\ntype ManagedTag = {\r\n /** Finds the existing tag, server-rendered or applied by a previous swap. */\r\n selector: string;\r\n create: (documentNode: Document) => Element;\r\n write: (element: Element, value: string) => void;\r\n};\r\n\r\nfunction metaTag(attribute: \"name\" | \"property\", key: string): ManagedTag {\r\n return {\r\n selector: `meta[${attribute}=\"${key}\"]`,\r\n create: documentNode => {\r\n const element = documentNode.createElement(\"meta\");\r\n\r\n element.setAttribute(attribute, key);\r\n\r\n return element;\r\n },\r\n write: (element, value) => element.setAttribute(\"content\", value),\r\n };\r\n}\r\n\r\nconst TITLE_TAG: ManagedTag = {\r\n selector: \"title\",\r\n create: documentNode => documentNode.createElement(\"title\"),\r\n write: (element, value) => {\r\n element.textContent = value;\r\n },\r\n};\r\n\r\nconst CANONICAL_TAG: ManagedTag = {\r\n selector: 'link[rel=\"canonical\"]',\r\n create: documentNode => {\r\n const element = documentNode.createElement(\"link\");\r\n\r\n element.setAttribute(\"rel\", \"canonical\");\r\n\r\n return element;\r\n },\r\n write: (element, value) => element.setAttribute(\"href\", value),\r\n};\r\n\r\n/**\r\n * The metadata, resolved into (tag, value) pairs in `<Head/>`'s ORDER and by\r\n * `<Head/>`'s RULES — including the og fallbacks and the fact that they apply\r\n * only when `openGraph` is present (`components/head.ts:21-24,43-48`).\r\n *\r\n * The duplication is deliberate and it is the known cost here. `<Head/>` is a\r\n * React component that renders elements into a tree; this writes elements into\r\n * a live `<head>` that no client tree owns. They cannot be one function today,\r\n * but they MUST agree: the head after navigating to a URL has to equal the head\r\n * after landing on it, or a share preview depends on how the visitor arrived.\r\n * The fix is a shared descriptor list both consume — see the report's followup.\r\n */\r\nfunction resolveManagedTags(\r\n metadata: MetadataOutput | undefined,\r\n): readonly (readonly [ManagedTag, string | undefined])[] {\r\n const keywords =\r\n metadata?.keywords === undefined\r\n ? undefined\r\n : Array.isArray(metadata.keywords)\r\n ? metadata.keywords.join(\", \")\r\n : (metadata.keywords as string);\r\n\r\n const openGraph = metadata?.openGraph;\r\n const twitter = metadata?.twitter;\r\n\r\n return [\r\n [TITLE_TAG, metadata?.title],\r\n [metaTag(\"name\", \"description\"), metadata?.description],\r\n [metaTag(\"name\", \"keywords\"), keywords],\r\n [CANONICAL_TAG, metadata?.canonical],\r\n [metaTag(\"name\", \"robots\"), metadata?.robots],\r\n [metaTag(\"property\", \"og:title\"), openGraph && (openGraph.title ?? metadata?.title)],\r\n [\r\n metaTag(\"property\", \"og:description\"),\r\n openGraph && (openGraph.description ?? metadata?.description),\r\n ],\r\n [metaTag(\"property\", \"og:image\"), openGraph?.image],\r\n [metaTag(\"property\", \"og:url\"), openGraph?.url],\r\n [metaTag(\"property\", \"og:type\"), openGraph?.type],\r\n [metaTag(\"name\", \"twitter:card\"), twitter?.card],\r\n [metaTag(\"name\", \"twitter:title\"), twitter?.title],\r\n [metaTag(\"name\", \"twitter:description\"), twitter?.description],\r\n [metaTag(\"name\", \"twitter:image\"), twitter?.image],\r\n ];\r\n}\r\n\r\n/**\r\n * Make `<head>` describe the page now on screen.\r\n *\r\n * ## Why this is imperative, and why that is not a shortcut\r\n *\r\n * `<Head/>` renders inside the App level, and the App level is deliberately NOT\r\n * in the hydrated tree — the client mounts at `#root`, which App contains\r\n * (`client/build-hydrated-tree.ts`'s header). So no client render can reach\r\n * `<head>`, and a swap either writes it directly or leaves the previous page's\r\n * title in the tab. It leaves it today; that is the bug.\r\n *\r\n * ## ABSENT MEANS REMOVED\r\n *\r\n * Every managed tag the new metadata does not set is REMOVED, not left alone.\r\n * `/` sets a description and `/contact-us` does not: keeping it would describe\r\n * the contact page with the home page's words to every crawler, share preview\r\n * and assistive reader that looks — a wrong answer, where an absent one is\r\n * merely absent. A title the new page does not set goes too, and the tab falls\r\n * back to the URL, which is the honest rendering of \"this page did not name\r\n * itself\".\r\n *\r\n * Only the tags `<Head/>` renders FROM METADATA are touched. The charset meta\r\n * is rendered unconditionally and belongs to the document, so it is left alone.\r\n *\r\n * Takes the document as an argument rather than reaching for the global, which\r\n * is what makes it provable in a suite with no DOM.\r\n */\r\nexport function applyDocumentMetadata(\r\n documentNode: Document,\r\n metadata: MetadataOutput | undefined,\r\n): void {\r\n for (const [tag, value] of resolveManagedTags(metadata)) {\r\n const existing = documentNode.querySelector(tag.selector);\r\n\r\n if (value === undefined) {\r\n existing?.remove();\r\n continue;\r\n }\r\n\r\n if (existing !== null) {\r\n tag.write(existing, value);\r\n continue;\r\n }\r\n\r\n const created = tag.create(documentNode);\r\n\r\n tag.write(created, value);\r\n documentNode.head.appendChild(created);\r\n }\r\n}\r\n\r\nexport type NavigationRootProps = {\r\n pages: readonly ClientPageEntry[];\r\n /** The payload the document was rendered from — the hydration one. */\r\n initialPayload: HydrationDocumentPayloadSource;\r\n /** The tree already built from {@link initialPayload}, rendered as-is first. */\r\n initialTree: ReactNode;\r\n /**\r\n * How a payload becomes a tree. Injected rather than imported so this\r\n * component is testable without the page registry or a bundler — the same\r\n * reason `hydratePage` takes its builder as an argument.\r\n */\r\n buildTree: (\r\n pages: readonly ClientPageEntry[],\r\n payload: HydrationDocumentPayloadSource,\r\n ) => Promise<ReactNode>;\r\n};\r\n\r\n/**\r\n * The page on screen. Defined in `refresh.ts` because the third field is that\r\n * file's decision: `routeSource` is the payload object `current-route.ts`\r\n * identifies the route by, which is `payload` after a navigation but the\r\n * PREVIOUS page's object after a refresh — a refresh must not shift\r\n * `previousRoute()` onto the page the user is already looking at.\r\n */\r\ntype Current = RefreshablePage;\r\n\r\nexport function NavigationRoot({\r\n pages,\r\n initialPayload,\r\n initialTree,\r\n buildTree,\r\n}: NavigationRootProps) {\r\n const [current, setCurrent] = useState<Current>({\r\n payload: initialPayload,\r\n tree: initialTree,\r\n routeSource: initialPayload,\r\n });\r\n\r\n /*\r\n The latest state, readable from the effect below — which closes over the\r\n render that created it and would otherwise see the page the user was on\r\n when the runtime connected. A ref rather than an effect dependency because\r\n re-running the effect on every swap would disconnect and reconnect the\r\n navigator mid-navigation.\r\n */\r\n const currentRef = useRef(current);\r\n\r\n currentRef.current = current;\r\n\r\n /*\r\n THE ORDERING PROBLEM, and this ref is half of the answer to it.\r\n\r\n The element a fragment names lives in the tree that has not been built yet:\r\n at the moment `apply` finishes fetching, the DOM still holds the page the\r\n user is LEAVING. Scrolling there finds nothing, and finding nothing is\r\n silent — indistinguishable from the fragment bug itself.\r\n\r\n So the fragment is not scrolled to; it is HANDED OVER. `apply` parks it here\r\n immediately before the `setCurrent` that swaps the tree, and the layout\r\n effect below — which React runs after it has committed that tree to the DOM\r\n and before the browser paints — spends it. Read the two together; neither\r\n half means anything alone.\r\n */\r\n const pendingFragment = useRef<string | undefined>(undefined);\r\n\r\n useEffect(() => {\r\n /*\r\n THE RACE THIS COUNTER EXISTS FOR. Two clicks in quick succession start two\r\n fetches; the second can easily answer first (a cached page beating an\r\n uncached one is the common case, not the exotic one). Without a token the\r\n slower FIRST response lands last and the user ends up on the page they\r\n navigated away from, with the address bar showing the other one.\r\n\r\n Every navigation takes a token; a result is applied only if its token is\r\n still the newest. Superseded responses are dropped silently — they are not\r\n errors, they are answers to a question the user stopped asking.\r\n */\r\n let token = 0;\r\n let disposed = false;\r\n /*\r\n The URL this runtime last put in the address bar, so `popstate` can tell a\r\n move BETWEEN pages from a move between two fragments of one page. Seeded\r\n with the URL the document was loaded at, which is the entry the first Back\r\n would come from.\r\n */\r\n let committedUrl = window.location.href;\r\n\r\n /**\r\n * @param honourFragment whether the URL's fragment should be SCROLLED to\r\n * once the new page is on screen. True for a navigation the app asked for\r\n * — a `<Link>` click, `navigateTo` — and false for Back/Forward, where the\r\n * browser has already restored the scroll position of the entry being\r\n * returned to and moving the page again would overwrite the user's own\r\n * position with the anchor they had scrolled away from. (Restoration is\r\n * the browser's, deliberately: canon `0342c0d4`.)\r\n *\r\n * The fragment is still PRESERVED in the URL in both cases — see below.\r\n */\r\n const apply = async (\r\n url: string,\r\n replace: boolean,\r\n honourFragment: boolean,\r\n ): Promise<void> => {\r\n const ticket = ++token;\r\n /*\r\n A prefetched response is CONSUMED, never merely read — `take` removes it,\r\n so the same speculative fetch can satisfy exactly one navigation and a\r\n second click on the same link goes to the network. That matters because\r\n the HTTP cache cannot stand in for this: dev responses are `no-store`\r\n (`server/dev-server.ts:254`) and production is `private` with no\r\n `max-age` (`server/render-page.ts:432`), so the browser will not reliably\r\n replay the speculative response on the real click.\r\n\r\n The race guard below still holds on a cache hit: `??` short-circuits the\r\n await, and the synchronous path reaches the same `ticket !== token` check.\r\n */\r\n const result = takePrefetchedPageData(url) ?? (await fetchPageData(url));\r\n\r\n if (disposed || ticket !== token) return;\r\n\r\n if (result.type === \"hard-navigate\") {\r\n // The documented degradation: hand the URL back to the browser. The\r\n // user still gets the page — see `fetch-page-data.ts`.\r\n console.warn(`Warlock navigation fell back to a full load (${result.reason}):`, url);\r\n window.location.assign(url);\r\n\r\n return;\r\n }\r\n\r\n let tree: ReactNode;\r\n\r\n try {\r\n tree = await buildTree(pages, result.payload);\r\n } catch (error) {\r\n // The payload was fine but its page chunk would not load or compose —\r\n // a stale bundle after a deploy is the realistic cause. A full load\r\n // fetches the current bundle, which is also the fix.\r\n console.warn(\"Warlock navigation could not build the page tree:\", error);\r\n window.location.assign(url);\r\n\r\n return;\r\n }\r\n\r\n if (disposed || ticket !== token) return;\r\n\r\n /*\r\n Shared state BEFORE the render that consumes it. `hydrateShared`\r\n installs the snapshot `useShared()` reads; swapping the tree first would\r\n render one frame of the new page against the previous page's shared\r\n state — locale, permissions, the current user.\r\n */\r\n hydrateShared(result.payload.shared);\r\n\r\n /*\r\n The fragment PUT BACK. `result.url` comes from `response.url`, and a\r\n fragment is never sent to a server, so the URL a navigation would\r\n otherwise be written to history from has had it stripped — which is how\r\n `<Link href=\"/docs#install\">` used to land on `/docs` with the author's\r\n fragment gone from the address bar for good.\r\n\r\n Applied on EVERY path, Back included: a popstate re-fetch that wrote\r\n `result.url` back would delete the fragment from an entry the user is\r\n merely returning to.\r\n */\r\n const finalUrl = withFragmentFrom(result.url, url);\r\n\r\n // History AFTER the fetch succeeded, never before. Pushing optimistically\r\n // would leave the address bar pointing at a page that then failed to\r\n // load, and a Back press would return to a URL the user never saw.\r\n if (replace) {\r\n window.history.replaceState(null, \"\", finalUrl);\r\n } else {\r\n window.history.pushState(null, \"\", finalUrl);\r\n }\r\n\r\n committedUrl = finalUrl;\r\n\r\n // Handed to the layout effect, which runs once React has committed the\r\n // tree below to the DOM — the first moment the target can exist. Set\r\n // unconditionally so a navigation with no fragment CLEARS a fragment left\r\n // pending by one that was superseded.\r\n pendingFragment.current = honourFragment ? fragmentOf(finalUrl) : undefined;\r\n\r\n // A navigation IS the route moving, so the fetched payload is both the\r\n // page and the route's identity.\r\n setCurrent({ payload: result.payload, tree, routeSource: result.payload });\r\n };\r\n\r\n /*\r\n The same counter `apply` above takes its tickets from, handed to\r\n `refresh()` as a predicate. ONE mechanism, not two: a refresh and a\r\n navigation can overtake each other in either direction, and separate\r\n counters would leave each blind to the other.\r\n */\r\n const claimTicket = (): (() => boolean) => {\r\n const ticket = ++token;\r\n\r\n return () => !disposed && ticket === token;\r\n };\r\n\r\n const previousRefresher = connectRefresher(\r\n createRefresher({\r\n readCurrent: () => currentRef.current,\r\n writeCurrent: setCurrent,\r\n buildTree: payload => buildTree(pages, payload),\r\n claimTicket,\r\n }),\r\n );\r\n\r\n const previousNavigator = connectNavigator((url, options) => {\r\n const replace = options?.replace === true;\r\n\r\n /*\r\n THIS page with a fragment on it — `#reviews`, or the current path spelled\r\n out with one appended. No fetch, no tree swap: the page is already here,\r\n and re-fetching it would discard its DOM and everything live in it to\r\n arrive back where we started, one round trip later. Address bar first,\r\n then the jump, which is the order the browser uses for a plain anchor.\r\n */\r\n const fragment = samePageFragment(url, window.location.href);\r\n\r\n if (fragment !== undefined) {\r\n if (replace) {\r\n window.history.replaceState(null, \"\", url);\r\n } else {\r\n window.history.pushState(null, \"\", url);\r\n }\r\n\r\n committedUrl = window.location.href;\r\n\r\n // The target is in the DOM already, so there is nothing to wait for —\r\n // and nothing to hand to the layout effect, which no swap would fire.\r\n scrollToFragment(document, fragment);\r\n\r\n return true;\r\n }\r\n\r\n void apply(url, replace, true);\r\n\r\n // Accepted: the caller suppresses the browser's default. Returning `true`\r\n // before the fetch resolves is deliberate — the decision to handle a link\r\n // cannot wait on the network without the browser having already followed\r\n // it.\r\n return true;\r\n });\r\n\r\n /*\r\n Back/Forward. The entry is already in history and the URL has already\r\n changed by the time this fires, so the page is fetched and swapped with\r\n `replace` — pushing here would append a duplicate entry and make Back\r\n require two presses.\r\n */\r\n const onPopState = (): void => {\r\n const target = window.location.href;\r\n /*\r\n A hash-only move within one page — Back off a `#section` click, or\r\n Forward onto one. The document is the same document and the tree on\r\n screen is already the right tree, so there is nothing to fetch: the\r\n browser has changed the URL and restored the position for that entry\r\n itself, and re-fetching would throw away a live page to rebuild the one\r\n already showing. Scroll restoration stays the browser's (canon\r\n `0342c0d4`), which is exactly what leaving this alone means.\r\n */\r\n const hashOnlyMove = withoutFragment(target) === withoutFragment(committedUrl);\r\n\r\n committedUrl = target;\r\n\r\n if (hashOnlyMove) return;\r\n\r\n void apply(target, true, false);\r\n };\r\n\r\n window.addEventListener(\"popstate\", onPopState);\r\n\r\n return () => {\r\n disposed = true;\r\n window.removeEventListener(\"popstate\", onPopState);\r\n connectNavigator(previousNavigator);\r\n connectRefresher(previousRefresher);\r\n };\r\n }, [pages, buildTree]);\r\n\r\n /*\r\n THE OTHER HALF OF THE ORDERING PROBLEM (see `pendingFragment` above).\r\n\r\n `useLayoutEffect`, not `useEffect`, and the difference is the whole point:\r\n React runs a layout effect after it has COMMITTED this render to the DOM and\r\n BEFORE the browser paints. That is the earliest instant the new page's\r\n elements exist — a scroll any sooner finds nothing — and the last instant\r\n before the user sees anything, so the page is never painted at the top and\r\n then jumped. `useEffect` would satisfy the first requirement and not the\r\n second: it runs after paint, which is a visible flash of the wrong position.\r\n\r\n Keyed on `current` rather than reaching for a fresh render: the effect fires\r\n on the swap that put the target in the DOM, so no polling, no rAF, no\r\n timeout. What it CANNOT wait for is content that arrives later still — an\r\n image without dimensions above the target, a component that suspends — which\r\n moves the target after we have scrolled to where it was. That is the known\r\n limit of this mechanism and it is the same one a browser has.\r\n\r\n Consumed once: the fragment is cleared as it is read, so a later re-render\r\n (a refresh, a parent's state change) does not yank the page back to an\r\n anchor the user has since scrolled away from.\r\n */\r\n useLayoutEffect(() => {\r\n const fragment = pendingFragment.current;\r\n\r\n if (fragment === undefined) return;\r\n\r\n pendingFragment.current = undefined;\r\n\r\n scrollToFragment(document, fragment);\r\n }, [current]);\r\n\r\n /*\r\n The payload whose metadata `<head>` currently reflects. Seeded with the\r\n HYDRATION payload, which is the point: on the initial mount the head is the\r\n server's own render of this very metadata, so there is nothing to correct —\r\n and re-applying would be a chance to get it wrong, since a payload from a\r\n build with no `metadata` key would wipe a head the server filled in\r\n correctly. The head is applied on SWAPS only.\r\n */\r\n const appliedMetadataSource = useRef(current.payload);\r\n\r\n useEffect(() => {\r\n if (appliedMetadataSource.current === current.payload) return;\r\n\r\n appliedMetadataSource.current = current.payload;\r\n\r\n /*\r\n Keyed on the payload's identity, so this covers a refresh as well as a\r\n navigation: `refresh()` produces a NEW payload for the page already on\r\n screen, and a page whose metadata is a function of its loader data can\r\n legitimately re-title itself when that data changes. One applier at the\r\n one place the page changes, rather than a call in each pathway that could\r\n be forgotten in the next one.\r\n */\r\n applyDocumentMetadata(document, current.payload.metadata);\r\n }, [current.payload]);\r\n\r\n /*\r\n DURING RENDER, not in an effect, and that placement is the requirement\r\n rather than a shortcut. A page component calling `currentRoute()` does it\r\n while IT is rendering, and child effects run before a parent's, so anything\r\n recorded from an effect here would be recorded too late to answer the first\r\n render of the page it describes — which on the initial mount is the only\r\n render there has been, and the server's match is all there is.\r\n\r\n Recording is keyed on the payload's identity (`current-route.ts`), so the\r\n extra passes render gives us for free — StrictMode's double invoke, a parent\r\n re-render — are recognised as the same page rather than counted as\r\n navigations.\r\n\r\n `routeSource`, NOT `payload`: they are the same object for every navigation,\r\n and differ only after a refresh, which produces a new payload for the page\r\n already on screen and must not be counted as a move (see `refresh.ts`).\r\n */\r\n recordCurrentRoute(current.routeSource);\r\n\r\n /*\r\n An INNER DocumentContext provider, nested inside the one `hydratePage`\r\n mounted. That outer provider is created once with the hydration payload and\r\n never changes, so anything reading the document context after a navigation\r\n would see the payload of the page the user has left. The nearest provider\r\n wins, so this one keeps it current.\r\n */\r\n /*\r\n `metadata` from the PAYLOAD, not `undefined`. The document context is the\r\n universal shape — the server provides the resolved metadata around the same\r\n tree — so handing the client's readers `undefined` was a lie the payload can\r\n now correct. It also removes a latent mismatch: a `<Head/>` rendered inside\r\n a Layout (not the App level) renders its tags into `#root` server-side, and\r\n hydrating that markup against `metadata: undefined` produced a tree the\r\n server never rendered.\r\n */\r\n return (\r\n <DocumentContext.Provider\r\n value={{ metadata: current.payload.metadata, payload: current.payload }}\r\n >\r\n {current.tree}\r\n </DocumentContext.Provider>\r\n );\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"navigation-root.mjs","names":[],"sources":["../../../../../../../../web/src/client/navigation/navigation-root.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from \"react\";\r\nimport { DocumentContext } from \"../../components/document-context\";\nimport { LocaleProvider } from \"../../localization\";\nimport type { HydrationDocumentPayloadSource } from \"../../hydration-payload\";\r\nimport type { MetadataOutput } from \"../../metadata\";\r\nimport { connectNavigator } from \"../../routing/navigator\";\r\nimport {\r\n fragmentOf,\r\n samePageFragment,\r\n withFragmentFrom,\r\n withoutFragment,\r\n} from \"../../routing/url-fragment\";\r\nimport { hydrateShared } from \"../../shared\";\r\nimport type { ClientPageEntry } from \"../runtime\";\r\nimport { recordCurrentRoute } from \"./current-route\";\r\nimport { fetchPageData } from \"./fetch-page-data\";\r\nimport { takePrefetchedPageData } from \"./prefetch\";\r\nimport { connectRefresher, createRefresher, type RefreshablePage } from \"./refresh\";\r\nimport { scrollToFragment } from \"./scroll-to-fragment\";\r\n\r\n/**\r\n * The component that makes a page REPLACEABLE.\r\n *\r\n * Hydration mounts a fixed tree — correct, because the first render must match\r\n * the server's markup byte for byte. Client navigation needs that same position\r\n * in the tree to be able to hold a *different* page later, which means state,\r\n * which means a component. This is that component and nothing more.\r\n *\r\n * ## Why the layout stays mounted\r\n *\r\n * The new tree is built by the same `buildHydratedTree` the server's payload\r\n * went through, so a navigation within one layout produces an element whose\r\n * layout components are the same types in the same positions. React reconciles\r\n * them rather than remounting, so layout state — an open menu, a scroll\r\n * container, a video — survives. That is the entire user-visible benefit of\r\n * client navigation, and it is a consequence of reusing the builder rather than\r\n * anything this file does.\r\n */\r\n\r\n/**\r\n * ONE tag `<head>` may hold at most one of, addressed the way the browser\r\n * already addresses it. No marker attribute: the tags this replaces were\r\n * rendered by `<Head/>` on the server and carry none, and a marker would make\r\n * the applier ignore exactly the tags it exists to correct — the first\r\n * navigation's.\r\n */\r\ntype ManagedTag = {\r\n /** Finds the existing tag, server-rendered or applied by a previous swap. */\r\n selector: string;\r\n create: (documentNode: Document) => Element;\r\n write: (element: Element, value: string) => void;\r\n};\r\n\r\nfunction metaTag(attribute: \"name\" | \"property\", key: string): ManagedTag {\r\n return {\r\n selector: `meta[${attribute}=\"${key}\"]`,\r\n create: documentNode => {\r\n const element = documentNode.createElement(\"meta\");\r\n\r\n element.setAttribute(attribute, key);\r\n\r\n return element;\r\n },\r\n write: (element, value) => element.setAttribute(\"content\", value),\r\n };\r\n}\r\n\r\nconst TITLE_TAG: ManagedTag = {\r\n selector: \"title\",\r\n create: documentNode => documentNode.createElement(\"title\"),\r\n write: (element, value) => {\r\n element.textContent = value;\r\n },\r\n};\r\n\r\nconst CANONICAL_TAG: ManagedTag = {\r\n selector: 'link[rel=\"canonical\"]',\r\n create: documentNode => {\r\n const element = documentNode.createElement(\"link\");\r\n\r\n element.setAttribute(\"rel\", \"canonical\");\r\n\r\n return element;\r\n },\r\n write: (element, value) => element.setAttribute(\"href\", value),\r\n};\r\n\r\n/**\r\n * The metadata, resolved into (tag, value) pairs in `<Head/>`'s ORDER and by\r\n * `<Head/>`'s RULES — including the og fallbacks and the fact that they apply\r\n * only when `openGraph` is present (`components/head.ts:21-24,43-48`).\r\n *\r\n * The duplication is deliberate and it is the known cost here. `<Head/>` is a\r\n * React component that renders elements into a tree; this writes elements into\r\n * a live `<head>` that no client tree owns. They cannot be one function today,\r\n * but they MUST agree: the head after navigating to a URL has to equal the head\r\n * after landing on it, or a share preview depends on how the visitor arrived.\r\n * The fix is a shared descriptor list both consume — see the report's followup.\r\n */\r\nfunction resolveManagedTags(\r\n metadata: MetadataOutput | undefined,\r\n): readonly (readonly [ManagedTag, string | undefined])[] {\r\n const keywords =\r\n metadata?.keywords === undefined\r\n ? undefined\r\n : Array.isArray(metadata.keywords)\r\n ? metadata.keywords.join(\", \")\r\n : (metadata.keywords as string);\r\n\r\n const openGraph = metadata?.openGraph;\r\n const twitter = metadata?.twitter;\r\n\r\n return [\r\n [TITLE_TAG, metadata?.title],\r\n [metaTag(\"name\", \"description\"), metadata?.description],\r\n [metaTag(\"name\", \"keywords\"), keywords],\r\n [CANONICAL_TAG, metadata?.canonical],\r\n [metaTag(\"name\", \"robots\"), metadata?.robots],\r\n [metaTag(\"property\", \"og:title\"), openGraph && (openGraph.title ?? metadata?.title)],\r\n [\r\n metaTag(\"property\", \"og:description\"),\r\n openGraph && (openGraph.description ?? metadata?.description),\r\n ],\r\n [metaTag(\"property\", \"og:image\"), openGraph?.image],\r\n [metaTag(\"property\", \"og:url\"), openGraph?.url],\r\n [metaTag(\"property\", \"og:type\"), openGraph?.type],\r\n [metaTag(\"name\", \"twitter:card\"), twitter?.card],\r\n [metaTag(\"name\", \"twitter:title\"), twitter?.title],\r\n [metaTag(\"name\", \"twitter:description\"), twitter?.description],\r\n [metaTag(\"name\", \"twitter:image\"), twitter?.image],\r\n ];\r\n}\r\n\r\n/**\r\n * Make `<head>` describe the page now on screen.\r\n *\r\n * ## Why this is imperative, and why that is not a shortcut\r\n *\r\n * `<Head/>` renders inside the App level, and the App level is deliberately NOT\r\n * in the hydrated tree — the client mounts at `#root`, which App contains\r\n * (`client/build-hydrated-tree.ts`'s header). So no client render can reach\r\n * `<head>`, and a swap either writes it directly or leaves the previous page's\r\n * title in the tab. It leaves it today; that is the bug.\r\n *\r\n * ## ABSENT MEANS REMOVED\r\n *\r\n * Every managed tag the new metadata does not set is REMOVED, not left alone.\r\n * `/` sets a description and `/contact-us` does not: keeping it would describe\r\n * the contact page with the home page's words to every crawler, share preview\r\n * and assistive reader that looks — a wrong answer, where an absent one is\r\n * merely absent. A title the new page does not set goes too, and the tab falls\r\n * back to the URL, which is the honest rendering of \"this page did not name\r\n * itself\".\r\n *\r\n * Only the tags `<Head/>` renders FROM METADATA are touched. The charset meta\r\n * is rendered unconditionally and belongs to the document, so it is left alone.\r\n *\r\n * Takes the document as an argument rather than reaching for the global, which\r\n * is what makes it provable in a suite with no DOM.\r\n */\r\nexport function applyDocumentMetadata(\r\n documentNode: Document,\r\n metadata: MetadataOutput | undefined,\r\n): void {\r\n for (const [tag, value] of resolveManagedTags(metadata)) {\r\n const existing = documentNode.querySelector(tag.selector);\r\n\r\n if (value === undefined) {\r\n existing?.remove();\r\n continue;\r\n }\r\n\r\n if (existing !== null) {\r\n tag.write(existing, value);\r\n continue;\r\n }\r\n\r\n const created = tag.create(documentNode);\r\n\r\n tag.write(created, value);\r\n documentNode.head.appendChild(created);\r\n }\r\n}\r\n\r\nexport type NavigationRootProps = {\r\n pages: readonly ClientPageEntry[];\r\n /** The payload the document was rendered from — the hydration one. */\r\n initialPayload: HydrationDocumentPayloadSource;\r\n /** The tree already built from {@link initialPayload}, rendered as-is first. */\r\n initialTree: ReactNode;\r\n /**\r\n * How a payload becomes a tree. Injected rather than imported so this\r\n * component is testable without the page registry or a bundler — the same\r\n * reason `hydratePage` takes its builder as an argument.\r\n */\r\n buildTree: (\r\n pages: readonly ClientPageEntry[],\r\n payload: HydrationDocumentPayloadSource,\r\n ) => Promise<ReactNode>;\r\n};\r\n\r\n/**\r\n * The page on screen. Defined in `refresh.ts` because the third field is that\r\n * file's decision: `routeSource` is the payload object `current-route.ts`\r\n * identifies the route by, which is `payload` after a navigation but the\r\n * PREVIOUS page's object after a refresh — a refresh must not shift\r\n * `previousRoute()` onto the page the user is already looking at.\r\n */\r\ntype Current = RefreshablePage;\r\n\r\nexport function NavigationRoot({\r\n pages,\r\n initialPayload,\r\n initialTree,\r\n buildTree,\r\n}: NavigationRootProps) {\r\n const [current, setCurrent] = useState<Current>({\r\n payload: initialPayload,\r\n tree: initialTree,\r\n routeSource: initialPayload,\r\n });\r\n\r\n /*\r\n The latest state, readable from the effect below — which closes over the\r\n render that created it and would otherwise see the page the user was on\r\n when the runtime connected. A ref rather than an effect dependency because\r\n re-running the effect on every swap would disconnect and reconnect the\r\n navigator mid-navigation.\r\n */\r\n const currentRef = useRef(current);\r\n\r\n currentRef.current = current;\r\n\r\n /*\r\n THE ORDERING PROBLEM, and this ref is half of the answer to it.\r\n\r\n The element a fragment names lives in the tree that has not been built yet:\r\n at the moment `apply` finishes fetching, the DOM still holds the page the\r\n user is LEAVING. Scrolling there finds nothing, and finding nothing is\r\n silent — indistinguishable from the fragment bug itself.\r\n\r\n So the fragment is not scrolled to; it is HANDED OVER. `apply` parks it here\r\n immediately before the `setCurrent` that swaps the tree, and the layout\r\n effect below — which React runs after it has committed that tree to the DOM\r\n and before the browser paints — spends it. Read the two together; neither\r\n half means anything alone.\r\n */\r\n const pendingFragment = useRef<string | undefined>(undefined);\r\n\r\n useEffect(() => {\r\n /*\r\n THE RACE THIS COUNTER EXISTS FOR. Two clicks in quick succession start two\r\n fetches; the second can easily answer first (a cached page beating an\r\n uncached one is the common case, not the exotic one). Without a token the\r\n slower FIRST response lands last and the user ends up on the page they\r\n navigated away from, with the address bar showing the other one.\r\n\r\n Every navigation takes a token; a result is applied only if its token is\r\n still the newest. Superseded responses are dropped silently — they are not\r\n errors, they are answers to a question the user stopped asking.\r\n */\r\n let token = 0;\r\n let disposed = false;\r\n /*\r\n The URL this runtime last put in the address bar, so `popstate` can tell a\r\n move BETWEEN pages from a move between two fragments of one page. Seeded\r\n with the URL the document was loaded at, which is the entry the first Back\r\n would come from.\r\n */\r\n let committedUrl = window.location.href;\r\n\r\n /**\r\n * @param honourFragment whether the URL's fragment should be SCROLLED to\r\n * once the new page is on screen. True for a navigation the app asked for\r\n * — a `<Link>` click, `navigateTo` — and false for Back/Forward, where the\r\n * browser has already restored the scroll position of the entry being\r\n * returned to and moving the page again would overwrite the user's own\r\n * position with the anchor they had scrolled away from. (Restoration is\r\n * the browser's, deliberately: canon `0342c0d4`.)\r\n *\r\n * The fragment is still PRESERVED in the URL in both cases — see below.\r\n */\r\n const apply = async (\r\n url: string,\r\n replace: boolean,\r\n honourFragment: boolean,\r\n ): Promise<void> => {\r\n const ticket = ++token;\r\n /*\r\n A prefetched response is CONSUMED, never merely read — `take` removes it,\r\n so the same speculative fetch can satisfy exactly one navigation and a\r\n second click on the same link goes to the network. That matters because\r\n the HTTP cache cannot stand in for this: dev responses are `no-store`\r\n (`server/dev-server.ts:254`) and production is `private` with no\r\n `max-age` (`server/render-page.ts:432`), so the browser will not reliably\r\n replay the speculative response on the real click.\r\n\r\n The race guard below still holds on a cache hit: `??` short-circuits the\r\n await, and the synchronous path reaches the same `ticket !== token` check.\r\n */\r\n const result = takePrefetchedPageData(url) ?? (await fetchPageData(url));\r\n\r\n if (disposed || ticket !== token) return;\r\n\r\n if (result.type === \"hard-navigate\") {\r\n // The documented degradation: hand the URL back to the browser. The\r\n // user still gets the page — see `fetch-page-data.ts`.\r\n console.warn(`Warlock navigation fell back to a full load (${result.reason}):`, url);\r\n window.location.assign(url);\r\n\r\n return;\r\n }\r\n\r\n let tree: ReactNode;\r\n\r\n try {\r\n tree = await buildTree(pages, result.payload);\r\n } catch (error) {\r\n // The payload was fine but its page chunk would not load or compose —\r\n // a stale bundle after a deploy is the realistic cause. A full load\r\n // fetches the current bundle, which is also the fix.\r\n console.warn(\"Warlock navigation could not build the page tree:\", error);\r\n window.location.assign(url);\r\n\r\n return;\r\n }\r\n\r\n if (disposed || ticket !== token) return;\r\n\r\n /*\r\n Shared state BEFORE the render that consumes it. `hydrateShared`\r\n installs the snapshot `useShared()` reads; swapping the tree first would\r\n render one frame of the new page against the previous page's shared\r\n state — locale, permissions, the current user.\r\n */\r\n hydrateShared(result.payload.shared);\r\n\r\n /*\r\n The fragment PUT BACK. `result.url` comes from `response.url`, and a\r\n fragment is never sent to a server, so the URL a navigation would\r\n otherwise be written to history from has had it stripped — which is how\r\n `<Link href=\"/docs#install\">` used to land on `/docs` with the author's\r\n fragment gone from the address bar for good.\r\n\r\n Applied on EVERY path, Back included: a popstate re-fetch that wrote\r\n `result.url` back would delete the fragment from an entry the user is\r\n merely returning to.\r\n */\r\n const finalUrl = withFragmentFrom(result.url, url);\r\n\r\n // History AFTER the fetch succeeded, never before. Pushing optimistically\r\n // would leave the address bar pointing at a page that then failed to\r\n // load, and a Back press would return to a URL the user never saw.\r\n if (replace) {\r\n window.history.replaceState(null, \"\", finalUrl);\r\n } else {\r\n window.history.pushState(null, \"\", finalUrl);\r\n }\r\n\r\n committedUrl = finalUrl;\r\n\r\n // Handed to the layout effect, which runs once React has committed the\r\n // tree below to the DOM — the first moment the target can exist. Set\r\n // unconditionally so a navigation with no fragment CLEARS a fragment left\r\n // pending by one that was superseded.\r\n pendingFragment.current = honourFragment ? fragmentOf(finalUrl) : undefined;\r\n\r\n // A navigation IS the route moving, so the fetched payload is both the\r\n // page and the route's identity.\r\n setCurrent({ payload: result.payload, tree, routeSource: result.payload });\r\n };\r\n\r\n /*\r\n The same counter `apply` above takes its tickets from, handed to\r\n `refresh()` as a predicate. ONE mechanism, not two: a refresh and a\r\n navigation can overtake each other in either direction, and separate\r\n counters would leave each blind to the other.\r\n */\r\n const claimTicket = (): (() => boolean) => {\r\n const ticket = ++token;\r\n\r\n return () => !disposed && ticket === token;\r\n };\r\n\r\n const previousRefresher = connectRefresher(\r\n createRefresher({\r\n readCurrent: () => currentRef.current,\r\n writeCurrent: setCurrent,\r\n buildTree: payload => buildTree(pages, payload),\r\n claimTicket,\r\n }),\r\n );\r\n\r\n const previousNavigator = connectNavigator((url, options) => {\r\n const replace = options?.replace === true;\r\n\r\n /*\r\n THIS page with a fragment on it — `#reviews`, or the current path spelled\r\n out with one appended. No fetch, no tree swap: the page is already here,\r\n and re-fetching it would discard its DOM and everything live in it to\r\n arrive back where we started, one round trip later. Address bar first,\r\n then the jump, which is the order the browser uses for a plain anchor.\r\n */\r\n const fragment = samePageFragment(url, window.location.href);\r\n\r\n if (fragment !== undefined) {\r\n if (replace) {\r\n window.history.replaceState(null, \"\", url);\r\n } else {\r\n window.history.pushState(null, \"\", url);\r\n }\r\n\r\n committedUrl = window.location.href;\r\n\r\n // The target is in the DOM already, so there is nothing to wait for —\r\n // and nothing to hand to the layout effect, which no swap would fire.\r\n scrollToFragment(document, fragment);\r\n\r\n return true;\r\n }\r\n\r\n void apply(url, replace, true);\r\n\r\n // Accepted: the caller suppresses the browser's default. Returning `true`\r\n // before the fetch resolves is deliberate — the decision to handle a link\r\n // cannot wait on the network without the browser having already followed\r\n // it.\r\n return true;\r\n });\r\n\r\n /*\r\n Back/Forward. The entry is already in history and the URL has already\r\n changed by the time this fires, so the page is fetched and swapped with\r\n `replace` — pushing here would append a duplicate entry and make Back\r\n require two presses.\r\n */\r\n const onPopState = (): void => {\r\n const target = window.location.href;\r\n /*\r\n A hash-only move within one page — Back off a `#section` click, or\r\n Forward onto one. The document is the same document and the tree on\r\n screen is already the right tree, so there is nothing to fetch: the\r\n browser has changed the URL and restored the position for that entry\r\n itself, and re-fetching would throw away a live page to rebuild the one\r\n already showing. Scroll restoration stays the browser's (canon\r\n `0342c0d4`), which is exactly what leaving this alone means.\r\n */\r\n const hashOnlyMove = withoutFragment(target) === withoutFragment(committedUrl);\r\n\r\n committedUrl = target;\r\n\r\n if (hashOnlyMove) return;\r\n\r\n void apply(target, true, false);\r\n };\r\n\r\n window.addEventListener(\"popstate\", onPopState);\r\n\r\n return () => {\r\n disposed = true;\r\n window.removeEventListener(\"popstate\", onPopState);\r\n connectNavigator(previousNavigator);\r\n connectRefresher(previousRefresher);\r\n };\r\n }, [pages, buildTree]);\r\n\r\n /*\r\n THE OTHER HALF OF THE ORDERING PROBLEM (see `pendingFragment` above).\r\n\r\n `useLayoutEffect`, not `useEffect`, and the difference is the whole point:\r\n React runs a layout effect after it has COMMITTED this render to the DOM and\r\n BEFORE the browser paints. That is the earliest instant the new page's\r\n elements exist — a scroll any sooner finds nothing — and the last instant\r\n before the user sees anything, so the page is never painted at the top and\r\n then jumped. `useEffect` would satisfy the first requirement and not the\r\n second: it runs after paint, which is a visible flash of the wrong position.\r\n\r\n Keyed on `current` rather than reaching for a fresh render: the effect fires\r\n on the swap that put the target in the DOM, so no polling, no rAF, no\r\n timeout. What it CANNOT wait for is content that arrives later still — an\r\n image without dimensions above the target, a component that suspends — which\r\n moves the target after we have scrolled to where it was. That is the known\r\n limit of this mechanism and it is the same one a browser has.\r\n\r\n Consumed once: the fragment is cleared as it is read, so a later re-render\r\n (a refresh, a parent's state change) does not yank the page back to an\r\n anchor the user has since scrolled away from.\r\n */\r\n useLayoutEffect(() => {\r\n const fragment = pendingFragment.current;\r\n\r\n if (fragment === undefined) return;\r\n\r\n pendingFragment.current = undefined;\r\n\r\n scrollToFragment(document, fragment);\r\n }, [current]);\r\n\r\n /*\r\n The payload whose metadata `<head>` currently reflects. Seeded with the\r\n HYDRATION payload, which is the point: on the initial mount the head is the\r\n server's own render of this very metadata, so there is nothing to correct —\r\n and re-applying would be a chance to get it wrong, since a payload from a\r\n build with no `metadata` key would wipe a head the server filled in\r\n correctly. The head is applied on SWAPS only.\r\n */\r\n const appliedMetadataSource = useRef(current.payload);\r\n\r\n useEffect(() => {\r\n if (appliedMetadataSource.current === current.payload) return;\r\n\r\n appliedMetadataSource.current = current.payload;\r\n\r\n /*\r\n Keyed on the payload's identity, so this covers a refresh as well as a\r\n navigation: `refresh()` produces a NEW payload for the page already on\r\n screen, and a page whose metadata is a function of its loader data can\r\n legitimately re-title itself when that data changes. One applier at the\r\n one place the page changes, rather than a call in each pathway that could\r\n be forgotten in the next one.\r\n */\r\n applyDocumentMetadata(document, current.payload.metadata);\r\n }, [current.payload]);\r\n\r\n /*\r\n DURING RENDER, not in an effect, and that placement is the requirement\r\n rather than a shortcut. A page component calling `currentRoute()` does it\r\n while IT is rendering, and child effects run before a parent's, so anything\r\n recorded from an effect here would be recorded too late to answer the first\r\n render of the page it describes — which on the initial mount is the only\r\n render there has been, and the server's match is all there is.\r\n\r\n Recording is keyed on the payload's identity (`current-route.ts`), so the\r\n extra passes render gives us for free — StrictMode's double invoke, a parent\r\n re-render — are recognised as the same page rather than counted as\r\n navigations.\r\n\r\n `routeSource`, NOT `payload`: they are the same object for every navigation,\r\n and differ only after a refresh, which produces a new payload for the page\r\n already on screen and must not be counted as a move (see `refresh.ts`).\r\n */\r\n recordCurrentRoute(current.routeSource);\r\n\r\n /*\r\n An INNER DocumentContext provider, nested inside the one `hydratePage`\r\n mounted. That outer provider is created once with the hydration payload and\r\n never changes, so anything reading the document context after a navigation\r\n would see the payload of the page the user has left. The nearest provider\r\n wins, so this one keeps it current.\r\n */\r\n /*\r\n `metadata` from the PAYLOAD, not `undefined`. The document context is the\r\n universal shape — the server provides the resolved metadata around the same\r\n tree — so handing the client's readers `undefined` was a lie the payload can\r\n now correct. It also removes a latent mismatch: a `<Head/>` rendered inside\r\n a Layout (not the App level) renders its tags into `#root` server-side, and\r\n hydrating that markup against `metadata: undefined` produced a tree the\r\n server never rendered.\r\n */\r\n return (\n <DocumentContext.Provider\n value={{ metadata: current.payload.metadata, payload: current.payload }}\n >\n <LocaleProvider locale={current.payload.locale}>{current.tree}</LocaleProvider>\n </DocumentContext.Provider>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"prefetch.mjs","names":[],"sources":["../../../../../../../../web/src/client/navigation/prefetch.ts"],"sourcesContent":["/**\n * Speculative page data, fetched on hover and spent on the click that follows.\n *\n * A prefetch is a GUESS. The user pointed at a link; they may never click it.\n * Everything about this module follows from that one fact:\n *\n * - it never reports a failure — nobody asked for this request, so nobody may\n * be told it failed. A failed prefetch leaves the cache empty and the click\n * fetches for real, which is the behaviour we had before this file existed.\n * - it never delays a click. `prefetchPageData` is fire-and-forget; the\n * navigation path reads the cache synchronously and does not wait on it.\n * - it is BOUNDED and it EXPIRES. A cached payload is a copy of a page that\n * may already have changed, so it may only be served while it is very\n * probably still true.\n *\n * ## The read side is deliberately separate from the write side\n *\n * `<Link>` writes (on hover); the navigation runtime reads (on click). Neither\n * knows about the other — they share a URL, and this module is the only thing\n * between them. That is what lets prefetch be added without touching the\n * navigation state machine.\n */\nimport type { PageDataResult } from \"./fetch-page-data\";\nimport { fetchPageData } from \"./fetch-page-data\";\n\n/** A prefetch result worth keeping — only ever a successful payload. */\nexport type PrefetchedPageData = Extract<PageDataResult, { type: \"payload\" }>;\n\n/**\n * How many pages may be held at once.\n *\n * Ten, because the working set this serves is \"links the pointer has crossed in\n * the last few seconds\", which is small by construction — a nav bar, a card\n * grid the user is scanning. A page payload is loader data, not markup, but it\n * is still measured in tens of kilobytes, so an unbounded map on a long-lived\n * SPA session is a slow leak with no upper edge. Ten covers scanning a menu and\n * costs at most a few hundred kilobytes in the worst case.\n *\n * Eviction is by INSERTION ORDER, not by recency: an entry here is written once\n * and read at most once, so there is no recency to track — the oldest guess is\n * always the one least likely to be spent.\n */\nexport const PREFETCH_CACHE_LIMIT = 10;\n\n/**\n * How long a cached payload may be served, in milliseconds.\n *\n * Thirty seconds. This is a SAFETY bound and not a performance knob: the\n * interval this feature exists to cover is hover-to-click, which is well under\n * a second, and every millisecond beyond that is pure staleness risk. A payload\n * served after the user has changed the data behind it renders a wrong page —\n * silently, and with no way for them to tell. Thirty seconds is long enough\n * that a hesitant click still hits, and short enough that no realistic\n * \"navigate away, mutate something, come back\" flow can complete inside it.\n */\nexport const PREFETCH_TTL_MS = 30_000;\n\ntype CacheEntry = {\n result: PrefetchedPageData;\n /** `Date.now()` at which this entry stops being servable. */\n expiresAt: number;\n};\n\n/**\n * Insertion-ordered by `Map` contract, which is what makes eviction a `keys()\n * .next()` and not a bookkeeping structure.\n */\nconst cache = new Map<string, CacheEntry>();\n\n/**\n * URLs with a request already in the air. Repeated `mouseenter` events on the\n * same anchor are the norm, not the exception — a pointer crossing a link fires\n * as the user's hand settles — and without this each one would be its own\n * request for the same page.\n */\nconst inFlight = new Set<string>();\n\n/**\n * Whether there is a browser to prefetch from.\n *\n * Called rather than assumed because `<Link>` is universal: the same module\n * graph is evaluated during a server render, where a speculative request would\n * be a request the server makes to itself for a page nobody is looking at.\n */\nfunction isBrowser(): boolean {\n return typeof window !== \"undefined\";\n}\n\nfunction evictOldest(): void {\n const oldest = cache.keys().next();\n\n if (oldest.done !== true) cache.delete(oldest.value);\n}\n\n/**\n * Fetch a URL's page data ahead of the click, and cache it.\n *\n * NEVER REJECTS and never reports. The returned promise exists so a test can\n * await the speculative work; callers in the component tree discard it\n * (`void prefetchPageData(url)`) and must not await it — a click that waited on\n * a guess would be slower than one that never made it.\n *\n * The caller decides WHETHER a URL may be prefetched. This function does not\n * re-derive that: it has a URL and no way to tell an in-app path from a\n * cross-origin one it must never touch. `<Link>` gates on `isInApp`.\n */\nexport async function prefetchPageData(url: string): Promise<void> {\n if (!isBrowser()) return;\n\n if (inFlight.has(url)) return;\n\n // A fresh entry means the answer is already here; an expired one is dropped\n // now so the fetch below can replace it.\n const cached = cache.get(url);\n\n if (cached !== undefined) {\n if (cached.expiresAt > Date.now()) return;\n\n cache.delete(url);\n }\n\n inFlight.add(url);\n\n try {\n const result = await fetchPageData(url);\n\n /*\n `hard-navigate` is NOT cached. It is the signal that this URL needs a full\n page load — a 404, a redirect to an interstitial, a proxy that stripped\n the marker header. Caching it would mean the click either replays a\n failure or, worse, consults an entry that cannot be rendered. The click\n re-asks and gets the same answer, correctly, through the navigation path\n that already knows how to degrade.\n */\n if (result.type !== \"payload\") return;\n\n if (cache.size >= PREFETCH_CACHE_LIMIT) evictOldest();\n\n cache.set(url, { result, expiresAt: Date.now() + PREFETCH_TTL_MS });\n } catch {\n /*\n `fetchPageData` already converts every network failure into a\n `hard-navigate`, so reaching here means something unforeseen. It is\n swallowed anyway, deliberately and without a log: this request was\n speculative, the user never asked for it, and a console full of warnings\n about requests nobody made is how a helpful optimisation becomes noise\n that hides real errors.\n */\n } finally {\n // In `finally` so a failed attempt does not poison the URL for the rest of\n // the session — the next hover is allowed to try again.\n inFlight.delete(url);\n }\n}\n\n/**\n * Take the prefetched payload for a URL, if there is a live one.\n *\n * CONSUMING: the entry is removed whether or not the caller ends up using it.\n * One hover buys one saved round trip. Serving the same payload to a second\n * navigation would double the window in which it can be wrong, in exchange for\n * a saving the user did not notice the first time.\n *\n * @returns the cached result, or `undefined` — in which case the caller fetches\n * exactly as it did before this module existed.\n */\nexport function takePrefetchedPageData(url: string): PrefetchedPageData | undefined {\n const entry = cache.get(url);\n\n if (entry === undefined) return undefined;\n\n cache.delete(url);\n\n // Expiry is checked on READ, not on a timer: a timer would keep a\n // long-lived page waking up to clean a cache that is already bounded, and an\n // entry nobody reads costs nothing but the slot it is evicted from anyway.\n return entry.expiresAt > Date.now() ? entry.result : undefined;\n}\n\n/**\n * Drop everything. For tests, and for any caller that knows the cached pages\n * are now wrong — the module-level cache would otherwise outlive a suite and\n * leak into the next one.\n */\nexport function resetPrefetchCache(): void {\n cache.clear();\n inFlight.clear();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA0CA,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,kBAAkB;;;;;AAY/B,MAAM,wBAAQ,IAAI,IAAwB;;;;;;;AAQ1C,MAAM,2BAAW,IAAI,IAAY;;;;;;;;AASjC,SAAS,YAAqB;CAC5B,OAAO,OAAO,WAAW;AAC3B;AAEA,SAAS,cAAoB;CAC3B,MAAM,SAAS,MAAM,KAAK,EAAE,KAAK;CAEjC,IAAI,OAAO,SAAS,MAAM,MAAM,OAAO,OAAO,KAAK;AACrD;;;;;;;;;;;;;AAcA,eAAsB,iBAAiB,KAA4B;CACjE,IAAI,CAAC,UAAU,GAAG;CAElB,IAAI,SAAS,IAAI,GAAG,GAAG;CAIvB,MAAM,SAAS,MAAM,IAAI,GAAG;CAE5B,IAAI,WAAW,QAAW;EACxB,IAAI,OAAO,YAAY,KAAK,IAAI,GAAG;EAEnC,MAAM,OAAO,GAAG;CAClB;CAEA,SAAS,IAAI,GAAG;CAEhB,IAAI;EACF,MAAM,SAAS,MAAM,cAAc,GAAG;EAUtC,IAAI,OAAO,SAAS,WAAW;EAE/B,IAAI,MAAM,YAA8B,YAAY;EAEpD,MAAM,IAAI,KAAK;GAAE;GAAQ,WAAW,KAAK,IAAI,IAAI;EAAgB,CAAC;CACpE,QAAQ,CASR,UAAU;EAGR,SAAS,OAAO,GAAG;CACrB;AACF;;;;;;;;;;;;AAaA,SAAgB,uBAAuB,KAA6C;CAClF,MAAM,QAAQ,MAAM,IAAI,GAAG;CAE3B,IAAI,UAAU,QAAW,OAAO;CAEhC,MAAM,OAAO,GAAG;CAKhB,OAAO,MAAM,YAAY,KAAK,IAAI,IAAI,MAAM,SAAS;AACvD"}
1
+ {"version":3,"file":"prefetch.mjs","names":[],"sources":["../../../../../../../../web/src/client/navigation/prefetch.ts"],"sourcesContent":["/**\n * Speculative page data, fetched on hover and spent on the click that follows.\n *\n * A prefetch is a GUESS. The user pointed at a link; they may never click it.\n * Everything about this module follows from that one fact:\n *\n * - it never reports a failure — nobody asked for this request, so nobody may\n * be told it failed. A failed prefetch leaves the cache empty and the click\n * fetches for real, which is the behaviour we had before this file existed.\n * - it never delays a click. `prefetchPageData` is fire-and-forget; the\n * navigation path reads the cache synchronously and does not wait on it.\n * - it is BOUNDED and it EXPIRES. A cached payload is a copy of a page that\n * may already have changed, so it may only be served while it is very\n * probably still true.\n *\n * ## The read side is deliberately separate from the write side\n *\n * `<Link>` writes (on hover); the navigation runtime reads (on click). Neither\n * knows about the other — they share a URL, and this module is the only thing\n * between them. That is what lets prefetch be added without touching the\n * navigation state machine.\n */\nimport type { PageDataResult } from \"./fetch-page-data\";\nimport { fetchPageData } from \"./fetch-page-data\";\n\n/** A prefetch result worth keeping — only ever a successful payload. */\nexport type PrefetchedPageData = Extract<PageDataResult, { type: \"payload\" }>;\n\n/**\n * How many pages may be held at once.\n *\n * Ten, because the working set this serves is \"links the pointer has crossed in\n * the last few seconds\", which is small by construction — a nav bar, a card\n * grid the user is scanning. A page payload is loader data, not markup, but it\n * is still measured in tens of kilobytes, so an unbounded map on a long-lived\n * SPA session is a slow leak with no upper edge. Ten covers scanning a menu and\n * costs at most a few hundred kilobytes in the worst case.\n *\n * Eviction is by INSERTION ORDER, not by recency: an entry here is written once\n * and read at most once, so there is no recency to track — the oldest guess is\n * always the one least likely to be spent.\n */\nexport const PREFETCH_CACHE_LIMIT = 10;\n\n/**\n * How long a cached payload may be served, in milliseconds.\n *\n * Thirty seconds. This is a SAFETY bound and not a performance knob: the\n * interval this feature exists to cover is hover-to-click, which is well under\n * a second, and every millisecond beyond that is pure staleness risk. A payload\n * served after the user has changed the data behind it renders a wrong page —\n * silently, and with no way for them to tell. Thirty seconds is long enough\n * that a hesitant click still hits, and short enough that no realistic\n * \"navigate away, mutate something, come back\" flow can complete inside it.\n */\nexport const PREFETCH_TTL_MS = 30_000;\n\ntype CacheEntry = {\n result: PrefetchedPageData;\n /** `Date.now()` at which this entry stops being servable. */\n expiresAt: number;\n};\n\n/**\n * Insertion-ordered by `Map` contract, which is what makes eviction a `keys()\n * .next()` and not a bookkeeping structure.\n */\nconst cache = new Map<string, CacheEntry>();\n\n/**\n * URLs with a request already in the air. Repeated `mouseenter` events on the\n * same anchor are the norm, not the exception — a pointer crossing a link fires\n * as the user's hand settles — and without this each one would be its own\n * request for the same page.\n */\nconst inFlight = new Set<string>();\n\n/**\n * Whether there is a browser to prefetch from.\n *\n * Called rather than assumed because `<Link>` is universal: the same module\n * graph is evaluated during a server render, where a speculative request would\n * be a request the server makes to itself for a page nobody is looking at.\n */\nfunction isBrowser(): boolean {\n return typeof window !== \"undefined\";\n}\n\nfunction evictOldest(): void {\n const oldest = cache.keys().next();\n\n if (oldest.done !== true) cache.delete(oldest.value);\n}\n\n/**\n * Fetch a URL's page data ahead of the click, and cache it.\n *\n * NEVER REJECTS and never reports. The returned promise exists so a test can\n * await the speculative work; callers in the component tree discard it\n * (`void prefetchPageData(url)`) and must not await it — a click that waited on\n * a guess would be slower than one that never made it.\n *\n * The caller decides WHETHER a URL may be prefetched. This function does not\n * re-derive that: it has a URL and no way to tell an in-app path from a\n * cross-origin one it must never touch. `<Link>` gates on `isInApp`.\n */\nexport async function prefetchPageData(url: string): Promise<void> {\n if (!isBrowser()) return;\n\n if (inFlight.has(url)) return;\n\n // A fresh entry means the answer is already here; an expired one is dropped\n // now so the fetch below can replace it.\n const cached = cache.get(url);\n\n if (cached !== undefined) {\n if (cached.expiresAt > Date.now()) return;\n\n cache.delete(url);\n }\n\n inFlight.add(url);\n\n try {\n const result = await fetchPageData(url);\n\n /*\n `hard-navigate` is NOT cached. It is the signal that this URL needs a full\n page load — a 404, a redirect to an interstitial, a proxy that stripped\n the marker header. Caching it would mean the click either replays a\n failure or, worse, consults an entry that cannot be rendered. The click\n re-asks and gets the same answer, correctly, through the navigation path\n that already knows how to degrade.\n */\n if (result.type !== \"payload\") return;\n\n if (cache.size >= PREFETCH_CACHE_LIMIT) evictOldest();\n\n cache.set(url, { result, expiresAt: Date.now() + PREFETCH_TTL_MS });\n } catch {\n /*\n `fetchPageData` already converts every network failure into a\n `hard-navigate`, so reaching here means something unforeseen. It is\n swallowed anyway, deliberately and without a log: this request was\n speculative, the user never asked for it, and a console full of warnings\n about requests nobody made is how a helpful optimisation becomes noise\n that hides real errors.\n */\n } finally {\n // In `finally` so a failed attempt does not poison the URL for the rest of\n // the session — the next hover is allowed to try again.\n inFlight.delete(url);\n }\n}\n\n/**\n * Take the prefetched payload for a URL, if there is a live one.\n *\n * CONSUMING: the entry is removed whether or not the caller ends up using it.\n * One hover buys one saved round trip. Serving the same payload to a second\n * navigation would double the window in which it can be wrong, in exchange for\n * a saving the user did not notice the first time.\n *\n * @returns the cached result, or `undefined` — in which case the caller fetches\n * exactly as it did before this module existed.\n */\nexport function takePrefetchedPageData(url: string): PrefetchedPageData | undefined {\n const entry = cache.get(url);\n\n if (entry === undefined) return undefined;\n\n cache.delete(url);\n\n // Expiry is checked on READ, not on a timer: a timer would keep a\n // long-lived page waking up to clean a cache that is already bounded, and an\n // entry nobody reads costs nothing but the slot it is evicted from anyway.\n return entry.expiresAt > Date.now() ? entry.result : undefined;\n}\n\n/**\n * Drop everything. For tests, and for any caller that knows the cached pages\n * are now wrong — the module-level cache would otherwise outlive a suite and\n * leak into the next one.\n */\nexport function resetPrefetchCache(): void {\n cache.clear();\n inFlight.clear();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA0CA,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,kBAAkB;;;;;AAY/B,MAAM,wBAAQ,IAAI,IAAwB;;;;;;;AAQ1C,MAAM,2BAAW,IAAI,IAAY;;;;;;;;AASjC,SAAS,YAAqB;CAC5B,OAAO,OAAO,WAAW;AAC3B;AAEA,SAAS,cAAoB;CAC3B,MAAM,SAAS,MAAM,KAAK,CAAC,CAAC,KAAK;CAEjC,IAAI,OAAO,SAAS,MAAM,MAAM,OAAO,OAAO,KAAK;AACrD;;;;;;;;;;;;;AAcA,eAAsB,iBAAiB,KAA4B;CACjE,IAAI,CAAC,UAAU,GAAG;CAElB,IAAI,SAAS,IAAI,GAAG,GAAG;CAIvB,MAAM,SAAS,MAAM,IAAI,GAAG;CAE5B,IAAI,WAAW,QAAW;EACxB,IAAI,OAAO,YAAY,KAAK,IAAI,GAAG;EAEnC,MAAM,OAAO,GAAG;CAClB;CAEA,SAAS,IAAI,GAAG;CAEhB,IAAI;EACF,MAAM,SAAS,MAAM,cAAc,GAAG;EAUtC,IAAI,OAAO,SAAS,WAAW;EAE/B,IAAI,MAAM,YAA8B,YAAY;EAEpD,MAAM,IAAI,KAAK;GAAE;GAAQ,WAAW,KAAK,IAAI,IAAI;EAAgB,CAAC;CACpE,QAAQ,CASR,UAAU;EAGR,SAAS,OAAO,GAAG;CACrB;AACF;;;;;;;;;;;;AAaA,SAAgB,uBAAuB,KAA6C;CAClF,MAAM,QAAQ,MAAM,IAAI,GAAG;CAE3B,IAAI,UAAU,QAAW,OAAO;CAEhC,MAAM,OAAO,GAAG;CAKhB,OAAO,MAAM,YAAY,KAAK,IAAI,IAAI,MAAM,SAAS;AACvD"}
@@ -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\", \"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
+ {"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\", \"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 +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\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
+ {"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\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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"}