@rangojs/router 0.12.1 → 0.12.3

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.
@@ -3746,7 +3746,7 @@ import { resolve } from "node:path";
3746
3746
  // package.json
3747
3747
  var package_default = {
3748
3748
  name: "@rangojs/router",
3749
- version: "0.12.1",
3749
+ version: "0.12.3",
3750
3750
  description: "Django-inspired RSC router with composable URL patterns",
3751
3751
  keywords: [
3752
3752
  "react",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -206,8 +206,8 @@
206
206
  "esbuild": "^0.28.1",
207
207
  "happy-dom": "^20.10.1",
208
208
  "jiti": "^2.7.0",
209
- "react": "^19.2.8",
210
- "react-dom": "^19.2.8",
209
+ "react": "^19.3.0",
210
+ "react-dom": "^19.3.0",
211
211
  "vitest": "^4.1.9",
212
212
  "@shared/e2e": "0.0.1"
213
213
  },
@@ -197,7 +197,7 @@
197
197
  },
198
198
  {
199
199
  "name": "react-compiler",
200
- "description": "Enable the React Compiler in a Rango app the @vitejs/plugin-rsc way a separate @rolldown/plugin-babel running reactCompilerPreset(), ordered after react() and before the plugin that supplies @vitejs/plugin-rsc. Use when a consumer wants to turn React Compiler on, hits the dead plugin-react v6 `react({ babel })` path, or is unsure why server components aren't being compiled.",
200
+ "description": "Enable the React Compiler in a Rango app with @vitejs/plugin-react 6.1's native `compiler` option (oxc-transform-react) one flag, client components only, no Babel. Use when a consumer wants to turn React Compiler on, hits the dead plugin-react v6 `react({ babel })` path, sees an unmet-peer / ERESOLVE on oxc-transform-react, or is unsure why server components aren't being compiled.",
201
201
  "argumentHint": "",
202
202
  "path": "skills/react-compiler/SKILL.md"
203
203
  },
@@ -289,7 +289,7 @@ Grouped by concern — read when you need to…
289
289
  | `/view-transitions` | React View Transitions on layouts, routes, and parallel slots |
290
290
  | `/defer-hydration` | Full body HTML in the PPR shell + hydration off the critical path (gated Suspense boundary, content-as-fallback) |
291
291
  | `/breadcrumbs` | Built-in Breadcrumbs handle for breadcrumb navigation |
292
- | `/react-compiler` | Enable React Compiler (opt-in) the vite-rsc way; client-only scope |
292
+ | `/react-compiler` | Enable React Compiler (opt-in) via plugin-react's native `compiler` option; client-only scope |
293
293
 
294
294
  **Observability & production health**:
295
295
 
@@ -1,45 +1,49 @@
1
1
  ---
2
2
  name: react-compiler
3
- description: Enable the React Compiler in a Rango app the @vitejs/plugin-rsc way a separate @rolldown/plugin-babel running reactCompilerPreset(), ordered after react() and before the plugin that supplies @vitejs/plugin-rsc. Use when a consumer wants to turn React Compiler on, hits the dead plugin-react v6 `react({ babel })` path, or is unsure why server components aren't being compiled.
3
+ description: Enable the React Compiler in a Rango app with @vitejs/plugin-react 6.1's native `compiler` option (oxc-transform-react) one flag, client components only, no Babel. Use when a consumer wants to turn React Compiler on, hits the dead plugin-react v6 `react({ babel })` path, sees an unmet-peer / ERESOLVE on oxc-transform-react, or is unsure why server components aren't being compiled.
4
4
  argument-hint:
5
5
  ---
6
6
 
7
7
  # React Compiler
8
8
 
