@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa

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 (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  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 +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. package/src/router/middleware-cookies.ts +0 -55
@@ -20,7 +20,7 @@
20
20
  * - The build-only `@rangojs/router:version` virtual and `@vitejs/plugin-rsc/rsc`
21
21
  * (whose real body imports unresolvable Vite virtuals) are stubbed.
22
22
  * - Cloudflare apps additionally import the `cloudflare:workers` /
23
- * `cloudflare:email` runtime virtuals; pass `{ cloudflare: true }` to stub them.
23
+ * `cloudflare:email` runtime virtuals; pass `{ preset: "cloudflare" }` to stub them.
24
24
  *
25
25
  * Usage (recommended one-call form — see {@link rangoTestConfig}):
26
26
  *
@@ -34,7 +34,7 @@
34
34
  * globals: true,
35
35
  * include: ["test/**\/*.test.{ts,tsx}"],
36
36
  * environment: "node",
37
- * ...rangoTestConfig({ cloudflare: true }),
37
+ * ...rangoTestConfig({ preset: "cloudflare" }),
38
38
  * },
39
39
  * });
40
40
  * ```
@@ -47,16 +47,32 @@
47
47
  * `rangoTestAliases` directly only if you wire `deps.inline` yourself.
48
48
  *
49
49
  * Notes:
50
- * - This is for the node/DOM project. The Flight project (real RSC rendering via
51
- * `@rangojs/router/testing/flight`) uses the `react-server` condition and pure
52
- * leaf server components it does NOT use this alias (which would crash under
53
- * the server React build). See the testing guide for the Flight config.
50
+ * - The Flight project (real RSC rendering via `@rangojs/router/testing/flight`)
51
+ * uses the `react-server` condition AND needs this same alias whenever a
52
+ * rendered handler/component imports a server API (`getRequestContext`,
53
+ * `cookies`) from the bare `@rangojs/router` without it that import resolves
54
+ * to the throwing out-of-react-server stub (`resolve.conditions` alone is not
55
+ * reliably applied to bare-package export resolution). The alias points at
56
+ * `index.rsc.ts` (the real react-server build) and leaves React itself
57
+ * untouched, so it does NOT crash the server React build. The router's OWN
58
+ * Flight tests omit it only because they import via RELATIVE paths, not the
59
+ * bare specifier; a consumer importing the bare specifier must include it. See
60
+ * the testing skill (`skills/testing/setup.md`, shipped in the package) for
61
+ * the complete Flight config.
54
62
  * - `renderRoute` (`@rangojs/router/testing/dom`) tests run in this same project
55
63
  * under a DOM environment (`happy-dom`/`jsdom`); the alias does not affect them.
56
- * - LIMITATION: the FULL app router still cannot be imported if it uses
57
- * `Prerender()` / `createLoader()` (their build-time-injected `$$id` is absent
58
- * in a bare test). Build a router from an importable, Prerender-free include for
59
- * `dispatch`, or assert whole-router behavior with e2e.
64
+ * - A router using `Prerender()` / `createLoader()` / `Static()` now CONSTRUCTS in
65
+ * a bare test: each assigns a process-stable runtime fallback `$$id` ONLY under
66
+ * a test runner (`process.env.VITEST`), so `createRouter().routes(...)` builds
67
+ * without the "missing `$$id`" throw (for `dispatch` / `assertGeneratedRoutesMatch`).
68
+ * Outside a test runner (a real build) a missing id still THROWS — so an
69
+ * unsupported handler shape the plugin skipped (e.g. `export let`) fails loud
70
+ * rather than getting a silent synthetic id. (The plugin always injects for
71
+ * supported `export const` shapes, and the static manifest keys on that id.)
72
+ * - Importing your app's whole router *file* can still fail for app-specific
73
+ * reasons (page modules pulling their own deps, or plugin `virtual:` modules
74
+ * that need the rango plugin) — build whole-router `dispatch`/drift checks from
75
+ * a focused include, or use e2e.
60
76
  */
61
77
 
62
78
  import { fileURLToPath } from "node:url";
@@ -70,11 +86,13 @@ export interface TestAlias {
70
86
  /** Options for {@link rangoTestAliases}. */
71
87
  export interface RangoTestAliasOptions {
72
88
  /**
73
- * Stub the Cloudflare Workers runtime virtuals (`cloudflare:workers` /
74
- * `cloudflare:email`). Enable for a Cloudflare app whose route tree imports
75
- * them. Default: false.
89
+ * Deployment preset, matching `rango({ preset })` in the Vite plugin. With
90
+ * `"cloudflare"` the helper additionally stubs the Cloudflare Workers runtime
91
+ * virtuals (`cloudflare:workers` / `cloudflare:email`) a CF app's route tree
92
+ * imports. A string (not a boolean) so more presets can be added without an
93
+ * API change. Default: `"node"`.
76
94
  */
77
- cloudflare?: boolean;
95
+ preset?: "node" | "cloudflare";
78
96
  }
79
97
 
80
98
  /**
@@ -113,7 +131,7 @@ export function rangoTestAliases(
113
131
  },
114
132
  ];
115
133
 
116
- if (opts.cloudflare) {
134
+ if (opts.preset === "cloudflare") {
117
135
  aliases.push(
118
136
  {
119
137
  find: "cloudflare:workers",
@@ -166,7 +184,7 @@ export interface RangoTestConfig {
166
184
  * globals: true,
167
185
  * include: ["test/**\/*.test.{ts,tsx}"],
168
186
  * environment: "node",
169
- * ...rangoTestConfig({ cloudflare: true }),
187
+ * ...rangoTestConfig({ preset: "cloudflare" }),
170
188
  * },
171
189
  * });
172
190
  * ```
