@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.140

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 (460) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +432 -7
  3. package/dist/bin/rango.js +2073 -213
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +7258 -2714
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +140 -67
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +329 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +487 -0
  12. package/skills/caching/SKILL.md +357 -25
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +246 -0
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +16 -10
  19. package/skills/document-cache/SKILL.md +87 -62
  20. package/skills/fonts/SKILL.md +6 -4
  21. package/skills/handler-use/SKILL.md +364 -0
  22. package/skills/hooks/SKILL.md +557 -79
  23. package/skills/host-router/SKILL.md +320 -0
  24. package/skills/i18n/SKILL.md +276 -0
  25. package/skills/intercept/SKILL.md +207 -15
  26. package/skills/layout/SKILL.md +146 -6
  27. package/skills/links/SKILL.md +304 -25
  28. package/skills/loader/SKILL.md +616 -54
  29. package/skills/middleware/SKILL.md +217 -37
  30. package/skills/migrate-nextjs/SKILL.md +611 -0
  31. package/skills/migrate-react-router/SKILL.md +927 -0
  32. package/skills/mime-routes/SKILL.md +42 -11
  33. package/skills/observability/SKILL.md +194 -0
  34. package/skills/parallel/SKILL.md +284 -3
  35. package/skills/ppr/SKILL.md +426 -0
  36. package/skills/prerender/SKILL.md +437 -52
  37. package/skills/rango/SKILL.md +369 -22
  38. package/skills/react-compiler/SKILL.md +168 -0
  39. package/skills/response-routes/SKILL.md +263 -121
  40. package/skills/route/SKILL.md +350 -21
  41. package/skills/router-setup/SKILL.md +246 -33
  42. package/skills/scripts/SKILL.md +179 -0
  43. package/skills/server-actions/SKILL.md +775 -0
  44. package/skills/shell-manifest/SKILL.md +185 -0
  45. package/skills/streams-and-websockets/SKILL.md +283 -0
  46. package/skills/tailwind/SKILL.md +27 -3
  47. package/skills/testing/SKILL.md +126 -222
  48. package/skills/testing/bindings.md +103 -0
  49. package/skills/testing/cache-prerender.md +127 -0
  50. package/skills/testing/client-components.md +124 -0
  51. package/skills/testing/e2e-parity.md +125 -0
  52. package/skills/testing/flight.md +91 -0
  53. package/skills/testing/handles.md +131 -0
  54. package/skills/testing/loader.md +128 -0
  55. package/skills/testing/middleware.md +99 -0
  56. package/skills/testing/render-handler.md +122 -0
  57. package/skills/testing/response-routes.md +95 -0
  58. package/skills/testing/reverse-and-types.md +85 -0
  59. package/skills/testing/server-actions.md +107 -0
  60. package/skills/testing/server-tree.md +128 -0
  61. package/skills/testing/setup.md +123 -0
  62. package/skills/theme/SKILL.md +9 -8
  63. package/skills/typesafety/SKILL.md +532 -103
  64. package/skills/use-cache/SKILL.md +367 -0
  65. package/skills/vercel/SKILL.md +128 -0
  66. package/skills/view-transitions/SKILL.md +337 -0
  67. package/src/__augment-tests__/augment.ts +81 -0
  68. package/src/__augment-tests__/augmented.check.ts +116 -0
  69. package/src/__internal.ts +77 -44
  70. package/src/bin/rango.ts +312 -15
  71. package/src/browser/action-coordinator.ts +114 -0
  72. package/src/browser/action-fence.ts +47 -0
  73. package/src/browser/app-shell.ts +39 -0
  74. package/src/browser/app-version.ts +14 -0
  75. package/src/browser/connection-warmup.ts +134 -0
  76. package/src/browser/cookie-name.ts +140 -0
  77. package/src/browser/event-controller.ts +293 -202
  78. package/src/browser/history-state.ts +101 -0
  79. package/src/browser/index.ts +3 -3
  80. package/src/browser/intercept-utils.ts +52 -0
  81. package/src/browser/invalidate-client-cache.ts +52 -0
  82. package/src/browser/link-interceptor.ts +24 -4
  83. package/src/browser/logging.ts +11 -0
  84. package/src/browser/merge-segment-loaders.ts +20 -12
  85. package/src/browser/navigation-bridge.ts +385 -576
  86. package/src/browser/navigation-client.ts +245 -75
  87. package/src/browser/navigation-store-handle.ts +38 -0
  88. package/src/browser/navigation-store.ts +184 -118
  89. package/src/browser/navigation-transaction.ts +247 -0
  90. package/src/browser/network-error-handler.ts +88 -0
  91. package/src/browser/partial-update.ts +412 -364
  92. package/src/browser/prefetch/cache.ts +359 -0
  93. package/src/browser/prefetch/fetch.ts +452 -0
  94. package/src/browser/prefetch/observer.ts +65 -0
  95. package/src/browser/prefetch/policy.ts +48 -0
  96. package/src/browser/prefetch/queue.ts +209 -0
  97. package/src/browser/prefetch/resource-ready.ts +77 -0
  98. package/src/browser/rango-state.ts +194 -0
  99. package/src/browser/react/Link.tsx +275 -68
  100. package/src/browser/react/NavigationProvider.tsx +265 -109
  101. package/src/browser/react/ScrollRestoration.tsx +10 -6
  102. package/src/browser/react/context.ts +11 -0
  103. package/src/browser/react/filter-segment-order.ts +70 -0
  104. package/src/browser/react/index.ts +0 -48
  105. package/src/browser/react/location-state-shared.ts +272 -60
  106. package/src/browser/react/location-state.ts +90 -20
  107. package/src/browser/react/mount-context.ts +6 -1
  108. package/src/browser/react/nonce-context.ts +23 -0
  109. package/src/browser/react/shallow-equal.ts +27 -0
  110. package/src/browser/react/use-action.ts +35 -66
  111. package/src/browser/react/use-handle.ts +39 -126
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +39 -13
  114. package/src/browser/react/use-navigation.ts +53 -69
  115. package/src/browser/react/use-params.ts +75 -0
  116. package/src/browser/react/use-pathname.ts +47 -0
  117. package/src/browser/react/use-reverse.ts +106 -0
  118. package/src/browser/react/use-router.ts +98 -0
  119. package/src/browser/react/use-search-params.ts +51 -0
  120. package/src/browser/react/use-segments.ts +72 -99
  121. package/src/browser/response-adapter.ts +164 -0
  122. package/src/browser/rsc-router.tsx +300 -72
  123. package/src/browser/scroll-restoration.ts +138 -50
  124. package/src/browser/segment-reconciler.ts +243 -0
  125. package/src/browser/segment-structure-assert.ts +17 -1
  126. package/src/browser/server-action-bridge.ts +668 -613
  127. package/src/browser/types.ts +223 -51
  128. package/src/browser/validate-redirect-origin.ts +56 -0
  129. package/src/build/collect-fallback-refs.ts +107 -0
  130. package/src/build/generate-manifest.ts +252 -161
  131. package/src/build/generate-route-types.ts +41 -1038
  132. package/src/build/index.ts +12 -7
  133. package/src/build/prefix-tree-utils.ts +123 -0
  134. package/src/build/route-trie.ts +225 -42
  135. package/src/build/route-types/ast-helpers.ts +25 -0
  136. package/src/build/route-types/ast-route-extraction.ts +105 -0
  137. package/src/build/route-types/codegen.ts +113 -0
  138. package/src/build/route-types/include-resolution.ts +812 -0
  139. package/src/build/route-types/param-extraction.ts +51 -0
  140. package/src/build/route-types/per-module-writer.ts +144 -0
  141. package/src/build/route-types/router-processing.ts +695 -0
  142. package/src/build/route-types/scan-filter.ts +85 -0
  143. package/src/build/route-types/source-scan.ts +216 -0
  144. package/src/build/runtime-discovery.ts +223 -0
  145. package/src/cache/background-task.ts +34 -0
  146. package/src/cache/cache-error.ts +104 -0
  147. package/src/cache/cache-key-utils.ts +60 -0
  148. package/src/cache/cache-policy.ts +199 -0
  149. package/src/cache/cache-runtime.ts +525 -0
  150. package/src/cache/cache-scope.ts +298 -332
  151. package/src/cache/cache-tag.ts +103 -0
  152. package/src/cache/cf/cf-base64.ts +33 -0
  153. package/src/cache/cf/cf-cache-constants.ts +127 -0
  154. package/src/cache/cf/cf-cache-store.ts +2500 -158
  155. package/src/cache/cf/cf-cache-types.ts +349 -0
  156. package/src/cache/cf/cf-kv-utils.ts +46 -0
  157. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  158. package/src/cache/cf/index.ts +17 -17
  159. package/src/cache/document-cache.ts +199 -92
  160. package/src/cache/handle-capture.ts +81 -0
  161. package/src/cache/handle-snapshot.ts +111 -0
  162. package/src/cache/index.ts +29 -35
  163. package/src/cache/memory-segment-store.ts +363 -30
  164. package/src/cache/profile-registry.ts +88 -0
  165. package/src/cache/read-through-swr.ts +178 -0
  166. package/src/cache/segment-codec.ts +248 -0
  167. package/src/cache/shell-cache.ts +386 -0
  168. package/src/cache/tag-invalidation.ts +230 -0
  169. package/src/cache/taint.ts +153 -0
  170. package/src/cache/types.ts +156 -211
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1102 -0
  173. package/src/client.rsc.tsx +43 -21
  174. package/src/client.tsx +131 -347
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +109 -0
  177. package/src/component-utils.ts +23 -4
  178. package/src/components/DefaultDocument.tsx +13 -3
  179. package/src/context-var.ts +168 -0
  180. package/src/debug.ts +19 -9
  181. package/src/decode-loader-results.ts +52 -0
  182. package/src/defer.ts +185 -0
  183. package/src/deps/ssr.ts +0 -1
  184. package/src/encode-kv.ts +49 -0
  185. package/src/errors.ts +106 -10
  186. package/src/escape-script.ts +52 -0
  187. package/src/handle.ts +110 -35
  188. package/src/handles/MetaTags.tsx +83 -59
  189. package/src/handles/Scripts.tsx +183 -0
  190. package/src/handles/breadcrumbs.ts +93 -0
  191. package/src/handles/deferred-resolution.ts +127 -0
  192. package/src/handles/is-thenable.ts +18 -0
  193. package/src/handles/meta.ts +44 -53
  194. package/src/handles/script.ts +244 -0
  195. package/src/host/cookie-handler.ts +20 -65
  196. package/src/host/errors.ts +21 -30
  197. package/src/host/index.ts +13 -9
  198. package/src/host/pattern-matcher.ts +50 -79
  199. package/src/host/router.ts +151 -121
  200. package/src/host/testing.ts +45 -32
  201. package/src/host/types.ts +52 -11
  202. package/src/host/utils.ts +2 -2
  203. package/src/href-client.ts +192 -57
  204. package/src/index.rsc.ts +177 -35
  205. package/src/index.ts +255 -71
  206. package/src/internal-debug.ts +9 -2
  207. package/src/loader-store.ts +500 -0
  208. package/src/loader.rsc.ts +31 -99
  209. package/src/loader.ts +30 -12
  210. package/src/missing-id-error.ts +68 -0
  211. package/src/outlet-context.ts +1 -1
  212. package/src/outlet-provider.tsx +41 -0
  213. package/src/prerender/param-hash.ts +16 -14
  214. package/src/prerender/store.ts +121 -21
  215. package/src/prerender.ts +460 -26
  216. package/src/redirect-origin.ts +100 -0
  217. package/src/regex-escape.ts +8 -0
  218. package/src/render-error-thrower.tsx +20 -0
  219. package/src/response-utils.ts +62 -0
  220. package/src/reverse.ts +198 -128
  221. package/src/root-error-boundary.tsx +42 -48
  222. package/src/route-content-wrapper.tsx +22 -77
  223. package/src/route-definition/dsl-helpers.ts +1116 -0
  224. package/src/route-definition/helper-factories.ts +88 -0
  225. package/src/route-definition/helpers-types.ts +505 -0
  226. package/src/route-definition/index.ts +54 -0
  227. package/src/route-definition/redirect.ts +134 -0
  228. package/src/route-definition/resolve-handler-use.ts +160 -0
  229. package/src/route-definition/use-item-types.ts +29 -0
  230. package/src/route-definition.ts +1 -1481
  231. package/src/route-map-builder.ts +82 -144
  232. package/src/route-name.ts +53 -0
  233. package/src/route-types.ts +71 -45
  234. package/src/router/basename.ts +14 -0
  235. package/src/router/content-negotiation.ts +263 -0
  236. package/src/router/debug-manifest.ts +72 -0
  237. package/src/router/error-handling.ts +54 -27
  238. package/src/router/find-match.ts +245 -0
  239. package/src/router/handler-context.ts +377 -125
  240. package/src/router/instrument.ts +350 -0
  241. package/src/router/intercept-resolution.ts +59 -28
  242. package/src/router/lazy-includes.ts +254 -0
  243. package/src/router/loader-resolution.ts +421 -157
  244. package/src/router/logging.ts +106 -6
  245. package/src/router/manifest.ts +131 -57
  246. package/src/router/match-api.ts +167 -246
  247. package/src/router/match-context.ts +4 -24
  248. package/src/router/match-handlers.ts +440 -0
  249. package/src/router/match-middleware/background-revalidation.ts +117 -93
  250. package/src/router/match-middleware/cache-lookup.ts +297 -150
  251. package/src/router/match-middleware/cache-store.ts +123 -51
  252. package/src/router/match-middleware/intercept-resolution.ts +44 -43
  253. package/src/router/match-middleware/segment-resolution.ts +64 -22
  254. package/src/router/match-pipelines.ts +11 -87
  255. package/src/router/match-result.ts +121 -50
  256. package/src/router/metrics.ts +219 -28
  257. package/src/router/middleware-types.ts +93 -0
  258. package/src/router/middleware.ts +505 -441
  259. package/src/router/navigation-snapshot.ts +133 -0
  260. package/src/router/params-util.ts +23 -0
  261. package/src/router/parse-pattern.ts +115 -0
  262. package/src/router/pattern-matching.ts +311 -142
  263. package/src/router/prefetch-cache-ttl.ts +51 -0
  264. package/src/router/prefetch-limits.ts +37 -0
  265. package/src/router/prerender-match.ts +547 -0
  266. package/src/router/preview-match.ts +102 -0
  267. package/src/router/request-classification.ts +278 -0
  268. package/src/router/revalidation.ts +203 -62
  269. package/src/router/route-snapshot.ts +246 -0
  270. package/src/router/router-context.ts +45 -48
  271. package/src/router/router-interfaces.ts +554 -0
  272. package/src/router/router-options.ts +779 -0
  273. package/src/router/router-registry.ts +21 -0
  274. package/src/router/segment-resolution/fresh.ts +772 -0
  275. package/src/router/segment-resolution/helpers.ts +348 -0
  276. package/src/router/segment-resolution/loader-cache.ts +250 -0
  277. package/src/router/segment-resolution/loader-mask.ts +44 -0
  278. package/src/router/segment-resolution/revalidation.ts +1331 -0
  279. package/src/router/segment-resolution/static-store.ts +81 -0
  280. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  281. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  282. package/src/router/segment-resolution.ts +25 -1354
  283. package/src/router/segment-wrappers.ts +292 -0
  284. package/src/router/state-cookie-name.ts +33 -0
  285. package/src/router/substitute-pattern-params.ts +75 -0
  286. package/src/router/telemetry-otel.ts +261 -0
  287. package/src/router/telemetry.ts +377 -0
  288. package/src/router/timeout.ts +128 -0
  289. package/src/router/tracing.ts +206 -0
  290. package/src/router/trie-matching.ts +240 -61
  291. package/src/router/types.ts +23 -70
  292. package/src/router/url-params.ts +57 -0
  293. package/src/router.ts +781 -2378
  294. package/src/rsc/full-payload.ts +70 -0
  295. package/src/rsc/handler-context.ts +46 -0
  296. package/src/rsc/handler.ts +905 -1142
  297. package/src/rsc/helpers.ts +275 -19
  298. package/src/rsc/index.ts +2 -25
  299. package/src/rsc/json-route-result.ts +38 -0
  300. package/src/rsc/loader-fetch.ts +305 -0
  301. package/src/rsc/manifest-init.ts +77 -0
  302. package/src/rsc/nonce.ts +14 -0
  303. package/src/rsc/origin-guard.ts +155 -0
  304. package/src/rsc/progressive-enhancement.ts +502 -0
  305. package/src/rsc/redirect-guard.ts +99 -0
  306. package/src/rsc/response-cache-serve.ts +238 -0
  307. package/src/rsc/response-error.ts +104 -0
  308. package/src/rsc/response-route-handler.ts +257 -0
  309. package/src/rsc/rsc-rendering.ts +337 -0
  310. package/src/rsc/runtime-warnings.ts +55 -0
  311. package/src/rsc/server-action.ts +522 -0
  312. package/src/rsc/shell-capture.ts +439 -0
  313. package/src/rsc/ssr-setup.ts +144 -0
  314. package/src/rsc/transition-gate.ts +89 -0
  315. package/src/rsc/types.ts +95 -12
  316. package/src/runtime-env.ts +18 -0
  317. package/src/search-params.ts +99 -82
  318. package/src/segment-content-promise.ts +67 -0
  319. package/src/segment-loader-promise.ts +149 -0
  320. package/src/segment-system.tsx +349 -134
  321. package/src/serialize.ts +243 -0
  322. package/src/server/context.ts +452 -85
  323. package/src/server/cookie-parse.ts +32 -0
  324. package/src/server/cookie-store.ts +310 -0
  325. package/src/server/fetchable-loader-store.ts +11 -6
  326. package/src/server/handle-store.ts +123 -42
  327. package/src/server/live.ts +130 -0
  328. package/src/server/loader-registry.ts +51 -100
  329. package/src/server/request-context.ts +842 -157
  330. package/src/server.ts +15 -8
  331. package/src/ssr/index.tsx +412 -136
  332. package/src/ssr/ssr-root.tsx +228 -0
  333. package/src/static-handler.ts +45 -18
  334. package/src/testing/cache-status.ts +162 -0
  335. package/src/testing/collect-handle.ts +46 -0
  336. package/src/testing/dispatch.ts +701 -0
  337. package/src/testing/dom.entry.ts +22 -0
  338. package/src/testing/e2e/fixture.ts +188 -0
  339. package/src/testing/e2e/index.ts +128 -0
  340. package/src/testing/e2e/matchers.ts +35 -0
  341. package/src/testing/e2e/page-helpers.ts +272 -0
  342. package/src/testing/e2e/parity.ts +387 -0
  343. package/src/testing/e2e/server.ts +195 -0
  344. package/src/testing/flight-matchers.ts +97 -0
  345. package/src/testing/flight-normalize.ts +11 -0
  346. package/src/testing/flight-runtime.d.ts +57 -0
  347. package/src/testing/flight-tree.ts +682 -0
  348. package/src/testing/flight.entry.ts +52 -0
  349. package/src/testing/flight.ts +257 -0
  350. package/src/testing/generated-routes.ts +199 -0
  351. package/src/testing/index.ts +105 -0
  352. package/src/testing/internal/context.ts +371 -0
  353. package/src/testing/internal/flight-client-globals.ts +30 -0
  354. package/src/testing/internal/seed-vars.ts +54 -0
  355. package/src/testing/render-handler.ts +357 -0
  356. package/src/testing/render-route.tsx +584 -0
  357. package/src/testing/run-loader.ts +385 -0
  358. package/src/testing/run-middleware.ts +205 -0
  359. package/src/testing/run-transition-when.ts +164 -0
  360. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  361. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  362. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  363. package/src/testing/vitest-stubs/version.ts +5 -0
  364. package/src/testing/vitest.ts +305 -0
  365. package/src/theme/ThemeProvider.tsx +40 -72
  366. package/src/theme/ThemeScript.tsx +12 -14
  367. package/src/theme/constants.ts +57 -15
  368. package/src/theme/index.ts +3 -20
  369. package/src/theme/theme-context.ts +5 -35
  370. package/src/theme/theme-script.ts +43 -39
  371. package/src/theme/use-theme.ts +0 -3
  372. package/src/types/boundaries.ts +123 -0
  373. package/src/types/cache-types.ts +207 -0
  374. package/src/types/error-types.ts +132 -0
  375. package/src/types/global-namespace.ts +113 -0
  376. package/src/types/handler-context.ts +839 -0
  377. package/src/types/index.ts +81 -0
  378. package/src/types/loader-types.ts +212 -0
  379. package/src/types/request-scope.ts +112 -0
  380. package/src/types/route-config.ts +138 -0
  381. package/src/types/route-entry.ts +114 -0
  382. package/src/types/segments.ts +271 -0
  383. package/src/types.ts +1 -1795
  384. package/src/urls/include-helper.ts +162 -0
  385. package/src/urls/include-provider.ts +71 -0
  386. package/src/urls/index.ts +43 -0
  387. package/src/urls/path-helper-types.ts +413 -0
  388. package/src/urls/path-helper.ts +275 -0
  389. package/src/urls/pattern-types.ts +124 -0
  390. package/src/urls/response-types.ts +109 -0
  391. package/src/urls/type-extraction.ts +316 -0
  392. package/src/urls/urls-function.ts +80 -0
  393. package/src/urls.ts +1 -1341
  394. package/src/use-loader.tsx +406 -141
  395. package/src/vercel/index.ts +11 -0
  396. package/src/vercel/tracing.ts +88 -0
  397. package/src/vite/debug.ts +185 -0
  398. package/src/vite/discovery/bundle-postprocess.ts +182 -0
  399. package/src/vite/discovery/discover-routers.ts +389 -0
  400. package/src/vite/discovery/discovery-errors.ts +255 -0
  401. package/src/vite/discovery/gate-state.ts +171 -0
  402. package/src/vite/discovery/prerender-collection.ts +467 -0
  403. package/src/vite/discovery/route-types-writer.ts +214 -0
  404. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  405. package/src/vite/discovery/state.ts +161 -0
  406. package/src/vite/discovery/virtual-module-codegen.ts +183 -0
  407. package/src/vite/index.ts +23 -2255
  408. package/src/vite/inject-client-debug.ts +36 -0
  409. package/src/vite/plugin-types.ts +303 -0
  410. package/src/vite/plugins/cjs-to-esm.ts +90 -0
  411. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  412. package/src/vite/plugins/client-ref-hashing.ts +118 -0
  413. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  414. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  415. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  416. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
  417. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
  418. package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
  419. package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
  420. package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
  421. package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
  422. package/src/vite/plugins/expose-ids/types.ts +45 -0
  423. package/src/vite/plugins/expose-internal-ids.ts +805 -0
  424. package/src/vite/plugins/performance-tracks.ts +89 -0
  425. package/src/vite/plugins/refresh-cmd.ts +127 -0
  426. package/src/vite/plugins/use-cache-transform.ts +313 -0
  427. package/src/vite/plugins/vercel-output.ts +384 -0
  428. package/src/vite/plugins/version-injector.ts +94 -0
  429. package/src/vite/plugins/version-plugin.ts +263 -0
  430. package/src/vite/plugins/virtual-entries.ts +234 -0
  431. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  432. package/src/vite/rango.ts +560 -0
  433. package/src/vite/router-discovery.ts +1638 -0
  434. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
  435. package/src/vite/utils/banner.ts +36 -0
  436. package/src/vite/utils/bundle-analysis.ts +132 -0
  437. package/src/vite/utils/client-chunks.ts +184 -0
  438. package/src/vite/utils/directive-prologue.ts +40 -0
  439. package/src/vite/utils/forward-user-plugins.ts +171 -0
  440. package/src/vite/utils/manifest-utils.ts +15 -0
  441. package/src/vite/utils/package-resolution.ts +89 -0
  442. package/src/vite/utils/prerender-utils.ts +249 -0
  443. package/src/vite/utils/shared-utils.ts +269 -0
  444. package/CLAUDE.md +0 -43
  445. package/dist/vite/index.named-routes.gen.ts +0 -103
  446. package/src/browser/lru-cache.ts +0 -69
  447. package/src/browser/react/use-client-cache.ts +0 -56
  448. package/src/browser/request-controller.ts +0 -164
  449. package/src/browser/shallow.ts +0 -35
  450. package/src/cache/memory-store.ts +0 -253
  451. package/src/handles/index.ts +0 -6
  452. package/src/href-context.ts +0 -33
  453. package/src/network-error-thrower.tsx +0 -21
  454. package/src/router.gen.ts +0 -6
  455. package/src/static-handler.gen.ts +0 -5
  456. package/src/urls.gen.ts +0 -8
  457. package/src/vite/expose-internal-ids.ts +0 -1167
  458. package/src/vite/package-resolution.ts +0 -125
  459. package/src/vite/virtual-entries.ts +0 -114
  460. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -5,7 +5,12 @@
