vinext 0.0.53 → 0.0.54

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 (188) hide show
  1. package/dist/build/inline-css.d.ts +7 -0
  2. package/dist/build/inline-css.js +50 -0
  3. package/dist/build/inline-css.js.map +1 -0
  4. package/dist/build/prerender.js +2 -1
  5. package/dist/build/prerender.js.map +1 -1
  6. package/dist/check.js +4 -0
  7. package/dist/check.js.map +1 -1
  8. package/dist/client/navigation-runtime.d.ts +2 -1
  9. package/dist/client/navigation-runtime.js.map +1 -1
  10. package/dist/client/window-next.d.ts +7 -0
  11. package/dist/client/window-next.js.map +1 -1
  12. package/dist/config/next-config.d.ts +83 -1
  13. package/dist/config/next-config.js +131 -2
  14. package/dist/config/next-config.js.map +1 -1
  15. package/dist/deploy.js +13 -0
  16. package/dist/deploy.js.map +1 -1
  17. package/dist/entries/app-browser-entry.d.ts +11 -1
  18. package/dist/entries/app-browser-entry.js +16 -6
  19. package/dist/entries/app-browser-entry.js.map +1 -1
  20. package/dist/entries/app-rsc-entry.d.ts +8 -1
  21. package/dist/entries/app-rsc-entry.js +18 -5
  22. package/dist/entries/app-rsc-entry.js.map +1 -1
  23. package/dist/entries/app-rsc-manifest.d.ts +21 -1
  24. package/dist/entries/app-rsc-manifest.js +6 -4
  25. package/dist/entries/app-rsc-manifest.js.map +1 -1
  26. package/dist/entries/pages-client-entry.d.ts +4 -1
  27. package/dist/entries/pages-client-entry.js +18 -2
  28. package/dist/entries/pages-client-entry.js.map +1 -1
  29. package/dist/entries/pages-server-entry.js +82 -4
  30. package/dist/entries/pages-server-entry.js.map +1 -1
  31. package/dist/entries/runtime-entry-module.d.ts +1 -10
  32. package/dist/entries/runtime-entry-module.js +2 -12
  33. package/dist/entries/runtime-entry-module.js.map +1 -1
  34. package/dist/index.js +63 -5
  35. package/dist/index.js.map +1 -1
  36. package/dist/plugins/remove-console.d.ts +16 -0
  37. package/dist/plugins/remove-console.js +176 -0
  38. package/dist/plugins/remove-console.js.map +1 -0
  39. package/dist/routing/app-route-graph.d.ts +24 -1
  40. package/dist/routing/app-route-graph.js +52 -4
  41. package/dist/routing/app-route-graph.js.map +1 -1
  42. package/dist/routing/app-router.d.ts +2 -2
  43. package/dist/routing/app-router.js +2 -2
  44. package/dist/routing/app-router.js.map +1 -1
  45. package/dist/routing/file-matcher.d.ts +21 -1
  46. package/dist/routing/file-matcher.js +39 -1
  47. package/dist/routing/file-matcher.js.map +1 -1
  48. package/dist/routing/pages-router.d.ts +1 -1
  49. package/dist/routing/pages-router.js +10 -3
  50. package/dist/routing/pages-router.js.map +1 -1
  51. package/dist/server/api-handler.js +1 -1
  52. package/dist/server/app-browser-entry.js +25 -16
  53. package/dist/server/app-browser-entry.js.map +1 -1
  54. package/dist/server/app-browser-navigation-controller.d.ts +2 -0
  55. package/dist/server/app-browser-navigation-controller.js +4 -0
  56. package/dist/server/app-browser-navigation-controller.js.map +1 -1
  57. package/dist/server/app-elements-wire.d.ts +13 -4
  58. package/dist/server/app-elements-wire.js +10 -1
  59. package/dist/server/app-elements-wire.js.map +1 -1
  60. package/dist/server/app-elements.d.ts +2 -2
  61. package/dist/server/app-elements.js +2 -2
  62. package/dist/server/app-elements.js.map +1 -1
  63. package/dist/server/app-fallback-renderer.d.ts +15 -5
  64. package/dist/server/app-fallback-renderer.js +10 -4
  65. package/dist/server/app-fallback-renderer.js.map +1 -1
  66. package/dist/server/app-inline-css-client.d.ts +7 -0
  67. package/dist/server/app-inline-css-client.js +37 -0
  68. package/dist/server/app-inline-css-client.js.map +1 -0
  69. package/dist/server/app-page-boundary.d.ts +21 -1
  70. package/dist/server/app-page-boundary.js +28 -3
  71. package/dist/server/app-page-boundary.js.map +1 -1
  72. package/dist/server/app-page-cache.d.ts +7 -3
  73. package/dist/server/app-page-cache.js +7 -7
  74. package/dist/server/app-page-cache.js.map +1 -1
  75. package/dist/server/app-page-dispatch.d.ts +10 -1
  76. package/dist/server/app-page-dispatch.js +126 -79
  77. package/dist/server/app-page-dispatch.js.map +1 -1
  78. package/dist/server/app-page-element-builder.js +12 -28
  79. package/dist/server/app-page-element-builder.js.map +1 -1
  80. package/dist/server/app-page-render-identity.d.ts +22 -0
  81. package/dist/server/app-page-render-identity.js +42 -0
  82. package/dist/server/app-page-render-identity.js.map +1 -0
  83. package/dist/server/app-page-render.d.ts +8 -1
  84. package/dist/server/app-page-render.js +4 -1
  85. package/dist/server/app-page-render.js.map +1 -1
  86. package/dist/server/app-page-request.d.ts +6 -3
  87. package/dist/server/app-page-request.js +5 -2
  88. package/dist/server/app-page-request.js.map +1 -1
  89. package/dist/server/app-page-response.js +2 -2
  90. package/dist/server/app-page-response.js.map +1 -1
  91. package/dist/server/app-page-route-wiring.d.ts +15 -0
  92. package/dist/server/app-page-route-wiring.js +7 -5
  93. package/dist/server/app-page-route-wiring.js.map +1 -1
  94. package/dist/server/app-page-stream.d.ts +11 -0
  95. package/dist/server/app-page-stream.js +1 -0
  96. package/dist/server/app-page-stream.js.map +1 -1
  97. package/dist/server/app-route-handler-response.js +37 -5
  98. package/dist/server/app-route-handler-response.js.map +1 -1
  99. package/dist/server/app-rsc-handler.d.ts +14 -3
  100. package/dist/server/app-rsc-handler.js +45 -5
  101. package/dist/server/app-rsc-handler.js.map +1 -1
  102. package/dist/server/app-rsc-request-normalization.d.ts +2 -1
  103. package/dist/server/app-rsc-request-normalization.js +3 -2
  104. package/dist/server/app-rsc-request-normalization.js.map +1 -1
  105. package/dist/server/app-server-action-execution.d.ts +21 -3
  106. package/dist/server/app-server-action-execution.js +42 -7
  107. package/dist/server/app-server-action-execution.js.map +1 -1
  108. package/dist/server/app-ssr-entry.d.ts +6 -0
  109. package/dist/server/app-ssr-entry.js +22 -7
  110. package/dist/server/app-ssr-entry.js.map +1 -1
  111. package/dist/server/app-ssr-error-meta.js +3 -3
  112. package/dist/server/app-ssr-error-meta.js.map +1 -1
  113. package/dist/server/app-ssr-stream.d.ts +2 -1
  114. package/dist/server/app-ssr-stream.js +176 -31
  115. package/dist/server/app-ssr-stream.js.map +1 -1
  116. package/dist/server/client-trace-metadata.d.ts +31 -0
  117. package/dist/server/client-trace-metadata.js +83 -0
  118. package/dist/server/client-trace-metadata.js.map +1 -0
  119. package/dist/server/cookie-utils.d.ts +13 -0
  120. package/dist/server/cookie-utils.js +20 -0
  121. package/dist/server/cookie-utils.js.map +1 -0
  122. package/dist/server/dev-server.d.ts +8 -1
  123. package/dist/server/dev-server.js +34 -5
  124. package/dist/server/dev-server.js.map +1 -1
  125. package/dist/server/html.d.ts +2 -1
  126. package/dist/server/html.js +6 -1
  127. package/dist/server/html.js.map +1 -1
  128. package/dist/server/isr-cache.d.ts +7 -5
  129. package/dist/server/isr-cache.js +17 -6
  130. package/dist/server/isr-cache.js.map +1 -1
  131. package/dist/server/middleware-runtime.js +1 -2
  132. package/dist/server/middleware-runtime.js.map +1 -1
  133. package/dist/server/pages-document-initial-props.d.ts +7 -0
  134. package/dist/server/pages-document-initial-props.js +14 -0
  135. package/dist/server/pages-document-initial-props.js.map +1 -0
  136. package/dist/server/pages-page-data.js +3 -0
  137. package/dist/server/pages-page-data.js.map +1 -1
  138. package/dist/server/pages-page-method.d.ts +48 -0
  139. package/dist/server/pages-page-method.js +19 -0
  140. package/dist/server/pages-page-method.js.map +1 -0
  141. package/dist/server/pages-page-response.d.ts +6 -0
  142. package/dist/server/pages-page-response.js +10 -3
  143. package/dist/server/pages-page-response.js.map +1 -1
  144. package/dist/server/pages-serializable-props.d.ts +25 -0
  145. package/dist/server/pages-serializable-props.js +69 -0
  146. package/dist/server/pages-serializable-props.js.map +1 -0
  147. package/dist/server/prod-server.js +3 -0
  148. package/dist/server/prod-server.js.map +1 -1
  149. package/dist/server/server-action-not-found.js +3 -2
  150. package/dist/server/server-action-not-found.js.map +1 -1
  151. package/dist/server/static-file-cache.js +2 -1
  152. package/dist/server/static-file-cache.js.map +1 -1
  153. package/dist/shims/app-router-scroll-state.d.ts +4 -2
  154. package/dist/shims/app-router-scroll-state.js +16 -3
  155. package/dist/shims/app-router-scroll-state.js.map +1 -1
  156. package/dist/shims/app-router-scroll.d.ts +16 -2
  157. package/dist/shims/app-router-scroll.js +18 -3
  158. package/dist/shims/app-router-scroll.js.map +1 -1
  159. package/dist/shims/cache.d.ts +6 -0
  160. package/dist/shims/cache.js +7 -0
  161. package/dist/shims/cache.js.map +1 -1
  162. package/dist/shims/error.js +3 -0
  163. package/dist/shims/error.js.map +1 -1
  164. package/dist/shims/headers.d.ts +7 -0
  165. package/dist/shims/headers.js +9 -1
  166. package/dist/shims/headers.js.map +1 -1
  167. package/dist/shims/internal/app-route-detection.d.ts +37 -0
  168. package/dist/shims/internal/app-route-detection.js +69 -0
  169. package/dist/shims/internal/app-route-detection.js.map +1 -0
  170. package/dist/shims/link.d.ts +18 -2
  171. package/dist/shims/link.js +70 -6
  172. package/dist/shims/link.js.map +1 -1
  173. package/dist/shims/metadata.d.ts +7 -6
  174. package/dist/shims/metadata.js +9 -5
  175. package/dist/shims/metadata.js.map +1 -1
  176. package/dist/shims/navigation.d.ts +1 -2
  177. package/dist/shims/navigation.js +63 -12
  178. package/dist/shims/navigation.js.map +1 -1
  179. package/dist/shims/router.d.ts +5 -0
  180. package/dist/shims/router.js +14 -4
  181. package/dist/shims/router.js.map +1 -1
  182. package/dist/shims/script.d.ts +11 -1
  183. package/dist/shims/script.js +75 -6
  184. package/dist/shims/script.js.map +1 -1
  185. package/dist/utils/path.d.ts +13 -0
  186. package/dist/utils/path.js +16 -0
  187. package/dist/utils/path.js.map +1 -0
  188. package/package.json +1 -1
