@rangojs/router 0.0.0-experimental.98 → 0.0.0-experimental.99

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.
@@ -2040,7 +2040,7 @@ import { resolve } from "node:path";
2040
2040
  // package.json
2041
2041
  var package_default = {
2042
2042
  name: "@rangojs/router",
2043
- version: "0.0.0-experimental.98",
2043
+ version: "0.0.0-experimental.99",
2044
2044
  description: "Django-inspired RSC router with composable URL patterns",
2045
2045
  keywords: [
2046
2046
  "react",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.98",
3
+ "version": "0.0.0-experimental.99",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -59,6 +59,8 @@ Now `ProductPage` carries its loader, loading state, and response-header middlew
59
59
  | `intercept()` | `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `layout`, `route`, `when`, `transition` |
60
60
  | Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
61
61
 
62
+ For per-item semantics see the dedicated skills: [middleware](../middleware/SKILL.md), [loader](../loader/SKILL.md), [parallel](../parallel/SKILL.md), [intercept](../intercept/SKILL.md), [layout](../layout/SKILL.md), [view-transitions](../view-transitions/SKILL.md).
63
+
62
64
  If `handler.use()` returns a disallowed item for a mount site, registration throws:
63
65
 
64
66
  ```
@@ -197,6 +197,31 @@ function ModalWrapper({ children }) {
197
197
  }
198
198
  ```
199
199
 
200
+ ## Interaction with View Transitions
201
+
202
+ A layout that owns the `@modal` slot can also configure `transition()` for page
203
+ fades — opening a modal does **not** fire the layout's view transition. Rango
204
+ narrows the layout's `<ViewTransition>` wrap to the layout's default outlet
205
+ content, so `<ParallelOutlet />` (the slot where the modal mounts) is a sibling
206
+ of the wrap, not inside its subtree. Form actions submitted from inside an open
207
+ modal also commit without firing the underlying layout's transition, and the
208
+ modal subtree identity is preserved across revalidation (no remount,
209
+ `useActionState` survives). Closing the modal restores the page without a
210
+ stray transition.
211
+
212
+ For a modal-only morph (e.g. when intercepted URLs change while the modal
213
+ stays open), use an element-level React `<ViewTransition>` inside the modal
214
+ component — `transition()` accepted on `intercept()` via the DSL is not
215
+ applied to slot rendering today.
216
+
217
+ Caveat: route-level `transition()` wraps the route component itself, so a
218
+ `<ParallelOutlet />` rendered directly inside that route component would still
219
+ be inside the route's VT subtree. Mount the slot in a layout instead when you
220
+ combine intercept modals with route-level transitions.
221
+
222
+ See [skills/view-transitions](../view-transitions/SKILL.md) for the full
223
+ contract and direction-aware examples.
224
+
200
225
  ## Interaction with Prerender
201
226
 
202
227
  When the target route of an intercept uses `Prerender`, the intercept handler is
@@ -118,6 +118,8 @@ function ShopLayout() {
118
118
  }
119
119
  ```
120
120
 
121
+ A layout's `transition()` config wraps the content that flows through `<Outlet />` — not the layout chrome itself, and not sibling `<ParallelOutlet />` slots. Stacking transitions across nested layouts collapses around the deepest default outlet content. See [skills/view-transitions](../view-transitions/SKILL.md) for the full wrap rules and intercept-modal interaction.
122
+
121
123
  ## Named Outlets
122
124
 
123
125
  For parallel routes, use named outlets:
@@ -237,6 +237,8 @@ A slot's `loading()` (whether from `handler.use` or explicit) makes that slot an
237
237
 
238
238
  The `parallel` mount site has the narrowest allow-list for `handler.use` items — slots cannot bring their own middleware or layout, only `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, and `transition`. See [skills/handler-use](../handler-use/SKILL.md) for the full table and merge rules.
239
239
 
240
+ `transition` is allowed in the slot allow-list, but slot-level rendering does **not** currently apply a `<ViewTransition>` wrapper — only the layout/route wraps take effect at render time. For a modal-only morph today, use an element-level React `<ViewTransition>` inside the slot's component. The reverse direction is the useful guarantee: a layout-level `transition()` fires when the layout's default outlet content changes but **not** when a `<ParallelOutlet />` mounts new content (modal opens are not subtree updates of the layout VT). See [skills/view-transitions](../view-transitions/SKILL.md) for the wrap rules and the intercept caveat.
241
+
240
242
  ### Two scopes for explicit `use`: shared (broadcast) and slot-local
241
243
 
242
244
  `parallel({...slots}, () => [...use])` runs the shared `use()` callback **once per slot** ([dsl-helpers.ts](../../src/route-definition/dsl-helpers.ts)) — items in that callback land on every slot's entry. That's the right behavior for the items the parallel allow-list permits and that accumulate (`loader`, `revalidate`, `errorBoundary`, `notFoundBoundary`, `transition`). (Slots cannot bring `middleware` or `layout` — see the allowed-types note above.)
@@ -403,6 +403,10 @@ urls(({ path, layout }) => [
403
403
  ])
404
404
  ```
405
405
 
406
+ ## View Transitions
407
+
408
+ A route can configure its own `transition()` — the wrap goes around the route's component itself (routes are leaves; they have no separate default outlet channel). If the route component renders a `<ParallelOutlet />` directly, that slot remains inside the route's VT subtree, so prefer mounting parallel slots in a layout when combining intercept modals with route-level transitions. See [skills/view-transitions](../view-transitions/SKILL.md) for examples and the wrap-location rules across layouts, routes, and slots.
409
+
406
410
  ## Handler-attached `.use`
407
411
 
408
412
  Page handlers can carry their own loader, middleware, error boundaries, parallels, and other defaults via a `.use` callback — so the page is self-contained and reusable across mount sites without re-wiring the same items.
@@ -0,0 +1,212 @@
1
+ ---
2
+ name: view-transitions
3
+ description: Configure React View Transitions on layouts, routes, and parallel slots in @rangojs/router
4
+ argument-hint: [layout|route|parallel|intercept]
5
+ ---
6
+
7
+ # View Transitions
8
+
9
+ Rango wires React's experimental `<ViewTransition>` into the segment tree via the `transition()` helper. Each segment can declare its own transition config; rango wraps it at the right tree position so navigations morph the right pieces and modals do not.
10
+
11
+ > Requires React experimental (the build that exports `<ViewTransition>` and `addTransitionType`). With stable React, `transition()` is a no-op — your routes still render, just without view-transition wrappers.
12
+
13
+ ## What `transition()` does
14
+
15
+ `transition(config)` attaches a [`TransitionConfig`](#transitionconfig) to the surrounding entry. Where the wrap actually lands in the rendered React tree depends on the segment type:
16
+
17
+ | Segment type | Wrap location |
18
+ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
19
+ | `layout()` | Around the layout's **default outlet content** (what the layout's `<Outlet />` renders), recursively pushed past nested layouts. Parallel slots (`<ParallelOutlet />`) are siblings of the wrap, not subtree members. |
20
+ | `path()` / `route()` | Around the **route's component itself** (the leaf content). |
21
+ | `parallel()` / `intercept()` slot | `transition()` is accepted by the DSL today, but slot-level rendering does not currently apply a `<ViewTransition>` wrapper. Mount intercept slots in layouts so layout transitions stay scoped to the default outlet. For modal-specific morphs today, use an element-level React `<ViewTransition>` inside the modal component. |
22
+
23
+ The layout case is the important one: stacking a layout transition does **not** wrap the layout chrome (header, sidebar, modal slot); it only morphs whatever flows through that layout's `<Outlet />`.
24
+
25
+ ## Basic Usage
26
+
27
+ A simple cross-fade between pages that share a layout:
28
+
29
+ ```tsx
30
+ import { urls } from "@rangojs/router";
31
+ import { Outlet } from "@rangojs/router/client";
32
+
33
+ function ShopShell({ children }: { children: React.ReactNode }) {
34
+ return (
35
+ <div className="shop">
36
+ <NavBar />
37
+ <main>
38
+ <Outlet /> {/* fade applies HERE */}
39
+ </main>
40
+ <Footer />
41
+ </div>
42
+ );
43
+ }
44
+
45
+ export const urlpatterns = urls(({ layout, path, transition }) => [
46
+ layout(<ShopShell />, () => [
47
+ transition({ default: "page-fade" }),
48
+ path("/", ShopIndex, { name: "index" }),
49
+ path("/about", AboutPage, { name: "about" }),
50
+ path("/contact", ContactPage, { name: "contact" }),
51
+ ]),
52
+ ]);
53
+ ```
54
+
55
+ ```css
56
+ ::view-transition-old(root) {
57
+ animation: fade-out 200ms ease both;
58
+ }
59
+ ::view-transition-new(root) {
60
+ animation: fade-in 200ms ease both;
61
+ }
62
+ .page-fade {
63
+ /* class hooks per phase */
64
+ }
65
+ ```
66
+
67
+ Navigating between `/`, `/about`, and `/contact` morphs the `<Outlet />` content with the `page-fade` class. The shell (NavBar, Footer) does not morph because the wrap sits inside the shell, not around it.
68
+
69
+ ## Direction-aware transitions
70
+
71
+ `ViewTransitionClass` accepts an object form keyed by transition type. Rango tags forward navigations as `"navigation"` and back/forward popstate as `"navigation-back"`:
72
+
73
+ ```tsx
74
+ layout(<ShopShell />, () => [
75
+ transition({
76
+ default: {
77
+ navigation: "slide-left",
78
+ "navigation-back": "slide-right",
79
+ },
80
+ }),
81
+ path("/", ShopIndex, { name: "index" }),
82
+ path("/about", AboutPage, { name: "about" }),
83
+ ]);
84
+ ```
85
+
86
+ ```css
87
+ .slide-left {
88
+ animation-name: slide-from-right;
89
+ }
90
+ .slide-right {
91
+ animation-name: slide-from-left;
92
+ }
93
+ ```
94
+
95
+ > Note: `"action"` is only tagged on partial-update action/refetch paths today; ordinary `server-action-bridge` commits (`useAction` / `useActionState` revalidations) are not currently tagged. Don't rely on an `action`-keyed class to fire on every form action.
96
+
97
+ ## Wrapper form: applying transition to a group of routes
98
+
99
+ `transition(config, () => [...])` creates a transparent layout that applies the config to its children — useful when you want a transition without authoring a real layout component:
100
+
101
+ ```tsx
102
+ urls(({ path, transition }) => [
103
+ // No layout component, but every route inside gets the fade.
104
+ transition({ default: "fade" }, () => [
105
+ path("/", HomePage, { name: "home" }),
106
+ path("/about", AboutPage, { name: "about" }),
107
+ ]),
108
+ // Outside the wrapper — no transition applied.
109
+ path("/admin", AdminPage, { name: "admin" }),
110
+ ]);
111
+ ```
112
+
113
+ ## Intercept (modal) interaction
114
+
115
+ This is where the rango-specific behavior pays off. A common shape:
116
+
117
+ ```tsx
118
+ import { urls } from "@rangojs/router";
119
+ import { Outlet, ParallelOutlet } from "@rangojs/router/client";
120
+
121
+ function GalleryShell() {
122
+ return (
123
+ <>
124
+ <NavBar />
125
+ <main>
126
+ <Outlet /> {/* page transition lands here */}
127
+ </main>
128
+ <ParallelOutlet name="@modal" />{" "}
129
+ {/* modal mounts here — sibling of the VT */}
130
+ </>
131
+ );
132
+ }
133
+
134
+ export const urlpatterns = urls(
135
+ ({ layout, path, intercept, transition, loader, loading }) => [
136
+ layout(<GalleryShell />, () => [
137
+ transition({ default: "fade" }),
138
+
139
+ path("/", GalleryFeed, { name: "feed" }),
140
+ path("/photos/:id", PhotoPage, { name: "photo" }),
141
+
142
+ intercept("@modal", "photo", <PhotoModal />, () => [
143
+ loader(PhotoLoader),
144
+ loading(<PhotoModalSkeleton />),
145
+ ]),
146
+ ]),
147
+ ],
148
+ );
149
+ ```
150
+
151
+ | Action | What fires |
152
+ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
153
+ | Navigate `/` ↔ `/about` (within `GalleryShell`) | Layout transition fires; `<Outlet />` content cross-fades |
154
+ | Click `<Link to="/photos/42" />` from `/` | Soft navigation opens `<PhotoModal />` in `@modal`; **no** view transition fires on the underlying feed |
155
+ | Submit a form action inside `<PhotoModal />` | Revalidation commits without firing the layout VT; modal subtree identity is preserved (no remount, `useActionState` survives) |
156
+ | Close modal via `router.back()` | Underlying page is restored; **no** view transition fires |
157
+ | Direct URL load `/photos/42` | Renders the full `<PhotoPage />` with no modal; the layout transition applies on subsequent in-layout navs |
158
+
159
+ The "no VT on modal open" guarantee holds at any depth — if the layout that owns `@modal` is itself nested inside another transitioned layout, the outer transition is pushed past the inner layout into its default outlet content, so the modal slot ends up outside both VTs.
160
+
161
+ ## Per-route transition
162
+
163
+ Routes are leaves: their `transition()` wraps the route component itself.
164
+
165
+ ```tsx
166
+ urls(({ path, transition }) => [
167
+ path("/checkout", CheckoutPage, { name: "checkout" }, () => [
168
+ transition({ default: "fade-in" }),
169
+ ]),
170
+ ]);
171
+ ```
172
+
173
+ This is the right level for one-off route-specific morphs that should not propagate to siblings.
174
+
175
+ ## TransitionConfig
176
+
177
+ `transition()` accepts the props of React's `<ViewTransition>` (minus `children`/refs). Each phase prop accepts either a plain class string or an object keyed by transition type:
178
+
179
+ ```ts
180
+ import type { TransitionConfig } from "@rangojs/router";
181
+
182
+ interface TransitionConfig {
183
+ enter?: string | Record<string, string>;
184
+ exit?: string | Record<string, string>;
185
+ update?: string | Record<string, string>;
186
+ share?: string | Record<string, string>;
187
+ default?: string | Record<string, string>; // fallback for any phase
188
+ name?: string; // explicit view-transition-name
189
+ }
190
+ ```
191
+
192
+ - `default` is the catch-all if a phase-specific prop is unset.
193
+ - The object form keys are React transition types tagged by rango: `"navigation"` (forward navigations), `"navigation-back"` (popstate cache restores), and `"action"` (partial-update action/refetch paths only — see the caveat in "Direction-aware transitions").
194
+ - `name` lets you participate in cross-page morphs by name (advanced; you usually don't need this on a layout/route-level wrap).
195
+
196
+ ## Recommendations
197
+
198
+ **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.
199
+
200
+ **Don't stack `transition()` on every layout level.** When ancestor and descendant layouts both configure transitions, both wraps end up nested around the deepest default outlet content. Two VTs fire on every nav within the inner layout. That's usually not what you want — pick the level where the morph belongs and apply it once.
201
+
202
+ **Need a modal-only morph?** Per-slot `transition()` is currently a no-op at render time, so use an element-level React `<ViewTransition>` inside the modal component (or a CSS animation) for the modal-entrance effect.
203
+
204
+ **Action revalidation inside a modal is safe.** Server-action submits inside an open modal don't fire the underlying layout VT. Modal subtree identity is preserved across revalidation — so `useActionState`, focus, and scroll all survive the round-trip.
205
+
206
+ ## Notes
207
+
208
+ - `transition()` is part of the route DSL. The allow-list table in [skills/handler-use](../handler-use/SKILL.md) permits it inside `layout()`, `path()`/`route()`, `parallel()` (per-slot or shared), and `intercept()`. At render time, only the layout and route wraps actually take effect today; `parallel()`/`intercept()` slot-level rendering does not currently apply the wrap.
209
+ - Wrap location for layouts: rango walks the rendered tree past `MountContextProvider`/`OutletProvider`/`LoaderBoundary` for layout segments and applies the wrap at the first non-layout target ([segment-system.tsx](../../src/segment-system.tsx) — `wrapDefaultOutletContent`). This is what keeps parallel slots out of the VT subtree.
210
+ - Tree consistency: the wrapper structure is identical across normal commits, intercept-active commits, and action revalidations — React never sees an element-type swap, so layout/modal subtrees are not remounted across these transitions.
211
+ - Element-level `<ViewTransition>` (importing it directly from React and using `name`/`share` to morph specific elements across pages) composes with rango's segment-level wraps as usual; rango doesn't intercept those.
212
+ - See also: [skills/intercept](../intercept/SKILL.md), [skills/parallel](../parallel/SKILL.md), [skills/layout](../layout/SKILL.md).
@@ -541,7 +541,14 @@ export function createNavigationBridge(
541
541
  },
542
542
  scroll: { restore: true, isStreaming },
543
543
  };
544
- const hasTransition = cachedSegments.some((s) => s.transition);
544
+ // Intercept-driven popstate (entering OR leaving an intercept) only
545
+ // mutates the parallel slot; the main outlet shows the same content.
546
+ // Skip startViewTransition in those cases — same rationale as the
547
+ // intercept guard in partial-update.ts's hasTransition computation.
548
+ const hasTransition =
549
+ !isIntercept &&
550
+ !isLeavingIntercept &&
551
+ cachedSegments.some((s) => s.transition);
545
552
  if (hasTransition) {
546
553
  startTransition(() => {
547
554
  if (addTransitionType) {
@@ -14,7 +14,10 @@ const addTransitionType: ((type: string) => void) | undefined =
14
14
  import type { RenderSegmentsOptions } from "../segment-system.js";
15
15
  import { reconcileSegments } from "./segment-reconciler.js";
16
16
  import type { ReconcileActor } from "./segment-reconciler.js";
17
- import { hasActiveIntercept as hasActiveInterceptSlots } from "./intercept-utils.js";
17
+ import {
18
+ hasActiveIntercept as hasActiveInterceptSlots,
19
+ isInterceptSegment,
20
+ } from "./intercept-utils.js";
18
21
  import type { BoundTransaction } from "./navigation-transaction.js";
19
22
  import { ServerRedirect } from "../errors.js";
20
23
  import { debugLog } from "./logging.js";
@@ -28,6 +31,23 @@ function toScrollPayload(
28
31
  return { enabled: scroll !== false ? scroll : false };
29
32
  }
30
33
 
34
+ /**
35
+ * Whether to wrap an update in startViewTransition.
36
+ *
37
+ * Intercept-driven updates only mutate the parallel slot — the main outlet
38
+ * shows the same content — so transitions on the underlying main segments
39
+ * shouldn't fire (otherwise their elements get hoisted above the modal).
40
+ */
41
+ function shouldStartViewTransition(segments: ResolvedSegment[]): boolean {
42
+ let hasIntercept = false;
43
+ let hasTransition = false;
44
+ for (const s of segments) {
45
+ if (isInterceptSegment(s)) hasIntercept = true;
46
+ else if (s.transition) hasTransition = true;
47
+ }
48
+ return !hasIntercept && hasTransition;
49
+ }
50
+
31
51
  /**
32
52
  * Configuration for creating a partial updater
33
53
  */
@@ -338,10 +358,7 @@ export function createPartialUpdater(
338
358
  scroll: toScrollPayload(commitScroll),
339
359
  };
340
360
 
341
- const cachedHasTransition = existingSegments.some(
342
- (s) => s.transition,
343
- );
344
- if (cachedHasTransition) {
361
+ if (shouldStartViewTransition(existingSegments)) {
345
362
  startTransition(() => {
346
363
  if (addTransitionType) {
347
364
  addTransitionType("navigation");
@@ -527,7 +544,7 @@ export function createPartialUpdater(
527
544
 
528
545
  // Emit update to trigger React render.
529
546
  // Scroll info is included so NavigationProvider applies it after React commits.
530
- const hasTransition = reconciled.mainSegments.some((s) => s.transition);
547
+ const hasTransition = shouldStartViewTransition(reconciled.segments);
531
548
  const scrollPayload = toScrollPayload(navScroll);
532
549
 
533
550
  if (mode.type === "action" || mode.type === "stale-revalidation") {
@@ -589,9 +606,7 @@ export function createPartialUpdater(
589
606
  })
590
607
  : tx.commit(segmentIds, segments);
591
608
 
592
- const fullHasTransition = segments.some(
593
- (s: ResolvedSegment) => s.transition,
594
- );
609
+ const fullHasTransition = shouldStartViewTransition(segments);
595
610
  const fullScrollPayload = toScrollPayload(fullScroll);
596
611
 
597
612
  if (mode.type === "stale-revalidation") {
@@ -131,6 +131,47 @@ export interface RenderSegmentsOptions {
131
131
  rootLayout?: ComponentType<RootLayoutProps>;
132
132
  }
133
133
 
134
+ function createViewTransitionBoundary(
135
+ transition: NonNullable<ResolvedSegment["transition"]>,
136
+ children: ReactNode,
137
+ ): ReactNode {
138
+ return createElement(ReactViewTransition, {
139
+ ...transition,
140
+ children,
141
+ });
142
+ }
143
+
144
+ function wrapDefaultOutletContent(
145
+ content: ReactNode,
146
+ transition: NonNullable<ResolvedSegment["transition"]>,
147
+ ): ReactNode {
148
+ if (!React.isValidElement(content)) {
149
+ return createViewTransitionBoundary(transition, content);
150
+ }
151
+
152
+ const props = content.props as any;
153
+
154
+ if (content.type === MountContextProvider) {
155
+ return React.cloneElement(content, {
156
+ children: wrapDefaultOutletContent(props.children, transition),
157
+ } as any);
158
+ }
159
+
160
+ if (content.type === OutletProvider && props.segment?.type === "layout") {
161
+ return React.cloneElement(content, {
162
+ content: wrapDefaultOutletContent(props.content, transition),
163
+ } as any);
164
+ }
165
+
166
+ if (content.type === LoaderBoundary && props.segment?.type === "layout") {
167
+ return React.cloneElement(content, {
168
+ outletContent: wrapDefaultOutletContent(props.outletContent, transition),
169
+ } as any);
170
+ }
171
+
172
+ return createViewTransitionBoundary(transition, content);
173
+ }
174
+
134
175
  /**
135
176
  * Render segments into a React tree with proper layout nesting
136
177
  *
@@ -273,17 +314,27 @@ export async function renderSegments(
273
314
  // in transitions without adding custom animation classes. Named element-level
274
315
  // <ViewTransition> components inside (with name/share props) morph independently
275
316
  // from the parent's default cross-fade.
276
- if (ReactViewTransition && node.segment.transition) {
277
- nodeContent = createElement(ReactViewTransition, {
278
- ...node.segment.transition,
279
- children: nodeContent,
280
- });
281
- }
282
-
283
- // Common props for OutletProvider
284
- const outletContent: ReactNode =
317
+ //
318
+ // For layouts, wrap the outlet content (what `<Outlet />` renders) rather
319
+ // than the layout component itself. Parallel slots like `<ParallelOutlet
320
+ // name="@modal" />` read from a separate context channel and end up as
321
+ // siblings of the VT in the rendered tree, so modal mounts don't trigger a
322
+ // subtree update on the layout-level VT — which would otherwise make
323
+ // React's commit walker fire `document.startViewTransition` and apply
324
+ // view-transition-names to the underlying main subtree (cover/title/etc.).
325
+ let outletContent: ReactNode =
285
326
  node.segment.type === "layout" ? content : null;
286
327
 
328
+ const transition = node.segment.transition;
329
+
330
+ if (ReactViewTransition && transition) {
331
+ if (node.segment.type === "layout") {
332
+ outletContent = wrapDefaultOutletContent(outletContent, transition);
333
+ } else {
334
+ nodeContent = createViewTransitionBoundary(transition, nodeContent);
335
+ }
336
+ }
337
+
287
338
  // Prepare loader data if there are loaders
288
339
  const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
289
340
  const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);