@rangojs/router 0.0.0-experimental.29 → 0.0.0-experimental.2a0dea97
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/AGENTS.md +4 -0
- package/README.md +78 -19
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +853 -435
- package/package.json +17 -16
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +45 -4
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +22 -4
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +71 -21
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/rango/SKILL.md +24 -22
- package/skills/route/SKILL.md +56 -2
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/typesafety/SKILL.md +33 -21
- package/src/__internal.ts +92 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +125 -16
- package/src/browser/navigation-client.ts +142 -57
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +94 -17
- package/src/browser/prefetch/cache.ts +82 -12
- package/src/browser/prefetch/fetch.ts +98 -27
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +88 -9
- package/src/browser/react/NavigationProvider.tsx +40 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +134 -59
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +72 -10
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +55 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +50 -24
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +223 -74
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +2 -0
- package/src/client.tsx +6 -66
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/index.rsc.ts +6 -36
- package/src/index.ts +50 -43
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +25 -1
- package/src/route-definition/dsl-helpers.ts +224 -37
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +111 -25
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +156 -21
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-api.ts +125 -190
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +94 -17
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +104 -10
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +16 -22
- package/src/router/middleware.ts +24 -30
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +198 -20
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +438 -300
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/types.ts +1 -0
- package/src/router.ts +59 -6
- package/src/rsc/handler.ts +472 -372
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +14 -2
- package/src/rsc/rsc-rendering.ts +12 -1
- package/src/rsc/server-action.ts +8 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +9 -1
- package/src/segment-content-promise.ts +33 -0
- package/src/segment-system.tsx +164 -23
- package/src/server/context.ts +140 -14
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +204 -28
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +149 -49
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-entry.ts +8 -1
- package/src/types/segments.ts +6 -0
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/discovery/prerender-collection.ts +128 -74
- package/src/vite/discovery/state.ts +13 -6
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +163 -211
- package/src/vite/router-discovery.ts +178 -45
- package/src/vite/utils/banner.ts +3 -3
- package/src/vite/utils/prerender-utils.ts +37 -5
- package/src/vite/utils/shared-utils.ts +3 -2
package/src/rsc/loader-fetch.ts
CHANGED
|
@@ -168,8 +168,19 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
168
168
|
loaderResult: unknown;
|
|
169
169
|
}
|
|
170
170
|
const loaderPayload: LoaderPayload = { loaderResult: result };
|
|
171
|
-
const rscStream =
|
|
172
|
-
|
|
171
|
+
const rscStream = ctx.renderToReadableStream<LoaderPayload>(
|
|
172
|
+
loaderPayload,
|
|
173
|
+
{
|
|
174
|
+
onError: (error: unknown) => {
|
|
175
|
+
ctx.callOnError(error, "rendering", {
|
|
176
|
+
request,
|
|
177
|
+
url,
|
|
178
|
+
env,
|
|
179
|
+
loaderName: loaderId,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
);
|
|
173
184
|
|
|
174
185
|
return createResponseWithMergedHeaders(rscStream, {
|
|
175
186
|
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
@@ -199,7 +210,16 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
199
210
|
name: err.name,
|
|
200
211
|
},
|
|
201
212
|
};
|
|
202
|
-
const rscStream = ctx.renderToReadableStream(errorPayload
|
|
213
|
+
const rscStream = ctx.renderToReadableStream(errorPayload, {
|
|
214
|
+
onError: (error: unknown) => {
|
|
215
|
+
ctx.callOnError(error, "rendering", {
|
|
216
|
+
request,
|
|
217
|
+
url,
|
|
218
|
+
env,
|
|
219
|
+
loaderName: loaderId,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
});
|
|
203
223
|
|
|
204
224
|
return createResponseWithMergedHeaders(rscStream, {
|
|
205
225
|
status: 500,
|
package/src/rsc/manifest-init.ts
CHANGED
|
@@ -31,7 +31,11 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
31
31
|
): Promise<void> {
|
|
32
32
|
const { generateManifestFull } =
|
|
33
33
|
await import("../build/generate-manifest.js");
|
|
34
|
-
const generated = generateManifestFull(
|
|
34
|
+
const generated = generateManifestFull(
|
|
35
|
+
router.urlpatterns,
|
|
36
|
+
undefined,
|
|
37
|
+
router.basename ? { urlPrefix: router.basename } : undefined,
|
|
38
|
+
);
|
|
35
39
|
if (
|
|
36
40
|
generated._routeAncestry &&
|
|
37
41
|
Object.keys(generated._routeAncestry).length > 0
|
|
@@ -243,6 +243,8 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
243
243
|
const payload: RscPayload = {
|
|
244
244
|
metadata: {
|
|
245
245
|
pathname: url.pathname,
|
|
246
|
+
routerId: ctx.router.id,
|
|
247
|
+
basename: ctx.router.basename,
|
|
246
248
|
segments: match.segments,
|
|
247
249
|
matched: match.matched,
|
|
248
250
|
diff: match.diff,
|
|
@@ -257,7 +259,11 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
257
259
|
formState: actionResult,
|
|
258
260
|
};
|
|
259
261
|
|
|
260
|
-
const rscStream = ctx.renderToReadableStream<RscPayload>(payload
|
|
262
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
263
|
+
onError: (error: unknown) => {
|
|
264
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
265
|
+
},
|
|
266
|
+
});
|
|
261
267
|
// metricsStore=undefined is safe: the handler already stashed the early
|
|
262
268
|
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
263
269
|
// without falling back to a fresh startSSRSetup.
|
|
@@ -342,6 +348,8 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
342
348
|
const payload: RscPayload = {
|
|
343
349
|
metadata: {
|
|
344
350
|
pathname: url.pathname,
|
|
351
|
+
routerId: ctx.router.id,
|
|
352
|
+
basename: ctx.router.basename,
|
|
345
353
|
segments: errorResult.segments,
|
|
346
354
|
matched: errorResult.matched,
|
|
347
355
|
diff: errorResult.diff,
|
|
@@ -356,7 +364,11 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
356
364
|
},
|
|
357
365
|
};
|
|
358
366
|
|
|
359
|
-
const rscStream = ctx.renderToReadableStream<RscPayload>(payload
|
|
367
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
368
|
+
onError: (error: unknown) => {
|
|
369
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
370
|
+
},
|
|
371
|
+
});
|
|
360
372
|
// metricsStore=undefined is safe: the handler already stashed the early
|
|
361
373
|
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
362
374
|
// without falling back to a fresh startSSRSetup.
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -54,6 +54,8 @@ export async function handleRscRendering<TEnv>(
|
|
|
54
54
|
payload = {
|
|
55
55
|
metadata: {
|
|
56
56
|
pathname: url.pathname,
|
|
57
|
+
routerId: ctx.router.id,
|
|
58
|
+
basename: ctx.router.basename,
|
|
57
59
|
segments: match.segments,
|
|
58
60
|
matched: match.matched,
|
|
59
61
|
diff: match.diff,
|
|
@@ -75,6 +77,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
75
77
|
payload = {
|
|
76
78
|
metadata: {
|
|
77
79
|
pathname: url.pathname,
|
|
80
|
+
routerId: ctx.router.id,
|
|
78
81
|
segments: result.segments,
|
|
79
82
|
matched: result.matched,
|
|
80
83
|
diff: result.diff,
|
|
@@ -83,6 +86,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
83
86
|
slots: result.slots,
|
|
84
87
|
handles: handleStore.stream(),
|
|
85
88
|
version: ctx.version,
|
|
89
|
+
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
86
90
|
},
|
|
87
91
|
};
|
|
88
92
|
}
|
|
@@ -135,6 +139,8 @@ export async function handleRscRendering<TEnv>(
|
|
|
135
139
|
|
|
136
140
|
metadata: {
|
|
137
141
|
pathname: url.pathname,
|
|
142
|
+
routerId: ctx.router.id,
|
|
143
|
+
basename: ctx.router.basename,
|
|
138
144
|
segments: match.segments,
|
|
139
145
|
matched: match.matched,
|
|
140
146
|
diff: match.diff,
|
|
@@ -143,6 +149,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
143
149
|
rootLayout: ctx.router.rootLayout,
|
|
144
150
|
handles: handleStore.stream(),
|
|
145
151
|
version: ctx.version,
|
|
152
|
+
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
146
153
|
themeConfig: ctx.router.themeConfig,
|
|
147
154
|
initialTheme: reqCtx.theme,
|
|
148
155
|
},
|
|
@@ -166,7 +173,11 @@ export async function handleRscRendering<TEnv>(
|
|
|
166
173
|
|
|
167
174
|
// Serialize to RSC stream
|
|
168
175
|
const rscSerializeStart = performance.now();
|
|
169
|
-
const rscStream = ctx.renderToReadableStream<RscPayload>(payload
|
|
176
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
177
|
+
onError: (error: unknown) => {
|
|
178
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
179
|
+
},
|
|
180
|
+
});
|
|
170
181
|
const rscSerializeDur = performance.now() - rscSerializeStart;
|
|
171
182
|
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
172
183
|
appendMetric(
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -208,6 +208,7 @@ export async function executeServerAction<TEnv>(
|
|
|
208
208
|
const payload: RscPayload = {
|
|
209
209
|
metadata: {
|
|
210
210
|
pathname: url.pathname,
|
|
211
|
+
routerId: ctx.router.id,
|
|
211
212
|
segments: errorResult.segments,
|
|
212
213
|
isPartial: true,
|
|
213
214
|
matched: errorResult.matched,
|
|
@@ -225,6 +226,9 @@ export async function executeServerAction<TEnv>(
|
|
|
225
226
|
|
|
226
227
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
227
228
|
temporaryReferences,
|
|
229
|
+
onError: (error: unknown) => {
|
|
230
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
231
|
+
},
|
|
228
232
|
});
|
|
229
233
|
|
|
230
234
|
return createResponseWithMergedHeaders(rscStream, {
|
|
@@ -314,6 +318,7 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
314
318
|
const payload: RscPayload = {
|
|
315
319
|
metadata: {
|
|
316
320
|
pathname: url.pathname,
|
|
321
|
+
routerId: ctx.router.id,
|
|
317
322
|
segments: matchResult.segments,
|
|
318
323
|
isPartial: true,
|
|
319
324
|
matched: matchResult.matched,
|
|
@@ -330,6 +335,9 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
330
335
|
const renderStart = performance.now();
|
|
331
336
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
332
337
|
temporaryReferences,
|
|
338
|
+
onError: (error: unknown) => {
|
|
339
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
340
|
+
},
|
|
333
341
|
});
|
|
334
342
|
const rscSerializeDur = performance.now() - renderStart;
|
|
335
343
|
// This measures synchronous stream creation, not end-to-end stream consumption.
|
package/src/rsc/ssr-setup.ts
CHANGED
|
@@ -77,7 +77,7 @@ export function getSSRSetup<TEnv>(
|
|
|
77
77
|
url: URL,
|
|
78
78
|
metricsStore: MetricsStore | undefined,
|
|
79
79
|
): Promise<SSRSetup> {
|
|
80
|
-
const early = _getRequestContext()?.
|
|
80
|
+
const early = _getRequestContext()?._variables?.[SSR_SETUP_VAR] as
|
|
81
81
|
| Promise<SSRSetup>
|
|
82
82
|
| undefined;
|
|
83
83
|
if (early) return early;
|
|
@@ -98,7 +98,7 @@ export function getSSRSetup<TEnv>(
|
|
|
98
98
|
* the isRscRequest decision in rsc-rendering.ts.
|
|
99
99
|
*
|
|
100
100
|
* Note: response/mime routes are excluded by the caller — this function
|
|
101
|
-
* runs after
|
|
101
|
+
* runs after classifyRequest() determines the request mode.
|
|
102
102
|
*/
|
|
103
103
|
export function mayNeedSSR(request: Request, url: URL): boolean {
|
|
104
104
|
if (
|
package/src/rsc/types.ts
CHANGED
|
@@ -19,6 +19,9 @@ export interface RscPayload {
|
|
|
19
19
|
metadata?: {
|
|
20
20
|
pathname: string;
|
|
21
21
|
segments: ResolvedSegment[];
|
|
22
|
+
/** Router instance ID. When this changes between navigations, the client
|
|
23
|
+
* discards cached segments and does a full tree replacement (app switch). */
|
|
24
|
+
routerId?: string;
|
|
22
25
|
isPartial?: boolean;
|
|
23
26
|
isError?: boolean;
|
|
24
27
|
matched?: string[];
|
|
@@ -38,6 +41,8 @@ export interface RscPayload {
|
|
|
38
41
|
themeConfig?: ResolvedThemeConfig | null;
|
|
39
42
|
/** Initial theme from cookie (for SSR hydration) */
|
|
40
43
|
initialTheme?: Theme;
|
|
44
|
+
/** URL prefix for all routes (from createRouter({ basename })). */
|
|
45
|
+
basename?: string;
|
|
41
46
|
/** Whether connection warmup is enabled */
|
|
42
47
|
warmupEnabled?: boolean;
|
|
43
48
|
/** Server-side redirect with optional state (for partial requests) */
|
|
@@ -63,7 +68,10 @@ export interface RSCDependencies {
|
|
|
63
68
|
*/
|
|
64
69
|
renderToReadableStream: <T>(
|
|
65
70
|
payload: T,
|
|
66
|
-
options?: {
|
|
71
|
+
options?: {
|
|
72
|
+
temporaryReferences?: unknown;
|
|
73
|
+
onError?: (error: unknown) => string | void;
|
|
74
|
+
},
|
|
67
75
|
) => ReadableStream<Uint8Array>;
|
|
68
76
|
|
|
69
77
|
/**
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ResolvedSegment } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Return a stable Promise wrapping `component`, memoized on `segment`.
|
|
6
|
+
*
|
|
7
|
+
* A fresh `Promise.resolve(component)` each render would suspend for one
|
|
8
|
+
* microtask and briefly commit the loading fallback inside Suspender — the
|
|
9
|
+
* intercept / parallel-slot flicker this indirection prevents. Reusing the
|
|
10
|
+
* same Promise ref keeps React's `use()` in "known fulfilled" state after
|
|
11
|
+
* the first observation. `component` is separate from `segment.component`
|
|
12
|
+
* so action renders can feed in the awaited value.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export function getMemoizedContentPromise(
|
|
17
|
+
segment: ResolvedSegment,
|
|
18
|
+
component: ReactNode,
|
|
19
|
+
): Promise<ReactNode> {
|
|
20
|
+
if (component instanceof Promise) {
|
|
21
|
+
return component as Promise<ReactNode>;
|
|
22
|
+
}
|
|
23
|
+
if (
|
|
24
|
+
segment.contentPromise !== undefined &&
|
|
25
|
+
segment.contentSource === component
|
|
26
|
+
) {
|
|
27
|
+
return segment.contentPromise;
|
|
28
|
+
}
|
|
29
|
+
const promise = Promise.resolve(component);
|
|
30
|
+
segment.contentPromise = promise;
|
|
31
|
+
segment.contentSource = component;
|
|
32
|
+
return promise;
|
|
33
|
+
}
|
package/src/segment-system.tsx
CHANGED
|
@@ -2,11 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
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
|
-
import type {
|
|
6
|
-
ResolvedSegment,
|
|
7
|
-
LoaderDataResult,
|
|
8
|
-
RootLayoutProps,
|
|
9
|
-
} from "./types.js";
|
|
5
|
+
import type { ResolvedSegment, RootLayoutProps } from "./types.js";
|
|
10
6
|
import { isLoaderDataResult } from "./types.js";
|
|
11
7
|
import { invariant } from "./errors.js";
|
|
12
8
|
import {
|
|
@@ -14,12 +10,103 @@ import {
|
|
|
14
10
|
LoaderBoundary,
|
|
15
11
|
} from "./route-content-wrapper.js";
|
|
16
12
|
import { RootErrorBoundary } from "./root-error-boundary.js";
|
|
13
|
+
import { getMemoizedContentPromise } from "./segment-content-promise.js";
|
|
17
14
|
|
|
18
15
|
// ViewTransition is only available in React experimental.
|
|
19
16
|
// Access via namespace import to avoid compile-time errors on stable React.
|
|
20
17
|
const ReactViewTransition: any =
|
|
21
18
|
"ViewTransition" in React ? (React as any).ViewTransition : null;
|
|
22
19
|
|
|
20
|
+
function restoreParallelLoaderMarkers(
|
|
21
|
+
segments: ResolvedSegment[],
|
|
22
|
+
): ResolvedSegment[] {
|
|
23
|
+
const parallelLoadingByNamespace = new Map<string, ReactNode>();
|
|
24
|
+
let nextSegments: ResolvedSegment[] | null = null;
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < segments.length; i++) {
|
|
27
|
+
const segment = segments[i];
|
|
28
|
+
|
|
29
|
+
if (segment.type === "parallel") {
|
|
30
|
+
if (
|
|
31
|
+
segment.namespace &&
|
|
32
|
+
segment.loading !== undefined &&
|
|
33
|
+
segment.loading !== null &&
|
|
34
|
+
segment.loading !== false
|
|
35
|
+
) {
|
|
36
|
+
parallelLoadingByNamespace.set(segment.namespace, segment.loading);
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (segment.type !== "loader" || segment.parallelLoading !== undefined) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parallelLoading = segment.namespace
|
|
46
|
+
? parallelLoadingByNamespace.get(segment.namespace)
|
|
47
|
+
: undefined;
|
|
48
|
+
if (parallelLoading === undefined) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!nextSegments) {
|
|
53
|
+
nextSegments = segments.slice();
|
|
54
|
+
}
|
|
55
|
+
nextSegments[i] = { ...segment, parallelLoading };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return nextSegments ?? segments;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasSameReferences(a: unknown[] | undefined, b: unknown[]): boolean {
|
|
62
|
+
if (!a || a.length !== b.length) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < a.length; i++) {
|
|
67
|
+
if (a[i] !== b[i]) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Memoize an aggregate Promise.all for a segment's owned loaders on the
|
|
77
|
+
* segment itself. Reusing the same aggregate across renders — invalidated
|
|
78
|
+
* only when an underlying loader.loaderData ref changes — keeps React's
|
|
79
|
+
* use() in "known fulfilled" state and prevents a fresh Promise.all from
|
|
80
|
+
* suspending (and briefly committing the Suspense fallback) on every
|
|
81
|
+
* partial update that doesn't actually change loader data.
|
|
82
|
+
*/
|
|
83
|
+
function memoizeLoaderPromise(
|
|
84
|
+
target: ResolvedSegment,
|
|
85
|
+
loaders: ResolvedSegment[],
|
|
86
|
+
sourcesKey: "layoutLoaderSources" | "parallelLoaderSources",
|
|
87
|
+
): Promise<any[]> | any[] {
|
|
88
|
+
const sources = loaders.map((loader) => loader.loaderData);
|
|
89
|
+
if (
|
|
90
|
+
target.loaderDataPromise !== undefined &&
|
|
91
|
+
hasSameReferences(target[sourcesKey], sources)
|
|
92
|
+
) {
|
|
93
|
+
return target.loaderDataPromise;
|
|
94
|
+
}
|
|
95
|
+
const promise: Promise<any[]> | any[] =
|
|
96
|
+
loaders.length > 0
|
|
97
|
+
? Promise.all(
|
|
98
|
+
loaders.map((loader) =>
|
|
99
|
+
loader.loaderData instanceof Promise
|
|
100
|
+
? loader.loaderData
|
|
101
|
+
: Promise.resolve(loader.loaderData),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
: Promise.resolve([]);
|
|
105
|
+
target.loaderDataPromise = promise;
|
|
106
|
+
target[sourcesKey] = sources;
|
|
107
|
+
return promise;
|
|
108
|
+
}
|
|
109
|
+
|
|
23
110
|
/**
|
|
24
111
|
* Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
|
|
25
112
|
*/
|
|
@@ -143,6 +230,10 @@ export async function renderSegments(
|
|
|
143
230
|
} = options || {};
|
|
144
231
|
|
|
145
232
|
const temporalLazyRefs: Promise<any>[] = [];
|
|
233
|
+
const normalizedSegments = restoreParallelLoaderMarkers(segments);
|
|
234
|
+
const normalizedInterceptSegments = interceptSegments
|
|
235
|
+
? restoreParallelLoaderMarkers(interceptSegments)
|
|
236
|
+
: undefined;
|
|
146
237
|
|
|
147
238
|
/**
|
|
148
239
|
* Registers promises from lazy/async components for awaiting.
|
|
@@ -167,7 +258,7 @@ export async function renderSegments(
|
|
|
167
258
|
);
|
|
168
259
|
}
|
|
169
260
|
// Separate segments by type, passing intercept segments for explicit injection
|
|
170
|
-
const tree = segmentTreeWalk(
|
|
261
|
+
const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
|
|
171
262
|
// Render content segments as siblings
|
|
172
263
|
let content: ReactNode = null;
|
|
173
264
|
for (const node of tree) {
|
|
@@ -219,10 +310,7 @@ export async function renderSegments(
|
|
|
219
310
|
loading !== null && loading !== undefined && loading !== false
|
|
220
311
|
? createElement(RouteContentWrapper, {
|
|
221
312
|
key: `suspense-loading-${id}`,
|
|
222
|
-
content:
|
|
223
|
-
resolvedComponent instanceof Promise
|
|
224
|
-
? resolvedComponent
|
|
225
|
-
: Promise.resolve(resolvedComponent),
|
|
313
|
+
content: getMemoizedContentPromise(node.segment, resolvedComponent),
|
|
226
314
|
fallback: loading,
|
|
227
315
|
segmentId: id,
|
|
228
316
|
})
|
|
@@ -246,16 +334,11 @@ export async function renderSegments(
|
|
|
246
334
|
|
|
247
335
|
// Prepare loader data if there are loaders
|
|
248
336
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
249
|
-
const loaderDataPromise =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
? loader.loaderData
|
|
255
|
-
: Promise.resolve(loader.loaderData),
|
|
256
|
-
),
|
|
257
|
-
)
|
|
258
|
-
: Promise.resolve([]);
|
|
337
|
+
const loaderDataPromise = memoizeLoaderPromise(
|
|
338
|
+
node.segment,
|
|
339
|
+
loaderEntries,
|
|
340
|
+
"layoutLoaderSources",
|
|
341
|
+
);
|
|
259
342
|
|
|
260
343
|
// Use LoaderBoundary when loading is defined to maintain consistent tree structure
|
|
261
344
|
// This ensures cached segments (which may not have loader segments) have the same
|
|
@@ -284,13 +367,71 @@ export async function renderSegments(
|
|
|
284
367
|
children: nodeContent,
|
|
285
368
|
});
|
|
286
369
|
} else {
|
|
287
|
-
// Has loaders but no loading skeleton
|
|
288
|
-
|
|
370
|
+
// Has loaders but no loading skeleton.
|
|
371
|
+
// Split: parallel-owned loaders stream (their parallel has loading()),
|
|
372
|
+
// layout-owned loaders are awaited (they gate the layout content).
|
|
373
|
+
const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
|
|
374
|
+
const parallelOwnedLoaders = loaderEntries.filter(
|
|
375
|
+
(l) => !!l.parallelLoading,
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
// Await only layout-owned loaders
|
|
379
|
+
const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
|
|
380
|
+
const layoutLoaderDataPromise =
|
|
381
|
+
layoutLoaders.length > 0
|
|
382
|
+
? Promise.all(
|
|
383
|
+
layoutLoaders.map((l) =>
|
|
384
|
+
l.loaderData instanceof Promise
|
|
385
|
+
? l.loaderData
|
|
386
|
+
: Promise.resolve(l.loaderData),
|
|
387
|
+
),
|
|
388
|
+
)
|
|
389
|
+
: Promise.resolve([]);
|
|
390
|
+
const resolvedData = await layoutLoaderDataPromise;
|
|
289
391
|
const { loaderData, errorFallback } = resolveLoaderData(
|
|
290
392
|
resolvedData,
|
|
291
|
-
|
|
393
|
+
layoutLoaderIds,
|
|
292
394
|
);
|
|
293
395
|
|
|
396
|
+
// Parallel-owned loaders: attach to their owning parallel segment
|
|
397
|
+
// as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
|
|
398
|
+
if (parallelOwnedLoaders.length > 0) {
|
|
399
|
+
const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
|
|
400
|
+
|
|
401
|
+
for (const loader of parallelOwnedLoaders) {
|
|
402
|
+
if (!loader.namespace) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const existing = loadersByParallelNamespace.get(loader.namespace);
|
|
406
|
+
if (existing) {
|
|
407
|
+
existing.push(loader);
|
|
408
|
+
} else {
|
|
409
|
+
loadersByParallelNamespace.set(loader.namespace, [loader]);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
for (const p of node.parallel) {
|
|
414
|
+
if (!p.loading || !p.namespace) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const ownedLoaders = loadersByParallelNamespace.get(p.namespace);
|
|
419
|
+
if (!ownedLoaders || ownedLoaders.length === 0) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
p.loaderIds = ownedLoaders.map((l) => l.loaderId!);
|
|
424
|
+
const aggregated = memoizeLoaderPromise(
|
|
425
|
+
p,
|
|
426
|
+
ownedLoaders,
|
|
427
|
+
"parallelLoaderSources",
|
|
428
|
+
);
|
|
429
|
+
if ((forceAwait || isAction) && aggregated instanceof Promise) {
|
|
430
|
+
p.loaderDataPromise = await aggregated;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
294
435
|
content = createElement(OutletProvider, {
|
|
295
436
|
key,
|
|
296
437
|
content: outletContent,
|