@rangojs/router 0.0.0-experimental.d20dd405 → 0.0.0-experimental.dacec167
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 +8 -8
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +914 -485
- package/package.json +55 -11
- 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 +1 -1
- package/skills/hooks/SKILL.md +196 -21
- package/skills/host-router/SKILL.md +45 -20
- package/skills/intercept/SKILL.md +1 -4
- package/skills/layout/SKILL.md +4 -7
- package/skills/links/SKILL.md +22 -10
- package/skills/loader/SKILL.md +149 -6
- package/skills/middleware/SKILL.md +13 -9
- package/skills/migrate-nextjs/SKILL.md +1 -1
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +3 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/route/SKILL.md +9 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +53 -41
- package/skills/testing/SKILL.md +778 -0
- package/skills/typesafety/SKILL.md +310 -26
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +85 -3
- 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/event-controller.ts +42 -66
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +9 -67
- package/src/browser/navigation-client.ts +12 -15
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +8 -16
- package/src/browser/react/NavigationProvider.tsx +55 -65
- 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-params.ts +3 -4
- package/src/browser/react/use-reverse.ts +19 -12
- package/src/browser/react/use-router.ts +14 -1
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +30 -16
- package/src/browser/scroll-restoration.ts +30 -25
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +2 -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-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 +49 -6
- package/src/client.tsx +5 -7
- 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 +136 -19
- package/src/index.rsc.ts +6 -4
- package/src/index.ts +13 -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/prerender.ts +4 -4
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +16 -13
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +238 -263
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +37 -14
- 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/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/loader-resolution.ts +16 -2
- package/src/router/match-handlers.ts +62 -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 +32 -30
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/middleware.ts +46 -78
- 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 +43 -1
- 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 +19 -6
- package/src/router/segment-resolution/revalidation.ts +19 -6
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/types.ts +8 -0
- package/src/router.ts +37 -21
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +20 -65
- package/src/rsc/helpers.ts +22 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/response-route-handler.ts +32 -52
- package/src/rsc/rsc-rendering.ts +27 -53
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +13 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +64 -49
- 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 +10 -0
- 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 +320 -0
- package/src/testing/flight.entry.ts +39 -0
- package/src/testing/flight.ts +197 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +331 -0
- package/src/testing/internal/flight-client-globals.ts +30 -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 +56 -11
- package/src/types/index.ts +1 -0
- package/src/types/segments.ts +18 -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 +20 -19
- 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 +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +70 -48
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +40 -84
- 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 +3 -7
- package/src/vite/plugins/client-ref-hashing.ts +12 -1
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
- package/src/vite/plugins/expose-action-id.ts +2 -2
- 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-internal-ids.ts +47 -67
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +13 -11
- package/src/vite/plugins/version-injector.ts +2 -12
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +67 -15
- package/src/vite/router-discovery.ts +208 -63
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- 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/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createResponseWithMergedHeaders,
|
|
9
9
|
carryOverRedirectHeaders,
|
|
10
10
|
} from "./helpers.js";
|
|
11
|
+
import { isRedirectResponse } from "../response-utils.js";
|
|
11
12
|
|
|
12
13
|
// W3 -----------------------------------------------------------------------
|
|
13
14
|
|
|
@@ -18,16 +19,14 @@ import {
|
|
|
18
19
|
*/
|
|
19
20
|
export function extractRedirectResponse(value: unknown): Response | null {
|
|
20
21
|
if (!(value instanceof Response)) return null;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
22
|
+
if (!isRedirectResponse(value)) return null;
|
|
23
|
+
const location = value.headers.get("Location")!;
|
|
24
|
+
const redirect = createResponseWithMergedHeaders(null, {
|
|
25
|
+
status: value.status,
|
|
26
|
+
headers: { Location: location },
|
|
27
|
+
});
|
|
28
|
+
carryOverRedirectHeaders(value, redirect);
|
|
29
|
+
return redirect;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
/**
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
hasBodyContent,
|
|
28
28
|
createResponseWithMergedHeaders,
|
|
29
29
|
createSimpleRedirectResponse,
|
|
30
|
-
|
|
30
|
+
interceptRedirectForPartial,
|
|
31
31
|
} from "./helpers.js";
|
|
32
32
|
import type { HandlerContext } from "./handler-context.js";
|
|
33
33
|
|
|
@@ -111,49 +111,25 @@ export async function executeServerAction<TEnv>(
|
|
|
111
111
|
loadedAction = await ctx.loadServerAction(actionId);
|
|
112
112
|
const data = await loadedAction!.apply(null, args);
|
|
113
113
|
|
|
114
|
-
// Intercept redirect
|
|
115
|
-
//
|
|
116
|
-
// and the revalidation step would run unnecessarily.
|
|
114
|
+
// Intercept redirect Responses: serializing one as the action returnValue
|
|
115
|
+
// would fail, and revalidation would run needlessly.
|
|
117
116
|
if (data instanceof Response) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (locationState) {
|
|
124
|
-
redirect = ctx.createRedirectFlightResponse(
|
|
125
|
-
redirectUrl,
|
|
126
|
-
resolveLocationStateEntries(locationState),
|
|
127
|
-
);
|
|
128
|
-
} else {
|
|
129
|
-
redirect = createSimpleRedirectResponse(redirectUrl);
|
|
130
|
-
}
|
|
131
|
-
carryOverRedirectHeaders(data, redirect);
|
|
132
|
-
return redirect;
|
|
133
|
-
}
|
|
117
|
+
const intercepted = interceptRedirectForPartial(
|
|
118
|
+
data,
|
|
119
|
+
ctx.createRedirectFlightResponse,
|
|
120
|
+
);
|
|
121
|
+
if (intercepted) return intercepted;
|
|
134
122
|
}
|
|
135
123
|
|
|
136
124
|
returnValue = { ok: true, data };
|
|
137
125
|
} catch (error) {
|
|
138
126
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
139
127
|
if (error instanceof Response) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
let redirect: Response;
|
|
146
|
-
if (locationState) {
|
|
147
|
-
redirect = ctx.createRedirectFlightResponse(
|
|
148
|
-
redirectUrl,
|
|
149
|
-
resolveLocationStateEntries(locationState),
|
|
150
|
-
);
|
|
151
|
-
} else {
|
|
152
|
-
redirect = createSimpleRedirectResponse(redirectUrl);
|
|
153
|
-
}
|
|
154
|
-
carryOverRedirectHeaders(error, redirect);
|
|
155
|
-
return redirect;
|
|
156
|
-
}
|
|
128
|
+
const intercepted = interceptRedirectForPartial(
|
|
129
|
+
error,
|
|
130
|
+
ctx.createRedirectFlightResponse,
|
|
131
|
+
);
|
|
132
|
+
if (intercepted) return intercepted;
|
|
157
133
|
|
|
158
134
|
// Non-redirect Response thrown from action — this will be treated
|
|
159
135
|
// as a regular error and routed to the error boundary. Warn in dev
|
package/src/rsc/ssr-setup.ts
CHANGED
|
@@ -126,3 +126,19 @@ export function mayNeedSSR(request: Request, url: URL): boolean {
|
|
|
126
126
|
|
|
127
127
|
return true;
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
// Final render-time decision: is the response an RSC stream (vs HTML)? Distinct
|
|
131
|
+
// from mayNeedSSR, which is a conservative pre-classifier (it treats a missing
|
|
132
|
+
// Accept header as needing SSR; this treats it as RSC).
|
|
133
|
+
export function isRscRequest(
|
|
134
|
+
request: Request,
|
|
135
|
+
url: URL,
|
|
136
|
+
isPartial: boolean,
|
|
137
|
+
): boolean {
|
|
138
|
+
return (
|
|
139
|
+
isPartial ||
|
|
140
|
+
(!request.headers.get("accept")?.includes("text/html") &&
|
|
141
|
+
!url.searchParams.has("__html")) ||
|
|
142
|
+
url.searchParams.has("__rsc")
|
|
143
|
+
);
|
|
144
|
+
}
|
package/src/rsc/types.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ResolvedSegment, SlotState } from "../types.js";
|
|
9
9
|
import type { HandleData } from "../server/handle-store.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type { RangoInternal } from "../router/router-interfaces.js";
|
|
11
11
|
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -185,7 +185,7 @@ export interface CreateRSCHandlerOptions<
|
|
|
185
185
|
/**
|
|
186
186
|
* The RSC router instance
|
|
187
187
|
*/
|
|
188
|
-
router:
|
|
188
|
+
router: RangoInternal<TEnv, TRoutes>;
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
191
|
* RSC dependencies from @vitejs/plugin-rsc/rsc.
|
package/src/search-params.ts
CHANGED
|
@@ -81,11 +81,11 @@ export type ResolveSearchSchema<T extends SearchSchema> = Simplify<
|
|
|
81
81
|
// ============================================================================
|
|
82
82
|
|
|
83
83
|
/** Resolve the global route map from RegisteredRoutes or GeneratedRouteMap. */
|
|
84
|
-
type GlobalRouteMap = keyof
|
|
85
|
-
? keyof
|
|
84
|
+
type GlobalRouteMap = keyof Rango.RegisteredRoutes extends never
|
|
85
|
+
? keyof Rango.GeneratedRouteMap extends never
|
|
86
86
|
? Record<string, string>
|
|
87
|
-
:
|
|
88
|
-
:
|
|
87
|
+
: Rango.GeneratedRouteMap
|
|
88
|
+
: Rango.RegisteredRoutes;
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* Extract the resolved search params type for a named route.
|
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
|
*/
|
|
@@ -135,8 +99,11 @@ function createViewTransitionBoundary(
|
|
|
135
99
|
transition: NonNullable<ResolvedSegment["transition"]>,
|
|
136
100
|
children: ReactNode,
|
|
137
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;
|
|
138
105
|
return createElement(ReactViewTransition, {
|
|
139
|
-
...
|
|
106
|
+
...vtProps,
|
|
140
107
|
children,
|
|
141
108
|
});
|
|
142
109
|
}
|
|
@@ -252,6 +219,25 @@ export async function renderSegments(
|
|
|
252
219
|
}
|
|
253
220
|
// Separate segments by type, passing intercept segments for explicit injection
|
|
254
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
|
+
);
|
|
255
241
|
// Render content segments as siblings
|
|
256
242
|
let content: ReactNode = null;
|
|
257
243
|
for (const node of tree) {
|
|
@@ -264,17 +250,31 @@ export async function renderSegments(
|
|
|
264
250
|
);
|
|
265
251
|
const { component, id, params, loading } = node.segment;
|
|
266
252
|
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
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.
|
|
273
272
|
const includeParams =
|
|
274
|
-
node.segment.type === "route" ||
|
|
275
273
|
node.segment.type === "error" ||
|
|
276
274
|
node.segment.type === "notFound" ||
|
|
277
|
-
(node.segment.type === "
|
|
275
|
+
((node.segment.type === "route" ||
|
|
276
|
+
(node.segment.type === "layout" && node.segment.belongsToRoute)) &&
|
|
277
|
+
!inTransitionScope);
|
|
278
278
|
|
|
279
279
|
const paramStr =
|
|
280
280
|
includeParams && params && Object.keys(params).length > 0
|
|
@@ -322,12 +322,25 @@ export async function renderSegments(
|
|
|
322
322
|
// subtree update on the layout-level VT — which would otherwise make
|
|
323
323
|
// React's commit walker fire `document.startViewTransition` and apply
|
|
324
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.
|
|
325
334
|
let outletContent: ReactNode =
|
|
326
335
|
node.segment.type === "layout" ? content : null;
|
|
327
336
|
|
|
328
337
|
const transition = node.segment.transition;
|
|
329
338
|
|
|
330
|
-
if (
|
|
339
|
+
if (
|
|
340
|
+
ReactViewTransition &&
|
|
341
|
+
transition &&
|
|
342
|
+
transition.viewTransition !== false
|
|
343
|
+
) {
|
|
331
344
|
if (node.segment.type === "layout") {
|
|
332
345
|
outletContent = wrapDefaultOutletContent(outletContent, transition);
|
|
333
346
|
} else {
|
|
@@ -337,13 +350,15 @@ export async function renderSegments(
|
|
|
337
350
|
|
|
338
351
|
// Prepare loader data if there are loaders
|
|
339
352
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
340
|
-
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
341
353
|
|
|
342
354
|
// Use LoaderBoundary when loading is defined to maintain consistent tree structure
|
|
343
355
|
// This ensures cached segments (which may not have loader segments) have the same
|
|
344
356
|
// tree structure as fresh segments, preventing React remounts
|
|
345
357
|
// If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
|
|
346
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);
|
|
347
362
|
content = createElement(LoaderBoundary, {
|
|
348
363
|
key: `loader-boundary-${key}`,
|
|
349
364
|
loaderDataPromise:
|
|
@@ -387,7 +402,7 @@ export async function renderSegments(
|
|
|
387
402
|
)
|
|
388
403
|
: Promise.resolve([]);
|
|
389
404
|
const resolvedData = await layoutLoaderDataPromise;
|
|
390
|
-
const { loaderData, errorFallback } =
|
|
405
|
+
const { loaderData, errorFallback } = decodeLoaderResults(
|
|
391
406
|
resolvedData,
|
|
392
407
|
layoutLoaderIds,
|
|
393
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
|
+
}
|