@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1
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 +9 -9
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -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 +3 -1
- package/skills/hooks/SKILL.md +214 -18
- package/skills/host-router/SKILL.md +45 -20
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +173 -17
- 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 +5 -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 +13 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +53 -41
- package/skills/testing/SKILL.md +599 -0
- package/skills/typesafety/SKILL.md +310 -26
- 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/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 +6 -6
- 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 +9 -19
- package/src/browser/react/NavigationProvider.tsx +29 -40
- 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-params.ts +3 -4
- package/src/browser/react/use-reverse.ts +106 -0
- 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 +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 +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.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 +6 -4
- package/src/index.ts +13 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- 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 +65 -41
- 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/handler-context.ts +4 -42
- 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/substitute-pattern-params.ts +56 -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 +121 -65
- 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 +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -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 +183 -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
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -13,8 +13,9 @@ import {
|
|
|
13
13
|
} from "../server/request-context.js";
|
|
14
14
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
15
15
|
import { appendMetric } from "../router/metrics.js";
|
|
16
|
-
import { getSSRSetup } from "./ssr-setup.js";
|
|
16
|
+
import { getSSRSetup, isRscRequest } from "./ssr-setup.js";
|
|
17
17
|
import type { RscPayload } from "./types.js";
|
|
18
|
+
import type { MatchResult } from "../types.js";
|
|
18
19
|
import {
|
|
19
20
|
createResponseWithMergedHeaders,
|
|
20
21
|
createSimpleRedirectResponse,
|
|
@@ -35,6 +36,28 @@ export async function handleRscRendering<TEnv>(
|
|
|
35
36
|
let payload: RscPayload;
|
|
36
37
|
let hasInterceptSlots = false;
|
|
37
38
|
|
|
39
|
+
// Shared by the partial-fallback and full-render paths. The partial-success
|
|
40
|
+
// payload below is intentionally different (omits rootLayout/theme, adds slots).
|
|
41
|
+
const buildFullPayload = (m: MatchResult): RscPayload => ({
|
|
42
|
+
metadata: {
|
|
43
|
+
pathname: url.pathname,
|
|
44
|
+
routerId: ctx.router.id,
|
|
45
|
+
basename: ctx.router.basename,
|
|
46
|
+
segments: m.segments,
|
|
47
|
+
matched: m.matched,
|
|
48
|
+
diff: m.diff,
|
|
49
|
+
resolvedIds: m.resolvedIds,
|
|
50
|
+
params: m.params,
|
|
51
|
+
isPartial: false,
|
|
52
|
+
rootLayout: ctx.router.rootLayout,
|
|
53
|
+
handles: handleStore.stream(),
|
|
54
|
+
version: ctx.version,
|
|
55
|
+
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
56
|
+
themeConfig: ctx.router.themeConfig,
|
|
57
|
+
initialTheme: reqCtx.theme,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
38
61
|
if (isPartial) {
|
|
39
62
|
// Partial render (navigation)
|
|
40
63
|
const result = await ctx.router.matchPartial(request, { env });
|
|
@@ -51,25 +74,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
51
74
|
return createSimpleRedirectResponse(match.redirect);
|
|
52
75
|
}
|
|
53
76
|
|
|
54
|
-
payload =
|
|
55
|
-
metadata: {
|
|
56
|
-
pathname: url.pathname,
|
|
57
|
-
routerId: ctx.router.id,
|
|
58
|
-
basename: ctx.router.basename,
|
|
59
|
-
segments: match.segments,
|
|
60
|
-
matched: match.matched,
|
|
61
|
-
diff: match.diff,
|
|
62
|
-
resolvedIds: match.resolvedIds,
|
|
63
|
-
params: match.params,
|
|
64
|
-
isPartial: false,
|
|
65
|
-
rootLayout: ctx.router.rootLayout,
|
|
66
|
-
handles: handleStore.stream(),
|
|
67
|
-
version: ctx.version,
|
|
68
|
-
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
69
|
-
themeConfig: ctx.router.themeConfig,
|
|
70
|
-
initialTheme: reqCtx.theme,
|
|
71
|
-
},
|
|
72
|
-
};
|
|
77
|
+
payload = buildFullPayload(match);
|
|
73
78
|
} else {
|
|
74
79
|
setRequestContextParams(result.params, result.routeName);
|
|
75
80
|
|
|
@@ -135,28 +140,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
135
140
|
{ headers: { "Content-Type": "application/json" } },
|
|
136
141
|
);
|
|
137
142
|
} else {
|
|
138
|
-
payload =
|
|
139
|
-
// Initial SSR can reconstruct the tree from segments + rootLayout,
|
|
140
|
-
// so we omit root to avoid sending the same structure twice.
|
|
141
|
-
|
|
142
|
-
metadata: {
|
|
143
|
-
pathname: url.pathname,
|
|
144
|
-
routerId: ctx.router.id,
|
|
145
|
-
basename: ctx.router.basename,
|
|
146
|
-
segments: match.segments,
|
|
147
|
-
matched: match.matched,
|
|
148
|
-
diff: match.diff,
|
|
149
|
-
resolvedIds: match.resolvedIds,
|
|
150
|
-
params: match.params,
|
|
151
|
-
isPartial: false,
|
|
152
|
-
rootLayout: ctx.router.rootLayout,
|
|
153
|
-
handles: handleStore.stream(),
|
|
154
|
-
version: ctx.version,
|
|
155
|
-
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
156
|
-
themeConfig: ctx.router.themeConfig,
|
|
157
|
-
initialTheme: reqCtx.theme,
|
|
158
|
-
},
|
|
159
|
-
};
|
|
143
|
+
payload = buildFullPayload(match);
|
|
160
144
|
}
|
|
161
145
|
}
|
|
162
146
|
|
|
@@ -190,17 +174,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
190
174
|
rscSerializeDur,
|
|
191
175
|
);
|
|
192
176
|
|
|
193
|
-
|
|
194
|
-
// Partial requests (_rsc_partial) are always RSC -- they come from client-side
|
|
195
|
-
// navigation or prefetch fetch(). We cannot rely on Accept alone since some
|
|
196
|
-
// browsers may send Accept: text/html for non-HTML requests.
|
|
197
|
-
const isRscRequest =
|
|
198
|
-
isPartial ||
|
|
199
|
-
(!request.headers.get("accept")?.includes("text/html") &&
|
|
200
|
-
!url.searchParams.has("__html")) ||
|
|
201
|
-
url.searchParams.has("__rsc");
|
|
202
|
-
|
|
203
|
-
if (isRscRequest) {
|
|
177
|
+
if (isRscRequest(request, url, isPartial)) {
|
|
204
178
|
const renderDur = performance.now() - renderStart;
|
|
205
179
|
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
206
180
|
const rscHeaders: Record<string, string> = {
|
|
@@ -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
|
*/
|
|
@@ -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
|
|
@@ -274,35 +315,50 @@ export async function renderSegments(
|
|
|
274
315
|
// <ViewTransition> components inside (with name/share props) morph independently
|
|
275
316
|
// from the parent's default cross-fade.
|
|
276
317
|
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const outletContent: ReactNode =
|
|
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 =
|
|
295
335
|
node.segment.type === "layout" ? content : null;
|
|
296
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
|
+
|
|
297
351
|
// Prepare loader data if there are loaders
|
|
298
352
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
299
|
-
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
300
353
|
|
|
301
354
|
// Use LoaderBoundary when loading is defined to maintain consistent tree structure
|
|
302
355
|
// This ensures cached segments (which may not have loader segments) have the same
|
|
303
356
|
// tree structure as fresh segments, preventing React remounts
|
|
304
357
|
// If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
|
|
305
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);
|
|
306
362
|
content = createElement(LoaderBoundary, {
|
|
307
363
|
key: `loader-boundary-${key}`,
|
|
308
364
|
loaderDataPromise:
|
|
@@ -346,7 +402,7 @@ export async function renderSegments(
|
|
|
346
402
|
)
|
|
347
403
|
: Promise.resolve([]);
|
|
348
404
|
const resolvedData = await layoutLoaderDataPromise;
|
|
349
|
-
const { loaderData, errorFallback } =
|
|
405
|
+
const { loaderData, errorFallback } = decodeLoaderResults(
|
|
350
406
|
resolvedData,
|
|
351
407
|
layoutLoaderIds,
|
|
352
408
|
);
|