@@ -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,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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"}
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"app-router-scroll.js","names":["React"],"sources":["../../src/shims/app-router-scroll.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { decodeHashFragment } from \"./hash-scroll.js\";\nimport {\n consumeAppRouterScrollIntent,\n getPendingAppRouterScrollIntent,\n} from \"./app-router-scroll-state.js\";\n\nconst reactDomInternalsKey = \"__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE\";\nconst rectProperties = [\"bottom\", \"height\", \"left\", \"right\", \"top\", \"width\", \"x\", \"y\"] as const;\n\nfunction readFindDOMNode(): ((instance: React.ReactInstance | null | undefined) => unknown) | null {\n const internals = Reflect.get(ReactDOM, reactDomInternalsKey);\n if (typeof internals !== \"object\" || internals === null) {\n return null;\n }\n\n const findDOMNode = Reflect.get(internals, \"findDOMNode\");\n return typeof findDOMNode === \"function\" ? findDOMNode : null;\n}\n\nfunction findDOMNode(instance: React.ReactInstance | null | undefined): Element | Text | null {\n if (typeof window === \"undefined\") return null;\n\n const findDOMNodeImpl = readFindDOMNode();\n if (!findDOMNodeImpl) return null;\n\n const node = findDOMNodeImpl(instance);\n return node instanceof Element || node instanceof Text ? node : null;\n}\n\nfunction shouldSkipElement(element: HTMLElement): boolean {\n const position = getComputedStyle(element).position;\n if (position === \"fixed\" || position === \"sticky\") {\n return true;\n }\n\n const rect = element.getBoundingClientRect();\n return rectProperties.every((property) => rect[property] === 0);\n}\n\nfunction topOfElementInViewport(element: HTMLElement, viewportHeight: number): boolean {\n const rects = element.getClientRects();\n if (rects.length === 0) {\n return false;\n }\n\n let elementTop = Number.POSITIVE_INFINITY;\n for (const rect of rects) {\n if (rect.top < elementTop) {\n elementTop = rect.top;\n }\n }\n\n return elementTop >= 0 && elementTop <= viewportHeight;\n}\n\nfunction getHashFragmentDomNode(hash: string): HTMLElement | null {\n const fragment = decodeHashFragment(hash.startsWith(\"#\") ? hash.slice(1) : hash);\n if (fragment === \"top\") {\n return document.body;\n }\n\n const element = document.getElementById(fragment) ?? document.getElementsByName(fragment)[0];\n return element instanceof HTMLElement ? element : null;\n}\n\nfunction findNextScrollTarget(node: Element | Text | null): HTMLElement | null {\n if (!(node instanceof Element)) {\n return null;\n }\n\n let target: Element = node;\n while (!(target instanceof HTMLElement) || shouldSkipElement(target)) {\n if (target.nextElementSibling === null) {\n return null;\n }\n target = target.nextElementSibling;\n }\n\n return target;\n}\n\nfunction scrollToElement(target: HTMLElement, hash: string | null): void {\n if (hash !== null) {\n target.scrollIntoView({ behavior: \"auto\" });\n return;\n }\n\n const htmlElement = document.documentElement;\n const viewportHeight = htmlElement.clientHeight;\n\n if (topOfElementInViewport(target, viewportHeight)) {\n return;\n }\n\n htmlElement.scrollTop = 0;\n\n if (!topOfElementInViewport(target, viewportHeight)) {\n target.scrollIntoView({ behavior: \"auto\", block: \"start\", inline: \"nearest\" });\n }\n}\n\n// Must stay a class component: findDOMNode() needs a mounted class instance to\n// locate the first DOM node rendered by the children without introducing a\n// wrapper element. Converting this to a function component would break the\n// wrapperless targeting that mirrors Next's default scroll handler.\nexport class AppRouterScrollTarget extends React.Component<{ children: React.ReactNode }> {\n handlePotentialScroll = () => {\n const intent = getPendingAppRouterScrollIntent();\n if (intent === null) return;\n\n let target: HTMLElement | null;\n if (intent.hash !== null) {\n target = getHashFragmentDomNode(intent.hash);\n } else {\n // oxlint-disable-next-line react/no-find-dom-node -- Next's default App Router scroll handler targets wrapperless route content after commit.\n target = findNextScrollTarget(findDOMNode(this));\n }\n if (target === null) return;\n\n const consumed = consumeAppRouterScrollIntent(intent);\n if (consumed === null) return;\n\n scrollToElement(target, consumed.hash);\n // Next's default handler uses plain focus(), but that lets the browser run\n // a second implicit scroll after our explicit navigation scroll. Keep the\n // focus transfer while preserving the scroll position we just chose.\n target.focus({ preventScroll: true });\n };\n\n componentDidMount() {\n this.handlePotentialScroll();\n }\n\n componentDidUpdate() {\n this.handlePotentialScroll();\n }\n\n render() {\n return this.props.children;\n }\n}\n"],"mappings":";;;;;;AAUA,MAAM,uBAAuB;AAC7B,MAAM,iBAAiB;CAAC;CAAU;CAAU;CAAQ;CAAS;CAAO;CAAS;CAAK;CAAI;AAEtF,SAAS,kBAA0F;CACjG,MAAM,YAAY,QAAQ,IAAI,UAAU,qBAAqB;CAC7D,IAAI,OAAO,cAAc,YAAY,cAAc,MACjD,OAAO;CAGT,MAAM,cAAc,QAAQ,IAAI,WAAW,cAAc;CACzD,OAAO,OAAO,gBAAgB,aAAa,cAAc;;AAG3D,SAAS,YAAY,UAAyE;CAC5F,IAAI,OAAO,WAAW,aAAa,OAAO;CAE1C,MAAM,kBAAkB,iBAAiB;CACzC,IAAI,CAAC,iBAAiB,OAAO;CAE7B,MAAM,OAAO,gBAAgB,SAAS;CACtC,OAAO,gBAAgB,WAAW,gBAAgB,OAAO,OAAO;;AAGlE,SAAS,kBAAkB,SAA+B;CACxD,MAAM,WAAW,iBAAiB,QAAQ,CAAC;CAC3C,IAAI,aAAa,WAAW,aAAa,UACvC,OAAO;CAGT,MAAM,OAAO,QAAQ,uBAAuB;CAC5C,OAAO,eAAe,OAAO,aAAa,KAAK,cAAc,EAAE;;AAGjE,SAAS,uBAAuB,SAAsB,gBAAiC;CACrF,MAAM,QAAQ,QAAQ,gBAAgB;CACtC,IAAI,MAAM,WAAW,GACnB,OAAO;CAGT,IAAI,aAAa,OAAO;CACxB,KAAK,MAAM,QAAQ,OACjB,IAAI,KAAK,MAAM,YACb,aAAa,KAAK;CAItB,OAAO,cAAc,KAAK,cAAc;;AAG1C,SAAS,uBAAuB,MAAkC;CAChE,MAAM,WAAW,mBAAmB,KAAK,WAAW,IAAI,GAAG,KAAK,MAAM,EAAE,GAAG,KAAK;CAChF,IAAI,aAAa,OACf,OAAO,SAAS;CAGlB,MAAM,UAAU,SAAS,eAAe,SAAS,IAAI,SAAS,kBAAkB,SAAS,CAAC;CAC1F,OAAO,mBAAmB,cAAc,UAAU;;AAGpD,SAAS,qBAAqB,MAAiD;CAC7E,IAAI,EAAE,gBAAgB,UACpB,OAAO;CAGT,IAAI,SAAkB;CACtB,OAAO,EAAE,kBAAkB,gBAAgB,kBAAkB,OAAO,EAAE;EACpE,IAAI,OAAO,uBAAuB,MAChC,OAAO;EAET,SAAS,OAAO;;CAGlB,OAAO;;AAGT,SAAS,gBAAgB,QAAqB,MAA2B;CACvE,IAAI,SAAS,MAAM;EACjB,OAAO,eAAe,EAAE,UAAU,QAAQ,CAAC;EAC3C;;CAGF,MAAM,cAAc,SAAS;CAC7B,MAAM,iBAAiB,YAAY;CAEnC,IAAI,uBAAuB,QAAQ,eAAe,EAChD;CAGF,YAAY,YAAY;CAExB,IAAI,CAAC,uBAAuB,QAAQ,eAAe,EACjD,OAAO,eAAe;EAAE,UAAU;EAAQ,OAAO;EAAS,QAAQ;EAAW,CAAC;;AAQlF,IAAa,wBAAb,cAA2CA,QAAM,UAAyC;CACxF,8BAA8B;EAC5B,MAAM,SAAS,iCAAiC;EAChD,IAAI,WAAW,MAAM;EAErB,IAAI;EACJ,IAAI,OAAO,SAAS,MAClB,SAAS,uBAAuB,OAAO,KAAK;OAG5C,SAAS,qBAAqB,YAAY,KAAK,CAAC;EAElD,IAAI,WAAW,MAAM;EAErB,MAAM,WAAW,6BAA6B,OAAO;EACrD,IAAI,aAAa,MAAM;EAEvB,gBAAgB,QAAQ,SAAS,KAAK;EAItC,OAAO,MAAM,EAAE,eAAe,MAAM,CAAC;;CAGvC,oBAAoB;EAClB,KAAK,uBAAuB;;CAG9B,qBAAqB;EACnB,KAAK,uBAAuB;;CAG9B,SAAS;EACP,OAAO,KAAK,MAAM"}
1
+ {"version":3,"file":"app-router-scroll.js","names":["React"],"sources":["../../src/shims/app-router-scroll.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport {\n consumeAppRouterScrollIntent,\n getPendingAppRouterScrollIntent,\n} from \"./app-router-scroll-state.js\";\nimport { decodeHashFragment } from \"./hash-scroll.js\";\n\nconst AppRouterScrollCommitContext = React.createContext<number | null>(null);\nconst reactDomInternalsKey = \"__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE\";\nconst rectProperties = [\"bottom\", \"height\", \"left\", \"right\", \"top\", \"width\", \"x\", \"y\"] as const;\n\nfunction readFindDOMNode(): ((instance: React.ReactInstance | null | undefined) => unknown) | null {\n const internals = Reflect.get(ReactDOM, reactDomInternalsKey);\n if (typeof internals !== \"object\" || internals === null) {\n return null;\n }\n\n const findDOMNode = Reflect.get(internals, \"findDOMNode\");\n return typeof findDOMNode === \"function\" ? findDOMNode : null;\n}\n\nfunction findDOMNode(instance: React.ReactInstance | null | undefined): Element | Text | null {\n if (typeof window === \"undefined\") return null;\n\n const findDOMNodeImpl = readFindDOMNode();\n if (!findDOMNodeImpl) return null;\n\n const node = findDOMNodeImpl(instance);\n return node instanceof Element || node instanceof Text ? node : null;\n}\n\nfunction shouldSkipElement(element: HTMLElement): boolean {\n const position = getComputedStyle(element).position;\n if (position === \"fixed\" || position === \"sticky\") {\n return true;\n }\n\n const rect = element.getBoundingClientRect();\n return rectProperties.every((property) => rect[property] === 0);\n}\n\nfunction topOfElementInViewport(element: HTMLElement, viewportHeight: number): boolean {\n const rects = element.getClientRects();\n if (rects.length === 0) {\n return false;\n }\n\n let elementTop = Number.POSITIVE_INFINITY;\n for (const rect of rects) {\n if (rect.top < elementTop) {\n elementTop = rect.top;\n }\n }\n\n return elementTop >= 0 && elementTop <= viewportHeight;\n}\n\nfunction getHashFragmentDomNode(hash: string): HTMLElement | null {\n const fragment = decodeHashFragment(hash.startsWith(\"#\") ? hash.slice(1) : hash);\n if (fragment === \"top\") {\n return document.body;\n }\n\n const element = document.getElementById(fragment) ?? document.getElementsByName(fragment)[0];\n return element instanceof HTMLElement ? element : null;\n}\n\nfunction findNextScrollTarget(node: Element | Text | null): HTMLElement | null {\n if (!(node instanceof Element)) {\n return null;\n }\n\n let target: Element = node;\n while (!(target instanceof HTMLElement) || shouldSkipElement(target)) {\n if (target.nextElementSibling === null) {\n return null;\n }\n target = target.nextElementSibling;\n }\n\n return target;\n}\n\nfunction scrollToElement(target: HTMLElement, hash: string | null): void {\n if (hash !== null) {\n target.scrollIntoView({ behavior: \"auto\" });\n return;\n }\n\n const htmlElement = document.documentElement;\n const viewportHeight = htmlElement.clientHeight;\n\n if (topOfElementInViewport(target, viewportHeight)) {\n return;\n }\n\n htmlElement.scrollTop = 0;\n\n if (!topOfElementInViewport(target, viewportHeight)) {\n target.scrollIntoView({ behavior: \"auto\", block: \"start\", inline: \"nearest\" });\n }\n}\n\n// The inner component must stay a class: findDOMNode() needs a mounted\n// class instance to locate the first DOM node rendered by the children\n// without introducing a wrapper element. The outer AppRouterScrollTarget\n// function component reads context and delegates here; only the inner\n// class retains wrapperless targeting.\nexport class AppRouterScrollTargetInner extends React.Component<{\n children: React.ReactNode;\n commitId: number | null;\n}> {\n handlePotentialScroll = () => {\n const intent = getPendingAppRouterScrollIntent();\n if (intent === null) return;\n if (this.props.commitId === null || intent.commitId !== this.props.commitId) return;\n\n let target: HTMLElement | null;\n if (intent.hash !== null) {\n target = getHashFragmentDomNode(intent.hash);\n } else {\n // oxlint-disable-next-line react/no-find-dom-node -- Next's default App Router scroll handler targets wrapperless route content after commit.\n target = findNextScrollTarget(findDOMNode(this));\n }\n if (target === null) return;\n\n const consumed = consumeAppRouterScrollIntent(intent, this.props.commitId);\n if (consumed === null) return;\n\n scrollToElement(target, consumed.hash);\n // Next's default handler uses plain focus(), but that lets the browser run\n // a second implicit scroll after our explicit navigation scroll. Keep the\n // focus transfer while preserving the scroll position we just chose.\n target.focus({ preventScroll: true });\n };\n\n componentDidMount() {\n this.handlePotentialScroll();\n }\n\n componentDidUpdate() {\n this.handlePotentialScroll();\n }\n\n render() {\n return this.props.children;\n }\n}\n\nexport function AppRouterScrollCommitProvider({\n children,\n commitId,\n}: {\n children?: React.ReactNode;\n commitId: number | null;\n}) {\n return (\n <AppRouterScrollCommitContext.Provider value={commitId}>\n {children}\n </AppRouterScrollCommitContext.Provider>\n );\n}\n\nexport function AppRouterScrollTarget({ children }: { children: React.ReactNode }) {\n const commitId = React.useContext(AppRouterScrollCommitContext);\n return <AppRouterScrollTargetInner commitId={commitId}>{children}</AppRouterScrollTargetInner>;\n}\n"],"mappings":";;;;;;;AAUA,MAAM,+BAA+BA,QAAM,cAA6B,KAAK;AAC7E,MAAM,uBAAuB;AAC7B,MAAM,iBAAiB;CAAC;CAAU;CAAU;CAAQ;CAAS;CAAO;CAAS;CAAK;CAAI;AAEtF,SAAS,kBAA0F;CACjG,MAAM,YAAY,QAAQ,IAAI,UAAU,qBAAqB;CAC7D,IAAI,OAAO,cAAc,YAAY,cAAc,MACjD,OAAO;CAGT,MAAM,cAAc,QAAQ,IAAI,WAAW,cAAc;CACzD,OAAO,OAAO,gBAAgB,aAAa,cAAc;;AAG3D,SAAS,YAAY,UAAyE;CAC5F,IAAI,OAAO,WAAW,aAAa,OAAO;CAE1C,MAAM,kBAAkB,iBAAiB;CACzC,IAAI,CAAC,iBAAiB,OAAO;CAE7B,MAAM,OAAO,gBAAgB,SAAS;CACtC,OAAO,gBAAgB,WAAW,gBAAgB,OAAO,OAAO;;AAGlE,SAAS,kBAAkB,SAA+B;CACxD,MAAM,WAAW,iBAAiB,QAAQ,CAAC;CAC3C,IAAI,aAAa,WAAW,aAAa,UACvC,OAAO;CAGT,MAAM,OAAO,QAAQ,uBAAuB;CAC5C,OAAO,eAAe,OAAO,aAAa,KAAK,cAAc,EAAE;;AAGjE,SAAS,uBAAuB,SAAsB,gBAAiC;CACrF,MAAM,QAAQ,QAAQ,gBAAgB;CACtC,IAAI,MAAM,WAAW,GACnB,OAAO;CAGT,IAAI,aAAa,OAAO;CACxB,KAAK,MAAM,QAAQ,OACjB,IAAI,KAAK,MAAM,YACb,aAAa,KAAK;CAItB,OAAO,cAAc,KAAK,cAAc;;AAG1C,SAAS,uBAAuB,MAAkC;CAChE,MAAM,WAAW,mBAAmB,KAAK,WAAW,IAAI,GAAG,KAAK,MAAM,EAAE,GAAG,KAAK;CAChF,IAAI,aAAa,OACf,OAAO,SAAS;CAGlB,MAAM,UAAU,SAAS,eAAe,SAAS,IAAI,SAAS,kBAAkB,SAAS,CAAC;CAC1F,OAAO,mBAAmB,cAAc,UAAU;;AAGpD,SAAS,qBAAqB,MAAiD;CAC7E,IAAI,EAAE,gBAAgB,UACpB,OAAO;CAGT,IAAI,SAAkB;CACtB,OAAO,EAAE,kBAAkB,gBAAgB,kBAAkB,OAAO,EAAE;EACpE,IAAI,OAAO,uBAAuB,MAChC,OAAO;EAET,SAAS,OAAO;;CAGlB,OAAO;;AAGT,SAAS,gBAAgB,QAAqB,MAA2B;CACvE,IAAI,SAAS,MAAM;EACjB,OAAO,eAAe,EAAE,UAAU,QAAQ,CAAC;EAC3C;;CAGF,MAAM,cAAc,SAAS;CAC7B,MAAM,iBAAiB,YAAY;CAEnC,IAAI,uBAAuB,QAAQ,eAAe,EAChD;CAGF,YAAY,YAAY;CAExB,IAAI,CAAC,uBAAuB,QAAQ,eAAe,EACjD,OAAO,eAAe;EAAE,UAAU;EAAQ,OAAO;EAAS,QAAQ;EAAW,CAAC;;AASlF,IAAa,6BAAb,cAAgDA,QAAM,UAGnD;CACD,8BAA8B;EAC5B,MAAM,SAAS,iCAAiC;EAChD,IAAI,WAAW,MAAM;EACrB,IAAI,KAAK,MAAM,aAAa,QAAQ,OAAO,aAAa,KAAK,MAAM,UAAU;EAE7E,IAAI;EACJ,IAAI,OAAO,SAAS,MAClB,SAAS,uBAAuB,OAAO,KAAK;OAG5C,SAAS,qBAAqB,YAAY,KAAK,CAAC;EAElD,IAAI,WAAW,MAAM;EAErB,MAAM,WAAW,6BAA6B,QAAQ,KAAK,MAAM,SAAS;EAC1E,IAAI,aAAa,MAAM;EAEvB,gBAAgB,QAAQ,SAAS,KAAK;EAItC,OAAO,MAAM,EAAE,eAAe,MAAM,CAAC;;CAGvC,oBAAoB;EAClB,KAAK,uBAAuB;;CAG9B,qBAAqB;EACnB,KAAK,uBAAuB;;CAG9B,SAAS;EACP,OAAO,KAAK,MAAM;;;AAItB,SAAgB,8BAA8B,EAC5C,UACA,YAIC;CACD,OACE,oBAAC,6BAA6B,UAA9B;EAAuC,OAAO;EAC3C;EACqC,CAAA;;AAI5C,SAAgB,sBAAsB,EAAE,YAA2C;CAEjF,OAAO,oBAAC,4BAAD;EAA4B,UADlBA,QAAM,WAAW,6BACmB;EAAG;EAAsC,CAAA"}
@@ -163,6 +163,12 @@ declare function refresh(): void;
163
163
  * Server Actions-only API that expires a tag so the next request
