@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,86 @@
1
+ // Per-link navigation pending state for `useLinkStatus()` (next/link). A single link is "pending" at a
2
+ // time: the last one whose click started an in-flight soft navigation. It clears on completion, and on
3
+ // any navigation started elsewhere - all of which broadcast a location change.
4
+ import { createContext } from 'preact/compat';
5
+ import { softNavigate } from '../../api/router';
6
+ import { locationListeners, onNavigationStart } from '../../api/router/events';
7
+ import { patchHistory } from '../../api/client-navigation';
8
+
9
+ export interface LinkStatus {
10
+ pending: boolean;
11
+ }
12
+
13
+ export const LinkStatusContext = createContext<LinkStatus>({ pending: false });
14
+
15
+ type Listener = () => void;
16
+
17
+ let pendingToken: symbol | null = null;
18
+ // Set right before navigateWithStatus starts its own soft-nav, so the resulting
19
+ // synchronous nav-start is recognized as the link's own, not a supersession.
20
+ let ownNavStart: symbol | null = null;
21
+ const listeners = new Set<Listener>();
22
+ let wired = false;
23
+
24
+ function notify() {
25
+ for (const listener of [...listeners]) listener();
26
+ }
27
+
28
+ function setPending(token: symbol | null) {
29
+ if (pendingToken === token) return;
30
+ pendingToken = token;
31
+ notify();
32
+ }
33
+
34
+ // A different navigation starting (router.push, refresh, another link)
35
+ // supersedes the pending link immediately, even while its own nav is still
36
+ // in flight — its synchronous start isn't the pending link's own.
37
+ function onNavStart() {
38
+ if (ownNavStart !== null) {
39
+ ownNavStart = null;
40
+ return;
41
+ }
42
+ setPending(null);
43
+ }
44
+
45
+ // A location change ends the pending state: for the pending link's own nav this
46
+ // only fires at completion (its pushState happens after the fetch resolves);
47
+ // for shallow pushState / browser back it fires immediately. Both should clear.
48
+ function onLocationChange() {
49
+ setPending(null);
50
+ }
51
+
52
+ function wire() {
53
+ if (wired || (!process.browser && typeof window === 'undefined')) return;
54
+ wired = true;
55
+ // Raw window.history.pushState/replaceState (shallow routing) must clear the
56
+ // pending link; the patch broadcasts them through locationListeners.
57
+ patchHistory();
58
+ onNavigationStart(onNavStart);
59
+ locationListeners().add(onLocationChange);
60
+ window.addEventListener('popstate', onLocationChange);
61
+ }
62
+
63
+ export function subscribeLinkStatus(_token: symbol, listener: Listener) {
64
+ wire();
65
+ listeners.add(listener);
66
+ return () => listeners.delete(listener);
67
+ }
68
+
69
+ export function linkPending(token: symbol) {
70
+ return pendingToken === token;
71
+ }
72
+
73
+ // Start a soft navigation owned by this link: mark it pending, clear on
74
+ // completion. A newer click flips the token, so a stale completion won't clear
75
+ // the newer link's pending state.
76
+ export function navigateWithStatus(
77
+ token: symbol,
78
+ href: string,
79
+ options: { replace?: boolean; scroll?: boolean },
80
+ ) {
81
+ ownNavStart = token;
82
+ setPending(token);
83
+ void softNavigate(href, options).finally(() => {
84
+ if (pendingToken === token) setPending(null);
85
+ });
86
+ }
@@ -0,0 +1,57 @@
1
+ // DEFERRED TIER of the navigation compat layer (see ./nav-compat). Every policy
2
+ // here is read by the router runtime and by nothing else, so it loads with the
3
+ // runtime chunk — the installers finish before the facade hands the runtime out,
4
+ // so the first fetch already sees Next's windows and caches.
5
+ import { setSoftNavBoundary } from '../../api/router';
6
+ import { setNavigationScrollAction } from '../../api/router/events';
7
+ import {
8
+ setLoadingShellPredictionPolicy,
9
+ setPrefetchStaleTimePolicy,
10
+ setRevalidationPrefetchDelay,
11
+ setSegmentCachePolicy,
12
+ setShellStaleTimePolicy,
13
+ setStylesheetReconciler,
14
+ } from '../../api/router/policies';
15
+ import { installSegmentCachePolicy } from './segment-cache-policy';
16
+ // `output: 'export'` document fetching. Imported for its module-scope
17
+ // registration, which has to beat the router's first prefetches — this module
18
+ // resolving before the runtime is handed out is exactly that guarantee.
19
+ import '../export/client';
20
+ import { reconcileNextStylesheets } from './css-order';
21
+ import { staleTimeForEntryMs, staticStaleTimeMs } from './prefetch-cache';
22
+ import { isOutsideBasePath } from './base-path';
23
+ import { predictNextLoadingShell } from './optimistic-routing';
24
+ import { applyNextNavigationScroll } from './navigation-scroll';
25
+ import { installRouteAnnouncer } from './route-announcer';
26
+
27
+ export function installNavPolicies() {
28
+ // Route announcer: mount the empty `<next-route-announcer>` and update it on
29
+ // every soft-nav commit (a11y — announce client route changes to AT). Next
30
+ // mounts it at hydration; it announces nothing until the first navigation, and
31
+ // this tier lands on idle, long before one can happen.
32
+ installRouteAnnouncer();
33
+ // Prefetch staleTime policy: static 300s / dynamic 0, with the
34
+ // experimental.staleTimes override injected as window.__PNEXT_STALE_TIMES__,
35
+ // and a response `x-nextjs-stale-time` header overriding both when present.
36
+ setPrefetchStaleTimePolicy(staleTimeForEntryMs);
37
+ setRevalidationPrefetchDelay(300);
38
+ // Loading-shell window: a route's cached loading boundary (and the prefetch
39
+ // entry carrying it) reuses for the STATIC window even when its dynamic data
40
+ // is immediately stale — Next's split staleness model.
41
+ setShellStaleTimePolicy(staticStaleTimeMs);
42
+ setLoadingShellPredictionPolicy(predictNextLoadingShell);
43
+ // Per-segment (vary-params) client cache — Segment-M2 deliverable 3. Entries
44
+ // are keyed on the params a segment provably reads, so one entry serves every
45
+ // param value it does not depend on.
46
+ setSegmentCachePolicy(installSegmentCachePolicy());
47
+ setNavigationScrollAction(applyNextNavigationScroll);
48
+ setStylesheetReconciler(reconcileNextStylesheets);
49
+ // basePath boundary: a same-origin URL outside the configured basePath must
50
+ // hard-navigate (a `basePath:false` target), not soft-swap.
51
+ setSoftNavBoundary(isOutsideBasePath);
52
+ // NOTE: the standalone segment-prefetch wire runtime (segment-prefetch.ts) is
53
+ // deliberately NOT installed: its intent-time tree/body fetches fed caches
54
+ // nothing consumed and broke Next's request-count contracts (a hover inside a
55
+ // "no-requests" scope must not fetch). The router's own prefetch cache +
56
+ // loading-shell cache cover the segment semantics on the HTML-swap model.
57
+ }
@@ -0,0 +1,42 @@
1
+ // Client-side navigation compat, installed once per compat entry.
2
+ //
3
+ // FIRST PAINT holds only what a click can hit before the navigation runtime lands: Next's
4
+ // javascript:-URL block message (a static Link click reports it with no runtime at all) and
5
+ // `window.next.router`, which app code can call at any time. Everything else here is a POLICY the
6
+ // runtime alone reads, so it rides the runtime's own chunk via registerDeferredInstall and never costs
7
+ // a page that never navigates.
8
+ import {
9
+ prefetchRoute,
10
+ registerDeferredInstall,
11
+ setBlockedJavascriptUrlReporter,
12
+ softNavigate,
13
+ } from '../../api/router';
14
+
15
+ const BLOCKED_JS_URL_MESSAGE =
16
+ 'Next.js has blocked a javascript: URL as a security precaution.';
17
+
18
+ export function installNavCompat() {
19
+ if (!process.browser && typeof window === 'undefined') return;
20
+ setBlockedJavascriptUrlReporter(() => console.error(BLOCKED_JS_URL_MESSAGE));
21
+ const windowWithReporter = window as typeof window & { __PNEXT_JS_URL_REPORTER__?: boolean };
22
+ if (!windowWithReporter.__PNEXT_JS_URL_REPORTER__) {
23
+ windowWithReporter.__PNEXT_JS_URL_REPORTER__ = true;
24
+ window.addEventListener('pnext:blocked-javascript-url', () => {
25
+ console.error(BLOCKED_JS_URL_MESSAGE);
26
+ });
27
+ }
28
+ installNextRouterGlobal();
29
+ registerDeferredInstall(() => import('./nav-compat-runtime').then(m => m.installNavPolicies()));
30
+ }
31
+
32
+ // `window.next.router` — Next's own global handle. Pure-core apps have no such
33
+ // namespace, so it installs from compat rather than from the router hub.
34
+ function installNextRouterGlobal() {
35
+ window.next ??= {};
36
+ window.next.router = {
37
+ ...window.next.router,
38
+ push: href => softNavigate(String(href)),
39
+ replace: href => softNavigate(String(href), { replace: true }),
40
+ prefetch: href => prefetchRoute(String(href), { strict: true }),
41
+ };
42
+ }
@@ -0,0 +1,154 @@
1
+ import type { NavigationScrollAction, NavigationScrollOptions } from '../../api/router';
2
+
3
+ const RECT_PROPERTIES = ['bottom', 'height', 'left', 'right', 'top', 'width', 'x', 'y'] as const;
4
+
5
+ export type NextScrollPlan = 'hash' | 'preserve' | 'restore' | 'top' | 'top-then-target';
6
+
7
+ export function nextScrollPlan(
8
+ url: URL,
9
+ options: NavigationScrollOptions,
10
+ targetInViewport: boolean | undefined,
11
+ ): NextScrollPlan {
12
+ if (options.pop) return 'restore';
13
+ if (options.scroll === false) return 'preserve';
14
+ if (url.hash) return 'hash';
15
+ if (targetInViewport === undefined) return 'top';
16
+ return targetInViewport ? 'preserve' : 'top-then-target';
17
+ }
18
+
19
+ export function firstScrollCandidate<T>(
20
+ candidates: Iterable<T>,
21
+ shouldSkip: (candidate: T) => boolean,
22
+ ): T | undefined {
23
+ for (const candidate of candidates) if (!shouldSkip(candidate)) return candidate;
24
+ return undefined;
25
+ }
26
+
27
+ export function deepestSingleChild<T>(node: T | undefined, onlyChild: (node: T) => T | undefined) {
28
+ while (node) {
29
+ const child = onlyChild(node);
30
+ if (!child) return node;
31
+ node = child;
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ export function topOnlyScrollPosition(scrollX: number): [number, number] {
37
+ return [scrollX, 0];
38
+ }
39
+
40
+ export const applyNextNavigationScroll: NavigationScrollAction = (url, options) => {
41
+ const target = scrollFocusTarget();
42
+ const plan = nextScrollPlan(
43
+ url,
44
+ options,
45
+ target ? targetTopInViewport(target, document.documentElement.clientHeight) : undefined,
46
+ );
47
+ if (plan === 'restore') {
48
+ const state = history.state as { __pnextScroll?: [number, number] } | null;
49
+ const [x, y] = state?.__pnextScroll ?? [0, 0];
50
+ window.scrollTo(x, y);
51
+ return;
52
+ }
53
+ // React hoists a rendered <style precedence>/<link rel=stylesheet>/metadata element out of the segment
54
+ // and into <head>. Next resolves its scroll target with findDOMNode() on the changed segment, so when
55
+ // the segment's FIRST host element is one of those the walk starts inside <head>, skips every
56
+ // zero-rect sibling there and runs out - the navigation scrolls nowhere and focuses nothing. pnext
57
+ // renders those elements in place, so recognize the leading hoisted element and reproduce the same
58
+ // no-op. Placed after `restore` so history traversal still restores its saved position.
59
+ if (segmentLeadsWithHoistedElement()) return;
60
+ if (plan === 'hash') {
61
+ scrollToHash(url.hash);
62
+ return;
63
+ }
64
+ if (plan === 'preserve') {
65
+ if (target && options.scroll !== false) focusSegment(target);
66
+ return;
67
+ }
68
+ if (plan === 'top') {
69
+ window.scrollTo(...topOnlyScrollPosition(window.scrollX));
70
+ return;
71
+ }
72
+ if (!target) return;
73
+ window.scrollTo(...topOnlyScrollPosition(window.scrollX));
74
+ if (!targetTopInViewport(target, document.documentElement.clientHeight)) target.scrollIntoView();
75
+ focusSegment(target);
76
+ };
77
+
78
+ function shouldSkipScrollElement(element: HTMLElement) {
79
+ const position = getComputedStyle(element).position;
80
+ if (position === 'sticky' || position === 'fixed') return true;
81
+ const rect = element.getBoundingClientRect();
82
+ return RECT_PROPERTIES.every(item => rect[item] === 0);
83
+ }
84
+
85
+ function targetTopInViewport(element: HTMLElement, viewportHeight: number) {
86
+ const rects = element.getClientRects();
87
+ if (rects.length === 0) return false;
88
+ let top = Number.POSITIVE_INFINITY;
89
+ for (const rect of rects) top = Math.min(top, rect.top);
90
+ return top >= 0 && top <= viewportHeight;
91
+ }
92
+
93
+ function scrollFocusTarget(): HTMLElement | null {
94
+ const segment = changedSegmentRoot();
95
+ if (!segment) return null;
96
+ // Next resolves the target with findDOMNode() on the changed segment: the
97
+ // FIRST host element the segment rendered, never a descendant of it. The page
98
+ // container IS that segment, so its first element child is the target as-is.
99
+ // The fallback roots below wrap the segment in pnext's own chrome, so there
100
+ // the single-child chain is walked down to the content that segment rendered.
101
+ const first =
102
+ segment.id === PAGE_CONTAINER_ID
103
+ ? (segment.firstElementChild ?? undefined)
104
+ : deepestSingleChild(segment.firstElementChild ?? undefined, node =>
105
+ node.nextElementSibling === null ? (node.firstElementChild ?? undefined) : undefined,
106
+ );
107
+ const nodes: HTMLElement[] = [];
108
+ for (let node = first; node; node = node.nextElementSibling ?? undefined) {
109
+ if (node instanceof HTMLElement) nodes.push(node);
110
+ }
111
+ return firstScrollCandidate(nodes, shouldSkipScrollElement) ?? null;
112
+ }
113
+
114
+ /** The renderer's page-slot wrapper — the segment a navigation replaced. */
115
+ const PAGE_CONTAINER_ID = 'pnext-page';
116
+
117
+ // Elements React relocates into <head> when a component renders them.
118
+ const HOISTED_TAGS = new Set(['style', 'link', 'title', 'meta', 'base']);
119
+
120
+ /**
121
+ * Whether the changed segment's first rendered element is one React would have hoisted into <head> -
122
+ * the state in which Next's scroll walk never reaches the page content (see the call site).
123
+ */
124
+ function segmentLeadsWithHoistedElement(): boolean {
125
+ const segment = changedSegmentRoot();
126
+ const first = segment?.firstElementChild;
127
+ return first !== null && first !== undefined && HOISTED_TAGS.has(first.localName);
128
+ }
129
+
130
+ function changedSegmentRoot(): HTMLElement | null {
131
+ const page = document.getElementById(PAGE_CONTAINER_ID);
132
+ if (page instanceof HTMLElement) return page;
133
+ const root = document.querySelector('[data-pnext-root]');
134
+ return root instanceof HTMLElement ? root : document.body;
135
+ }
136
+
137
+ function scrollToHash(hash: string) {
138
+ const id = decodeURIComponent(hash.slice(1));
139
+ const target = id === 'top' ? document.body : (document.getElementById(id) ?? namedTarget(id));
140
+ target?.scrollIntoView();
141
+ }
142
+
143
+ function namedTarget(name: string): HTMLElement | null {
144
+ const named = document.getElementsByName(name)[0];
145
+ return named instanceof HTMLElement ? named : null;
146
+ }
147
+
148
+ function focusSegment(target: HTMLElement) {
149
+ try {
150
+ target.focus({ preventScroll: true });
151
+ } catch {
152
+ // A swap can detach the target before focus runs.
153
+ }
154
+ }
@@ -0,0 +1,206 @@
1
+ // Optimistic routing (Next's route-prediction trie) - COMPAT client policy.
2
+ //
3
+ // After a route pattern is learned from one URL (its prefetched/visited shell carries
4
+ // `window.__PNEXT_ROUTE__`), a navigation to a DIFFERENT URL matching the same pattern paints that
5
+ // cached loading shell instantly with the predicted params - no tree prefetch needed.
6
+ //
7
+ // Two guards keep predictions honest, both mirroring Next:
8
+ // - `staticChildren`: literal siblings of the dynamic level. A target whose segment names a static
9
+ // sibling must resolve on the server, since the sibling route wins over the dynamic pattern, so
10
+ // prediction bails.
11
+ // - rewrite detection: a shell whose OWN stored pathname does not match its route pattern was
12
+ // produced by a rewrite. Such a response must never seed predictions - the rewrite may map a
13
+ // sibling URL somewhere else entirely, or vary on search params.
14
+
15
+ import type { RouteParamValue } from '../../types';
16
+
17
+ interface RouteState {
18
+ // Bracket-free colon form: `:id` for a dynamic segment, `:slug*` for a
19
+ // catch-all; `catchAllOptional` distinguishes `[[...slug]]` (matches zero
20
+ // segments) from `[...slug]` (requires at least one).
21
+ route: string;
22
+ params: Record<string, RouteParamValue>;
23
+ catchAllOptional?: boolean;
24
+ staticChildren?: string[];
25
+ staticChildrenBySegment?: Record<string, string[]>;
26
+ prefetchKind?: 'shell' | 'eager';
27
+ /** `prefetch = 'allow-runtime'`: the shell is request-sampled PER URL. */
28
+ runtimePrefetch?: boolean;
29
+ }
30
+
31
+ interface PredictedShell {
32
+ html: string;
33
+ route: RouteState;
34
+ prefetch?: 'shell' | 'eager';
35
+ /**
36
+ * True when the predicted shell belongs to an `allow-runtime` route. Its content is derived from the
37
+ * sampled request's params, so a cached shell of ONE param value says nothing about another - the
38
+ * router must still prefetch every unseen URL of the route (a `partial` shell, being static, is shared).
39
+ *
40
+ * ...UNLESS `experimental.appShells` is on: there the runtime prefetch of a non-eager route renders
41
+ * with its `params` HANGING, so the cached shell IS the route's shared App Shell and says the same
42
+ * thing for every param. Revealing another link of the route then has nothing to fetch.
43
+ */
44
+ runtimePrefetch?: boolean;
45
+ }
46
+
47
+ /** `experimental.appShells`, stamped into the document by register-render. */
48
+ function appShellsEnabled(): boolean {
49
+ if (!process.browser && typeof window === 'undefined') return false;
50
+ return (window as { __PNEXT_APP_SHELLS__?: boolean }).__PNEXT_APP_SHELLS__ === true;
51
+ }
52
+
53
+ export function predictNextLoadingShell(
54
+ url: URL,
55
+ shells: ReadonlyMap<string, string>,
56
+ ): PredictedShell | undefined {
57
+ // Most recently stored shells first (Map preserves insertion order).
58
+ for (const [pathname, html] of [...shells.entries()].reverse()) {
59
+ const state = routeStateFromShell(html);
60
+ if (!state) continue;
61
+ // A shell's own URL must fit its route pattern before it can teach the
62
+ // route trie. This catches rewrites, including a dynamic response served
63
+ // for a known static sibling.
64
+ if (!matchRoute(state, pathname)) continue;
65
+ const params = matchRoute(state, url.pathname);
66
+ if (params) {
67
+ return {
68
+ html: reifyShellParams(html, state.params, params),
69
+ route: {
70
+ route: state.route,
71
+ params,
72
+ ...(state.catchAllOptional ? { catchAllOptional: true } : {}),
73
+ },
74
+ ...(state.prefetchKind ? { prefetch: state.prefetchKind } : {}),
75
+ ...(state.runtimePrefetch && !appShellsEnabled() ? { runtimePrefetch: true } : {}),
76
+ };
77
+ }
78
+ }
79
+ return undefined;
80
+ }
81
+
82
+ function routeStateFromShell(html: string): RouteState | undefined {
83
+ const raw = /window\.__PNEXT_ROUTE__=(\{.*?\});<\/script>/.exec(html)?.[1];
84
+ if (!raw) return undefined;
85
+ try {
86
+ const state = JSON.parse(raw) as RouteState;
87
+ return typeof state.route === 'string' && state.params && typeof state.params === 'object'
88
+ ? state
89
+ : undefined;
90
+ } catch {
91
+ return undefined;
92
+ }
93
+ }
94
+
95
+ function matchRoute(state: RouteState, pathname: string): Record<string, RouteParamValue> | undefined {
96
+ const pattern = state.route.split('/').filter(Boolean);
97
+ const target = pathname.split('/').filter(Boolean).map(decodeSegment);
98
+ const params: Record<string, RouteParamValue> = {};
99
+ let cursor = 0;
100
+ for (let index = 0; index < pattern.length; index++) {
101
+ const segment = pattern[index]!;
102
+ // Colon form: `:slug*` is a catch-all, `:id` a single dynamic segment.
103
+ const catchAll = /^:([\w$]+)\*$/.exec(segment);
104
+ const dynamic = catchAll ? null : /^:([\w$]+)$/.exec(segment);
105
+ const siblingKey = String(index);
106
+ const siblings = state.staticChildrenBySegment?.[siblingKey] ??
107
+ (index === pattern.length - 1 ? state.staticChildren : undefined);
108
+ // A dynamic trie node is usable only after the server has said its static
109
+ // siblings are complete. This is the production-only null-vs-empty rule.
110
+ if ((dynamic || catchAll) && siblings === undefined) return undefined;
111
+ if (siblings?.includes(target[cursor] ?? '')) return undefined;
112
+ if (catchAll) {
113
+ const name = catchAll[1]!;
114
+ if (state.catchAllOptional) {
115
+ // `[[...slug]]`: matches the base path plus any nested tail.
116
+ const rest = target.slice(cursor);
117
+ if (rest.length > 0) params[name] = rest;
118
+ return params;
119
+ }
120
+ // `[...slug]`: a catch-all learned from a parallel slot must not predict a
121
+ // sibling URL for the primary route. Only reuse it when the source shell
122
+ // itself was rendered through that catch-all, and require ≥1 segment.
123
+ if (!Array.isArray(state.params[name])) return undefined;
124
+ const rest = target.slice(cursor);
125
+ if (rest.length === 0) return undefined;
126
+ params[name] = rest;
127
+ return params;
128
+ }
129
+ const value = target[cursor++];
130
+ if (value === undefined) return undefined;
131
+ if (dynamic) params[dynamic[1]!] = value;
132
+ else if (segment !== value) return undefined;
133
+ }
134
+ return cursor === target.length ? params : undefined;
135
+ }
136
+
137
+ function reifyShellParams(
138
+ html: string,
139
+ previous: Record<string, RouteParamValue>,
140
+ params: Record<string, RouteParamValue>,
141
+ ): string {
142
+ if (typeof DOMParser === 'undefined') return html;
143
+ const doc = new DOMParser().parseFromString(html, 'text/html');
144
+ materializeClientMarkers(doc);
145
+ for (const root of doc.querySelectorAll<HTMLElement>('[data-pnext-params]')) {
146
+ const raw = root.getAttribute('data-pnext-params');
147
+ if (!raw) continue;
148
+ try {
149
+ const scoped = JSON.parse(raw) as Record<string, RouteParamValue>;
150
+ if (sameParams(scoped, previous)) root.setAttribute('data-pnext-params', JSON.stringify(params));
151
+ } catch {
152
+ // A malformed island scope cannot safely be reified.
153
+ }
154
+ }
155
+ return `<!doctype html>${doc.documentElement.outerHTML}`;
156
+ }
157
+
158
+ function materializeClientMarkers(doc: Document): void {
159
+ const comments: Comment[] = [];
160
+ const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_COMMENT);
161
+ while (walker.nextNode()) comments.push(walker.currentNode as Comment);
162
+ for (const start of comments.reverse()) {
163
+ const match = /^(pnext-client|pnext-page):([^>]*)$/.exec(start.data);
164
+ const kind = match?.[1];
165
+ const encoded = match?.[2];
166
+ if (!kind || !encoded || !start.parentNode) continue;
167
+ const container = doc.createElement('div');
168
+ const tag = kind === 'pnext-page' ? 'div' : 'pnext-client';
169
+ container.innerHTML = `<${tag} ${encoded}></${tag}>`;
170
+ const island = container.firstElementChild;
171
+ if (!island) continue;
172
+ let node = start.nextSibling;
173
+ while (node && !(node.nodeType === Node.COMMENT_NODE && node.nodeValue === `/${kind}`)) {
174
+ const next = node.nextSibling;
175
+ island.append(node);
176
+ node = next;
177
+ }
178
+ if (!node) continue;
179
+ start.replaceWith(island);
180
+ node.remove();
181
+ }
182
+ }
183
+
184
+ function sameParams(
185
+ left: Record<string, RouteParamValue>,
186
+ right: Record<string, RouteParamValue>,
187
+ ): boolean {
188
+ const leftKeys = Object.keys(left);
189
+ const rightKeys = Object.keys(right);
190
+ if (leftKeys.length !== rightKeys.length) return false;
191
+ return leftKeys.every(key => {
192
+ const a = left[key];
193
+ const b = right[key];
194
+ return Array.isArray(a)
195
+ ? Array.isArray(b) && a.length === b.length && a.every((value, index) => value === b[index])
196
+ : a === b;
197
+ });
198
+ }
199
+
200
+ function decodeSegment(segment: string) {
201
+ try {
202
+ return decodeURIComponent(segment);
203
+ } catch {
204
+ return segment;
205
+ }
206
+ }
@@ -0,0 +1,111 @@
1
+ // Client prefetch-cache policy (COMPAT - ships to the browser).
2
+ //
3
+ // Core's router owns a single HTML-swap prefetch cache with a flat TTL. Next's semantics split that
4
+ // window into a static staleTime and a dynamic staleTime, overridable through
5
+ // `experimental.staleTimes.{static,dynamic}`, and it seeds the cache from visited pages, uses low fetch
6
+ // priority for viewport prefetches, and skips prefetching entirely for bot user agents.
7
+ //
8
+ // The exact static/dynamic distinction Next draws is a property of the RSC segment payload. On the
9
+ // HTML-swap model the router can still learn a route's staleness from the `x-nextjs-stale-time`
10
+ // response header; when present it wins, otherwise the entry falls back to the configured dynamic
11
+ // window. This module is the single source of the windows plus the config override, read by the core
12
+ // router through a registered hook so core carries no next.config dependency.
13
+ //
14
+ // Injected into the client at build time as `window.__PNEXT_STALE_TIMES__`; undefined means Next defaults.
15
+
16
+
17
+ /** Next's built-in prefetch staleTimes (milliseconds). */
18
+ // Since Next 15 the DYNAMIC default is 0: dynamic data is never reused across
19
+ // navigations unless `experimental.staleTimes.dynamic` opts in. (The loading
20
+ // shell of a dynamic route still reuses for the STATIC window.)
21
+ export const DEFAULT_DYNAMIC_STALE_TIME_MS = 0;
22
+ export const DEFAULT_STATIC_STALE_TIME_MS = 300_000;
23
+ /** Next's runtime-prefetch stale threshold (RUNTIME_PREFETCH_DYNAMIC_STALE). */
24
+ const RUNTIME_DOCUMENT_STALE_TIME_MS = 30_000;
25
+
26
+ /** Shape injected by the server (seconds, mirroring next.config). */
27
+ interface StaleTimesConfig {
28
+ /** experimental.staleTimes.dynamic (seconds). */
29
+ dynamic?: number;
30
+ /** experimental.staleTimes.static (seconds). */
31
+ static?: number;
32
+ }
33
+
34
+ declare global {
35
+ interface Window {
36
+ __PNEXT_STALE_TIMES__?: StaleTimesConfig;
37
+ }
38
+ }
39
+
40
+ function configuredStaleTimes(): StaleTimesConfig {
41
+ if (!process.browser && typeof window === 'undefined') return {};
42
+ return window.__PNEXT_STALE_TIMES__ ?? {};
43
+ }
44
+
45
+ function windowFromSeconds(value: number | undefined, fallbackMs: number): number {
46
+ // A configured 0 means "always fresh" (never reuse) — honour it exactly.
47
+ return typeof value === 'number' && Number.isFinite(value) ? value * 1000 : fallbackMs;
48
+ }
49
+
50
+ /** The dynamic prefetch window in ms (config override or 30s default). */
51
+ export function dynamicStaleTimeMs(): number {
52
+ return windowFromSeconds(configuredStaleTimes().dynamic, DEFAULT_DYNAMIC_STALE_TIME_MS);
53
+ }
54
+
55
+ /** The static prefetch window in ms (config override or 300s default). */
56
+ export function staticStaleTimeMs(): number {
57
+ return windowFromSeconds(configuredStaleTimes().static, DEFAULT_STATIC_STALE_TIME_MS);
58
+ }
59
+
60
+ /**
61
+ * The reuse window for a prefetched/visited entry. `prefetchFull` marks a `prefetch={true}` full-page
62
+ * prefetch, which Next reuses for the FULL static window even when the route itself is dynamic - it
63
+ * wins over the header. A route that reported an explicit `x-nextjs-stale-time` uses that value;
64
+ * otherwise the entry is treated as dynamic.
65
+ *
66
+ * `static`/`runtime` classify a per-segment entry (the cached static stage a navigation paints before
67
+ * its dynamic stage lands). A STATIC payload reports its own staleness and that value is authoritative:
68
+ * widening it to the configured static window keeps painting an expired static stage. A RUNTIME payload
69
+ * is request-sampled data, never static content: it reuses for the window its own short-lived cache
70
+ * reported, or the dynamic window when the response named none - never the static one.
71
+ *
72
+ * The header value is used RAW here, not through `getStaleTimeMs`: this cache backs the classic
73
+ * staleTimes semantics, where a configured `staleTimes.dynamic: 0` must mean "never reuse". The clamp
74
+ * applies only to the per-segment cache.
75
+ */
76
+ export function staleTimeForEntryMs(options: {
77
+ headerStaleTimeSeconds?: number;
78
+ prefetchFull?: boolean;
79
+ static?: boolean;
80
+ runtime?: boolean;
81
+ runtimeDocument?: boolean;
82
+ }): number {
83
+ // A NAVIGATION document of an `allow-runtime` route: its header reports the route's PUBLIC cache
84
+ // window (the private/runtime caches resolve after the headers flushed), but the content is
85
+ // request-sampled. Next files no such entry from a navigation at all - the runtime-prefetch stream is
86
+ // what seeds its segment cache - so anything recorded from the document must not outlive the
87
+ // runtime-prefetch threshold: shorter-lived private caches are hung out of runtime samples entirely.
88
+ if (options.runtimeDocument) {
89
+ const headerMs =
90
+ typeof options.headerStaleTimeSeconds === 'number'
91
+ ? options.headerStaleTimeSeconds * 1000
92
+ : Number.POSITIVE_INFINITY;
93
+ return Math.min(headerMs, RUNTIME_DOCUMENT_STALE_TIME_MS);
94
+ }
95
+ if ((options.static || options.runtime) && typeof options.headerStaleTimeSeconds === 'number') {
96
+ return options.headerStaleTimeSeconds * 1000;
97
+ }
98
+ if (options.runtime) return dynamicStaleTimeMs();
99
+ if (options.prefetchFull) {
100
+ return Math.max(
101
+ staticStaleTimeMs(),
102
+ typeof options.headerStaleTimeSeconds === 'number'
103
+ ? options.headerStaleTimeSeconds * 1000
104
+ : 0,
105
+ );
106
+ }
107
+ if (typeof options.headerStaleTimeSeconds === 'number') {
108
+ return options.headerStaleTimeSeconds * 1000;
109
+ }
110
+ return dynamicStaleTimeMs();
111
+ }