@rangojs/router 0.0.0-experimental.19 → 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/{CLAUDE.md → AGENTS.md} +4 -0
- package/README.md +122 -30
- package/dist/bin/rango.js +245 -63
- package/dist/vite/index.js +859 -418
- package/package.json +3 -3
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +49 -8
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +33 -31
- package/skills/host-router/SKILL.md +218 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +72 -22
- package/skills/middleware/SKILL.md +2 -0
- package/skills/parallel/SKILL.md +126 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +0 -1
- package/skills/route/SKILL.md +34 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/typesafety/SKILL.md +35 -23
- package/src/__internal.ts +92 -0
- package/src/bin/rango.ts +18 -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 +114 -18
- package/src/browser/navigation-client.ts +126 -44
- 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 +166 -27
- package/src/browser/prefetch/fetch.ts +52 -39
- 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 +70 -14
- 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 +143 -59
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/server-action-bridge.ts +454 -436
- package/src/browser/types.ts +60 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +346 -87
- 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 -1
- package/src/client.tsx +3 -102
- 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/host/index.ts +0 -3
- package/src/index.rsc.ts +8 -37
- package/src/index.ts +40 -66
- package/src/prerender/store.ts +57 -15
- 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 -3
- 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 +108 -25
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +123 -11
- 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 +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 -15
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +53 -12
- package/src/router/middleware.ts +172 -85
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +20 -5
- 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/revalidation.ts +27 -7
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +50 -5
- package/src/router/router-options.ts +50 -19
- package/src/router/segment-resolution/fresh.ts +200 -19
- 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 +429 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +1 -0
- package/src/router.ts +88 -15
- package/src/rsc/handler.ts +546 -359
- package/src/rsc/index.ts +0 -20
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +25 -8
- package/src/rsc/rsc-rendering.ts +35 -43
- package/src/rsc/server-action.ts +16 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +10 -1
- package/src/search-params.ts +16 -13
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +148 -16
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +182 -34
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/theme/index.ts +4 -13
- 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-config.ts +17 -8
- package/src/types/route-entry.ts +8 -1
- package/src/types/segments.ts +2 -5
- 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 +61 -89
- package/src/vite/discovery/discover-routers.ts +23 -5
- package/src/vite/discovery/prerender-collection.ts +48 -15
- package/src/vite/discovery/state.ts +17 -13
- package/src/vite/index.ts +8 -3
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- 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 +127 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +174 -211
- package/src/vite/router-discovery.ts +169 -42
- package/src/vite/utils/banner.ts +3 -3
- package/src/vite/utils/prerender-utils.ts +78 -0
- package/src/vite/utils/shared-utils.ts +3 -2
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
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
|
@@ -26,6 +26,7 @@ export interface PerformanceMetric {
|
|
|
26
26
|
label: string; // e.g., "route-matching", "loader:UserLoader"
|
|
27
27
|
duration: number; // milliseconds
|
|
28
28
|
startTime: number; // relative to request start
|
|
29
|
+
depth?: number; // nesting level for hierarchical display (0 = top-level)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -156,10 +157,24 @@ export type InterceptEntry = {
|
|
|
156
157
|
when: InterceptWhenFn[]; // Selector conditions - all must return true to intercept
|
|
157
158
|
};
|
|
158
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
|
+
|
|
159
174
|
export type EntryPropSegments = {
|
|
160
175
|
loader: LoaderEntry[];
|
|
161
176
|
layout: EntryData[];
|
|
162
|
-
parallel:
|
|
177
|
+
parallel: ParallelEntries; // slot -> parallel entry (same entry may back multiple slots)
|
|
163
178
|
intercept: InterceptEntry[]; // intercept definitions for soft navigation
|
|
164
179
|
};
|
|
165
180
|
|
|
@@ -176,8 +191,12 @@ export type EntryData =
|
|
|
176
191
|
/** Original PrerenderHandlerDefinition (for build-time getParams access) */
|
|
177
192
|
prerenderDef?: {
|
|
178
193
|
getParams?: (ctx: any) => Promise<any[]> | any[];
|
|
179
|
-
options?: {
|
|
194
|
+
options?: { concurrency?: number };
|
|
180
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>;
|
|
181
200
|
/** Set when handler is a Static definition (build-time only) */
|
|
182
201
|
isStaticPrerender?: true;
|
|
183
202
|
/** Static handler $$id for build-time store lookup */
|
|
@@ -199,18 +218,7 @@ export type EntryData =
|
|
|
199
218
|
} & EntryPropCommon &
|
|
200
219
|
EntryPropDatas &
|
|
201
220
|
EntryPropSegments)
|
|
202
|
-
|
|
|
203
|
-
type: "parallel";
|
|
204
|
-
handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
|
|
205
|
-
loading?: ReactNode | false;
|
|
206
|
-
transition?: TransitionConfig;
|
|
207
|
-
/** Set when any parallel slot is a Static definition */
|
|
208
|
-
isStaticPrerender?: true;
|
|
209
|
-
/** Per-slot static handler $$ids for build-time store lookup */
|
|
210
|
-
staticHandlerIds?: Record<string, string>;
|
|
211
|
-
} & EntryPropCommon &
|
|
212
|
-
EntryPropDatas &
|
|
213
|
-
EntryPropSegments)
|
|
221
|
+
| ParallelEntryData
|
|
214
222
|
| ({
|
|
215
223
|
type: "cache";
|
|
216
224
|
/** Cache entries create cache boundaries and render like layouts (with Outlet) */
|
|
@@ -269,6 +277,9 @@ interface HelperContext {
|
|
|
269
277
|
string,
|
|
270
278
|
import("../cache/profile-registry.js").CacheProfile
|
|
271
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;
|
|
272
283
|
}
|
|
273
284
|
// Use a global symbol key so the AsyncLocalStorage instance survives HMR
|
|
274
285
|
// module re-evaluation. Without this, Vite's RSC module runner may create
|
|
@@ -552,6 +563,80 @@ export function getRootScoped(): boolean {
|
|
|
552
563
|
// Export HelperContext type for use in other modules
|
|
553
564
|
export type { HelperContext };
|
|
554
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
|
+
|
|
555
640
|
// ============================================================================
|
|
556
641
|
// Performance Metrics Helpers
|
|
557
642
|
// ============================================================================
|
|
@@ -567,7 +652,7 @@ export type { HelperContext };
|
|
|
567
652
|
* done(); // Records duration
|
|
568
653
|
* ```
|
|
569
654
|
*/
|
|
570
|
-
export function track(label: string): () => void {
|
|
655
|
+
export function track(label: string, depth?: number): () => void {
|
|
571
656
|
const store = RSCRouterContext.getStore();
|
|
572
657
|
|
|
573
658
|
// No-op if context unavailable or metrics not enabled
|
|
@@ -580,6 +665,53 @@ export function track(label: string): () => void {
|
|
|
580
665
|
return () => {
|
|
581
666
|
const duration =
|
|
582
667
|
performance.now() - store.metrics!.requestStart - startTime;
|
|
583
|
-
store.metrics!.metrics.push({
|
|
668
|
+
store.metrics!.metrics.push({
|
|
669
|
+
label,
|
|
670
|
+
duration,
|
|
671
|
+
startTime,
|
|
672
|
+
...(depth != null ? { depth } : {}),
|
|
673
|
+
});
|
|
584
674
|
};
|
|
585
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);
|