9
- React Compiler is **opt-in** in Rango. The plugin pipeline is fully compatible
10
- you just add one more plugin. The catch on a current Rango stack (Vite 8 +
11
- `@vitejs/plugin-react` v6) is that **v6 dropped its internal Babel for oxc**, so
12
- the way the React docs and most blog posts show it — `react({ babel: { plugins:
13
- [...] } })` silently does nothing. The compiler has to be its own top-level
14
- plugin.
9
+ React Compiler is **opt-in** in Rango, and it is one option on the plugin you
10
+ already have: `@vitejs/plugin-react` 6.1 ships a native React Compiler behind
11
+ `react({ compiler: true })`, backed by
12
+ [`oxc-transform-react`](https://www.npmjs.com/package/oxc-transform-react),
13
+ Oxc's Rust port of the compiler. No Babel, no extra plugin, no ordering rules.
14
+ Upstream marks the option experimental; the previous Babel wiring still works
15
+ and is kept below as the [fallback](#babel-fallback).
15
16
 
16
17
  ## The shape (read first)
17
18
 
18
- - The compiler is a **Babel** plugin, run via
19
- [`@rolldown/plugin-babel`](https://www.npmjs.com/package/@rolldown/plugin-babel)
20
- with `reactCompilerPreset()` from `@vitejs/plugin-react`.
21
- - **Ordering is load-bearing:** put `babel(...)` **after `react()`** and
22
- **before the plugin that supplies `@vitejs/plugin-rsc`**. In a default Rango
23
- app that plugin is `rango()` itself; in a Cloudflare app it is
24
- `@cloudflare/vite-plugin`.
25
- - **It is client-only.** `reactCompilerPreset()` gates itself to the client
26
- environment. Server/RSC components are not compiled, and that is the upstream
27
- example's behavior — not a Rango limitation. See
19
+ - **One option:** `react({ compiler: true })`. Needs `@vitejs/plugin-react` 6.1+
20
+ and its optional peer `oxc-transform-react`.
21
+ - **It is client-only.** plugin-react runs the compiler only in environments
22
+ whose `consumer` is not `"server"`. Server/RSC components are not compiled,
23
+ and that is plugin-react's contract — not a Rango limitation. See
28
24
  [What gets compiled](#what-gets-compiled-client-only).
29
- - **Rango's build-time prerender is unaffected.** You do not need to do anything
30
- special. See [Prerender](#interaction-with-build-time-prerender).
25
+ - **It owns JSX and Fast Refresh too.** With `compiler` on, plugin-react hands
26
+ TypeScript, JSX and Fast Refresh to the same native pass and disables Vite's
27
+ built-in refresh injection. Nothing to configure, and dev line numbers still
28
+ point at your source.
29
+ - **Rango's build-time prerender is unaffected.** See
30
+ [Prerender](#interaction-with-build-time-prerender).
31
31
 
32
32
  ## Step 1: Install
33
33
 
34
34
  ```bash
35
- pnpm add -D @rolldown/plugin-babel @babel/core babel-plugin-react-compiler
36
- # TypeScript users also want the Babel core types:
37
- pnpm add -D @types/babel__core
35
+ pnpm add -D oxc-transform-react@^0.145.0
38
36
  ```
39
37
 
38
+ Take the range from `@vitejs/plugin-react`'s `peerDependencies` (`^0.145.0` for
39
+ 6.1.x) rather than npm's latest. `oxc-transform-react` cuts a new minor every
40
+ couple of weeks and plugin-react widens its range in its own releases, so a newer
41
+ binding shows up as an unmet-peer warning under pnpm and an `ERESOLVE` error
42
+ under npm (vitejs/vite-plugin-react#1437). Bump both together.
43
+
40
44
  React 19 ships `react/compiler-runtime` in-tree, so there is **no** extra runtime
41
- to install and **no** `target` option to set. Only pass `target: '17' | '18'` to
42
- `reactCompilerPreset()` if you are on an older React.
45
+ to install and **no** `target` option to set. Only pass `target: '17' | '18'` if
46
+ you are on an older React.
43
47
 
44
48
  ## Step 2: Wire it in
45
49
 
@@ -48,16 +52,11 @@ to install and **no** `target` option to set. Only pass `target: '17' | '18'` to
48
52
  ```ts
49
53
  // vite.config.ts
50
54
  import { defineConfig } from "vite";
51
- import react, { reactCompilerPreset } from "@vitejs/plugin-react";
52
- import babel from "@rolldown/plugin-babel";
55
+ import react from "@vitejs/plugin-react";
53
56
  import { rango } from "@rangojs/router/vite";
54
57
 
55
58
  export default defineConfig({
56
- plugins: [
57
- react(),
58
- babel({ presets: [reactCompilerPreset()] }),
59
- rango(), // supplies @vitejs/plugin-rsc
60
- ],
59
+ plugins: [react({ compiler: true }), rango()],
61
60
  });
62
61
  ```
63
62
 
@@ -66,29 +65,29 @@ export default defineConfig({
66
65
  ```ts
67
66
  // vite.config.ts
68
67
  import { cloudflare } from "@cloudflare/vite-plugin";
69
- import react, { reactCompilerPreset } from "@vitejs/plugin-react";
70
- import babel from "@rolldown/plugin-babel";
68
+ import react from "@vitejs/plugin-react";
71
69
  import { defineConfig } from "vite";
72
70
  import { rango } from "@rangojs/router/vite";
73
71
 
74
72
  export default defineConfig({
75
73
  plugins: [
76
- react(),
77
- babel({ presets: [reactCompilerPreset()] }),
74
+ react({ compiler: true }),
78
75
  rango({ preset: "cloudflare" }),
79
76
  cloudflare({
80
77
  /* ... */
81
- }), // supplies @vitejs/plugin-rsc
78
+ }),
82
79
  ],
83
80
  });
84
81
  ```
85
82
 
83
+ Both layouts keep `react()` ahead of `rango()` / `cloudflare()`, which is what
84
+ Rango's own e2e apps run.
85
+
86
86
  ## What gets compiled (client-only)
87
87
 
88
- `reactCompilerPreset()` carries
89
- `rolldown.applyToEnvironmentHook: (env) => env.config.consumer === "client"`, so
90
- even though the babel plugin is top-level, the transform runs **only in the
91
- `client` environment**:
88
+ plugin-react's `vite:react-compiler` transform runs in every environment, but
89
+ passes `reactCompiler: false` whenever `environment.config.consumer === "server"`.
90
+ Server environments only get the TypeScript/JSX pass:
92
91
 
93
92
  | Environment | `consumer` | Compiled? |
94
93
  | ----------- | ---------- | --------- |
@@ -96,43 +95,53 @@ even though the babel plugin is top-level, the transform runs **only in the
96
95
  | ssr | `server` | No |
97
96
  | rsc | `server` | No |
98
97
 
99
- This matches the upstream `@vitejs/plugin-rsc` example. If you genuinely need to
100
- compile **server** components, you would have to invoke
101
- `babel-plugin-react-compiler` yourself without the preset's
102
- `applyToEnvironmentHook` — that is outside what the example does and is not
98
+ If you genuinely need to compile **server** components, the native option cannot
99
+ do it; you would have to run `babel-plugin-react-compiler` yourself. That is not
103
100
  covered here.
104
101
 
105
102
  ## Options
106
103
 
107
- `reactCompilerPreset()` forwards to `babel-plugin-react-compiler`:
104
+ `compiler` takes `true` or the React Compiler configuration plus one plugin-level
105
+ flag:
106
+
107
+ | Option | Effect |
108
+ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
109
+ | `compilationMode: 'annotation'` | Compile only components marked with the `"use memo"` directive, not every eligible one |
110
+ | `target: '17' \| '18'` | Emit `react-compiler-runtime` calls for React < 19. Omit on React 19+. |
111
+ | `logDiagnostics: true` | Log recoverable compiler diagnostics (why a component was skipped) through Vite. Default `false`. Fatal diagnostics always fail the transform. |
108
112
 
109
- | Option | Effect |
110
- | ------------------------------- | -------------------------------------------------------------------------------------- |
111
- | `compilationMode: 'annotation'` | Compile only components marked with the `"use memo"` directive, not every eligible one |
112
- | `target: '17' \| '18'` | Emit `react-compiler-runtime` calls for React < 19. Omit on React 19+. |
113
+ `logDiagnostics` can only relay what the binding reports. `oxc-transform-react`
114
+ 0.145.x, the range plugin-react 6.1 declares, reports bail-out reasons. 0.148.0
115
+ dropped them (oxc-project/oxc#26318), so the flag goes silent once plugin-react's
116
+ peer range moves past it; 0.146 and 0.147 still report but need a later
117
+ plugin-react. Callback-valued Babel
118
+ options (`logger`, function-valued `sources`) do not exist on the native path;
119
+ `sources` takes an array of filename substrings.
113
120
 
114
121
  ## Interaction with build-time prerender
115
122
 
116
123
  Nothing to configure. Rango's discovery/prerender step runs a throwaway temp Vite
117
124
  server (`createTempRscServer`) that forwards only your **resolution** plugins
118
- (`resolveId` / `load`). A pure transform plugin like `@rolldown/plugin-babel` is
119
- intentionally **not** forwarded and that is correct: the temp runner only
120
- produces **data** (serialized Flight payloads + the route manifest), not shipped
121
- code, and React Compiler is a memoization-only transform that does not change
122
- rendered output. Your shipped client bundle still gets compiled, because the
123
- babel plugin lives in your app's top-level plugin array alongside `react()`.
125
+ (`resolveId` / `load`) and denies every `vite:*` plugin, so `vite:react-compiler`
126
+ never runs there. That is correct: the temp runner only produces **data**
127
+ (serialized Flight payloads + the route manifest), not shipped code, and React
128
+ Compiler is a memoization-only transform that does not change rendered output.
129
+ Your shipped client bundle is compiled by the `react({ compiler: true })` in your
130
+ app's own plugin array.
124
131
 
125
132
  ## Step 3: Verify the compiler actually ran
126
133
 
127
- A compiled module imports the cache allocator from `react/compiler-runtime` and
134
+ The native compiler emits the same shape as `babel-plugin-react-compiler`. A
135
+ compiled module imports the cache allocator from `react/compiler-runtime` and
128
136
  calls `_c(n)`. Those two appear in **every** compiled module, so they are the
129
- reliable per-module signal in dev:
137
+ reliable per-module signal in dev, and a compiled module also carries Fast
138
+ Refresh's `$RefreshReg$(...)` registration:
130
139
 
131
140
  ```bash
132
141
  pnpm dev
133
142
  # fetch any client component module straight from Vite and look for the markers:
134
143
  curl -s "http://localhost:5173/src/components/SomeClientComponent.tsx" \
135
- | grep -E "compiler-runtime|_c\("
144
+ | grep -E "compiler-runtime|_c\(|\\\$RefreshReg\\\$\("
136
145
  ```
137
146
 
138
147
  For a production build, grep the built client bundle for the compiler's
@@ -150,19 +159,48 @@ also defines that symbol once with a single `=` assignment, so count comparisons
150
159
  not the bare string.) Run the same grep over `dist/rsc` / `dist/ssr` and you
151
160
  should find **none** — that is the client-only contract.
152
161
 
162
+ Output is not byte-identical to Babel's: the port tracks React's experimental
163
+ compiler channel, so a handful of components memoize a different number of
164
+ values, and comments inside a compiled function body are dropped. Neither changes
165
+ rendered output.
166
+
167
+ ## Babel fallback
168
+
169
+ On `@vitejs/plugin-react` < 6.1, or if you need a Babel-only compiler option, the
170
+ previous wiring still works: a top-level
171
+ [`@rolldown/plugin-babel`](https://www.npmjs.com/package/@rolldown/plugin-babel)
172
+ running `reactCompilerPreset()` from `@vitejs/plugin-react`, placed **after
173
+ `react()`** and **before the plugin that supplies `@vitejs/plugin-rsc`**. The
174
+ preset gates itself to `consumer === "client"`, so the client-only contract is
175
+ the same. Do not combine it with `compiler: true`.
176
+
177
+ ```bash
178
+ pnpm add -D @rolldown/plugin-babel @babel/core babel-plugin-react-compiler @types/babel__core
179
+ ```
180
+
181
+ ```ts
182
+ import react, { reactCompilerPreset } from "@vitejs/plugin-react";
183
+ import babel from "@rolldown/plugin-babel";
184
+
185
+ // plugins: [react(), babel({ presets: [reactCompilerPreset()] }), rango()]
186
+ ```
187
+
153
188
  ## Troubleshooting
154
189
 
155
- | Symptom | Cause / fix |
156
- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
157
- | Nothing is compiled; no `compiler-runtime` import anywhere | You used `react({ babel: { plugins: [...] } })`. plugin-react v6 has no internal Babel add `@rolldown/plugin-babel` as its own plugin. |
158
- | Client compiled, but server/RSC components are not | Expected. `reactCompilerPreset()` is client-only (see the table). Not a bug. |
159
- | `Cannot find module 'babel-plugin-react-compiler'` (or `@babel/core`) | Install the peer deps from Step 1; they are not bundled by `reactCompilerPreset()`. |
160
- | Build pulls in `react-compiler-runtime` | You set `target: '17'`/`'18'` on React 19. Drop `target` React 19 ships `react/compiler-runtime` in-tree. |
161
- | Output looks compiled but a component misbehaves | The component likely breaks the Rules of React. Fix the component, or scope the compiler with `compilationMode: 'annotation'` while you do. |
190
+ | Symptom | Cause / fix |
191
+ | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
192
+ | `React Compiler requires the optional oxc-transform-react package` on startup | Install it (Step 1). plugin-react checks in its `config` hook, so the error is immediate. |
193
+ | Nothing is compiled; no `compiler-runtime` import anywhere | You used `react({ babel: { plugins: [...] } })`. plugin-react v6 has no internal Babel — use `react({ compiler: true })`. |
194
+ | pnpm warns `unmet peer oxc-transform-react`, npm fails with `ERESOLVE` | The binding is newer than plugin-react's peer range. Pin the range plugin-react declares. |
195
+ | Client compiled, but server/RSC components are not | Expected. The option is client-only (see the table). Not a bug. |
196
+ | `logDiagnostics: true` prints nothing | You are on `oxc-transform-react` 0.148.0 or later, which dropped recoverable diagnostics. On plugin-react 6.1.x stay on 0.145.x; 0.146 and 0.147 also report, but need a later plugin-react peer range. |
197
+ | Build pulls in `react-compiler-runtime` | You set `target: '17'`/`'18'` on React 19. Drop `target` — React 19 ships `react/compiler-runtime` in-tree. |
198
+ | Output looks compiled but a component misbehaves | The component likely breaks the Rules of React. Fix the component, or scope the compiler with `compilationMode: 'annotation'` while you do. |
162
199
 
163
200
  ## Reference
164
201
 
165
202
  A worked, tested wiring (dev + production e2e markers, incl. the client-only
166
- contract) lives in the `@rangojs/router` repository — not shipped in this
167
- package: `docs/react-compiler.md` and the `react-compiler.test.ts` files under
168
- `e2e/e2e-basic`, `tests/cloudflare-basic`, and `tests/vite-rsc-demo`.
203
+ contract and the Fast Refresh check) lives in the `@rangojs/router` repository —
204
+ not shipped in this package: `docs/react-compiler.md` and the
205
+ `react-compiler.test.ts` files under `e2e/e2e-basic`, `tests/cloudflare-basic`,
206
+ and `tests/vite-rsc-demo`.
@@ -9,9 +9,9 @@ argument-hint: [layout|route|parallel|intercept]
9
9
  `transition()` opts a route (or group of routes) into transition-driven navigation. It does two things, and you choose how far to go:
10
10
 
11
11
  1. **`startTransition` (the foundation).** The navigation commit is driven through React's `startTransition`. That holds the previous content across a same-route navigation (stale-while-revalidate — no loading-skeleton flash) and is the **precondition** for any view-transition animation. Works on **all** React versions.
12
- 2. **`<ViewTransition>` (the animation, layered on top).** On experimental React, rango also wraps the segment content in React's `<ViewTransition>` so the swap cross-fades/morphs. This is the only part that needs experimental React; pass `viewTransition: false` to keep #1 without it (and place your own `<ViewTransition>` where you want it).
12
+ 2. **`<ViewTransition>` (the animation, layered on top).** On React 19.3+ (or an experimental build), rango also wraps the segment content in React's `<ViewTransition>` so the swap cross-fades/morphs. This is the only part that needs 19.3+; pass `viewTransition: false` to keep #1 without it (and place your own `<ViewTransition>` where you want it).
13
13
 
14
- > The `<ViewTransition>` layer requires React experimental (the build that exports `<ViewTransition>` / `addTransitionType`). On stable React that layer is a no-op — but the `startTransition` driving (content hold) still applies.
14
+ > The `<ViewTransition>` layer requires a React that exports `<ViewTransition>` / `addTransitionType`: stable 19.3+ or an experimental build. Rango feature-detects it; on React 19.2 that layer is a no-op — but the `startTransition` driving (content hold) still applies.
15
15
 
16
16
  ## Purpose: `startTransition` vs `<ViewTransition>`
17
17
 
@@ -24,11 +24,11 @@ These are two **independent** mechanisms. `startTransition` controls _fallbacks_
24
24
 
25
25
  The bottom-left cell is the key constraint: a view transition cannot exist without a `startTransition`. So once you reach for `transition()`, the only real choice is _startTransition_ vs _startTransition + ViewTransition_:
26
26
 
27
- | What you want | Config | Effect |
28
- | -------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
29
- | nothing (default nav) | no `transition()` | remount + skeleton on param change |
30
- | `startTransition` only | `transition({ viewTransition: false })` | hold content; place your own `<ViewTransition>` where you want it |
31
- | `startTransition` + `<ViewTransition>` | `transition({})` / `transition({ enter, exit, … })` | hold + router cross-fade (experimental React; on stable it degrades to the `startTransition`-only row) |
27
+ | What you want | Config | Effect |
28
+ | -------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
29
+ | nothing (default nav) | no `transition()` | remount + skeleton on param change |
30
+ | `startTransition` only | `transition({ viewTransition: false })` | hold content; place your own `<ViewTransition>` where you want it |
31
+ | `startTransition` + `<ViewTransition>` | `transition({})` / `transition({ enter, exit, … })` | hold + router cross-fade (React 19.3+ or experimental; on 19.2 it degrades to the `startTransition`-only row) |
32
32
 
33
33
  `createRouter({ viewTransition: "auto" \| false })` sets the app-wide default for the third row; a per-segment `viewTransition` wins. See [Opting out of the router boundary](#opting-out-of-the-router-boundary-place-your-own-viewtransition) for the full opt-out story.
34
34
 
@@ -322,6 +322,24 @@ On stable React the "VT" column is always a no-op (there is no `<ViewTransition>
322
322
 
323
323
  > On **stable** React there is no `<ViewTransition>` at all, so `viewTransition: false` is visually a no-op there — but the startTransition driving and content-hold still apply, identical to `transition({})`.
324
324
 
325
+ ## Testing a transition() route
326
+
327
+ On React 19.3+ a transition commit briefly keeps the exiting `<ViewTransition>`
328
+ host in the DOM next to the entering one (hidden, carrying React's `vt-*`
329
+ attributes) for a few hundred milliseconds. A strict Playwright locator on a
330
+ test id that exists in both the old and the new view (same route, different
331
+ params; a hard load followed by a revalidation commit) resolves to two elements
332
+ during that window. Target the entering host, which is appended after the
333
+ exiting one:
334
+
335
+ ```ts
336
+ const page$ = page.getByTestId("feature-page").last();
337
+ await expect(page$).toBeVisible();
338
+ ```
339
+
340
+ Waiting for `toHaveCount(1)` first is not enough: the count passes through 1
341
+ before the second host is inserted.
342
+
325
343
  ## Recommendations
326
344
 
327
345
  **Put `<ParallelOutlet />` in layouts, not routes.** A route-level `transition` wraps the route component itself, so a `<ParallelOutlet />` rendered directly inside that route component remains inside the route VT subtree — modal opens on a route with a parallel outlet _will_ trigger the route's VT walker. The narrowing fix only applies at layout boundaries. If you combine intercept modals with route-level transitions, mount the slot one level up in a layout.
@@ -40,8 +40,9 @@ function segDebugLog(msg: string, details?: Record<string, unknown>): void {
40
40
  console.log(prefix);
41
41
  }
42
42
 
43
- // ViewTransition is only available in React experimental.
44
- // Access via namespace import to avoid compile-time errors on stable React.
43
+ // ViewTransition ships in React 19.3+ (and experimental builds); older stable
44
+ // releases lack it. Feature-detect via the namespace import so the router
45
+ // compiles against any supported React and degrades to a no-op boundary.
45
46
  const ReactViewTransition: any =
46
47
  "ViewTransition" in React ? (React as any).ViewTransition : null;
47
48
 
@@ -435,7 +436,7 @@ export async function renderSegments(
435
436
  nodeContent = registerLazyRef(resolvedComponent);
436
437
  }
437
438
 
438
- // Wrap with <ViewTransition> if transition config exists (React experimental only).
439
+ // Wrap with <ViewTransition> if transition config exists (React 19.3+ / experimental).
439
440
  // An empty config ({}) creates a bare <ViewTransition> boundary that participates
440
441
  // in transitions without adding custom animation classes. Named element-level
441
442
  // <ViewTransition> components inside (with name/share props) morph independently