@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c

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 (392) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +126 -16
  3. package/dist/bin/rango.js +319 -95
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +2724 -1053
  6. package/package.json +68 -14
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +64 -2
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +224 -32
  11. package/skills/caching/SKILL.md +279 -17
  12. package/skills/composability/SKILL.md +27 -3
  13. package/skills/css/SKILL.md +76 -0
  14. package/skills/debug-manifest/SKILL.md +4 -2
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +11 -9
  17. package/skills/hooks/SKILL.md +243 -29
  18. package/skills/host-router/SKILL.md +83 -23
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +68 -19
  21. package/skills/layout/SKILL.md +13 -9
  22. package/skills/links/SKILL.md +190 -23
  23. package/skills/loader/SKILL.md +235 -9
  24. package/skills/middleware/SKILL.md +18 -10
  25. package/skills/migrate-nextjs/SKILL.md +43 -19
  26. package/skills/migrate-react-router/SKILL.md +8 -2
  27. package/skills/mime-routes/SKILL.md +28 -1
  28. package/skills/observability/SKILL.md +172 -0
  29. package/skills/parallel/SKILL.md +18 -7
  30. package/skills/prerender/SKILL.md +65 -60
  31. package/skills/rango/SKILL.md +251 -24
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +115 -48
  34. package/skills/route/SKILL.md +46 -5
  35. package/skills/router-setup/SKILL.md +30 -8
  36. package/skills/scripts/SKILL.md +179 -0
  37. package/skills/server-actions/SKILL.md +775 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +130 -0
  40. package/skills/testing/bindings.md +103 -0
  41. package/skills/testing/cache-prerender.md +127 -0
  42. package/skills/testing/client-components.md +124 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +91 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +122 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +123 -0
  54. package/skills/typesafety/SKILL.md +322 -29
  55. package/skills/use-cache/SKILL.md +57 -14
  56. package/skills/view-transitions/SKILL.md +337 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +0 -65
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/connection-warmup.ts +134 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +192 -150
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/navigation-bridge.ts +94 -25
  70. package/src/browser/navigation-client.ts +121 -84
  71. package/src/browser/navigation-store-handle.ts +38 -0
  72. package/src/browser/navigation-store.ts +115 -67
  73. package/src/browser/navigation-transaction.ts +9 -59
  74. package/src/browser/network-error-handler.ts +34 -7
  75. package/src/browser/partial-update.ts +147 -128
  76. package/src/browser/prefetch/cache.ts +107 -56
  77. package/src/browser/prefetch/fetch.ts +204 -34
  78. package/src/browser/prefetch/queue.ts +6 -3
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +30 -7
  81. package/src/browser/react/NavigationProvider.tsx +283 -118
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  84. package/src/browser/react/filter-segment-order.ts +66 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +178 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +17 -14
  90. package/src/browser/react/use-href.tsx +8 -1
  91. package/src/browser/react/use-link-status.ts +33 -8
  92. package/src/browser/react/use-navigation.ts +10 -5
  93. package/src/browser/react/use-params.ts +11 -11
  94. package/src/browser/react/use-reverse.ts +106 -0
  95. package/src/browser/react/use-router.ts +25 -3
  96. package/src/browser/react/use-search-params.ts +0 -5
  97. package/src/browser/react/use-segments.ts +11 -21
  98. package/src/browser/response-adapter.ts +99 -8
  99. package/src/browser/rsc-router.tsx +91 -24
  100. package/src/browser/scroll-restoration.ts +30 -17
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +214 -55
  103. package/src/browser/types.ts +80 -9
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +60 -35
  107. package/src/build/generate-route-types.ts +2 -1
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +117 -14
  111. package/src/build/route-types/ast-route-extraction.ts +15 -8
  112. package/src/build/route-types/codegen.ts +16 -5
  113. package/src/build/route-types/include-resolution.ts +117 -23
  114. package/src/build/route-types/param-extraction.ts +6 -3
  115. package/src/build/route-types/per-module-writer.ts +22 -6
  116. package/src/build/route-types/router-processing.ts +55 -28
  117. package/src/build/route-types/scan-filter.ts +1 -1
  118. package/src/build/route-types/source-scan.ts +216 -0
  119. package/src/build/runtime-discovery.ts +9 -20
  120. package/src/cache/cache-error.ts +104 -0
  121. package/src/cache/cache-key-utils.ts +29 -13
  122. package/src/cache/cache-policy.ts +108 -34
  123. package/src/cache/cache-runtime.ts +224 -41
  124. package/src/cache/cache-scope.ts +188 -82
  125. package/src/cache/cache-tag.ts +103 -0
  126. package/src/cache/cf/cf-base64.ts +33 -0
  127. package/src/cache/cf/cf-cache-constants.ts +127 -0
  128. package/src/cache/cf/cf-cache-store.ts +1989 -378
  129. package/src/cache/cf/cf-cache-types.ts +349 -0
  130. package/src/cache/cf/cf-kv-utils.ts +46 -0
  131. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  132. package/src/cache/cf/index.ts +6 -16
  133. package/src/cache/document-cache.ts +89 -21
  134. package/src/cache/handle-snapshot.ts +70 -0
  135. package/src/cache/index.ts +10 -20
  136. package/src/cache/memory-segment-store.ts +136 -37
  137. package/src/cache/profile-registry.ts +46 -31
  138. package/src/cache/read-through-swr.ts +56 -12
  139. package/src/cache/segment-codec.ts +9 -17
  140. package/src/cache/tag-invalidation.ts +230 -0
  141. package/src/cache/types.ts +37 -100
  142. package/src/client.rsc.tsx +44 -21
  143. package/src/client.tsx +36 -61
  144. package/src/cloudflare/index.ts +11 -0
  145. package/src/cloudflare/tracing.ts +109 -0
  146. package/src/component-utils.ts +19 -0
  147. package/src/components/DefaultDocument.tsx +8 -2
  148. package/src/context-var.ts +18 -6
  149. package/src/decode-loader-results.ts +52 -0
  150. package/src/defer.ts +196 -0
  151. package/src/deps/ssr.ts +0 -1
  152. package/src/encode-kv.ts +49 -0
  153. package/src/errors.ts +30 -4
  154. package/src/escape-script.ts +52 -0
  155. package/src/handle.ts +31 -23
  156. package/src/handles/MetaTags.tsx +62 -19
  157. package/src/handles/Scripts.tsx +183 -0
  158. package/src/handles/breadcrumbs.ts +37 -8
  159. package/src/handles/is-thenable.ts +19 -0
  160. package/src/handles/meta.ts +51 -40
  161. package/src/handles/script.ts +244 -0
  162. package/src/host/cookie-handler.ts +9 -60
  163. package/src/host/errors.ts +0 -24
  164. package/src/host/index.ts +8 -2
  165. package/src/host/pattern-matcher.ts +23 -52
  166. package/src/host/router.ts +107 -99
  167. package/src/host/testing.ts +40 -27
  168. package/src/host/types.ts +37 -4
  169. package/src/host/utils.ts +1 -1
  170. package/src/href-client.ts +137 -22
  171. package/src/index.rsc.ts +96 -12
  172. package/src/index.ts +94 -14
  173. package/src/internal-debug.ts +11 -10
  174. package/src/loader-store.ts +500 -0
  175. package/src/loader.rsc.ts +20 -13
  176. package/src/loader.ts +12 -11
  177. package/src/missing-id-error.ts +68 -0
  178. package/src/outlet-context.ts +1 -1
  179. package/src/outlet-provider.tsx +1 -5
  180. package/src/prerender/param-hash.ts +16 -16
  181. package/src/prerender/store.ts +32 -37
  182. package/src/prerender.ts +61 -6
  183. package/src/redirect-origin.ts +100 -0
  184. package/src/regex-escape.ts +8 -0
  185. package/src/render-error-thrower.tsx +20 -0
  186. package/src/response-utils.ts +34 -0
  187. package/src/reverse.ts +65 -40
  188. package/src/root-error-boundary.tsx +1 -19
  189. package/src/route-content-wrapper.tsx +19 -77
  190. package/src/route-definition/dsl-helpers.ts +304 -309
  191. package/src/route-definition/helper-factories.ts +28 -140
  192. package/src/route-definition/helpers-types.ts +82 -55
  193. package/src/route-definition/index.ts +1 -2
  194. package/src/route-definition/redirect.ts +44 -11
  195. package/src/route-definition/resolve-handler-use.ts +12 -1
  196. package/src/route-definition/use-item-types.ts +29 -0
  197. package/src/route-map-builder.ts +0 -16
  198. package/src/route-types.ts +19 -46
  199. package/src/router/basename.ts +14 -0
  200. package/src/router/content-negotiation.ts +73 -25
  201. package/src/router/error-handling.ts +45 -18
  202. package/src/router/find-match.ts +44 -23
  203. package/src/router/handler-context.ts +27 -43
  204. package/src/router/instrument.ts +350 -0
  205. package/src/router/intercept-resolution.ts +39 -20
  206. package/src/router/lazy-includes.ts +10 -47
  207. package/src/router/loader-resolution.ts +155 -72
  208. package/src/router/logging.ts +0 -6
  209. package/src/router/manifest.ts +18 -29
  210. package/src/router/match-api.ts +9 -24
  211. package/src/router/match-context.ts +0 -22
  212. package/src/router/match-handlers.ts +58 -58
  213. package/src/router/match-middleware/background-revalidation.ts +40 -24
  214. package/src/router/match-middleware/cache-lookup.ts +159 -285
  215. package/src/router/match-middleware/cache-store.ts +64 -52
  216. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  217. package/src/router/match-middleware/segment-resolution.ts +0 -22
  218. package/src/router/match-pipelines.ts +1 -42
  219. package/src/router/match-result.ts +44 -74
  220. package/src/router/metrics.ts +0 -34
  221. package/src/router/middleware-types.ts +7 -134
  222. package/src/router/middleware.ts +247 -166
  223. package/src/router/navigation-snapshot.ts +0 -51
  224. package/src/router/params-util.ts +23 -0
  225. package/src/router/pattern-matching.ts +85 -94
  226. package/src/router/prefetch-cache-ttl.ts +51 -0
  227. package/src/router/prerender-match.ts +104 -65
  228. package/src/router/preview-match.ts +3 -1
  229. package/src/router/request-classification.ts +28 -62
  230. package/src/router/revalidation.ts +123 -73
  231. package/src/router/route-snapshot.ts +0 -1
  232. package/src/router/router-context.ts +3 -28
  233. package/src/router/router-interfaces.ts +83 -35
  234. package/src/router/router-options.ts +136 -5
  235. package/src/router/router-registry.ts +2 -5
  236. package/src/router/segment-resolution/fresh.ts +97 -84
  237. package/src/router/segment-resolution/helpers.ts +86 -6
  238. package/src/router/segment-resolution/loader-cache.ts +76 -39
  239. package/src/router/segment-resolution/revalidation.ts +272 -320
  240. package/src/router/segment-resolution/static-store.ts +19 -5
  241. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  242. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  243. package/src/router/segment-resolution.ts +5 -1
  244. package/src/router/segment-wrappers.ts +6 -5
  245. package/src/router/state-cookie-name.ts +33 -0
  246. package/src/router/substitute-pattern-params.ts +56 -0
  247. package/src/router/telemetry-otel.ts +161 -199
  248. package/src/router/telemetry.ts +96 -19
  249. package/src/router/timeout.ts +0 -20
  250. package/src/router/tracing.ts +206 -0
  251. package/src/router/trie-matching.ts +162 -64
  252. package/src/router/types.ts +9 -63
  253. package/src/router/url-params.ts +0 -5
  254. package/src/router.ts +110 -55
  255. package/src/rsc/handler-context.ts +3 -2
  256. package/src/rsc/handler.ts +264 -220
  257. package/src/rsc/helpers.ts +100 -6
  258. package/src/rsc/index.ts +2 -5
  259. package/src/rsc/json-route-result.ts +38 -0
  260. package/src/rsc/loader-fetch.ts +114 -38
  261. package/src/rsc/manifest-init.ts +28 -41
  262. package/src/rsc/origin-guard.ts +39 -25
  263. package/src/rsc/progressive-enhancement.ts +117 -11
  264. package/src/rsc/redirect-guard.ts +99 -0
  265. package/src/rsc/response-cache-serve.ts +238 -0
  266. package/src/rsc/response-error.ts +79 -12
  267. package/src/rsc/response-route-handler.ts +88 -188
  268. package/src/rsc/rsc-rendering.ts +98 -76
  269. package/src/rsc/runtime-warnings.ts +23 -10
  270. package/src/rsc/server-action.ts +281 -117
  271. package/src/rsc/ssr-setup.ts +16 -0
  272. package/src/rsc/transition-gate.ts +89 -0
  273. package/src/rsc/types.ts +23 -5
  274. package/src/runtime-env.ts +18 -0
  275. package/src/search-params.ts +35 -30
  276. package/src/segment-loader-promise.ts +31 -4
  277. package/src/segment-system.tsx +254 -143
  278. package/src/serialize.ts +243 -0
  279. package/src/server/context.ts +163 -51
  280. package/src/server/cookie-parse.ts +32 -0
  281. package/src/server/cookie-store.ts +80 -5
  282. package/src/server/handle-store.ts +21 -38
  283. package/src/server/loader-registry.ts +33 -42
  284. package/src/server/request-context.ts +287 -178
  285. package/src/ssr/index.tsx +21 -16
  286. package/src/static-handler.ts +10 -13
  287. package/src/testing/cache-status.ts +162 -0
  288. package/src/testing/collect-handle.ts +40 -0
  289. package/src/testing/dispatch.ts +701 -0
  290. package/src/testing/dom.entry.ts +22 -0
  291. package/src/testing/e2e/fixture.ts +188 -0
  292. package/src/testing/e2e/index.ts +128 -0
  293. package/src/testing/e2e/matchers.ts +35 -0
  294. package/src/testing/e2e/page-helpers.ts +272 -0
  295. package/src/testing/e2e/parity.ts +387 -0
  296. package/src/testing/e2e/server.ts +195 -0
  297. package/src/testing/flight-matchers.ts +97 -0
  298. package/src/testing/flight-normalize.ts +11 -0
  299. package/src/testing/flight-runtime.d.ts +57 -0
  300. package/src/testing/flight-tree.ts +682 -0
  301. package/src/testing/flight.entry.ts +52 -0
  302. package/src/testing/flight.ts +257 -0
  303. package/src/testing/generated-routes.ts +183 -0
  304. package/src/testing/index.ts +105 -0
  305. package/src/testing/internal/context.ts +371 -0
  306. package/src/testing/internal/flight-client-globals.ts +30 -0
  307. package/src/testing/internal/seed-vars.ts +54 -0
  308. package/src/testing/render-handler.ts +357 -0
  309. package/src/testing/render-route.tsx +581 -0
  310. package/src/testing/run-loader.ts +385 -0
  311. package/src/testing/run-middleware.ts +205 -0
  312. package/src/testing/run-transition-when.ts +164 -0
  313. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  314. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  315. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  316. package/src/testing/vitest-stubs/version.ts +5 -0
  317. package/src/testing/vitest.ts +305 -0
  318. package/src/theme/ThemeProvider.tsx +20 -58
  319. package/src/theme/ThemeScript.tsx +7 -9
  320. package/src/theme/constants.ts +52 -13
  321. package/src/theme/index.ts +0 -7
  322. package/src/theme/theme-context.ts +1 -5
  323. package/src/theme/theme-script.ts +22 -21
  324. package/src/theme/use-theme.ts +0 -3
  325. package/src/types/boundaries.ts +0 -35
  326. package/src/types/cache-types.ts +13 -4
  327. package/src/types/error-types.ts +30 -90
  328. package/src/types/global-namespace.ts +54 -41
  329. package/src/types/handler-context.ts +110 -62
  330. package/src/types/index.ts +3 -10
  331. package/src/types/loader-types.ts +11 -9
  332. package/src/types/request-scope.ts +112 -0
  333. package/src/types/route-config.ts +6 -50
  334. package/src/types/route-entry.ts +0 -6
  335. package/src/types/segments.ts +135 -14
  336. package/src/urls/include-helper.ts +9 -56
  337. package/src/urls/index.ts +1 -11
  338. package/src/urls/path-helper-types.ts +29 -12
  339. package/src/urls/path-helper.ts +17 -106
  340. package/src/urls/pattern-types.ts +36 -19
  341. package/src/urls/response-types.ts +22 -29
  342. package/src/urls/type-extraction.ts +58 -139
  343. package/src/urls/urls-function.ts +1 -19
  344. package/src/use-loader.tsx +292 -107
  345. package/src/vite/debug.ts +185 -0
  346. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  347. package/src/vite/discovery/discover-routers.ts +126 -85
  348. package/src/vite/discovery/discovery-errors.ts +194 -0
  349. package/src/vite/discovery/gate-state.ts +171 -0
  350. package/src/vite/discovery/prerender-collection.ts +96 -68
  351. package/src/vite/discovery/route-types-writer.ts +40 -84
  352. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  353. package/src/vite/discovery/state.ts +44 -0
  354. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  355. package/src/vite/index.ts +2 -0
  356. package/src/vite/inject-client-debug.ts +36 -0
  357. package/src/vite/plugin-types.ts +126 -8
  358. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  359. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  360. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  361. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  362. package/src/vite/plugins/expose-action-id.ts +48 -95
  363. package/src/vite/plugins/expose-id-utils.ts +88 -55
  364. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  365. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  366. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  367. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  368. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  369. package/src/vite/plugins/performance-tracks.ts +26 -25
  370. package/src/vite/plugins/refresh-cmd.ts +1 -1
  371. package/src/vite/plugins/use-cache-transform.ts +73 -83
  372. package/src/vite/plugins/version-injector.ts +40 -29
  373. package/src/vite/plugins/version-plugin.ts +37 -40
  374. package/src/vite/plugins/virtual-entries.ts +39 -25
  375. package/src/vite/rango.ts +109 -118
  376. package/src/vite/router-discovery.ts +718 -119
  377. package/src/vite/utils/ast-handler-extract.ts +26 -35
  378. package/src/vite/utils/banner.ts +1 -1
  379. package/src/vite/utils/bundle-analysis.ts +10 -15
  380. package/src/vite/utils/client-chunks.ts +184 -0
  381. package/src/vite/utils/directive-prologue.ts +40 -0
  382. package/src/vite/utils/forward-user-plugins.ts +171 -0
  383. package/src/vite/utils/manifest-utils.ts +4 -59
  384. package/src/vite/utils/package-resolution.ts +20 -52
  385. package/src/vite/utils/prerender-utils.ts +54 -39
  386. package/src/vite/utils/shared-utils.ts +90 -41
  387. package/src/browser/action-response-classifier.ts +0 -99
  388. package/src/browser/react/use-client-cache.ts +0 -58
  389. package/src/browser/shallow.ts +0 -40
  390. package/src/handles/index.ts +0 -7
  391. package/src/network-error-thrower.tsx +0 -23
  392. package/src/router/middleware-cookies.ts +0 -55
