@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
package/src/handle.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { missingInjectedIdError } from "./missing-id-error.js";
2
+ import { isUnderTestRunner } from "./runtime-env.js";
3
+
1
4
  /**
2
5
  * Handle definition for accumulating data across route segments.
3
6
  *
@@ -7,8 +10,11 @@
7
10
  *
8
11
  * @example
9
12
  * ```ts
10
- * // Define a handle (name auto-generated from file + export)
11
- * export const Breadcrumbs = createHandle<BreadcrumbItem>();
13
+ * // Define a handle (name auto-generated from file + export).
14
+ * // Default collect is the identity: one array per segment that pushed.
15
+ * export const Breadcrumbs = createHandle<BreadcrumbItem, BreadcrumbItem[]>(
16
+ * (segments) => segments.flat(), // opt into a single flat list
17
+ * );
12
18
  *
13
19
  * // Use in handler
14
20
  * const push = ctx.use(Breadcrumbs);
@@ -18,7 +24,7 @@
18
24
  * const crumbs = useHandle(Breadcrumbs);
19
25
  * ```
20
26
  */
21
- export interface Handle<TData, TAccumulated = TData[]> {
27
+ export interface Handle<TData, TAccumulated = TData[][]> {
22
28
  /**
23
29
  * Brand to distinguish handles from loaders in ctx.use()
24
30
  */
@@ -29,14 +35,16 @@ export interface Handle<TData, TAccumulated = TData[]> {
29
35
  * Format: "filePath#ExportName" in dev, "hash#ExportName" in production
30
36
  */
31
37
  readonly $$id: string;
32
-
33
38
  }
34
39
 
35
40
  /**
36
- * Default collect function that flattens segment arrays into a single array.
41
+ * Default collect: pass the per-segment data through as-is — one array per segment
42
+ * that pushed, in segment order. Lossless, so a consumer can tell which/how-many
43
+ * segments contributed. Callers that want a single flat list opt in with
44
+ * `createHandle((segments) => segments.flat())`.
37
45
  */
38
- function defaultCollect<T>(segments: T[][]): T[] {
39
- return segments.flat();
46
+ function defaultCollect<T>(segments: T[][]): T[][] {
47
+ return segments;
40
48
  }
41
49
 
42
50
  // Module-level registry mapping $$id to collect functions.
@@ -44,11 +52,13 @@ function defaultCollect<T>(segments: T[][]): T[] {
44
52
  // Used by useHandle() to recover collect when handle is deserialized from RSC prop.
45
53
  const collectRegistry = new Map<string, (segments: unknown[][]) => unknown>();
46
54
 
47
- /**
48
- * Look up a collect function from the registry by handle $$id.
49
- * Returns undefined if not registered (falls back to defaultCollect in useHandle).
50
- */
51
- export function getCollectFn(id: string): ((segments: unknown[][]) => unknown) | undefined {
55
+ // Monotonic counter for runtime fallback ids (see createHandle). Only used
56
+ // when no build id was injected (a bare unit test).
57
+ let runtimeHandleIdCounter = 0;
58
+
59
+ export function getCollectFn(
60
+ id: string,
61
+ ): ((segments: unknown[][]) => unknown) | undefined {
52
62
  return collectRegistry.get(id);
53
63
  }
54
64
 
@@ -58,13 +68,22 @@ export function getCollectFn(id: string): ((segments: unknown[][]) => unknown) |
58
68
  * The $$id is auto-generated by the Vite exposeInternalIds plugin based on
59
69
  * file path and export name. No manual naming required.
60
70
  *
61
- * @param collect - Optional collect function (default: flatten into array)
71
+ * @param collect - Optional collect function. Default: pass the per-segment data
72
+ * through as-is (one array per segment that pushed, in segment order). Lossless,
73
+ * so a consumer can tell which/how-many segments contributed. Opt into a single
74
+ * flat list with `(segments) => segments.flat()`.
62
75
  * @param __injectedId - Auto-injected by Vite plugin, do not provide manually
63
76
  *
64
77
  * @example
65
78
  * ```ts
66
- * // Default: flatten into array
67
- * export const Breadcrumbs = createHandle<BreadcrumbItem>();
79
+ * // Default: per-segment grouping, as-is
80
+ * export const Pushed = createHandle<string>();
81
+ * // Result type: string[][] (e.g. [["a"], ["b", "c"]])
82
+ *
83
+ * // Opt into a single flat list
84
+ * export const Breadcrumbs = createHandle<BreadcrumbItem, BreadcrumbItem[]>(
85
+ * (segments) => segments.flat()
86
+ * );
68
87
  * // Result type: BreadcrumbItem[]
69
88
  *
70
89
  * // Custom: last value wins
@@ -79,8 +98,9 @@ export function getCollectFn(id: string): ((segments: unknown[][]) => unknown) |
79
98
  * );
80
99
  * // Result type: MetaTags
81
100
  *
82
- * // Custom: dedupe by href
83
- * export const Breadcrumbs = createHandle<BreadcrumbItem>(
101
+ * // Custom: dedupe by href (TAccumulated must be given — a custom collect that
102
+ * // returns a flat array no longer matches the default TData[][])
103
+ * export const Breadcrumbs = createHandle<BreadcrumbItem, BreadcrumbItem[]>(
84
104
  * (segments) => {
85
105
  * const all = segments.flat();
86
106
  * return all.filter((item, i) => all.findIndex(x => x.href === item.href) === i);
@@ -88,28 +108,40 @@ export function getCollectFn(id: string): ((segments: unknown[][]) => unknown) |
88
108
  * );
89
109
  * ```
90
110
  */
91
- export function createHandle<TData, TAccumulated = TData[]>(
111
+ export function createHandle<TData, TAccumulated = TData[][]>(
92
112
  collect?: (segments: TData[][]) => TAccumulated,
93
- __injectedId?: string
113
+ __injectedId?: string,
94
114
  ): Handle<TData, TAccumulated> {
95
- const handleId = __injectedId ?? "";
115
+ let handleId = __injectedId ?? "";
96
116
 
97
- if (!handleId && process.env.NODE_ENV !== "production") {
98
- throw new Error(
99
- "[rsc-router] Handle is missing $$id. " +
100
- "Make sure the exposeInternalIds Vite plugin is enabled and " +
101
- "the handle is exported with: export const MyHandle = createHandle(...)"
102
- );
117
+ // No build-injected id. Under a test runner: fall back to a synthetic id so the
118
+ // collect registers below and the handle is exercisable in tests (useHandle,
119
+ // collectHandle, renderRoute's `handles` run the REAL collect). Otherwise (dev
120
+ // or a real build) it means an UNSUPPORTED handler shape the plugin skipped —
121
+ // fail loud. The rich, stack-parsing diagnostic stays behind the NODE_ENV check
122
+ // so a production build folds it away and tree-shakes missing-id-error.ts out,
123
+ // shipping the small throw instead. isUnderTestRunner() is runtime-safe.
124
+ if (!handleId) {
125
+ if (isUnderTestRunner()) {
126
+ handleId = `__rango_runtime_handle_${runtimeHandleIdCounter++}`;
127
+ } else if (process.env.NODE_ENV !== "production") {
128
+ throw missingInjectedIdError("Handle", "createHandle");
129
+ } else {
130
+ throw new Error(
131
+ "[rango] Handle is missing $$id — the build plugin did not inject one. " +
132
+ "Export it as `export const X = createHandle(...)`.",
133
+ );
134
+ }
103
135
  }
104
136
 
105
- const collectFn = collect ??
137
+ const collectFn =
138
+ collect ??
106
139
  (defaultCollect as unknown as (segments: TData[][]) => TAccumulated);
107
140
 
108
- // Register collect in module-level registry so useHandle() can recover it
109
- // when the handle is deserialized from RSC props (toJSON strips collect).
110
- if (handleId) {
111
- collectRegistry.set(handleId, collectFn as (segments: unknown[][]) => unknown);
112
- }
141
+ collectRegistry.set(
142
+ handleId,
143
+ collectFn as (segments: unknown[][]) => unknown,
144
+ );
113
145
 
114
146
  return {
115
147
  __brand: "handle" as const,
@@ -117,9 +149,6 @@ export function createHandle<TData, TAccumulated = TData[]>(
117
149
  };
118
150
  }
119
151
 
120
- /**
121
- * Type guard to check if a value is a Handle.
122
- */
123
152
  export function isHandle(value: unknown): value is Handle<unknown, unknown> {
124
153
  return (
125
154
  typeof value === "object" &&
@@ -128,3 +157,49 @@ export function isHandle(value: unknown): value is Handle<unknown, unknown> {
128
157
  (value as { __brand: unknown }).__brand === "handle"
129
158
  );
130
159
  }
160
+
161
+ /**
162
+ * Collect handle data from a HandleData map, applying the handle's collect
163
+ * function over segments in order. Shared between server-side rendered()
164
+ * reads and client-side useHandle().
165
+ *
166
+ * @param handle - The handle to collect data for
167
+ * @param data - Full handle data map (handleName -> segmentId -> entries[])
168
+ * @param segmentOrder - Segment IDs in parent -> child resolution order
169
+ */
170
+ export function collectHandleData<TData, TAccumulated>(
171
+ handle: Handle<TData, TAccumulated>,
172
+ data: Record<string, Record<string, unknown[]>>,
173
+ segmentOrder: string[],
174
+ ): TAccumulated {
175
+ // Fall back to the default (identity, pass-through) collect when none is
176
+ // registered — e.g. the handle's module was not imported so createHandle() never
177
+ // ran. This is harmless for a handle that wanted the default, but a handle with a
178
+ // CUSTOM collect that failed to register silently gets the wrong shape (identity
179
+ // TData[][]) cast as its declared TAccumulated. The runtime can't tell the two
180
+ // apart (a Handle only carries $$id), so warn in dev (folded out of production).
181
+ const collectFn = getCollectFn(handle.$$id);
182
+ if (!collectFn && process.env.NODE_ENV !== "production") {
183
+ console.warn(
184
+ `[rango] Handle "${handle.$$id}" has no registered collect — falling back ` +
185
+ `to the identity (per-segment data as-is). If this handle declares a ` +
186
+ `CUSTOM collect, import its module so createHandle() runs where it is read.`,
187
+ );
188
+ }
189
+ const collect = (collectFn ??
190
+ (defaultCollect as unknown as (segments: unknown[][]) => unknown)) as (
191
+ segments: TData[][],
192
+ ) => TAccumulated;
193
+
194
+ const segmentData = data[handle.$$id];
195
+ if (!segmentData) return collect([]);
196
+
197
+ const segmentArrays: TData[][] = [];
198
+ for (const segmentId of segmentOrder) {
199
+ const entries = segmentData[segmentId];
200
+ if (entries && entries.length > 0) {
201
+ segmentArrays.push(entries as TData[]);
202
+ }
203
+ }
204
+ return collect(segmentArrays);
205
+ }
@@ -3,11 +3,16 @@
3
3
  /**
4
4
  * Component to render collected meta descriptors in the document head.
5
5
  *
6
- * Supports both sync and async meta descriptors. Async descriptors
7
- * (Promise<MetaDescriptorBase>) are resolved using React's use() hook.
6
+ * Deferred (Promise) meta descriptors are resolved before MetaTags renders
7
+ * (server-side on the full render, client-side before apply on navigation), so
8
+ * it only ever receives resolved descriptors and never suspends.
8
9
  *
9
10
  * When theme is enabled in the router config, MetaTags also renders
10
11
  * the theme initialization script to prevent FOUC (flash of unstyled content).
12
+ * This makes MetaTags the sole FOUC-script injector for apps that render it;
13
+ * the standalone `<ThemeScript />` is only needed when MetaTags is not used.
14
+ * Rendering both is safe (the inline script guards listener registration) but
15
+ * redundant.
11
16
  *
12
17
  * @example
13
18
  * ```tsx
@@ -24,12 +29,13 @@
24
29
  * ```
25
30
  */
26
31
 
27
- import { use } from "react";
28
32
  import { useHandle } from "../browser/react/use-handle.js";
29
33
  import { Meta } from "./meta.js";
30
- import type { MetaDescriptor, MetaDescriptorBase } from "../router/types.js";
31
- import { getSSRThemeConfig } from "../theme/theme-context.js";
34
+ import type { MetaDescriptorBase } from "../router/types.js";
35
+ import { useThemeContext } from "../theme/theme-context.js";
32
36
  import { generateThemeScript } from "../theme/theme-script.js";
37
+ import { useNonce } from "../browser/react/nonce-context.js";
38
+ import { escapeJsonForScript } from "../escape-script.js";
33
39
 
34
40
  // Type guards for MetaDescriptorBase variants
35
41
  function hasCharSet(d: MetaDescriptorBase): d is { charSet: "utf-8" } {
@@ -40,57 +46,67 @@ function hasTitle(d: MetaDescriptorBase): d is { title: string } {
40
46
  return "title" in d && typeof (d as { title?: unknown }).title === "string";
41
47
  }
42
48
 
43
- function hasNameContent(d: MetaDescriptorBase): d is { name: string; content: string } {
44
- return "name" in d && "content" in d &&
49
+ function hasNameContent(
50
+ d: MetaDescriptorBase,
51
+ ): d is { name: string; content: string } {
52
+ return (
53
+ "name" in d &&
54
+ "content" in d &&
45
55
  typeof (d as { name?: unknown }).name === "string" &&
46
- typeof (d as { content?: unknown }).content === "string";
56
+ typeof (d as { content?: unknown }).content === "string"
57
+ );
47
58
  }
48
59
 
49
- function hasPropertyContent(d: MetaDescriptorBase): d is { property: string; content: string } {
50
- return "property" in d && "content" in d &&
60
+ function hasPropertyContent(
61
+ d: MetaDescriptorBase,
62
+ ): d is { property: string; content: string } {
63
+ return (
64
+ "property" in d &&
65
+ "content" in d &&
51
66
  typeof (d as { property?: unknown }).property === "string" &&
52
- typeof (d as { content?: unknown }).content === "string";
67
+ typeof (d as { content?: unknown }).content === "string"
68
+ );
53
69
  }
54
70
 
55
- function hasHttpEquivContent(d: MetaDescriptorBase): d is { httpEquiv: string; content: string } {
56
- return "httpEquiv" in d && "content" in d &&
71
+ function hasHttpEquivContent(
72
+ d: MetaDescriptorBase,
73
+ ): d is { httpEquiv: string; content: string } {
74
+ return (
75
+ "httpEquiv" in d &&
76
+ "content" in d &&
57
77
  typeof (d as { httpEquiv?: unknown }).httpEquiv === "string" &&
58
- typeof (d as { content?: unknown }).content === "string";
78
+ typeof (d as { content?: unknown }).content === "string"
79
+ );
59
80
  }
60
81
 
61
- function hasScriptLdJson(d: MetaDescriptorBase): d is { "script:ld+json": object } {
82
+ function hasScriptLdJson(
83
+ d: MetaDescriptorBase,
84
+ ): d is { "script:ld+json": object } {
62
85
  return "script:ld+json" in d;
63
86
  }
64
87
 
65
- function hasTagName(d: MetaDescriptorBase): d is { tagName: "meta" | "link"; [name: string]: string } {
66
- return "tagName" in d && ((d as { tagName?: unknown }).tagName === "meta" || (d as { tagName?: unknown }).tagName === "link");
67
- }
68
-
69
- /**
70
- * Check if a value is a Promise.
71
- */
72
- function isPromise(value: unknown): value is Promise<unknown> {
73
- return value !== null && typeof value === "object" && "then" in value;
88
+ function hasTagName(
89
+ d: MetaDescriptorBase,
90
+ ): d is { tagName: "meta" | "link"; [name: string]: string } {
91
+ return (
92
+ "tagName" in d &&
93
+ ((d as { tagName?: unknown }).tagName === "meta" ||
94
+ (d as { tagName?: unknown }).tagName === "link")
95
+ );
74
96
  }
75
97
 
76
- /**
77
- * Render a single meta descriptor as a React element.
78
- */
79
- function renderMetaDescriptor(
98
+ export function renderMetaDescriptor(
80
99
  descriptor: MetaDescriptorBase,
81
- index: number
100
+ index: number,
82
101
  ): React.ReactNode {
83
- // charset
84
102
  if (hasCharSet(descriptor)) {
85
103
  return <meta key="charSet" charSet={descriptor.charSet} />;
86
104
  }
87
105
 
88
- // title
89
106
  if (hasTitle(descriptor)) {
90
107
  return <title key="title">{descriptor.title}</title>;
91
108
  }
92
109
 
93
- // name + content (description, viewport, etc.)
94
110
  if (hasNameContent(descriptor)) {
95
111
  return (
96
112
  <meta
@@ -101,7 +117,6 @@ function renderMetaDescriptor(
101
117
  );
102
118
  }
103
119
 
104
- // property + content (Open Graph, etc.)
105
120
  if (hasPropertyContent(descriptor)) {
106
121
  return (
107
122
  <meta
@@ -112,7 +127,6 @@ function renderMetaDescriptor(
112
127
  );
113
128
  }
114
129
 
115
- // http-equiv + content
116
130
  if (hasHttpEquivContent(descriptor)) {
117
131
  return (
118
132
  <meta
@@ -123,9 +137,10 @@ function renderMetaDescriptor(
123
137
  );
124
138
  }
125
139
 
126
- // JSON-LD structured data
127
140
  if (hasScriptLdJson(descriptor)) {
128
- const json = JSON.stringify(descriptor["script:ld+json"]);
141
+ const json = escapeJsonForScript(
142
+ JSON.stringify(descriptor["script:ld+json"]),
143
+ );
129
144
  return (
130
145
  <script
131
146
  key={`ld-json-${index}`}
@@ -135,27 +150,32 @@ function renderMetaDescriptor(
135
150
  );
136
151
  }
137
152
 
138
- // Custom tagName (meta or link with arbitrary attributes)
139
153
  if (hasTagName(descriptor)) {
140
154
  const { tagName, ...rest } = descriptor;
141
155
  if (tagName === "link") {
142
- return <link key={`link-${index}`} {...(rest as React.LinkHTMLAttributes<HTMLLinkElement>)} />;
156
+ return (
157
+ <link
158
+ key={`link-${index}`}
159
+ {...(rest as React.LinkHTMLAttributes<HTMLLinkElement>)}
160
+ />
161
+ );
143
162
  }
144
163
  if (tagName === "meta") {
145
- return <meta key={`meta-${index}`} {...(rest as React.MetaHTMLAttributes<HTMLMetaElement>)} />;
164
+ return (
165
+ <meta
166
+ key={`meta-${index}`}
167
+ {...(rest as React.MetaHTMLAttributes<HTMLMetaElement>)}
168
+ />
169
+ );
146
170
  }
147
171
  }
148
172
 
149
- // Fallback: treat as meta attributes
150
- return <meta key={`meta-fallback-${index}`} {...(descriptor as React.MetaHTMLAttributes<HTMLMetaElement>)} />;
151
- }
152
-
153
- /**
154
- * Wrapper component to resolve a Promise<MetaDescriptorBase> using use().
155
- */
156
- function AsyncMetaTag({ promise, index }: { promise: Promise<MetaDescriptorBase>; index: number }): React.ReactNode {
157
- const resolved = use(promise);
158
- return renderMetaDescriptor(resolved, index);
173
+ return (
174
+ <meta
175
+ key={`meta-fallback-${index}`}
176
+ {...(descriptor as React.MetaHTMLAttributes<HTMLMetaElement>)}
177
+ />
178
+ );
159
179
  }
160
180
 
161
181
  /**
@@ -167,27 +187,31 @@ function AsyncMetaTag({ promise, index }: { promise: Promise<MetaDescriptorBase>
167
187
  * When theme is enabled in router config, also renders the theme initialization
168
188
  * script to prevent FOUC (flash of unstyled content).
169
189
  *
170
- * Async meta descriptors (Promise<MetaDescriptorBase>) are resolved using
171
- * React's use() hook. RSC streaming handles the Promise resolution.
190
+ * Deferred (Promise) meta descriptors are resolved BEFORE MetaTags renders —
191
+ * server-side on the full/SSR render, client-side before apply on navigation
192
+ * (resolve-by-default) — so MetaTags only ever receives resolved descriptors and
193
+ * never suspends.
172
194
  */
173
195
  export function MetaTags(): React.ReactNode {
174
- const descriptors = useHandle(Meta) as MetaDescriptor[];
175
- const themeConfig = getSSRThemeConfig();
196
+ // Deferred descriptors are resolved BEFORE collect runs (resolve-by-default),
197
+ // and collectMeta strips unset markers, so the collected output is always
198
+ // resolved base descriptors (never a Promise).
199
+ const descriptors = useHandle(Meta) as MetaDescriptorBase[];
200
+ const themeConfig = useThemeContext()?.config ?? null;
201
+ const nonce = useNonce();
176
202
 
177
203
  return (
178
204
  <>
179
205
  {/* Theme script must be first to prevent FOUC */}
180
206
  {themeConfig && (
181
207
  <script
208
+ nonce={nonce}
182
209
  dangerouslySetInnerHTML={{ __html: generateThemeScript(themeConfig) }}
183
210
  />
184
211
  )}
185
- {descriptors.map((descriptor, index) => {
186
- if (isPromise(descriptor)) {
187
- return <AsyncMetaTag key={`async-${index}`} promise={descriptor} index={index} />;
188
- }
189
- return renderMetaDescriptor(descriptor, index);
190
- })}
212
+ {descriptors.map((descriptor, index) =>
213
+ renderMetaDescriptor(descriptor, index),
214
+ )}
191
215
  </>
192
216
  );
193
217
  }
@@ -0,0 +1,183 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Renders the scripts collected by the Script handle into the document.
5
+ *
6
+ * Place `<Scripts />` inside `<head>` (default) and, if you push body scripts,
7
+ * `<Scripts position="body" />` at the top of `<body>`. Each site renders the
8
+ * configs whose `position` matches; the request CSP nonce is applied
9
+ * automatically to every DOCUMENT-RENDERED <script> (consumers never pass it). An
10
+ * async script first encountered on a soft navigation is injected client-side
11
+ * where the nonce is unavailable, so it carries no nonce and relies on
12
+ * 'strict-dynamic' (or a host allowance) — see the nonce caveat in the /scripts
13
+ * skill.
14
+ *
15
+ * EXECUTION CONTRACT — see the Script handle's docs. Inline + ordered (defer)
16
+ * scripts are document-load: they execute only when present in the initial HTML,
17
+ * so this component FREEZES that set after hydration (the initializer below runs
18
+ * once) — a later soft navigation never inserts an inert <script> (React creates
19
+ * client-mounted scripts via innerHTML, which the HTML spec makes non-executing).
20
+ * Async external scripts are React resources and stay reactive: React loads them
21
+ * on first encounter, including after navigation, deduped by src.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * <html>
26
+ * <head>
27
+ * <MetaTags />
28
+ * <Scripts />
29
+ * </head>
30
+ * <body>
31
+ * <Scripts position="body" />
32
+ * {children}
33
+ * </body>
34
+ * </html>
35
+ * ```
36
+ */
37
+
38
+ import { useState, type ReactNode } from "react";
39
+ import { useHandle } from "../browser/react/use-handle.js";
40
+ import { useNonce } from "../browser/react/nonce-context.js";
41
+ import { escapeScriptBody } from "../escape-script.js";
42
+ import { Script, type ScriptAttributes, type ScriptConfig } from "./script.js";
43
+
44
+ /** An external async script is a React-managed resource (reactive on nav). */
45
+ function isAsyncResource(config: ScriptConfig): boolean {
46
+ return config.src != null && config.async === true;
47
+ }
48
+
49
+ // Fields the Script handle owns (set via the ScriptConfig fields, applied as
50
+ // explicit props by renderScript) plus the inline-content props. Dropped from the
51
+ // attributes bag so untyped/serialized input cannot smuggle them in — e.g.
52
+ // `children`/`dangerouslySetInnerHTML` alongside an inline body makes React throw,
53
+ // or `src` on an inline script. The discriminated type already excludes these;
54
+ // this is the runtime guard.
55
+ const MANAGED_ATTRS = new Set([
56
+ "id",
57
+ "src",
58
+ "async",
59
+ "defer",
60
+ "type",
61
+ "children",
62
+ "nonce",
63
+ "dangerouslySetInnerHTML",
64
+ ]);
65
+
66
+ // Drop managed fields + any `on*` event handlers (a config serializes across the
67
+ // server -> client boundary, so a function cannot survive it) from the passthrough
68
+ // attributes, warning in dev.
69
+ function passthroughAttributes(
70
+ attributes: ScriptAttributes | undefined,
71
+ ): Record<string, unknown> {
72
+ if (!attributes) return {};
73
+ const out: Record<string, unknown> = {};
74
+ const dev = process.env.NODE_ENV !== "production";
75
+ for (const [key, value] of Object.entries(
76
+ attributes as Record<string, unknown>,
77
+ )) {
78
+ const isHandler = key.startsWith("on");
79
+ if (isHandler || MANAGED_ATTRS.has(key)) {
80
+ if (dev) {
81
+ console.warn(
82
+ isHandler
83
+ ? `[Scripts] event handler "${key}" in a script's attributes is ` +
84
+ `dropped; callbacks cannot cross the server -> client handle ` +
85
+ `boundary. Use a "use client" component for load/error handling.`
86
+ : `[Scripts] managed field "${key}" in a script's attributes is ` +
87
+ `dropped; set it via the ScriptConfig fields (the request nonce ` +
88
+ `is applied automatically).`,
89
+ );
90
+ }
91
+ continue;
92
+ }
93
+ out[key] = value;
94
+ }
95
+ return out;
96
+ }
97
+
98
+ function renderScript(
99
+ config: ScriptConfig,
100
+ nonce: string | undefined,
101
+ index: number,
102
+ ): ReactNode {
103
+ const { id, src, children, async, defer, type, attributes } = config;
104
+ const key = id ?? src ?? `rango-script-${index}`;
105
+ const attrs = passthroughAttributes(attributes);
106
+
107
+ // Inline: rendered in place (never hoisted), escaped against </script> breakout.
108
+ // The server-only nonce makes the attribute differ from the (undefined) client
109
+ // value, so suppressHydrationWarning is required — the same sanctioned pattern
110
+ // as the theme/Meta inline scripts.
111
+ if (src == null) {
112
+ if (children == null) return null;
113
+ return (
114
+ <script
115
+ key={key}
116
+ {...attrs}
117
+ id={id}
118
+ type={type}
119
+ nonce={nonce}
120
+ suppressHydrationWarning
121
+ dangerouslySetInnerHTML={{ __html: escapeScriptBody(children) }}
122
+ />
123
+ );
124
+ }
125
+
126
+ if (
127
+ process.env.NODE_ENV !== "production" &&
128
+ config.position === "body" &&
129
+ async
130
+ ) {
131
+ console.warn(
132
+ `[Scripts] An async external script (src="${src}") is hoisted into ` +
133
+ `<head> by React; position: "body" is ignored for it.`,
134
+ );
135
+ }
136
+
137
+ // External: async => React-hoisted, src-deduped resource; otherwise in place
138
+ // (defer or plain), preserving authoring order.
139
+ return (
140
+ <script
141
+ key={key}
142
+ {...attrs}
143
+ id={id}
144
+ type={type}
145
+ src={src}
146
+ async={async}
147
+ defer={defer}
148
+ nonce={nonce}
149
+ suppressHydrationWarning
150
+ />
151
+ );
152
+ }
153
+
154
+ export function Scripts({
155
+ position = "head",
156
+ }: { position?: "head" | "body" } = {}): ReactNode {
157
+ const all = useHandle(Script) as ScriptConfig[];
158
+ const nonce = useNonce();
159
+
160
+ const forPosition = all.filter(
161
+ (config) => (config.position ?? "head") === position,
162
+ );
163
+
164
+ // Document-load scripts (inline + ordered external) execute only from the
165
+ // initial HTML, so freeze them to the first-render set. The initializer runs
166
+ // during SSR and again at hydration with the same handle data, so the output
167
+ // matches; afterwards a navigation cannot add an inert <script>.
168
+ const [documentLoad] = useState(() =>
169
+ forPosition.filter((config) => !isAsyncResource(config)),
170
+ );
171
+ // Async external scripts are resources React loads on first encounter; keep
172
+ // them reactive so a script first reached via navigation still loads.
173
+ const asyncResources = forPosition.filter(isAsyncResource);
174
+
175
+ return (
176
+ <>
177
+ {documentLoad.map((config, index) => renderScript(config, nonce, index))}
178
+ {asyncResources.map((config, index) =>
179
+ renderScript(config, nonce, index),
180
+ )}
181
+ </>
182
+ );
183
+ }