@wular/pnext 0.0.1

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 (371) hide show
  1. package/README.md +153 -0
  2. package/bin/pnext +67 -0
  3. package/config/lint/base.js +48 -0
  4. package/config/ts/base.json +26 -0
  5. package/config/ts/react.json +13 -0
  6. package/package.json +70 -0
  7. package/reference/compat.md +63 -0
  8. package/reference/config.md +120 -0
  9. package/reference/css.md +58 -0
  10. package/reference/dev.md +69 -0
  11. package/reference/env.md +40 -0
  12. package/reference/metadata.md +86 -0
  13. package/reference/navigation.md +149 -0
  14. package/reference/overview.md +35 -0
  15. package/reference/performance.md +97 -0
  16. package/reference/rendering.md +127 -0
  17. package/reference/routing.md +167 -0
  18. package/reference/typegen.md +64 -0
  19. package/src/api/cache.ts +80 -0
  20. package/src/api/client-cache.ts +9 -0
  21. package/src/api/client-navigation.ts +279 -0
  22. package/src/api/dynamic.tsx +102 -0
  23. package/src/api/link.tsx +119 -0
  24. package/src/api/navigation.ts +198 -0
  25. package/src/api/router/events.ts +53 -0
  26. package/src/api/router/history.ts +70 -0
  27. package/src/api/router/hub.ts +194 -0
  28. package/src/api/router/policies.ts +107 -0
  29. package/src/api/router/runtime.ts +5238 -0
  30. package/src/api/router/types.ts +299 -0
  31. package/src/api/router.ts +167 -0
  32. package/src/api/server.ts +323 -0
  33. package/src/api/suspense.ts +16 -0
  34. package/src/cache/context.ts +61 -0
  35. package/src/cli/adapters/vercel-warm.ts +375 -0
  36. package/src/cli/adapters/vercel.ts +1310 -0
  37. package/src/cli/analyze-print.ts +181 -0
  38. package/src/cli/analyze.ts +328 -0
  39. package/src/cli/boot-trace.ts +29 -0
  40. package/src/cli/build.ts +3114 -0
  41. package/src/cli/dev.ts +276 -0
  42. package/src/cli/index.ts +196 -0
  43. package/src/cli/named-bin.ts +119 -0
  44. package/src/cli/serve-ui.ts +160 -0
  45. package/src/cli/start.ts +1425 -0
  46. package/src/client/build.ts +2136 -0
  47. package/src/client/chunk-fold.ts +526 -0
  48. package/src/client/entry.ts +1525 -0
  49. package/src/client/paths.ts +22 -0
  50. package/src/client/prebuilt.ts +621 -0
  51. package/src/client/profile.ts +75 -0
  52. package/src/client/react-compiler.ts +94 -0
  53. package/src/client/reference-stub.ts +145 -0
  54. package/src/client/reference.ts +47 -0
  55. package/src/compat/actions/action-client.ts +531 -0
  56. package/src/compat/actions/action-dispatch.ts +676 -0
  57. package/src/compat/actions/action-router.ts +40 -0
  58. package/src/compat/actions/action-shared.ts +95 -0
  59. package/src/compat/actions/client-plugin.ts +135 -0
  60. package/src/compat/actions/client-stub.ts +65 -0
  61. package/src/compat/actions/config.ts +164 -0
  62. package/src/compat/actions/detect.ts +208 -0
  63. package/src/compat/actions/discovery.ts +244 -0
  64. package/src/compat/actions/early-submit.ts +40 -0
  65. package/src/compat/actions/endpoint.ts +602 -0
  66. package/src/compat/actions/flight.ts +52 -0
  67. package/src/compat/actions/form-state.ts +73 -0
  68. package/src/compat/actions/hoist.ts +485 -0
  69. package/src/compat/actions/ids.ts +39 -0
  70. package/src/compat/actions/index.ts +41 -0
  71. package/src/compat/actions/instances.ts +144 -0
  72. package/src/compat/actions/origin.ts +109 -0
  73. package/src/compat/actions/protocol.ts +125 -0
  74. package/src/compat/actions/registry.ts +74 -0
  75. package/src/compat/actions/rewrite.ts +282 -0
  76. package/src/compat/actions/serve.ts +414 -0
  77. package/src/compat/actions/server-tag.ts +21 -0
  78. package/src/compat/actions/unrecognized-error.ts +30 -0
  79. package/src/compat/adapter/build-complete.ts +257 -0
  80. package/src/compat/bundler/bun-externals.ts +53 -0
  81. package/src/compat/bundler/cjs-exports.ts +542 -0
  82. package/src/compat/bundler/config.ts +363 -0
  83. package/src/compat/bundler/externals.ts +34 -0
  84. package/src/compat/bundler/import-meta-url.ts +60 -0
  85. package/src/compat/bundler/modularize-imports.ts +119 -0
  86. package/src/compat/bundler/new-url-asset.ts +87 -0
  87. package/src/compat/bundler/optimize-package-imports.ts +273 -0
  88. package/src/compat/bundler/polyfill.ts +88 -0
  89. package/src/compat/bundler/react-compiler.ts +61 -0
  90. package/src/compat/bundler/react-profiler.tsx +25 -0
  91. package/src/compat/bundler/relay-transform.ts +116 -0
  92. package/src/compat/bundler/require-context.ts +281 -0
  93. package/src/compat/bundler/resolve-extensions.ts +75 -0
  94. package/src/compat/bundler/source-cache.ts +61 -0
  95. package/src/compat/bundler/static-imports.ts +25 -0
  96. package/src/compat/bundler/symlink-imports.ts +119 -0
  97. package/src/compat/bundler/tsconfig-paths.ts +50 -0
  98. package/src/compat/bundler/wasm.ts +153 -0
  99. package/src/compat/bundler/webpack-loaders.ts +685 -0
  100. package/src/compat/bundler/worker.ts +278 -0
  101. package/src/compat/cache/build-flags.ts +81 -0
  102. package/src/compat/cache/build-prerender-errors.ts +163 -0
  103. package/src/compat/cache/custom-handler.ts +159 -0
  104. package/src/compat/cache/fetch-patch.ts +745 -0
  105. package/src/compat/cache/handler.ts +100 -0
  106. package/src/compat/cache/modern-handler.ts +275 -0
  107. package/src/compat/cache/resume-data-cache.ts +143 -0
  108. package/src/compat/cache/revalidate.ts +759 -0
  109. package/src/compat/cache/runtime-error.ts +124 -0
  110. package/src/compat/cache/use-cache-transform.ts +961 -0
  111. package/src/compat/cache/use-cache.ts +1695 -0
  112. package/src/compat/cache-control.ts +269 -0
  113. package/src/compat/client/base-path.ts +64 -0
  114. package/src/compat/client/css-order.ts +36 -0
  115. package/src/compat/client/errors/control-flow.ts +92 -0
  116. package/src/compat/client/errors/error-boundary.ts +222 -0
  117. package/src/compat/client/errors/global-error.ts +238 -0
  118. package/src/compat/client/errors/install.ts +217 -0
  119. package/src/compat/client/errors/lazy.ts +53 -0
  120. package/src/compat/client/errors/primitive-throw.ts +126 -0
  121. package/src/compat/client/errors/soft-refresh.ts +14 -0
  122. package/src/compat/client/link-status.ts +86 -0
  123. package/src/compat/client/nav-compat-runtime.ts +57 -0
  124. package/src/compat/client/nav-compat.ts +42 -0
  125. package/src/compat/client/navigation-scroll.ts +154 -0
  126. package/src/compat/client/optimistic-routing.ts +206 -0
  127. package/src/compat/client/prefetch-cache.ts +111 -0
  128. package/src/compat/client/route-announcer.ts +72 -0
  129. package/src/compat/client/segment-cache-policy.ts +159 -0
  130. package/src/compat/client/segment-cache.ts +1077 -0
  131. package/src/compat/client/segment-prefetch.ts +375 -0
  132. package/src/compat/client/trailing-slash.ts +24 -0
  133. package/src/compat/css/chunking.ts +254 -0
  134. package/src/compat/css/inline-css.ts +73 -0
  135. package/src/compat/css/lightningcss.ts +90 -0
  136. package/src/compat/css/modules.ts +373 -0
  137. package/src/compat/css/nonce.ts +30 -0
  138. package/src/compat/css/sass-plugin.ts +65 -0
  139. package/src/compat/css/sass.ts +392 -0
  140. package/src/compat/css/styled-jsx-runtime.ts +80 -0
  141. package/src/compat/css/styled-jsx.ts +49 -0
  142. package/src/compat/edge-runtime.ts +71 -0
  143. package/src/compat/export/client.ts +112 -0
  144. package/src/compat/export/index.ts +272 -0
  145. package/src/compat/export/standalone.ts +207 -0
  146. package/src/compat/image-optimizer/cache.ts +119 -0
  147. package/src/compat/image-optimizer/detect.ts +143 -0
  148. package/src/compat/image-optimizer/index.ts +601 -0
  149. package/src/compat/image-optimizer/source.ts +243 -0
  150. package/src/compat/index.ts +458 -0
  151. package/src/compat/lifecycle/after-scope.ts +86 -0
  152. package/src/compat/lifecycle/after.ts +173 -0
  153. package/src/compat/lifecycle/error-funnel.ts +306 -0
  154. package/src/compat/lifecycle/error-serialize.ts +87 -0
  155. package/src/compat/lifecycle/error-ui.ts +167 -0
  156. package/src/compat/lifecycle/instrumentation-client.ts +138 -0
  157. package/src/compat/lifecycle/instrumentation.ts +277 -0
  158. package/src/compat/lifecycle/node-console.ts +19 -0
  159. package/src/compat/lifecycle/testmode.ts +263 -0
  160. package/src/compat/mdx/compile.ts +219 -0
  161. package/src/compat/mdx/next-mdx-stub.ts +46 -0
  162. package/src/compat/mdx/plugin.ts +37 -0
  163. package/src/compat/metadata-route-artifacts.ts +458 -0
  164. package/src/compat/metadata.ts +295 -0
  165. package/src/compat/middleware/manifest.ts +210 -0
  166. package/src/compat/misc/action-return.ts +173 -0
  167. package/src/compat/next/cache.ts +211 -0
  168. package/src/compat/next/canonical-url.ts +35 -0
  169. package/src/compat/next/client-cache.ts +57 -0
  170. package/src/compat/next/client-navigation.ts +313 -0
  171. package/src/compat/next/client-only.ts +3 -0
  172. package/src/compat/next/client-script.tsx +215 -0
  173. package/src/compat/next/client-server.ts +39 -0
  174. package/src/compat/next/config-loader.ts +569 -0
  175. package/src/compat/next/config.ts +29 -0
  176. package/src/compat/next/constants.cjs +6 -0
  177. package/src/compat/next/constants.ts +6 -0
  178. package/src/compat/next/custom-server.ts +236 -0
  179. package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
  180. package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
  181. package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
  182. package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
  183. package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
  184. package/src/compat/next/dynamic.tsx +46 -0
  185. package/src/compat/next/error.tsx +148 -0
  186. package/src/compat/next/font/cache.ts +171 -0
  187. package/src/compat/next/font/google.ts +2 -0
  188. package/src/compat/next/font/index.ts +8 -0
  189. package/src/compat/next/font/local.ts +5 -0
  190. package/src/compat/next/font/runtime-client.ts +71 -0
  191. package/src/compat/next/font/runtime.ts +974 -0
  192. package/src/compat/next/font/shared.ts +281 -0
  193. package/src/compat/next/form.tsx +156 -0
  194. package/src/compat/next/head.tsx +10 -0
  195. package/src/compat/next/headers.ts +247 -0
  196. package/src/compat/next/image/config.ts +196 -0
  197. package/src/compat/next/image/optimizer.ts +96 -0
  198. package/src/compat/next/image/patterns.ts +103 -0
  199. package/src/compat/next/image/shared.ts +141 -0
  200. package/src/compat/next/image/static-metadata.ts +283 -0
  201. package/src/compat/next/image/validate.ts +269 -0
  202. package/src/compat/next/image-client.tsx +215 -0
  203. package/src/compat/next/image-props.ts +575 -0
  204. package/src/compat/next/image-usage.ts +102 -0
  205. package/src/compat/next/image.tsx +56 -0
  206. package/src/compat/next/index.ts +1 -0
  207. package/src/compat/next/legacy-image.tsx +97 -0
  208. package/src/compat/next/link-usage.ts +29 -0
  209. package/src/compat/next/link-validation-transform.ts +200 -0
  210. package/src/compat/next/link.tsx +466 -0
  211. package/src/compat/next/navigation.cjs +21 -0
  212. package/src/compat/next/navigation.ts +188 -0
  213. package/src/compat/next/offline.ts +51 -0
  214. package/src/compat/next/og.ts +324 -0
  215. package/src/compat/next/optimistic-route-state.ts +188 -0
  216. package/src/compat/next/preferred-region.ts +39 -0
  217. package/src/compat/next/redirects.ts +131 -0
  218. package/src/compat/next/resource-hints.ts +136 -0
  219. package/src/compat/next/rewrites.ts +350 -0
  220. package/src/compat/next/root-params.ts +142 -0
  221. package/src/compat/next/router.cjs +49 -0
  222. package/src/compat/next/router.ts +143 -0
  223. package/src/compat/next/script.tsx +355 -0
  224. package/src/compat/next/server-only.ts +3 -0
  225. package/src/compat/next/server.ts +28 -0
  226. package/src/compat/next/svgr.ts +58 -0
  227. package/src/compat/next/telemetry.ts +77 -0
  228. package/src/compat/next/user-agent.ts +100 -0
  229. package/src/compat/next/web-vitals.ts +56 -0
  230. package/src/compat/otel/api.ts +95 -0
  231. package/src/compat/otel/client-trace-metadata.ts +71 -0
  232. package/src/compat/otel/fetch-span.ts +77 -0
  233. package/src/compat/otel/tracer.ts +944 -0
  234. package/src/compat/pages/client-plugin.ts +108 -0
  235. package/src/compat/pages/index.ts +527 -0
  236. package/src/compat/pages/router-state.ts +94 -0
  237. package/src/compat/ppr/io.ts +38 -0
  238. package/src/compat/ppr/missing-root-params.ts +105 -0
  239. package/src/compat/ppr/root-params-scan.ts +164 -0
  240. package/src/compat/ppr/root-params-transform.ts +75 -0
  241. package/src/compat/ppr/root-params.ts +129 -0
  242. package/src/compat/ppr/segment-config-incompat.ts +34 -0
  243. package/src/compat/protocol.ts +202 -0
  244. package/src/compat/react/client.ts +59 -0
  245. package/src/compat/react/compiler-runtime.ts +60 -0
  246. package/src/compat/react/dom-client.ts +115 -0
  247. package/src/compat/react/dom-react-server.ts +20 -0
  248. package/src/compat/react/dom-server.ts +40 -0
  249. package/src/compat/react/dom.ts +154 -0
  250. package/src/compat/react/preact.ts +522 -0
  251. package/src/compat/react/react-server.ts +84 -0
  252. package/src/compat/react/router-shim.ts +26 -0
  253. package/src/compat/react/server-component-use.ts +48 -0
  254. package/src/compat/react/server-inserted-html.ts +87 -0
  255. package/src/compat/react/server.ts +156 -0
  256. package/src/compat/react/view-transition.ts +60 -0
  257. package/src/compat/register/actions.ts +875 -0
  258. package/src/compat/register/boot.ts +141 -0
  259. package/src/compat/register/build-tier.ts +11 -0
  260. package/src/compat/register/build.ts +182 -0
  261. package/src/compat/register/bundler.ts +587 -0
  262. package/src/compat/register/cache.ts +85 -0
  263. package/src/compat/register/client-errors.ts +18 -0
  264. package/src/compat/register/config.ts +17 -0
  265. package/src/compat/register/css-extras.ts +120 -0
  266. package/src/compat/register/edge-runtime.ts +6 -0
  267. package/src/compat/register/errors.ts +46 -0
  268. package/src/compat/register/export.ts +23 -0
  269. package/src/compat/register/font.ts +36 -0
  270. package/src/compat/register/hooks.ts +34 -0
  271. package/src/compat/register/image.ts +133 -0
  272. package/src/compat/register/index.ts +111 -0
  273. package/src/compat/register/instrumentation-client.ts +35 -0
  274. package/src/compat/register/lifecycle.ts +86 -0
  275. package/src/compat/register/mdx.ts +48 -0
  276. package/src/compat/register/middleware.ts +36 -0
  277. package/src/compat/register/misc.ts +44 -0
  278. package/src/compat/register/otel.ts +288 -0
  279. package/src/compat/register/pages-api.ts +473 -0
  280. package/src/compat/register/ppr.ts +56 -0
  281. package/src/compat/register/protocol.ts +57 -0
  282. package/src/compat/register/proxy.ts +127 -0
  283. package/src/compat/register/render.ts +268 -0
  284. package/src/compat/register/routing.ts +410 -0
  285. package/src/compat/register/segment.ts +1903 -0
  286. package/src/compat/register/static-image.ts +21 -0
  287. package/src/compat/register/typed-routes.ts +35 -0
  288. package/src/compat/register/usecache.ts +131 -0
  289. package/src/compat/register/validation.ts +56 -0
  290. package/src/compat/segment/loading-boundary.ts +113 -0
  291. package/src/compat/segment/page-slot.ts +200 -0
  292. package/src/compat/segment/tree.ts +481 -0
  293. package/src/compat/segment/vary-key.ts +102 -0
  294. package/src/compat/segment/vary-params.ts +551 -0
  295. package/src/compat/static-params.ts +33 -0
  296. package/src/compat/tsconfig-defaults.ts +301 -0
  297. package/src/compat/typecheck/index.ts +1481 -0
  298. package/src/compat/typecheck/worker.ts +26 -0
  299. package/src/compat/typed-routes/index.ts +92 -0
  300. package/src/compat/typed-routes/manifest.ts +356 -0
  301. package/src/compat/typed-routes/typegen.ts +566 -0
  302. package/src/compat/validation/errors.ts +159 -0
  303. package/src/compat/validation/index.ts +1770 -0
  304. package/src/compat/validation/prerender-diagnostics.ts +1508 -0
  305. package/src/compat-bootstrap.ts +67 -0
  306. package/src/config.ts +218 -0
  307. package/src/css/build.ts +697 -0
  308. package/src/css/index.ts +2 -0
  309. package/src/css/postcss.ts +236 -0
  310. package/src/css/worker.ts +34 -0
  311. package/src/dev/client-actions.ts +35 -0
  312. package/src/dev/client-chunk-store.ts +92 -0
  313. package/src/dev/client-key-cache.ts +178 -0
  314. package/src/dev/global-css-cache.ts +212 -0
  315. package/src/dev/imports.ts +2430 -0
  316. package/src/dev/module-cache.ts +721 -0
  317. package/src/dev/module-generations.ts +38 -0
  318. package/src/dev/module-transform.ts +188 -0
  319. package/src/dev/node-module-bundle-cache.ts +63 -0
  320. package/src/dev/restart-cache.ts +10 -0
  321. package/src/dev/route-bundle-key-cache.ts +154 -0
  322. package/src/dev/route-facts-cache.ts +223 -0
  323. package/src/dev/server.ts +1710 -0
  324. package/src/dynamic/source.ts +307 -0
  325. package/src/dynamic/tree-shake.ts +262 -0
  326. package/src/env.ts +92 -0
  327. package/src/extensions.ts +1898 -0
  328. package/src/index.ts +34 -0
  329. package/src/internal.ts +43 -0
  330. package/src/islands/boundary-error.ts +8 -0
  331. package/src/islands/static-children.ts +37 -0
  332. package/src/islands/static-slots.ts +106 -0
  333. package/src/ppr-postpone.ts +24 -0
  334. package/src/ppr.ts +784 -0
  335. package/src/proxy.ts +752 -0
  336. package/src/render/hooks.ts +384 -0
  337. package/src/render/index.ts +1 -0
  338. package/src/render/island-context.ts +47 -0
  339. package/src/render/metadata.ts +857 -0
  340. package/src/render/renderer.ts +7391 -0
  341. package/src/render/resource-hints.ts +44 -0
  342. package/src/render/slots.tsx +679 -0
  343. package/src/request/context.ts +396 -0
  344. package/src/resolve/engine.ts +219 -0
  345. package/src/resolve/imports.ts +1104 -0
  346. package/src/resolve/scan-facts.ts +474 -0
  347. package/src/resolve/source-text.ts +86 -0
  348. package/src/routing/forwarded.ts +41 -0
  349. package/src/routing/handler.ts +271 -0
  350. package/src/routing/href.ts +203 -0
  351. package/src/routing/metadata.ts +1018 -0
  352. package/src/routing/request-runtime.ts +43 -0
  353. package/src/routing/routes.ts +2560 -0
  354. package/src/routing/slots.ts +432 -0
  355. package/src/runtime/server.ts +3453 -0
  356. package/src/runtime/vendor.ts +1160 -0
  357. package/src/style-modules.d.ts +9 -0
  358. package/src/typegen.ts +151 -0
  359. package/src/types.ts +725 -0
  360. package/src/utils/ansi.ts +9 -0
  361. package/src/utils/content-type.ts +31 -0
  362. package/src/utils/decode.ts +7 -0
  363. package/src/utils/dev-profile.ts +31 -0
  364. package/src/utils/error-log.ts +29 -0
  365. package/src/utils/fs-cache.ts +31 -0
  366. package/src/utils/fs.ts +119 -0
  367. package/src/utils/html.ts +46 -0
  368. package/src/utils/serialize.ts +378 -0
  369. package/src/utils/source.ts +35 -0
  370. package/src/utils/verbose.ts +39 -0
  371. package/tsconfig.json +10 -0