@@ -181,3 +199,107 @@ export function rangoTestConfig(
181
199
  server: { deps: { inline: [...rangoInlineDeps] } },
182
200
  };
183
201
  }
202
+
203
+ /** A minimal Vite plugin shape (avoids a hard dependency on Vite's types). */
204
+ interface FlightTransformPlugin {
205
+ name: string;
206
+ transform(
207
+ code: string,
208
+ id: string,
209
+ ): Promise<{ code: string; map: unknown } | undefined>;
210
+ }
211
+
212
+ /**
213
+ * A Vite plugin for the FLIGHT (react-server) Vitest project that applies the
214
+ * `"use client"` transform to a consumer's client modules — the same transform a
215
+ * real build applies. With it, `renderServerTree` (`@rangojs/router/testing/flight`)
216
+ * resolves client islands AUTOMATICALLY from the server tree's own imports: no
217
+ * `clientComponents` to pass, no filename convention. Without it, a `"use client"`
218
+ * module is imported as a plain (unmarked) function and would render server-side,
219
+ * so you must list islands via `renderServerTree(..., { clientComponents })`.
220
+ *
221
+ * Add it to your react-server Vitest project. This is the COMPLETE config — the
222
+ * alias, `server.deps.inline`, and `NODE_ENV` are load-bearing, not optional (see
223
+ * the inline notes). The testing skill (`skills/testing/setup.md`, shipped in the
224
+ * package) has the annotated walkthrough.
225
+ *
226
+ * ```ts
227
+ * // vitest.rsc.config.ts
228
+ * import { defineConfig } from "vitest/config";
229
+ * import {
230
+ * rangoUseClientTransform,
231
+ * rangoTestAliases,
232
+ * rangoInlineDeps,
233
+ * } from "@rangojs/router/testing/vitest";
234
+ *
235
+ * // Flight serialization needs React's production build; the dev build's jsxDEV
236
+ * // crashes / yields unstable snapshots.
237
+ * process.env.NODE_ENV = "production";
238
+ *
239
+ * export default defineConfig({
240
+ * plugins: [rangoUseClientTransform()],
241
+ * resolve: {
242
+ * conditions: ["react-server"],
243
+ * // Bare `@rangojs/router` -> its react-server build, so a handler/component
244
+ * // reading getRequestContext()/cookies() resolves the real impl, not the
245
+ * // throwing stub. Pass { preset: "cloudflare" } for a CF app.
246
+ * alias: rangoTestAliases(),
247
+ * },
248
+ * test: {
249
+ * include: ["test/**\/*.rsc-test.{ts,tsx}"],
250
+ * pool: "forks",
251
+ * execArgv: ["--conditions=react-server"],
252
+ * // Required for an INSTALLED consumer on Node >= 23 (rango ships TS source).
253
+ * server: { deps: { inline: rangoInlineDeps } },
254
+ * },
255
+ * });
256
+ * ```
257
+ *
258
+ * Each `"use client"` module's exports are replaced with client references keyed
259
+ * by the module's absolute path (the boundary id), the export name becoming the
260
+ * boundary name. Modules without the directive (server components) are untouched,
261
+ * so `renderToFlightString` of pure leaf trees is unaffected.
262
+ */
263
+ export function rangoUseClientTransform(): FlightTransformPlugin {
264
+ return {
265
+ name: "rango:testing-use-client",
266
+ async transform(code, id) {
267
+ if (id.includes("/node_modules/")) return undefined;
268
+ // Fast path: only parse modules that mention the directive.
269
+ if (!code.includes("use client")) return undefined;
270
+ const { parseAstAsync } = await import("vite");
271
+ const { hasDirective, transformDirectiveProxyExport } =
272
+ await import("@vitejs/plugin-rsc/transforms");
273
+ // vite's parser and the transforms ship structurally-compatible but
274
+ // distinctly-typed ASTs (oxc vs estree); cast through the transform's own
275
+ // parameter type, exactly as plugin-rsc does at runtime.
276
+ type TransformAst = Parameters<typeof transformDirectiveProxyExport>[0];
277
+ let ast: TransformAst;
278
+ try {
279
+ ast = (await parseAstAsync(code)) as unknown as TransformAst;
280
+ } catch {
281
+ return undefined;
282
+ }
283
+ if (!hasDirective(ast.body, "use client")) return undefined;
284
+ const result = transformDirectiveProxyExport(ast, {
285
+ directive: "use client",
286
+ code,
287
+ runtime: (name: string) =>
288
+ `$$RangoRSD.registerClientReference(` +
289
+ `() => { throw new Error("client reference " + ${JSON.stringify(name)} + " is not callable on the server"); }, ` +
290
+ `${JSON.stringify(id)}, ${JSON.stringify(name)})`,
291
+ });
292
+ if (!result) return undefined;
293
+ const { output } = result;
294
+ // The vendored server serializer is the one renderToFlightString uses;
295
+ // resolvable here under the react-server condition.
296
+ output.prepend(
297
+ `import * as $$RangoRSD from "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge";\n`,
298
+ );
299
+ return {
300
+ code: output.toString(),
301
+ map: output.generateMap({ hires: true }),
302
+ };
303
+ },
304
+ };
305
+ }
@@ -26,11 +26,8 @@ import type {
26
26
  ThemeContextValue,
27
27
  ThemeProviderProps,
28
28
  } from "./types.js";
