@rangojs/router 0.0.0-experimental.f2337aef → 0.0.0-experimental.fa8a383a
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/dist/bin/rango.js +8 -3
- package/dist/vite/index.js +139 -200
- package/package.json +1 -1
- package/skills/caching/SKILL.md +37 -4
- package/skills/parallel/SKILL.md +59 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +1 -3
- package/src/browser/navigation-client.ts +60 -27
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +39 -9
- package/src/browser/prefetch/cache.ts +57 -5
- package/src/browser/prefetch/fetch.ts +30 -21
- package/src/browser/prefetch/queue.ts +53 -13
- package/src/browser/react/Link.tsx +9 -1
- package/src/browser/react/NavigationProvider.tsx +27 -0
- package/src/browser/rsc-router.tsx +109 -57
- package/src/browser/scroll-restoration.ts +20 -7
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/types.ts +9 -0
- package/src/build/route-types/router-processing.ts +12 -2
- package/src/cache/cache-scope.ts +2 -2
- 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/debug.ts +2 -2
- package/src/route-definition/dsl-helpers.ts +32 -7
- package/src/route-definition/redirect.ts +2 -2
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/logging.ts +1 -1
- package/src/router/manifest.ts +9 -3
- package/src/router/match-middleware/background-revalidation.ts +18 -1
- package/src/router/match-middleware/cache-lookup.ts +20 -3
- package/src/router/match-middleware/cache-store.ts +32 -6
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +7 -5
- package/src/router/match-result.ts +11 -1
- package/src/router/middleware.ts +2 -1
- package/src/router/segment-resolution/fresh.ts +104 -14
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +307 -272
- package/src/router.ts +5 -1
- package/src/rsc/handler.ts +9 -0
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +90 -13
- package/src/server/request-context.ts +10 -4
- package/src/ssr/index.tsx +1 -0
- package/src/types/route-entry.ts +7 -0
- package/src/types/segments.ts +2 -0
- package/src/urls/path-helper.ts +1 -1
- package/src/vite/discovery/state.ts +0 -2
- package/src/vite/plugin-types.ts +0 -83
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +144 -209
- package/src/vite/router-discovery.ts +0 -8
- package/src/vite/utils/banner.ts +3 -3
package/src/router.ts
CHANGED
|
@@ -560,6 +560,7 @@ export function createRouter<TEnv = any>(
|
|
|
560
560
|
mergedRouteMap,
|
|
561
561
|
nextMountIndex: () => mountIndex++,
|
|
562
562
|
getPrecomputedByPrefix,
|
|
563
|
+
routerId,
|
|
563
564
|
};
|
|
564
565
|
|
|
565
566
|
function evaluateLazyEntry(entry: RouteEntry<TEnv>): void {
|
|
@@ -689,7 +690,7 @@ export function createRouter<TEnv = any>(
|
|
|
689
690
|
errorBoundary: [],
|
|
690
691
|
notFoundBoundary: [],
|
|
691
692
|
layout: [],
|
|
692
|
-
parallel:
|
|
693
|
+
parallel: {},
|
|
693
694
|
intercept: [],
|
|
694
695
|
loader: [],
|
|
695
696
|
};
|
|
@@ -751,6 +752,7 @@ export function createRouter<TEnv = any>(
|
|
|
751
752
|
trailingSlash: trailingSlashConfig,
|
|
752
753
|
handler: urlPatterns.handler,
|
|
753
754
|
mountIndex: currentMountIndex,
|
|
755
|
+
routerId,
|
|
754
756
|
cacheProfiles: resolvedCacheProfiles,
|
|
755
757
|
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
756
758
|
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
@@ -770,6 +772,7 @@ export function createRouter<TEnv = any>(
|
|
|
770
772
|
trailingSlash: trailingSlashConfig,
|
|
771
773
|
handler: urlPatterns.handler,
|
|
772
774
|
mountIndex: currentMountIndex,
|
|
775
|
+
routerId,
|
|
773
776
|
cacheProfiles: resolvedCacheProfiles,
|
|
774
777
|
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
775
778
|
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
@@ -813,6 +816,7 @@ export function createRouter<TEnv = any>(
|
|
|
813
816
|
trailingSlash: trailingSlashConfig,
|
|
814
817
|
handler: urlPatterns.handler,
|
|
815
818
|
mountIndex: mountIndex++,
|
|
819
|
+
routerId,
|
|
816
820
|
// Lazy evaluation fields
|
|
817
821
|
lazy: true,
|
|
818
822
|
lazyPatterns: lazyInclude.patterns,
|
package/src/rsc/handler.ts
CHANGED
|
@@ -490,6 +490,7 @@ export function createRSCHandler<
|
|
|
490
490
|
// has completed so :post spans are captured in the timeline.
|
|
491
491
|
// Handler timing parts are always emitted (even without debug metrics)
|
|
492
492
|
// so non-debug requests still get bootstrap Server-Timing entries.
|
|
493
|
+
const finalizeStart = performance.now();
|
|
493
494
|
const handlerTimingArr: string[] = variables.__handlerTiming || [];
|
|
494
495
|
// Preserve any existing Server-Timing set by response routes or middleware
|
|
495
496
|
const existingTiming = response.headers.get("Server-Timing");
|
|
@@ -506,6 +507,14 @@ export function createRSCHandler<
|
|
|
506
507
|
const totalStart = earlyMetricsStore
|
|
507
508
|
? handlerStart
|
|
508
509
|
: metricsStore.requestStart;
|
|
510
|
+
// response-finalize measures the gap between render completion and
|
|
511
|
+
// handler return: header assembly, onResponse callbacks, etc.
|
|
512
|
+
appendMetric(
|
|
513
|
+
metricsStore,
|
|
514
|
+
"response-finalize",
|
|
515
|
+
finalizeStart,
|
|
516
|
+
performance.now() - finalizeStart,
|
|
517
|
+
);
|
|
509
518
|
appendMetric(
|
|
510
519
|
metricsStore,
|
|
511
520
|
"handler:total",
|
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
|
|
|
@@ -200,18 +214,7 @@ export type EntryData =
|
|
|
200
214
|
} & EntryPropCommon &
|
|
201
215
|
EntryPropDatas &
|
|
202
216
|
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)
|
|
217
|
+
| ParallelEntryData
|
|
215
218
|
| ({
|
|
216
219
|
type: "cache";
|
|
217
220
|
/** Cache entries create cache boundaries and render like layouts (with Outlet) */
|
|
@@ -553,6 +556,80 @@ export function getRootScoped(): boolean {
|
|
|
553
556
|
// Export HelperContext type for use in other modules
|
|
554
557
|
export type { HelperContext };
|
|
555
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Return an isolated copy of a lazy include's captured parent entry.
|
|
561
|
+
*
|
|
562
|
+
* DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
|
|
563
|
+
* Multiple include() scopes capture the *same* syntheticMapRoot as their
|
|
564
|
+
* parent, so without isolation one include's loaders/middleware leak into
|
|
565
|
+
* every other route that shares that root.
|
|
566
|
+
*
|
|
567
|
+
* The clone is shallow: only the mutable arrays are copied so each
|
|
568
|
+
* include pushes to its own list. The rest of the entry (id, shortCode,
|
|
569
|
+
* parent pointer, handler) stays shared, which is correct and cheap.
|
|
570
|
+
*/
|
|
571
|
+
export function getIsolatedLazyParent(
|
|
572
|
+
captured: EntryData | null | undefined,
|
|
573
|
+
): EntryData | null {
|
|
574
|
+
if (!captured) return null;
|
|
575
|
+
return {
|
|
576
|
+
...captured,
|
|
577
|
+
loader: [...captured.loader],
|
|
578
|
+
middleware: [...captured.middleware],
|
|
579
|
+
revalidate: [...captured.revalidate],
|
|
580
|
+
errorBoundary: [...captured.errorBoundary],
|
|
581
|
+
notFoundBoundary: [...captured.notFoundBoundary],
|
|
582
|
+
layout: [...captured.layout],
|
|
583
|
+
parallel: { ...captured.parallel },
|
|
584
|
+
intercept: [...captured.intercept],
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function getParallelEntries(
|
|
589
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
590
|
+
): ParallelEntryData[] {
|
|
591
|
+
if (!parallels) return [];
|
|
592
|
+
if (Array.isArray(parallels)) {
|
|
593
|
+
return parallels.filter(
|
|
594
|
+
(entry): entry is ParallelEntryData => entry.type === "parallel",
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
return Object.values(parallels).filter(
|
|
598
|
+
(entry): entry is ParallelEntryData => !!entry,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function getParallelSlotEntries(
|
|
603
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
604
|
+
): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
|
|
605
|
+
if (!parallels) return [];
|
|
606
|
+
|
|
607
|
+
if (Array.isArray(parallels)) {
|
|
608
|
+
return getParallelEntries(parallels).flatMap((entry) =>
|
|
609
|
+
(Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
|
|
610
|
+
slot,
|
|
611
|
+
entry,
|
|
612
|
+
})),
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return Object.entries(parallels)
|
|
617
|
+
.filter(([, entry]) => !!entry)
|
|
618
|
+
.map(([slot, entry]) => ({
|
|
619
|
+
slot: slot as `@${string}`,
|
|
620
|
+
entry: entry!,
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function getParallelSlotCount(
|
|
625
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
626
|
+
): number {
|
|
627
|
+
if (!parallels) return 0;
|
|
628
|
+
return Array.isArray(parallels)
|
|
629
|
+
? parallels.filter((entry) => entry?.type === "parallel").length
|
|
630
|
+
: Object.keys(parallels).length;
|
|
631
|
+
}
|
|
632
|
+
|
|
556
633
|
// ============================================================================
|
|
557
634
|
// Performance Metrics Helpers
|
|
558
635
|
// ============================================================================
|
|
@@ -30,7 +30,10 @@ import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
|
30
30
|
import { THEME_COOKIE } from "../theme/constants.js";
|
|
31
31
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
32
32
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
createReverseFunction,
|
|
35
|
+
stripInternalParams,
|
|
36
|
+
} from "../router/handler-context.js";
|
|
34
37
|
import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
|
|
35
38
|
import { invariant } from "../errors.js";
|
|
36
39
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
@@ -58,7 +61,7 @@ export interface RequestContext<
|
|
|
58
61
|
originalUrl: URL;
|
|
59
62
|
/** URL pathname */
|
|
60
63
|
pathname: string;
|
|
61
|
-
/** URL search params (
|
|
64
|
+
/** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
|
|
62
65
|
searchParams: URLSearchParams;
|
|
63
66
|
/** Variables set by middleware (same as ctx.var) */
|
|
64
67
|
var: Record<string, any>;
|
|
@@ -555,14 +558,17 @@ export function createRequestContext<TEnv>(
|
|
|
555
558
|
invalidateResponseCookieCache();
|
|
556
559
|
};
|
|
557
560
|
|
|
561
|
+
// Strip internal _rsc* params so userland sees a clean URL.
|
|
562
|
+
const cleanUrl = stripInternalParams(url);
|
|
563
|
+
|
|
558
564
|
// Build the context object first (without use), then add use
|
|
559
565
|
const ctx: RequestContext<TEnv> = {
|
|
560
566
|
env,
|
|
561
567
|
request,
|
|
562
|
-
url,
|
|
568
|
+
url: cleanUrl,
|
|
563
569
|
originalUrl: new URL(request.url),
|
|
564
570
|
pathname: url.pathname,
|
|
565
|
-
searchParams:
|
|
571
|
+
searchParams: cleanUrl.searchParams,
|
|
566
572
|
var: variables,
|
|
567
573
|
get: ((keyOrVar: any) =>
|
|
568
574
|
contextGet(variables, keyOrVar)) as RequestContext<TEnv>["get"],
|
package/src/ssr/index.tsx
CHANGED
package/src/types/route-entry.ts
CHANGED
|
@@ -55,6 +55,13 @@ export interface RouteEntry<TEnv = any> {
|
|
|
55
55
|
| Promise<() => Array<AllUseItems>>;
|
|
56
56
|
mountIndex: number;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Router ID that owns this entry. Used to namespace the manifest cache
|
|
60
|
+
* so multi-router setups (host routing) don't share cached EntryData
|
|
61
|
+
* across routers with overlapping mountIndex + routeKey combinations.
|
|
62
|
+
*/
|
|
63
|
+
routerId?: string;
|
|
64
|
+
|
|
58
65
|
/**
|
|
59
66
|
* Route keys in this entry that have pre-render handlers.
|
|
60
67
|
* Used by the non-trie match path to set the `pr` flag.
|
package/src/types/segments.ts
CHANGED
|
@@ -51,9 +51,11 @@ export interface ResolvedSegment {
|
|
|
51
51
|
// Loader-specific fields
|
|
52
52
|
loaderId?: string; // For loaders: the loader $$id identifier
|
|
53
53
|
loaderData?: any; // For loaders: the resolved data from loader execution
|
|
54
|
+
parallelLoading?: ReactNode; // For parallel-owned loaders: the parallel's loading fallback
|
|
54
55
|
// Intercept loader fields (for streaming loader data in parallel segments)
|
|
55
56
|
loaderDataPromise?: Promise<any[]> | any[]; // Loader data promise or resolved array
|
|
56
57
|
loaderIds?: string[]; // IDs ($$id) of loaders for this segment
|
|
58
|
+
parallelLoaderSources?: any[]; // Internal: preserves stable aggregate promise across renders
|
|
57
59
|
// Error-specific fields
|
|
58
60
|
error?: ErrorInfo; // For error segments: the error information
|
|
59
61
|
// NotFound-specific fields
|
package/src/urls/path-helper.ts
CHANGED
|
@@ -13,8 +13,6 @@ export const VIRTUAL_ROUTES_MANIFEST_ID = "virtual:rsc-router/routes-manifest";
|
|
|
13
13
|
export interface PluginOptions {
|
|
14
14
|
enableBuildPrerender?: boolean;
|
|
15
15
|
staticRouteTypesGeneration?: boolean;
|
|
16
|
-
include?: string[];
|
|
17
|
-
exclude?: string[];
|
|
18
16
|
// Mutable ref for deferred auto-discovery (node preset).
|
|
19
17
|
// The auto-discover config() hook populates this before configResolved.
|
|
20
18
|
routerPathRef?: { path?: string };
|
package/src/vite/plugin-types.ts
CHANGED
|
@@ -1,39 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RSC plugin entry points configuration.
|
|
3
|
-
* All entries use virtual modules by default. Specify a path to use a custom entry file.
|
|
4
|
-
*/
|
|
5
|
-
export interface RscEntries {
|
|
6
|
-
/**
|
|
7
|
-
* Path to a custom browser/client entry file.
|
|
8
|
-
* If not specified, a default virtual entry is used.
|
|
9
|
-
*/
|
|
10
|
-
client?: string;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Path to a custom SSR entry file.
|
|
14
|
-
* If not specified, a default virtual entry is used.
|
|
15
|
-
*/
|
|
16
|
-
ssr?: string;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Path to a custom RSC entry file.
|
|
20
|
-
* If not specified, a default virtual entry is used that imports the router from the `entry` option.
|
|
21
|
-
*/
|
|
22
|
-
rsc?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Options for @vitejs/plugin-rsc integration
|
|
27
|
-
*/
|
|
28
|
-
export interface RscPluginOptions {
|
|
29
|
-
/**
|
|
30
|
-
* Entry points for client, ssr, and rsc environments.
|
|
31
|
-
* All entries use virtual modules by default.
|
|
32
|
-
* Specify paths only when you need custom entry files.
|
|
33
|
-
*/
|
|
34
|
-
entries?: RscEntries;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
1
|
/**
|
|
38
2
|
* Base options shared by all presets
|
|
39
3
|
*/
|
|
@@ -51,21 +15,6 @@ interface RangoBaseOptions {
|
|
|
51
15
|
* @default true
|
|
52
16
|
*/
|
|
53
17
|
staticRouteTypesGeneration?: boolean;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Glob patterns for files to include in route type scanning.
|
|
57
|
-
* Only files matching at least one pattern will be scanned.
|
|
58
|
-
* Patterns are relative to the project root.
|
|
59
|
-
* When unset, all .ts/.tsx files are scanned.
|
|
60
|
-
*/
|
|
61
|
-
include?: string[];
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Glob patterns for files to exclude from route type scanning.
|
|
65
|
-
* Takes precedence over `include`. Patterns are relative to the project root.
|
|
66
|
-
* Defaults to common test/build directories.
|
|
67
|
-
*/
|
|
68
|
-
exclude?: string[];
|
|
69
18
|
}
|
|
70
19
|
|
|
71
20
|
/**
|
|
@@ -76,38 +25,6 @@ export interface RangoNodeOptions extends RangoBaseOptions {
|
|
|
76
25
|
* Deployment preset. Defaults to 'node' when not specified.
|
|
77
26
|
*/
|
|
78
27
|
preset?: "node";
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Path to your router configuration file that exports the route tree.
|
|
82
|
-
* This file must export a `router` object created with `createRouter()`.
|
|
83
|
-
*
|
|
84
|
-
* When omitted, auto-discovers the router by scanning for files containing
|
|
85
|
-
* `createRouter`. If exactly one is found, it is used automatically.
|
|
86
|
-
* If multiple are found, an error is thrown with the list of candidates.
|
|
87
|
-
*
|
|
88
|
-
* @example
|
|
89
|
-
* ```ts
|
|
90
|
-
* rango({ router: './src/router.tsx' })
|
|
91
|
-
* // or simply:
|
|
92
|
-
* rango()
|
|
93
|
-
* ```
|
|
94
|
-
*/
|
|
95
|
-
router?: string;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* RSC plugin configuration. By default, rsc-router includes @vitejs/plugin-rsc
|
|
99
|
-
* with sensible defaults.
|
|
100
|
-
*
|
|
101
|
-
* Entry files (browser, ssr, rsc) are optional - if they don't exist,
|
|
102
|
-
* virtual defaults are used.
|
|
103
|
-
*
|
|
104
|
-
* - Omit or pass `true`/`{}` to use defaults (recommended)
|
|
105
|
-
* - Pass `{ entries: {...} }` to customize entry paths
|
|
106
|
-
* - Pass `false` to disable (for manual @vitejs/plugin-rsc configuration)
|
|
107
|
-
*
|
|
108
|
-
* @default true
|
|
109
|
-
*/
|
|
110
|
-
rsc?: boolean | RscPluginOptions;
|
|
111
28
|
}
|
|
112
29
|
|
|
113
30
|
/**
|
|
@@ -278,9 +278,7 @@ export function exposeActionId(): Plugin {
|
|
|
278
278
|
if (!rscPluginApi) {
|
|
279
279
|
throw new Error(
|
|
280
280
|
"[rsc-router] Could not find @vitejs/plugin-rsc. " +
|
|
281
|
-
"@rangojs/router requires the Vite RSC plugin
|
|
282
|
-
"The RSC plugin should be included automatically. If you disabled it with\n" +
|
|
283
|
-
"rango({ rsc: false }), add rsc() before rango() in your config.",
|
|
281
|
+
"@rangojs/router requires the Vite RSC plugin, which is included automatically by rango().",
|
|
284
282
|
);
|
|
285
283
|
}
|
|
286
284
|
|
|
@@ -135,8 +135,11 @@ export function createVersionPlugin(): Plugin {
|
|
|
135
135
|
let server: any = null;
|
|
136
136
|
const clientModuleSignatures = new Map<string, ClientModuleSignature>();
|
|
137
137
|
|
|
138
|
+
let versionCounter = 0;
|
|
138
139
|
const bumpVersion = (reason: string) => {
|
|
139
|
-
|
|
140
|
+
// Use timestamp + counter to guarantee uniqueness even when multiple
|
|
141
|
+
// bumps happen within the same millisecond (e.g. cascading HMR events).
|
|
142
|
+
currentVersion = Date.now().toString(16) + String(++versionCounter);
|
|
140
143
|
console.log(`[rsc-router] ${reason}, version updated: ${currentVersion}`);
|
|
141
144
|
|
|
142
145
|
const rscEnv = server?.environments?.rsc;
|
|
@@ -211,6 +214,15 @@ export function createVersionPlugin(): Plugin {
|
|
|
211
214
|
|
|
212
215
|
if (!isRscModule) return;
|
|
213
216
|
|
|
217
|
+
// Skip re-bumping when the version virtual module itself is invalidated
|
|
218
|
+
// (our own bumpVersion() invalidates it, which re-triggers hotUpdate).
|
|
219
|
+
if (
|
|
220
|
+
ctx.modules.length === 1 &&
|
|
221
|
+
ctx.modules[0].id === "\0" + VIRTUAL_IDS.version
|
|
222
|
+
) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
214
226
|
if (isCodeModule(ctx.file)) {
|
|
215
227
|
const filePath = normalizeModuleId(ctx.file);
|
|
216
228
|
const previousSignature = clientModuleSignatures.get(filePath);
|