vinext 0.0.53 → 0.0.55

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 (276) hide show
  1. package/README.md +1 -0
  2. package/dist/build/inline-css.d.ts +7 -0
  3. package/dist/build/inline-css.js +50 -0
  4. package/dist/build/inline-css.js.map +1 -0
  5. package/dist/build/prerender.js +2 -1
  6. package/dist/build/prerender.js.map +1 -1
  7. package/dist/check.js +19 -3
  8. package/dist/check.js.map +1 -1
  9. package/dist/client/navigation-runtime.d.ts +3 -1
  10. package/dist/client/navigation-runtime.js +1 -1
  11. package/dist/client/navigation-runtime.js.map +1 -1
  12. package/dist/client/window-next.d.ts +7 -0
  13. package/dist/client/window-next.js.map +1 -1
  14. package/dist/config/next-config.d.ts +97 -2
  15. package/dist/config/next-config.js +155 -6
  16. package/dist/config/next-config.js.map +1 -1
  17. package/dist/config/tsconfig-paths.d.ts +12 -3
  18. package/dist/config/tsconfig-paths.js +55 -24
  19. package/dist/config/tsconfig-paths.js.map +1 -1
  20. package/dist/deploy.js +13 -0
  21. package/dist/deploy.js.map +1 -1
  22. package/dist/entries/app-browser-entry.d.ts +11 -1
  23. package/dist/entries/app-browser-entry.js +16 -6
  24. package/dist/entries/app-browser-entry.js.map +1 -1
  25. package/dist/entries/app-rsc-entry.d.ts +9 -1
  26. package/dist/entries/app-rsc-entry.js +30 -5
  27. package/dist/entries/app-rsc-entry.js.map +1 -1
  28. package/dist/entries/app-rsc-manifest.d.ts +21 -1
  29. package/dist/entries/app-rsc-manifest.js +28 -9
  30. package/dist/entries/app-rsc-manifest.js.map +1 -1
  31. package/dist/entries/pages-client-entry.d.ts +4 -1
  32. package/dist/entries/pages-client-entry.js +18 -2
  33. package/dist/entries/pages-client-entry.js.map +1 -1
  34. package/dist/entries/pages-server-entry.js +123 -8
  35. package/dist/entries/pages-server-entry.js.map +1 -1
  36. package/dist/entries/runtime-entry-module.d.ts +1 -10
  37. package/dist/entries/runtime-entry-module.js +2 -12
  38. package/dist/entries/runtime-entry-module.js.map +1 -1
  39. package/dist/index.js +144 -44
  40. package/dist/index.js.map +1 -1
  41. package/dist/plugins/import-meta-url.d.ts +16 -0
  42. package/dist/plugins/import-meta-url.js +193 -0
  43. package/dist/plugins/import-meta-url.js.map +1 -0
  44. package/dist/plugins/remove-console.d.ts +16 -0
  45. package/dist/plugins/remove-console.js +176 -0
  46. package/dist/plugins/remove-console.js.map +1 -0
  47. package/dist/routing/app-route-graph.d.ts +24 -1
  48. package/dist/routing/app-route-graph.js +52 -4
  49. package/dist/routing/app-route-graph.js.map +1 -1
  50. package/dist/routing/app-router.d.ts +2 -2
  51. package/dist/routing/app-router.js +2 -2
  52. package/dist/routing/app-router.js.map +1 -1
  53. package/dist/routing/file-matcher.d.ts +21 -1
  54. package/dist/routing/file-matcher.js +39 -1
  55. package/dist/routing/file-matcher.js.map +1 -1
  56. package/dist/routing/pages-router.d.ts +1 -1
  57. package/dist/routing/pages-router.js +10 -3
  58. package/dist/routing/pages-router.js.map +1 -1
  59. package/dist/server/api-handler.js +1 -1
  60. package/dist/server/app-browser-action-result.d.ts +9 -16
  61. package/dist/server/app-browser-action-result.js +25 -14
  62. package/dist/server/app-browser-action-result.js.map +1 -1
  63. package/dist/server/app-browser-entry.js +195 -60
  64. package/dist/server/app-browser-entry.js.map +1 -1
  65. package/dist/server/app-browser-mpa-navigation.d.ts +16 -0
  66. package/dist/server/app-browser-mpa-navigation.js +36 -0
  67. package/dist/server/app-browser-mpa-navigation.js.map +1 -0
  68. package/dist/server/app-browser-navigation-controller.d.ts +2 -0
  69. package/dist/server/app-browser-navigation-controller.js +4 -0
  70. package/dist/server/app-browser-navigation-controller.js.map +1 -1
  71. package/dist/server/app-browser-popstate.d.ts +3 -1
  72. package/dist/server/app-browser-popstate.js +15 -1
  73. package/dist/server/app-browser-popstate.js.map +1 -1
  74. package/dist/server/app-browser-state.js +2 -1
  75. package/dist/server/app-browser-state.js.map +1 -1
  76. package/dist/server/app-elements-wire.d.ts +13 -4
  77. package/dist/server/app-elements-wire.js +10 -1
  78. package/dist/server/app-elements-wire.js.map +1 -1
  79. package/dist/server/app-elements.d.ts +2 -2
  80. package/dist/server/app-elements.js +2 -2
  81. package/dist/server/app-elements.js.map +1 -1
  82. package/dist/server/app-fallback-renderer.d.ts +15 -5
  83. package/dist/server/app-fallback-renderer.js +10 -4
  84. package/dist/server/app-fallback-renderer.js.map +1 -1
  85. package/dist/server/app-inline-css-client.d.ts +7 -0
  86. package/dist/server/app-inline-css-client.js +37 -0
  87. package/dist/server/app-inline-css-client.js.map +1 -0
  88. package/dist/server/app-layout-param-observation.d.ts +30 -0
  89. package/dist/server/app-layout-param-observation.js +130 -0
  90. package/dist/server/app-layout-param-observation.js.map +1 -0
  91. package/dist/server/app-page-boundary-render.js +2 -2
  92. package/dist/server/app-page-boundary-render.js.map +1 -1
  93. package/dist/server/app-page-boundary.d.ts +21 -1
  94. package/dist/server/app-page-boundary.js +28 -3
  95. package/dist/server/app-page-boundary.js.map +1 -1
  96. package/dist/server/app-page-cache.d.ts +7 -3
  97. package/dist/server/app-page-cache.js +7 -7
  98. package/dist/server/app-page-cache.js.map +1 -1
  99. package/dist/server/app-page-dispatch.d.ts +10 -1
  100. package/dist/server/app-page-dispatch.js +126 -79
  101. package/dist/server/app-page-dispatch.js.map +1 -1
  102. package/dist/server/app-page-element-builder.js +12 -28
  103. package/dist/server/app-page-element-builder.js.map +1 -1
  104. package/dist/server/app-page-params.d.ts +2 -1
  105. package/dist/server/app-page-params.js +14 -1
  106. package/dist/server/app-page-params.js.map +1 -1
  107. package/dist/server/app-page-probe.d.ts +12 -1
  108. package/dist/server/app-page-probe.js +116 -1
  109. package/dist/server/app-page-probe.js.map +1 -1
  110. package/dist/server/app-page-render-identity.d.ts +22 -0
  111. package/dist/server/app-page-render-identity.js +42 -0
  112. package/dist/server/app-page-render-identity.js.map +1 -0
  113. package/dist/server/app-page-render.d.ts +8 -1
  114. package/dist/server/app-page-render.js +4 -1
  115. package/dist/server/app-page-render.js.map +1 -1
  116. package/dist/server/app-page-request.d.ts +6 -3
  117. package/dist/server/app-page-request.js +5 -2
  118. package/dist/server/app-page-request.js.map +1 -1
  119. package/dist/server/app-page-response.js +2 -2
  120. package/dist/server/app-page-response.js.map +1 -1
  121. package/dist/server/app-page-route-wiring.d.ts +15 -0
  122. package/dist/server/app-page-route-wiring.js +7 -5
  123. package/dist/server/app-page-route-wiring.js.map +1 -1
  124. package/dist/server/app-page-stream.d.ts +11 -0
  125. package/dist/server/app-page-stream.js +1 -0
  126. package/dist/server/app-page-stream.js.map +1 -1
  127. package/dist/server/app-route-handler-response.js +37 -5
  128. package/dist/server/app-route-handler-response.js.map +1 -1
  129. package/dist/server/app-rsc-cache-busting.d.ts +3 -2
  130. package/dist/server/app-rsc-cache-busting.js +9 -7
  131. package/dist/server/app-rsc-cache-busting.js.map +1 -1
  132. package/dist/server/app-rsc-handler.d.ts +14 -3
  133. package/dist/server/app-rsc-handler.js +56 -6
  134. package/dist/server/app-rsc-handler.js.map +1 -1
  135. package/dist/server/app-rsc-request-normalization.d.ts +2 -1
  136. package/dist/server/app-rsc-request-normalization.js +3 -2
  137. package/dist/server/app-rsc-request-normalization.js.map +1 -1
  138. package/dist/server/app-segment-config.d.ts +1 -1
  139. package/dist/server/app-segment-config.js +4 -1
  140. package/dist/server/app-segment-config.js.map +1 -1
  141. package/dist/server/app-server-action-execution.d.ts +26 -3
  142. package/dist/server/app-server-action-execution.js +240 -29
  143. package/dist/server/app-server-action-execution.js.map +1 -1
  144. package/dist/server/app-ssr-entry.d.ts +6 -0
  145. package/dist/server/app-ssr-entry.js +22 -7
  146. package/dist/server/app-ssr-entry.js.map +1 -1
  147. package/dist/server/app-ssr-error-meta.js +3 -3
  148. package/dist/server/app-ssr-error-meta.js.map +1 -1
  149. package/dist/server/app-ssr-stream.d.ts +2 -1
  150. package/dist/server/app-ssr-stream.js +176 -31
  151. package/dist/server/app-ssr-stream.js.map +1 -1
  152. package/dist/server/artifact-compatibility.d.ts +2 -1
  153. package/dist/server/artifact-compatibility.js +10 -1
  154. package/dist/server/artifact-compatibility.js.map +1 -1
  155. package/dist/server/client-reuse-manifest.d.ts +9 -4
  156. package/dist/server/client-reuse-manifest.js +2 -1
  157. package/dist/server/client-reuse-manifest.js.map +1 -1
  158. package/dist/server/client-trace-metadata.d.ts +31 -0
  159. package/dist/server/client-trace-metadata.js +83 -0
  160. package/dist/server/client-trace-metadata.js.map +1 -0
  161. package/dist/server/cookie-utils.d.ts +13 -0
  162. package/dist/server/cookie-utils.js +20 -0
  163. package/dist/server/cookie-utils.js.map +1 -0
  164. package/dist/server/dev-server.d.ts +8 -1
  165. package/dist/server/dev-server.js +83 -12
  166. package/dist/server/dev-server.js.map +1 -1
  167. package/dist/server/document-initial-head.d.ts +7 -0
  168. package/dist/server/document-initial-head.js +35 -0
  169. package/dist/server/document-initial-head.js.map +1 -0
  170. package/dist/server/html.d.ts +2 -1
  171. package/dist/server/html.js +6 -1
  172. package/dist/server/html.js.map +1 -1
  173. package/dist/server/isr-cache.d.ts +7 -5
  174. package/dist/server/isr-cache.js +17 -6
  175. package/dist/server/isr-cache.js.map +1 -1
  176. package/dist/server/middleware-runtime.js +1 -2
  177. package/dist/server/middleware-runtime.js.map +1 -1
  178. package/dist/server/pages-document-initial-props.d.ts +89 -0
  179. package/dist/server/pages-document-initial-props.js +140 -0
  180. package/dist/server/pages-document-initial-props.js.map +1 -0
  181. package/dist/server/pages-node-compat.js +1 -1
  182. package/dist/server/pages-page-data.js +3 -0
  183. package/dist/server/pages-page-data.js.map +1 -1
  184. package/dist/server/pages-page-method.d.ts +48 -0
  185. package/dist/server/pages-page-method.js +19 -0
  186. package/dist/server/pages-page-method.js.map +1 -0
  187. package/dist/server/pages-page-response.d.ts +20 -0
  188. package/dist/server/pages-page-response.js +37 -7
  189. package/dist/server/pages-page-response.js.map +1 -1
  190. package/dist/server/pages-serializable-props.d.ts +25 -0
  191. package/dist/server/pages-serializable-props.js +69 -0
  192. package/dist/server/pages-serializable-props.js.map +1 -0
  193. package/dist/server/prod-server.js +16 -6
  194. package/dist/server/prod-server.js.map +1 -1
  195. package/dist/server/server-action-not-found.js +3 -2
  196. package/dist/server/server-action-not-found.js.map +1 -1
  197. package/dist/server/skip-cache-proof.d.ts +23 -2
  198. package/dist/server/skip-cache-proof.js +81 -12
  199. package/dist/server/skip-cache-proof.js.map +1 -1
  200. package/dist/server/static-file-cache.js +2 -1
  201. package/dist/server/static-file-cache.js.map +1 -1
  202. package/dist/server/static-layout-client-reuse-proof.d.ts +16 -0
  203. package/dist/server/static-layout-client-reuse-proof.js +35 -0
  204. package/dist/server/static-layout-client-reuse-proof.js.map +1 -0
  205. package/dist/shims/app-router-scroll-state.d.ts +4 -2
  206. package/dist/shims/app-router-scroll-state.js +16 -3
  207. package/dist/shims/app-router-scroll-state.js.map +1 -1
  208. package/dist/shims/app-router-scroll.d.ts +16 -2
  209. package/dist/shims/app-router-scroll.js +18 -3
  210. package/dist/shims/app-router-scroll.js.map +1 -1
  211. package/dist/shims/cache.d.ts +27 -1
  212. package/dist/shims/cache.js +108 -6
  213. package/dist/shims/cache.js.map +1 -1
  214. package/dist/shims/document.d.ts +6 -0
  215. package/dist/shims/document.js +7 -8
  216. package/dist/shims/document.js.map +1 -1
  217. package/dist/shims/error-boundary.d.ts +4 -4
  218. package/dist/shims/error-boundary.js +27 -28
  219. package/dist/shims/error-boundary.js.map +1 -1
  220. package/dist/shims/error.js +3 -0
  221. package/dist/shims/error.js.map +1 -1
  222. package/dist/shims/fetch-cache.d.ts +3 -1
  223. package/dist/shims/fetch-cache.js +16 -5
  224. package/dist/shims/fetch-cache.js.map +1 -1
  225. package/dist/shims/hash-scroll.d.ts +4 -1
  226. package/dist/shims/hash-scroll.js +13 -1
  227. package/dist/shims/hash-scroll.js.map +1 -1
  228. package/dist/shims/head-state.d.ts +1 -0
  229. package/dist/shims/head-state.js +18 -3
  230. package/dist/shims/head-state.js.map +1 -1
  231. package/dist/shims/head.d.ts +35 -1
  232. package/dist/shims/head.js +113 -14
  233. package/dist/shims/head.js.map +1 -1
  234. package/dist/shims/headers.d.ts +7 -0
  235. package/dist/shims/headers.js +9 -1
  236. package/dist/shims/headers.js.map +1 -1
  237. package/dist/shims/internal/app-route-detection.d.ts +37 -0
  238. package/dist/shims/internal/app-route-detection.js +69 -0
  239. package/dist/shims/internal/app-route-detection.js.map +1 -0
  240. package/dist/shims/internal/pages-data-fetch-dedup.d.ts +56 -0
  241. package/dist/shims/internal/pages-data-fetch-dedup.js +70 -0
  242. package/dist/shims/internal/pages-data-fetch-dedup.js.map +1 -0
  243. package/dist/shims/link.d.ts +18 -2
  244. package/dist/shims/link.js +98 -8
  245. package/dist/shims/link.js.map +1 -1
  246. package/dist/shims/metadata.d.ts +7 -6
  247. package/dist/shims/metadata.js +9 -5
  248. package/dist/shims/metadata.js.map +1 -1
  249. package/dist/shims/navigation.d.ts +40 -3
  250. package/dist/shims/navigation.js +124 -25
  251. package/dist/shims/navigation.js.map +1 -1
  252. package/dist/shims/router.d.ts +5 -0
  253. package/dist/shims/router.js +51 -21
  254. package/dist/shims/router.js.map +1 -1
  255. package/dist/shims/script.d.ts +11 -1
  256. package/dist/shims/script.js +75 -6
  257. package/dist/shims/script.js.map +1 -1
  258. package/dist/shims/thenable-params.d.ts +5 -2
  259. package/dist/shims/thenable-params.js +25 -1
  260. package/dist/shims/thenable-params.js.map +1 -1
  261. package/dist/shims/unified-request-context.js +3 -0
  262. package/dist/shims/unified-request-context.js.map +1 -1
  263. package/dist/utils/client-build-manifest.d.ts +15 -0
  264. package/dist/utils/client-build-manifest.js +54 -0
  265. package/dist/utils/client-build-manifest.js.map +1 -0
  266. package/dist/utils/hash.js +1 -1
  267. package/dist/utils/hash.js.map +1 -1
  268. package/dist/utils/lazy-chunks.d.ts +1 -1
  269. package/dist/utils/lazy-chunks.js.map +1 -1
  270. package/dist/utils/path.d.ts +13 -0
  271. package/dist/utils/path.js +16 -0
  272. package/dist/utils/path.js.map +1 -0
  273. package/dist/utils/vite-version.d.ts +11 -0
  274. package/dist/utils/vite-version.js +36 -0
  275. package/dist/utils/vite-version.js.map +1 -0
  276. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"server-action-not-found.js","names":["SERVER_ACTION_NOT_FOUND_HEADER"],"sources":["../../src/server/server-action-not-found.ts"],"sourcesContent":["import { NEXTJS_ACTION_NOT_FOUND_HEADER as SERVER_ACTION_NOT_FOUND_HEADER } from \"./headers.js\";\nimport { UnrecognizedActionError } from \"vinext/shims/unrecognized-action-error\";\n\nconst SERVER_ACTION_NOT_FOUND_DOCS =\n \"https://nextjs.org/docs/messages/failed-to-find-server-action\";\nconst SERVER_ACTION_NOT_FOUND_BODY = \"Server action not found.\";\n\nfunction getServerActionNotFoundPrefix(actionId: string | null): string {\n return `Failed to find Server Action${actionId ? ` \"${actionId}\"` : \"\"}.`;\n}\n\nexport function getServerActionNotFoundMessage(actionId: string | null): string {\n return `${getServerActionNotFoundPrefix(\n actionId,\n )} This request might be from an older or newer deployment.\\nRead more: ${SERVER_ACTION_NOT_FOUND_DOCS}`;\n}\n\nfunction getServerActionNotFoundClientMessage(actionId: string): string {\n return `Server Action \"${actionId}\" was not found on the server. \\nRead more: ${SERVER_ACTION_NOT_FOUND_DOCS}`;\n}\n\nfunction getUnknownMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n\n return typeof error === \"string\" ? error : \"\";\n}\n\nexport function isServerActionNotFoundError(error: unknown, actionId: string | null): boolean {\n const message = getUnknownMessage(error);\n if (!message) {\n return false;\n }\n\n if (actionId && message.startsWith(getServerActionNotFoundPrefix(actionId))) {\n return true;\n }\n\n if (!actionId && message.startsWith(\"Failed to find Server Action\")) {\n return true;\n }\n\n // `@vitejs/plugin-rsc` raises two different \"no such server reference\"\n // errors depending on the build mode. Both mean the same thing — the\n // referenced server action id isn't in the runtime manifest — and must\n // surface as Next.js' 404 + action-not-found header rather than a generic\n // 500. The progressive (no-JS) path also hits this in `decodeAction(body)`\n // before it has any actionId in hand, so match these patterns whether or\n // not the caller has resolved an action id from request headers.\n //\n // - dev: `[vite-rsc] invalid server reference '<id>'` (from the reference\n // validation virtual module loaded ahead of dynamic import)\n // - prod: `server reference not found '<id>'` (from the built\n // `virtual:vite-rsc/server-references` lookup, including the case\n // where the build has no server actions at all)\n //\n // See: @vitejs/plugin-rsc dist/rsc.js (`server reference not found`) and\n // dist/plugin-*.js (`[vite-rsc] invalid <type> reference`).\n if (actionId) {\n if (message.includes(`[vite-rsc] invalid server reference '${actionId}'`)) {\n return true;\n }\n if (message.includes(`server reference not found '${actionId}'`)) {\n return true;\n }\n return false;\n }\n\n return (\n /\\[vite-rsc] invalid server reference '/.test(message) ||\n /server reference not found '/.test(message)\n );\n}\n\nexport function createServerActionNotFoundResponse(): Response {\n return new Response(SERVER_ACTION_NOT_FOUND_BODY, {\n status: 404,\n headers: {\n [SERVER_ACTION_NOT_FOUND_HEADER]: \"1\",\n \"content-type\": \"text/plain\",\n },\n });\n}\n\nfunction isServerActionNotFoundResponse(response: Pick<Response, \"headers\">): boolean {\n return response.headers.get(SERVER_ACTION_NOT_FOUND_HEADER) === \"1\";\n}\n\n/**\n * Throw an `UnrecognizedActionError` when the server reported the requested\n * server action id as unknown (the `x-nextjs-action-not-found` response\n * header); otherwise return so the caller can keep processing the response.\n *\n * The client-side counterpart of `createServerActionNotFoundResponse`. The\n * typed error lets client `catch` blocks call the public\n * `unstable_isUnrecognizedActionError` predicate to detect client/server\n * deployment skew and recover (typically by reloading the page).\n *\n * Mirrors Next.js, whose server-action reducer throws `UnrecognizedActionError`\n * on this same response header:\n * https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts\n */\nexport function throwOnServerActionNotFound(\n response: Pick<Response, \"headers\">,\n actionId: string,\n): void {\n if (isServerActionNotFoundResponse(response)) {\n throw new UnrecognizedActionError(getServerActionNotFoundClientMessage(actionId));\n }\n}\n"],"mappings":";;;AAGA,MAAM,+BACJ;AACF,MAAM,+BAA+B;AAErC,SAAS,8BAA8B,UAAiC;CACtE,OAAO,+BAA+B,WAAW,KAAK,SAAS,KAAK,GAAG;;AAGzE,SAAgB,+BAA+B,UAAiC;CAC9E,OAAO,GAAG,8BACR,SACD,CAAC,wEAAwE;;AAG5E,SAAS,qCAAqC,UAA0B;CACtE,OAAO,kBAAkB,SAAS,8CAA8C;;AAGlF,SAAS,kBAAkB,OAAwB;CACjD,IAAI,iBAAiB,OACnB,OAAO,MAAM;CAGf,OAAO,OAAO,UAAU,WAAW,QAAQ;;AAG7C,SAAgB,4BAA4B,OAAgB,UAAkC;CAC5F,MAAM,UAAU,kBAAkB,MAAM;CACxC,IAAI,CAAC,SACH,OAAO;CAGT,IAAI,YAAY,QAAQ,WAAW,8BAA8B,SAAS,CAAC,EACzE,OAAO;CAGT,IAAI,CAAC,YAAY,QAAQ,WAAW,+BAA+B,EACjE,OAAO;CAmBT,IAAI,UAAU;EACZ,IAAI,QAAQ,SAAS,wCAAwC,SAAS,GAAG,EACvE,OAAO;EAET,IAAI,QAAQ,SAAS,+BAA+B,SAAS,GAAG,EAC9D,OAAO;EAET,OAAO;;CAGT,OACE,yCAAyC,KAAK,QAAQ,IACtD,+BAA+B,KAAK,QAAQ;;AAIhD,SAAgB,qCAA+C;CAC7D,OAAO,IAAI,SAAS,8BAA8B;EAChD,QAAQ;EACR,SAAS;IACNA,iCAAiC;GAClC,gBAAgB;GACjB;EACF,CAAC;;AAGJ,SAAS,+BAA+B,UAA8C;CACpF,OAAO,SAAS,QAAQ,IAAIA,+BAA+B,KAAK;;;;;;;;;;;;;;;;AAiBlE,SAAgB,4BACd,UACA,UACM;CACN,IAAI,+BAA+B,SAAS,EAC1C,MAAM,IAAI,wBAAwB,qCAAqC,SAAS,CAAC"}
