@rangojs/router 0.0.0-experimental.d20dd405 → 0.0.0-experimental.d98a8e9d

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 (242) hide show
  1. package/README.md +8 -8
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +917 -500
  5. package/package.json +55 -11
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/bundle-analysis/SKILL.md +159 -0
  8. package/skills/cache-guide/SKILL.md +220 -30
  9. package/skills/caching/SKILL.md +116 -8
  10. package/skills/composability/SKILL.md +27 -2
  11. package/skills/document-cache/SKILL.md +78 -55
  12. package/skills/handler-use/SKILL.md +1 -1
  13. package/skills/hooks/SKILL.md +196 -21
  14. package/skills/host-router/SKILL.md +45 -20
  15. package/skills/intercept/SKILL.md +1 -4
  16. package/skills/layout/SKILL.md +4 -7
  17. package/skills/links/SKILL.md +22 -10
  18. package/skills/loader/SKILL.md +166 -23
  19. package/skills/middleware/SKILL.md +13 -9
  20. package/skills/migrate-nextjs/SKILL.md +1 -1
  21. package/skills/mime-routes/SKILL.md +27 -0
  22. package/skills/observability/SKILL.md +137 -0
  23. package/skills/parallel/SKILL.md +3 -6
  24. package/skills/prerender/SKILL.md +14 -33
  25. package/skills/rango/SKILL.md +243 -26
  26. package/skills/react-compiler/SKILL.md +168 -0
  27. package/skills/response-routes/SKILL.md +114 -47
  28. package/skills/route/SKILL.md +9 -4
  29. package/skills/router-setup/SKILL.md +3 -3
  30. package/skills/server-actions/SKILL.md +53 -41
  31. package/skills/testing/SKILL.md +128 -0
  32. package/skills/testing/bindings.md +89 -0
  33. package/skills/testing/cache-prerender.md +98 -0
  34. package/skills/testing/client-components.md +121 -0
  35. package/skills/testing/e2e-parity.md +124 -0
  36. package/skills/testing/flight.md +89 -0
  37. package/skills/testing/handles.md +127 -0
  38. package/skills/testing/loader.md +108 -0
  39. package/skills/testing/middleware.md +97 -0
  40. package/skills/testing/render-handler.md +102 -0
  41. package/skills/testing/response-routes.md +94 -0
  42. package/skills/testing/reverse-and-types.md +83 -0
  43. package/skills/testing/server-actions.md +89 -0
  44. package/skills/testing/server-tree.md +128 -0
  45. package/skills/testing/setup.md +120 -0
  46. package/skills/typesafety/SKILL.md +310 -26
  47. package/skills/use-cache/SKILL.md +34 -5
  48. package/skills/view-transitions/SKILL.md +85 -3
  49. package/src/__augment-tests__/augment.ts +81 -0
  50. package/src/__augment-tests__/augmented.check.ts +116 -0
  51. package/src/browser/action-coordinator.ts +53 -36
  52. package/src/browser/event-controller.ts +42 -66
  53. package/src/browser/history-state.ts +21 -0
  54. package/src/browser/index.ts +3 -3
  55. package/src/browser/navigation-bridge.ts +9 -67
  56. package/src/browser/navigation-client.ts +68 -83
  57. package/src/browser/navigation-store.ts +7 -8
  58. package/src/browser/navigation-transaction.ts +10 -28
  59. package/src/browser/partial-update.ts +8 -16
  60. package/src/browser/prefetch/cache.ts +58 -27
  61. package/src/browser/prefetch/fetch.ts +92 -33
  62. package/src/browser/react/NavigationProvider.tsx +55 -65
  63. package/src/browser/react/location-state-shared.ts +175 -4
  64. package/src/browser/react/location-state.ts +39 -13
  65. package/src/browser/react/use-handle.ts +17 -9
  66. package/src/browser/react/use-params.ts +3 -4
  67. package/src/browser/react/use-reverse.ts +19 -12
  68. package/src/browser/react/use-router.ts +14 -1
  69. package/src/browser/response-adapter.ts +32 -1
  70. package/src/browser/rsc-router.tsx +35 -16
  71. package/src/browser/scroll-restoration.ts +30 -25
  72. package/src/browser/segment-structure-assert.ts +2 -2
  73. package/src/browser/server-action-bridge.ts +23 -30
  74. package/src/browser/types.ts +2 -0
  75. package/src/build/collect-fallback-refs.ts +107 -0
  76. package/src/build/generate-manifest.ts +60 -35
  77. package/src/build/generate-route-types.ts +2 -0
  78. package/src/build/index.ts +8 -1
  79. package/src/build/prefix-tree-utils.ts +123 -0
  80. package/src/build/route-trie.ts +43 -0
  81. package/src/build/route-types/codegen.ts +4 -4
  82. package/src/build/route-types/include-resolution.ts +1 -1
  83. package/src/build/route-types/per-module-writer.ts +7 -4
  84. package/src/build/route-types/router-processing.ts +55 -14
  85. package/src/build/route-types/scan-filter.ts +1 -1
  86. package/src/build/route-types/source-scan.ts +118 -0
  87. package/src/build/runtime-discovery.ts +9 -20
  88. package/src/cache/cache-scope.ts +28 -42
  89. package/src/cache/cf/cf-cache-store.ts +49 -6
  90. package/src/client.tsx +9 -30
  91. package/src/context-var.ts +5 -5
  92. package/src/decode-loader-results.ts +36 -0
  93. package/src/errors.ts +30 -4
  94. package/src/handle.ts +32 -14
  95. package/src/host/index.ts +2 -2
  96. package/src/host/router.ts +129 -57
  97. package/src/host/types.ts +31 -2
  98. package/src/host/utils.ts +1 -1
  99. package/src/href-client.ts +136 -20
  100. package/src/index.rsc.ts +7 -6
  101. package/src/index.ts +14 -8
  102. package/src/loader-store.ts +500 -0
  103. package/src/loader.rsc.ts +25 -7
  104. package/src/loader.ts +16 -9
  105. package/src/missing-id-error.ts +68 -0
  106. package/src/prerender.ts +27 -6
  107. package/src/response-utils.ts +9 -0
  108. package/src/reverse.ts +16 -13
  109. package/src/route-content-wrapper.tsx +6 -28
  110. package/src/route-definition/dsl-helpers.ts +238 -263
  111. package/src/route-definition/helper-factories.ts +29 -139
  112. package/src/route-definition/helpers-types.ts +37 -14
  113. package/src/route-definition/use-item-types.ts +32 -0
  114. package/src/route-types.ts +19 -41
  115. package/src/router/basename.ts +14 -0
  116. package/src/router/content-negotiation.ts +15 -2
  117. package/src/router/error-handling.ts +1 -1
  118. package/src/router/find-match.ts +54 -6
  119. package/src/router/intercept-resolution.ts +4 -18
  120. package/src/router/lazy-includes.ts +35 -16
  121. package/src/router/loader-resolution.ts +79 -36
  122. package/src/router/manifest.ts +19 -6
  123. package/src/router/match-handlers.ts +62 -20
  124. package/src/router/match-middleware/cache-lookup.ts +44 -91
  125. package/src/router/match-middleware/cache-store.ts +3 -2
  126. package/src/router/match-result.ts +32 -30
  127. package/src/router/metrics.ts +1 -1
  128. package/src/router/middleware-types.ts +1 -1
  129. package/src/router/middleware.ts +46 -78
  130. package/src/router/pattern-matching.ts +15 -2
  131. package/src/router/prerender-match.ts +1 -1
  132. package/src/router/preview-match.ts +3 -1
  133. package/src/router/request-classification.ts +4 -28
  134. package/src/router/revalidation.ts +43 -1
  135. package/src/router/router-interfaces.ts +45 -28
  136. package/src/router/router-options.ts +40 -1
  137. package/src/router/router-registry.ts +2 -5
  138. package/src/router/segment-resolution/fresh.ts +19 -6
  139. package/src/router/segment-resolution/revalidation.ts +19 -6
  140. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  141. package/src/router/telemetry.ts +99 -0
  142. package/src/router/trie-matching.ts +22 -3
  143. package/src/router/types.ts +8 -0
  144. package/src/router.ts +51 -28
  145. package/src/rsc/handler-context.ts +2 -2
  146. package/src/rsc/handler.ts +20 -65
  147. package/src/rsc/helpers.ts +22 -2
  148. package/src/rsc/index.ts +1 -1
  149. package/src/rsc/manifest-init.ts +28 -41
  150. package/src/rsc/origin-guard.ts +28 -10
  151. package/src/rsc/response-error.ts +79 -12
  152. package/src/rsc/response-route-handler.ts +43 -60
  153. package/src/rsc/rsc-rendering.ts +27 -53
  154. package/src/rsc/runtime-warnings.ts +9 -10
  155. package/src/rsc/server-action.ts +13 -37
  156. package/src/rsc/ssr-setup.ts +16 -0
  157. package/src/rsc/types.ts +2 -2
  158. package/src/runtime-env.ts +18 -0
  159. package/src/search-params.ts +4 -4
  160. package/src/segment-system.tsx +64 -49
  161. package/src/serialize.ts +243 -0
  162. package/src/server/context.ts +150 -51
  163. package/src/server/cookie-store.ts +28 -4
  164. package/src/server/request-context.ts +57 -9
  165. package/src/static-handler.ts +25 -3
  166. package/src/testing/cache-status.ts +166 -0
  167. package/src/testing/collect-handle.ts +63 -0
  168. package/src/testing/dispatch.ts +581 -0
  169. package/src/testing/dom.entry.ts +22 -0
  170. package/src/testing/e2e/fixture.ts +188 -0
  171. package/src/testing/e2e/index.ts +149 -0
  172. package/src/testing/e2e/matchers.ts +51 -0
  173. package/src/testing/e2e/page-helpers.ts +272 -0
  174. package/src/testing/e2e/parity.ts +326 -0
  175. package/src/testing/e2e/server.ts +195 -0
  176. package/src/testing/flight-matchers.ts +110 -0
  177. package/src/testing/flight-normalize.ts +38 -0
  178. package/src/testing/flight-runtime.d.ts +57 -0
  179. package/src/testing/flight-tree.ts +682 -0
  180. package/src/testing/flight.entry.ts +51 -0
  181. package/src/testing/flight.ts +234 -0
  182. package/src/testing/generated-routes.ts +223 -0
  183. package/src/testing/index.ts +106 -0
  184. package/src/testing/internal/context.ts +304 -0
  185. package/src/testing/internal/flight-client-globals.ts +30 -0
  186. package/src/testing/internal/seed-vars.ts +42 -0
  187. package/src/testing/render-handler.ts +323 -0
  188. package/src/testing/render-route.tsx +590 -0
  189. package/src/testing/run-loader.ts +363 -0
  190. package/src/testing/run-middleware.ts +205 -0
  191. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  192. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  193. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  194. package/src/testing/vitest-stubs/version.ts +5 -0
  195. package/src/testing/vitest.ts +285 -0
  196. package/src/types/global-namespace.ts +39 -26
  197. package/src/types/handler-context.ts +56 -11
  198. package/src/types/index.ts +1 -0
  199. package/src/types/loader-types.ts +6 -3
  200. package/src/types/segments.ts +18 -1
  201. package/src/urls/include-helper.ts +10 -53
  202. package/src/urls/index.ts +1 -5
  203. package/src/urls/path-helper-types.ts +11 -3
  204. package/src/urls/path-helper.ts +17 -52
  205. package/src/urls/pattern-types.ts +36 -19
  206. package/src/urls/response-types.ts +20 -19
  207. package/src/urls/type-extraction.ts +58 -139
  208. package/src/urls/urls-function.ts +1 -5
  209. package/src/use-loader.tsx +413 -42
  210. package/src/vite/debug.ts +1 -0
  211. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  212. package/src/vite/discovery/discover-routers.ts +75 -72
  213. package/src/vite/discovery/discovery-errors.ts +194 -0
  214. package/src/vite/discovery/prerender-collection.ts +19 -25
  215. package/src/vite/discovery/route-types-writer.ts +40 -84
  216. package/src/vite/discovery/state.ts +33 -0
  217. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  218. package/src/vite/index.ts +2 -0
  219. package/src/vite/plugin-types.ts +67 -0
  220. package/src/vite/plugins/cjs-to-esm.ts +3 -7
  221. package/src/vite/plugins/client-ref-hashing.ts +12 -1
  222. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
  223. package/src/vite/plugins/expose-action-id.ts +2 -2
  224. package/src/vite/plugins/expose-id-utils.ts +12 -8
  225. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  226. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  227. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  228. package/src/vite/plugins/expose-internal-ids.ts +47 -67
  229. package/src/vite/plugins/performance-tracks.ts +12 -16
  230. package/src/vite/plugins/use-cache-transform.ts +13 -11
  231. package/src/vite/plugins/version-injector.ts +2 -12
  232. package/src/vite/plugins/version-plugin.ts +59 -2
  233. package/src/vite/plugins/virtual-entries.ts +2 -2
  234. package/src/vite/rango.ts +67 -15
  235. package/src/vite/router-discovery.ts +208 -63
  236. package/src/vite/utils/ast-handler-extract.ts +15 -15
  237. package/src/vite/utils/bundle-analysis.ts +4 -2
  238. package/src/vite/utils/client-chunks.ts +190 -0
  239. package/src/vite/utils/forward-user-plugins.ts +193 -0
  240. package/src/vite/utils/manifest-utils.ts +8 -59
  241. package/src/vite/utils/shared-utils.ts +107 -26
  242. package/src/browser/action-response-classifier.ts +0 -99