@@ -1,58 +1,26 @@
1
- /**
2
- * Navigation Snapshot
3
- *
4
- * Pure data type representing the navigation-specific state for partial requests.
5
- * Consolidates the header parsing, previous-route matching, intercept-context
6
- * detection, and segment ID filtering that previously lived inline in
7
- * createMatchContextForPartial (match-api.ts).
8
- *
9
- * resolveNavigation() is the factory: given a request + URL + current route key,
10
- * it returns a NavigationSnapshot (or null if no previous URL).
11
- */
12
-
13
1
  import type { RouteMatchResult } from "./pattern-matching.js";
14
2
 
15
- /**
16
- * Snapshot of navigation state for a partial (navigation/action) request.
17
- *
18
- * Contains the "where are we coming from?" data: previous route, intercept
19
- * source, client segment state, and derived flags.
20
- */
21
3
  export interface NavigationSnapshot {
22
- /** Previous page URL (from X-RSC-Router-Client-Path or Referer) */
23
4
  prevUrl: URL;
24
- /** Params from the previous route match */
25
5
  prevParams: Record<string, string>;
26
- /** Previous route match result (null if prev URL doesn't match any route) */
27
6
  prevMatch: RouteMatchResult | null;
28
7
 
29
- /** URL used as intercept context source */
30
8
  interceptContextUrl: URL;
31
- /** Route match for the intercept context URL */
32
9
  interceptContextMatch: RouteMatchResult | null;
33
10
 
34
- /** Raw segment IDs the client currently has */
35
11
  clientSegmentIds: string[];
36
- /** Set version for O(1) lookup */
37
12
  clientSegmentSet: Set<string>;
38
- /** Segment IDs filtered to remove parallel (.@) and loader (D\d+.) entries */
39
13
  filteredSegmentIds: string[];
40
14
 
41
- /** Whether client considers its cache stale */
42
15
  stale: boolean;
43
16
 
44
- /** Whether the intercept context route is the same as the current route */
45
17
  isSameRouteNavigation: boolean;
46
18
 
47
- /** Effective "from" URL (intercept source URL when present, else prevUrl) */
48
19
  effectiveFromUrl: URL;
49
- /** Effective "from" match (intercept source match when present, else prevMatch) */
50
20
  effectiveFromMatch: RouteMatchResult | null;
51
21
 
52
- /** Whether an intercept source header was present */
53
22
  hasInterceptSource: boolean;
54
23
 
55
- /** Whether an HMR request header was present */
56
24
  isHmr: boolean;
57
25
  }