1
+ {"version":3,"file":"server-action-not-found.js","names":["SERVER_ACTION_NOT_FOUND_HEADER"],"sources":["../../src/server/server-action-not-found.ts"],"sourcesContent":["import { NEXTJS_ACTION_NOT_FOUND_HEADER as SERVER_ACTION_NOT_FOUND_HEADER } from \"./headers.js\";\nimport { UnrecognizedActionError } from \"vinext/shims/unrecognized-action-error\";\n\nconst SERVER_ACTION_NOT_FOUND_DOCS =\n \"https://nextjs.org/docs/messages/failed-to-find-server-action\";\nconst SERVER_ACTION_NOT_FOUND_BODY = \"Server action not found.\";\n\nfunction getServerActionNotFoundPrefix(actionId: string | null): string {\n return `Failed to find Server Action${actionId ? ` \"${actionId}\"` : \"\"}.`;\n}\n\nexport function getServerActionNotFoundMessage(actionId: string | null): string {\n return `${getServerActionNotFoundPrefix(\n actionId,\n )} This request might be from an older or newer deployment.\\nRead more: ${SERVER_ACTION_NOT_FOUND_DOCS}`;\n}\n\nfunction getServerActionNotFoundClientMessage(actionId: string): string {\n return `Server Action \"${actionId}\" was not found on the server. \\nRead more: ${SERVER_ACTION_NOT_FOUND_DOCS}`;\n}\n\nfunction getUnknownMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n\n return typeof error === \"string\" ? error : \"\";\n}\n\nexport function isServerActionNotFoundError(error: unknown, actionId: string | null): boolean {\n const message = getUnknownMessage(error);\n if (!message) {\n return false;\n }\n\n if (actionId && message.startsWith(getServerActionNotFoundPrefix(actionId))) {\n return true;\n }\n\n if (!actionId && message.startsWith(\"Failed to find Server Action\")) {\n return true;\n }\n\n // `@vitejs/plugin-rsc` raises two different \"no such server reference\"\n // errors depending on the build mode. Both mean the same thing — the\n // referenced server action id isn't in the runtime manifest — and must\n // surface as Next.js' 404 + action-not-found header rather than a generic\n // 500. The progressive (no-JS) path also hits this in `decodeAction(body)`\n // before it has any actionId in hand, so match these patterns whether or\n // not the caller has resolved an action id from request headers.\n //\n // - dev: `[vite-rsc] invalid server reference '<id>'` (from the reference\n // validation virtual module loaded ahead of dynamic import)\n // - prod: `server reference not found '<id>'` (from the built\n // `virtual:vite-rsc/server-references` lookup, including the case\n // where the build has no server actions at all)\n //\n // See: @vitejs/plugin-rsc dist/rsc.js (`server reference not found`) and\n // dist/plugin-*.js (`[vite-rsc] invalid <type> reference`).\n //\n // Action ids resolved from request headers carry the `#<exportName>` suffix\n // (e.g. `/app/foo.ts#bar`), but `loadServerAction(id)` strips that suffix\n // before calling `requireModule(file)`. The dev-mode validator therefore\n // emits the module path WITHOUT the `#<exportName>` — so we also check the\n // pre-`#` portion to match either shape (#1340).\n if (actionId) {\n const moduleId = actionId.split(\"#\")[0];\n if (\n message.includes(`[vite-rsc] invalid server reference '${actionId}'`) ||\n (moduleId &&\n moduleId !== actionId &&\n message.includes(`[vite-rsc] invalid server reference '${moduleId}'`))\n ) {\n return true;\n }\n if (\n message.includes(`server reference not found '${actionId}'`) ||\n (moduleId &&\n moduleId !== actionId &&\n message.includes(`server reference not found '${moduleId}'`))\n ) {\n return true;\n }\n return false;\n }\n\n return (\n /\\[vite-rsc] invalid server reference '/.test(message) ||\n /server reference not found '/.test(message)\n );\n}\n\nexport function createServerActionNotFoundResponse(): Response {\n return new Response(SERVER_ACTION_NOT_FOUND_BODY, {\n status: 404,\n headers: {\n [SERVER_ACTION_NOT_FOUND_HEADER]: \"1\",\n \"content-type\": \"text/plain\",\n },\n });\n}\n\nfunction isServerActionNotFoundResponse(response: Pick<Response, \"headers\">): boolean {\n return response.headers.get(SERVER_ACTION_NOT_FOUND_HEADER) === \"1\";\n}\n\n/**\n * Throw an `UnrecognizedActionError` when the server reported the requested\n * server action id as unknown (the `x-nextjs-action-not-found` response\n * header); otherwise return so the caller can keep processing the response.\n *\n * The client-side counterpart of `createServerActionNotFoundResponse`. The\n * typed error lets client `catch` blocks call the public\n * `unstable_isUnrecognizedActionError` predicate to detect client/server\n * deployment skew and recover (typically by reloading the page).\n *\n * Mirrors Next.js, whose server-action reducer throws `UnrecognizedActionError`\n * on this same response header:\n * https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts\n */\nexport function throwOnServerActionNotFound(\n response: Pick<Response, \"headers\">,\n actionId: string,\n): void {\n if (isServerActionNotFoundResponse(response)) {\n throw new UnrecognizedActionError(getServerActionNotFoundClientMessage(actionId));\n }\n}\n"],"mappings":";;;AAGA,MAAM,+BACJ;AACF,MAAM,+BAA+B;AAErC,SAAS,8BAA8B,UAAiC;CACtE,OAAO,+BAA+B,WAAW,KAAK,SAAS,KAAK,GAAG;;AAGzE,SAAgB,+BAA+B,UAAiC;CAC9E,OAAO,GAAG,8BACR,SACD,CAAC,wEAAwE;;AAG5E,SAAS,qCAAqC,UAA0B;CACtE,OAAO,kBAAkB,SAAS,8CAA8C;;AAGlF,SAAS,kBAAkB,OAAwB;CACjD,IAAI,iBAAiB,OACnB,OAAO,MAAM;CAGf,OAAO,OAAO,UAAU,WAAW,QAAQ;;AAG7C,SAAgB,4BAA4B,OAAgB,UAAkC;CAC5F,MAAM,UAAU,kBAAkB,MAAM;CACxC,IAAI,CAAC,SACH,OAAO;CAGT,IAAI,YAAY,QAAQ,WAAW,8BAA8B,SAAS,CAAC,EACzE,OAAO;CAGT,IAAI,CAAC,YAAY,QAAQ,WAAW,+BAA+B,EACjE,OAAO;CAyBT,IAAI,UAAU;EACZ,MAAM,WAAW,SAAS,MAAM,IAAI,CAAC;EACrC,IACE,QAAQ,SAAS,wCAAwC,SAAS,GAAG,IACpE,YACC,aAAa,YACb,QAAQ,SAAS,wCAAwC,SAAS,GAAG,EAEvE,OAAO;EAET,IACE,QAAQ,SAAS,+BAA+B,SAAS,GAAG,IAC3D,YACC,aAAa,YACb,QAAQ,SAAS,+BAA+B,SAAS,GAAG,EAE9D,OAAO;EAET,OAAO;;CAGT,OACE,yCAAyC,KAAK,QAAQ,IACtD,+BAA+B,KAAK,QAAQ;;AAIhD,SAAgB,qCAA+C;CAC7D,OAAO,IAAI,SAAS,8BAA8B;EAChD,QAAQ;EACR,SAAS;IACNA,iCAAiC;GAClC,gBAAgB;GACjB;EACF,CAAC;;AAGJ,SAAS,+BAA+B,UAA8C;CACpF,OAAO,SAAS,QAAQ,IAAIA,+BAA+B,KAAK;;;;;;;;;;;;;;;;AAiBlE,SAAgB,4BACd,UACA,UACM;CACN,IAAI,+BAA+B,SAAS,EAC1C,MAAM,IAAI,wBAAwB,qCAAqC,SAAS,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import { ArtifactCompatibilityEnvelope, ArtifactCompatibilityEvaluationOptions } from "./artifact-compatibility.js";
2
2
  import { StaticLayoutArtifactReuseDecision } from "./cache-proof.js";
3
- import { ClientReuseManifestEntry, ClientReuseManifestEntryRejection, ClientReuseManifestSkipDisposition, ClientReuseManifestTraceFields } from "./client-reuse-manifest.js";
3
+ import { ClientReuseManifestEntry, ClientReuseManifestEntryRejection, ClientReuseManifestParseResult, ClientReuseManifestRejection, ClientReuseManifestSkipDisposition, ClientReuseManifestTraceFields } from "./client-reuse-manifest.js";
4
+ import { createStaticLayoutClientReuseArtifactCompatibility, createStaticLayoutClientReusePayloadHash, createStaticLayoutClientReuseRouteId } from "./static-layout-client-reuse-proof.js";
4
5
 
5
6
  //#region src/server/skip-cache-proof.d.ts
6
7
  type SkipCacheInvalidationProof = Readonly<{
@@ -35,7 +36,27 @@ type CrossCheckClientReuseManifestEntryWithCacheInput = Readonly<{
35
36
  cacheDecision: StaticLayoutArtifactReuseDecision | null;
36
37
  entry: ClientReuseManifestEntry;
37
38
  }> & ArtifactCompatibilityEvaluationOptions;
39
+ type ClientReuseSkipTransportPlan = Readonly<{
40
+ entryRejections: readonly ClientReuseManifestEntryRejection[];
41
+ kind: "renderAndSend";
42
+ manifestRejection?: ClientReuseManifestRejection;
43
+ skipDisposition: ClientReuseManifestSkipDisposition;
44
+ skipIneligibleEntryIds: readonly string[];
45
+ skippedEntryIds: readonly string[];
46
+ }> | Readonly<{
47
+ entryRejections: readonly ClientReuseManifestEntryRejection[];
48
+ kind: "skip";
49
+ skipDisposition: ClientReuseManifestSkipDisposition;
50
+ skipIneligibleEntryIds: readonly string[];
51
+ skippedEntryIds: readonly string[];
52
+ }>;
53
+ type CreateClientReuseSkipTransportPlanInput = Readonly<{
54
+ manifest: ClientReuseManifestParseResult;
55
+ maxWireEntriesToVerify?: number;
56
+ verifyEntry: (entry: ClientReuseManifestEntry) => SkipCacheCrossCheckResult;
57
+ }>;
38
58
  declare function crossCheckClientReuseManifestEntryWithCache(input: CrossCheckClientReuseManifestEntryWithCacheInput): SkipCacheCrossCheckResult;
59
+ declare function createClientReuseSkipTransportPlan(input: CreateClientReuseSkipTransportPlanInput): ClientReuseSkipTransportPlan;
39
60
  //#endregion
40
- export { SkipCacheInvalidationProof, crossCheckClientReuseManifestEntryWithCache };
61
+ export { SkipCacheInvalidationProof, createClientReuseSkipTransportPlan, createStaticLayoutClientReuseArtifactCompatibility, createStaticLayoutClientReusePayloadHash, createStaticLayoutClientReuseRouteId, crossCheckClientReuseManifestEntryWithCache };
41
62
  //# sourceMappingURL=skip-cache-proof.d.ts.map
@@ -1,15 +1,7 @@
1
- import { evaluateArtifactCompatibility } from "./artifact-compatibility.js";
1
+ import { ARTIFACT_COMPATIBILITY_PROOF_FIELDS, evaluateArtifactCompatibility } from "./artifact-compatibility.js";
2
2
  import { createClientReusePayloadHash } from "./client-reuse-manifest.js";
3
+ import { createStaticLayoutClientReuseArtifactCompatibility, createStaticLayoutClientReusePayloadHash, createStaticLayoutClientReuseRouteId } from "./static-layout-client-reuse-proof.js";
3
4
  //#region src/server/skip-cache-proof.ts
4
- const ARTIFACT_COMPATIBILITY_PROOF_FIELDS = [
5
- "schemaVersion",
6
- "graphVersion",
7
- "deploymentVersion",
8
- "appElementsSchemaVersion",
9
- "rscPayloadSchemaVersion",
10
- "rootBoundaryId",
11
- "renderEpoch"
12
- ];
13
5
  function createDisabledSkipDisposition() {
14
6
  return {
15
7
  code: "SKIP_MODEL_DISABLED",
@@ -17,6 +9,14 @@ function createDisabledSkipDisposition() {
17
9
  mode: "renderAndSend"
18
10
  };
19
11
  }
12
+ function createStaticLayoutSkipDisposition(skippedEntryIds) {
13
+ return {
14
+ code: "SKIP_STATIC_LAYOUT_VERIFIED",
15
+ enabled: true,
16
+ mode: "skipStaticLayout",
17
+ skippedEntryIds: [...skippedEntryIds]
18
+ };
19
+ }
20
20
  function rejectSkipCacheCrossCheck(entry, code, fields = {}) {
21
21
  return {
22
22
  kind: "rejected",
@@ -33,9 +33,31 @@ function collectArtifactCompatibilityProofMismatches(artifactCompatibility, proo
33
33
  for (const field of ARTIFACT_COMPATIBILITY_PROOF_FIELDS) if (artifactCompatibility[field] !== proofCompatibility[field]) mismatchedFields.push(field);
34
34
  return mismatchedFields;
35
35
  }
36
+ function isExactArtifactCompatibility(artifactCompatibility, entryCompatibility) {
37
+ return collectArtifactCompatibilityProofMismatches(artifactCompatibility, entryCompatibility).length === 0;
38
+ }
36
39
  function assertNever(value) {
37
40
  throw new Error(`Unhandled skip/cache proof state: ${String(value)}`);
38
41
  }
42
+ function createRenderAndSendPlan(options) {
43
+ return {
44
+ kind: "renderAndSend",
45
+ entryRejections: options.entryRejections ?? [],
46
+ ...options.manifestRejection ? { manifestRejection: options.manifestRejection } : {},
47
+ skipDisposition: createDisabledSkipDisposition(),
48
+ skipIneligibleEntryIds: options.skipIneligibleEntryIds ?? [],
49
+ skippedEntryIds: []
50
+ };
51
+ }
52
+ function createVerificationBudgetExceededRejection(totalWireEntries, maxWireEntriesToVerify) {
53
+ return {
54
+ code: "SKIP_VERIFICATION_BUDGET_EXCEEDED",
55
+ fields: {
56
+ totalWireEntries,
57
+ maxWireEntriesToVerify
58
+ }
59
+ };
60
+ }
39
61
  function crossCheckInvalidationProof(entry, invalidation) {
40
62
  switch (invalidation.kind) {
41
63
  case "valid": return null;
@@ -83,6 +105,7 @@ function crossCheckClientReuseManifestEntryWithCache(input) {
83
105
  });
84
106
  const invalidationRejection = crossCheckInvalidationProof(entry, input.artifact.invalidation);
85
107
  if (invalidationRejection) return invalidationRejection;
108
+ const skipDisposition = isExactArtifactCompatibility(input.artifact.compatibility, entry.artifactCompatibility) ? createStaticLayoutSkipDisposition([entry.id]) : createDisabledSkipDisposition();
86
109
  return {
87
110
  kind: "verified",
88
111
  code: "SKIP_CACHE_CROSS_CHECK_PASSED",
@@ -92,10 +115,56 @@ function crossCheckClientReuseManifestEntryWithCache(input) {
92
115
  reuseClass: proof.reuseClass,
93
116
  variantCacheKeyHash: createClientReusePayloadHash(proof.variant.cacheKey)
94
117
  },
95
- skipDisposition: createDisabledSkipDisposition()
118
+ skipDisposition
119
+ };
120
+ }
121
+ function createClientReuseSkipTransportPlan(input) {
122
+ const { manifest } = input;
123
+ if (manifest.kind === "absent") return createRenderAndSendPlan({});
124
+ if (manifest.kind === "rejected") return createRenderAndSendPlan({ manifestRejection: manifest.rejection });
125
+ const maxWireEntriesToVerify = input.maxWireEntriesToVerify ?? 8;
126
+ if (!Number.isSafeInteger(maxWireEntriesToVerify) || maxWireEntriesToVerify < 0) throw new RangeError("maxWireEntriesToVerify must be a non-negative safe integer");
127
+ const totalWireEntries = manifest.manifest.entries.length + manifest.entryRejections.length;
128
+ if (totalWireEntries > maxWireEntriesToVerify) return createRenderAndSendPlan({
129
+ entryRejections: manifest.entryRejections,
130
+ manifestRejection: createVerificationBudgetExceededRejection(totalWireEntries, maxWireEntriesToVerify)
131
+ });
132
+ const skippedEntryIds = [];
133
+ const skipIneligibleEntryIds = [];
134
+ const entryRejections = [...manifest.entryRejections];
135
+ for (const entry of manifest.manifest.entries) {
136
+ const verification = input.verifyEntry(entry);
137
+ if (verification.kind === "rejected") {
138
+ entryRejections.push(verification.rejection);
139
+ continue;
140
+ }
141
+ if (verification.entryId !== entry.id) {
142
+ entryRejections.push({
143
+ code: "SKIP_CACHE_ENTRY_ID_MISMATCH",
144
+ entryId: entry.id,
145
+ fields: {
146
+ verifierEntryId: verification.entryId,
147
+ manifestEntryId: entry.id
148
+ }
149
+ });
150
+ continue;
151
+ }
152
+ if (verification.skipDisposition.enabled) skippedEntryIds.push(entry.id);
153
+ else skipIneligibleEntryIds.push(entry.id);
154
+ }
155
+ if (skippedEntryIds.length === 0) return createRenderAndSendPlan({
156
+ entryRejections,
157
+ skipIneligibleEntryIds
158
+ });
159
+ return {
160
+ kind: "skip",
161
+ entryRejections,
162
+ skipDisposition: createStaticLayoutSkipDisposition(skippedEntryIds),
163
+ skipIneligibleEntryIds,
164
+ skippedEntryIds
96
165
  };
97
166
  }
98
167
  //#endregion
99
- export { crossCheckClientReuseManifestEntryWithCache };
168
+ export { createClientReuseSkipTransportPlan, createStaticLayoutClientReuseArtifactCompatibility, createStaticLayoutClientReusePayloadHash, createStaticLayoutClientReuseRouteId, crossCheckClientReuseManifestEntryWithCache };
100
169
 
101
170
  //# sourceMappingURL=skip-cache-proof.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"skip-cache-proof.js","names":[],"sources":["../../src/server/skip-cache-proof.ts"],"sourcesContent":["import {\n evaluateArtifactCompatibility,\n type ArtifactCompatibilityEnvelope,\n type ArtifactCompatibilityEvaluationOptions,\n} from \"./artifact-compatibility.js\";\nimport type { StaticLayoutArtifactReuseDecision } from \"./cache-proof.js\";\nimport {\n createClientReusePayloadHash,\n type ClientReuseManifestEntry,\n type ClientReuseManifestEntryRejection,\n type ClientReuseManifestSkipDisposition,\n type ClientReuseManifestTraceFields,\n} from \"./client-reuse-manifest.js\";\n\nexport type SkipCacheInvalidationProof =\n | Readonly<{ kind: \"invalidated\"; invalidationEpoch: string | null }>\n | Readonly<{ kind: \"unknown\" }>\n | Readonly<{ kind: \"valid\" }>;\n\ntype SkipCacheArtifactProof = Readonly<{\n compatibility: ArtifactCompatibilityEnvelope;\n invalidation: SkipCacheInvalidationProof;\n payloadHash: string | null;\n}>;\n\ntype SkipCacheCrossCheckAcceptanceCode = \"SKIP_CACHE_CROSS_CHECK_PASSED\";\n\ntype SkipCacheCrossCheckVerified = Readonly<{\n code: SkipCacheCrossCheckAcceptanceCode;\n entryId: string;\n fields: ClientReuseManifestTraceFields;\n kind: \"verified\";\n skipDisposition: ClientReuseManifestSkipDisposition;\n}>;\n\ntype SkipCacheCrossCheckRejected = Readonly<{\n kind: \"rejected\";\n rejection: ClientReuseManifestEntryRejection;\n skipDisposition: ClientReuseManifestSkipDisposition;\n}>;\n\ntype SkipCacheCrossCheckResult = SkipCacheCrossCheckRejected | SkipCacheCrossCheckVerified;\n\ntype CrossCheckClientReuseManifestEntryWithCacheInput = Readonly<{\n artifact: SkipCacheArtifactProof;\n cacheDecision: StaticLayoutArtifactReuseDecision | null;\n entry: ClientReuseManifestEntry;\n}> &\n ArtifactCompatibilityEvaluationOptions;\n\nconst ARTIFACT_COMPATIBILITY_PROOF_FIELDS: readonly (keyof ArtifactCompatibilityEnvelope)[] = [\n \"schemaVersion\",\n \"graphVersion\",\n \"deploymentVersion\",\n \"appElementsSchemaVersion\",\n \"rscPayloadSchemaVersion\",\n \"rootBoundaryId\",\n \"renderEpoch\",\n];\n\nfunction createDisabledSkipDisposition(): ClientReuseManifestSkipDisposition {\n return {\n code: \"SKIP_MODEL_DISABLED\",\n enabled: false,\n mode: \"renderAndSend\",\n };\n}\n\nfunction rejectSkipCacheCrossCheck(\n entry: ClientReuseManifestEntry,\n code: ClientReuseManifestEntryRejection[\"code\"],\n fields: ClientReuseManifestTraceFields = {},\n): SkipCacheCrossCheckRejected {\n return {\n kind: \"rejected\",\n rejection: {\n code,\n entryId: entry.id,\n fields,\n },\n skipDisposition: createDisabledSkipDisposition(),\n };\n}\n\nfunction collectArtifactCompatibilityProofMismatches(\n artifactCompatibility: ArtifactCompatibilityEnvelope,\n proofCompatibility: ArtifactCompatibilityEnvelope,\n): readonly string[] {\n const mismatchedFields: string[] = [];\n for (const field of ARTIFACT_COMPATIBILITY_PROOF_FIELDS) {\n if (artifactCompatibility[field] !== proofCompatibility[field]) {\n mismatchedFields.push(field);\n }\n }\n return mismatchedFields;\n}\n\nfunction assertNever(value: never): never {\n throw new Error(`Unhandled skip/cache proof state: ${String(value)}`);\n}\n\nfunction crossCheckInvalidationProof(\n entry: ClientReuseManifestEntry,\n invalidation: SkipCacheInvalidationProof,\n): SkipCacheCrossCheckRejected | null {\n switch (invalidation.kind) {\n case \"valid\":\n return null;\n case \"unknown\":\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_INVALIDATION_UNKNOWN\");\n case \"invalidated\":\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_INVALIDATED\", {\n invalidationEpoch: invalidation.invalidationEpoch,\n });\n default:\n return assertNever(invalidation);\n }\n}\n\nexport function crossCheckClientReuseManifestEntryWithCache(\n input: CrossCheckClientReuseManifestEntryWithCacheInput,\n): SkipCacheCrossCheckResult {\n const { cacheDecision, entry } = input;\n if (cacheDecision === null) {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_PROOF_MISSING\");\n }\n if (cacheDecision.kind === \"fallback\") {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_PROOF_REJECTED\", {\n cacheProofCode: cacheDecision.fallback.code,\n cacheProofMode: cacheDecision.fallback.mode,\n cacheProofScope: cacheDecision.fallback.scope,\n });\n }\n\n const { proof } = cacheDecision;\n if (entry.kind !== \"layout\" || proof.reuseClass !== \"static-layout\") {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_REUSE_CLASS_UNSUPPORTED\", {\n entryKind: entry.kind,\n reuseClass: proof.reuseClass,\n });\n }\n\n if (entry.id !== proof.candidateOutput.layoutId) {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_ENTRY_ID_MISMATCH\", {\n cacheEntryId: proof.candidateOutput.layoutId,\n manifestEntryId: entry.id,\n });\n }\n\n const artifactProofMismatches = collectArtifactCompatibilityProofMismatches(\n input.artifact.compatibility,\n proof.candidateArtifactCompatibility,\n );\n if (artifactProofMismatches.length > 0) {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_ARTIFACT_PROOF_MISMATCH\", {\n mismatchedFields: artifactProofMismatches,\n });\n }\n\n const artifactCompatibility = evaluateArtifactCompatibility(\n input.artifact.compatibility,\n entry.artifactCompatibility,\n { compatibilityMap: input.compatibilityMap },\n );\n if (artifactCompatibility.kind === \"unknown\") {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_ARTIFACT_COMPATIBILITY_UNKNOWN\", {\n compatibilityFallback: artifactCompatibility.fallback,\n reason: artifactCompatibility.reason,\n });\n }\n if (artifactCompatibility.kind === \"incompatible\") {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_ARTIFACT_COMPATIBILITY_INCOMPATIBLE\", {\n compatibilityFallback: artifactCompatibility.fallback,\n reason: artifactCompatibility.reason,\n });\n }\n\n if (entry.variantCacheKey !== proof.variant.cacheKey) {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_VARIANT_MISMATCH\", {\n cacheVariantCacheKeyHash: createClientReusePayloadHash(proof.variant.cacheKey),\n entryVariantCacheKeyHash: createClientReusePayloadHash(entry.variantCacheKey),\n });\n }\n\n if (input.artifact.payloadHash === null) {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_PAYLOAD_HASH_MISSING\");\n }\n\n if (entry.payloadHash !== input.artifact.payloadHash) {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_PAYLOAD_HASH_MISMATCH\", {\n cachePayloadHash: input.artifact.payloadHash,\n entryPayloadHash: entry.payloadHash,\n });\n }\n\n const invalidationRejection = crossCheckInvalidationProof(entry, input.artifact.invalidation);\n if (invalidationRejection) return invalidationRejection;\n\n return {\n kind: \"verified\",\n code: \"SKIP_CACHE_CROSS_CHECK_PASSED\",\n entryId: entry.id,\n fields: {\n entryKind: entry.kind,\n reuseClass: proof.reuseClass,\n variantCacheKeyHash: createClientReusePayloadHash(proof.variant.cacheKey),\n },\n skipDisposition: createDisabledSkipDisposition(),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"skip-cache-proof.js","names":[],"sources":["../../src/server/skip-cache-proof.ts"],"sourcesContent":["import {\n evaluateArtifactCompatibility,\n ARTIFACT_COMPATIBILITY_PROOF_FIELDS,\n type ArtifactCompatibilityEnvelope,\n type ArtifactCompatibilityEvaluationOptions,\n} from \"./artifact-compatibility.js\";\nimport type { StaticLayoutArtifactReuseDecision } from \"./cache-proof.js\";\nimport {\n CLIENT_REUSE_MANIFEST_SKIP_VERIFICATION_ENTRY_BUDGET,\n createClientReusePayloadHash,\n type ClientReuseManifestEntry,\n type ClientReuseManifestEntryRejection,\n type ClientReuseManifestParseResult,\n type ClientReuseManifestRejection,\n type ClientReuseManifestSkipDisposition,\n type ClientReuseManifestTraceFields,\n} from \"./client-reuse-manifest.js\";\nexport {\n createStaticLayoutClientReuseArtifactCompatibility,\n createStaticLayoutClientReusePayloadHash,\n createStaticLayoutClientReuseRouteId,\n} from \"./static-layout-client-reuse-proof.js\";\n\nexport type SkipCacheInvalidationProof =\n | Readonly<{ kind: \"invalidated\"; invalidationEpoch: string | null }>\n | Readonly<{ kind: \"unknown\" }>\n | Readonly<{ kind: \"valid\" }>;\n\ntype SkipCacheArtifactProof = Readonly<{\n compatibility: ArtifactCompatibilityEnvelope;\n invalidation: SkipCacheInvalidationProof;\n payloadHash: string | null;\n}>;\n\ntype SkipCacheCrossCheckAcceptanceCode = \"SKIP_CACHE_CROSS_CHECK_PASSED\";\n\ntype SkipCacheCrossCheckVerified = Readonly<{\n code: SkipCacheCrossCheckAcceptanceCode;\n entryId: string;\n fields: ClientReuseManifestTraceFields;\n kind: \"verified\";\n skipDisposition: ClientReuseManifestSkipDisposition;\n}>;\n\ntype SkipCacheCrossCheckRejected = Readonly<{\n kind: \"rejected\";\n rejection: ClientReuseManifestEntryRejection;\n skipDisposition: ClientReuseManifestSkipDisposition;\n}>;\n\ntype SkipCacheCrossCheckResult = SkipCacheCrossCheckRejected | SkipCacheCrossCheckVerified;\n\ntype CrossCheckClientReuseManifestEntryWithCacheInput = Readonly<{\n artifact: SkipCacheArtifactProof;\n cacheDecision: StaticLayoutArtifactReuseDecision | null;\n entry: ClientReuseManifestEntry;\n}> &\n ArtifactCompatibilityEvaluationOptions;\n\ntype ClientReuseSkipTransportPlan =\n | Readonly<{\n entryRejections: readonly ClientReuseManifestEntryRejection[];\n kind: \"renderAndSend\";\n manifestRejection?: ClientReuseManifestRejection;\n skipDisposition: ClientReuseManifestSkipDisposition;\n skipIneligibleEntryIds: readonly string[];\n skippedEntryIds: readonly string[];\n }>\n | Readonly<{\n entryRejections: readonly ClientReuseManifestEntryRejection[];\n kind: \"skip\";\n skipDisposition: ClientReuseManifestSkipDisposition;\n skipIneligibleEntryIds: readonly string[];\n skippedEntryIds: readonly string[];\n }>;\n\ntype CreateClientReuseSkipTransportPlanInput = Readonly<{\n manifest: ClientReuseManifestParseResult;\n maxWireEntriesToVerify?: number;\n verifyEntry: (entry: ClientReuseManifestEntry) => SkipCacheCrossCheckResult;\n}>;\n\nfunction createDisabledSkipDisposition(): ClientReuseManifestSkipDisposition {\n return {\n code: \"SKIP_MODEL_DISABLED\",\n enabled: false,\n mode: \"renderAndSend\",\n };\n}\n\nfunction createStaticLayoutSkipDisposition(\n skippedEntryIds: readonly string[],\n): ClientReuseManifestSkipDisposition {\n return {\n code: \"SKIP_STATIC_LAYOUT_VERIFIED\",\n enabled: true,\n mode: \"skipStaticLayout\",\n skippedEntryIds: [...skippedEntryIds],\n };\n}\n\nfunction rejectSkipCacheCrossCheck(\n entry: ClientReuseManifestEntry,\n code: ClientReuseManifestEntryRejection[\"code\"],\n fields: ClientReuseManifestTraceFields = {},\n): SkipCacheCrossCheckRejected {\n return {\n kind: \"rejected\",\n rejection: {\n code,\n entryId: entry.id,\n fields,\n },\n skipDisposition: createDisabledSkipDisposition(),\n };\n}\n\nfunction collectArtifactCompatibilityProofMismatches(\n artifactCompatibility: ArtifactCompatibilityEnvelope,\n proofCompatibility: ArtifactCompatibilityEnvelope,\n): readonly string[] {\n const mismatchedFields: string[] = [];\n for (const field of ARTIFACT_COMPATIBILITY_PROOF_FIELDS) {\n if (artifactCompatibility[field] !== proofCompatibility[field]) {\n mismatchedFields.push(field);\n }\n }\n return mismatchedFields;\n}\n\nfunction isExactArtifactCompatibility(\n artifactCompatibility: ArtifactCompatibilityEnvelope,\n entryCompatibility: ArtifactCompatibilityEnvelope,\n): boolean {\n return (\n collectArtifactCompatibilityProofMismatches(artifactCompatibility, entryCompatibility)\n .length === 0\n );\n}\n\nfunction assertNever(value: never): never {\n throw new Error(`Unhandled skip/cache proof state: ${String(value)}`);\n}\n\nfunction createRenderAndSendPlan(options: {\n entryRejections?: readonly ClientReuseManifestEntryRejection[];\n manifestRejection?: ClientReuseManifestRejection;\n skipIneligibleEntryIds?: readonly string[];\n}): ClientReuseSkipTransportPlan {\n return {\n kind: \"renderAndSend\",\n entryRejections: options.entryRejections ?? [],\n ...(options.manifestRejection ? { manifestRejection: options.manifestRejection } : {}),\n skipDisposition: createDisabledSkipDisposition(),\n skipIneligibleEntryIds: options.skipIneligibleEntryIds ?? [],\n skippedEntryIds: [],\n };\n}\n\nfunction createVerificationBudgetExceededRejection(\n totalWireEntries: number,\n maxWireEntriesToVerify: number,\n): ClientReuseManifestRejection {\n return {\n code: \"SKIP_VERIFICATION_BUDGET_EXCEEDED\",\n fields: { totalWireEntries, maxWireEntriesToVerify },\n };\n}\n\nfunction crossCheckInvalidationProof(\n entry: ClientReuseManifestEntry,\n invalidation: SkipCacheInvalidationProof,\n): SkipCacheCrossCheckRejected | null {\n switch (invalidation.kind) {\n case \"valid\":\n return null;\n case \"unknown\":\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_INVALIDATION_UNKNOWN\");\n case \"invalidated\":\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_INVALIDATED\", {\n invalidationEpoch: invalidation.invalidationEpoch,\n });\n default:\n return assertNever(invalidation);\n }\n}\n\nexport function crossCheckClientReuseManifestEntryWithCache(\n input: CrossCheckClientReuseManifestEntryWithCacheInput,\n): SkipCacheCrossCheckResult {\n const { cacheDecision, entry } = input;\n if (cacheDecision === null) {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_PROOF_MISSING\");\n }\n if (cacheDecision.kind === \"fallback\") {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_PROOF_REJECTED\", {\n cacheProofCode: cacheDecision.fallback.code,\n cacheProofMode: cacheDecision.fallback.mode,\n cacheProofScope: cacheDecision.fallback.scope,\n });\n }\n\n const { proof } = cacheDecision;\n if (entry.kind !== \"layout\" || proof.reuseClass !== \"static-layout\") {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_REUSE_CLASS_UNSUPPORTED\", {\n entryKind: entry.kind,\n reuseClass: proof.reuseClass,\n });\n }\n\n if (entry.id !== proof.candidateOutput.layoutId) {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_ENTRY_ID_MISMATCH\", {\n cacheEntryId: proof.candidateOutput.layoutId,\n manifestEntryId: entry.id,\n });\n }\n\n const artifactProofMismatches = collectArtifactCompatibilityProofMismatches(\n input.artifact.compatibility,\n proof.candidateArtifactCompatibility,\n );\n if (artifactProofMismatches.length > 0) {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_ARTIFACT_PROOF_MISMATCH\", {\n mismatchedFields: artifactProofMismatches,\n });\n }\n\n const artifactCompatibility = evaluateArtifactCompatibility(\n input.artifact.compatibility,\n entry.artifactCompatibility,\n { compatibilityMap: input.compatibilityMap },\n );\n if (artifactCompatibility.kind === \"unknown\") {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_ARTIFACT_COMPATIBILITY_UNKNOWN\", {\n compatibilityFallback: artifactCompatibility.fallback,\n reason: artifactCompatibility.reason,\n });\n }\n if (artifactCompatibility.kind === \"incompatible\") {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_ARTIFACT_COMPATIBILITY_INCOMPATIBLE\", {\n compatibilityFallback: artifactCompatibility.fallback,\n reason: artifactCompatibility.reason,\n });\n }\n\n if (entry.variantCacheKey !== proof.variant.cacheKey) {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_VARIANT_MISMATCH\", {\n cacheVariantCacheKeyHash: createClientReusePayloadHash(proof.variant.cacheKey),\n entryVariantCacheKeyHash: createClientReusePayloadHash(entry.variantCacheKey),\n });\n }\n\n if (input.artifact.payloadHash === null) {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_PAYLOAD_HASH_MISSING\");\n }\n\n if (entry.payloadHash !== input.artifact.payloadHash) {\n return rejectSkipCacheCrossCheck(entry, \"SKIP_CACHE_PAYLOAD_HASH_MISMATCH\", {\n cachePayloadHash: input.artifact.payloadHash,\n entryPayloadHash: entry.payloadHash,\n });\n }\n\n const invalidationRejection = crossCheckInvalidationProof(entry, input.artifact.invalidation);\n if (invalidationRejection) return invalidationRejection;\n\n // Skip transport requires exact artifact field equality — not just\n // compatibility-map-based equivalence. evaluateArtifactCompatibility (above)\n // returns \"compatible\" when a declared set bridges differing fields (canary →\n // rollback), but the skip planner must not elide a fresh render when the two\n // payloads were built from observably different artifact environments.\n // isExactArtifactCompatibility performs a distinct field-level walk that\n // ignores the compatibility map, guaranteeing only bit-identical artifact\n // envelopes are eligible for transport skip.\n const skipDisposition = isExactArtifactCompatibility(\n input.artifact.compatibility,\n entry.artifactCompatibility,\n )\n ? createStaticLayoutSkipDisposition([entry.id])\n : createDisabledSkipDisposition();\n\n return {\n kind: \"verified\",\n code: \"SKIP_CACHE_CROSS_CHECK_PASSED\",\n entryId: entry.id,\n fields: {\n entryKind: entry.kind,\n reuseClass: proof.reuseClass,\n variantCacheKeyHash: createClientReusePayloadHash(proof.variant.cacheKey),\n },\n skipDisposition,\n };\n}\n\nexport function createClientReuseSkipTransportPlan(\n input: CreateClientReuseSkipTransportPlanInput,\n): ClientReuseSkipTransportPlan {\n const { manifest } = input;\n if (manifest.kind === \"absent\") {\n return createRenderAndSendPlan({});\n }\n if (manifest.kind === \"rejected\") {\n return createRenderAndSendPlan({ manifestRejection: manifest.rejection });\n }\n\n const maxWireEntriesToVerify =\n input.maxWireEntriesToVerify ?? CLIENT_REUSE_MANIFEST_SKIP_VERIFICATION_ENTRY_BUDGET;\n if (!Number.isSafeInteger(maxWireEntriesToVerify) || maxWireEntriesToVerify < 0) {\n throw new RangeError(\"maxWireEntriesToVerify must be a non-negative safe integer\");\n }\n\n const totalWireEntries = manifest.manifest.entries.length + manifest.entryRejections.length;\n if (totalWireEntries > maxWireEntriesToVerify) {\n return createRenderAndSendPlan({\n entryRejections: manifest.entryRejections,\n manifestRejection: createVerificationBudgetExceededRejection(\n totalWireEntries,\n maxWireEntriesToVerify,\n ),\n });\n }\n\n const skippedEntryIds: string[] = [];\n const skipIneligibleEntryIds: string[] = [];\n const entryRejections: ClientReuseManifestEntryRejection[] = [...manifest.entryRejections];\n for (const entry of manifest.manifest.entries) {\n const verification = input.verifyEntry(entry);\n if (verification.kind === \"rejected\") {\n entryRejections.push(verification.rejection);\n continue;\n }\n\n // The planner must not trust a verifier that returns a verified result\n // for a different entry than the one it was given. This is an internal\n // invariant, not a hostile-client defence.\n if (verification.entryId !== entry.id) {\n entryRejections.push({\n code: \"SKIP_CACHE_ENTRY_ID_MISMATCH\",\n entryId: entry.id,\n fields: {\n verifierEntryId: verification.entryId,\n manifestEntryId: entry.id,\n },\n });\n continue;\n }\n\n if (verification.skipDisposition.enabled) {\n skippedEntryIds.push(entry.id);\n } else {\n skipIneligibleEntryIds.push(entry.id);\n }\n }\n\n if (skippedEntryIds.length === 0) {\n return createRenderAndSendPlan({ entryRejections, skipIneligibleEntryIds });\n }\n\n return {\n kind: \"skip\",\n entryRejections,\n skipDisposition: createStaticLayoutSkipDisposition(skippedEntryIds),\n skipIneligibleEntryIds,\n skippedEntryIds,\n };\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { normalizePathSeparators } from "../utils/path.js";
1
2
  import "../utils/asset-prefix.js";
2
3
  import path from "node:path";
3
4
  import fs from "node:fs/promises";
@@ -205,7 +206,7 @@ async function* walkFilesWithStats(dir, base = dir) {
205
206
  const batch = files.slice(i, i + STAT_BATCH_SIZE);
206
207
  const stats = await Promise.all(batch.map((f) => fs.stat(f)));
207
208
  for (let j = 0; j < batch.length; j++) yield {
208
- relativePath: path.relative(base, batch[j]).replaceAll(path.sep, "/"),
209
+ relativePath: normalizePathSeparators(path.relative(base, batch[j])),
209
210
  fullPath: batch[j],
210
211
  stat: {
211
212
  size: stats[j].size,
@@ -1 +1 @@
1
- {"version":3,"file":"static-file-cache.js","names":["fsp"],"sources":["../../src/server/static-file-cache.ts"],"sourcesContent":["/**\n * Startup metadata cache for static file serving.\n *\n * Walks dist/client/ once at server boot, pre-computes response headers for\n * every file variant (original, brotli, gzip, zstd), and caches everything\n * in memory. The per-request hot path is just: Map.get() → string compare\n * (ETag) → writeHead(precomputed) → pipe.\n *\n * Modeled after sirv's production mode. Key insight from sirv: pre-compute\n * ALL response headers at startup — Content-Type, Content-Length, ETag,\n * Cache-Control, Content-Encoding, Vary — as reusable objects. The common\n * per-request path (no extraHeaders) does zero object allocation for headers.\n */\nimport fsp from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { ASSET_PREFIX_URL_DIR } from \"../utils/asset-prefix.js\";\n\n/** Content-type lookup for static assets. Shared with prod-server.ts. */\nexport const CONTENT_TYPES: Record<string, string> = {\n \".js\": \"application/javascript\",\n \".mjs\": \"application/javascript\",\n \".css\": \"text/css\",\n \".html\": \"text/html\",\n \".json\": \"application/json\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".svg\": \"image/svg+xml\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".eot\": \"application/vnd.ms-fontobject\",\n \".webp\": \"image/webp\",\n \".avif\": \"image/avif\",\n \".map\": \"application/json\",\n \".rsc\": \"text/x-component\",\n};\n\n/**\n * Files below this size are buffered in memory at startup for zero-syscall\n * serving via res.end(buffer). Above this, files stream via createReadStream.\n * 64KB covers virtually all precompressed assets (a 200KB JS bundle compresses\n * to ~50KB with brotli q5).\n */\nconst BUFFER_THRESHOLD = 64 * 1024;\n\n/** A servable file variant with pre-computed response headers. */\ntype FileVariant = {\n /** Absolute file path (used for streaming large files). */\n path: string;\n /** Uncompressed or encoded byte size for buffer-threshold decisions. */\n size: number;\n /** Pre-computed response headers. */\n headers: Record<string, string>;\n /** In-memory buffer for small files (below BUFFER_THRESHOLD). */\n buffer?: Buffer;\n};\n\ntype StaticFileEntry = {\n /** Weak ETag for conditional request matching. */\n etag: string;\n /** Pre-computed headers for 304 Not Modified response. */\n notModifiedHeaders: Record<string, string>;\n /** Original file variant (uncompressed). */\n original: FileVariant;\n /** Brotli precompressed variant, if .br file exists. */\n br?: FileVariant;\n /** Gzip precompressed variant, if .gz file exists. */\n gz?: FileVariant;\n /** Zstandard precompressed variant, if .zst file exists. */\n zst?: FileVariant;\n};\n\n/**\n * In-memory cache of static file metadata, populated once at server startup.\n *\n * Usage:\n * const cache = await StaticFileCache.create(clientDir);\n * const entry = cache.lookup(\"/_next/static/app-abc123.js\");\n * // entry.br?.headers, entry.original.headers, etc.\n */\nexport class StaticFileCache {\n private readonly entries: Map<string, StaticFileEntry>;\n\n private constructor(entries: Map<string, StaticFileEntry>) {\n this.entries = entries;\n }\n\n /**\n * Scan the client directory and build the cache.\n *\n * Gracefully handles non-existent directories (returns an empty cache).\n */\n static async create(clientDir: string): Promise<StaticFileCache> {\n const entries = new Map<string, StaticFileEntry>();\n\n // First pass: collect all regular files with their metadata\n const allFiles = new Map<string, { fullPath: string; size: number; mtimeMs: number }>();\n\n for await (const { relativePath, fullPath, stat } of walkFilesWithStats(clientDir)) {\n allFiles.set(relativePath, { fullPath, size: stat.size, mtimeMs: stat.mtimeMs });\n }\n\n // Second pass: build cache entries with pre-computed headers per variant\n for (const [relativePath, fileInfo] of allFiles) {\n // Skip precompressed variants — they're linked to their originals\n if (\n relativePath.endsWith(\".br\") ||\n relativePath.endsWith(\".gz\") ||\n relativePath.endsWith(\".zst\")\n )\n continue;\n\n // Skip .vite/ internal directory\n if (relativePath.startsWith(\".vite/\") || relativePath === \".vite\") continue;\n\n const ext = path.extname(relativePath);\n const contentType = CONTENT_TYPES[ext] ?? \"application/octet-stream\";\n // Files under Vite's `assetsDir` are content-hashed. The default\n // layout writes to `<ASSET_PREFIX_URL_DIR>/` (Next.js's canonical\n // convention); when `assetPrefix` is a path prefix the layout\n // becomes `<prefix>/<ASSET_PREFIX_URL_DIR>/...`. Both forms get\n // long-lived `immutable` cache headers — the hash in the filename\n // invalidates safely.\n //\n // `relativePath` is the path relative to `clientDir`, with no\n // leading slash. Because of that, `startsWith(\"<dir>/\")` and\n // `includes(\"/<dir>/\")` are NOT equivalent — the former covers the\n // default and absolute-URL prefix layouts (no parent directory),\n // the latter covers the path-prefix layout (under an arbitrary\n // parent like `cdn/`).\n const isHashed =\n relativePath.startsWith(`${ASSET_PREFIX_URL_DIR}/`) ||\n relativePath.includes(`/${ASSET_PREFIX_URL_DIR}/`);\n const cacheControl = isHashed\n ? \"public, max-age=31536000, immutable\"\n : \"public, max-age=3600\";\n const etag =\n (isHashed && etagFromFilenameHash(relativePath, ext)) ||\n `W/\"${fileInfo.size}-${Math.floor(fileInfo.mtimeMs / 1000)}\"`;\n\n // Base headers shared by all variants (Content-Type, Cache-Control, ETag)\n const baseHeaders = {\n \"Content-Type\": contentType,\n \"Cache-Control\": cacheControl,\n ETag: etag,\n };\n\n // Pre-compute original variant headers\n const original: FileVariant = {\n path: fileInfo.fullPath,\n size: fileInfo.size,\n headers: { ...baseHeaders, \"Content-Length\": String(fileInfo.size) },\n };\n\n const entry: StaticFileEntry = {\n etag,\n notModifiedHeaders: { ETag: etag, \"Cache-Control\": cacheControl },\n original,\n };\n\n // Pre-compute compressed variant headers (with Content-Encoding, Vary, correct Content-Length)\n const brInfo = allFiles.get(relativePath + \".br\");\n if (brInfo) {\n entry.br = buildVariant(brInfo, baseHeaders, \"br\");\n }\n\n const gzInfo = allFiles.get(relativePath + \".gz\");\n if (gzInfo) {\n entry.gz = buildVariant(gzInfo, baseHeaders, \"gzip\");\n }\n\n const zstInfo = allFiles.get(relativePath + \".zst\");\n if (zstInfo) {\n entry.zst = buildVariant(zstInfo, baseHeaders, \"zstd\");\n }\n\n // When compressed variants exist, the original needs Vary too so\n // shared caches don't serve uncompressed to compression-capable clients.\n if (entry.br || entry.gz || entry.zst) {\n original.headers[\"Vary\"] = \"Accept-Encoding\";\n entry.notModifiedHeaders[\"Vary\"] = \"Accept-Encoding\";\n }\n\n // Register under the URL pathname (leading /)\n // NOTE: aliases below share the same entry by reference, so all header\n // mutations (e.g. Vary above) must happen before registration.\n const pathname = \"/\" + relativePath;\n entries.set(pathname, entry);\n\n // Register HTML fallback aliases (same entry object — no duplication)\n if (ext === \".html\") {\n if (relativePath.endsWith(\"/index.html\")) {\n const dirPath = \"/\" + relativePath.slice(0, -\"/index.html\".length);\n if (dirPath !== \"/\") {\n entries.set(dirPath, entry);\n }\n } else {\n const withoutExt = \"/\" + relativePath.slice(0, -ext.length);\n entries.set(withoutExt, entry);\n }\n }\n }\n\n // Third pass: buffer small files in memory for zero-syscall serving.\n // For small compressed variants (e.g. a 50KB JS bundle → ~15KB brotli),\n // res.end(buffer) is ~2x faster than createReadStream().pipe() because\n // it skips fd open/close and stream plumbing overhead.\n // Reads are chunked at 64 concurrent to avoid fd exhaustion on large projects.\n // Deduplicate at the entry level first: HTML aliases share the same\n // StaticFileEntry by reference, so entries.values() yields duplicates for\n // paths like /about and /about.html. Deduping entries avoids iterating\n // their variants multiple times on sites with many HTML pages.\n const toBuffer: FileVariant[] = [];\n const seenEntries = new Set<StaticFileEntry>();\n for (const entry of entries.values()) {\n if (seenEntries.has(entry)) continue;\n seenEntries.add(entry);\n for (const variant of [entry.original, entry.br, entry.gz, entry.zst]) {\n if (!variant || variant.size > BUFFER_THRESHOLD) continue;\n toBuffer.push(variant);\n }\n }\n for (let i = 0; i < toBuffer.length; i += 64) {\n await Promise.all(\n toBuffer.slice(i, i + 64).map(async (v) => {\n v.buffer = await fsp.readFile(v.path);\n }),\n );\n }\n\n return new StaticFileCache(entries);\n }\n\n /**\n * Look up cached metadata for a URL pathname.\n *\n * Returns undefined if the file is not in the cache. The root path \"/\"\n * always returns undefined — index.html is served by SSR/RSC.\n */\n lookup(pathname: string): StaticFileEntry | undefined {\n if (pathname === \"/\") return undefined;\n\n // Block .vite/ access (including encoded variants that were decoded before lookup)\n if (pathname.startsWith(\"/.vite/\") || pathname === \"/.vite\") return undefined;\n\n return this.entries.get(pathname);\n }\n}\n\n/**\n * Extract a stable weak ETag from a Vite hashed filename (e.g. `app-DqZc3R4n.js`).\n * The hash is a content hash computed by the bundler — deterministic across\n * identical builds regardless of filesystem timestamps.\n *\n * Must be a weak validator (W/) because the same tag is shared across\n * content-encoded variants (original, .br, .gz, .zst) which are byte-different.\n * Returns null if the filename doesn't contain a recognizable hash suffix,\n * so the caller can fall back to mtime-based ETags.\n */\nexport function etagFromFilenameHash(relativePath: string, ext: string): string | null {\n const basename = path.basename(relativePath, ext);\n const lastDash = basename.lastIndexOf(\"-\");\n if (lastDash === -1 || lastDash === basename.length - 1) return null;\n const suffix = basename.slice(lastDash + 1);\n // Vite emits 8-char base64url hashes; allow 6-12 for other bundlers.\n // If Rolldown changes its hash length, update this range.\n return suffix.length >= 6 && suffix.length <= 12 && /^[A-Za-z0-9_-]+$/.test(suffix)\n ? `W/\"${suffix}\"`\n : null;\n}\n\nfunction buildVariant(\n info: { fullPath: string; size: number },\n baseHeaders: Record<string, string>,\n encoding: string,\n): FileVariant {\n return {\n path: info.fullPath,\n size: info.size,\n headers: {\n ...baseHeaders,\n \"Content-Encoding\": encoding,\n \"Content-Length\": String(info.size),\n Vary: \"Accept-Encoding\",\n },\n };\n}\n\n/** Batch size for concurrent stat() calls during directory walk. */\nconst STAT_BATCH_SIZE = 64;\n\n/**\n * Walk a directory recursively, yielding file paths and stats.\n *\n * Batches stat() calls per directory to avoid sequential syscall overhead\n * for large dist/client/ directories.\n */\nasync function* walkFilesWithStats(\n dir: string,\n base: string = dir,\n): AsyncGenerator<{\n relativePath: string;\n fullPath: string;\n stat: { size: number; mtimeMs: number };\n}> {\n let entries;\n try {\n entries = await fsp.readdir(dir, { withFileTypes: true });\n } catch {\n return; // directory doesn't exist or unreadable\n }\n\n // Recurse into subdirectories first (they yield their own batched stats)\n const files: string[] = [];\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n yield* walkFilesWithStats(fullPath, base);\n } else if (entry.isFile()) {\n files.push(fullPath);\n }\n }\n\n // Batch stat() calls for files in this directory\n for (let i = 0; i < files.length; i += STAT_BATCH_SIZE) {\n const batch = files.slice(i, i + STAT_BATCH_SIZE);\n const stats = await Promise.all(batch.map((f) => fsp.stat(f)));\n for (let j = 0; j < batch.length; j++) {\n yield {\n relativePath: path.relative(base, batch[j]).replaceAll(path.sep, \"/\"),\n fullPath: batch[j],\n stat: { size: stats[j].size, mtimeMs: stats[j].mtimeMs },\n };\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"static-file-cache.js","names":["fsp"],"sources":["../../src/server/static-file-cache.ts"],"sourcesContent":["/**\n * Startup metadata cache for static file serving.\n *\n * Walks dist/client/ once at server boot, pre-computes response headers for\n * every file variant (original, brotli, gzip, zstd), and caches everything\n * in memory. The per-request hot path is just: Map.get() → string compare\n * (ETag) → writeHead(precomputed) → pipe.\n *\n * Modeled after sirv's production mode. Key insight from sirv: pre-compute\n * ALL response headers at startup — Content-Type, Content-Length, ETag,\n * Cache-Control, Content-Encoding, Vary — as reusable objects. The common\n * per-request path (no extraHeaders) does zero object allocation for headers.\n */\nimport fsp from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { ASSET_PREFIX_URL_DIR } from \"../utils/asset-prefix.js\";\nimport { normalizePathSeparators } from \"../utils/path.js\";\n\n/** Content-type lookup for static assets. Shared with prod-server.ts. */\nexport const CONTENT_TYPES: Record<string, string> = {\n \".js\": \"application/javascript\",\n \".mjs\": \"application/javascript\",\n \".css\": \"text/css\",\n \".html\": \"text/html\",\n \".json\": \"application/json\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".svg\": \"image/svg+xml\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".eot\": \"application/vnd.ms-fontobject\",\n \".webp\": \"image/webp\",\n \".avif\": \"image/avif\",\n \".map\": \"application/json\",\n \".rsc\": \"text/x-component\",\n};\n\n/**\n * Files below this size are buffered in memory at startup for zero-syscall\n * serving via res.end(buffer). Above this, files stream via createReadStream.\n * 64KB covers virtually all precompressed assets (a 200KB JS bundle compresses\n * to ~50KB with brotli q5).\n */\nconst BUFFER_THRESHOLD = 64 * 1024;\n\n/** A servable file variant with pre-computed response headers. */\ntype FileVariant = {\n /** Absolute file path (used for streaming large files). */\n path: string;\n /** Uncompressed or encoded byte size for buffer-threshold decisions. */\n size: number;\n /** Pre-computed response headers. */\n headers: Record<string, string>;\n /** In-memory buffer for small files (below BUFFER_THRESHOLD). */\n buffer?: Buffer;\n};\n\ntype StaticFileEntry = {\n /** Weak ETag for conditional request matching. */\n etag: string;\n /** Pre-computed headers for 304 Not Modified response. */\n notModifiedHeaders: Record<string, string>;\n /** Original file variant (uncompressed). */\n original: FileVariant;\n /** Brotli precompressed variant, if .br file exists. */\n br?: FileVariant;\n /** Gzip precompressed variant, if .gz file exists. */\n gz?: FileVariant;\n /** Zstandard precompressed variant, if .zst file exists. */\n zst?: FileVariant;\n};\n\n/**\n * In-memory cache of static file metadata, populated once at server startup.\n *\n * Usage:\n * const cache = await StaticFileCache.create(clientDir);\n * const entry = cache.lookup(\"/_next/static/app-abc123.js\");\n * // entry.br?.headers, entry.original.headers, etc.\n */\nexport class StaticFileCache {\n private readonly entries: Map<string, StaticFileEntry>;\n\n private constructor(entries: Map<string, StaticFileEntry>) {\n this.entries = entries;\n }\n\n /**\n * Scan the client directory and build the cache.\n *\n * Gracefully handles non-existent directories (returns an empty cache).\n */\n static async create(clientDir: string): Promise<StaticFileCache> {\n const entries = new Map<string, StaticFileEntry>();\n\n // First pass: collect all regular files with their metadata\n const allFiles = new Map<string, { fullPath: string; size: number; mtimeMs: number }>();\n\n for await (const { relativePath, fullPath, stat } of walkFilesWithStats(clientDir)) {\n allFiles.set(relativePath, { fullPath, size: stat.size, mtimeMs: stat.mtimeMs });\n }\n\n // Second pass: build cache entries with pre-computed headers per variant\n for (const [relativePath, fileInfo] of allFiles) {\n // Skip precompressed variants — they're linked to their originals\n if (\n relativePath.endsWith(\".br\") ||\n relativePath.endsWith(\".gz\") ||\n relativePath.endsWith(\".zst\")\n )\n continue;\n\n // Skip .vite/ internal directory\n if (relativePath.startsWith(\".vite/\") || relativePath === \".vite\") continue;\n\n const ext = path.extname(relativePath);\n const contentType = CONTENT_TYPES[ext] ?? \"application/octet-stream\";\n // Files under Vite's `assetsDir` are content-hashed. The default\n // layout writes to `<ASSET_PREFIX_URL_DIR>/` (Next.js's canonical\n // convention); when `assetPrefix` is a path prefix the layout\n // becomes `<prefix>/<ASSET_PREFIX_URL_DIR>/...`. Both forms get\n // long-lived `immutable` cache headers — the hash in the filename\n // invalidates safely.\n //\n // `relativePath` is the path relative to `clientDir`, with no\n // leading slash. Because of that, `startsWith(\"<dir>/\")` and\n // `includes(\"/<dir>/\")` are NOT equivalent — the former covers the\n // default and absolute-URL prefix layouts (no parent directory),\n // the latter covers the path-prefix layout (under an arbitrary\n // parent like `cdn/`).\n const isHashed =\n relativePath.startsWith(`${ASSET_PREFIX_URL_DIR}/`) ||\n relativePath.includes(`/${ASSET_PREFIX_URL_DIR}/`);\n const cacheControl = isHashed\n ? \"public, max-age=31536000, immutable\"\n : \"public, max-age=3600\";\n const etag =\n (isHashed && etagFromFilenameHash(relativePath, ext)) ||\n `W/\"${fileInfo.size}-${Math.floor(fileInfo.mtimeMs / 1000)}\"`;\n\n // Base headers shared by all variants (Content-Type, Cache-Control, ETag)\n const baseHeaders = {\n \"Content-Type\": contentType,\n \"Cache-Control\": cacheControl,\n ETag: etag,\n };\n\n // Pre-compute original variant headers\n const original: FileVariant = {\n path: fileInfo.fullPath,\n size: fileInfo.size,\n headers: { ...baseHeaders, \"Content-Length\": String(fileInfo.size) },\n };\n\n const entry: StaticFileEntry = {\n etag,\n notModifiedHeaders: { ETag: etag, \"Cache-Control\": cacheControl },\n original,\n };\n\n // Pre-compute compressed variant headers (with Content-Encoding, Vary, correct Content-Length)\n const brInfo = allFiles.get(relativePath + \".br\");\n if (brInfo) {\n entry.br = buildVariant(brInfo, baseHeaders, \"br\");\n }\n\n const gzInfo = allFiles.get(relativePath + \".gz\");\n if (gzInfo) {\n entry.gz = buildVariant(gzInfo, baseHeaders, \"gzip\");\n }\n\n const zstInfo = allFiles.get(relativePath + \".zst\");\n if (zstInfo) {\n entry.zst = buildVariant(zstInfo, baseHeaders, \"zstd\");\n }\n\n // When compressed variants exist, the original needs Vary too so\n // shared caches don't serve uncompressed to compression-capable clients.\n if (entry.br || entry.gz || entry.zst) {\n original.headers[\"Vary\"] = \"Accept-Encoding\";\n entry.notModifiedHeaders[\"Vary\"] = \"Accept-Encoding\";\n }\n\n // Register under the URL pathname (leading /)\n // NOTE: aliases below share the same entry by reference, so all header\n // mutations (e.g. Vary above) must happen before registration.\n const pathname = \"/\" + relativePath;\n entries.set(pathname, entry);\n\n // Register HTML fallback aliases (same entry object — no duplication)\n if (ext === \".html\") {\n if (relativePath.endsWith(\"/index.html\")) {\n const dirPath = \"/\" + relativePath.slice(0, -\"/index.html\".length);\n if (dirPath !== \"/\") {\n entries.set(dirPath, entry);\n }\n } else {\n const withoutExt = \"/\" + relativePath.slice(0, -ext.length);\n entries.set(withoutExt, entry);\n }\n }\n }\n\n // Third pass: buffer small files in memory for zero-syscall serving.\n // For small compressed variants (e.g. a 50KB JS bundle → ~15KB brotli),\n // res.end(buffer) is ~2x faster than createReadStream().pipe() because\n // it skips fd open/close and stream plumbing overhead.\n // Reads are chunked at 64 concurrent to avoid fd exhaustion on large projects.\n // Deduplicate at the entry level first: HTML aliases share the same\n // StaticFileEntry by reference, so entries.values() yields duplicates for\n // paths like /about and /about.html. Deduping entries avoids iterating\n // their variants multiple times on sites with many HTML pages.\n const toBuffer: FileVariant[] = [];\n const seenEntries = new Set<StaticFileEntry>();\n for (const entry of entries.values()) {\n if (seenEntries.has(entry)) continue;\n seenEntries.add(entry);\n for (const variant of [entry.original, entry.br, entry.gz, entry.zst]) {\n if (!variant || variant.size > BUFFER_THRESHOLD) continue;\n toBuffer.push(variant);\n }\n }\n for (let i = 0; i < toBuffer.length; i += 64) {\n await Promise.all(\n toBuffer.slice(i, i + 64).map(async (v) => {\n v.buffer = await fsp.readFile(v.path);\n }),\n );\n }\n\n return new StaticFileCache(entries);\n }\n\n /**\n * Look up cached metadata for a URL pathname.\n *\n * Returns undefined if the file is not in the cache. The root path \"/\"\n * always returns undefined — index.html is served by SSR/RSC.\n */\n lookup(pathname: string): StaticFileEntry | undefined {\n if (pathname === \"/\") return undefined;\n\n // Block .vite/ access (including encoded variants that were decoded before lookup)\n if (pathname.startsWith(\"/.vite/\") || pathname === \"/.vite\") return undefined;\n\n return this.entries.get(pathname);\n }\n}\n\n/**\n * Extract a stable weak ETag from a Vite hashed filename (e.g. `app-DqZc3R4n.js`).\n * The hash is a content hash computed by the bundler — deterministic across\n * identical builds regardless of filesystem timestamps.\n *\n * Must be a weak validator (W/) because the same tag is shared across\n * content-encoded variants (original, .br, .gz, .zst) which are byte-different.\n * Returns null if the filename doesn't contain a recognizable hash suffix,\n * so the caller can fall back to mtime-based ETags.\n */\nexport function etagFromFilenameHash(relativePath: string, ext: string): string | null {\n const basename = path.basename(relativePath, ext);\n const lastDash = basename.lastIndexOf(\"-\");\n if (lastDash === -1 || lastDash === basename.length - 1) return null;\n const suffix = basename.slice(lastDash + 1);\n // Vite emits 8-char base64url hashes; allow 6-12 for other bundlers.\n // If Rolldown changes its hash length, update this range.\n return suffix.length >= 6 && suffix.length <= 12 && /^[A-Za-z0-9_-]+$/.test(suffix)\n ? `W/\"${suffix}\"`\n : null;\n}\n\nfunction buildVariant(\n info: { fullPath: string; size: number },\n baseHeaders: Record<string, string>,\n encoding: string,\n): FileVariant {\n return {\n path: info.fullPath,\n size: info.size,\n headers: {\n ...baseHeaders,\n \"Content-Encoding\": encoding,\n \"Content-Length\": String(info.size),\n Vary: \"Accept-Encoding\",\n },\n };\n}\n\n/** Batch size for concurrent stat() calls during directory walk. */\nconst STAT_BATCH_SIZE = 64;\n\n/**\n * Walk a directory recursively, yielding file paths and stats.\n *\n * Batches stat() calls per directory to avoid sequential syscall overhead\n * for large dist/client/ directories.\n */\nasync function* walkFilesWithStats(\n dir: string,\n base: string = dir,\n): AsyncGenerator<{\n relativePath: string;\n fullPath: string;\n stat: { size: number; mtimeMs: number };\n}> {\n let entries;\n try {\n entries = await fsp.readdir(dir, { withFileTypes: true });\n } catch {\n return; // directory doesn't exist or unreadable\n }\n\n // Recurse into subdirectories first (they yield their own batched stats)\n const files: string[] = [];\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n yield* walkFilesWithStats(fullPath, base);\n } else if (entry.isFile()) {\n files.push(fullPath);\n }\n }\n\n // Batch stat() calls for files in this directory\n for (let i = 0; i < files.length; i += STAT_BATCH_SIZE) {\n const batch = files.slice(i, i + STAT_BATCH_SIZE);\n const stats = await Promise.all(batch.map((f) => fsp.stat(f)));\n for (let j = 0; j < batch.length; j++) {\n yield {\n relativePath: normalizePathSeparators(path.relative(base, batch[j])),\n fullPath: batch[j],\n stat: { size: stats[j].size, mtimeMs: stats[j].mtimeMs },\n };\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { ArtifactCompatibilityEnvelope } from "./artifact-compatibility.js";
2
+
3
+ //#region src/server/static-layout-client-reuse-proof.d.ts
4
+ type StaticLayoutClientReuseProofInput = Readonly<{
5
+ artifactCompatibility: ArtifactCompatibilityEnvelope;
6
+ layoutId: string;
7
+ rootBoundaryId: string | null;
8
+ routeId: string;
9
+ variantCacheKey: string;
10
+ }>;
11
+ declare function createStaticLayoutClientReuseRouteId(layoutId: string): string;
12
+ declare function createStaticLayoutClientReusePayloadHash(input: StaticLayoutClientReuseProofInput): string;
13
+ declare function createStaticLayoutClientReuseArtifactCompatibility(input: StaticLayoutClientReuseProofInput): ArtifactCompatibilityEnvelope;
14
+ //#endregion
15
+ export { createStaticLayoutClientReuseArtifactCompatibility, createStaticLayoutClientReusePayloadHash, createStaticLayoutClientReuseRouteId };
16
+ //# sourceMappingURL=static-layout-client-reuse-proof.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { ARTIFACT_COMPATIBILITY_PROOF_FIELDS } from "./artifact-compatibility.js";
2
+ import { createClientReusePayloadHash } from "./client-reuse-manifest.js";
3
+ //#region src/server/static-layout-client-reuse-proof.ts
4
+ function createStaticLayoutClientReuseRouteId(layoutId) {
5
+ return `static-layout:${createClientReusePayloadHash(layoutId)}`;
6
+ }
7
+ function createCanonicalProofPairs(input) {
8
+ return ARTIFACT_COMPATIBILITY_PROOF_FIELDS.map((field) => [field, input.artifactCompatibility[field]]);
9
+ }
10
+ function createStaticLayoutClientReusePayloadHash(input) {
11
+ return createClientReusePayloadHash(JSON.stringify({
12
+ artifactCompatibilityPairs: createCanonicalProofPairs(input),
13
+ layoutId: input.layoutId,
14
+ rootBoundaryId: input.rootBoundaryId,
15
+ variantCacheKey: input.variantCacheKey
16
+ }));
17
+ }
18
+ function createStaticLayoutClientReuseArtifactCompatibility(input) {
19
+ return {
20
+ ...input.artifactCompatibility,
21
+ graphVersion: `static-layout-graph:${createClientReusePayloadHash(JSON.stringify({
22
+ layoutId: input.layoutId,
23
+ rootBoundaryId: input.rootBoundaryId
24
+ }))}`,
25
+ renderEpoch: `static-layout:${createClientReusePayloadHash(JSON.stringify({
26
+ layoutId: input.layoutId,
27
+ rootBoundaryId: input.rootBoundaryId,
28
+ variantCacheKey: input.variantCacheKey
29
+ }))}`
30
+ };
31
+ }
32
+ //#endregion
33
+ export { createStaticLayoutClientReuseArtifactCompatibility, createStaticLayoutClientReusePayloadHash, createStaticLayoutClientReuseRouteId };
34
+
35
+ //# sourceMappingURL=static-layout-client-reuse-proof.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static-layout-client-reuse-proof.js","names":[],"sources":["../../src/server/static-layout-client-reuse-proof.ts"],"sourcesContent":["import {\n type ArtifactCompatibilityEnvelope,\n ARTIFACT_COMPATIBILITY_PROOF_FIELDS,\n} from \"./artifact-compatibility.js\";\nimport { createClientReusePayloadHash } from \"./client-reuse-manifest.js\";\n\ntype StaticLayoutClientReuseProofInput = Readonly<{\n artifactCompatibility: ArtifactCompatibilityEnvelope;\n layoutId: string;\n rootBoundaryId: string | null;\n routeId: string;\n variantCacheKey: string;\n}>;\n\ntype ProofFieldPair = readonly [string, string | number | null];\n\nexport function createStaticLayoutClientReuseRouteId(layoutId: string): string {\n return `static-layout:${createClientReusePayloadHash(layoutId)}`;\n}\n\nfunction createCanonicalProofPairs(\n input: StaticLayoutClientReuseProofInput,\n): readonly ProofFieldPair[] {\n return ARTIFACT_COMPATIBILITY_PROOF_FIELDS.map((field) => [\n field,\n input.artifactCompatibility[field],\n ]);\n}\n\nexport function createStaticLayoutClientReusePayloadHash(\n input: StaticLayoutClientReuseProofInput,\n): string {\n // ARTIFACT_COMPATIBILITY_PROOF_FIELDS order is load-bearing for hash\n // determinism. The artifact compatibility section is serialized as an\n // array of [field, value] pairs in declaration order — no object key-order\n // dependency — so the hash is invariant across runtimes. Reordering the\n // field list silently changes every hash.\n //\n // routeId is intentionally excluded. This is a layout-scoped hash —\n // sibling routes under the same layout must produce identical payload\n // hashes, otherwise the cross-check rejects on payload hash mismatch\n // before reaching the exact-equality gate.\n // If the field list must change, bump the hash algorithm or namespace to\n // avoid silent cross-deployment hash drift.\n return createClientReusePayloadHash(\n JSON.stringify({\n artifactCompatibilityPairs: createCanonicalProofPairs(input),\n layoutId: input.layoutId,\n rootBoundaryId: input.rootBoundaryId,\n variantCacheKey: input.variantCacheKey,\n }),\n );\n}\n\n// The app-route payload envelope is route-scoped and may not carry a render epoch\n// during this wave. Static-layout skip needs layout-scoped compatibility so\n// sibling-route navigation can reuse a retained static layout without treating\n// the source route id as part of the retained artifact identity.\n//\n// renderEpoch is scoped to layout identity (layoutId, rootBoundaryId) and the\n// rendering variant (variantCacheKey). The leaf routeId is intentionally\n// excluded — sibling routes under the same layout must produce the same\n// renderEpoch, otherwise exact-equality skip gating would silently disable\n// skip transport for the primary use case (navigating between sibling pages).\nexport function createStaticLayoutClientReuseArtifactCompatibility(\n input: StaticLayoutClientReuseProofInput,\n): ArtifactCompatibilityEnvelope {\n return {\n ...input.artifactCompatibility,\n graphVersion: `static-layout-graph:${createClientReusePayloadHash(\n JSON.stringify({\n layoutId: input.layoutId,\n rootBoundaryId: input.rootBoundaryId,\n }),\n )}`,\n renderEpoch: `static-layout:${createClientReusePayloadHash(\n JSON.stringify({\n layoutId: input.layoutId,\n rootBoundaryId: input.rootBoundaryId,\n variantCacheKey: input.variantCacheKey,\n }),\n )}`,\n };\n}\n"],"mappings":";;;AAgBA,SAAgB,qCAAqC,UAA0B;CAC7E,OAAO,iBAAiB,6BAA6B,SAAS;;AAGhE,SAAS,0BACP,OAC2B;CAC3B,OAAO,oCAAoC,KAAK,UAAU,CACxD,OACA,MAAM,sBAAsB,OAC7B,CAAC;;AAGJ,SAAgB,yCACd,OACQ;CAaR,OAAO,6BACL,KAAK,UAAU;EACb,4BAA4B,0BAA0B,MAAM;EAC5D,UAAU,MAAM;EAChB,gBAAgB,MAAM;EACtB,iBAAiB,MAAM;EACxB,CAAC,CACH;;AAaH,SAAgB,mDACd,OAC+B;CAC/B,OAAO;EACL,GAAG,MAAM;EACT,cAAc,uBAAuB,6BACnC,KAAK,UAAU;GACb,UAAU,MAAM;GAChB,gBAAgB,MAAM;GACvB,CAAC,CACH;EACD,aAAa,iBAAiB,6BAC5B,KAAK,UAAU;GACb,UAAU,MAAM;GAChB,gBAAgB,MAAM;GACtB,iBAAiB,MAAM;GACxB,CAAC,CACH;EACF"}
@@ -1,12 +1,14 @@
1
1
  //#region src/shims/app-router-scroll-state.d.ts
2
2
  type AppRouterScrollIntent = Readonly<{
3
+ commitId: number | null;
3
4
  hash: string | null;
4
5
  id: number;
5
6
  }>;
6
7
  declare function beginAppRouterScrollIntent(hash: string | null): AppRouterScrollIntent;
7
8
  declare function clearAppRouterScrollIntent(): void;
8
9
  declare function getPendingAppRouterScrollIntent(): AppRouterScrollIntent | null;
9
- declare function consumeAppRouterScrollIntent(expected?: AppRouterScrollIntent): AppRouterScrollIntent | null;
10
+ declare function claimAppRouterScrollIntentForCommit(expected: AppRouterScrollIntent | null | undefined, commitId: number): void;
11
+ declare function consumeAppRouterScrollIntent(expected: AppRouterScrollIntent | null | undefined, commitId?: number): AppRouterScrollIntent | null;
10
12
  //#endregion
11
- export { AppRouterScrollIntent, beginAppRouterScrollIntent, clearAppRouterScrollIntent, consumeAppRouterScrollIntent, getPendingAppRouterScrollIntent };
13
+ export { AppRouterScrollIntent, beginAppRouterScrollIntent, claimAppRouterScrollIntentForCommit, clearAppRouterScrollIntent, consumeAppRouterScrollIntent, getPendingAppRouterScrollIntent };
12
14
  //# sourceMappingURL=app-router-scroll-state.d.ts.map
@@ -12,6 +12,7 @@ function beginAppRouterScrollIntent(hash) {
12
12
  const store = getScrollIntentStore();
13
13
  store.nextId += 1;
14
14
  const intent = {
15
+ commitId: null,
15
16
  hash,
16
17
  id: store.nextId
17
18
  };
@@ -24,15 +25,27 @@ function clearAppRouterScrollIntent() {
24
25
  function getPendingAppRouterScrollIntent() {
25
26
  return getScrollIntentStore().pending;
26
27
  }
27
- function consumeAppRouterScrollIntent(expected) {
28
+ function claimAppRouterScrollIntentForCommit(expected, commitId) {
29
+ const store = getScrollIntentStore();
30
+ const intent = store.pending;
31
+ if (expected === null || expected === void 0 || intent === null) return;
32
+ if (intent.id !== expected.id) return;
33
+ store.pending = {
34
+ ...intent,
35
+ commitId
36
+ };
37
+ }
38
+ function consumeAppRouterScrollIntent(expected, commitId) {
39
+ if (expected === null || expected === void 0) return null;
28
40
  const store = getScrollIntentStore();
29
41
  const intent = store.pending;
30
42
  if (intent === null) return null;
31
- if (expected && intent.id !== expected.id) return null;
43
+ if (intent.id !== expected.id) return null;
44
+ if (commitId !== void 0 && intent.commitId !== commitId) return null;
32
45
  store.pending = null;
33
46
  return intent;
34
47
  }
35
48
  //#endregion
36
- export { beginAppRouterScrollIntent, clearAppRouterScrollIntent, consumeAppRouterScrollIntent, getPendingAppRouterScrollIntent };
49
+ export { beginAppRouterScrollIntent, claimAppRouterScrollIntentForCommit, clearAppRouterScrollIntent, consumeAppRouterScrollIntent, getPendingAppRouterScrollIntent };
37
50
 
38
51
  //# sourceMappingURL=app-router-scroll-state.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"app-router-scroll-state.js","names":[],"sources":["../../src/shims/app-router-scroll-state.ts"],"sourcesContent":["export type AppRouterScrollIntent = Readonly<{\n hash: string | null;\n id: number;\n}>;\n\n// A scroll intent is staged by `navigateClientSide` (next/navigation) before an\n// RSC navigation and consumed by the committed `AppRouterScrollTarget`. Both run\n// in the browser, but next/navigation and this module can be loaded through\n// separate Vite module instances (see the Symbol.for navigation state in\n// navigation.ts and AGENTS.md \"RSC and SSR Are Separate Vite Environments\"). If\n// the writer and consumer held different module-level copies, the staged intent\n// would be invisible to the consumer and scroll/focus would silently no-op.\n// Store the single pending intent and the id counter on a Symbol.for global so\n// every instance shares one slot, matching the rest of the navigation state.\nconst _SCROLL_INTENT_KEY = Symbol.for(\"vinext.appRouterScrollIntent\");\n\ntype ScrollIntentStore = {\n nextId: number;\n pending: AppRouterScrollIntent | null;\n};\n\ntype ScrollIntentGlobal = typeof globalThis & {\n [_SCROLL_INTENT_KEY]?: ScrollIntentStore;\n};\n\nfunction getScrollIntentStore(): ScrollIntentStore {\n const globalState = globalThis as ScrollIntentGlobal;\n globalState[_SCROLL_INTENT_KEY] ??= { nextId: 0, pending: null };\n return globalState[_SCROLL_INTENT_KEY]!;\n}\n\nexport function beginAppRouterScrollIntent(hash: string | null): AppRouterScrollIntent {\n const store = getScrollIntentStore();\n store.nextId += 1;\n const intent = {\n hash,\n id: store.nextId,\n };\n store.pending = intent;\n return intent;\n}\n\nexport function clearAppRouterScrollIntent(): void {\n getScrollIntentStore().pending = null;\n}\n\nexport function getPendingAppRouterScrollIntent(): AppRouterScrollIntent | null {\n return getScrollIntentStore().pending;\n}\n\nexport function consumeAppRouterScrollIntent(\n expected?: AppRouterScrollIntent,\n): AppRouterScrollIntent | null {\n const store = getScrollIntentStore();\n const intent = store.pending;\n if (intent === null) return null;\n if (expected && intent.id !== expected.id) return null;\n\n store.pending = null;\n return intent;\n}\n"],"mappings":";AAcA,MAAM,qBAAqB,OAAO,IAAI,+BAA+B;AAWrE,SAAS,uBAA0C;CACjD,MAAM,cAAc;CACpB,YAAY,wBAAwB;EAAE,QAAQ;EAAG,SAAS;EAAM;CAChE,OAAO,YAAY;;AAGrB,SAAgB,2BAA2B,MAA4C;CACrF,MAAM,QAAQ,sBAAsB;CACpC,MAAM,UAAU;CAChB,MAAM,SAAS;EACb;EACA,IAAI,MAAM;EACX;CACD,MAAM,UAAU;CAChB,OAAO;;AAGT,SAAgB,6BAAmC;CACjD,sBAAsB,CAAC,UAAU;;AAGnC,SAAgB,kCAAgE;CAC9E,OAAO,sBAAsB,CAAC;;AAGhC,SAAgB,6BACd,UAC8B;CAC9B,MAAM,QAAQ,sBAAsB;CACpC,MAAM,SAAS,MAAM;CACrB,IAAI,WAAW,MAAM,OAAO;CAC5B,IAAI,YAAY,OAAO,OAAO,SAAS,IAAI,OAAO;CAElD,MAAM,UAAU;CAChB,OAAO"}
1
+ {"version":3,"file":"app-router-scroll-state.js","names":[],"sources":["../../src/shims/app-router-scroll-state.ts"],"sourcesContent":["export type AppRouterScrollIntent = Readonly<{\n commitId: number | null;\n hash: string | null;\n id: number;\n}>;\n\n// A scroll intent is staged by `navigateClientSide` (next/navigation) before an\n// RSC navigation and consumed by the committed `AppRouterScrollTarget`. Both run\n// in the browser, but next/navigation and this module can be loaded through\n// separate Vite module instances (see the Symbol.for navigation state in\n// navigation.ts and AGENTS.md \"RSC and SSR Are Separate Vite Environments\"). If\n// the writer and consumer held different module-level copies, the staged intent\n// would be invisible to the consumer and scroll/focus would silently no-op.\n// Store the single pending intent and the id counter on a Symbol.for global so\n// every instance shares one slot, matching the rest of the navigation state.\nconst _SCROLL_INTENT_KEY = Symbol.for(\"vinext.appRouterScrollIntent\");\n\ntype ScrollIntentStore = {\n nextId: number;\n pending: AppRouterScrollIntent | null;\n};\n\ntype ScrollIntentGlobal = typeof globalThis & {\n [_SCROLL_INTENT_KEY]?: ScrollIntentStore;\n};\n\nfunction getScrollIntentStore(): ScrollIntentStore {\n const globalState = globalThis as ScrollIntentGlobal;\n globalState[_SCROLL_INTENT_KEY] ??= { nextId: 0, pending: null };\n return globalState[_SCROLL_INTENT_KEY]!;\n}\n\nexport function beginAppRouterScrollIntent(hash: string | null): AppRouterScrollIntent {\n const store = getScrollIntentStore();\n store.nextId += 1;\n const intent = {\n commitId: null,\n hash,\n id: store.nextId,\n };\n store.pending = intent;\n return intent;\n}\n\nexport function clearAppRouterScrollIntent(): void {\n getScrollIntentStore().pending = null;\n}\n\nexport function getPendingAppRouterScrollIntent(): AppRouterScrollIntent | null {\n return getScrollIntentStore().pending;\n}\n\nexport function claimAppRouterScrollIntentForCommit(\n expected: AppRouterScrollIntent | null | undefined,\n commitId: number,\n): void {\n const store = getScrollIntentStore();\n const intent = store.pending;\n if (expected === null || expected === undefined || intent === null) return;\n if (intent.id !== expected.id) return;\n\n store.pending = {\n ...intent,\n commitId,\n };\n}\n\nexport function consumeAppRouterScrollIntent(\n expected: AppRouterScrollIntent | null | undefined,\n commitId?: number,\n): AppRouterScrollIntent | null {\n if (expected === null || expected === undefined) return null;\n const store = getScrollIntentStore();\n const intent = store.pending;\n if (intent === null) return null;\n if (intent.id !== expected.id) return null;\n if (commitId !== undefined && intent.commitId !== commitId) return null;\n\n store.pending = null;\n return intent;\n}\n"],"mappings":";AAeA,MAAM,qBAAqB,OAAO,IAAI,+BAA+B;AAWrE,SAAS,uBAA0C;CACjD,MAAM,cAAc;CACpB,YAAY,wBAAwB;EAAE,QAAQ;EAAG,SAAS;EAAM;CAChE,OAAO,YAAY;;AAGrB,SAAgB,2BAA2B,MAA4C;CACrF,MAAM,QAAQ,sBAAsB;CACpC,MAAM,UAAU;CAChB,MAAM,SAAS;EACb,UAAU;EACV;EACA,IAAI,MAAM;EACX;CACD,MAAM,UAAU;CAChB,OAAO;;AAGT,SAAgB,6BAAmC;CACjD,sBAAsB,CAAC,UAAU;;AAGnC,SAAgB,kCAAgE;CAC9E,OAAO,sBAAsB,CAAC;;AAGhC,SAAgB,oCACd,UACA,UACM;CACN,MAAM,QAAQ,sBAAsB;CACpC,MAAM,SAAS,MAAM;CACrB,IAAI,aAAa,QAAQ,aAAa,KAAA,KAAa,WAAW,MAAM;CACpE,IAAI,OAAO,OAAO,SAAS,IAAI;CAE/B,MAAM,UAAU;EACd,GAAG;EACH;EACD;;AAGH,SAAgB,6BACd,UACA,UAC8B;CAC9B,IAAI,aAAa,QAAQ,aAAa,KAAA,GAAW,OAAO;CACxD,MAAM,QAAQ,sBAAsB;CACpC,MAAM,SAAS,MAAM;CACrB,IAAI,WAAW,MAAM,OAAO;CAC5B,IAAI,OAAO,OAAO,SAAS,IAAI,OAAO;CACtC,IAAI,aAAa,KAAA,KAAa,OAAO,aAAa,UAAU,OAAO;CAEnE,MAAM,UAAU;CAChB,OAAO"}
@@ -1,14 +1,28 @@
1
1
  import * as React$1 from "react";
2
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
2
3
 
3
4
  //#region src/shims/app-router-scroll.d.ts
4
- declare class AppRouterScrollTarget extends React$1.Component<{
5
+ declare class AppRouterScrollTargetInner extends React$1.Component<{
5
6
  children: React$1.ReactNode;
7
+ commitId: number | null;
6
8
  }> {
7
9
  handlePotentialScroll: () => void;
8
10
  componentDidMount(): void;
9
11
  componentDidUpdate(): void;
10
12
  render(): React$1.ReactNode;
11
13
  }
14
+ declare function AppRouterScrollCommitProvider({
15
+ children,
16
+ commitId
17
+ }: {
18
+ children?: React$1.ReactNode;
19
+ commitId: number | null;
20
+ }): _$react_jsx_runtime0.JSX.Element;
21
+ declare function AppRouterScrollTarget({
22
+ children
23
+ }: {
24
+ children: React$1.ReactNode;
25
+ }): _$react_jsx_runtime0.JSX.Element;
12
26
  //#endregion
13
- export { AppRouterScrollTarget };
27
+ export { AppRouterScrollCommitProvider, AppRouterScrollTarget, AppRouterScrollTargetInner };
14
28
  //# sourceMappingURL=app-router-scroll.d.ts.map
@@ -2,8 +2,10 @@
2
2
  import { decodeHashFragment } from "./hash-scroll.js";
3
3
  import { consumeAppRouterScrollIntent, getPendingAppRouterScrollIntent } from "./app-router-scroll-state.js";
4
4
  import * as React$1 from "react";
5
+ import { jsx } from "react/jsx-runtime";
5
6
  import * as ReactDOM from "react-dom";
6
7
  //#region src/shims/app-router-scroll.tsx
8
+ const AppRouterScrollCommitContext = React$1.createContext(null);
7
9
  const reactDomInternalsKey = "__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE";
8
10
  const rectProperties = [
9
11
  "bottom",
@@ -71,15 +73,16 @@ function scrollToElement(target, hash) {
71
73
  inline: "nearest"
72
74
  });
73
75
  }
74
- var AppRouterScrollTarget = class extends React$1.Component {
76
+ var AppRouterScrollTargetInner = class extends React$1.Component {
75
77
  handlePotentialScroll = () => {
76
78
  const intent = getPendingAppRouterScrollIntent();
77
79
  if (intent === null) return;
80
+ if (this.props.commitId === null || intent.commitId !== this.props.commitId) return;
78
81
  let target;
79
82
  if (intent.hash !== null) target = getHashFragmentDomNode(intent.hash);
80
83
  else target = findNextScrollTarget(findDOMNode(this));
81
84
  if (target === null) return;
82
- const consumed = consumeAppRouterScrollIntent(intent);
85
+ const consumed = consumeAppRouterScrollIntent(intent, this.props.commitId);
83
86
  if (consumed === null) return;
84
87
  scrollToElement(target, consumed.hash);
85
88
  target.focus({ preventScroll: true });
@@ -94,7 +97,19 @@ var AppRouterScrollTarget = class extends React$1.Component {
94
97
  return this.props.children;
95
98
  }
96
99
  };
100
+ function AppRouterScrollCommitProvider({ children, commitId }) {
101
+ return /* @__PURE__ */ jsx(AppRouterScrollCommitContext.Provider, {
102
+ value: commitId,
103
+ children
104
+ });
105
+ }
106
+ function AppRouterScrollTarget({ children }) {
107
+ return /* @__PURE__ */ jsx(AppRouterScrollTargetInner, {
108
+ commitId: React$1.useContext(AppRouterScrollCommitContext),
109
+ children
110
+ });
111
+ }
97
112
  //#endregion
98
- export { AppRouterScrollTarget };
113
+ export { AppRouterScrollCommitProvider, AppRouterScrollTarget, AppRouterScrollTargetInner };
99
114
 
100
115
  //# sourceMappingURL=app-router-scroll.js.map