@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7d061845
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.
- package/README.md +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2138 -841
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +68 -21
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +15 -9
- package/skills/migrate-nextjs/SKILL.md +4 -2
- package/skills/migrate-react-router/SKILL.md +5 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +12 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +33 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +816 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +65 -9
- package/src/browser/navigation-client.ts +45 -25
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +52 -26
- package/src/browser/prefetch/cache.ts +124 -26
- package/src/browser/prefetch/fetch.ts +114 -38
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +18 -13
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +64 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +2 -1
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +10 -8
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +9 -4
- package/src/index.ts +16 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +21 -6
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -39
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +253 -265
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +43 -15
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +21 -41
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +3 -3
- package/src/router/loader-resolution.ts +19 -2
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +38 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +122 -56
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +20 -42
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +332 -0
- package/src/testing/flight.entry.ts +46 -0
- package/src/testing/flight.ts +224 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +304 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +42 -0
- package/src/testing/render-handler.ts +267 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +341 -0
- package/src/testing/run-middleware.ts +188 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +270 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/segments.ts +35 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +101 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +5 -4
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
package/src/segment-system.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { createElement, type ReactNode, type ComponentType } from "react";
|
|
|
3
3
|
import { OutletProvider } from "./client.js";
|
|
4
4
|
import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
5
5
|
import type { ResolvedSegment, RootLayoutProps } from "./types.js";
|
|
6
|
-
import {
|
|
6
|
+
import { decodeLoaderResults } from "./decode-loader-results.js";
|
|
7
7
|
import { invariant } from "./errors.js";
|
|
8
8
|
import {
|
|
9
9
|
RouteContentWrapper,
|
|
@@ -59,42 +59,6 @@ function restoreParallelLoaderMarkers(
|
|
|
59
59
|
return nextSegments ?? segments;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
* Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
|
|
64
|
-
*/
|
|
65
|
-
function resolveLoaderData(
|
|
66
|
-
resolvedData: any[],
|
|
67
|
-
loaderIds: string[],
|
|
68
|
-
): { loaderData: Record<string, any>; errorFallback: ReactNode } {
|
|
69
|
-
const loaderData: Record<string, any> = {};
|
|
70
|
-
let errorFallback: ReactNode = null;
|
|
71
|
-
|
|
72
|
-
for (let i = 0; i < loaderIds.length; i++) {
|
|
73
|
-
const id = loaderIds[i];
|
|
74
|
-
const result = resolvedData[i];
|
|
75
|
-
|
|
76
|
-
if (!isLoaderDataResult(result)) {
|
|
77
|
-
// Legacy format - direct data
|
|
78
|
-
loaderData[id] = result;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (result.ok) {
|
|
83
|
-
loaderData[id] = result.data;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Error case
|
|
88
|
-
if (result.fallback) {
|
|
89
|
-
errorFallback = result.fallback;
|
|
90
|
-
} else {
|
|
91
|
-
throw new Error(result.error.message);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return { loaderData, errorFallback };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
62
|
/**
|
|
99
63
|
* Options for renderSegments
|
|
100
64
|
*/
|
|
@@ -131,6 +95,50 @@ export interface RenderSegmentsOptions {
|
|
|
131
95
|
rootLayout?: ComponentType<RootLayoutProps>;
|
|
132
96
|
}
|
|
133
97
|
|
|
98
|
+
function createViewTransitionBoundary(
|
|
99
|
+
transition: NonNullable<ResolvedSegment["transition"]>,
|
|
100
|
+
children: ReactNode,
|
|
101
|
+
): ReactNode {
|
|
102
|
+
// `viewTransition` is a router-specific flag (boundary opt-out), not a React
|
|
103
|
+
// <ViewTransition> prop — strip it so it never reaches React.
|
|
104
|
+
const { viewTransition: _viewTransition, ...vtProps } = transition;
|
|
105
|
+
return createElement(ReactViewTransition, {
|
|
106
|
+
...vtProps,
|
|
107
|
+
children,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function wrapDefaultOutletContent(
|
|
112
|
+
content: ReactNode,
|
|
113
|
+
transition: NonNullable<ResolvedSegment["transition"]>,
|
|
114
|
+
): ReactNode {
|
|
115
|
+
if (!React.isValidElement(content)) {
|
|
116
|
+
return createViewTransitionBoundary(transition, content);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const props = content.props as any;
|
|
120
|
+
|
|
121
|
+
if (content.type === MountContextProvider) {
|
|
122
|
+
return React.cloneElement(content, {
|
|
123
|
+
children: wrapDefaultOutletContent(props.children, transition),
|
|
124
|
+
} as any);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (content.type === OutletProvider && props.segment?.type === "layout") {
|
|
128
|
+
return React.cloneElement(content, {
|
|
129
|
+
content: wrapDefaultOutletContent(props.content, transition),
|
|
130
|
+
} as any);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (content.type === LoaderBoundary && props.segment?.type === "layout") {
|
|
134
|
+
return React.cloneElement(content, {
|
|
135
|
+
outletContent: wrapDefaultOutletContent(props.outletContent, transition),
|
|
136
|
+
} as any);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return createViewTransitionBoundary(transition, content);
|
|
140
|
+
}
|
|
141
|
+
|
|
134
142
|
/**
|
|
135
143
|
* Render segments into a React tree with proper layout nesting
|
|
136
144
|
*
|
|
@@ -211,6 +219,25 @@ export async function renderSegments(
|
|
|
211
219
|
}
|
|
212
220
|
// Separate segments by type, passing intercept segments for explicit injection
|
|
213
221
|
const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
|
|
222
|
+
|
|
223
|
+
// A route is "in a transition scope" when its own segment OR any layout in
|
|
224
|
+
// its matched chain declares transition(). Both transition() forms land here:
|
|
225
|
+
// the per-route item form sets transition on the route entry, and the block
|
|
226
|
+
// wrapper form sets it on a transparent ancestor layout (dsl-helpers.ts). When
|
|
227
|
+
// in scope, the route and its route-owned layouts use param-agnostic keys so a
|
|
228
|
+
// same-route navigation reconciles (holds content) instead of remounting. The
|
|
229
|
+
// value is a static property of the route's position in the tree, so it is the
|
|
230
|
+
// same on every render of that route (SSR, navigation, action) — the keys
|
|
231
|
+
// never drift. Cross-route navigation still remounts: different routes have
|
|
232
|
+
// different segment ids regardless of transition scope.
|
|
233
|
+
const inTransitionScope = normalizedSegments.some(
|
|
234
|
+
(s) =>
|
|
235
|
+
s.transition != null &&
|
|
236
|
+
(s.type === "layout" ||
|
|
237
|
+
s.type === "route" ||
|
|
238
|
+
s.type === "error" ||
|
|
239
|
+
s.type === "notFound"),
|
|
240
|
+
);
|
|
214
241
|
// Render content segments as siblings
|
|
215
242
|
let content: ReactNode = null;
|
|
216
243
|
for (const node of tree) {
|
|
@@ -223,17 +250,31 @@ export async function renderSegments(
|
|
|
223
250
|
);
|
|
224
251
|
const { component, id, params, loading } = node.segment;
|
|
225
252
|
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
253
|
+
// Param-agnostic keys are opt-in via the transition() DSL (see
|
|
254
|
+
// inTransitionScope above). A route (and its route-owned layouts) inside a
|
|
255
|
+
// transition scope drops the param from its key, so navigating between two
|
|
256
|
+
// param values of the SAME route (e.g. /product/1 -> /product/2) reconciles
|
|
257
|
+
// the route subtree instead of remounting it. Combined with the
|
|
258
|
+
// startTransition wrap that shouldStartViewTransition already applies to
|
|
259
|
+
// transition routes (browser/partial-update.ts), the previous content stays
|
|
260
|
+
// on screen while the new loaders resolve (stale-while-revalidate) instead
|
|
261
|
+
// of flashing the loading skeleton. This works on stable React; experimental
|
|
262
|
+
// React adds the animated <ViewTransition> cross-fade on top.
|
|
263
|
+
//
|
|
264
|
+
// Outside a transition scope the key stays param-bearing and the route
|
|
265
|
+
// remounts on param change (the default: a fresh skeleton and fresh
|
|
266
|
+
// component state).
|
|
267
|
+
//
|
|
268
|
+
// error/notFound always keep param-bearing keys: createErrorSegment reuses
|
|
269
|
+
// the boundary layout's shortCode as the error segment id (router/
|
|
270
|
+
// error-handling.ts), so a param-agnostic error key could collide with that
|
|
271
|
+
// layout's key within the same render.
|
|
232
272
|
const includeParams =
|
|
233
|
-
node.segment.type === "route" ||
|
|
234
273
|
node.segment.type === "error" ||
|
|
235
274
|
node.segment.type === "notFound" ||
|
|
236
|
-
(node.segment.type === "
|
|
275
|
+
((node.segment.type === "route" ||
|
|
276
|
+
(node.segment.type === "layout" && node.segment.belongsToRoute)) &&
|
|
277
|
+
!inTransitionScope);
|
|
237
278
|
|
|
238
279
|
const paramStr =
|
|
239
280
|
includeParams && params && Object.keys(params).length > 0
|
|
@@ -273,26 +314,51 @@ 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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
|
|
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
|
+
//
|
|
326
|
+
// `transition.viewTransition === false` opts out of the router-owned
|
|
327
|
+
// boundary only. Driving (the startTransition wrap in browser/partial-update.ts
|
|
328
|
+
// and the param-agnostic key/hold below) keys off transition *presence*, not
|
|
329
|
+
// this flag, so a boundary-less transition still holds content and lets
|
|
330
|
+
// consumer-placed <ViewTransition> elements animate. The global
|
|
331
|
+
// createRouter({ viewTransition }) default is resolved into this field
|
|
332
|
+
// during segment resolution (only `false` is stamped; unset/"auto" is left
|
|
333
|
+
// as-is and means "wrap"), so this gate needs no router-option threading.
|
|
334
|
+
let outletContent: ReactNode =
|
|
285
335
|
node.segment.type === "layout" ? content : null;
|
|
286
336
|
|
|
337
|
+
const transition = node.segment.transition;
|
|
338
|
+
|
|
339
|
+
if (
|
|
340
|
+
ReactViewTransition &&
|
|
341
|
+
transition &&
|
|
342
|
+
transition.viewTransition !== false
|
|
343
|
+
) {
|
|
344
|
+
if (node.segment.type === "layout") {
|
|
345
|
+
outletContent = wrapDefaultOutletContent(outletContent, transition);
|
|
346
|
+
} else {
|
|
347
|
+
nodeContent = createViewTransitionBoundary(transition, nodeContent);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
287
351
|
// Prepare loader data if there are loaders
|
|
288
352
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
289
|
-
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
290
353
|
|
|
291
354
|
// Use LoaderBoundary when loading is defined to maintain consistent tree structure
|
|
292
355
|
// This ensures cached segments (which may not have loader segments) have the same
|
|
293
356
|
// tree structure as fresh segments, preventing React remounts
|
|
294
357
|
// If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
|
|
295
358
|
if (loading !== undefined && loading !== null) {
|
|
359
|
+
// Aggregate built here only — the loaderless and no-loading branches don't
|
|
360
|
+
// read it (the latter builds its own per-parallel promises).
|
|
361
|
+
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
296
362
|
content = createElement(LoaderBoundary, {
|
|
297
363
|
key: `loader-boundary-${key}`,
|
|
298
364
|
loaderDataPromise:
|
|
@@ -336,7 +402,7 @@ export async function renderSegments(
|
|
|
336
402
|
)
|
|
337
403
|
: Promise.resolve([]);
|
|
338
404
|
const resolvedData = await layoutLoaderDataPromise;
|
|
339
|
-
const { loaderData, errorFallback } =
|
|
405
|
+
const { loaderData, errorFallback } = decodeLoaderResults(
|
|
340
406
|
resolvedData,
|
|
341
407
|
layoutLoaderIds,
|
|
342
408
|
);
|
package/src/serialize.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-type serialization transforms.
|
|
3
|
+
*
|
|
4
|
+
* The type a handler or loader returns on the server is frequently NOT the type
|
|
5
|
+
* a client receives after serialization. These transforms model that boundary so
|
|
6
|
+
* consumer-facing types (e.g. `Rango.PathResponse`) describe the wire value, not
|
|
7
|
+
* the source value.
|
|
8
|
+
*
|
|
9
|
+
* Two serializers, two transforms — they are intentionally NOT interchangeable:
|
|
10
|
+
*
|
|
11
|
+
* - `JsonSerialize<T>` models plain `JSON.stringify` (`path.json()` /
|
|
12
|
+
* `fetch().then(r => r.json())`). Lossy: `Date -> string`, `undefined` /
|
|
13
|
+
* functions / symbols dropped, `Map`/`Set` -> `{}`. `bigint` *throws* (no wire
|
|
14
|
+
* value), so it collapses the whole result to `never`. Honors `toJSON()`.
|
|
15
|
+
* - `FlightSerialize<T>` models React RSC Flight (loaders, RSC props, cache).
|
|
16
|
+
* High fidelity: `Date`/`Map`/`Set`/`bigint`/typed arrays/`Promise` are
|
|
17
|
+
* preserved; ordinary functions and non-global symbols do not cross.
|
|
18
|
+
*
|
|
19
|
+
* ## Overriding (full-transform replacement)
|
|
20
|
+
*
|
|
21
|
+
* Because `Rango.JsonSerialize` / `Rango.FlightSerialize` are type *aliases*, TS
|
|
22
|
+
* cannot let you redefine them directly (aliases don't merge). Instead each alias
|
|
23
|
+
* consults a generic override slot — augment it with a single member that is your
|
|
24
|
+
* complete transform. Delegate to the built-in for the cases you don't change:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* declare global {
|
|
28
|
+
* namespace Rango {
|
|
29
|
+
* interface FlightSerializeOverride<T> {
|
|
30
|
+
* app: T extends Money ? number : Rango.FlightSerializeBuiltin<T>;
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* // now Rango.FlightSerialize<Money> is number; everything else is the built-in.
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Provide exactly one member: the slot is read as `Override<T>[keyof Override<T>]`,
|
|
38
|
+
* so multiple members union (and conflict). The built-in recurses through the
|
|
39
|
+
* override-aware alias, so an override applies at every nesting level.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import type { ReactNode } from "react";
|
|
43
|
+
|
|
44
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
45
|
+
|
|
46
|
+
type AnyFunction = (...args: never[]) => unknown;
|
|
47
|
+
|
|
48
|
+
// --- JSON ---------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Internal marker for a value that makes `JSON.stringify` throw (`bigint`, or a
|
|
52
|
+
* `toJSON()` returning one). Distinct from `never`, which means "omitted":
|
|
53
|
+
* `undefined`/function/symbol-valued keys are dropped, and such array slots
|
|
54
|
+
* become `null`. A throwing value has no valid JSON wire form, so it propagates
|
|
55
|
+
* up through every container and is excluded at the public boundary (`bigint`
|
|
56
|
+
* alone -> `never`; `{ id: bigint }` -> `never`).
|
|
57
|
+
*/
|
|
58
|
+
declare const JSON_THROWS: unique symbol;
|
|
59
|
+
type JsonThrows = typeof JSON_THROWS;
|
|
60
|
+
|
|
61
|
+
/** True if union `U` contains the throw marker. */
|
|
62
|
+
type HasThrow<U> = [Extract<U, JsonThrows>] extends [never] ? false : true;
|
|
63
|
+
|
|
64
|
+
/** Map a JSON array/tuple: propagate a throw; else omitted elements become null. */
|
|
65
|
+
type JsonSerializeArray<T extends readonly unknown[]> =
|
|
66
|
+
HasThrow<{ [K in keyof T]: JsonRawResolve<T[K]> }[number]> extends true
|
|
67
|
+
? JsonThrows
|
|
68
|
+
: {
|
|
69
|
+
[K in keyof T]: [JsonRawResolve<T[K]>] extends [never]
|
|
70
|
+
? null
|
|
71
|
+
: JsonRawResolve<T[K]>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Map a JSON object: propagate a throw; else drop omitted keys. */
|
|
75
|
+
type JsonSerializeObject<T> =
|
|
76
|
+
HasThrow<{ [K in keyof T]: JsonRawResolve<T[K]> }[keyof T]> extends true
|
|
77
|
+
? JsonThrows
|
|
78
|
+
: {
|
|
79
|
+
[K in keyof T as [JsonRawResolve<T[K]>] extends [never]
|
|
80
|
+
? never
|
|
81
|
+
: K]: JsonRawResolve<T[K]>;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Built-in JSON rules, *raw* (may yield the throw marker). Honors `toJSON()` (so
|
|
86
|
+
* `Date -> string` and any class with `toJSON()` serialize correctly), preserves
|
|
87
|
+
* JSON primitives and literals, omits functions / symbols / `undefined`,
|
|
88
|
+
* collapses `Map`/`Set` to `{}`, and marks `bigint` as throwing. Recurses through
|
|
89
|
+
* the override-aware resolver, so registered overrides apply at every level.
|
|
90
|
+
*/
|
|
91
|
+
type JsonSerializeBuiltinRaw<T> = T extends {
|
|
92
|
+
toJSON(...args: never[]): infer R;
|
|
93
|
+
}
|
|
94
|
+
? JsonRawResolve<R>
|
|
95
|
+
: T extends JsonPrimitive
|
|
96
|
+
? T
|
|
97
|
+
: T extends bigint
|
|
98
|
+
? JsonThrows
|
|
99
|
+
: T extends AnyFunction
|
|
100
|
+
? never
|
|
101
|
+
: T extends symbol
|
|
102
|
+
? never
|
|
103
|
+
: T extends undefined
|
|
104
|
+
? never
|
|
105
|
+
: T extends readonly unknown[]
|
|
106
|
+
? JsonSerializeArray<T>
|
|
107
|
+
: T extends ReadonlyMap<unknown, unknown>
|
|
108
|
+
? {}
|
|
109
|
+
: T extends ReadonlySet<unknown>
|
|
110
|
+
? {}
|
|
111
|
+
: T extends object
|
|
112
|
+
? JsonSerializeObject<T>
|
|
113
|
+
: never;
|
|
114
|
+
|
|
115
|
+
/** Override-aware raw JSON resolution (the recursion entry). */
|
|
116
|
+
type JsonRawResolve<T> = [keyof Rango.JsonSerializeOverride<T>] extends [never]
|
|
117
|
+
? JsonSerializeBuiltinRaw<T>
|
|
118
|
+
: Rango.JsonSerializeOverride<T>[keyof Rango.JsonSerializeOverride<T>];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Model the result of round-tripping a value through `JSON.stringify` /
|
|
122
|
+
* `JSON.parse`. A registered `Rango.JsonSerializeOverride` replaces the transform
|
|
123
|
+
* wholesale; otherwise the built-in rules apply. Throwing values collapse to
|
|
124
|
+
* `never`.
|
|
125
|
+
*/
|
|
126
|
+
export type JsonSerialize<T> = Exclude<JsonRawResolve<T>, JsonThrows>;
|
|
127
|
+
|
|
128
|
+
// --- Flight -------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Built-in Flight rules. Mirrors React's `ReactClientValue` contract: primitives
|
|
132
|
+
* including `bigint`, `undefined`, `null`, symbols, `Date`, `ArrayBuffer` and
|
|
133
|
+
* typed-array views, `Map`, `Set`, `FormData`, `Blob`, `Promise`,
|
|
134
|
+
* `ReadableStream`, and (async) iterables are preserved; ordinary functions
|
|
135
|
+
* resolve to `never`. JSX (`ReactNode`, and the async-node union
|
|
136
|
+
* `ReactNode | Promise<ReactNode>`) is preserved as-is via a non-distributive
|
|
137
|
+
* leaf, so handle/loader returns that carry JSX round-trip unchanged. Recurses
|
|
138
|
+
* through the override-aware `FlightSerialize`.
|
|
139
|
+
*
|
|
140
|
+
* The source of truth is React's own contract, which is intentionally NOT
|
|
141
|
+
* semver-stable across RSC framework APIs — this tracks the React version Rango
|
|
142
|
+
* pins. See:
|
|
143
|
+
* https://react.dev/reference/rsc/use-client#serializable-types-returned-by-server-components
|
|
144
|
+
*
|
|
145
|
+
* Type-level limitations (not detectable structurally, so not modeled): class
|
|
146
|
+
* instances and null-prototype objects are rejected by React at runtime but pass
|
|
147
|
+
* here as their structural shape; non-global symbols are rejected at runtime but
|
|
148
|
+
* `symbol` is preserved here; Server Functions would need an override to be
|
|
149
|
+
* distinguished from ordinary functions (which resolve to `never`).
|
|
150
|
+
*/
|
|
151
|
+
type FlightSerializeBuiltinRaw<T> = [T] extends [ReactNode | Promise<ReactNode>]
|
|
152
|
+
? T
|
|
153
|
+
: T extends string | number | boolean | bigint | symbol | null | undefined
|
|
154
|
+
? T
|
|
155
|
+
: T extends AnyFunction
|
|
156
|
+
? never
|
|
157
|
+
: T extends Date
|
|
158
|
+
? Date
|
|
159
|
+
: T extends ArrayBuffer
|
|
160
|
+
? ArrayBuffer
|
|
161
|
+
: T extends ArrayBufferView
|
|
162
|
+
? T
|
|
163
|
+
: T extends FormData
|
|
164
|
+
? FormData
|
|
165
|
+
: T extends Blob
|
|
166
|
+
? Blob
|
|
167
|
+
: T extends Map<infer K, infer V>
|
|
168
|
+
? Map<FlightSerialize<K>, FlightSerialize<V>>
|
|
169
|
+
: T extends Set<infer V>
|
|
170
|
+
? Set<FlightSerialize<V>>
|
|
171
|
+
: T extends Promise<infer V>
|
|
172
|
+
? Promise<FlightSerialize<V>>
|
|
173
|
+
: T extends ReadableStream<infer V>
|
|
174
|
+
? ReadableStream<FlightSerialize<V>>
|
|
175
|
+
: T extends readonly unknown[]
|
|
176
|
+
? { [K in keyof T]: FlightSerialize<T[K]> }
|
|
177
|
+
: T extends AsyncIterable<infer V>
|
|
178
|
+
? AsyncIterable<FlightSerialize<V>>
|
|
179
|
+
: T extends Iterable<infer V>
|
|
180
|
+
? Iterable<FlightSerialize<V>>
|
|
181
|
+
: T extends object
|
|
182
|
+
? { [K in keyof T]: FlightSerialize<T[K]> }
|
|
183
|
+
: never;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Model React RSC Flight serialization. A registered `Rango.FlightSerializeOverride`
|
|
187
|
+
* replaces the transform wholesale; otherwise the built-in rules apply.
|
|
188
|
+
*/
|
|
189
|
+
export type FlightSerialize<T> = [
|
|
190
|
+
keyof Rango.FlightSerializeOverride<T>,
|
|
191
|
+
] extends [never]
|
|
192
|
+
? FlightSerializeBuiltinRaw<T>
|
|
193
|
+
: Rango.FlightSerializeOverride<T>[keyof Rango.FlightSerializeOverride<T>];
|
|
194
|
+
|
|
195
|
+
// Module-scoped aliases so the ambient `Rango.*` members below can reference the
|
|
196
|
+
// module-level transforms without the global namespace shadowing the names.
|
|
197
|
+
type GlobalJsonSerialize<T> = JsonSerialize<T>;
|
|
198
|
+
type GlobalJsonSerializeBuiltin<T> = JsonSerializeBuiltinRaw<T>;
|
|
199
|
+
type GlobalFlightSerialize<T> = FlightSerialize<T>;
|
|
200
|
+
type GlobalFlightSerializeBuiltin<T> = FlightSerializeBuiltinRaw<T>;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Ambient serialization transforms and their override slots on the `Rango`
|
|
204
|
+
* namespace. Available with no import wherever the router's types are in scope,
|
|
205
|
+
* alongside `Rango.Path` / `Rango.PathResponse`.
|
|
206
|
+
*
|
|
207
|
+
* `Rango.JsonSerialize` is what `Rango.PathResponse` applies; `Rango.FlightSerialize`
|
|
208
|
+
* is exposed for RSC/loader/cache wire types and must NOT be used for `path.json()`.
|
|
209
|
+
* `Rango.JsonSerializeBuiltin` / `Rango.FlightSerializeBuiltin` are the defaults,
|
|
210
|
+
* exported so an override can delegate to them.
|
|
211
|
+
*/
|
|
212
|
+
declare global {
|
|
213
|
+
namespace Rango {
|
|
214
|
+
/**
|
|
215
|
+
* Full-transform override slot for `Rango.JsonSerialize`. Empty by default;
|
|
216
|
+
* augment with one member that is your complete transform (delegate to
|
|
217
|
+
* `Rango.JsonSerializeBuiltin<T>` for the cases you don't change).
|
|
218
|
+
*/
|
|
219
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
220
|
+
interface JsonSerializeOverride<T> {}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Full-transform override slot for `Rango.FlightSerialize`. Empty by default;
|
|
224
|
+
* augment with one member that is your complete transform (delegate to
|
|
225
|
+
* `Rango.FlightSerializeBuiltin<T>` for the cases you don't change).
|
|
226
|
+
*/
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
228
|
+
interface FlightSerializeOverride<T> {}
|
|
229
|
+
|
|
230
|
+
/** Wire type after `JSON.stringify` (`path.json()` / `fetch().json()`). */
|
|
231
|
+
type JsonSerialize<T> = GlobalJsonSerialize<T>;
|
|
232
|
+
/**
|
|
233
|
+
* Built-in `JsonSerialize` rules, for an override to delegate to. Raw: a
|
|
234
|
+
* `bigint`-bearing type yields the internal throw marker here, which
|
|
235
|
+
* `Rango.JsonSerialize` excludes to `never` at the boundary.
|
|
236
|
+
*/
|
|
237
|
+
type JsonSerializeBuiltin<T> = GlobalJsonSerializeBuiltin<T>;
|
|
238
|
+
/** Wire type after RSC Flight serialization (loaders / RSC props / cache). */
|
|
239
|
+
type FlightSerialize<T> = GlobalFlightSerialize<T>;
|
|
240
|
+
/** Built-in `FlightSerialize` rules, for an override to delegate to. */
|
|
241
|
+
type FlightSerializeBuiltin<T> = GlobalFlightSerializeBuiltin<T>;
|
|
242
|
+
}
|
|
243
|
+
}
|