164
164
  * fetches fresh data. Unlike `revalidateTag`, which uses stale-while-revalidate,
165
165
  * `updateTag` invalidates synchronously within the same request context.
166
+ *
167
+ * Throws if called outside a Server Action — e.g. from a Route Handler or
168
+ * during render — matching Next.js's enforcement. For Route Handlers, callers
169
+ * should use `revalidateTag` instead.
170
+ *
171
+ * @see https://nextjs.org/docs/app/api-reference/functions/updateTag
166
172
  */
167
173
  declare function updateTag(tag: string): Promise<void>;
168
174
  /**
@@ -195,8 +195,15 @@ function refresh() {
195
195
  * Server Actions-only API that expires a tag so the next request
196
196
  * fetches fresh data. Unlike `revalidateTag`, which uses stale-while-revalidate,
197
197
  * `updateTag` invalidates synchronously within the same request context.
198
+ *
199
+ * Throws if called outside a Server Action — e.g. from a Route Handler or
200
+ * during render — matching Next.js's enforcement. For Route Handlers, callers
201
+ * should use `revalidateTag` instead.
202
+ *
203
+ * @see https://nextjs.org/docs/app/api-reference/functions/updateTag
198
204
  */
199
205
  async function updateTag(tag) {
206
+ if (getHeadersAccessPhase() !== "action") throw new Error("updateTag can only be called from within a Server Action. To invalidate cache tags in Route Handlers or other contexts, use revalidateTag instead. See more info here: https://nextjs.org/docs/app/api-reference/functions/updateTag");
200
207
  markActionRevalidation(ACTION_DID_REVALIDATE_STATIC_AND_DYNAMIC);
201
208
  await _getActiveHandler().revalidateTag(encodeCacheTag(tag));
202
209
  }