58
26
 
@@ -60,23 +28,12 @@ export interface ResolveNavigationDeps {
60
28
  findMatch: (pathname: string) => RouteMatchResult | null;
61
29
  }
62
30
 
63
- /**
64
- * Resolve navigation state from a partial request.
65
- *
66
- * Returns null if no previous URL is available (required for partial navigation).
67
- *
68
- * @param request - The incoming HTTP request
69
- * @param url - Parsed URL of the request
70
- * @param currentRouteKey - Route key of the current (target) route match
71
- * @param deps - Dependencies (findMatch)
72
- */
73
31
  export function resolveNavigation(
74
32
  request: Request,
75
33
  url: URL,
76
34
  currentRouteKey: string,
77
35
  deps: ResolveNavigationDeps,
78
36
  ): NavigationSnapshot | null {
79
- // Parse client state from RSC request params/headers
80
37
  const clientSegmentIds =
81
38
  url.searchParams.get("_rsc_segments")?.split(",").filter(Boolean) || [];
82
39
  const stale = url.searchParams.get("_rsc_stale") === "true";
@@ -92,7 +49,6 @@ export function resolveNavigation(
92
49
  return null;
93
50
  }
94
51
 
95
- // Parse previous URL
96
52
  let prevUrl: URL;
97
53
  try {
98
54
  prevUrl = new URL(previousUrl, url.origin);
@@ -100,7 +56,6 @@ export function resolveNavigation(
100
56
  return null;
101
57
  }
102
58
 
103
- // Parse intercept context URL
104
59
  let interceptContextUrl: URL;
105
60
  try {
106
61
  interceptContextUrl = interceptSourceUrl
@@ -110,14 +65,12 @@ export function resolveNavigation(
110
65
  interceptContextUrl = prevUrl;
111
66
  }
112
67
 
113
- // Match previous and intercept context routes
114
68
  const prevMatch = deps.findMatch(prevUrl.pathname);
115
69
  const prevParams = prevMatch?.params || {};
116
70
  const interceptContextMatch = interceptSourceUrl
117
71
  ? deps.findMatch(interceptContextUrl.pathname)
118
72
  : prevMatch;
119
73
 
120
- // Derived state
121
74
  const isSameRouteNavigation = !!(
122
75
  interceptContextMatch && interceptContextMatch.routeKey === currentRouteKey
123
76
  );
@@ -128,7 +81,6 @@ export function resolveNavigation(
128
81
  ? interceptContextMatch
129
82
  : prevMatch;
130
83
 
131
- // Filter segment IDs: remove parallel (.@) and loader (D\d+.) entries
132
84
  const filteredSegmentIds = clientSegmentIds.filter((id) => {
133
85
  if (id.includes(".@")) return false;
134
86
  if (/D\d+\./.test(id)) return false;
@@ -155,9 +107,6 @@ export function resolveNavigation(
155
107
  };
156
108
  }
157
109
 
158
- /**
159
- * Test helper: create a NavigationSnapshot with sensible defaults and overrides.
160
- */
161
110
  export function createNavigationSnapshot(
162
111
  overrides?: Partial<NavigationSnapshot>,
163
112
  ): NavigationSnapshot {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared route-param comparison helpers.
3
+ */
4
+
5
+ /**
6
+ * Shallow equality for two route-param records. Same-reference is a fast path;
7
+ * otherwise compares key count then each value.
8
+ */
9
+ export function paramsEqual(
10
+ a: Record<string, string>,
11
+ b: Record<string, string>,
12
+ ): boolean {
13
+ if (a === b) return true;
14
+
15
+ const keysA = Object.keys(a);
16
+ if (keysA.length !== Object.keys(b).length) return false;
17
+
18
+ for (const key of keysA) {
19
+ if (a[key] !== b[key]) return false;
20
+ }
21
+
22
+ return true;
23
+ }
@@ -7,6 +7,7 @@
7
7
  import type { RouteEntry, TrailingSlashMode } from "../types";
8
8
  import type { EntryData } from "../server/context";
9
9
  import { debugLog, isRouterDebugEnabled } from "./logging.js";
10
+ import { escapeRegExp } from "../regex-escape.js";
10
11
  import { safeDecodeURIComponent } from "./url-params.js";
11
12
 
12
13
  /**
@@ -33,13 +34,6 @@ export interface ParsedSegment {
33
34
  */
34
35
  export function parsePattern(pattern: string): ParsedSegment[] {
35
36
  const segments: ParsedSegment[] = [];
36
- // Match: /segment where segment can be:
37
- // - static text
38
- // - :param
39
- // - :param?
40
- // - :param(a|b)
41
- // - :param(a|b)?
42
- // - *
43
37
  const segmentRegex =
44
38
  /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
45
39
 
@@ -81,7 +75,6 @@ export function parsePattern(pattern: string): ParsedSegment[] {
81
75
  export interface CompiledPattern {
82
76
  regex: RegExp;
83
77
  paramNames: string[];
84
- optionalParams: Set<string>;
85
78
  hasTrailingSlash: boolean;
86
79
  /**
87
80
  * Param-name → allowed values for constrained params (e.g. `:lang(en|gb)`).
@@ -149,7 +142,6 @@ export function compilePattern(pattern: string): CompiledPattern {
149
142
 
150
143
  const segments = parsePattern(normalizedPattern);
151
144
  const paramNames: string[] = [];
152
- const optionalParams = new Set<string>();
153
145
  let constraints: Record<string, string[]> | undefined;
154
146
 
155
147
  let regexPattern = "";
@@ -160,7 +152,7 @@ export function compilePattern(pattern: string): CompiledPattern {
160
152
  regexPattern += "/(.*)";
161
153
  } else if (segment.type === "param") {
162
154
  paramNames.push(segment.value);
163
- const suffixPattern = segment.suffix ? escapeRegex(segment.suffix) : "";
155
+ const suffixPattern = segment.suffix ? escapeRegExp(segment.suffix) : "";
164
156
  // Constrained params capture anything here; the allowed values are
165
157
  // checked post-decode in findMatch so URL-encoded constraint values
166
158
  // (e.g. `:lang(en GB)` via `/en%20GB`) still match.
@@ -171,7 +163,6 @@ export function compilePattern(pattern: string): CompiledPattern {
171
163
  }
172
164
 
173
165
  if (segment.optional) {
174
- optionalParams.add(segment.value);
175
166
  // Optional: make the whole /segment optional
176
167
  regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
177
168
  } else {
@@ -179,15 +170,28 @@ export function compilePattern(pattern: string): CompiledPattern {
179
170
  }
180
171
  } else {
181
172
  // Static segment
182
- regexPattern += `/${escapeRegex(segment.value)}`;
173
+ regexPattern += `/${escapeRegExp(segment.value)}`;
183
174
  }
184
175
  }
185
176
 
186
- // Handle root path
187
177
  if (regexPattern === "") {
188
178
  regexPattern = "/";
189
179
  }
190
180
 
181
+ // Patterns of only optional segments (e.g. `/:locale?`, `/:a?/:b?`) need
182
+ // an explicit `/` alternative so a bare `/` matches the absent form. The
183
+ // optional template `(?:/X)?` matches `/X` or empty string, but pathnames
184
+ // are never empty. Arises from `include("/:locale?", routes)` + inner
185
+ // `path("/")`. Skip when an explicit trailing slash already anchors the
186
+ // match.
187
+ const hasOnlyOptionalSegments =
188
+ !hasTrailingSlash &&
189
+ segments.length > 0 &&
190
+ segments.every((segment) => segment.type === "param" && segment.optional);
191
+ if (hasOnlyOptionalSegments) {
192
+ regexPattern = `(?:/|${regexPattern})`;
193
+ }
194
+
191
195
  // Add trailing slash to regex if pattern has one
192
196
  if (hasTrailingSlash) {
193
197
  regexPattern += "/";
@@ -196,7 +200,6 @@ export function compilePattern(pattern: string): CompiledPattern {
196
200
  return {
197
201
  regex: new RegExp(`^${regexPattern}$`),
198
202
  paramNames,
199
- optionalParams,
200
203
  hasTrailingSlash,
201
204
  ...(constraints ? { constraints } : {}),
202
205
  };
@@ -205,7 +208,9 @@ export function compilePattern(pattern: string): CompiledPattern {
205
208
  /**
206
209
  * Validate decoded params against a compiled pattern's constraints.
207
210
  * Returns false if any constrained param has a non-empty value not in the
208
- * allowed list (empty-string = absent optional, which is allowed).
211
+ * allowed list. Absent optionals (key missing or `undefined`) are allowed;
212
+ * `""` is also tolerated as "absent" so user-provided params or fixtures
213
+ * that pass empty strings explicitly behave the same way.
209
214
  */
210
215
  function satisfiesConstraints(
211
216
  params: Record<string, string>,
@@ -226,10 +231,24 @@ function satisfiesConstraints(
226
231
  }
227
232
 
228
233
  /**
229
- * Escape special regex characters in a string
234
+ * Build the named-params record from a regex match. Optional segments that
235
+ * didn't capture leave the corresponding group `undefined`; we skip those
236
+ * keys so `ctx.params.<name>` reads as `undefined` rather than `""`. This
237
+ * keeps the runtime aligned with the `ExtractParams` type and matches the
238
+ * trie matcher's contract (see `trie-matching.ts:validateAndBuild`).
230
239
  */
231
- function escapeRegex(str: string): string {
232
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
240
+ function buildParamsFromMatch(
241
+ match: RegExpExecArray,
242
+ paramNames: string[],
243
+ ): Record<string, string> {
244
+ const params: Record<string, string> = {};
245
+ paramNames.forEach((name, index) => {
246
+ const captured = match[index + 1];
247
+ if (captured !== undefined) {
248
+ params[name] = safeDecodeURIComponent(captured);
249
+ }
250
+ });
251
+ return params;
233
252
  }
234
253
 
235
254
  /**
@@ -248,43 +267,58 @@ function escapeRegex(str: string): string {
248
267
  export function extractStaticPrefix(pattern: string): string {
249
268
  if (!pattern || pattern === "/") return "";
250
269
 
251
- // Find the first occurrence of : or *
252
- const paramIndex = pattern.indexOf(":");
253
- const wildcardIndex = pattern.indexOf("*");
254
-
255
- let cutIndex = -1;
256
- if (paramIndex !== -1 && wildcardIndex !== -1) {
257
- cutIndex = Math.min(paramIndex, wildcardIndex);
258
- } else if (paramIndex !== -1) {
259
- cutIndex = paramIndex;
260
- } else if (wildcardIndex !== -1) {
261
- cutIndex = wildcardIndex;
262
- }
263
-
264
- if (cutIndex === -1) {
265
- // No params or wildcards - entire pattern is static
266
- return pattern;
270
+ // Walk segments and stop at the first that is a real param (`:name`) or a
271
+ // wildcard (`*`). A literal `:` or `*` not at a segment boundary (e.g. the
272
+ // `a:b` in `/a:b/c/:id`, or `tel:+1`) is a STATIC segment and must NOT
273
+ // terminate the prefix — `pattern.indexOf(":")` misread it as a param marker,
274
+ // returning "" and dropping the findMatch fast-skip optimization for that
275
+ // entry on every request. Classification mirrors parsePattern: a leading `:`
276
+ // marks a param, a leading `*` marks a wildcard.
277
+ const hasLeadingSlash = pattern.startsWith("/");
278
+ const body = hasLeadingSlash ? pattern.slice(1) : pattern;
279
+ const segments = body.split("/");
280
+
281
+ const staticSegments: string[] = [];
282
+ for (const segment of segments) {
283
+ if (segment.startsWith(":") || segment.startsWith("*")) {
284
+ break;
285
+ }
286
+ staticSegments.push(segment);
267
287
  }
268
288
 
269
- if (cutIndex === 0) {
270
- // Pattern starts with : or * - no static prefix
271
- return "";
272
- }
289
+ // No leading static segment (first segment is a param/wildcard) -> no prefix.
290
+ if (staticSegments.length === 0) return "";
291
+ // Every segment was static (no param/wildcard) -> the whole pattern is the
292
+ // prefix. Preserve a trailing slash only when it existed in the input; a
293
+ // split of "/a/b" yields ["a","b"] (no empty tail) so a re-join is exact.
294
+ if (staticSegments.length === segments.length) return pattern;
273
295
 
274
- // Find the last / before the param
275
- const lastSlash = pattern.lastIndexOf("/", cutIndex - 1);
276
- if (lastSlash === -1 || lastSlash === 0) {
277
- return "";
278
- }
296
+ const prefix = staticSegments.join("/");
297
+ return hasLeadingSlash ? "/" + prefix : prefix;
298
+ }
279
299
 
280
- return pattern.slice(0, lastSlash);
300
+ /**
301
+ * Join a URL prefix to a sub-prefix, collapsing the duplicate slash when the
302
+ * base ends with "/" and the sub-prefix starts with "/". This mirrors the
303
+ * canonical join in `include()` (urls/include-helper.ts) and `runWithPrefixes`
304
+ * (server/context.ts) so a nested lazy include's runtime staticPrefix matches
305
+ * the build-time trie's `sp` (e.g. `include("/parent/", …)` containing
306
+ * `include("/child", …)` resolves to `/parent/child`, not `/parent//child`).
307
+ */
308
+ export function joinPrefix(base: string | undefined, prefix: string): string {
309
+ if (!base) return prefix;
310
+ return base.endsWith("/") && prefix.startsWith("/")
311
+ ? base + prefix.slice(1)
312
+ : base + prefix;
281
313
  }
282
314
 
283
315
  /**
284
316
  * Match a pathname against registered routes
285
317
  *
286
- * Note: Optional params that are absent in the path will have empty string value.
287
- * Use the pattern definition to determine if a param is optional.
318
+ * Note: Optional params that are absent in the path are omitted from the
319
+ * returned `params` (read as `undefined`), matching the trie matcher and
320
+ * the `ExtractParams<"/:locale?/...">` type. Use the pattern definition to
321
+ * determine which keys are optional.
288
322
  *
289
323
  * Trailing slash handling (priority order):
290
324
  * 1. Per-route `trailingSlash` config from route()
@@ -302,10 +336,7 @@ export interface RouteMatchResult<TEnv = any> {
302
336
  entry: RouteEntry<TEnv>;
303
337
  routeKey: string;
304
338
  params: Record<string, string>;
305
- optionalParams: Set<string>;
306
339
  redirectTo?: string;
307
- /** Ancestry shortCodes for layout pruning (from trie match) */
308
- ancestry?: string[];
309
340
  /** Route has pre-rendered data available (from trie) */
310
341
  pr?: true;
311
342
  /** Passthrough: handler kept for live fallback on unknown params (from trie) */
@@ -383,8 +414,6 @@ export function findMatch<TEnv>(
383
414
  : pathname + "/";
384
415
 
385
416
  for (const entry of routesEntries) {
386
- // Short-circuit: skip entry if pathname doesn't start with static prefix
387
- // staticPrefix is pre-computed at registration time, so this is O(1)
388
417
  if (entry.staticPrefix && !pathname.startsWith(entry.staticPrefix)) {
389
418
  if (effectiveDebug) {
390
419
  debugStats.entriesSkipped++;
@@ -396,8 +425,6 @@ export function findMatch<TEnv>(
396
425
  continue;
397
426
  }
398
427
 
399
- // Check if this is a lazy entry that needs evaluation
400
- // When staticPrefix matches but routes are not yet populated, signal caller to evaluate
401
428
  if (entry.lazy && !entry.lazyEvaluated) {
402
429
  if (effectiveDebug) {
403
430
  debugLog("findMatch", "lazy entry requires evaluation", {
@@ -418,7 +445,6 @@ export function findMatch<TEnv>(
418
445
  debugStats.routesChecked++;
419
446
  }
420
447
 
421
- // Join prefix and pattern, handling edge cases
422
448
  let fullPattern: string;
423
449
  if (entry.prefix === "" || entry.prefix === "/") {
424
450
  fullPattern = pattern;
@@ -428,19 +454,12 @@ export function findMatch<TEnv>(
428
454
  fullPattern = entry.prefix + pattern;
429
455
  }
430
456
 
431
- const {
432
- regex,
433
- paramNames,
434
- optionalParams,
435
- hasTrailingSlash,
436
- constraints,
437
- } = getCompiledPattern(fullPattern);
457
+ const { regex, paramNames, hasTrailingSlash, constraints } =
458
+ getCompiledPattern(fullPattern);
438
459
 
439
- // Get trailing slash mode for this route (per-route config or pattern-based)
440
460
  const trailingSlashMode: TrailingSlashMode | undefined =
441
461
  entry.trailingSlash?.[routeKey];
442
462
 
443
- // Prerender flag from entry metadata (set by urls() for prerender handlers)
444
463
  const prFlag = entry.prerenderRouteKeys?.has(routeKey)
445
464
  ? { pr: true as const }
446
465
  : {};
@@ -448,16 +467,10 @@ export function findMatch<TEnv>(
448
467
  ? { pt: true as const }
449
468
  : {};
450
469
 
451
- // Try exact match first
452
470
  const match = regex.exec(pathname);
453
471
  if (match) {
454
- const params: Record<string, string> = {};
455
- paramNames.forEach((name, index) => {
456
- params[name] = safeDecodeURIComponent(match[index + 1] ?? "");
457
- });
472
+ const params = buildParamsFromMatch(match, paramNames);
458
473
 
459
- // Validate constraints against decoded values; a failure falls
460
- // through to the next route so other patterns can still match.
461
474
  if (!satisfiesConstraints(params, constraints)) {
462
475
  continue;
463
476
  }
@@ -470,29 +483,24 @@ export function findMatch<TEnv>(
470
483
  });
471
484
  }
472
485
 
473
- // Check if trailing slash mode requires redirect even on exact match
474
486
  if (
475
487
  trailingSlashMode === "always" &&
476
488
  !pathnameHasTrailingSlash &&
477
489
  pathname !== "/"
478
490
  ) {
479
- // Mode says always have trailing slash, but pathname doesn't have it
480
491
  return {
481
492
  entry,
482
493
  routeKey,
483
494
  params,
484
- optionalParams,
485
495
  redirectTo: pathname + "/",
486
496
  ...prFlag,
487
497
  ...ptFlag,
488
498
  };
489
499
  } else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
490
- // Mode says never have trailing slash, but pathname has it
491
500
  return {
492
501
  entry,
493
502
  routeKey,
494
503
  params,
495
- optionalParams,
496
504
  redirectTo: pathname.slice(0, -1),
497
505
  ...prFlag,
498
506
  ...ptFlag,
@@ -503,43 +511,33 @@ export function findMatch<TEnv>(
503
511
  entry,
504
512
  routeKey,
505
513
  params,
506
- optionalParams,
507
514
  ...prFlag,
508
515
  ...ptFlag,
509
516
  };
510
517
  }
511
518
 
512
- // Try alternate pathname (opposite trailing slash)
513
519
  const altMatch = regex.exec(alternatePathname);
514
520
  if (altMatch) {
515
- const params: Record<string, string> = {};
516
- paramNames.forEach((name, index) => {
517
- params[name] = safeDecodeURIComponent(altMatch[index + 1] ?? "");
518
- });
521
+ const params = buildParamsFromMatch(altMatch, paramNames);
519
522
 
520
523
  if (!satisfiesConstraints(params, constraints)) {
521
524
  continue;
522
525
  }
523
526
 
524
- // Determine redirect behavior based on mode
525
527
  if (trailingSlashMode === "ignore") {
526
- // Match without redirect
527
528
  return {
528
529
  entry,
529
530
  routeKey,
530
531
  params,
531
- optionalParams,
532
532
  ...prFlag,
533
533
  ...ptFlag,
534
534
  };
535
535
  } else if (trailingSlashMode === "never") {
536
- // Redirect to no trailing slash
537
536
  if (pathnameHasTrailingSlash) {
538
537
  return {
539
538
  entry,
540
539
  routeKey,
541
540
  params,
542
- optionalParams,
543
541
  redirectTo: alternatePathname,
544
542
  ...prFlag,
545
543
  ...ptFlag,
@@ -549,18 +547,15 @@ export function findMatch<TEnv>(
549
547
  entry,
550
548
  routeKey,
551
549
  params,
552
- optionalParams,
553
550
  ...prFlag,
554
551
  ...ptFlag,
555
552
  };
556
553
  } else if (trailingSlashMode === "always") {
557
- // Redirect to with trailing slash
558
554
  if (!pathnameHasTrailingSlash) {
559
555
  return {
560
556
  entry,
561
557
  routeKey,
562
558
  params,
563
- optionalParams,
564
559
  redirectTo: alternatePathname,
565
560
  ...prFlag,
566
561
  ...ptFlag,
@@ -570,13 +565,10 @@ export function findMatch<TEnv>(
570
565
  entry,
571
566
  routeKey,
572
567
  params,
573
- optionalParams,
574
568
  ...prFlag,
575
569
  ...ptFlag,
576
570
  };
577
571
  } else {
578
- // No explicit mode - use pattern-based detection
579
- // Redirect to canonical form (what the pattern defines)
580
572
  const canonicalPath = hasTrailingSlash
581
573
  ? alternatePathname
582
574
  : pathname.slice(0, -1);
@@ -584,7 +576,6 @@ export function findMatch<TEnv>(
584
576
  entry,
585
577
  routeKey,
586
578
  params,
587
- optionalParams,
588
579
  redirectTo: canonicalPath,
589
580
  ...prFlag,
590
581
  ...ptFlag,
@@ -605,7 +596,7 @@ export function* traverseBack(entry: EntryData): Generator<EntryData> {
605
596
  let current: EntryData | null = entry;
606
597
  const items = [] as EntryData[];
607
598
  while (current !== null) {
608
- items.push(current); // Move up to next parent
599
+ items.push(current);
609
600
  current = current.parent;
610
601
  }
611
602
  for (let i = items.length - 1; i >= 0; i--) {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Resolve the prefetch cache TTL once, at router init, into the three derived
3
+ * values the rest of the router consumes: seconds (for the Cache-Control
4
+ * max-age), milliseconds (for the client-side in-memory cache), and the
5
+ * Cache-Control header string (or false when caching is disabled).
6
+ *
7
+ * `false` disables prefetch caching (seconds 0). A non-finite input
8
+ * (NaN/Infinity, e.g. from an env-derived number that failed to parse) is
9
+ * treated as the default rather than producing a malformed
10
+ * `Cache-Control: max-age=NaN` header; CDNs/browsers ignore such a directive,
11
+ * which would silently disable caching on EVERY prefetch response. Negative
12
+ * finite values clamp to 0 (disabled).
13
+ *
14
+ * Policy note: this is the prefetch-TTL policy specifically. Other finite-number
15
+ * guards in the codebase deliberately differ — defer.ts treats Infinity as an
16
+ * intentional disable, and profile-registry.ts THROWS on non-finite/negative ttl
17
+ * at config time. They are NOT the same guard, so don't unify them.
18
+ */
19
+ export interface ResolvedPrefetchCacheTTL {
20
+ /** TTL in seconds for the Cache-Control max-age directive. */
21
+ seconds: number;
22
+ /** TTL in milliseconds for the client-side in-memory prefetch cache. */
23
+ ms: number;
24
+ /** Cache-Control header value, or false when caching is disabled. */
25
+ cacheControl: string | false;
26
+ }
27
+
28
+ const DEFAULT_PREFETCH_CACHE_TTL_SECONDS = 300;
29
+
30
+ export function resolvePrefetchCacheTTL(
31
+ rawTTL: number | false | undefined,
32
+ ): ResolvedPrefetchCacheTTL {
33
+ let seconds: number;
34
+ if (rawTTL === false) {
35
+ seconds = 0;
36
+ } else if (rawTTL === undefined) {
37
+ seconds = DEFAULT_PREFETCH_CACHE_TTL_SECONDS;
38
+ } else if (typeof rawTTL === "number" && Number.isFinite(rawTTL)) {
39
+ seconds = Math.max(0, Math.floor(rawTTL));
40
+ } else {
41
+ // Non-finite (NaN/Infinity): fall back to the default rather than emit a
42
+ // malformed max-age=NaN/Infinity that CDNs and browsers reject.
43
+ seconds = DEFAULT_PREFETCH_CACHE_TTL_SECONDS;
44
+ }
45
+
46
+ return {
47
+ seconds,
48
+ ms: seconds * 1000,
49
+ cacheControl: seconds === 0 ? false : `private, max-age=${seconds}`,
50
+ };
51
+ }