@rangojs/router 0.12.2 → 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.2",
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.2",
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
  },
@@ -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