29
- import { THEME_COOKIE } from "./constants.js";
29
+ import { THEME_COOKIE, isValidTheme, warnInvalidTheme } from "./constants.js";
30
30
 
31
- /**
32
- * Get system preference for color scheme
33
- */
34
31
  function getSystemTheme(): ResolvedTheme {
35
32
  if (typeof window !== "undefined" && window.matchMedia) {
36
33
  return window.matchMedia("(prefers-color-scheme: dark)").matches
@@ -40,9 +37,6 @@ function getSystemTheme(): ResolvedTheme {
40
37
  return "light";
41
38
  }
42
39
 
43
- /**
44
- * Read theme from cookie
45
- */
46
40
  function readThemeFromCookie(storageKey: string): string | null {
47
41
  if (typeof document === "undefined") return null;
48
42
 
@@ -61,9 +55,6 @@ function readThemeFromCookie(storageKey: string): string | null {
61
55
  return null;
62
56
  }
63
57
 
64
- /**
65
- * Read theme from localStorage
66
- */
67
58
  function readThemeFromStorage(storageKey: string): string | null {
68
59
  if (typeof localStorage === "undefined") return null;
69
60
 
@@ -74,9 +65,6 @@ function readThemeFromStorage(storageKey: string): string | null {
74
65
  }
75
66
  }
76
67
 
77
- /**
78
- * Write theme to cookie
79
- */
80
68
  function writeThemeToCookie(storageKey: string, theme: Theme): void {
81
69
  if (typeof document === "undefined") return;
82
70
 
@@ -85,9 +73,6 @@ function writeThemeToCookie(storageKey: string, theme: Theme): void {
85
73
  document.cookie = cookie;
86
74
  }
87
75
 
88
- /**
89
- * Write theme to localStorage
90
- */
91
76
  function writeThemeToStorage(storageKey: string, theme: Theme): void {
92
77
  if (typeof localStorage === "undefined") return;
93
78
 
@@ -98,9 +83,6 @@ function writeThemeToStorage(storageKey: string, theme: Theme): void {
98
83
  }
99
84
  }
100
85
 
101
- /**
102
- * Apply theme to HTML element
103
- */
104
86
  function applyThemeToDocument(theme: Theme, config: ResolvedThemeConfig): void {
105
87
  if (typeof document === "undefined") return;
106
88
 
@@ -112,86 +94,83 @@ function applyThemeToDocument(theme: Theme, config: ResolvedThemeConfig): void {
112
94
  const value = config.value[resolved] || resolved;
113
95
  const el = document.documentElement;
114
96
 
115
- // Apply attribute
116
97
  if (config.attribute === "class") {
117
- // Remove all theme classes
118
98
  for (const t of config.themes) {
119
99
  const v = config.value[t] || t;
120
100
  el.classList.remove(v);
121
101
  }
122
- // Add current theme class
123
102
  el.classList.add(value);
124
103
  } else {
125
104
  el.setAttribute(config.attribute, value);
126
105
  }
127
106
 
128
- // Set color-scheme for native dark mode support
129
107
  if (config.enableColorScheme) {
130
108
  el.style.colorScheme = resolved;
131
109
  }
132
110
  }
133
111
 
134
- /**
135
- * Get the resolved stored theme (validated against available themes)
136
- */
137
- function getStoredTheme(config: ResolvedThemeConfig): Theme {
138
- const { storageKey, themes, defaultTheme, enableSystem } = config;
139
-
140
- // Try cookie first (for SSR consistency)
141
- let stored = readThemeFromCookie(storageKey);
142
-
143
- // Fall back to localStorage
144
- if (!stored) {
145
- stored = readThemeFromStorage(storageKey);
146
- }
147
-
148
- // Validate stored value
149
- if (stored) {
150
- if (stored === "system" && enableSystem) {
151
- return "system";
152
- }
153
- if (themes.includes(stored)) {
154
- return stored as Theme;
155
- }
156
- }
157
-
158
- return defaultTheme;
159
- }
160
-
161
- /**
162
- * ThemeProvider component
163
- *
164
- * Provides theme state to the component tree via context.
165
- * Handles theme persistence, system preference detection, and cross-tab sync.
166
- */
167
112
  export function ThemeProvider({
168
113
  config,
169
114
  initialTheme,
170
115
  children,
171
116
  }: ThemeProviderProps): React.ReactNode {
172
- // Track mount state to avoid hydration mismatches
173
- // During SSR and initial hydration, mounted is false
174
117
  const [mounted, setMounted] = useState(false);
175
118
 
176
- // Initialize theme from prop, storage, or default
177
- const [theme, setThemeState] = useState<Theme>(() => {
178
- if (initialTheme) return initialTheme;
179
- if (typeof window === "undefined") return config.defaultTheme;
180
- return getStoredTheme(config);
181
- });
119
+ // HYDRATION PARITY: this initializer is the server (SSR/resume) render AND
120
+ // the client's hydration render both must produce the same value. It must
121
+ // NEVER read cookie/localStorage: whenever the payload's initialTheme
122
+ // differs from the visitor's stored theme (a PPR shell HIT deliberately
123
+ // replays the CAPTURE's initialTheme), a storage-reading initializer makes
124
+ // the client's first render diverge from the server tree. Any raw-theme text
125
+ // (a toggle label) then fails hydration, React regenerates the tree, and the
126
+ // FOUC-applied class is wiped from <html>. The visitor's stored theme is
127
+ // applied by the post-mount re-sync effect below instead.
128
+ const [theme, setThemeState] = useState<Theme>(
129
+ () => initialTheme ?? config.defaultTheme,
130
+ );
182
131
 
183
- // Track system preference - use stable default during SSR
184
132
  const [systemTheme, setSystemTheme] = useState<ResolvedTheme>("light");
185
133
 
186
- // Set mounted after hydration and detect actual system theme
187
134
  useEffect(() => {
188
135
  setMounted(true);
189
136
  setSystemTheme(getSystemTheme());
137
+ // Re-sync state from an EXPLICITLY stored theme after mount. initialTheme
138
+ // comes from the payload and can legitimately differ from the visitor's
139
+ // stored theme — on a PPR shell HIT it is deliberately the CAPTURE's theme
140
+ // (the resume tree must match the frozen prelude; see
141
+ // ShellCacheEntry.initialTheme). The FOUC script already applied the stored
142
+ // theme to the document pre-paint; this brings the provider state (toggles,
143
+ // useTheme readers) in line with it, and is the ONLY place the provider
144
+ // reads storage (the initializer must not — see the parity note above).
145
+ // Only an explicit cookie/localStorage value re-syncs — a defaultTheme
146
+ // fallback must not override a server-provided initialTheme when the
147
+ // visitor never chose a theme.
148
+ // First VALID candidate wins — an empty/garbage cookie value (e.g. a
149
+ // deleted cookie leaving "theme=") must not shadow a valid localStorage
150
+ // value behind a bare null-coalesce.
151
+ const explicit =
152
+ [
153
+ readThemeFromCookie(config.storageKey),
154
+ readThemeFromStorage(config.storageKey),
155
+ ].find((v) => v !== null && isValidTheme(v, config)) ?? null;
156
+ if (explicit !== null && explicit !== theme) {
157
+ setThemeState(explicit as Theme);
158
+ applyThemeToDocument(explicit as Theme, config);
159
+ }
160
+ // eslint-disable-next-line react-hooks/exhaustive-deps
190
161
  }, []);
191
162
 
192
- // Set theme and persist to storage
193
163
  const setTheme = useCallback(
194
164
  (newTheme: Theme) => {
165
+ // Shared guard (isValidTheme) used by the server ctx.setTheme too: reject
166
+ // any value not in the configured theme set, AND reject "system" when
167
+ // system detection is off (applyThemeToDocument would write a bogus
168
+ // class="system"). Keeps the cookie from holding a value the server would
169
+ // reinterpret as defaultTheme on the next SSR (desyncing markup).
170
+ if (!isValidTheme(newTheme, config)) {
171
+ warnInvalidTheme(newTheme, config);
172
+ return;
173
+ }
195
174
  setThemeState(newTheme);
196
175
  writeThemeToCookie(config.storageKey, newTheme);
197
176
  writeThemeToStorage(config.storageKey, newTheme);
@@ -200,7 +179,6 @@ export function ThemeProvider({
200
179
  [config],
201
180
  );
202
181
 
203
- // Listen for system preference changes
204
182
  useEffect(() => {
205
183
  if (!config.enableSystem) return;
206
184
  if (typeof window === "undefined" || !window.matchMedia) return;
@@ -211,13 +189,11 @@ export function ThemeProvider({
211
189
  const newSystemTheme = e.matches ? "dark" : "light";
212
190
  setSystemTheme(newSystemTheme);
213
191
 
214
- // If current theme is "system", re-apply to update document
215
192
  if (theme === "system") {
216
193
  applyThemeToDocument("system", config);
217
194
  }
218
195
  };
219
196
 
220
- // Modern browsers
221
197
  mediaQuery.addEventListener("change", handleChange);
222
198
 
223
199
  return () => {
@@ -225,7 +201,6 @@ export function ThemeProvider({
225
201
  };
226
202
  }, [config, theme]);
227
203
 
228
- // Cross-tab synchronization via localStorage storage event
229
204
  useEffect(() => {
230
205
  if (typeof window === "undefined") return;
231
206
 
@@ -235,11 +210,16 @@ export function ThemeProvider({
235
210
  const newTheme = e.newValue;
236
211
  if (!newTheme) return;
237
212
 
238
- // Validate and apply
239
- if (newTheme === "system" || config.themes.includes(newTheme)) {
240
- setThemeState(newTheme as Theme);
241
- applyThemeToDocument(newTheme as Theme, config);
242
- }
213
+ // A cross-tab storage event can carry any value (another tab, or stale
214
+ // localStorage). Reuse the shared validity rule: reject anything not a
215
+ // configured theme, AND reject "system" when system detection is off (it
216
+ // would apply a bogus class="system"). An invalid received value falls back
217
+ // to defaultTheme rather than applying as-is.
218
+ const applied: Theme = isValidTheme(newTheme, config)
219
+ ? (newTheme as Theme)
220
+ : config.defaultTheme;
221
+ setThemeState(applied);
222
+ applyThemeToDocument(applied, config);
243
223
  };
244
224
 
245
225
  window.addEventListener("storage", handleStorageChange);
@@ -249,12 +229,8 @@ export function ThemeProvider({
249
229
  };
250
230
  }, [config]);
251
231
 
252
- // Compute resolved theme
253
- // During SSR (not mounted), use the initial theme or default to avoid hydration mismatch
254
232
  const resolvedTheme: ResolvedTheme = useMemo(() => {
255
233
  if (!mounted) {
256
- // During SSR, return the initial theme if it's not "system", otherwise "light"
257
- // The inline script will apply the correct class before hydration
258
234
  if (initialTheme && initialTheme !== "system") {
259
235
  return initialTheme as ResolvedTheme;
260
236
  }
@@ -266,7 +242,6 @@ export function ThemeProvider({
266
242
  return theme as ResolvedTheme;
267
243
  }, [theme, systemTheme, config.enableSystem, mounted, initialTheme]);
268
244
 
269
- // Build themes list (include "system" if enabled)
270
245
  const themes = useMemo(() => {
271
246
  if (config.enableSystem) {
272
247
  return ["system", ...config.themes.filter((t) => t !== "system")];
@@ -274,14 +249,11 @@ export function ThemeProvider({
274
249
  return config.themes;
275
250
  }, [config.themes, config.enableSystem]);
276
251
 
277
- // Context value
278
- // During SSR (not mounted), return stable values to avoid hydration mismatch
279
252
  const contextValue: ThemeContextValue = useMemo(
280
253
  () => ({
281
254
  theme,
282
255
  setTheme,
283
256
  resolvedTheme,
284
- // Return stable "light" for systemTheme during SSR - actual value updates after mount
285
257
  systemTheme: mounted ? systemTheme : "light",
286
258
  themes,
287
259
  config,
@@ -8,17 +8,21 @@
8
8
  *
9
9
  * Must be placed in the <head> element of your document, before any stylesheets.
10
10
  *
11
+ * Note: when theme is enabled in the router config, `<MetaTags />` ALREADY
12
+ * renders this FOUC script. Use `<ThemeScript />` only if you do NOT render
13
+ * `<MetaTags />`. Rendering both is safe — the inline script guards the
14
+ * matchMedia listener registration against double-running — but it is redundant.
15
+ *
11
16
  * @example
12
17
  * ```tsx
13
- * // In your document component
18
+ * // In your document component. Use ThemeScript only when you do not render MetaTags.
14
19
  * import { ThemeScript } from "@rangojs/router/theme";
15
20
  *
16
21
  * export function Document({ children }) {
17
22
  * return (
18
23
  * <html lang="en" suppressHydrationWarning>
19
24
  * <head>
20
- * <ThemeScript />
21
- * <MetaTags />
25
+ * <ThemeScript config={config} />
22
26
  * </head>
23
27
  * <body>{children}</body>
24
28
  * </html>
@@ -43,12 +47,6 @@ export interface ThemeScriptProps {
43
47
  nonce?: string;
44
48
  }
45
49
 
46
- /**
47
- * Server component that renders the theme initialization script.
48
- *
49
- * This renders a synchronous inline script that applies the theme
50
- * to the HTML element before React hydration, preventing FOUC.
51
- */
52
50
  export function ThemeScript({
53
51
  config,
54
52
  nonce,
@@ -2,11 +2,8 @@
2
2
  * Default values for theme configuration
3
3
  */
4
4
 
5
- import type { ResolvedThemeConfig, ThemeConfig } from "./types.js";
5
+ import type { ResolvedThemeConfig, Theme, ThemeConfig } from "./types.js";
6
6
 
7
- /**
8
- * Default theme configuration values
9
- */
10
7
  export const THEME_DEFAULTS = {
11
8
  defaultTheme: "system",
12
9
  themes: ["light", "dark"],
@@ -16,9 +13,6 @@ export const THEME_DEFAULTS = {
16
13
  enableColorScheme: true,
17
14
  } as const;
18
15
 
19
- /**
20
- * Cookie configuration for theme persistence
21
- */
22
16
  export const THEME_COOKIE: {
23
17
  readonly maxAge: number;
24
18
  readonly path: string;
@@ -30,31 +24,76 @@ export const THEME_COOKIE: {
30
24
  };
31
25
 
32
26
  /**
33
- * Resolve theme config by applying defaults.
34
- * Accepts `true` to enable with all defaults, or a config object.
27
+ * Single owner of the setTheme validity rule, shared by the client
28
+ * (ThemeProvider) and server (ctx.setTheme) guards so they cannot drift.
29
+ *
30
+ * A theme is valid when it is one of the configured concrete themes, OR
31
+ * "system" but only while system detection is enabled. Rejecting "system" when
32
+ * `enableSystem` is false is load-bearing: applyThemeToDocument would otherwise
33
+ * leave "system" unresolved and write a bogus class="system" / colorScheme
34
+ * ="system" on <html> (the same bogus value resolveThemeConfig coerces away for
35
+ * the default).
36
+ */
37
+ export function isValidTheme(
38
+ theme: string,
39
+ config: Pick<ResolvedThemeConfig, "themes" | "enableSystem">,
40
+ ): boolean {
41
+ if (theme === "system") return config.enableSystem;
42
+ return config.themes.includes(theme);
43
+ }
44
+
45
+ /**
46
+ * Emit the shared "[Theme] Invalid theme value" warning. One owner of the
47
+ * message string so the client and server guards stay byte-identical.
48
+ *
49
+ * The valid-values list mirrors isValidTheme: "system" is only listed when
50
+ * enableSystem is true, otherwise the message would advertise a value the guard
51
+ * itself rejects.
35
52
  */
53
+ export function warnInvalidTheme(
54
+ theme: string,
55
+ config: Pick<ResolvedThemeConfig, "themes" | "enableSystem">,
56
+ ): void {
57
+ const validValues = config.enableSystem
58
+ ? ["system", ...config.themes]
59
+ : config.themes;
60
+ console.warn(
61
+ `[Theme] Invalid theme value: "${theme}". Valid values: ${validValues.join(", ")}`,
62
+ );
63
+ }
64
+
36
65
  export function resolveThemeConfig(
37
66
  config: ThemeConfig | true,
38
67
  ): ResolvedThemeConfig {
39
- // Handle `theme: true` shorthand
40
68
  if (config === true) {
41
69
  config = {};
42
70
  }
43
71
 
44
72
  const themes = config.themes ?? [...THEME_DEFAULTS.themes];
45
73
 
46
- // Build value mapping - default to identity mapping
47
74
  const value: Record<string, string> = {};
48
75
  for (const theme of themes) {
49
76
  value[theme] = config.value?.[theme] ?? theme;
50
77
  }
51
78
 
79
+ const enableSystem = config.enableSystem ?? THEME_DEFAULTS.enableSystem;
80
+
81
+ // When system detection is disabled, "system" is not a valid resolved theme.
82
+ // Coerce both the unset default and an explicit defaultTheme:"system" to the
83
+ // first concrete theme, so the FOUC script / ThemeProvider never apply a bogus
84
+ // class="system" / colorScheme="system" on <html>.
85
+ const requestedDefault = config.defaultTheme ?? THEME_DEFAULTS.defaultTheme;
86
+ const defaultTheme =
87
+ !enableSystem && requestedDefault === "system"
88
+ ? (themes[0] as Theme)
89
+ : requestedDefault;
90
+
52
91
  return {
53
- defaultTheme: config.defaultTheme ?? THEME_DEFAULTS.defaultTheme,
92
+ defaultTheme,
54
93
  themes,
55
94
  attribute: config.attribute ?? THEME_DEFAULTS.attribute,
56
95
  storageKey: config.storageKey ?? THEME_DEFAULTS.storageKey,
57
- enableSystem: config.enableSystem ?? THEME_DEFAULTS.enableSystem,
96
+ enableSystem,
58
97
  enableColorScheme:
59
98
  config.enableColorScheme ?? THEME_DEFAULTS.enableColorScheme,
60
99
  value,
@@ -23,16 +23,10 @@
23
23
  * ```
24
24
  */
25
25
 
26
- // Main hook for accessing theme
27
26
  export { useTheme } from "./use-theme.js";
28
-
29
- // Provider (typically auto-included via NavigationProvider when theme is enabled)
30
27
  export { ThemeProvider } from "./ThemeProvider.js";
31
-
32
- // Script component for FOUC prevention (use in document head)
33
28
  export { ThemeScript, type ThemeScriptProps } from "./ThemeScript.js";
34
29
 
35
- // Types
36
30
  export type {
37
31
  Theme,
38
32
  ResolvedTheme,
@@ -44,5 +38,4 @@ export type {
44
38
  ThemeContextValue,
45
39
  } from "./types.js";
46
40
 
47
- // Constants
48
41
  export { THEME_DEFAULTS, THEME_COOKIE } from "./constants.js";
@@ -19,17 +19,13 @@ import type { ThemeContextValue } from "./types.js";
19
19
  export const ThemeContext: Context<ThemeContextValue | null> =
20
20
  createContext<ThemeContextValue | null>(null);
21
21
 
22
- /**
23
- * Get theme context (internal use)
24
- * Returns null if theme is not enabled
25
- */
26
22
  export function useThemeContext(): ThemeContextValue | null {
27
23
  return useContext(ThemeContext);
28
24
  }
29
25
 
30
26
  /**
31
27
  * Get theme context, throwing if not available
32
- * Use this in useTheme hook
28
+ * Used by useTheme hook
33
29
  */
34
30
  export function requireThemeContext(): ThemeContextValue {
35
31
  const ctx = useContext(ThemeContext);