5
5
  * Falls back to null when no match is found (caller uses regex fallback).
6
6
  */
7
7
 
8
- import type { TrieNode, TrieLeaf } from "../build/route-trie.js";
8
+ import type {
9
+ TrieNode,
10
+ TrieLeaf,
11
+ NegotiateVariant,
12
+ } from "../build/route-trie.js";
13
+ import { safeDecodeURIComponent } from "./url-params.js";
9
14
 
10
15
  export interface TrieMatchResult {
11
16
  /** Route name */
@@ -14,10 +19,6 @@ export interface TrieMatchResult {
14
19
  sp: string;
15
20
  /** Matched route params */
16
21
  params: Record<string, string>;
17
- /** Optional param names (absent params have empty string value) */
18
- optionalParams?: string[];
19
- /** Ancestry shortCodes for layout pruning */
20
- ancestry: string[];
21
22
  /** Redirect target if trailing slash requires it */
22
23
  redirectTo?: string;
23
24
  /** Route has pre-rendered data available */
@@ -27,7 +28,7 @@ export interface TrieMatchResult {
27
28
  /** Response type for non-RSC routes (json, text, image, any) */
28
29
  responseType?: string;
29
30
  /** Negotiate variants: response-type routes sharing this path */
30
- negotiateVariants?: Array<{ routeKey: string; responseType: string }>;
31
+ negotiateVariants?: NegotiateVariant[];
31
32
  /** RSC-first: RSC route was defined before response-type variants */
32
33
  rscFirst?: true;
33
34
  }
@@ -42,25 +43,51 @@ export function tryTrieMatch(
42
43
  ): TrieMatchResult | null {
43
44
  if (!trie) return null;
44
45
 
45
- // Split pathname into segments, filtering empty strings from leading/trailing slashes
46
- const pathnameHasTrailingSlash = pathname.length > 1 && pathname.endsWith("/");
47
- const normalizedPath = pathnameHasTrailingSlash ? pathname.slice(0, -1) : pathname;
46
+ const pathnameHasTrailingSlash =
47
+ pathname.length > 1 && pathname.endsWith("/");
48
+ const normalizedPath = pathnameHasTrailingSlash
49
+ ? pathname.slice(0, -1)
50
+ : pathname;
48
51
 
49
- // Handle root path
50
52
  if (normalizedPath === "" || normalizedPath === "/") {
51
53
  if (trie.r) {
52
- return validateAndBuild(trie.r, {}, pathname, pathnameHasTrailingSlash);
54
+ return validateAndBuild(
55
+ trie.r,
56
+ [],
57
+ undefined,
58
+ pathname,
59
+ pathnameHasTrailingSlash,
60
+ );
61
+ }
62
+ // A root-level wildcard ("/*") matches "/" with an empty remainder, the
63
+ // same value the regex matcher produces for the bare prefix. Without this
64
+ // the trie misses, the regex fallback runs, and its no-config branch emits
65
+ // a corrupt slice-off redirect. The static terminal still wins above.
66
+ // A one-or-more catch-all (`w1`, from `:name+`) rejects this empty case.
67
+ if (trie.w && !trie.w.w1) {
68
+ return validateAndBuild(
69
+ trie.w,
70
+ [],
71
+ "",
72
+ pathname,
73
+ pathnameHasTrailingSlash,
74
+ );
53
75
  }
54
76
  return null;
55
77
  }
56
78
 
57
- // Remove leading slash and split
58
79
  const segments = normalizedPath.slice(1).split("/");
59
80
 
60
- // Try exact match with normalized path (no trailing slash)
61
- const result = walkTrie(trie, segments, 0, {});
81
+ const result = walkTrie(trie, segments, 0, []);
62
82
  if (result) {
63
- return validateAndBuild(result.leaf, result.params, pathname, pathnameHasTrailingSlash);
83
+ return validateAndBuild(
84
+ result.leaf,
85
+ result.paramValues,
86
+ result.wildcardValue,
87
+ pathname,
88
+ pathnameHasTrailingSlash,
89
+ result.validatedParams,
90
+ );
64
91
  }
65
92
 
66
93
  return null;
@@ -68,105 +95,257 @@ export function tryTrieMatch(
68
95
 
69
96
  interface WalkResult {
70
97
  leaf: TrieLeaf;
71
- params: Record<string, string>;
98
+ paramValues: string[];
99
+ wildcardValue?: string;
100
+ /**
101
+ * For a constraint-bearing leaf (leaf.cv set), the params map that
102
+ * leafConstraintsPass already decoded AND validated during the walk. Carried
103
+ * forward so validateAndBuild reuses it instead of re-decoding paramValues and
104
+ * re-running constraintsSatisfied a second time on the winning leaf. Undefined
105
+ * for unconstrained leaves (no decode/validate happened in the walk).
106
+ */
107
+ validatedParams?: Record<string, string>;
108
+ }
109
+
110
+ /**
111
+ * Check a leaf's constraints (leaf.cv) against already-resolved named params.
112
+ * Empty/undefined values are exempt (optional params that were not bound).
113
+ */
114
+ function constraintsSatisfied(
115
+ leaf: TrieLeaf,
116
+ params: Record<string, string>,
117
+ ): boolean {
118
+ if (!leaf.cv) return true;
119
+ for (const paramName in leaf.cv) {
120
+ const allowed = leaf.cv[paramName]!;
121
+ const value = params[paramName];
122
+ if (value !== undefined && value !== "" && !allowed.includes(value)) {
123
+ return false;
124
+ }
125
+ }
126
+ return true;
72
127
  }
73
128
 
74
129
  /**
75
- * Walk the trie by segments with priority: static > param > wildcard.
76
- * Uses backtracking to try all possible matches.
130
+ * Constraint check for a candidate terminal DURING the walk. Builds the named
131
+ * params from positional walk values (decoded the same way validateAndBuild
132
+ * does) and validates leaf.cv. Returning null lets walkTrie unwind to a
133
+ * lower-priority sibling instead of committing to a leaf that would only be
134
+ * rejected post-walk — that post-walk rejection is what forced the regex
135
+ * fallback (and its false "trie gap" R3 warning) for perfectly valid configs.
136
+ *
137
+ * On success returns the built+validated params for a constraint-bearing leaf so
138
+ * walkTrie can carry them to validateAndBuild (avoiding a second decode + a
139
+ * second constraintsSatisfied pass on the winner); returns the shared EMPTY_PASS
140
+ * sentinel for an unconstrained leaf (no work was done, nothing to carry).
141
+ */
142
+ const EMPTY_PASS: Record<string, string> = {};
143
+ function leafConstraintsPass(
144
+ leaf: TrieLeaf,
145
+ paramValues: string[],
146
+ wildcardValue: string | undefined,
147
+ ): Record<string, string> | null {
148
+ if (!leaf.cv) return EMPTY_PASS;
149
+ const params: Record<string, string> = {};
150
+ if (leaf.pa) {
151
+ for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
152
+ params[leaf.pa[i]] = safeDecodeURIComponent(paramValues[i]);
153
+ }
154
+ }
155
+ if (wildcardValue !== undefined && "pn" in leaf) {
156
+ params[(leaf as TrieLeaf & { pn: string }).pn] =
157
+ safeDecodeURIComponent(wildcardValue);
158
+ }
159
+ return constraintsSatisfied(leaf, params) ? params : null;
160
+ }
161
+
162
+ /**
163
+ * Walk the trie by segments with priority: static > suffix-param > param >
164
+ * wildcard (Priority 1-4 below; matches the canonical M4 ordering in
165
+ * docs/internal/matching-and-lazy-discovery.md).
166
+ * Uses backtracking to try all possible matches. Per-leaf constraints are
167
+ * enforced at each candidate terminal so a constraint miss backtracks to a
168
+ * lower-priority sibling rather than aborting the whole match.
77
169
  */
78
170
  function walkTrie(
79
171
  node: TrieNode,
80
172
  segments: string[],
81
173
  index: number,
82
- params: Record<string, string>,
174
+ paramValues: string[],
83
175
  ): WalkResult | null {
84
- // All segments consumed: check for terminal
85
176
  if (index === segments.length) {
86
177
  if (node.r) {
87
- return { leaf: node.r, params };
178
+ const validatedParams = leafConstraintsPass(
179
+ node.r,
180
+ paramValues,
181
+ undefined,
182
+ );
183
+ if (validatedParams) {
184
+ return {
185
+ leaf: node.r,
186
+ paramValues: [...paramValues],
187
+ validatedParams,
188
+ };
189
+ }
190
+ }
191
+ // A wildcard at this node matches the bare prefix with an empty remainder
192
+ // (e.g. "/files" against "/files/*"), mirroring the regex matcher's `*=""`.
193
+ // walkTrie otherwise only reaches node.w in the index<length branch below,
194
+ // so without this a request to the wildcard's own prefix misses the trie
195
+ // and the regex fallback emits a corrupt redirect. A static terminal
196
+ // (node.r) still wins. A one-or-more catch-all (`w1`, from `:name+`) rejects
197
+ // this empty case — it requires at least one trailing segment.
198
+ if (node.w && !node.w.w1) {
199
+ const validatedParams = leafConstraintsPass(node.w, paramValues, "");
200
+ if (validatedParams) {
201
+ return {
202
+ leaf: node.w,
203
+ paramValues: [...paramValues],
204
+ wildcardValue: "",
205
+ validatedParams,
206
+ };
207
+ }
88
208
  }
89
209
  return null;
90
210
  }
91
211
 
92
212
  const segment = segments[index];
213
+ const staticChild = node.s?.[segment];
93
214
 
94
- // Priority 1: Static match
95
- if (node.s?.[segment]) {
96
- const result = walkTrie(node.s[segment], segments, index + 1, params);
215
+ if (staticChild) {
216
+ const result = walkTrie(staticChild, segments, index + 1, paramValues);
97
217
  if (result) return result;
98
218
  }
99
219
 
100
- // Priority 2: Param match
101
- if (node.p) {
102
- const result = walkTrie(node.p.c, segments, index + 1, {
103
- ...params,
104
- [node.p.n]: segment,
105
- });
220
+ if (node.xp) {
221
+ // node.xp keys are pre-sorted longest-suffix-first at build time
222
+ // (route-trie.ts sortSuffixParams), so the first match is the most specific
223
+ // suffix: `/app.min.js` matches `:file.min.js` before `:file.js`.
224
+ for (const suffix in node.xp) {
225
+ if (segment.endsWith(suffix) && segment.length > suffix.length) {
226
+ const paramValue = segment.slice(0, -suffix.length);
227
+ paramValues.push(paramValue);
228
+ const result = walkTrie(
229
+ node.xp[suffix].c,
230
+ segments,
231
+ index + 1,
232
+ paramValues,
233
+ );
234
+ paramValues.pop();
235
+ if (result) return result;
236
+ }
237
+ }
238
+ }
239
+
240
+ // A required single-segment param captures 1+ chars (the regex matcher emits
241
+ // `([^/]+)`), so an empty path segment from a double slash (`/a//b`) must NOT
242
+ // bind `:s` to "". Reject it here so the trie matches the regex contract and
243
+ // a malformed URL 404s instead of running the handler with an empty param.
244
+ // The suffix-param branch above already requires `segment.length > suffix`,
245
+ // and node.w may legitimately be empty, so only this branch needs the guard.
246
+ if (node.p && segment !== "") {
247
+ paramValues.push(segment);
248
+ const result = walkTrie(node.p.c, segments, index + 1, paramValues);
249
+ paramValues.pop();
106
250
  if (result) return result;
107
251
  }
108
252
 
109
- // Priority 3: Wildcard match (consumes rest)
110
253
  if (node.w) {
111
- const rest = segments.slice(index).join("/");
112
- return {
113
- leaf: node.w,
114
- params: { ...params, [node.w.pn]: rest },
115
- };
254
+ const rest = joinRemainingSegments(segments, index);
255
+ // A one-or-more catch-all (`w1`, from `:name+`) requires at least one
256
+ // non-empty trailing segment. `rest` can still be "" here on a malformed
257
+ // double-slash URL (e.g. `/docs//` splits to a trailing "" segment), so
258
+ // guard this in-path site the same way the root and base-case sites are.
259
+ if (!(node.w.w1 && rest === "")) {
260
+ const validatedParams = leafConstraintsPass(node.w, paramValues, rest);
261
+ if (validatedParams) {
262
+ return {
263
+ leaf: node.w,
264
+ paramValues: [...paramValues],
265
+ wildcardValue: rest,
266
+ validatedParams,
267
+ };
268
+ }
269
+ }
116
270
  }
117
271
 
118
272
  return null;
119
273
  }
120
274
 
275
+ function joinRemainingSegments(segments: string[], start: number): string {
276
+ if (start >= segments.length) return "";
277
+ return segments.slice(start).join("/");
278
+ }
279
+
121
280
  /**
122
281
  * Post-match: validate constraints and handle trailing slash logic.
282
+ *
283
+ * `validatedParams` is the params map walkTrie already decoded AND validated via
284
+ * leafConstraintsPass for a constraint-bearing winning leaf. When present (and
285
+ * non-empty) we reuse it verbatim and SKIP the second decode + the second
286
+ * constraintsSatisfied pass — both are byte-identical to the walk-time work.
287
+ * When absent (unconstrained leaf, or the root-path call sites that never walk)
288
+ * we still BUILD the params here (that is not redundant — they must be returned)
289
+ * and run constraintsSatisfied for safety; an unconstrained leaf's check is a
290
+ * cheap early `!leaf.cv` return.
123
291
  */
124
292
  function validateAndBuild(
125
293
  leaf: TrieLeaf,
126
- params: Record<string, string>,
294
+ paramValues: string[],
295
+ wildcardValue: string | undefined,
127
296
  originalPathname: string,
128
297
  pathnameHasTrailingSlash: boolean,
298
+ validatedParams?: Record<string, string>,
129
299
  ): TrieMatchResult | null {
130
- // Validate constraints
131
- if (leaf.cv) {
132
- for (const [paramName, allowed] of Object.entries(leaf.cv)) {
133
- const value = params[paramName];
134
- if (value !== undefined && value !== "" && !allowed.includes(value)) {
135
- return null;
300
+ let params: Record<string, string>;
301
+ // EMPTY_PASS (the unconstrained sentinel) and undefined both mean "nothing was
302
+ // pre-validated"; only a populated map carried from a constraint-bearing leaf
303
+ // lets us skip the rebuild + re-check.
304
+ if (validatedParams && validatedParams !== EMPTY_PASS) {
305
+ params = validatedParams;
306
+ } else {
307
+ params = {};
308
+ if (leaf.pa) {
309
+ for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
310
+ params[leaf.pa[i]] = safeDecodeURIComponent(paramValues[i]);
136
311
  }
137
312
  }
138
- }
139
313
 
140
- // Fill in empty strings for optional params that weren't matched
141
- if (leaf.op) {
142
- for (const name of leaf.op) {
143
- if (!(name in params)) {
144
- params[name] = "";
145
- }
314
+ if (wildcardValue !== undefined && "pn" in leaf) {
315
+ params[(leaf as TrieLeaf & { pn: string }).pn] =
316
+ safeDecodeURIComponent(wildcardValue);
317
+ }
318
+
319
+ if (!constraintsSatisfied(leaf, params)) {
320
+ return null;
146
321
  }
147
322
  }
148
323
 
149
- // Trailing slash handling
150
324
  const tsMode = leaf.ts as "never" | "always" | "ignore" | undefined;
151
325
  let redirectTo: string | undefined;
152
326
 
153
- if (tsMode === "always" && !pathnameHasTrailingSlash && originalPathname !== "/") {
327
+ if (
328
+ tsMode === "always" &&
329
+ !pathnameHasTrailingSlash &&
330
+ originalPathname !== "/"
331
+ ) {
154
332
  redirectTo = originalPathname + "/";
155
333
  } else if (tsMode === "never" && pathnameHasTrailingSlash) {
156
334
  redirectTo = originalPathname.slice(0, -1);
157
335
  }
158
336
 
159
- return {
337
+ const result: TrieMatchResult = {
160
338
  routeKey: leaf.n,
161
339
  sp: leaf.sp,
162
340
  params,
163
- optionalParams: leaf.op,
164
- ancestry: leaf.a,
165
- ...(redirectTo ? { redirectTo } : {}),
166
- ...(leaf.pr ? { pr: true } : {}),
167
- ...(leaf.pt ? { pt: true } : {}),
168
- ...(leaf.rt ? { responseType: leaf.rt } : {}),
169
- ...(leaf.nv ? { negotiateVariants: leaf.nv } : {}),
170
- ...(leaf.rf ? { rscFirst: true } : {}),
171
341
  };
342
+
343
+ if (redirectTo) result.redirectTo = redirectTo;
344
+ if (leaf.pr) result.pr = true;
345
+ if (leaf.pt) result.pt = true;
346
+ if (leaf.rt) result.responseType = leaf.rt;
347
+ if (leaf.nv) result.negotiateVariants = leaf.nv;
348
+ if (leaf.rf) result.rscFirst = true;
349
+
350
+ return result;
172
351
  }
@@ -5,7 +5,11 @@
5
5
  */
6
6
 
7
7
  import type { ReactNode } from "react";
8
- import type { EntryData, InterceptEntry, InterceptSelectorContext } from "../server/context";
8
+ import type {
9
+ EntryData,
10
+ InterceptEntry,
11
+ InterceptSelectorContext,
12
+ } from "../server/context";
9
13
  import type {
10
14
  ErrorInfo,
11
15
  ErrorPhase,
@@ -18,27 +22,11 @@ import type {
18
22
  ShouldRevalidateFn,
19
23
  } from "../types";
20
24
 
21
- /**
22
- * Result of resolving loaders with revalidation
23
- * Contains both segments to render and all matched segment IDs
24
- */
25
- export interface LoaderRevalidationResult {
26
- segments: ResolvedSegment[];
27
- matchedIds: string[];
28
- }
29
-
30
- /**
31
- * Result of resolving segments with revalidation
32
- * Contains both segments to render and all matched segment IDs
33
- */
34
25
  export interface SegmentRevalidationResult {
35
26
  segments: ResolvedSegment[];
36
27
  matchedIds: string[];
37
28
  }
38
29
 
39
- /**
40
- * Action context type for revalidation
41
- */
42
30
  export type ActionContext = {
43
31
  actionId?: string;
44
32
  actionUrl?: URL;
@@ -46,23 +34,6 @@ export type ActionContext = {
46
34
  formData?: FormData;
47
35
  };
48
36
 
49
- /**
50
- * Dependencies passed to segment resolution functions
51
- * These are created within createRouter and passed to extracted utilities
52
- */
53
- export interface RouterDependencies<TEnv> {
54
- findNearestErrorBoundary: (
55
- entry: EntryData | null
56
- ) => ReactNode | ErrorBoundaryHandler | null;
57
- findNearestNotFoundBoundary: (
58
- entry: EntryData | null
59
- ) => ReactNode | NotFoundBoundaryHandler | null;
60
- }
61
-
62
- /**
63
- * Dependencies injected from createRouter closure into extracted segment resolution functions.
64
- * These are the closure-bound helpers that cannot be imported directly.
65
- */
66
37
  export interface SegmentResolutionDeps<TEnv = any> {
67
38
  wrapLoaderPromise: <T>(
68
39
  promise: Promise<T>,
@@ -79,35 +50,31 @@ export interface SegmentResolutionDeps<TEnv = any> {
79
50
  requestStartTime?: number;
80
51
  },
81
52
  ) => Promise<LoaderDataResult<T>>;
82
- trackHandler: <T>(promise: Promise<T>) => Promise<T>;
53
+ trackHandler: <T>(
54
+ promise: Promise<T>,
55
+ errorContext?: {
56
+ segmentId?: string;
57
+ segmentType?: string;
58
+ },
59
+ ) => Promise<T>;
83
60
  findNearestErrorBoundary: (
84
61
  entry: EntryData | null,
85
62
  ) => ReactNode | ErrorBoundaryHandler | null;
86
63
  findNearestNotFoundBoundary: (
87
64
  entry: EntryData | null,
88
65
  ) => ReactNode | NotFoundBoundaryHandler | null;
89
- callOnError: (
90
- error: unknown,
91
- phase: ErrorPhase,
92
- context: any,
93
- ) => void;
94
- }
95
-
96
- /**
97
- * Dependencies injected from createRouter closure into extracted intercept resolution functions.
98
- */
99
- export interface InterceptResolutionDeps<TEnv = any> {
100
- wrapLoaderPromise: SegmentResolutionDeps<TEnv>["wrapLoaderPromise"];
101
- evaluateInterceptWhen: (
102
- intercept: InterceptEntry,
103
- selectorContext: InterceptSelectorContext | null,
104
- isAction: boolean,
105
- ) => boolean;
66
+ notFoundComponent?: ReactNode | ((props: { pathname: string }) => ReactNode);
67
+ callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
68
+ /**
69
+ * Router-level default for the per-segment `transition({ viewTransition })`
70
+ * flag, from createRouter({ viewTransition }). Resolved into each segment's
71
+ * transition config during resolution (only `false` is stamped) so the render
72
+ * gate reads the boundary decision off the segment on both server and client.
73
+ * Undefined is treated as "auto" (wrap).
74
+ */
75
+ viewTransitionDefault?: "auto" | false;
106
76
  }
107
77
 
108
- /**
109
- * Dependencies injected from createRouter closure into extracted match API functions.
110
- */
111
78
  export interface MatchApiDeps<TEnv = any> {
112
79
  findMatch: (pathname: string, ms?: any) => any;
113
80
  getMetricsStore: () => any;
@@ -122,23 +89,13 @@ export interface MatchApiDeps<TEnv = any> {
122
89
  getRouteMap: () => Record<string, string>;
123
90
  }
124
91
 
125
- /**
126
- * Title descriptor types for template support
127
- */
128
92
  export type TitleDescriptor =
129
93
  | string
130
94
  | { template: string; default: string } // For layouts - template applied to child titles
131
- | { absolute: string }; // Bypass parent template
95
+ | { absolute: string };
132
96
 
133
- /**
134
- * Unset descriptor to remove inherited meta
135
- * Key format matches getMetaKey output: "title", "name:description", "property:og:image"
136
- */
137
97
  export type UnsetDescriptor = { unset: string };
138
98
 
139
- /**
140
- * Base meta descriptor types (sync values)
141
- */
142
99
  export type MetaDescriptorBase =
143
100
  | { charSet: "utf-8" }
144
101
  | { title: TitleDescriptor }
@@ -150,10 +107,6 @@ export type MetaDescriptorBase =
150
107
  | UnsetDescriptor
151
108
  | { [name: string]: unknown };
152
109
 
153
- /**
154
- * Meta descriptor that can be sync or async.
155
- * Use Promise<MetaDescriptorBase> for streaming meta that resolves after initial render.
156
- */
157
110
  export type MetaDescriptor = MetaDescriptorBase | Promise<MetaDescriptorBase>;
158
111
 
159
112
  type LdJsonObject = { [Key in string]: LdJsonValue } & {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * URL param encode/decode at the route boundary.
3
+ *
4
+ * Extraction (decode): regex/trie matchers keep param values URL-encoded;
5
+ * `safeDecodeURIComponent` turns them back into raw strings so `ctx.params`
6
+ * matches the contract apps expect (Express/React Router/Fastify/Koa) and
7
+ * round-trips through reverse stay stable. Malformed %-encoding is
8
+ * preserved as-is so a broken URL doesn't crash matching.
9
+ *
10
+ * Reversal (encode): `encodePathSegment` escapes only what RFC 3986
11
+ * requires for a path segment — `/`, `?`, `#`, space, control chars,
12
+ * non-ASCII — and leaves pchar sub-delims (`@ : $ & + , ; =` and friends)
13
+ * readable. `encodeURIComponent` over-encodes for path segments, which
14
+ * makes generated URLs harder for humans to read in the address bar
15
+ * (e.g. mailbox IDs like `ivo@example.com` would become
16
+ * `ivo%40example.com` even though `@` is path-legal).
17
+ */
18
+
19
+ export function safeDecodeURIComponent(raw: string): string {
20
+ if (raw === "" || raw.indexOf("%") === -1) return raw;
21
+ try {
22
+ return decodeURIComponent(raw);
23
+ } catch {
24
+ return raw;
25
+ }
26
+ }
27
+
28
+ const PATH_SAFE_ESCAPES: Record<string, string> = {
29
+ "%3A": ":",
30
+ "%40": "@",
31
+ "%24": "$",
32
+ "%26": "&",
33
+ "%2B": "+",
34
+ "%2C": ",",
35
+ "%3B": ";",
36
+ "%3D": "=",
37
+ };
38
+
39
+ export function encodePathSegment(value: string): string {
40
+ return encodeURIComponent(value).replace(
41
+ /%(?:3A|40|24|26|2B|2C|3B|3D)/gi,
42
+ (match) => PATH_SAFE_ESCAPES[match.toUpperCase()] ?? match,
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Encode a catch-all remainder: encode each `/`-separated segment but keep the
48
+ * separators, so `a/b c` -> `a/b%20c` (not `a%2Fb%20c`). Shared by the reverse
49
+ * helper and the build-time prerender substitution so both produce identical
50
+ * URLs. `encode` defaults to the path-safe `encodePathSegment`.
51
+ */
52
+ export function encodePathRemainder(
53
+ value: string,
54
+ encode: (segment: string) => string = encodePathSegment,
55
+ ): string {
56
+ return value.split("/").map(encode).join("/");
57
+ }