@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/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
|
+
}
|
|
@@ -13,6 +13,25 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export type HandleData = Record<string, Record<string, unknown[]>>;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Build a HandleData snapshot from a HandleStore using segment ordering.
|
|
18
|
+
* Reads data directly from the store for each segment in order.
|
|
19
|
+
*/
|
|
20
|
+
export function buildHandleSnapshot(
|
|
21
|
+
handleStore: HandleStore,
|
|
22
|
+
segmentOrder: string[],
|
|
23
|
+
): HandleData {
|
|
24
|
+
const data: HandleData = {};
|
|
25
|
+
for (const segmentId of segmentOrder) {
|
|
26
|
+
const segData = handleStore.getDataForSegment(segmentId);
|
|
27
|
+
for (const handleName in segData) {
|
|
28
|
+
if (!data[handleName]) data[handleName] = {};
|
|
29
|
+
data[handleName][segmentId] = segData[handleName];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
function createLateHandlePushError(
|
|
17
36
|
handleName: string,
|
|
18
37
|
segmentId: string,
|
|
@@ -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);
|
|
@@ -20,8 +20,18 @@ import type {
|
|
|
20
20
|
DefaultRouteName,
|
|
21
21
|
} from "../types/global-namespace.js";
|
|
22
22
|
import type { Handle } from "../handle.js";
|
|
23
|
-
import {
|
|
24
|
-
|
|
23
|
+
import {
|
|
24
|
+
type ContextVar,
|
|
25
|
+
contextGet,
|
|
26
|
+
contextSet,
|
|
27
|
+
isNonCacheable,
|
|
28
|
+
} from "../context-var.js";
|
|
29
|
+
import {
|
|
30
|
+
createHandleStore,
|
|
31
|
+
buildHandleSnapshot,
|
|
32
|
+
type HandleStore,
|
|
33
|
+
type HandleData,
|
|
34
|
+
} from "./handle-store.js";
|
|
25
35
|
import { isHandle } from "../handle.js";
|
|
26
36
|
import { track, type MetricsStore } from "./context.js";
|
|
27
37
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
@@ -30,7 +40,11 @@ import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
|
30
40
|
import { THEME_COOKIE } from "../theme/constants.js";
|
|
31
41
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
32
42
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
33
|
-
import {
|
|
43
|
+
import { isInsideCacheScope } from "./context.js";
|
|
44
|
+
import {
|
|
45
|
+
createReverseFunction,
|
|
46
|
+
stripInternalParams,
|
|
47
|
+
} from "../router/handler-context.js";
|
|
34
48
|
import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
|
|
35
49
|
import { invariant } from "../errors.js";
|
|
36
50
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
@@ -49,14 +63,19 @@ export interface RequestContext<
|
|
|
49
63
|
env: TEnv;
|
|
50
64
|
/** Original HTTP request */
|
|
51
65
|
request: Request;
|
|
52
|
-
/** Parsed URL (
|
|
66
|
+
/** Parsed URL (with internal `_rsc*` params stripped) */
|
|
53
67
|
url: URL;
|
|
68
|
+
/**
|
|
69
|
+
* The original request URL with all parameters intact, including
|
|
70
|
+
* internal `_rsc*` transport params.
|
|
71
|
+
*/
|
|
72
|
+
originalUrl: URL;
|
|
54
73
|
/** URL pathname */
|
|
55
74
|
pathname: string;
|
|
56
|
-
/** URL search params (
|
|
75
|
+
/** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
|
|
57
76
|
searchParams: URLSearchParams;
|
|
58
|
-
/**
|
|
59
|
-
|
|
77
|
+
/** @internal Shared variable backing store for ctx.get()/ctx.set(). */
|
|
78
|
+
_variables: Record<string, any>;
|
|
60
79
|
/** Get a variable set by middleware */
|
|
61
80
|
get: {
|
|
62
81
|
<T>(contextVar: ContextVar<T>): T | undefined;
|
|
@@ -64,20 +83,19 @@ export interface RequestContext<
|
|
|
64
83
|
};
|
|
65
84
|
/** Set a variable (shared with middleware and handlers) */
|
|
66
85
|
set: {
|
|
67
|
-
<T>(
|
|
68
|
-
|
|
86
|
+
<T>(
|
|
87
|
+
contextVar: ContextVar<T>,
|
|
88
|
+
value: T,
|
|
89
|
+
options?: { cache?: boolean },
|
|
90
|
+
): void;
|
|
91
|
+
<K extends string>(key: K, value: any, options?: { cache?: boolean }): void;
|
|
69
92
|
};
|
|
70
93
|
/**
|
|
71
94
|
* Route params (populated after route matching)
|
|
72
95
|
* Initially empty, then set to matched params
|
|
73
96
|
*/
|
|
74
97
|
params: TParams;
|
|
75
|
-
/**
|
|
76
|
-
* Stub response for setting headers/cookies (read-only).
|
|
77
|
-
* Headers set here are merged into the final response.
|
|
78
|
-
* Use header() or setStatus() to mutate response headers/status.
|
|
79
|
-
* Use cookies().set()/cookies().delete() for cookie mutations.
|
|
80
|
-
*/
|
|
98
|
+
/** @internal Stub response for collecting headers/cookies. Use ctx.headers or ctx.header() instead. */
|
|
81
99
|
readonly res: Response;
|
|
82
100
|
|
|
83
101
|
/** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
|
|
@@ -95,6 +113,8 @@ export interface RequestContext<
|
|
|
95
113
|
header(name: string, value: string): void;
|
|
96
114
|
/** Set the response status code */
|
|
97
115
|
setStatus(status: number): void;
|
|
116
|
+
/** @internal Set status bypassing cache-exec guard (for framework error handling) */
|
|
117
|
+
_setStatus(status: number): void;
|
|
98
118
|
|
|
99
119
|
/**
|
|
100
120
|
* Access loader data or push handle data.
|
|
@@ -256,6 +276,54 @@ export interface RequestContext<
|
|
|
256
276
|
/** @internal Previous route key (from the navigation source), used for revalidation */
|
|
257
277
|
_prevRouteKey?: string;
|
|
258
278
|
|
|
279
|
+
/**
|
|
280
|
+
* @internal Render barrier for experimental `rendered()` API.
|
|
281
|
+
* Resolves when all non-loader segments have settled and handle data
|
|
282
|
+
* is available. Used by DSL loaders that call `ctx.rendered()`.
|
|
283
|
+
*/
|
|
284
|
+
_renderBarrier: Promise<void>;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @internal Resolve the render barrier. Accepts resolved segments, filters
|
|
288
|
+
* out loaders, and captures non-loader segment IDs as the handle ordering.
|
|
289
|
+
* Called after segment resolution (fresh) or handle replay (cache/prerender).
|
|
290
|
+
*/
|
|
291
|
+
_resolveRenderBarrier: (
|
|
292
|
+
segments: Array<{ type: string; id: string }>,
|
|
293
|
+
) => void;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @internal Segment order at barrier resolution time, used by loader
|
|
297
|
+
* ctx.use(handle) to collect handle data in correct order.
|
|
298
|
+
*/
|
|
299
|
+
_renderBarrierSegmentOrder?: string[];
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @internal Set to true when the matched entry tree contains any `loading()`
|
|
303
|
+
* entries (streaming). Used by rendered() to fail fast.
|
|
304
|
+
*/
|
|
305
|
+
_treeHasStreaming?: boolean;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* @internal Loader IDs that have called rendered() and are waiting for the
|
|
309
|
+
* barrier. Used to detect deadlocks when a handler tries to await the same
|
|
310
|
+
* loader via ctx.use(Loader).
|
|
311
|
+
*/
|
|
312
|
+
_renderBarrierWaiters?: Set<string>;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @internal Loader IDs that handlers have started awaiting via ctx.use().
|
|
316
|
+
* Used for bidirectional deadlock detection: if a loader later calls
|
|
317
|
+
* rendered() and a handler already awaits it, we can detect the deadlock.
|
|
318
|
+
*/
|
|
319
|
+
_handlerLoaderDeps?: Set<string>;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @internal Cached HandleData snapshot built at barrier resolution time.
|
|
323
|
+
* Avoids rebuilding the snapshot on every loader ctx.use(handle) call.
|
|
324
|
+
*/
|
|
325
|
+
_renderBarrierHandleSnapshot?: HandleData;
|
|
326
|
+
|
|
259
327
|
/** @internal Per-request error dedup set for onError reporting */
|
|
260
328
|
_reportedErrors: WeakSet<object>;
|
|
261
329
|
|
|
@@ -272,6 +340,15 @@ export interface RequestContext<
|
|
|
272
340
|
|
|
273
341
|
/** @internal Request-scoped performance metrics store */
|
|
274
342
|
_metricsStore?: MetricsStore;
|
|
343
|
+
|
|
344
|
+
/** @internal Router basename for this request (used by redirect()) */
|
|
345
|
+
_basename?: string;
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
|
|
349
|
+
* to avoid a second resolveRoute call. Cleared on HMR invalidation.
|
|
350
|
+
*/
|
|
351
|
+
_classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
|
|
275
352
|
}
|
|
276
353
|
|
|
277
354
|
/**
|
|
@@ -298,9 +375,21 @@ export type PublicRequestContext<
|
|
|
298
375
|
| "_routeName"
|
|
299
376
|
| "_prevRouteKey"
|
|
300
377
|
| "_reportedErrors"
|
|
378
|
+
| "_renderBarrier"
|
|
379
|
+
| "_resolveRenderBarrier"
|
|
380
|
+
| "_renderBarrierSegmentOrder"
|
|
381
|
+
| "_treeHasStreaming"
|
|
382
|
+
| "_renderBarrierWaiters"
|
|
383
|
+
| "_handlerLoaderDeps"
|
|
384
|
+
| "_renderBarrierHandleSnapshot"
|
|
301
385
|
| "_reportBackgroundError"
|
|
302
386
|
| "_debugPerformance"
|
|
303
387
|
| "_metricsStore"
|
|
388
|
+
| "_basename"
|
|
389
|
+
| "_setStatus"
|
|
390
|
+
| "_variables"
|
|
391
|
+
| "_classifiedRoute"
|
|
392
|
+
| "res"
|
|
304
393
|
>;
|
|
305
394
|
|
|
306
395
|
// AsyncLocalStorage instance for request context
|
|
@@ -499,6 +588,18 @@ export function createRequestContext<TEnv>(
|
|
|
499
588
|
responseCookieCache = null;
|
|
500
589
|
};
|
|
501
590
|
|
|
591
|
+
// Guard: throw if a response-level side effect is called inside a cache() scope.
|
|
592
|
+
// Uses ALS to detect the scope (set during segment resolution).
|
|
593
|
+
function assertNotInsideCacheScopeALS(methodName: string): void {
|
|
594
|
+
if (isInsideCacheScope()) {
|
|
595
|
+
throw new Error(
|
|
596
|
+
`ctx.${methodName}() cannot be called inside a cache() boundary. ` +
|
|
597
|
+
`On cache hit the handler is skipped, so this side effect would be lost. ` +
|
|
598
|
+
`Move ctx.${methodName}() to a middleware or layout outside the cache() scope.`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
502
603
|
// Effective cookie read: response stub Set-Cookie wins, then original header.
|
|
503
604
|
// The stub IS the source of truth for same-request mutations.
|
|
504
605
|
const effectiveCookie = (name: string): string | undefined => {
|
|
@@ -551,19 +652,31 @@ export function createRequestContext<TEnv>(
|
|
|
551
652
|
invalidateResponseCookieCache();
|
|
552
653
|
};
|
|
553
654
|
|
|
655
|
+
// Strip internal _rsc* params so userland sees a clean URL.
|
|
656
|
+
const cleanUrl = stripInternalParams(url);
|
|
657
|
+
|
|
554
658
|
// Build the context object first (without use), then add use
|
|
555
659
|
const ctx: RequestContext<TEnv> = {
|
|
556
660
|
env,
|
|
557
661
|
request,
|
|
558
|
-
url,
|
|
662
|
+
url: cleanUrl,
|
|
663
|
+
originalUrl: new URL(request.url),
|
|
559
664
|
pathname: url.pathname,
|
|
560
|
-
searchParams:
|
|
561
|
-
|
|
562
|
-
get: ((keyOrVar: any) =>
|
|
563
|
-
|
|
564
|
-
|
|
665
|
+
searchParams: cleanUrl.searchParams,
|
|
666
|
+
_variables: variables,
|
|
667
|
+
get: ((keyOrVar: any) => {
|
|
668
|
+
if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
|
|
669
|
+
throw new Error(
|
|
670
|
+
`ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
|
|
671
|
+
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
672
|
+
`and its value would be stale on cache hit. Move the read outside the cached scope.`,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
return contextGet(variables, keyOrVar);
|
|
676
|
+
}) as RequestContext<TEnv>["get"],
|
|
677
|
+
set: ((keyOrVar: any, value: any, options?: any) => {
|
|
565
678
|
assertNotInsideCacheExec(ctx, "set");
|
|
566
|
-
contextSet(variables, keyOrVar, value);
|
|
679
|
+
contextSet(variables, keyOrVar, value, options);
|
|
567
680
|
}) as RequestContext<TEnv>["set"],
|
|
568
681
|
params: {} as Record<string, string>,
|
|
569
682
|
|
|
@@ -601,6 +714,7 @@ export function createRequestContext<TEnv>(
|
|
|
601
714
|
|
|
602
715
|
setCookie(name: string, value: string, options?: CookieOptions): void {
|
|
603
716
|
assertNotInsideCacheExec(ctx, "setCookie");
|
|
717
|
+
assertNotInsideCacheScopeALS("setCookie");
|
|
604
718
|
stubResponse.headers.append(
|
|
605
719
|
"Set-Cookie",
|
|
606
720
|
serializeCookieValue(name, value, options),
|
|
@@ -613,6 +727,7 @@ export function createRequestContext<TEnv>(
|
|
|
613
727
|
options?: Pick<CookieOptions, "domain" | "path">,
|
|
614
728
|
): void {
|
|
615
729
|
assertNotInsideCacheExec(ctx, "deleteCookie");
|
|
730
|
+
assertNotInsideCacheScopeALS("deleteCookie");
|
|
616
731
|
stubResponse.headers.append(
|
|
617
732
|
"Set-Cookie",
|
|
618
733
|
serializeCookieValue(name, "", { ...options, maxAge: 0 }),
|
|
@@ -622,13 +737,20 @@ export function createRequestContext<TEnv>(
|
|
|
622
737
|
|
|
623
738
|
header(name: string, value: string): void {
|
|
624
739
|
assertNotInsideCacheExec(ctx, "header");
|
|
740
|
+
assertNotInsideCacheScopeALS("header");
|
|
625
741
|
stubResponse.headers.set(name, value);
|
|
626
742
|
},
|
|
627
743
|
|
|
628
744
|
setStatus(status: number): void {
|
|
629
745
|
assertNotInsideCacheExec(ctx, "setStatus");
|
|
630
|
-
|
|
631
|
-
|
|
746
|
+
assertNotInsideCacheScopeALS("setStatus");
|
|
747
|
+
stubResponse = new Response(null, {
|
|
748
|
+
status,
|
|
749
|
+
headers: stubResponse.headers,
|
|
750
|
+
});
|
|
751
|
+
},
|
|
752
|
+
|
|
753
|
+
_setStatus(status: number): void {
|
|
632
754
|
stubResponse = new Response(null, {
|
|
633
755
|
status,
|
|
634
756
|
headers: stubResponse.headers,
|
|
@@ -660,6 +782,7 @@ export function createRequestContext<TEnv>(
|
|
|
660
782
|
|
|
661
783
|
onResponse(callback: (response: Response) => Response): void {
|
|
662
784
|
assertNotInsideCacheExec(ctx, "onResponse");
|
|
785
|
+
assertNotInsideCacheScopeALS("onResponse");
|
|
663
786
|
this._onResponseCallbacks.push(callback);
|
|
664
787
|
},
|
|
665
788
|
|
|
@@ -687,9 +810,58 @@ export function createRequestContext<TEnv>(
|
|
|
687
810
|
_reportedErrors: new WeakSet<object>(),
|
|
688
811
|
_metricsStore: undefined,
|
|
689
812
|
|
|
813
|
+
// Render barrier: deferred promise resolved after non-loader segments settle.
|
|
814
|
+
_renderBarrier: null as any, // set below
|
|
815
|
+
_resolveRenderBarrier: null as any, // set below
|
|
816
|
+
_renderBarrierSegmentOrder: undefined,
|
|
817
|
+
|
|
690
818
|
reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
|
|
691
819
|
};
|
|
692
820
|
|
|
821
|
+
// Lazy render barrier: only allocate the Promise when a loader actually
|
|
822
|
+
// calls rendered(). Requests that don't use rendered() pay zero cost.
|
|
823
|
+
let barrierResolved = false;
|
|
824
|
+
let resolveBarrier: (() => void) | undefined;
|
|
825
|
+
ctx._renderBarrier = null as any; // lazy — created on first access
|
|
826
|
+
ctx._resolveRenderBarrier = (
|
|
827
|
+
segments: Array<{ type: string; id: string }>,
|
|
828
|
+
) => {
|
|
829
|
+
if (barrierResolved) return;
|
|
830
|
+
barrierResolved = true;
|
|
831
|
+
const segOrder = segments
|
|
832
|
+
.filter((s) => s.type !== "loader")
|
|
833
|
+
.map((s) => s.id);
|
|
834
|
+
ctx._renderBarrierSegmentOrder = segOrder;
|
|
835
|
+
// Build and cache handle snapshot so loader ctx.use(handle) calls
|
|
836
|
+
// don't rebuild it on every invocation.
|
|
837
|
+
ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
|
|
838
|
+
handleStore,
|
|
839
|
+
segOrder,
|
|
840
|
+
);
|
|
841
|
+
ctx._renderBarrierWaiters = undefined;
|
|
842
|
+
ctx._handlerLoaderDeps = undefined;
|
|
843
|
+
if (resolveBarrier) resolveBarrier();
|
|
844
|
+
};
|
|
845
|
+
Object.defineProperty(ctx, "_renderBarrier", {
|
|
846
|
+
get() {
|
|
847
|
+
// Barrier already resolved (cache/prerender hit) or first lazy access.
|
|
848
|
+
// Either way, replace the getter with a concrete value to avoid
|
|
849
|
+
// repeated Promise.resolve() allocations on subsequent reads.
|
|
850
|
+
const p = barrierResolved
|
|
851
|
+
? Promise.resolve()
|
|
852
|
+
: new Promise<void>((resolve) => {
|
|
853
|
+
resolveBarrier = resolve;
|
|
854
|
+
});
|
|
855
|
+
Object.defineProperty(ctx, "_renderBarrier", {
|
|
856
|
+
value: p,
|
|
857
|
+
writable: false,
|
|
858
|
+
configurable: false,
|
|
859
|
+
});
|
|
860
|
+
return p;
|
|
861
|
+
},
|
|
862
|
+
configurable: true,
|
|
863
|
+
});
|
|
864
|
+
|
|
693
865
|
// Now create use() with access to ctx
|
|
694
866
|
ctx.use = createUseFunction({
|
|
695
867
|
handleStore,
|
|
@@ -872,14 +1044,13 @@ export function createUseFunction<TEnv>(
|
|
|
872
1044
|
pathname: ctx.pathname,
|
|
873
1045
|
url: ctx.url,
|
|
874
1046
|
env: ctx.env as any,
|
|
875
|
-
var: ctx.var as any,
|
|
876
1047
|
get: ctx.get as any,
|
|
877
|
-
use: <TDep, TDepParams = any>(
|
|
1048
|
+
use: (<TDep, TDepParams = any>(
|
|
878
1049
|
dep: LoaderDefinition<TDep, TDepParams>,
|
|
879
1050
|
): Promise<TDep> => {
|
|
880
1051
|
// Recursive call - will start dep loader if not already started
|
|
881
1052
|
return ctx.use(dep);
|
|
882
|
-
},
|
|
1053
|
+
}) as LoaderContext["use"],
|
|
883
1054
|
method: "GET",
|
|
884
1055
|
body: undefined,
|
|
885
1056
|
reverse: createReverseFunction(
|
|
@@ -888,9 +1059,14 @@ export function createUseFunction<TEnv>(
|
|
|
888
1059
|
ctx.params as Record<string, string>,
|
|
889
1060
|
ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
|
|
890
1061
|
),
|
|
1062
|
+
rendered: () => {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
`ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
|
|
1065
|
+
`It cannot be used from request-context loaders or server actions.`,
|
|
1066
|
+
);
|
|
1067
|
+
},
|
|
891
1068
|
};
|
|
892
1069
|
|
|
893
|
-
// Start loader execution with tracking
|
|
894
1070
|
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
|
895
1071
|
const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
|
|
896
1072
|
doneLoader();
|
package/src/ssr/index.tsx
CHANGED
|
@@ -129,6 +129,7 @@ interface RscPayload {
|
|
|
129
129
|
matched?: string[];
|
|
130
130
|
pathname?: string;
|
|
131
131
|
params?: Record<string, string>;
|
|
132
|
+
basename?: string;
|
|
132
133
|
themeConfig?: ResolvedThemeConfig | null;
|
|
133
134
|
initialTheme?: Theme;
|
|
134
135
|
version?: string;
|
|
@@ -168,6 +169,7 @@ function createSsrEventController(opts: {
|
|
|
168
169
|
const state: DerivedNavigationState = {
|
|
169
170
|
state: "idle",
|
|
170
171
|
isStreaming: false,
|
|
172
|
+
isNavigating: false,
|
|
171
173
|
location,
|
|
172
174
|
pendingUrl: null,
|
|
173
175
|
inflightActions: [],
|
|
@@ -260,6 +262,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
260
262
|
function SsrRoot() {
|
|
261
263
|
payload ??= createFromReadableStream<RscPayload>(rscStream1);
|
|
262
264
|
const resolved = React.use(payload);
|
|
265
|
+
|
|
263
266
|
const themeConfig = resolved.metadata?.themeConfig ?? null;
|
|
264
267
|
const pathname = resolved.metadata?.pathname ?? "/";
|
|
265
268
|
|
|
@@ -285,6 +288,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
285
288
|
navigate: async () => {},
|
|
286
289
|
refresh: async () => {},
|
|
287
290
|
version: resolved.metadata?.version,
|
|
291
|
+
basename: resolved.metadata?.basename,
|
|
288
292
|
};
|
|
289
293
|
|
|
290
294
|
// Build content tree from segments.
|