@@ -0,0 +1,285 @@
1
+ /**
2
+ * @rangojs/router/testing/vitest
3
+ *
4
+ * Vitest setup helper for the UNIT + INTEGRATION + DOM test project of a
5
+ * @rangojs/router consumer app. It returns the `resolve.alias` entries that make
6
+ * a real app's router / loaders / middleware importable in a bare Vitest process.
7
+ *
8
+ * Why this is needed (the documented "vi.mock(plugin-rsc) + import router"
9
+ * recipe is not sufficient for a real app):
10
+ *
11
+ * - `@rangojs/router` resolves to SERVER-ONLY STUBS outside the `react-server`
12
+ * condition — `urls()`, `createRouter()`, `cookies()`, `getRequestContext()`
13
+ * throw "only available in a react-server environment". Importing the app's own
14
+ * router/loaders/middleware then fails immediately. Vitest does NOT apply the
15
+ * `react-server` condition to bare-package exports resolution, and enabling it
16
+ * globally flips React to its server build (no `createContext`), crashing the
17
+ * router's client-boundary imports. The surgical fix is to alias ONLY the bare
18
+ * `@rangojs/router` specifier to its react-server entry (real impls) while
19
+ * leaving React as the client build — which is exactly what this helper does.
20
+ * - The build-only `@rangojs/router:version` virtual and `@vitejs/plugin-rsc/rsc`
21
+ * (whose real body imports unresolvable Vite virtuals) are stubbed.
22
+ * - Cloudflare apps additionally import the `cloudflare:workers` /
23
+ * `cloudflare:email` runtime virtuals; pass `{ preset: "cloudflare" }` to stub them.
24
+ *
25
+ * Usage (recommended one-call form — see {@link rangoTestConfig}):
26
+ *
27
+ * ```ts
28
+ * // vitest.config.ts
29
+ * import { defineConfig } from "vitest/config";
30
+ * import { rangoTestConfig } from "@rangojs/router/testing/vitest";
31
+ *
32
+ * export default defineConfig({
33
+ * test: {
34
+ * globals: true,
35
+ * include: ["test/**\/*.test.{ts,tsx}"],
36
+ * environment: "node",
37
+ * ...rangoTestConfig({ preset: "cloudflare" }),
38
+ * },
39
+ * });
40
+ * ```
41
+ *
42
+ * `rangoTestConfig` bundles the resolve aliases ({@link rangoTestAliases}) with
43
+ * the `server.deps.inline` contract ({@link rangoInlineDeps}) an installed
44
+ * consumer needs — @rangojs/router ships as TS source, and without `deps.inline`
45
+ * Vitest hands those `.ts` files to Node, which on Node >= 23 throws
46
+ * `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. Use the lower-level
47
+ * `rangoTestAliases` directly only if you wire `deps.inline` yourself.
48
+ *
49
+ * Notes:
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 guide for the full Flight config.
61
+ * - `renderRoute` (`@rangojs/router/testing/dom`) tests run in this same project
62
+ * under a DOM environment (`happy-dom`/`jsdom`); the alias does not affect them.
63
+ * - A router using `Prerender()` / `createLoader()` / `Static()` now CONSTRUCTS in
64
+ * a bare test: each assigns a process-stable runtime fallback `$$id` ONLY under
65
+ * a test runner (`process.env.VITEST`), so `createRouter().routes(...)` builds
66
+ * without the "missing `$$id`" throw (for `dispatch` / `assertGeneratedRoutesMatch`).
67
+ * Outside a test runner (a real build) a missing id still THROWS — so an
68
+ * unsupported handler shape the plugin skipped (e.g. `export let`) fails loud
69
+ * rather than getting a silent synthetic id. (The plugin always injects for
70
+ * supported `export const` shapes, and the static manifest keys on that id.)
71
+ * - Importing your app's whole router *file* can still fail for app-specific
72
+ * reasons (page modules pulling their own deps, or plugin `virtual:` modules
73
+ * that need the rango plugin) — build whole-router `dispatch`/drift checks from
74
+ * a focused include, or use e2e.
75
+ */
76
+
77
+ import { fileURLToPath } from "node:url";
78
+
79
+ /** A single Vite/Vitest resolve alias entry. Structurally a Vite `Alias`. */
80
+ export interface TestAlias {
81
+ find: string | RegExp;
82
+ replacement: string;
83
+ }
84
+
85
+ /** Options for {@link rangoTestAliases}. */
86
+ export interface RangoTestAliasOptions {
87
+ /**
88
+ * Deployment preset, matching `rango({ preset })` in the Vite plugin. With
89
+ * `"cloudflare"` the helper additionally stubs the Cloudflare Workers runtime
90
+ * virtuals (`cloudflare:workers` / `cloudflare:email`) a CF app's route tree
91
+ * imports. A string (not a boolean) so more presets can be added without an
92
+ * API change. Default: `"node"`.
93
+ */
94
+ preset?: "node" | "cloudflare";
95
+ }
96
+
97
+ /**
98
+ * Resolve a path relative to this module. Anchored at the PACKAGE ROOT
99
+ * (`../../` from both `src/testing/vitest.ts` and the shipped
100
+ * `dist/testing/vitest.js` — each is two levels below the root), so the alias
101
+ * targets always point at the `src/*.ts` files Vite transpiles at test time,
102
+ * regardless of whether this helper is loaded as source (in-repo) or as the
103
+ * compiled `dist` entry (an installed consumer).
104
+ */
105
+ function here(relativeFromRoot: string): string {
106
+ return fileURLToPath(new URL(`../../${relativeFromRoot}`, import.meta.url));
107
+ }
108
+
109
+ /**
110
+ * Build the `resolve.alias` entries a consumer's node/DOM Vitest project needs to
111
+ * import a real @rangojs/router app's router/loaders/middleware. Spread into a
112
+ * Vitest config: `resolve: { alias: rangoTestAliases(...) }` (concat your own
113
+ * aliases as needed).
114
+ */
115
+ export function rangoTestAliases(
116
+ opts: RangoTestAliasOptions = {},
117
+ ): TestAlias[] {
118
+ const aliases: TestAlias[] = [
119
+ // Real impls (index.rsc.ts) for the bare specifier ONLY — exact regex so
120
+ // subpaths (/testing, /client, /cache, ...) are untouched. React stays the
121
+ // client build, so createContext and "use client" modules work.
122
+ { find: /^@rangojs\/router$/, replacement: here("src/index.rsc.ts") },
123
+ {
124
+ find: "@rangojs/router:version",
125
+ replacement: here("src/testing/vitest-stubs/version.ts"),
126
+ },
127
+ {
128
+ find: /^@vitejs\/plugin-rsc\/rsc$/,
129
+ replacement: here("src/testing/vitest-stubs/plugin-rsc.ts"),
130
+ },
131
+ ];
132
+
133
+ if (opts.preset === "cloudflare") {
134
+ aliases.push(
135
+ {
136
+ find: "cloudflare:workers",
137
+ replacement: here("src/testing/vitest-stubs/cloudflare-workers.ts"),
138
+ },
139
+ {
140
+ find: "cloudflare:email",
141
+ replacement: here("src/testing/vitest-stubs/cloudflare-email.ts"),
142
+ },
143
+ );
144
+ }
145
+
146
+ return aliases;
147
+ }
148
+
149
+ /**
150
+ * Vitest `server.deps.inline` patterns that force Vite (not Node) to transpile
151
+ * @rangojs/router's TypeScript source under test.
152
+ *
153
+ * REQUIRED for an installed (node_modules) consumer: @rangojs/router ships as TS
154
+ * source, and Vitest externalizes node_modules by default — so without this Node
155
+ * loads the `.ts` files directly and, on Node >= 23, throws
156
+ * `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. In this monorepo it is a no-op
157
+ * (the workspace symlink resolves to a realpath outside node_modules, which Vite
158
+ * already transpiles), which is precisely why an in-repo dogfood never surfaces
159
+ * the need and the contract has to be shipped explicitly.
160
+ */
161
+ export const rangoInlineDeps: RegExp[] = [/@rangojs[/\\]router/];
162
+
163
+ /** The Vitest `test`-block fragment {@link rangoTestConfig} returns. */
164
+ export interface RangoTestConfig {
165
+ alias: TestAlias[];
166
+ server: { deps: { inline: RegExp[] } };
167
+ }
168
+
169
+ /**
170
+ * The complete Vitest `test`-block fragment a consumer needs: the resolve
171
+ * aliases ({@link rangoTestAliases}) AND the `server.deps.inline` contract
172
+ * ({@link rangoInlineDeps}). Spread it into your `test` block so both land in
173
+ * one place and a consumer cannot forget the `deps.inline` half (omitting it
174
+ * loads rango's TS source through Node and breaks on Node >= 23):
175
+ *
176
+ * ```ts
177
+ * // vitest.config.ts
178
+ * import { defineConfig } from "vitest/config";
179
+ * import { rangoTestConfig } from "@rangojs/router/testing/vitest";
180
+ *
181
+ * export default defineConfig({
182
+ * test: {
183
+ * globals: true,
184
+ * include: ["test/**\/*.test.{ts,tsx}"],
185
+ * environment: "node",
186
+ * ...rangoTestConfig({ preset: "cloudflare" }),
187
+ * },
188
+ * });
189
+ * ```
190
+ */
191
+ export function rangoTestConfig(
192
+ opts: RangoTestAliasOptions = {},
193
+ ): RangoTestConfig {
194
+ return {
195
+ alias: rangoTestAliases(opts),
196
+ // fresh copy so the shared rangoInlineDeps const is never aliased into (or
197
+ // mutated through) a consumer's resolved config
198
+ server: { deps: { inline: [...rangoInlineDeps] } },
199
+ };
200
+ }
201
+
202
+ /** A minimal Vite plugin shape (avoids a hard dependency on Vite's types). */
203
+ interface FlightTransformPlugin {
204
+ name: string;
205
+ transform(
206
+ code: string,
207
+ id: string,
208
+ ): Promise<{ code: string; map: unknown } | undefined>;
209
+ }
210
+
211
+ /**
212
+ * A Vite plugin for the FLIGHT (react-server) Vitest project that applies the
213
+ * `"use client"` transform to a consumer's client modules — the same transform a
214
+ * real build applies. With it, `renderServerTree` (`@rangojs/router/testing/flight`)
215
+ * resolves client islands AUTOMATICALLY from the server tree's own imports: no
216
+ * `clientComponents` to pass, no filename convention. Without it, a `"use client"`
217
+ * module is imported as a plain (unmarked) function and would render server-side,
218
+ * so you must list islands via `renderServerTree(..., { clientComponents })`.
219
+ *
220
+ * Add it to your react-server Vitest project:
221
+ *
222
+ * ```ts
223
+ * // vitest.rsc.config.ts
224
+ * import { defineConfig } from "vitest/config";
225
+ * import { rangoUseClientTransform } from "@rangojs/router/testing/vitest";
226
+ *
227
+ * export default defineConfig({
228
+ * resolve: { conditions: ["react-server"] },
229
+ * plugins: [rangoUseClientTransform()],
230
+ * test: {
231
+ * include: ["test/**\/*.rsc-test.{ts,tsx}"],
232
+ * pool: "forks",
233
+ * execArgv: ["--conditions=react-server"],
234
+ * },
235
+ * });
236
+ * ```
237
+ *
238
+ * Each `"use client"` module's exports are replaced with client references keyed
239
+ * by the module's absolute path (the boundary id), the export name becoming the
240
+ * boundary name. Modules without the directive (server components) are untouched,
241
+ * so `renderToFlightString` of pure leaf trees is unaffected.
242
+ */
243
+ export function rangoUseClientTransform(): FlightTransformPlugin {
244
+ return {
245
+ name: "rango:testing-use-client",
246
+ async transform(code, id) {
247
+ if (id.includes("/node_modules/")) return undefined;
248
+ // Fast path: only parse modules that mention the directive.
249
+ if (!code.includes("use client")) return undefined;
250
+ const { parseAstAsync } = await import("vite");
251
+ const { hasDirective, transformDirectiveProxyExport } =
252
+ await import("@vitejs/plugin-rsc/transforms");
253
+ // vite's parser and the transforms ship structurally-compatible but
254
+ // distinctly-typed ASTs (oxc vs estree); cast through the transform's own
255
+ // parameter type, exactly as plugin-rsc does at runtime.
256
+ type TransformAst = Parameters<typeof transformDirectiveProxyExport>[0];
257
+ let ast: TransformAst;
258
+ try {
259
+ ast = (await parseAstAsync(code)) as unknown as TransformAst;
260
+ } catch {
261
+ return undefined;
262
+ }
263
+ if (!hasDirective(ast.body, "use client")) return undefined;
264
+ const result = transformDirectiveProxyExport(ast, {
265
+ directive: "use client",
266
+ code,
267
+ runtime: (name: string) =>
268
+ `$$RangoRSD.registerClientReference(` +
269
+ `() => { throw new Error("client reference " + ${JSON.stringify(name)} + " is not callable on the server"); }, ` +
270
+ `${JSON.stringify(id)}, ${JSON.stringify(name)})`,
271
+ });
272
+ if (!result) return undefined;
273
+ const { output } = result;
274
+ // The vendored server serializer is the one renderToFlightString uses;
275
+ // resolvable here under the react-server condition.
276
+ output.prepend(
277
+ `import * as $$RangoRSD from "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge";\n`,
278
+ );
279
+ return {
280
+ code: output.toString(),
281
+ map: output.generateMap({ hires: true }),
282
+ };
283
+ },
284
+ };
285
+ }
@@ -7,7 +7,7 @@
7
7
  * ```typescript
8
8
  * // In env.ts or env.d.ts
9
9
  * declare global {
10
- * namespace RSCRouter {
10
+ * namespace Rango {
11
11
  * interface Env extends AppBindings {}
12
12
  * interface Vars extends AppVariables {}
13
13
  * }
@@ -18,7 +18,7 @@
18
18
  * ```