@@ -0,0 +1,1077 @@
1
+ // Per-segment client cache (COMPAT client policy).
2
+ //
3
+ // The router's own caches are whole-URL, which cannot express Next's central segment-cache property:
4
+ // a segment that provably never READ a param is shareable across every value of that param, so
5
+ // `/vary/a` and `/vary/b` hit ONE entry.
6
+ //
7
+ // This module adds that layer. The server publishes, per segment response, `vary` (the param names
8
+ // the render actually read; `'?'` = it read searchParams, so the whole query keys it), `route` (the
9
+ // matched route pattern in colon form) and `params` (the concrete params of the URL that produced
10
+ // the response). Entries here are keyed by (segmentPath, route, the VARY SUBSET of params,
11
+ // query-if-varied), so an empty vary set collapses every param value onto one entry.
12
+ //
13
+ // Reuse policy: a navigation NEVER short-circuits on a cache hit unless the entry is COMPLETE (a
14
+ // whole document, not a truncated shell) AND STATIC (its bytes are a pure function of the URL) AND
15
+ // still FRESH. Everything else paints what it has and still issues the dynamic-stage request.
16
+ // Runtime prefetches are the one request-sampled exception: a COMPLETE one commits network-free, an
17
+ // incomplete one is shell-only.
18
+ //
19
+ // Cross-URL sharing is deliberately conservative: only a STATIC entry may serve a URL other than the
20
+ // one it was fetched for, the target must match the entry's learned route pattern, and a pathname
21
+ // already known to resolve to a DIFFERENT route is never predicted.
22
+
23
+ import {
24
+ normalizeSegmentSearch,
25
+ segmentVaryCacheKey,
26
+ SEARCH_PARAMS_SENTINEL,
27
+ VARY_KEY_SEPARATOR,
28
+ } from '../segment/vary-key';
29
+ import {
30
+ composeSegmentFrames,
31
+ pageSlotRange,
32
+ stripPageSlotContent,
33
+ stripStreamedContinuation,
34
+ } from '../segment/page-slot';
35
+
36
+ /** LRU bound, matching the router's own segment caches. */
37
+ const SEGMENT_ENTRY_LIMIT = 64;
38
+
39
+ /**
40
+ * w9-segment-split: the two derived frames of a body response. The LAYOUT frame
41
+ * is the response document with the page's markup cut out; the PAGE frame is the
42
+ * document as received (only its page-slot content is ever read back out). They
43
+ * are filed under their OWN vary sets, so a navigation whose layout varied and
44
+ * whose page did not re-fetches `/_layout` alone and composes it with the
45
+ * cached page frame.
46
+ */
47
+ export const LAYOUT_FRAME_PATH = '/_layout';
48
+ export const PAGE_FRAME_PATH = '/_page';
49
+
50
+ /** The body-segment request key the router sends (`/` is the legacy alias). */
51
+ export const BODY_SEGMENT_PATHS = ['/_index', '/', PAGE_FRAME_PATH] as const;
52
+
53
+ export interface SegmentPayloadMeta {
54
+ /** Segment vary names (may include the `?` sentinel), or null when unknown. */
55
+ vary: string[] | null;
56
+ /**
57
+ * w9-segment-split: the LAYOUT frame's own vary set, when the server split
58
+ * the response. Null/absent means the response was not split and only the
59
+ * whole-document entry is filed.
60
+ */
61
+ layoutVary?: string[] | null;
62
+ /** w9-segment-split: the PAGE frame's own vary set, when the server split. */
63
+ pageVary?: string[] | null;
64
+ /** Matched route pattern in colon form, or null. */
65
+ route: string | null;
66
+ /** Concrete params of the URL that produced the payload. */
67
+ params: Record<string, string | string[]>;
68
+ /**
69
+ * The route's shared APP SHELL, when the per-URL prerender carried one. Filed
70
+ * as its own param-independent entry so a navigation to another param of the
71
+ * route paints instantly.
72
+ */
73
+ shell?: { html: string; route: string };
74
+ /**
75
+ * Literal siblings of each DYNAMIC level of the route, keyed by segment index
76
+ * (the server's `staticChildrenBySegment`). A pathname naming one of them is
77
+ * served by its own static route, so this payload must never key onto it.
78
+ */
79
+ staticSiblings?: Record<string, string[]>;
80
+ }
81
+
82
+ export interface SegmentRecordInput extends SegmentPayloadMeta {
83
+ /** Segment request path (`/_index`, `/_head`, …). */
84
+ segmentPath: string;
85
+ /** Requested pathname (percent-encoding preserved). */
86
+ pathname: string;
87
+ /** Requested query string (leading `?` optional). */
88
+ search: string;
89
+ /** The payload the router would commit for this segment. */
90
+ html: string;
91
+ /** Reuse window in milliseconds. */
92
+ staleTimeMs: number;
93
+ /** True when the payload is a whole document, not a truncated shell. */
94
+ complete: boolean;
95
+ /** True when the bytes are a pure function of the URL (a prerender). */
96
+ static: boolean;
97
+ /** True when the payload came from a runtime (request-sampled) prefetch. */
98
+ runtime: boolean;
99
+ /** True when the route's catch-all is optional (`[[...slug]]`). */
100
+ catchAllOptional?: boolean;
101
+ }
102
+
103
+ interface SegmentEntry {
104
+ key: string;
105
+ html: string;
106
+ /** The URL this payload was fetched for, canonicalized (`_rsc` stripped). */
107
+ url: string;
108
+ time: number;
109
+ staleTimeMs: number;
110
+ complete: boolean;
111
+ static: boolean;
112
+ runtime: boolean;
113
+ }
114
+
115
+ export interface SegmentHit {
116
+ html: string;
117
+ /** True when this hit may commit a navigation with no network request. */
118
+ networkFree: boolean;
119
+ /**
120
+ * True when a PREFETCH of this URL has nothing left to fetch: the entry is fresh and its own vary set
121
+ * justifies serving this URL. Weaker than `networkFree` - a navigation may still need the dynamic
122
+ * remainder.
123
+ */
124
+ prefetchSatisfied: boolean;
125
+ runtime: boolean;
126
+ }
127
+
128
+ interface LearnedRoute {
129
+ route: string;
130
+ vary: string[];
131
+ catchAllOptional: boolean;
132
+ /** Literal siblings of the route's dynamic levels, by segment index. */
133
+ staticSiblings?: Record<string, string[]>;
134
+ }
135
+
136
+ const entries = new Map<string, SegmentEntry>();
137
+ /**
138
+ * Learned route vary sets, per segment path, keyed by route + vary set.
139
+ *
140
+ * One route can publish SEVERAL vary sets for the same segment: the per-URL
141
+ * prefetch of `/static-posts/1` reports `['id']` (it rendered the param), while
142
+ * the App-Shell prefetch of the same route reports `[]` (params hung). Both are
143
+ * live candidates for a lookup, so they are kept side by side rather than
144
+ * overwriting each other; `candidateKeys` tries the most specific first.
145
+ * Insertion order = learn order.
146
+ */
147
+ const learnedRoutes = new Map<string, Map<string, LearnedRoute>>();
148
+ /** pathname -> the route pattern the SERVER resolved it to (sibling guard). */
149
+ const resolvedRoutes = new Map<string, string>();
150
+
151
+ /** Wall clock, overridable in tests (the e2e suites mock `Date`). */
152
+ function now(): number {
153
+ return Date.now();
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Recording.
158
+ // ---------------------------------------------------------------------------
159
+
160
+ export function recordSegment(input: SegmentRecordInput): void {
161
+ recordOneSegment(input);
162
+ // w9-segment-split: a body response the server split also files its two
163
+ // frames, each under its OWN vary set. Additive — the whole-document entry
164
+ // above is untouched, so nothing that hits today starts missing.
165
+ if (!BODY_SEGMENT_PATHS.includes(input.segmentPath as (typeof BODY_SEGMENT_PATHS)[number])) {
166
+ return;
167
+ }
168
+ if (input.segmentPath === PAGE_FRAME_PATH) return;
169
+ // The response's layout frame, filed under this URL's EXACT key. Independent of `layoutVary` - the
170
+ // server publishes that set only when it can prove what the layout read, but a frame keyed on the
171
+ // URL it was fetched for needs no such proof. BOTH layout-frame records are skipped when the payload
172
+ // carries no page slot: `stripPageSlotContent` would hand back the whole document, and a "layout
173
+ // frame" with no slot to splice into makes `needsPageFrameOnly` promise a composition that always
174
+ // fails - two serial round trips for one navigation.
175
+ const framable = pageSlotRange(input.html) !== null;
176
+ if (framable) {
177
+ recordOneSegment({
178
+ ...input,
179
+ segmentPath: LAYOUT_FRAME_PATH,
180
+ html: stripPageSlotContent(input.html),
181
+ // Exact-URL only: no route, no vary set, so this frame is never shared
182
+ // across params the way a `layoutVary` frame is.
183
+ route: null,
184
+ params: {},
185
+ vary: null,
186
+ complete: false,
187
+ });
188
+ }
189
+ if (framable && input.layoutVary) {
190
+ recordOneSegment({
191
+ ...input,
192
+ segmentPath: LAYOUT_FRAME_PATH,
193
+ html: stripPageSlotContent(input.html),
194
+ vary: input.layoutVary,
195
+ // A layout frame is never the whole page: it may paint, but a navigation
196
+ // still needs the page below it.
197
+ complete: false,
198
+ });
199
+ }
200
+ if (input.pageVary) {
201
+ recordOneSegment({ ...input, segmentPath: PAGE_FRAME_PATH, vary: input.pageVary });
202
+ }
203
+ }
204
+
205
+ /**
206
+ * A payload is REWRITE-PRODUCED when the route the server resolved does not describe the URL it was
207
+ * served at - either structurally, or in its concrete param values.
208
+ *
209
+ * Such a payload must never teach the route trie: a rewrite maps ONE url somewhere, and a sibling URL
210
+ * of the same shape may rewrite somewhere else entirely, or not at all. Learning from it would hand
211
+ * every URL of the pattern a "free" shared shell it is not entitled to, so the router would paint a
212
+ * loading boundary for a URL it has never seen.
213
+ *
214
+ * Params the payload leaves ABSENT are not a divergence: an App Shell renders with its params hanging,
215
+ * and that shell IS shared across params by design.
216
+ */
217
+ function isRewriteResponse(input: SegmentRecordInput): boolean {
218
+ if (!input.route) return false;
219
+ // The catch-all tail is matched in its OPTIONAL form regardless of what the payload declared: a
220
+ // `[[...slug]]` index response does not always carry `catchAllOptional` on the wire, and reading
221
+ // its absence as "the URL is a segment short" would mis-file every optional-catchall index as a
222
+ // rewrite. The guard's bias is to accuse only what it can prove.
223
+ const derived = matchRoutePattern(input.route, input.pathname, true);
224
+ if (!derived) return true;
225
+ return Object.entries(input.params).some(([name, value]) => {
226
+ const own = derived[name];
227
+ if (own === undefined) return false;
228
+ return Array.isArray(value) || Array.isArray(own)
229
+ ? !Array.isArray(value) ||
230
+ !Array.isArray(own) ||
231
+ value.length !== own.length ||
232
+ value.some((part, index) => part !== own[index])
233
+ : value !== own;
234
+ });
235
+ }
236
+
237
+ function recordOneSegment(rawInput: SegmentRecordInput): void {
238
+ // A rewrite payload is filed EXACT-URL only (no route, no vary set): it stays
239
+ // available for the URL it was fetched for and is invisible to every other.
240
+ const input: SegmentRecordInput = isRewriteResponse(rawInput)
241
+ ? { ...rawInput, route: null, vary: null }
242
+ : rawInput;
243
+ if (rawInput.route) {
244
+ // LRU-bound like `entries`: one record per distinct pathname otherwise
245
+ // grows without limit (memory-pressure leak-slope test).
246
+ resolvedRoutes.delete(input.pathname);
247
+ // The resolved-route record stays TRUE even for a rewrite: the server
248
+ // really did answer this pathname with that route, and the sibling guard
249
+ // in `candidateKeys` reads it to keep other routes off this URL.
250
+ resolvedRoutes.set(rawInput.pathname, rawInput.route);
251
+ while (resolvedRoutes.size > SEGMENT_ENTRY_LIMIT) {
252
+ const oldest = resolvedRoutes.keys().next().value;
253
+ if (oldest === undefined) break;
254
+ resolvedRoutes.delete(oldest);
255
+ }
256
+ }
257
+ const key = entryKey(input.segmentPath, input.route, input.params, input.search, input.vary, {
258
+ pathname: input.pathname,
259
+ });
260
+ if (input.route && input.vary !== null) {
261
+ let byRoute = learnedRoutes.get(input.segmentPath);
262
+ if (!byRoute) {
263
+ byRoute = new Map<string, LearnedRoute>();
264
+ learnedRoutes.set(input.segmentPath, byRoute);
265
+ }
266
+ const learnedKey = `${input.route}${VARY_KEY_SEPARATOR}${[...input.vary].sort().join(',')}`;
267
+ // The sibling set is a property of the ROUTE, not of one response: a payload
268
+ // that omits it (a framed segment truncated above the state script) must not
269
+ // un-learn what an earlier one published.
270
+ const staticSiblings = input.staticSiblings ?? byRoute.get(learnedKey)?.staticSiblings;
271
+ // Re-inserting moves the route to the end: most recently learned wins on
272
+ // lookup, mirroring the loading-shell prediction order.
273
+ byRoute.delete(learnedKey);
274
+ byRoute.set(learnedKey, {
275
+ route: input.route,
276
+ vary: input.vary,
277
+ catchAllOptional: input.catchAllOptional === true,
278
+ ...(staticSiblings ? { staticSiblings } : {}),
279
+ });
280
+ // LRU-bound like `entries`/`resolvedRoutes`: a route publishing a new vary
281
+ // set per prefetch (dynamic params in the vary set) would otherwise grow this
282
+ // map without limit (memory-pressure leak-slope test).
283
+ trimLearnedRoutes(byRoute);
284
+ }
285
+ entries.set(key, {
286
+ key,
287
+ html: input.html,
288
+ url: input.pathname + normalizeSegmentSearch(input.search),
289
+ time: now(),
290
+ staleTimeMs: input.staleTimeMs,
291
+ complete: input.complete,
292
+ static: input.static,
293
+ runtime: input.runtime,
294
+ });
295
+ touch(key);
296
+ trim();
297
+ }
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // Lookup.
301
+ // ---------------------------------------------------------------------------
302
+
303
+ /**
304
+ * The cached segment usable for `pathname`+`search`, or null.
305
+ *
306
+ * `networkFree` is the spec's commit rule: only a complete + static + fresh
307
+ * entry (or a complete runtime prefetch) lets a navigation skip the dynamic
308
+ * stage. Everything else is returned for painting only.
309
+ */
310
+ export function takeSegment(options: {
311
+ pathname: string;
312
+ search: string;
313
+ segmentPath: string;
314
+ }): SegmentHit | null {
315
+ const requestedUrl = options.pathname + normalizeSegmentSearch(options.search);
316
+ const usable: {
317
+ candidate: { key: string; learnedKey?: string; params?: Record<string, string | string[]> };
318
+ entry: SegmentEntry;
319
+ }[] = [];
320
+ for (const candidate of candidateKeys(options.segmentPath, options.pathname, options.search)) {
321
+ const entry = entries.get(candidate.key);
322
+ if (!entry) continue;
323
+ if (now() - entry.time >= entry.staleTimeMs) {
324
+ entries.delete(candidate.key);
325
+ continue;
326
+ }
327
+ // A shared (different-URL) hit is sound for STATIC content - and for a request-sampled payload
328
+ // the SERVER's own vary set authorised: a non-exact candidate key exists only because a learned
329
+ // route published a vary set, and that set is the server's assertion of what the payload depends
330
+ // on. Everything else stays exact-URL: a request-sampled payload belongs to the URL it sampled.
331
+ const shared = entry.url !== requestedUrl;
332
+ const varyAuthorised = candidate.learnedKey !== undefined;
333
+ if (shared && !entry.static && !(entry.runtime && varyAuthorised)) continue;
334
+ usable.push({ candidate, entry });
335
+ }
336
+ // Candidate ORDER is specificity (exact URL first), but an INCOMPLETE first
337
+ // match yields to a RUNTIME prefetch of this exact URL: an `allow-runtime`
338
+ // route's request-sampled payload is the only one carrying the content behind
339
+ // its Suspense boundaries, while a document-derived entry for the same URL
340
+ // holds the fallbacks. A complete first match already covers everything a
341
+ // runtime sample could, so it keeps the established first-match order.
342
+ const first = usable[0];
343
+ const chosen =
344
+ first && !first.entry.complete
345
+ ? (usable.find(({ entry }) => entry.runtime && entry.url === requestedUrl) ?? first)
346
+ : first;
347
+ if (chosen) {
348
+ const { candidate, entry } = chosen;
349
+ const key = candidate.key;
350
+ const shared = entry.url !== requestedUrl;
351
+ touch(key);
352
+ // A learned route that is still serving lookups is live: re-insert it so the
353
+ // LRU bound evicts the route patterns nobody navigates to.
354
+ if (candidate.learnedKey) touchLearnedRoute(options.segmentPath, candidate.learnedKey);
355
+ return {
356
+ // A SHARED payload was rendered for a DIFFERENT URL of the same route, so the concrete params
357
+ // baked into its three param carriers belong to that URL. Re-stamp them for the URL being served -
358
+ // the client-side mirror of the server's `withRequestRouteParams`, and the same soundness rule:
359
+ // the vary set authorises sharing the CONTENT, never the param values.
360
+ html: shared && candidate.params ? restampSharedParams(entry.html, candidate.params) : entry.html,
361
+ // A COMPLETE + STATIC entry is network-free even when SHARED: a static prerender whose vary set
362
+ // does not contain the differing param is by construction byte-identical for this URL, so there
363
+ // is nothing left to fetch. Everything else stays exact-URL only: a shared SHELL (an App Shell
364
+ // whose params hung) is missing the per-param content, and a shared request-sampled payload
365
+ // belongs to the URL it sampled. Both may paint, but the navigation must still issue its request.
366
+ networkFree: entry.complete && (entry.static || (!shared && entry.runtime)),
367
+ // A PREFETCH asks a weaker question than a commit: is there anything left to fetch FOR THIS URL?
368
+ // An exact-URL hit always answers no. A SHARED hit answers no when the sharing was authorised -
369
+ // either the payload is static, or a published vary set let `candidateKeys` produce the shared
370
+ // match. The navigation that follows still fetches the dynamic remainder.
371
+ prefetchSatisfied:
372
+ !shared || (optimisticRoutingEnabled() && (entry.static || entry.runtime)),
373
+ runtime: entry.runtime,
374
+ };
375
+ }
376
+ return null;
377
+ }
378
+
379
+ /**
380
+ * `experimental.optimisticRouting` (default ON; register-render stamps the
381
+ * document only for an app that turned it OFF).
382
+ *
383
+ * With it off, a prefetch of a URL that was never fetched still goes to the
384
+ * wire even when a sibling entry's vary set covers it: optimistic reuse of
385
+ * another URL's entry to answer a prefetch IS the feature the flag names, and
386
+ * the suites that pin the pre-flag behavior block that first request
387
+ * (`segment-cache/search-params` shared-loading-state). The reuse a NAVIGATION
388
+ * makes (`networkFree`) is a separate, older contract and stays on.
389
+ */
390
+ function optimisticRoutingEnabled(): boolean {
391
+ return (
392
+ (globalThis as { __PNEXT_NO_OPTIMISTIC_ROUTING__?: boolean })
393
+ .__PNEXT_NO_OPTIMISTIC_ROUTING__ !== true
394
+ );
395
+ }
396
+
397
+ /**
398
+ * True when a PREFETCH of this segment has nothing left to fetch because a fresh entry the server's
399
+ * own vary set covers is already cached.
400
+ *
401
+ * Deliberately separate from `takeSegment`: this answers only "skip the wire", never "what may this
402
+ * navigation paint or commit". A PPR shell is postponed and carries no `x-nextjs-prerender`, so it is
403
+ * not static and `takeSegment` will not share it - but when the server published a vary set that does
404
+ * NOT name the differing param, that set is its assertion that the shell's bytes are identical for
405
+ * this URL.
406
+ *
407
+ * A request-sampled (`runtime`) payload is excluded: its bytes belong to the URL it was sampled for,
408
+ * and the App-Shell scheduler relies on re-fetching them.
409
+ */
410
+ export function segmentPrefetchCovered(options: {
411
+ pathname: string;
412
+ search: string;
413
+ segmentPath: string;
414
+ }): { html: string } | null {
415
+ const requestedUrl = options.pathname + normalizeSegmentSearch(options.search);
416
+ for (const candidate of candidateKeys(options.segmentPath, options.pathname, options.search)) {
417
+ const entry = entries.get(candidate.key);
418
+ if (!entry) continue;
419
+ if (now() - entry.time >= entry.staleTimeMs) continue;
420
+ if (entry.runtime) continue;
421
+ // An exact-URL hit is `takeSegment`'s business (it already dedupes there);
422
+ // this predicate exists for the SHARED case the vary set authorised.
423
+ if (entry.url === requestedUrl || candidate.learnedKey === undefined) continue;
424
+ return {
425
+ html: candidate.params ? restampSharedParams(entry.html, candidate.params) : entry.html,
426
+ };
427
+ }
428
+ return null;
429
+ }
430
+
431
+ // ---------------------------------------------------------------------------
432
+ // w9-segment-split: composed lookups.
433
+ // ---------------------------------------------------------------------------
434
+
435
+ /**
436
+ * Whether a navigation may commit a COMPOSED (layout frame + page frame)
437
+ * document. Off by default: the split frames are recorded unconditionally (they
438
+ * cost one extra entry and can only ever add hits), but composing them changes
439
+ * what a navigation PAINTS, and that is the half of the split that has not been
440
+ * validated end to end. `setSegmentSplitCommit(true)` opts in.
441
+ */
442
+ let segmentSplitCommit = false;
443
+
444
+ export function setSegmentSplitCommit(enabled: boolean): void {
445
+ segmentSplitCommit = enabled;
446
+ }
447
+
448
+ export function segmentSplitCommitEnabled(): boolean {
449
+ return segmentSplitCommit;
450
+ }
451
+
452
+ /**
453
+ * A document composed from the cached LAYOUT and PAGE frames of this URL, or null when either frame
454
+ * misses (or composition is off / the frames have no page slot to splice). This is the layout/page
455
+ * split's payoff: a sibling URL can miss the whole-document entry (the layout read the differing
456
+ * param) but hit the page frame, so only the layout has to come off the network.
457
+ */
458
+ export function takeComposedSegment(options: {
459
+ pathname: string;
460
+ search: string;
461
+ }): SegmentHit | null {
462
+ if (!segmentSplitCommit) return null;
463
+ const layout = takeSegment({ ...options, segmentPath: LAYOUT_FRAME_PATH });
464
+ if (!layout) return null;
465
+ const page = takeSegment({ ...options, segmentPath: PAGE_FRAME_PATH });
466
+ if (!page) return null;
467
+ const html = composeSegmentFrames(layout.html, page.html);
468
+ if (html === null) return null;
469
+ return {
470
+ html,
471
+ // A composed document is only as committable as its weaker half.
472
+ networkFree: layout.networkFree && page.networkFree,
473
+ prefetchSatisfied: layout.prefetchSatisfied && page.prefetchSatisfied,
474
+ runtime: layout.runtime || page.runtime,
475
+ };
476
+ }
477
+
478
+ // ---------------------------------------------------------------------------
479
+ // w9-segment-split (G2): per-segment NAVIGATION — fetch `/_page`, keep the
480
+ // layout.
481
+ // ---------------------------------------------------------------------------
482
+
483
+ /**
484
+ * The layout frame of a whole DOCUMENT the router committed (the hard-loaded
485
+ * page, a navigation response). Kept apart from `entries`:
486
+ *
487
+ * - a document carries no vary metadata, so the frame is only ever valid for
488
+ * the exact URL it was rendered for, and
489
+ * - it is only ever consumed by a SAME-URL navigation (Next's "refresh the
490
+ * page segments, keep the layouts" semantics). A later navigation BACK to
491
+ * this URL must not paint a layout this old, so `cachedLayoutFrame` takes it
492
+ * only when the caller says the destination is the current location.
493
+ */
494
+ interface DocumentLayoutFrame {
495
+ html: string;
496
+ time: number;
497
+ staleTimeMs: number;
498
+ }
499
+ const documentLayoutFrames = new Map<string, DocumentLayoutFrame>();
500
+ /** Small on purpose: only the live URL's frame is ever read back. */
501
+ const DOCUMENT_LAYOUT_LIMIT = 8;
502
+
503
+ export function recordDocumentLayoutFrame(input: {
504
+ pathname: string;
505
+ search: string;
506
+ html: string;
507
+ staleTimeMs: number;
508
+ }): void {
509
+ // No page slot: `stripPageSlotContent` would return the whole document, and a
510
+ // composed document would then carry the page twice.
511
+ if (pageSlotRange(input.html) === null) return;
512
+ const key = input.pathname + normalizeSegmentSearch(input.search);
513
+ documentLayoutFrames.delete(key);
514
+ documentLayoutFrames.set(key, {
515
+ html: stripPageSlotContent(input.html),
516
+ time: now(),
517
+ staleTimeMs: input.staleTimeMs,
518
+ });
519
+ while (documentLayoutFrames.size > DOCUMENT_LAYOUT_LIMIT) {
520
+ const oldest = documentLayoutFrames.keys().next().value;
521
+ if (oldest === undefined) break;
522
+ documentLayoutFrames.delete(oldest);
523
+ }
524
+ }
525
+
526
+ /**
527
+ * The LAYOUT frame a navigation to this URL may keep: a cached `/_layout` segment (fetched, or derived
528
+ * from a prefetch of this URL), or - for a navigation to the URL already on screen - the live
529
+ * document's own frame.
530
+ */
531
+ function cachedLayoutFrame(options: {
532
+ pathname: string;
533
+ search: string;
534
+ sameUrl: boolean;
535
+ }): string | null {
536
+ const hit = takeSegment({
537
+ pathname: options.pathname,
538
+ search: options.search,
539
+ segmentPath: LAYOUT_FRAME_PATH,
540
+ });
541
+ // The continuation chunks are cut at CONSUMPTION time, so every source of a
542
+ // layout frame — the server's `/_layout` response, a frame derived from a
543
+ // prefetch, the live document's — is covered by one rule.
544
+ if (hit) return stripStreamedContinuation(hit.html);
545
+ if (!options.sameUrl) return null;
546
+ const frame = documentLayoutFrames.get(options.pathname + normalizeSegmentSearch(options.search));
547
+ if (!frame) return null;
548
+ if (now() - frame.time >= frame.staleTimeMs) return null;
549
+ return stripStreamedContinuation(frame.html);
550
+ }
551
+
552
+ /**
553
+ * True when a navigation to this URL may fetch the PAGE frame alone: its layout chain is already
554
+ * cached (or already on screen), so `/_page` carries everything the commit still needs. This is the
555
+ * gate the router asks BEFORE it goes to the wire - the commit itself goes through
556
+ * `composeCachedLayout`, and any failure there falls back to the whole-document fetch.
557
+ */
558
+ export function needsPageFrameOnly(options: {
559
+ pathname: string;
560
+ search: string;
561
+ sameUrl: boolean;
562
+ }): boolean {
563
+ return cachedLayoutFrame(options) !== null;
564
+ }
565
+
566
+ /**
567
+ * Splice a `/_page` response into this URL's cached layout frame, yielding the document the
568
+ * whole-document response would have been. Null when the frame is gone (it can expire between the
569
+ * gate and the response) or the two cannot be proven to line up.
570
+ */
571
+ export function composeCachedLayout(options: {
572
+ pathname: string;
573
+ search: string;
574
+ sameUrl: boolean;
575
+ pageHtml: string;
576
+ }): string | null {
577
+ const layout = cachedLayoutFrame(options);
578
+ return layout === null ? null : composeSegmentFrames(layout, options.pageHtml);
579
+ }
580
+
581
+ /**
582
+ * Compose a just-fetched `/_layout` response with this URL's cached PAGE frame into the whole
583
+ * document the `/_index` request would have returned. The layout-only fetch is only worth issuing if
584
+ * the navigation that follows can still paint a complete page, so the two frames are joined HERE, at
585
+ * record time, and filed as an ordinary whole-document entry. Null when the page frame is gone or the
586
+ * two cannot be proven to line up.
587
+ */
588
+ export function composeFetchedLayoutFrame(options: {
589
+ pathname: string;
590
+ search: string;
591
+ layoutHtml: string;
592
+ }): string | null {
593
+ const page = takeSegment({
594
+ pathname: options.pathname,
595
+ search: options.search,
596
+ segmentPath: PAGE_FRAME_PATH,
597
+ });
598
+ if (!page) return null;
599
+ return composeSegmentFrames(stripStreamedContinuation(options.layoutHtml), page.html);
600
+ }
601
+
602
+ /**
603
+ * True when the PAGE frame for this URL is cached but its LAYOUT frame is not: the navigation should
604
+ * fetch `/_layout` alone rather than the whole route.
605
+ */
606
+ /**
607
+ * True when a RUNTIME-prefetched PAGE frame already covers this URL. Only the page opts into
608
+ * `prefetch = 'allow-runtime'`; the layouts above it have no static data to prefetch (their
609
+ * params-dependent content sits behind a Suspense boundary and resolves in the dynamic stage), so once
610
+ * the page frame's vary set covers the URL the prefetch has nothing left to fetch.
611
+ */
612
+ export function runtimePageFrameSatisfied(options: {
613
+ pathname: string;
614
+ search: string;
615
+ }): boolean {
616
+ const requestedUrl = options.pathname + normalizeSegmentSearch(options.search);
617
+ for (const candidate of candidateKeys(PAGE_FRAME_PATH, options.pathname, options.search)) {
618
+ const entry = entries.get(candidate.key);
619
+ if (!entry?.runtime) continue;
620
+ if (now() - entry.time >= entry.staleTimeMs) continue;
621
+ // SHARED coverage only: a URL the client fetched for itself keeps its own
622
+ // prefetch (a stale/incomplete entry of that URL must still refetch); this
623
+ // rule exists for the sibling param the vary set already covers.
624
+ if (entry.url === requestedUrl || candidate.learnedKey === undefined) continue;
625
+ return true;
626
+ }
627
+ return false;
628
+ }
629
+
630
+ export function needsLayoutFrameOnly(options: {
631
+ pathname: string;
632
+ search: string;
633
+ }): boolean {
634
+ if (takeSegment({ ...options, segmentPath: LAYOUT_FRAME_PATH })) return false;
635
+ return takeSegment({ ...options, segmentPath: PAGE_FRAME_PATH }) !== null;
636
+ }
637
+
638
+ /**
639
+ * Every cache key that could serve this URL, most specific first: the EXACT-URL key (what a response
640
+ * with no vary metadata is filed under), then the learned route/vary keys, widest vary set first, so
641
+ * a per-URL entry always beats the route's shared App Shell for the URL it was fetched for.
642
+ *
643
+ * When the server has told us which route serves this exact pathname, only that route's keys are
644
+ * considered - a static sibling of a dynamic segment is never predicted onto the dynamic pattern.
645
+ */
646
+ function candidateKeys(
647
+ segmentPath: string,
648
+ pathname: string,
649
+ search: string,
650
+ ): { key: string; learnedKey?: string; params?: Record<string, string | string[]> }[] {
651
+ const keys: { key: string; learnedKey?: string; params?: Record<string, string | string[]> }[] = [
652
+ { key: entryKey(segmentPath, null, {}, search, null, { pathname }) },
653
+ ];
654
+ const byRoute = learnedRoutes.get(segmentPath);
655
+ if (!byRoute) return keys;
656
+ const ownRoute = resolvedRoutes.get(pathname);
657
+ const learned = [...byRoute.entries()]
658
+ .reverse()
659
+ .filter(([, candidate]) => ownRoute === undefined || candidate.route === ownRoute)
660
+ // Stable sort: within one specificity the most recently learned wins.
661
+ .sort(([, a], [, b]) => b.vary.length - a.vary.length);
662
+ for (const [learnedKey, candidate] of learned) {
663
+ const params = matchRoutePattern(candidate.route, pathname, candidate.catchAllOptional);
664
+ if (!params) continue;
665
+ // The pattern matches, but the segment it matched is a literal the server
666
+ // published as a STATIC SIBLING of this dynamic level: that sibling's own
667
+ // route serves the URL, so the dynamic route's shared bytes are not its.
668
+ if (namesStaticSibling(pathname, candidate.staticSiblings)) continue;
669
+ keys.push({
670
+ key: entryKey(segmentPath, candidate.route, params, search, candidate.vary, { pathname }),
671
+ learnedKey,
672
+ params,
673
+ });
674
+ }
675
+ return keys;
676
+ }
677
+
678
+ function entryKey(
679
+ segmentPath: string,
680
+ route: string | null,
681
+ params: Record<string, string | string[]>,
682
+ search: string,
683
+ vary: string[] | null,
684
+ context: { pathname: string },
685
+ ): string {
686
+ return segmentVaryCacheKey({
687
+ route: route ?? '',
688
+ segmentPath,
689
+ params,
690
+ search,
691
+ vary: route ? vary : null,
692
+ pathname: context.pathname,
693
+ });
694
+ }
695
+
696
+ // ---------------------------------------------------------------------------
697
+ // Route pattern matching (pure — the unit tests drive this directly).
698
+ // ---------------------------------------------------------------------------
699
+
700
+ /**
701
+ * Match `pathname` against a colon-form route pattern (`/vary/:cat`,
702
+ * `/blog/:slug*`) and return its params, or null when it does not match.
703
+ * `optionalCatchAll` lets a trailing `:slug*` match zero segments.
704
+ */
705
+ export function matchRoutePattern(
706
+ pattern: string,
707
+ pathname: string,
708
+ optionalCatchAll = false,
709
+ ): Record<string, string | string[]> | null {
710
+ const patternParts = pattern.split('/').filter(Boolean);
711
+ const pathParts = pathname.split('/').filter(Boolean);
712
+ const params: Record<string, string | string[]> = {};
713
+ let cursor = 0;
714
+ for (const part of patternParts) {
715
+ const catchAll = /^:([\w$]+)\*$/.exec(part);
716
+ if (catchAll) {
717
+ const rest = pathParts.slice(cursor).map(decodeSegment);
718
+ if (rest.length === 0 && !optionalCatchAll) return null;
719
+ if (rest.length > 0) params[catchAll[1]!] = rest;
720
+ return params;
721
+ }
722
+ const dynamic = /^:([\w$]+)$/.exec(part);
723
+ const value = pathParts[cursor++];
724
+ if (value === undefined) return null;
725
+ if (dynamic) params[dynamic[1]!] = decodeSegment(value);
726
+ else if (part !== value) return null;
727
+ }
728
+ return cursor === pathParts.length ? params : null;
729
+ }
730
+
731
+ // ---------------------------------------------------------------------------
732
+ // Shared-payload param re-stamping (pure — no DOM, driven by the unit tests).
733
+ // ---------------------------------------------------------------------------
734
+
735
+ /** The island prop placeholder the server fills per request (slots.tsx). */
736
+ const PROMISE_MARKER_KEY = '__pnextPromise';
737
+ const ISLAND_PARAMS_ATTRIBUTE = 'data-pnext-params';
738
+
739
+ /**
740
+ * Rewrite every param carrier in a payload rendered for a SIBLING URL of the
741
+ * same route so it describes `params` instead.
742
+ *
743
+ * Three carriers, all of which must agree (the server's `withRequestRouteParams`
744
+ * writes exactly these three): `window.__PNEXT_ROUTE__` (what `useParams()`
745
+ * reads), the `data-pnext-params` island scopes (which OVERRIDE the window
746
+ * state for the island carrying them), and the `data-pnext-props` promise
747
+ * markers a partial prerender baked for a hanging `params` prop. Leaving any of
748
+ * them behind hands the destination URL the source URL's params for the whole
749
+ * time its dynamic stage is in flight.
750
+ *
751
+ * The payload's own embedded route state is the source of truth for what to
752
+ * replace: only values byte-identical to ITS params are rewritten, so app
753
+ * content that merely happens to contain the same string is untouched.
754
+ */
755
+ function restampSharedParams(html: string, params: Record<string, string | string[]>): string {
756
+ const match = /(window\.__PNEXT_ROUTE__=)(\{.*?\})(;?<\/script>)/s.exec(html);
757
+ if (!match?.[2]) return html;
758
+ let state: Record<string, unknown>;
759
+ try {
760
+ state = JSON.parse(match[2]) as Record<string, unknown>;
761
+ } catch {
762
+ // A route script we cannot parse cannot be re-stamped safely.
763
+ return html;
764
+ }
765
+ const baked = (state.params ?? {}) as Record<string, string | string[]>;
766
+ const bakedJson = paramsJson(baked);
767
+ const targetJson = paramsJson(params);
768
+ if (bakedJson === targetJson) return html;
769
+ const rewritten =
770
+ html.slice(0, match.index) +
771
+ `${match[1]}${paramsJson({ ...state, params })}${match[3]}` +
772
+ html.slice(match.index + match[0].length);
773
+ const replacements: [string, string][] = [
774
+ [islandParamsAttribute(bakedJson), islandParamsAttribute(targetJson)],
775
+ [promiseMarkerJson(bakedJson), promiseMarkerJson(targetJson)],
776
+ ];
777
+ return rewriteIslandMarkup(rewritten, text => {
778
+ let next = text;
779
+ for (const [from, to] of replacements) {
780
+ next = next.split(from).join(to).split(escapeAttribute(from)).join(escapeAttribute(to));
781
+ }
782
+ return next;
783
+ });
784
+ }
785
+
786
+ const islandParamsAttribute = (json: string) =>
787
+ `${ISLAND_PARAMS_ATTRIBUTE}="${json.replaceAll('"', '&quot;')}"`;
788
+ const promiseMarkerJson = (json: string) => `{"${PROMISE_MARKER_KEY}":${json}}`;
789
+ const escapeAttribute = (json: string) => json.replaceAll('"', '&quot;');
790
+
791
+ /** `serializeProps`' output shape, mirrored so the strings compare byte-wise. */
792
+ const JSON_ESCAPES: Record<string, string> = {
793
+ '<': '\\u003c',
794
+ '>': '\\u003e',
795
+ '&': '\\u0026',
796
+ '\u2028': '\\u2028',
797
+ '\u2029': '\\u2029',
798
+ };
799
+
800
+ function paramsJson(value: unknown): string {
801
+ return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, char => JSON_ESCAPES[char] ?? char);
802
+ }
803
+
804
+ /**
805
+ * Apply `rewrite` to the markup AND to the island markers the renderer emits for neutral islands - the
806
+ * same two-surface pass the server does, since an island's params live inside the marker until it
807
+ * materializes.
808
+ */
809
+ function rewriteIslandMarkup(html: string, rewrite: (text: string) => string): string {
810
+ return rewrite(html).replace(
811
+ /<!--pnext-(client|client-after|page):([^>]*)-->/g,
812
+ (marker, kind: string, encoded: string) => {
813
+ const decoded = encoded.replaceAll('&lt;', '<').replaceAll('&gt;', '>');
814
+ const next = rewrite(decoded);
815
+ return next === decoded
816
+ ? marker
817
+ : `<!--pnext-${kind}:${next.replaceAll('<', '&lt;').replaceAll('>', '&gt;')}-->`;
818
+ },
819
+ );
820
+ }
821
+
822
+ function decodeSegment(segment: string): string {
823
+ try {
824
+ return decodeURIComponent(segment);
825
+ } catch {
826
+ // A malformed escape is the route-cache identity as written; keep it.
827
+ return segment;
828
+ }
829
+ }
830
+
831
+ // ---------------------------------------------------------------------------
832
+ // Eviction.
833
+ // ---------------------------------------------------------------------------
834
+
835
+ /**
836
+ * A same-URL navigation refreshes PAGE segments only: the layout/head segments of the current tree stay
837
+ * cached (Next's router.refresh semantics - the shared layout is not re-fetched). Everything below the
838
+ * page segment is dropped.
839
+ */
840
+ export function evictPageSegments(): void {
841
+ for (const key of [...entries.keys()]) {
842
+ if (BODY_SEGMENT_PATHS.some(path => key.startsWith(path + VARY_KEY_SEPARATOR))) {
843
+ entries.delete(key);
844
+ }
845
+ }
846
+ // The vary sets learned for those page segments describe payloads that are
847
+ // gone; keeping them would leak one map per route across refreshes.
848
+ for (const path of BODY_SEGMENT_PATHS) learnedRoutes.delete(path);
849
+ }
850
+
851
+ /** Drop every entry (a revalidation invalidates the whole segment cache). */
852
+ export function clearSegmentCache(): void {
853
+ entries.clear();
854
+ learnedRoutes.clear();
855
+ resolvedRoutes.clear();
856
+ // A revalidation invalidates the layouts too: a `/_page`-only navigation
857
+ // after one would keep painting the pre-revalidation layout chain.
858
+ documentLayoutFrames.clear();
859
+ }
860
+
861
+ /** Test seam: the number of live entries. */
862
+ export function segmentEntryCount(): number {
863
+ return entries.size;
864
+ }
865
+
866
+ function touch(key: string): void {
867
+ const entry = entries.get(key);
868
+ if (!entry) return;
869
+ entries.delete(key);
870
+ entries.set(key, entry);
871
+ }
872
+
873
+ function trim(): void {
874
+ while (entries.size > SEGMENT_ENTRY_LIMIT) {
875
+ const oldest = entries.keys().next().value;
876
+ if (oldest === undefined) return;
877
+ entries.delete(oldest);
878
+ }
879
+ }
880
+
881
+ function touchLearnedRoute(segmentPath: string, learnedKey: string): void {
882
+ const byRoute = learnedRoutes.get(segmentPath);
883
+ const learned = byRoute?.get(learnedKey);
884
+ if (!byRoute || !learned) return;
885
+ byRoute.delete(learnedKey);
886
+ byRoute.set(learnedKey, learned);
887
+ }
888
+
889
+ function trimLearnedRoutes(byRoute: Map<string, LearnedRoute>): void {
890
+ while (byRoute.size > SEGMENT_ENTRY_LIMIT) {
891
+ const oldest = byRoute.keys().next().value;
892
+ if (oldest === undefined) return;
893
+ byRoute.delete(oldest);
894
+ }
895
+ }
896
+
897
+ /**
898
+ * The route pattern the SERVER resolved `pathname` to (learned from a segment
899
+ * or document payload), or null when this exact pathname was never answered.
900
+ *
901
+ * The router's route-tree cache uses it as the static-sibling guard: a pathname
902
+ * the server resolved to its OWN route must never be predicted onto a dynamic
903
+ * sibling pattern.
904
+ */
905
+ export function resolvedRouteFor(pathname: string): string | null {
906
+ return resolvedRoutes.get(pathname) ?? null;
907
+ }
908
+
909
+ /** Test seam: how many route/vary sets are remembered for a segment path. */
910
+ export function learnedRouteCount(segmentPath: string): number {
911
+ return learnedRoutes.get(segmentPath)?.size ?? 0;
912
+ }
913
+
914
+ // ---------------------------------------------------------------------------
915
+ // Wire decoding.
916
+ // ---------------------------------------------------------------------------
917
+
918
+ /**
919
+ * Decode a segment response body into its payload plus vary metadata. Returns null when the body is not
920
+ * the expected segment payload (a header-stripping proxy, a plain document, ...).
921
+ */
922
+ export function decodeSegmentPayload(
923
+ body: string,
924
+ expectedSegment: string,
925
+ ): (SegmentPayloadMeta & { html: string }) | null {
926
+ const json = body.startsWith('0:') ? body.slice(2) : body;
927
+ let payload: {
928
+ segment?: unknown;
929
+ html?: unknown;
930
+ vary?: unknown;
931
+ layoutVary?: unknown;
932
+ pageVary?: unknown;
933
+ route?: unknown;
934
+ params?: unknown;
935
+ shell?: unknown;
936
+ };
937
+ try {
938
+ payload = JSON.parse(json) as typeof payload;
939
+ } catch {
940
+ return null;
941
+ }
942
+ if (payload.segment !== expectedSegment || typeof payload.html !== 'string') return null;
943
+ const payloadSiblings = staticSiblingsFromPayloadHtml(payload.html);
944
+ return {
945
+ html: payload.html,
946
+ vary: Array.isArray(payload.vary) ? payload.vary.map(String) : null,
947
+ // Absent (not null) when the server did not split the response — the shape
948
+ // stays byte-identical to the pre-split payload for every such response.
949
+ ...(Array.isArray(payload.layoutVary) ? { layoutVary: payload.layoutVary.map(String) } : {}),
950
+ ...(Array.isArray(payload.pageVary) ? { pageVary: payload.pageVary.map(String) } : {}),
951
+ route: typeof payload.route === 'string' ? payload.route : null,
952
+ params: normalizeWireParams(payload.params),
953
+ ...(payloadSiblings ? { staticSiblings: payloadSiblings } : {}),
954
+ ...(appShellOf(payload.shell) ? { shell: appShellOf(payload.shell)! } : {}),
955
+ };
956
+ }
957
+
958
+ /** The `shell` field of a segment payload, when it carries a usable one. */
959
+ function appShellOf(value: unknown): { html: string; route: string } | null {
960
+ const shell = value as { html?: unknown; route?: unknown } | null | undefined;
961
+ if (!shell || typeof shell.html !== 'string' || typeof shell.route !== 'string') return null;
962
+ return { html: shell.html, route: shell.route };
963
+ }
964
+
965
+ /**
966
+ * Decode the segment metadata of a whole HTML DOCUMENT - a navigation response or the initial
967
+ * hard-loaded page - rather than a framed segment payload.
968
+ *
969
+ * An ordinary document render carries no vary set (the server only tracks param access inside a
970
+ * segment-prefetch render), so the vary set is UNKNOWN and the entry keys on its exact URL. The route
971
+ * identity it does carry still matters: it teaches the sibling guard which route serves this pathname.
972
+ *
973
+ * A document resumed from a BAKED SHELL is the exception: the server publishes that shell's own vary
974
+ * set as `pageVary`, and that set IS the assertion the exact-URL fallback exists for want of. Without
975
+ * it a hard load of `/route/foo` seeds an entry no navigation to `/route/bar` can key onto.
976
+ */
977
+ export function decodeDocumentSegmentMeta(html: string): SegmentPayloadMeta {
978
+ const state = documentRouteState(html);
979
+ if (!state) return { vary: null, route: null, params: {} };
980
+ const siblings = staticSiblingsOf(state);
981
+ return {
982
+ vary: Array.isArray(state.pageVary) ? state.pageVary.map(String) : null,
983
+ route: typeof state.route === 'string' ? state.route : null,
984
+ params: normalizeWireParams(state.params),
985
+ ...(siblings ? { staticSiblings: siblings } : {}),
986
+ };
987
+ }
988
+
989
+ interface WireRouteState {
990
+ route?: unknown;
991
+ params?: unknown;
992
+ pageVary?: unknown;
993
+ staticChildren?: unknown;
994
+ staticChildrenBySegment?: unknown;
995
+ }
996
+
997
+ /** The `window.__PNEXT_ROUTE__` state embedded in a rendered document. */
998
+ function documentRouteState(html: string): WireRouteState | null {
999
+ const match = /window\.__PNEXT_ROUTE__=(\{.*?\});?<\/script>/s.exec(html);
1000
+ if (!match?.[1]) return null;
1001
+ try {
1002
+ return JSON.parse(match[1]) as WireRouteState;
1003
+ } catch {
1004
+ return null;
1005
+ }
1006
+ }
1007
+
1008
+ /**
1009
+ * The literal siblings the server published for each dynamic level of the
1010
+ * route, keyed by segment index (`staticChildrenBySegment`, plus the legacy
1011
+ * deepest-level `staticChildren` at the pattern's last dynamic index).
1012
+ */
1013
+ function staticSiblingsOf(state: WireRouteState): Record<string, string[]> | undefined {
1014
+ const siblings: Record<string, string[]> = {};
1015
+ const bySegment = state.staticChildrenBySegment;
1016
+ if (typeof bySegment === 'object' && bySegment !== null) {
1017
+ for (const [index, names] of Object.entries(bySegment as Record<string, unknown>)) {
1018
+ if (Array.isArray(names)) siblings[index] = names.map(String);
1019
+ }
1020
+ }
1021
+ if (Array.isArray(state.staticChildren) && typeof state.route === 'string') {
1022
+ const parts = state.route.split('/').filter(Boolean);
1023
+ const deepest = parts.reduce((last, part, index) => (part.startsWith(':') ? index : last), -1);
1024
+ if (deepest >= 0 && siblings[String(deepest)] === undefined) {
1025
+ siblings[String(deepest)] = state.staticChildren.map(String);
1026
+ }
1027
+ }
1028
+ return Object.keys(siblings).length > 0 ? siblings : undefined;
1029
+ }
1030
+
1031
+ /** A framed segment payload's static siblings, read off the HTML it carries. */
1032
+ function staticSiblingsFromPayloadHtml(html: string): Record<string, string[]> | undefined {
1033
+ const state = documentRouteState(html);
1034
+ return state ? staticSiblingsOf(state) : undefined;
1035
+ }
1036
+
1037
+ /**
1038
+ * True when `pathname` names one of the learned route's STATIC SIBLINGS at one of its dynamic levels -
1039
+ * `/products/sale` beside `/products/:id`.
1040
+ *
1041
+ * The pattern match alone cannot tell the two apart: a dynamic segment matches any literal, so a
1042
+ * learned route's (param-shared) entry would otherwise serve the sibling's URL with the dynamic
1043
+ * route's bytes. The server publishes the literal siblings of every dynamic level alongside the route,
1044
+ * the same signal the optimistic predictor bails on. Only a POSITIVE match rejects: a route whose
1045
+ * siblings the server did not publish keeps sharing exactly as before.
1046
+ */
1047
+ function namesStaticSibling(
1048
+ pathname: string,
1049
+ siblings: Record<string, string[]> | undefined,
1050
+ ): boolean {
1051
+ if (!siblings) return false;
1052
+ const parts = pathname.split('/').filter(Boolean).map(decodeSegment);
1053
+ return Object.entries(siblings).some(([index, names]) => {
1054
+ const segment = parts[Number(index)];
1055
+ return segment !== undefined && names.includes(segment);
1056
+ });
1057
+ }
1058
+
1059
+ function normalizeWireParams(value: unknown): Record<string, string | string[]> {
1060
+ if (typeof value !== 'object' || value === null) return {};
1061
+ const params: Record<string, string | string[]> = {};
1062
+ for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
1063
+ if (Array.isArray(raw)) params[key] = (raw as unknown[]).map(stringifyParam);
1064
+ else if (raw !== undefined && raw !== null) params[key] = stringifyParam(raw);
1065
+ }
1066
+ return params;
1067
+ }
1068
+
1069
+ /** Wire params are strings; anything else is coerced defensively. */
1070
+ function stringifyParam(value: unknown): string {
1071
+ return typeof value === 'string' ? value : JSON.stringify(value) ?? '';
1072
+ }
1073
+
1074
+ /** True when a decoded vary set makes the segment query-dependent. */
1075
+ export function varyIncludesSearch(vary: readonly string[] | null): boolean {
1076
+ return vary?.includes(SEARCH_PARAMS_SENTINEL) === true;
1077
+ }