@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1fa245e2
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 +76 -18
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +558 -319
- package/package.json +16 -15
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +45 -4
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +53 -43
- package/skills/middleware/SKILL.md +2 -0
- package/skills/parallel/SKILL.md +126 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/route/SKILL.md +31 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/__internal.ts +1 -1
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +19 -13
- package/src/browser/navigation-client.ts +115 -58
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +80 -15
- package/src/browser/prefetch/cache.ts +57 -5
- package/src/browser/prefetch/fetch.ts +38 -23
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +53 -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 +6 -1
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +36 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- 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.tsx +2 -56
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +8 -0
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +22 -1
- package/src/route-definition/dsl-helpers.ts +73 -25
- package/src/route-definition/helpers-types.ts +10 -6
- 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 +79 -23
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +122 -10
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-api.ts +124 -189
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +88 -16
- 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 +22 -6
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +6 -8
- package/src/router/middleware.ts +4 -6
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +110 -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 +183 -20
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +412 -297
- 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 +460 -368
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/rsc-rendering.ts +5 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +8 -1
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +140 -14
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +144 -18
- 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 +137 -33
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-entry.ts +8 -1
- package/src/types/segments.ts +2 -0
- package/src/urls/path-helper-types.ts +9 -2
- 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 +73 -4
- 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 +14 -1
- 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/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 +153 -42
- package/src/vite/utils/banner.ts +3 -3
- package/src/vite/utils/prerender-utils.ts +18 -0
- package/src/vite/utils/shared-utils.ts +3 -2
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,
|
|
@@ -342,6 +344,8 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
342
344
|
const payload: RscPayload = {
|
|
343
345
|
metadata: {
|
|
344
346
|
pathname: url.pathname,
|
|
347
|
+
routerId: ctx.router.id,
|
|
348
|
+
basename: ctx.router.basename,
|
|
345
349
|
segments: errorResult.segments,
|
|
346
350
|
matched: errorResult.matched,
|
|
347
351
|
diff: errorResult.diff,
|
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,
|
|
@@ -136,6 +139,8 @@ export async function handleRscRendering<TEnv>(
|
|
|
136
139
|
|
|
137
140
|
metadata: {
|
|
138
141
|
pathname: url.pathname,
|
|
142
|
+
routerId: ctx.router.id,
|
|
143
|
+
basename: ctx.router.basename,
|
|
139
144
|
segments: match.segments,
|
|
140
145
|
matched: match.matched,
|
|
141
146
|
diff: match.diff,
|
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,
|
|
@@ -314,6 +315,7 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
314
315
|
const payload: RscPayload = {
|
|
315
316
|
metadata: {
|
|
316
317
|
pathname: url.pathname,
|
|
318
|
+
routerId: ctx.router.id,
|
|
317
319
|
segments: matchResult.segments,
|
|
318
320
|
isPartial: true,
|
|
319
321
|
matched: matchResult.matched,
|
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,9 @@ export interface RSCDependencies {
|
|
|
63
68
|
*/
|
|
64
69
|
renderToReadableStream: <T>(
|
|
65
70
|
payload: T,
|
|
66
|
-
options?: {
|
|
71
|
+
options?: {
|
|
72
|
+
temporaryReferences?: unknown;
|
|
73
|
+
},
|
|
67
74
|
) => ReadableStream<Uint8Array>;
|
|
68
75
|
|
|
69
76
|
/**
|
package/src/segment-system.tsx
CHANGED
|
@@ -20,6 +20,61 @@ import { RootErrorBoundary } from "./root-error-boundary.js";
|
|
|
20
20
|
const ReactViewTransition: any =
|
|
21
21
|
"ViewTransition" in React ? (React as any).ViewTransition : null;
|
|
22
22
|
|
|
23
|
+
function restoreParallelLoaderMarkers(
|
|
24
|
+
segments: ResolvedSegment[],
|
|
25
|
+
): ResolvedSegment[] {
|
|
26
|
+
const parallelLoadingByNamespace = new Map<string, ReactNode>();
|
|
27
|
+
let nextSegments: ResolvedSegment[] | null = null;
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < segments.length; i++) {
|
|
30
|
+
const segment = segments[i];
|
|
31
|
+
|
|
32
|
+
if (segment.type === "parallel") {
|
|
33
|
+
if (
|
|
34
|
+
segment.namespace &&
|
|
35
|
+
segment.loading !== undefined &&
|
|
36
|
+
segment.loading !== null &&
|
|
37
|
+
segment.loading !== false
|
|
38
|
+
) {
|
|
39
|
+
parallelLoadingByNamespace.set(segment.namespace, segment.loading);
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (segment.type !== "loader" || segment.parallelLoading !== undefined) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parallelLoading = segment.namespace
|
|
49
|
+
? parallelLoadingByNamespace.get(segment.namespace)
|
|
50
|
+
: undefined;
|
|
51
|
+
if (parallelLoading === undefined) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!nextSegments) {
|
|
56
|
+
nextSegments = segments.slice();
|
|
57
|
+
}
|
|
58
|
+
nextSegments[i] = { ...segment, parallelLoading };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return nextSegments ?? segments;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasSameReferences(a: unknown[] | undefined, b: unknown[]): boolean {
|
|
65
|
+
if (!a || a.length !== b.length) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < a.length; i++) {
|
|
70
|
+
if (a[i] !== b[i]) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
23
78
|
/**
|
|
24
79
|
* Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
|
|
25
80
|
*/
|
|
@@ -143,6 +198,10 @@ export async function renderSegments(
|
|
|
143
198
|
} = options || {};
|
|
144
199
|
|
|
145
200
|
const temporalLazyRefs: Promise<any>[] = [];
|
|
201
|
+
const normalizedSegments = restoreParallelLoaderMarkers(segments);
|
|
202
|
+
const normalizedInterceptSegments = interceptSegments
|
|
203
|
+
? restoreParallelLoaderMarkers(interceptSegments)
|
|
204
|
+
: undefined;
|
|
146
205
|
|
|
147
206
|
/**
|
|
148
207
|
* Registers promises from lazy/async components for awaiting.
|
|
@@ -167,7 +226,7 @@ export async function renderSegments(
|
|
|
167
226
|
);
|
|
168
227
|
}
|
|
169
228
|
// Separate segments by type, passing intercept segments for explicit injection
|
|
170
|
-
const tree = segmentTreeWalk(
|
|
229
|
+
const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
|
|
171
230
|
// Render content segments as siblings
|
|
172
231
|
let content: ReactNode = null;
|
|
173
232
|
for (const node of tree) {
|
|
@@ -284,13 +343,90 @@ export async function renderSegments(
|
|
|
284
343
|
children: nodeContent,
|
|
285
344
|
});
|
|
286
345
|
} else {
|
|
287
|
-
// Has loaders but no loading skeleton
|
|
288
|
-
|
|
346
|
+
// Has loaders but no loading skeleton.
|
|
347
|
+
// Split: parallel-owned loaders stream (their parallel has loading()),
|
|
348
|
+
// layout-owned loaders are awaited (they gate the layout content).
|
|
349
|
+
const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
|
|
350
|
+
const parallelOwnedLoaders = loaderEntries.filter(
|
|
351
|
+
(l) => !!l.parallelLoading,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Await only layout-owned loaders
|
|
355
|
+
const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
|
|
356
|
+
const layoutLoaderDataPromise =
|
|
357
|
+
layoutLoaders.length > 0
|
|
358
|
+
? Promise.all(
|
|
359
|
+
layoutLoaders.map((l) =>
|
|
360
|
+
l.loaderData instanceof Promise
|
|
361
|
+
? l.loaderData
|
|
362
|
+
: Promise.resolve(l.loaderData),
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
: Promise.resolve([]);
|
|
366
|
+
const resolvedData = await layoutLoaderDataPromise;
|
|
289
367
|
const { loaderData, errorFallback } = resolveLoaderData(
|
|
290
368
|
resolvedData,
|
|
291
|
-
|
|
369
|
+
layoutLoaderIds,
|
|
292
370
|
);
|
|
293
371
|
|
|
372
|
+
// Parallel-owned loaders: attach to their owning parallel segment
|
|
373
|
+
// as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
|
|
374
|
+
if (parallelOwnedLoaders.length > 0) {
|
|
375
|
+
const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
|
|
376
|
+
|
|
377
|
+
for (const loader of parallelOwnedLoaders) {
|
|
378
|
+
if (!loader.namespace) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const existing = loadersByParallelNamespace.get(loader.namespace);
|
|
382
|
+
if (existing) {
|
|
383
|
+
existing.push(loader);
|
|
384
|
+
} else {
|
|
385
|
+
loadersByParallelNamespace.set(loader.namespace, [loader]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const p of node.parallel) {
|
|
390
|
+
if (!p.loading || !p.namespace) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const ownedLoaders = loadersByParallelNamespace.get(p.namespace);
|
|
395
|
+
if (!ownedLoaders || ownedLoaders.length === 0) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const parallelLoaderIds = ownedLoaders.map((l) => l.loaderId!);
|
|
400
|
+
const parallelLoaderSources = ownedLoaders.map((l) => l.loaderData);
|
|
401
|
+
p.loaderIds = parallelLoaderIds;
|
|
402
|
+
|
|
403
|
+
const shouldReuseParallelPromise =
|
|
404
|
+
p.loaderDataPromise !== undefined &&
|
|
405
|
+
hasSameReferences(p.parallelLoaderSources, parallelLoaderSources);
|
|
406
|
+
|
|
407
|
+
const parallelLoaderDataPromise = shouldReuseParallelPromise
|
|
408
|
+
? p.loaderDataPromise
|
|
409
|
+
: forceAwait || isAction
|
|
410
|
+
? await Promise.all(
|
|
411
|
+
ownedLoaders.map((l) =>
|
|
412
|
+
l.loaderData instanceof Promise
|
|
413
|
+
? l.loaderData
|
|
414
|
+
: Promise.resolve(l.loaderData),
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
: Promise.all(
|
|
418
|
+
ownedLoaders.map((l) =>
|
|
419
|
+
l.loaderData instanceof Promise
|
|
420
|
+
? l.loaderData
|
|
421
|
+
: Promise.resolve(l.loaderData),
|
|
422
|
+
),
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
p.loaderDataPromise = parallelLoaderDataPromise;
|
|
426
|
+
p.parallelLoaderSources = parallelLoaderSources;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
294
430
|
content = createElement(OutletProvider, {
|
|
295
431
|
key,
|
|
296
432
|
content: outletContent,
|
package/src/server/context.ts
CHANGED
|
@@ -157,10 +157,24 @@ export type InterceptEntry = {
|
|
|
157
157
|
when: InterceptWhenFn[]; // Selector conditions - all must return true to intercept
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
+
export interface ParallelEntryData
|
|
161
|
+
extends EntryPropCommon, EntryPropDatas, EntryPropSegments {
|
|
162
|
+
type: "parallel";
|
|
163
|
+
handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
|
|
164
|
+
loading?: ReactNode | false;
|
|
165
|
+
transition?: TransitionConfig;
|
|
166
|
+
/** Set when any parallel slot is a Static definition */
|
|
167
|
+
isStaticPrerender?: true;
|
|
168
|
+
/** Per-slot static handler $$ids for build-time store lookup */
|
|
169
|
+
staticHandlerIds?: Record<string, string>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
|
|
173
|
+
|
|
160
174
|
export type EntryPropSegments = {
|
|
161
175
|
loader: LoaderEntry[];
|
|
162
176
|
layout: EntryData[];
|
|
163
|
-
parallel:
|
|
177
|
+
parallel: ParallelEntries; // slot -> parallel entry (same entry may back multiple slots)
|
|
164
178
|
intercept: InterceptEntry[]; // intercept definitions for soft navigation
|
|
165
179
|
};
|
|
166
180
|
|
|
@@ -177,8 +191,12 @@ export type EntryData =
|
|
|
177
191
|
/** Original PrerenderHandlerDefinition (for build-time getParams access) */
|
|
178
192
|
prerenderDef?: {
|
|
179
193
|
getParams?: (ctx: any) => Promise<any[]> | any[];
|
|
180
|
-
options?: {
|
|
194
|
+
options?: { concurrency?: number };
|
|
181
195
|
};
|
|
196
|
+
/** Set when route is wrapped with Passthrough() — has a separate live handler */
|
|
197
|
+
isPassthrough?: true;
|
|
198
|
+
/** Live handler for runtime fallback (only set on Passthrough routes) */
|
|
199
|
+
liveHandler?: Handler<any, any, any>;
|
|
182
200
|
/** Set when handler is a Static definition (build-time only) */
|
|
183
201
|
isStaticPrerender?: true;
|
|
184
202
|
/** Static handler $$id for build-time store lookup */
|
|
@@ -200,18 +218,7 @@ export type EntryData =
|
|
|
200
218
|
} & EntryPropCommon &
|
|
201
219
|
EntryPropDatas &
|
|
202
220
|
EntryPropSegments)
|
|
203
|
-
|
|
|
204
|
-
type: "parallel";
|
|
205
|
-
handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
|
|
206
|
-
loading?: ReactNode | false;
|
|
207
|
-
transition?: TransitionConfig;
|
|
208
|
-
/** Set when any parallel slot is a Static definition */
|
|
209
|
-
isStaticPrerender?: true;
|
|
210
|
-
/** Per-slot static handler $$ids for build-time store lookup */
|
|
211
|
-
staticHandlerIds?: Record<string, string>;
|
|
212
|
-
} & EntryPropCommon &
|
|
213
|
-
EntryPropDatas &
|
|
214
|
-
EntryPropSegments)
|
|
221
|
+
| ParallelEntryData
|
|
215
222
|
| ({
|
|
216
223
|
type: "cache";
|
|
217
224
|
/** Cache entries create cache boundaries and render like layouts (with Outlet) */
|
|
@@ -270,6 +277,9 @@ interface HelperContext {
|
|
|
270
277
|
string,
|
|
271
278
|
import("../cache/profile-registry.js").CacheProfile
|
|
272
279
|
>;
|
|
280
|
+
/** True when resolving handlers inside a cache() DSL boundary.
|
|
281
|
+
* Read by ctx.get() to guard non-cacheable variable reads. */
|
|
282
|
+
insideCacheScope?: boolean;
|
|
273
283
|
}
|
|
274
284
|
// Use a global symbol key so the AsyncLocalStorage instance survives HMR
|
|
275
285
|
// module re-evaluation. Without this, Vite's RSC module runner may create
|
|
@@ -553,6 +563,80 @@ export function getRootScoped(): boolean {
|
|
|
553
563
|
// Export HelperContext type for use in other modules
|
|
554
564
|
export type { HelperContext };
|
|
555
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Return an isolated copy of a lazy include's captured parent entry.
|
|
568
|
+
*
|
|
569
|
+
* DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
|
|
570
|
+
* Multiple include() scopes capture the *same* syntheticMapRoot as their
|
|
571
|
+
* parent, so without isolation one include's loaders/middleware leak into
|
|
572
|
+
* every other route that shares that root.
|
|
573
|
+
*
|
|
574
|
+
* The clone is shallow: only the mutable arrays are copied so each
|
|
575
|
+
* include pushes to its own list. The rest of the entry (id, shortCode,
|
|
576
|
+
* parent pointer, handler) stays shared, which is correct and cheap.
|
|
577
|
+
*/
|
|
578
|
+
export function getIsolatedLazyParent(
|
|
579
|
+
captured: EntryData | null | undefined,
|
|
580
|
+
): EntryData | null {
|
|
581
|
+
if (!captured) return null;
|
|
582
|
+
return {
|
|
583
|
+
...captured,
|
|
584
|
+
loader: [...captured.loader],
|
|
585
|
+
middleware: [...captured.middleware],
|
|
586
|
+
revalidate: [...captured.revalidate],
|
|
587
|
+
errorBoundary: [...captured.errorBoundary],
|
|
588
|
+
notFoundBoundary: [...captured.notFoundBoundary],
|
|
589
|
+
layout: [...captured.layout],
|
|
590
|
+
parallel: { ...captured.parallel },
|
|
591
|
+
intercept: [...captured.intercept],
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export function getParallelEntries(
|
|
596
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
597
|
+
): ParallelEntryData[] {
|
|
598
|
+
if (!parallels) return [];
|
|
599
|
+
if (Array.isArray(parallels)) {
|
|
600
|
+
return parallels.filter(
|
|
601
|
+
(entry): entry is ParallelEntryData => entry.type === "parallel",
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
return Object.values(parallels).filter(
|
|
605
|
+
(entry): entry is ParallelEntryData => !!entry,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function getParallelSlotEntries(
|
|
610
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
611
|
+
): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
|
|
612
|
+
if (!parallels) return [];
|
|
613
|
+
|
|
614
|
+
if (Array.isArray(parallels)) {
|
|
615
|
+
return getParallelEntries(parallels).flatMap((entry) =>
|
|
616
|
+
(Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
|
|
617
|
+
slot,
|
|
618
|
+
entry,
|
|
619
|
+
})),
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return Object.entries(parallels)
|
|
624
|
+
.filter(([, entry]) => !!entry)
|
|
625
|
+
.map(([slot, entry]) => ({
|
|
626
|
+
slot: slot as `@${string}`,
|
|
627
|
+
entry: entry!,
|
|
628
|
+
}));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export function getParallelSlotCount(
|
|
632
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
633
|
+
): number {
|
|
634
|
+
if (!parallels) return 0;
|
|
635
|
+
return Array.isArray(parallels)
|
|
636
|
+
? parallels.filter((entry) => entry?.type === "parallel").length
|
|
637
|
+
: Object.keys(parallels).length;
|
|
638
|
+
}
|
|
639
|
+
|
|
556
640
|
// ============================================================================
|
|
557
641
|
// Performance Metrics Helpers
|
|
558
642
|
// ============================================================================
|
|
@@ -589,3 +673,45 @@ export function track(label: string, depth?: number): () => void {
|
|
|
589
673
|
});
|
|
590
674
|
};
|
|
591
675
|
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Separate ALS for tracking loader execution scope.
|
|
679
|
+
* Uses a dedicated ALS (not RSCRouterContext) to avoid issues with
|
|
680
|
+
* nested RSCRouterContext.run() calls in Vite's module runner.
|
|
681
|
+
*/
|
|
682
|
+
const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
|
|
683
|
+
const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
684
|
+
globalThis as any
|
|
685
|
+
)[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Check if the current execution is inside a cache() DSL boundary.
|
|
689
|
+
* Returns false inside loader execution — loaders are always fresh
|
|
690
|
+
* (never cached), so non-cacheable reads are safe.
|
|
691
|
+
*/
|
|
692
|
+
export function isInsideCacheScope(): boolean {
|
|
693
|
+
if (RSCRouterContext.getStore()?.insideCacheScope !== true) return false;
|
|
694
|
+
// Loaders are always fresh — even inside a cache() boundary, the loader
|
|
695
|
+
// function re-executes on every request. Skip the guard when running
|
|
696
|
+
// inside a loader.
|
|
697
|
+
if (loaderScopeALS.getStore()?.active) return false;
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Check if the current execution is inside a DSL loader scope
|
|
703
|
+
* (wrapped by runInsideLoaderScope). Used by rendered() barrier
|
|
704
|
+
* to distinguish DSL loaders from handler-invoked loaders.
|
|
705
|
+
*/
|
|
706
|
+
export function isInsideLoaderScope(): boolean {
|
|
707
|
+
return loaderScopeALS.getStore()?.active === true;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Run `fn` inside a loader scope. While active, cache-scope guards
|
|
712
|
+
* are bypassed because loaders are always fresh (never cached) and
|
|
713
|
+
* their side effects (setCookie, header, etc.) are safe.
|
|
714
|
+
*/
|
|
715
|
+
export function runInsideLoaderScope<T>(fn: () => T): T {
|
|
716
|
+
return loaderScopeALS.run({ active: true }, fn);
|
|
717
|
+
}
|
|
@@ -44,20 +44,21 @@ export function setLoaderImports(
|
|
|
44
44
|
export async function getLoaderLazy(
|
|
45
45
|
id: string,
|
|
46
46
|
): Promise<LoaderRegistryEntry | undefined> {
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return existing;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check the fetchable loader registry (populated by createLoader)
|
|
47
|
+
// Always check fetchableLoaderRegistry first — it's the source of truth.
|
|
48
|
+
// createLoader() updates it during module re-evaluation (HMR), so checking
|
|
49
|
+
// here ensures we pick up the fresh function after a loader file change.
|
|
54
50
|
const fetchable = getFetchableLoader(id);
|
|
55
51
|
if (fetchable) {
|
|
56
|
-
// Cache in main registry for future requests
|
|
57
52
|
loaderRegistry.set(id, fetchable);
|
|
58
53
|
return fetchable;
|
|
59
54
|
}
|
|
60
55
|
|
|
56
|
+
// Fall back to local cache (populated by previous lazy imports in production)
|
|
57
|
+
const existing = loaderRegistry.get(id);
|
|
58
|
+
if (existing) {
|
|
59
|
+
return existing;
|
|
60
|
+
}
|
|
61
|
+
|
|
61
62
|
// Try to lazy load from the import map (production mode)
|
|
62
63
|
if (lazyLoaderImports && lazyLoaderImports.size > 0) {
|
|
63
64
|
const lazyImport = lazyLoaderImports.get(id);
|