19
19
  */
20
20
  declare global {
21
- namespace RSCRouter {
21
+ namespace Rango {
22
22
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
23
23
  interface Env {
24
24
  // Empty by default - users augment with their bindings (e.g., { DB: D1Database })
@@ -44,13 +44,25 @@ declare global {
44
44
  }
45
45
 
46
46
  /**
47
- * Get registered routes or fallback to generic Record<string, string>
48
- * When RSCRouter.RegisteredRoutes is augmented, provides autocomplete for route names
49
- * When not augmented, allows any string (no autocomplete)
47
+ * Route map for path-validation surfaces (`href`, `ValidPaths`, `PathResponse`).
48
+ *
49
+ * Resolution order:
50
+ * 1. `RegisteredRoutes` — manual `extends typeof router.routeMap`; richest map,
51
+ * the only one carrying response-route payload metadata.
52
+ * 2. `GeneratedRouteMap` — auto-wired by `router.named-routes.gen.ts`; path +
53
+ * search only. Lets `rango generate` alone enable `href()`/`ValidPaths` path
54
+ * checking without a manual `RegisteredRoutes` declaration.
55
+ * 3. `Record<string, string>` — permissive fallback when nothing is registered.
56
+ *
57
+ * Referencing `GeneratedRouteMap` here is cycle-safe: it is declared in the
58
+ * standalone gen file with no imports, unlike `RegisteredRoutes extends typeof
59
+ * router.routeMap`.
50
60
  */
51
- export type GetRegisteredRoutes = keyof RSCRouter.RegisteredRoutes extends never
52
- ? Record<string, string>
53
- : RSCRouter.RegisteredRoutes;
61
+ export type GetRegisteredRoutes = keyof Rango.RegisteredRoutes extends never
62
+ ? keyof Rango.GeneratedRouteMap extends never
63
+ ? Record<string, string>
64
+ : Rango.GeneratedRouteMap
65
+ : Rango.RegisteredRoutes;
54
66
 
55
67
  /**
56
68
  * Default route map for reverse() surfaces.
@@ -58,12 +70,11 @@ export type GetRegisteredRoutes = keyof RSCRouter.RegisteredRoutes extends never
58
70
  * cycles, but falls back to RegisteredRoutes for manual augmentation and then to
59
71
  * a permissive record when no route types are available.
60
72
  */
61
- export type DefaultReverseRouteMap =
62
- keyof RSCRouter.GeneratedRouteMap extends never
63
- ? keyof RSCRouter.RegisteredRoutes extends never
64
- ? Record<string, string>
65
- : RSCRouter.RegisteredRoutes
66
- : RSCRouter.GeneratedRouteMap;
73
+ export type DefaultReverseRouteMap = keyof Rango.GeneratedRouteMap extends never
74
+ ? keyof Rango.RegisteredRoutes extends never
75
+ ? Record<string, string>
76
+ : Rango.RegisteredRoutes
77
+ : Rango.GeneratedRouteMap;
67
78
 
68
79
  /**
69
80
  * Default route map for Handler type.
@@ -71,30 +82,32 @@ export type DefaultReverseRouteMap =
71
82
  * circular dependencies: router.tsx -> urls.tsx -> handler.tsx -> RegisteredRoutes -> router.tsx.
72
83
  * GeneratedRouteMap is declared in a standalone gen file with no imports.
73
84
  */
74
- export type DefaultHandlerRouteMap =
75
- keyof RSCRouter.GeneratedRouteMap extends never
76
- ? {}
77
- : RSCRouter.GeneratedRouteMap;
85
+ export type DefaultHandlerRouteMap = keyof Rango.GeneratedRouteMap extends never
86
+ ? {}
87
+ : Rango.GeneratedRouteMap;
78
88
 
79
89
  /**
80
- * Default environment type - uses global augmentation if available, any otherwise
90
+ * Default environment type - uses global augmentation if available.
91
+ *
92
+ * Falls back to `unknown` (not `any`) when `Rango.Env` is not augmented, so
93
+ * an app that forgets to register its bindings gets a compile error on
94
+ * `ctx.env.SOMETHING` instead of silently losing all env type-checking. Augment
95
+ * `Rango.Env` to type `ctx.env`.
81
96
  */
82
- export type DefaultEnv = keyof RSCRouter.Env extends never
83
- ? any
84
- : RSCRouter.Env;
97
+ export type DefaultEnv = keyof Rango.Env extends never ? unknown : Rango.Env;
85
98
 
86
99
  /**
87
100
  * Default variables type - uses global augmentation if available, Record<string, any> otherwise
88
101
  */
89
- export type DefaultVars = keyof RSCRouter.Vars extends never
102
+ export type DefaultVars = keyof Rango.Vars extends never
90
103
  ? Record<string, any>
91
- : RSCRouter.Vars;
104
+ : Rango.Vars;
92
105
 
93
106
  /**
94
107
  * Default route name type for public `routeName` on contexts.
95
108
  * When GeneratedRouteMap is augmented, narrows to the known route names.
96
109
  * Otherwise falls back to `string` for untyped usage.
97
110
  */
98
- export type DefaultRouteName = keyof RSCRouter.GeneratedRouteMap extends never
111
+ export type DefaultRouteName = keyof Rango.GeneratedRouteMap extends never
99
112
  ? string
100
- : keyof RSCRouter.GeneratedRouteMap & string;
113
+ : keyof Rango.GeneratedRouteMap & string;
@@ -43,7 +43,7 @@ export type { MiddlewareFn } from "../router/middleware.js";
43
43
  */
44
44
  export type ScopedRouteMap<
45
45
  TPrefix extends string,
46
- TMap = RSCRouter.GeneratedRouteMap,
46
+ TMap = Rango.GeneratedRouteMap,
47
47
  > = {
48
48
  [K in keyof TMap as K extends `${TPrefix}.${infer Rest}`
49
49
  ? Rest
@@ -513,6 +513,19 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
513
513
  * })
514
514
  * ```
515
515
  */
516
+ /**
517
+ * A reference to a server action, used by `isAction()` in a revalidate predicate.
518
+ *
519
+ * Either a directly imported action (`import { addToCart }`) or a namespace
520
+ * import of an action module (`import * as CartActions`). Matching resolves the
521
+ * action's build-injected id (`path#export`) — the same identity the router uses
522
+ * for `actionId` — so a renamed or moved action breaks at compile time instead
523
+ * of silently failing to match.
524
+ */
525
+ export type ActionRef =
526
+ | ((...args: never[]) => unknown)
527
+ | Record<string, unknown>;
528
+
516
529
  /**
517
530
  * Revalidation function called during client-side navigation to decide whether
518
531
  * a segment (layout, route, parallel slot, or loader) should be re-rendered.
@@ -524,9 +537,10 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
524
537
  *
525
538
  * @example
526
539
  * ```ts
527
- * // Re-render only when a cart action happened or browser signals staleness
540
+ * // Re-render when a cart action happened or the browser signals staleness;
541
+ * // defer otherwise (|| undefined) so the segment default still applies
528
542
  * revalidate(({ actionId, stale }) =>
529
- * actionId?.includes("cart") || stale || false
543
+ * actionId?.includes("cart") || stale || undefined
530
544
  * )
531
545
  *
532
546
  * // Always re-render when params change (default behavior made explicit)
@@ -553,8 +567,11 @@ export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
553
567
 
554
568
  // ── Segment metadata (which segment is being evaluated) ──────────────
555
569
 
556
- /** The type of segment being revalidated. */
557
- segmentType: "layout" | "route" | "parallel";
570
+ /**
571
+ * The type of segment being revalidated. `"loader"` is passed to revalidate
572
+ * functions attached to a `loader(Fn, () => [revalidate(...)])` registration.
573
+ */
574
+ segmentType: "layout" | "route" | "parallel" | "loader";
558
575
  /** Layout name (e.g., `"root"`, `"shop"`, `"auth"`). Only set for layout segments. */
559
576
  layoutName?: string;
560
577
  /** Slot name (e.g., `"@sidebar"`, `"@modal"`). Only set for parallel segments. */
@@ -570,21 +587,49 @@ export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
570
587
  * relative to the project root, followed by `#` and the exported function name.
571
588
  *
572
589
  * This is stable and can be used for path-based matching to revalidate
573
- * when any action in a module or directory fires:
590
+ * when any action in a module or directory fires. Prefer `|| undefined`
591
+ * (defer to the segment default / downstream revalidators) over `?? false`
592
+ * (hard short-circuit that suppresses the default and ends the chain):
574
593
  *
575
594
  * @example
576
595
  * ```ts
577
596
  * // Match a specific action
578
- * revalidate(({ actionId }) => actionId === "src/actions/cart.ts#addToCart")
597
+ * revalidate(({ actionId }) => actionId === "src/actions/cart.ts#addToCart" || undefined)
579
598
  *
580
599
  * // Match any action in the cart module
581
- * revalidate(({ actionId }) => actionId?.includes("cart") ?? false)
600
+ * revalidate(({ actionId }) => actionId?.includes("cart") || undefined)
582
601
  *
583
602
  * // Match any action under src/apps/store/actions/
584
- * revalidate(({ actionId }) => actionId?.startsWith("src/apps/store/actions/") ?? false)
603
+ * revalidate(({ actionId }) => actionId?.startsWith("src/apps/store/actions/") || undefined)
585
604
  * ```
586
605
  */
587
606
  actionId?: string;
607
+ /**
608
+ * Typed, rename-safe action matching. Returns `true` when the action that
609
+ * triggered this revalidation is one of the given references — or, for a
610
+ * namespace import (`import * as CartActions`), any export of that module —
611
+ * and `false` otherwise (including plain navigation with no action).
612
+ *
613
+ * Prefer this over hand-written `actionId` substring matches: it resolves the
614
+ * action's stable `path#export` id from the imported reference, so a rename is
615
+ * a type error in one place instead of silent drift across consumers. It
616
+ * resolves the reference the same way the action boundary derives `actionId`
617
+ * (`$id ?? $$id`), so it matches in both dev and production.
618
+ *
619
+ * Returns a raw boolean, so for the common "revalidate on match, else defer"
620
+ * intent combine with `|| undefined`:
621
+ *
622
+ * @example
623
+ * ```ts
624
+ * import { addToCart, removeFromCart } from "./actions/cart";
625
+ * import * as CartActions from "./actions/cart";
626
+ *
627
+ * revalidate((ctx) => ctx.isAction(addToCart) || undefined); // one action
628
+ * revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
629
+ * revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any in the module
630
+ * ```
631
+ */
632
+ isAction: (...actions: ActionRef[]) => boolean;
588
633
  /** URL where the action was executed (the page the user was on when they triggered the action). */
589
634
  actionUrl?: URL;
590
635
  /** Return value from the action execution. Can be used to conditionally revalidate based on the action's outcome. */
@@ -725,7 +770,7 @@ export type Revalidate<
725
770
  * Middleware function with typed params and environment
726
771
  *
727
772
  * @template TParams - Params object (defaults to generic)
728
- * @template TEnv - Environment type (defaults to global RSCRouter.Env)
773
+ * @template TEnv - Environment type (defaults to global Rango.Env)
729
774
  *
730
775
  * Note: Middleware cannot directly use route names for params typing because
731
776
  * middleware is defined during router setup, before RegisteredRoutes is populated.
@@ -733,7 +778,7 @@ export type Revalidate<
733
778
  *
734
779
  * @example
735
780
  * ```typescript
736
- * // Basic middleware (uses global RSCRouter.Env via module augmentation)
781
+ * // Basic middleware (uses global Rango.Env via module augmentation)
737
782
  * const middleware: Middleware = async (ctx, next) => {
738
783
  * ctx.set("user", { id: "123" }); // Type-safe!
739
784
  * await next();
@@ -42,6 +42,7 @@ export type {
42
42
  GenericParams,
43
43
  RevalidateParams,
44
44
  ShouldRevalidateFn,
45
+ ActionRef,
45
46
  RouteKeys,
46
47
  ExtractRouteParams,
47
48
  HandlersForRouteMap,
@@ -72,9 +72,12 @@ export type LoaderContext<
72
72
  * **Experimental.** Wait for all non-loader segments to settle.
73
73
  *
74
74
  * After the returned promise resolves, handle data is available via
75
- * `ctx.use(handle)`. Only supported in DSL loaders on non-streaming
76
- * trees (no `loading()`). Throws if called from a handler-invoked
77
- * loader or when the tree uses streaming.
75
+ * `ctx.use(handle)`. Supported in DSL loaders, including on streaming
76
+ * trees that use `loading()` the barrier waits for the streaming
77
+ * handlers to finish pushing before it resolves. Throws if called from a
78
+ * handler-invoked loader, or if a handler is already awaiting this loader
79
+ * via `ctx.use()` (that would deadlock — use a loader-to-loader
80
+ * dependency instead).
78
81
  *
79
82
  * @example
80
83
  * ```typescript
@@ -10,7 +10,10 @@ export type ViewTransitionClass = Record<string, string> | string;
10
10
 
11
11
  /**
12
12
  * Configuration for React's <ViewTransition> component.
13
- * Maps directly to ViewTransitionProps (minus children/ref/callbacks).
13
+ *
14
+ * The phase fields (enter/exit/update/share/default/name) map directly to
15
+ * ViewTransitionProps (minus children/ref/callbacks). The `viewTransition`
16
+ * field is router-specific and is stripped before the config reaches React.
14
17
  */
15
18
  export interface TransitionConfig {
16
19
  enter?: ViewTransitionClass;
@@ -19,6 +22,20 @@ export interface TransitionConfig {
19
22
  share?: ViewTransitionClass;
20
23
  default?: ViewTransitionClass;
21
24
  name?: string;
25
+ /**
26
+ * Whether the router wraps this segment's content in its own
27
+ * <ViewTransition> boundary.
28
+ *
29
+ * - "auto" (default): the router places the boundary, producing the
30
+ * router-owned cross-fade described by the phase fields above.
31
+ * - false: the router places no boundary. The navigation commit is still
32
+ * driven through startTransition (so loaders hold instead of flashing a
33
+ * skeleton, and consumer-placed <ViewTransition> elements still animate),
34
+ * but the router contributes no cross-fade of its own.
35
+ *
36
+ * When unset, inherits the createRouter({ viewTransition }) default.
37
+ */
38
+ viewTransition?: "auto" | false;
22
39
  }
23
40
 
24
41
  /**