@rangojs/router 0.1.0 → 0.1.1
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 +11 -1
- package/dist/types/browser/dev-discovery.d.ts +11 -0
- package/dist/types/browser/navigation-store.d.ts +3 -13
- package/dist/types/browser/prefetch/loader.d.ts +10 -0
- package/dist/types/browser/prefetch/runtime.d.ts +3 -0
- package/dist/types/browser/types.d.ts +4 -16
- package/dist/types/build/generate-manifest.d.ts +6 -0
- package/dist/types/cache/cache-scope.d.ts +7 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +10 -9
- package/dist/types/cache/memory-segment-store.d.ts +6 -6
- package/dist/types/cache/shell-snapshot.d.ts +21 -8
- package/dist/types/cache/types.d.ts +24 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +9 -8
- package/dist/types/dev-discovery-protocol.d.ts +5 -0
- package/dist/types/prerender/store.d.ts +1 -0
- package/dist/types/route-map-builder.d.ts +4 -29
- package/dist/types/router/prerender-match.d.ts +4 -1
- package/dist/types/router/router-interfaces.d.ts +3 -1
- package/dist/types/rsc/handler-context.d.ts +1 -0
- package/dist/types/rsc/render-pipeline.d.ts +3 -2
- package/dist/types/rsc/rsc-rendering.d.ts +5 -10
- package/dist/types/rsc/shell-capture.d.ts +4 -8
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/server/context.d.ts +16 -0
- package/dist/types/server/request-context.d.ts +18 -8
- package/dist/types/server.d.ts +1 -1
- package/dist/types/types/route-entry.d.ts +3 -0
- package/dist/types/vite/discovery/state.d.ts +3 -2
- package/dist/types/vite/utils/manifest-utils.d.ts +1 -1
- package/dist/vite/index.js +194 -107
- package/package.json +7 -2
- package/skills/caching/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +48 -4
- package/src/browser/dev-discovery.ts +66 -0
- package/src/browser/navigation-client.ts +6 -0
- package/src/browser/navigation-store.ts +4 -271
- package/src/browser/partial-update.ts +7 -14
- package/src/browser/prefetch/cache.ts +1 -1
- package/src/browser/prefetch/loader.ts +110 -0
- package/src/browser/prefetch/runtime.ts +7 -0
- package/src/browser/react/Link.tsx +7 -10
- package/src/browser/react/NavigationProvider.tsx +1 -1
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/rsc-router.tsx +4 -2
- package/src/browser/types.ts +4 -27
- package/src/build/generate-manifest.ts +11 -0
- package/src/cache/cache-scope.ts +17 -6
- package/src/cache/cf/cf-cache-store.ts +63 -39
- package/src/cache/memory-segment-store.ts +17 -6
- package/src/cache/shell-snapshot.ts +45 -15
- package/src/cache/types.ts +23 -6
- package/src/cache/vercel/vercel-cache-store.ts +52 -22
- package/src/dev-discovery-protocol.ts +11 -0
- package/src/prerender/build-shell-capture.ts +7 -1
- package/src/prerender/store.ts +5 -0
- package/src/route-definition/dsl-helpers.ts +7 -2
- package/src/route-map-builder.ts +56 -49
- package/src/router/handler-context.ts +13 -5
- package/src/router/lazy-includes.ts +1 -0
- package/src/router/match-api.ts +8 -4
- package/src/router/prerender-match.ts +19 -2
- package/src/router/router-interfaces.ts +4 -0
- package/src/router/segment-resolution/static-store.ts +36 -10
- package/src/router.ts +34 -3
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +3 -1
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +4 -11
- package/src/rsc/render-pipeline.ts +10 -2
- package/src/rsc/rsc-rendering.ts +207 -146
- package/src/rsc/shell-capture.ts +34 -11
- package/src/rsc/types.ts +2 -0
- package/src/server/context.ts +28 -0
- package/src/server/request-context.ts +32 -10
- package/src/server.ts +0 -2
- package/src/testing/dispatch.ts +1 -0
- package/src/types/route-entry.ts +3 -0
- package/src/urls/include-helper.ts +1 -0
- package/src/urls/path-helper.ts +8 -4
- package/src/vite/discovery/bundle-postprocess.ts +10 -7
- package/src/vite/discovery/discover-routers.ts +7 -66
- package/src/vite/discovery/prerender-collection.ts +13 -2
- package/src/vite/discovery/state.ts +4 -4
- package/src/vite/discovery/virtual-module-codegen.ts +75 -42
- package/src/vite/plugins/version-injector.ts +0 -1
- package/src/vite/plugins/virtual-entries.ts +11 -1
- package/src/vite/router-discovery.ts +132 -22
- package/src/vite/utils/manifest-utils.ts +1 -4
- package/src/vite/utils/shared-utils.ts +7 -0
|
@@ -134,9 +134,9 @@ type CacheFamily = "s" | "i" | "r" | "h" | "tm";
|
|
|
134
134
|
* invalidateTags for the build-shell read-through's isTagsInvalidatedSince
|
|
135
135
|
* gate. The platform's expireTag() DELETES tagged entries (no queryable
|
|
136
136
|
* history), so the markers are rango's own record of "tag X was invalidated
|
|
137
|
-
* at T". One year:
|
|
138
|
-
* buildVersion
|
|
139
|
-
*
|
|
137
|
+
* at T". One year: runtime tagged-shell retention is capped to this lifetime,
|
|
138
|
+
* while buildVersion retires build shells on the next deploy. An expired marker
|
|
139
|
+
* therefore cannot resurrect a runtime shell that outlived its invalidation.
|
|
140
140
|
*/
|
|
141
141
|
const TAG_MARKER_TTL_SECONDS = 365 * 24 * 60 * 60;
|
|
142
142
|
|
|
@@ -190,7 +190,7 @@ interface VercelShellEnvelope {
|
|
|
190
190
|
rv: string;
|
|
191
191
|
/** Build version at capture (ShellCacheEntry.buildVersion). */
|
|
192
192
|
bv?: string;
|
|
193
|
-
/**
|
|
193
|
+
/** Capture-generation start time, used by tag marker checks. */
|
|
194
194
|
c: number;
|
|
195
195
|
/** staleAt (ms since epoch). */
|
|
196
196
|
s: number;
|
|
@@ -209,6 +209,8 @@ interface VercelShellEnvelope {
|
|
|
209
209
|
* their holes only a handler re-run can fill.
|
|
210
210
|
*/
|
|
211
211
|
lh?: boolean;
|
|
212
|
+
/** ShellCacheEntry.transitionWhen; conditional transitions must re-run. */
|
|
213
|
+
tw?: true;
|
|
212
214
|
}
|
|
213
215
|
|
|
214
216
|
/** Read-path outcome for the debug sink. */
|
|
@@ -355,6 +357,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
355
357
|
export class VercelCacheStore<
|
|
356
358
|
TEnv = unknown,
|
|
357
359
|
> implements SegmentCacheStore<TEnv> {
|
|
360
|
+
readonly supportsPassiveShellReads: true = true;
|
|
358
361
|
readonly defaults?: CacheDefaults;
|
|
359
362
|
readonly keyGenerator?: (
|
|
360
363
|
ctx: RequestContext<TEnv>,
|
|
@@ -731,6 +734,7 @@ export class VercelCacheStore<
|
|
|
731
734
|
|
|
732
735
|
async getShell(
|
|
733
736
|
key: string,
|
|
737
|
+
options?: { claimRevalidation?: boolean },
|
|
734
738
|
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
|
|
735
739
|
const storeKey = this.toStoreKey(key, "h");
|
|
736
740
|
const started = Date.now();
|
|
@@ -774,14 +778,25 @@ export class VercelCacheStore<
|
|
|
774
778
|
return null;
|
|
775
779
|
}
|
|
776
780
|
|
|
781
|
+
if (
|
|
782
|
+
env.t &&
|
|
783
|
+
env.t.length > 0 &&
|
|
784
|
+
(await this.isTagsInvalidatedSince(env.t, env.c))
|
|
785
|
+
) {
|
|
786
|
+
void this.safeDelete(storeKey);
|
|
787
|
+
this.emitDebug({ op: "getShell", key, outcome: "miss", readMs });
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
|
|
777
791
|
const isStale = env.s > 0 && now > env.s;
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
792
|
+
let shouldRevalidate = isStale;
|
|
793
|
+
if (isStale && options?.claimRevalidation !== false) {
|
|
794
|
+
shouldRevalidate = await this.claimRevalidation(
|
|
795
|
+
storeKey,
|
|
796
|
+
env.e,
|
|
797
|
+
"[VercelCacheStore] getShell",
|
|
798
|
+
);
|
|
799
|
+
}
|
|
785
800
|
this.emitDebug({
|
|
786
801
|
op: "getShell",
|
|
787
802
|
key,
|
|
@@ -800,6 +815,7 @@ export class VercelCacheStore<
|
|
|
800
815
|
initialTheme: env.i,
|
|
801
816
|
snapshot: env.sn,
|
|
802
817
|
handlerLiveHoles: env.lh,
|
|
818
|
+
transitionWhen: env.tw,
|
|
803
819
|
createdAt: env.c,
|
|
804
820
|
},
|
|
805
821
|
shouldRevalidate,
|
|
@@ -812,10 +828,12 @@ export class VercelCacheStore<
|
|
|
812
828
|
ttlSeconds?: number,
|
|
813
829
|
swrSeconds?: number,
|
|
814
830
|
tags?: string[],
|
|
815
|
-
): Promise<void> {
|
|
831
|
+
): Promise<"stored" | "invalidated" | void> {
|
|
816
832
|
try {
|
|
817
833
|
const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
818
834
|
const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
|
|
835
|
+
const totalTtl = ttl + swrWindow;
|
|
836
|
+
const now = Date.now();
|
|
819
837
|
const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
|
|
820
838
|
// Store the CLAMPED tags (see putResponse/setItem) so dropped tags don't
|
|
821
839
|
// resurface on a hit; write() re-clamps idempotently.
|
|
@@ -823,6 +841,17 @@ export class VercelCacheStore<
|
|
|
823
841
|
tags,
|
|
824
842
|
"[VercelCacheStore] putShell",
|
|
825
843
|
);
|
|
844
|
+
const storeKey = this.toStoreKey(key, "h");
|
|
845
|
+
if (
|
|
846
|
+
safeTags.length > 0 &&
|
|
847
|
+
(await this.isTagsInvalidatedSince(safeTags, entry.createdAt))
|
|
848
|
+
) {
|
|
849
|
+
return "invalidated";
|
|
850
|
+
}
|
|
851
|
+
const retentionTtl =
|
|
852
|
+
safeTags.length > 0
|
|
853
|
+
? Math.min(totalTtl, TAG_MARKER_TTL_SECONDS)
|
|
854
|
+
: totalTtl;
|
|
826
855
|
const env: VercelShellEnvelope = {
|
|
827
856
|
p: entry.prelude,
|
|
828
857
|
po: entry.postponed,
|
|
@@ -830,22 +859,24 @@ export class VercelCacheStore<
|
|
|
830
859
|
bv: entry.buildVersion,
|
|
831
860
|
c: entry.createdAt,
|
|
832
861
|
s: staleAt,
|
|
833
|
-
e: expiresAt,
|
|
862
|
+
e: Math.min(expiresAt, now + retentionTtl * 1000),
|
|
834
863
|
t: safeTags.length > 0 ? safeTags : undefined,
|
|
835
864
|
i: entry.initialTheme,
|
|
836
865
|
sn: entry.snapshot,
|
|
837
866
|
lh: entry.handlerLiveHoles,
|
|
867
|
+
tw: entry.transitionWhen,
|
|
838
868
|
};
|
|
839
869
|
// write() enforces the 2 MB per-item ceiling (withinSizeLimit): an
|
|
840
870
|
// oversized shell prelude is reported and skipped (fail-open to a full
|
|
841
871
|
// render), never silently no-op'd on the platform.
|
|
842
872
|
await this.write(
|
|
843
|
-
|
|
873
|
+
storeKey,
|
|
844
874
|
env,
|
|
845
|
-
|
|
875
|
+
retentionTtl,
|
|
846
876
|
safeTags,
|
|
847
877
|
"[VercelCacheStore] putShell",
|
|
848
878
|
);
|
|
879
|
+
return "stored";
|
|
849
880
|
} catch (error) {
|
|
850
881
|
reportCacheError(error, "cache-write", "[VercelCacheStore] putShell");
|
|
851
882
|
}
|
|
@@ -854,12 +885,10 @@ export class VercelCacheStore<
|
|
|
854
885
|
// --- Tags ---
|
|
855
886
|
|
|
856
887
|
/**
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
* (>= so a same-millisecond invalidation wins). Fails open to `false` on
|
|
862
|
-
* read errors — the same posture as the CF marker check.
|
|
888
|
+
* Shell tag-generation gate (SegmentCacheStore.isTagsInvalidatedSince).
|
|
889
|
+
* expireTag() keeps no queryable history, so invalidateTags() writes "tm"
|
|
890
|
+
* markers for runtime capture races and immutable build shells. Marker >=
|
|
891
|
+
* generation start wins; read errors fail open like the CF marker check.
|
|
863
892
|
*/
|
|
864
893
|
async isTagsInvalidatedSince(
|
|
865
894
|
tags: string[],
|
|
@@ -1163,7 +1192,7 @@ export class VercelCacheStore<
|
|
|
1163
1192
|
|
|
1164
1193
|
private asShellEnvelope(raw: unknown): VercelShellEnvelope | null {
|
|
1165
1194
|
if (!isRecord(raw)) return null;
|
|
1166
|
-
const { p, po, rv, bv, c, s, e, t, i, sn, lh } = raw;
|
|
1195
|
+
const { p, po, rv, bv, c, s, e, t, i, sn, lh, tw } = raw;
|
|
1167
1196
|
if (typeof p !== "string" || typeof rv !== "string") return null;
|
|
1168
1197
|
if (po !== null && typeof po !== "string") return null;
|
|
1169
1198
|
if (typeof c !== "number") return null;
|
|
@@ -1180,6 +1209,7 @@ export class VercelCacheStore<
|
|
|
1180
1209
|
i: typeof i === "string" ? i : undefined,
|
|
1181
1210
|
sn: Array.isArray(sn) ? (sn as ShellSnapshotRecord[]) : undefined,
|
|
1182
1211
|
lh: lh === true ? true : undefined,
|
|
1212
|
+
tw: tw === true ? true : undefined,
|
|
1183
1213
|
};
|
|
1184
1214
|
}
|
|
1185
1215
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const DEV_DISCOVERY_READY_EVENT = "rango:dev-discovery-ready";
|
|
2
|
+
|
|
3
|
+
export const DEV_DISCOVERY_QUERY_EVENT = "rango:dev-discovery-query";
|
|
4
|
+
|
|
5
|
+
export const DEV_DISCOVERY_PROBE_HEADER = "x-rango-dev-discovery-probe";
|
|
6
|
+
|
|
7
|
+
export const DEV_DISCOVERY_EPOCH_HEADER = "x-rango-dev-discovery-epoch";
|
|
8
|
+
|
|
9
|
+
export function isValidDevDiscoveryEpoch(value: unknown): value is number {
|
|
10
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
11
|
+
}
|
|
@@ -44,7 +44,7 @@ import { isRouteNotFoundError } from "../errors.js";
|
|
|
44
44
|
import { createReverseFunction } from "../router/handler-context.js";
|
|
45
45
|
import { executeMiddleware, matchMiddleware } from "../router/middleware.js";
|
|
46
46
|
import type { MiddlewareEntry } from "../router/middleware.js";
|
|
47
|
-
import { getGlobalRouteMap } from "../route-map-builder.js";
|
|
47
|
+
import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
|
|
48
48
|
import {
|
|
49
49
|
resolvePprConfig,
|
|
50
50
|
type ResolvedPprConfig,
|
|
@@ -183,6 +183,9 @@ async function attemptBuildCapture(
|
|
|
183
183
|
stateCookieName: router.resolvedStateCookieName,
|
|
184
184
|
version: opts.buildVersion,
|
|
185
185
|
});
|
|
186
|
+
// Scope registry lookups (root-scope/search-schema) per router during the
|
|
187
|
+
// bake, mirroring rsc/handler.ts on the request path (#762).
|
|
188
|
+
baseCtx._routerId = router.id;
|
|
186
189
|
|
|
187
190
|
// Entry collector: captureAndStoreShell's sink. putShell never fails here,
|
|
188
191
|
// so a "stored" outcome always carries the entry.
|
|
@@ -235,6 +238,9 @@ async function attemptBuildCapture(
|
|
|
235
238
|
getGlobalRouteMap(),
|
|
236
239
|
preview?.routeKey,
|
|
237
240
|
preview?.params ?? {},
|
|
241
|
+
preview?.routeKey
|
|
242
|
+
? isRouteRootScoped(preview.routeKey, router.id)
|
|
243
|
+
: undefined,
|
|
238
244
|
);
|
|
239
245
|
|
|
240
246
|
const runCapture = () =>
|
package/src/prerender/store.ts
CHANGED
|
@@ -54,6 +54,11 @@ declare global {
|
|
|
54
54
|
var __STATIC_MANIFEST:
|
|
55
55
|
| Record<string, () => Promise<{ default: string | StaticEntry }>>
|
|
56
56
|
| undefined;
|
|
57
|
+
// Injected by closeBundle post-processing: lazy loader for the static
|
|
58
|
+
// manifest module (which assigns __STATIC_MANIFEST on evaluation). Resolved
|
|
59
|
+
// by static-store.ts on the first Static() lookup — issue #760.
|
|
60
|
+
// eslint-disable-next-line no-var
|
|
61
|
+
var __loadStaticManifestModule: (() => Promise<unknown>) | undefined;
|
|
57
62
|
// Injected by virtual module in dev mode for on-demand prerender
|
|
58
63
|
// eslint-disable-next-line no-var
|
|
59
64
|
var __PRERENDER_DEV_URL: string | undefined;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getNamePrefix,
|
|
13
13
|
getUrlPrefix,
|
|
14
14
|
requireDslContext,
|
|
15
|
+
stampStaticDefScope,
|
|
15
16
|
type EntryData,
|
|
16
17
|
type EntryPropDatas,
|
|
17
18
|
type EntryPropSegments,
|
|
@@ -560,6 +561,7 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
560
561
|
if (ctx.namePrefix) {
|
|
561
562
|
(slotHandler as any).$$routePrefix = ctx.namePrefix;
|
|
562
563
|
}
|
|
564
|
+
stampStaticDefScope(slotHandler);
|
|
563
565
|
}
|
|
564
566
|
} else {
|
|
565
567
|
unwrappedSlots[slotName] = slotHandler;
|
|
@@ -1016,8 +1018,11 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
1016
1018
|
} satisfies EntryData;
|
|
1017
1019
|
|
|
1018
1020
|
// Capture namespace prefix on static handler for build-time reverse() resolution
|
|
1019
|
-
if (isStatic && handler.$$id
|
|
1020
|
-
(
|
|
1021
|
+
if (isStatic && handler.$$id) {
|
|
1022
|
+
if (ctx.namePrefix) {
|
|
1023
|
+
(handler as any).$$routePrefix = ctx.namePrefix;
|
|
1024
|
+
}
|
|
1025
|
+
stampStaticDefScope(handler);
|
|
1021
1026
|
}
|
|
1022
1027
|
|
|
1023
1028
|
// Merge handler.use defaults with explicit use
|
package/src/route-map-builder.ts
CHANGED
|
@@ -12,11 +12,6 @@ let globalRouteMap: Record<string, string> = {};
|
|
|
12
12
|
|
|
13
13
|
let cachedManifest: Record<string, string> | null = null;
|
|
14
14
|
|
|
15
|
-
let cachedPrecomputedEntries: Array<{
|
|
16
|
-
staticPrefix: string;
|
|
17
|
-
routes: Record<string, string>;
|
|
18
|
-
}> | null = null;
|
|
19
|
-
|
|
20
15
|
/**
|
|
21
16
|
* Register routes into the global route map.
|
|
22
17
|
* Routes are merged with any existing registered routes.
|
|
@@ -87,43 +82,6 @@ export function clearCachedManifest(): void {
|
|
|
87
82
|
cachedManifest = null;
|
|
88
83
|
}
|
|
89
84
|
|
|
90
|
-
/**
|
|
91
|
-
* Set pre-computed route entries from build-time data.
|
|
92
|
-
*
|
|
93
|
-
* Each entry corresponds to a leaf node in the prefix tree (no nested includes).
|
|
94
|
-
* evaluateLazyEntry() checks these before running the handler, avoiding the
|
|
95
|
-
* 5-50ms cost of handler evaluation for route matching on the first request.
|
|
96
|
-
*
|
|
97
|
-
* @param entries - Array of { staticPrefix, routes } from build-time prefix tree leaves
|
|
98
|
-
*/
|
|
99
|
-
export function setPrecomputedEntries(
|
|
100
|
-
entries: Array<{
|
|
101
|
-
staticPrefix: string;
|
|
102
|
-
routes: Record<string, string>;
|
|
103
|
-
}> | null,
|
|
104
|
-
): void {
|
|
105
|
-
cachedPrecomputedEntries = entries;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Get pre-computed route entries (if available)
|
|
110
|
-
*/
|
|
111
|
-
export function getPrecomputedEntries(): typeof cachedPrecomputedEntries {
|
|
112
|
-
return cachedPrecomputedEntries;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Route trie for O(path_length) matching at runtime.
|
|
116
|
-
// Built at build time from the route manifest and serialized into the virtual module.
|
|
117
|
-
let cachedRouteTrie: import("./build/route-trie.js").TrieNode | null = null;
|
|
118
|
-
|
|
119
|
-
export function setRouteTrie(trie: typeof cachedRouteTrie): void {
|
|
120
|
-
cachedRouteTrie = trie;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function getRouteTrie(): typeof cachedRouteTrie {
|
|
124
|
-
return cachedRouteTrie;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
85
|
// Per-router isolated data: each router gets its own manifest, trie, and
|
|
128
86
|
// precomputed entries so multi-router setups (e.g. site + admin via
|
|
129
87
|
// createHostRouter()) don't see each other's routes.
|
|
@@ -146,14 +104,18 @@ const perRouterPrecomputedEntriesMap: Map<
|
|
|
146
104
|
export function clearAllRouterData(): void {
|
|
147
105
|
globalRouteMap = {};
|
|
148
106
|
cachedManifest = null;
|
|
149
|
-
cachedPrecomputedEntries = null;
|
|
150
|
-
cachedRouteTrie = null;
|
|
151
107
|
rootScopeRoutes.clear();
|
|
108
|
+
perRouterRootScopeRoutes.clear();
|
|
152
109
|
globalSearchSchemas.clear();
|
|
110
|
+
perRouterSearchSchemas.clear();
|
|
153
111
|
perRouterManifestMap.clear();
|
|
154
112
|
perRouterTrieMap.clear();
|
|
155
113
|
perRouterPrecomputedEntriesMap.clear();
|
|
156
114
|
authoritativeTrieRouters.clear();
|
|
115
|
+
// Clear the loader registry too: a loader that survives a clear closes over
|
|
116
|
+
// a module import whose data predates the clear, and re-running it would
|
|
117
|
+
// re-install that stale trie as authoritative.
|
|
118
|
+
routerManifestLoaders.clear();
|
|
157
119
|
}
|
|
158
120
|
|
|
159
121
|
export function setRouterManifest(
|
|
@@ -229,9 +191,9 @@ export async function ensureRouterManifest(routerId: string): Promise<void> {
|
|
|
229
191
|
// name->path map is pre-set by the eager virtual module's
|
|
230
192
|
// setRouterManifest() and never comes from the loader). Gating on the
|
|
231
193
|
// manifest instead would let the loader be skipped while the per-router
|
|
232
|
-
// trie is missing, and findMatch would fall back to
|
|
233
|
-
//
|
|
234
|
-
//
|
|
194
|
+
// trie is missing, and findMatch would fall back to insertion-order regex
|
|
195
|
+
// matching — which mishandles wildcard priority (catch-alls match before
|
|
196
|
+
// specific routes).
|
|
235
197
|
if (perRouterTrieMap.has(routerId)) return;
|
|
236
198
|
const loader = routerManifestLoaders.get(routerId);
|
|
237
199
|
if (loader) {
|
|
@@ -265,7 +227,16 @@ export function waitForManifestReady(): Promise<void> | null {
|
|
|
265
227
|
// Tracks whether each route is at root scope (no named include boundary above).
|
|
266
228
|
// Used by dot-local reverse resolution to decide whether bare-name fallback
|
|
267
229
|
// is allowed after scoped lookups are exhausted.
|
|
230
|
+
//
|
|
231
|
+
// Both this and the search-schema registry below are keyed by route NAME and
|
|
232
|
+
// registered by path() at evaluation time. Two routers can legally declare the
|
|
233
|
+
// same route name (per-router manifests keep them apart), so each registry has
|
|
234
|
+
// a per-router tier consulted first; the global tier is the fallback for
|
|
235
|
+
// contexts with no router identity (single-router apps, unit tests, evaluation
|
|
236
|
+
// outside generateManifestFull). Without the per-router tier, the
|
|
237
|
+
// last-evaluated router silently won for BOTH routers.
|
|
268
238
|
const rootScopeRoutes: Map<string, boolean> = new Map();
|
|
239
|
+
const perRouterRootScopeRoutes: Map<string, Map<string, boolean>> = new Map();
|
|
269
240
|
|
|
270
241
|
/**
|
|
271
242
|
* Register whether a route is at root scope.
|
|
@@ -274,29 +245,65 @@ const rootScopeRoutes: Map<string, boolean> = new Map();
|
|
|
274
245
|
export function registerRouteRootScope(
|
|
275
246
|
routeName: string,
|
|
276
247
|
rootScoped: boolean,
|
|
248
|
+
routerId?: string,
|
|
277
249
|
): void {
|
|
278
250
|
rootScopeRoutes.set(routeName, rootScoped);
|
|
251
|
+
if (routerId) {
|
|
252
|
+
let perRouter = perRouterRootScopeRoutes.get(routerId);
|
|
253
|
+
if (!perRouter) {
|
|
254
|
+
perRouter = new Map();
|
|
255
|
+
perRouterRootScopeRoutes.set(routerId, perRouter);
|
|
256
|
+
}
|
|
257
|
+
perRouter.set(routeName, rootScoped);
|
|
258
|
+
}
|
|
279
259
|
}
|
|
280
260
|
|
|
281
261
|
/**
|
|
282
262
|
* Check if a route is at root scope.
|
|
283
263
|
* Returns undefined if the route has not been registered (e.g. in unit tests).
|
|
284
264
|
*/
|
|
285
|
-
export function isRouteRootScoped(
|
|
265
|
+
export function isRouteRootScoped(
|
|
266
|
+
routeName: string,
|
|
267
|
+
routerId?: string,
|
|
268
|
+
): boolean | undefined {
|
|
269
|
+
if (routerId) {
|
|
270
|
+
const scoped = perRouterRootScopeRoutes.get(routerId)?.get(routeName);
|
|
271
|
+
if (scoped !== undefined) return scoped;
|
|
272
|
+
}
|
|
286
273
|
return rootScopeRoutes.get(routeName);
|
|
287
274
|
}
|
|
288
275
|
|
|
289
276
|
import type { SearchSchema } from "./search-params.js";
|
|
290
277
|
|
|
291
278
|
const globalSearchSchemas: Map<string, SearchSchema> = new Map();
|
|
279
|
+
const perRouterSearchSchemas: Map<
|
|
280
|
+
string,
|
|
281
|
+
Map<string, SearchSchema>
|
|
282
|
+
> = new Map();
|
|
292
283
|
|
|
293
284
|
export function registerSearchSchema(
|
|
294
285
|
routeName: string,
|
|
295
286
|
schema: SearchSchema,
|
|
287
|
+
routerId?: string,
|
|
296
288
|
): void {
|
|
297
289
|
globalSearchSchemas.set(routeName, schema);
|
|
290
|
+
if (routerId) {
|
|
291
|
+
let perRouter = perRouterSearchSchemas.get(routerId);
|
|
292
|
+
if (!perRouter) {
|
|
293
|
+
perRouter = new Map();
|
|
294
|
+
perRouterSearchSchemas.set(routerId, perRouter);
|
|
295
|
+
}
|
|
296
|
+
perRouter.set(routeName, schema);
|
|
297
|
+
}
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
export function getSearchSchema(
|
|
300
|
+
export function getSearchSchema(
|
|
301
|
+
routeName: string,
|
|
302
|
+
routerId?: string,
|
|
303
|
+
): SearchSchema | undefined {
|
|
304
|
+
if (routerId) {
|
|
305
|
+
const schema = perRouterSearchSchemas.get(routerId)?.get(routeName);
|
|
306
|
+
if (schema !== undefined) return schema;
|
|
307
|
+
}
|
|
301
308
|
return globalSearchSchemas.get(routeName);
|
|
302
309
|
}
|
|
@@ -96,7 +96,7 @@ function resolveRouteName(
|
|
|
96
96
|
// Resolution order: explicit param > scope registry > heuristic.
|
|
97
97
|
const isRootScoped =
|
|
98
98
|
rootScoped ??
|
|
99
|
-
isRouteRootScoped(currentRoutePrefix) ??
|
|
99
|
+
isRouteRootScoped(currentRoutePrefix, _getRequestContext()?._routerId) ??
|
|
100
100
|
!currentRoutePrefix.includes(".");
|
|
101
101
|
if (isRootScoped) {
|
|
102
102
|
if (routeMap[lookupName] !== undefined) {
|
|
@@ -208,7 +208,9 @@ export function createHandlerContext<TEnv>(
|
|
|
208
208
|
const variables: any = requestContext?._variables ?? {};
|
|
209
209
|
|
|
210
210
|
// If route has a search schema, parse URLSearchParams into typed object
|
|
211
|
-
const searchSchema = routeName
|
|
211
|
+
const searchSchema = routeName
|
|
212
|
+
? getSearchSchema(routeName, requestContext?._routerId)
|
|
213
|
+
: undefined;
|
|
212
214
|
const resolvedSearchParams = searchSchema
|
|
213
215
|
? parseSearchParams(searchParams, searchSchema)
|
|
214
216
|
: searchParams;
|
|
@@ -306,7 +308,9 @@ export function createHandlerContext<TEnv>(
|
|
|
306
308
|
routeMap,
|
|
307
309
|
routeName,
|
|
308
310
|
params,
|
|
309
|
-
routeName
|
|
311
|
+
routeName
|
|
312
|
+
? isRouteRootScoped(routeName, requestContext?._routerId)
|
|
313
|
+
: undefined,
|
|
310
314
|
),
|
|
311
315
|
passthrough: createPrerenderPassthroughFn(false, isPassthroughRoute),
|
|
312
316
|
_responseType: responseType,
|
|
@@ -399,7 +403,9 @@ export function createPrerenderContext<TEnv>(
|
|
|
399
403
|
routeMap,
|
|
400
404
|
routeName,
|
|
401
405
|
params,
|
|
402
|
-
routeName
|
|
406
|
+
routeName
|
|
407
|
+
? isRouteRootScoped(routeName, _getRequestContext()?._routerId)
|
|
408
|
+
: undefined,
|
|
403
409
|
),
|
|
404
410
|
passthrough: createPrerenderPassthroughFn(
|
|
405
411
|
true,
|
|
@@ -496,7 +502,9 @@ export function createStaticContext<TEnv>(
|
|
|
496
502
|
routeMap,
|
|
497
503
|
routeName,
|
|
498
504
|
undefined,
|
|
499
|
-
routeName
|
|
505
|
+
routeName
|
|
506
|
+
? isRouteRootScoped(routeName, _getRequestContext()?._routerId)
|
|
507
|
+
: undefined,
|
|
500
508
|
),
|
|
501
509
|
_routeName: routeName,
|
|
502
510
|
} as InternalHandlerContext<any, TEnv>;
|
package/src/router/match-api.ts
CHANGED
|
@@ -184,9 +184,8 @@ export async function createMatchContextForFull<TEnv>(
|
|
|
184
184
|
},
|
|
185
185
|
isSameRouteNavigation: false,
|
|
186
186
|
interceptResult: null,
|
|
187
|
-
// Shell fast path: a capture or an eligible HIT
|
|
188
|
-
// implicit doc-level scope (
|
|
189
|
-
// Full matches only — partial navigations never serve a shell.
|
|
187
|
+
// Shell fast path: a capture or an eligible document/navigation HIT may
|
|
188
|
+
// substitute an implicit doc-level scope (a route-derived scope wins).
|
|
190
189
|
cacheScope: resolveShellImplicitCacheScope(snapshot.cacheScope),
|
|
191
190
|
isIntercept: false,
|
|
192
191
|
actionContext: undefined,
|
|
@@ -426,7 +425,12 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
426
425
|
interceptSelectorContext,
|
|
427
426
|
isSameRouteNavigation: nav.isSameRouteNavigation,
|
|
428
427
|
interceptResult,
|
|
429
|
-
|
|
428
|
+
// A normal-route navigation may replay a PPR shell's canonical segment
|
|
429
|
+
// snapshot. Intercepts remain source-dependent and always use their normal
|
|
430
|
+
// cache path.
|
|
431
|
+
cacheScope: isIntercept
|
|
432
|
+
? snapshot.cacheScope
|
|
433
|
+
: resolveShellImplicitCacheScope(snapshot.cacheScope),
|
|
430
434
|
isIntercept,
|
|
431
435
|
actionContext,
|
|
432
436
|
isAction,
|
|
@@ -34,6 +34,9 @@ import type {
|
|
|
34
34
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
35
35
|
|
|
36
36
|
export interface PrerenderMatchDeps<TEnv = any> {
|
|
37
|
+
/** Owning router id; scopes bake-time root-scope/search-schema lookups the
|
|
38
|
+
* same way reqCtx._routerId does at runtime (issue #762). */
|
|
39
|
+
routerId?: string;
|
|
37
40
|
findMatch: (
|
|
38
41
|
pathname: string,
|
|
39
42
|
) => RouteMatchResult<TEnv> | null | Promise<RouteMatchResult<TEnv> | null>;
|
|
@@ -244,8 +247,13 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
244
247
|
deps.mergedRouteMap,
|
|
245
248
|
matched.routeKey,
|
|
246
249
|
matchedParams,
|
|
247
|
-
matched.routeKey
|
|
250
|
+
matched.routeKey
|
|
251
|
+
? isRouteRootScoped(matched.routeKey, deps.routerId)
|
|
252
|
+
: undefined,
|
|
248
253
|
),
|
|
254
|
+
// Scope the bake context's own lookups (createPrerenderContext reads it
|
|
255
|
+
// through the request ALS) per router as well.
|
|
256
|
+
_routerId: deps.routerId,
|
|
249
257
|
};
|
|
250
258
|
|
|
251
259
|
return runWithRequestContext(minimalRequestContext, async () => {
|
|
@@ -464,6 +472,8 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
464
472
|
routeName?: string,
|
|
465
473
|
buildEnv?: TEnv,
|
|
466
474
|
devMode?: boolean,
|
|
475
|
+
routerId?: string,
|
|
476
|
+
rootScoped?: boolean,
|
|
467
477
|
): Promise<{ encoded: string; handles: string } | null> {
|
|
468
478
|
const syntheticUrl = new URL("http://prerender/");
|
|
469
479
|
const syntheticRequest = new Request(syntheticUrl);
|
|
@@ -514,8 +524,15 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
514
524
|
mergedRouteMap,
|
|
515
525
|
routeName,
|
|
516
526
|
{},
|
|
517
|
-
|
|
527
|
+
// Def-stamped value first: the collector passes the mount-time
|
|
528
|
+
// root-scope captured on the Static definition (stampStaticDefScope),
|
|
529
|
+
// which needs no registry lookup and survives name-prefix arguments.
|
|
530
|
+
rootScoped ??
|
|
531
|
+
(routeName ? isRouteRootScoped(routeName, routerId) : undefined),
|
|
518
532
|
),
|
|
533
|
+
// Scope the bake context's own lookups (createStaticContext reads it
|
|
534
|
+
// through the request ALS) per router as well.
|
|
535
|
+
_routerId: routerId,
|
|
519
536
|
};
|
|
520
537
|
|
|
521
538
|
return runWithRequestContext(minimalRequestContext, async () => {
|
|
@@ -419,6 +419,9 @@ export interface RangoInternal<
|
|
|
419
419
|
/** @internal basename for runtime manifest generation */
|
|
420
420
|
readonly __basename?: string;
|
|
421
421
|
|
|
422
|
+
/** @internal Cloudflare dev worker generation captured at construction. */
|
|
423
|
+
readonly __devDiscoveryEpoch?: number;
|
|
424
|
+
|
|
422
425
|
/**
|
|
423
426
|
* @internal Router-level error/notFound fallbacks (`createRouter` options),
|
|
424
427
|
* exposed for the build-time clientChunks discovery so a `"use client"`
|
|
@@ -469,6 +472,7 @@ export interface RangoInternal<
|
|
|
469
472
|
routeName?: string,
|
|
470
473
|
buildEnv?: any,
|
|
471
474
|
devMode?: boolean,
|
|
475
|
+
rootScoped?: boolean,
|
|
472
476
|
): Promise<{ encoded: string; handles: string } | null>;
|
|
473
477
|
|
|
474
478
|
/**
|
|
@@ -11,14 +11,19 @@ import type { StaticStore } from "../../prerender/store.js";
|
|
|
11
11
|
import type { SegmentHandleData } from "../../cache/types.js";
|
|
12
12
|
|
|
13
13
|
// Lazy-initialized static store for production Static handler interception.
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// in workerd/Cloudflare runtime.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
// undefined = not yet checked; null = checked, no manifest (dev mode, or an
|
|
15
|
+
// app without Static handlers) — a permanent SYNCHRONOUS fast path: no await
|
|
16
|
+
// may ever run on that path (an await per lookup disrupts AsyncLocalStorage
|
|
17
|
+
// in workerd/Cloudflare runtime).
|
|
18
|
+
//
|
|
19
|
+
// The check happens on the FIRST LOOKUP, not at module eval: the manifest
|
|
20
|
+
// loader global (__loadStaticManifestModule, bundle-postprocess.ts) is
|
|
21
|
+
// assigned by the RSC entry's BODY, which runs after hoisted module imports
|
|
22
|
+
// evaluate this file — an eval-time read here could never see it. The
|
|
23
|
+
// pre-#760 eager `import "./__static-manifest.js"` existed precisely to beat
|
|
24
|
+
// that hoisting; deferring the manifest module keeps its O(#Static handlers)
|
|
25
|
+
// thunk table off the worker's cold-start path.
|
|
26
|
+
let _staticStore: StaticStore | null | undefined = undefined;
|
|
22
27
|
let _deserializeComponent: ((encoded: string) => Promise<unknown>) | undefined;
|
|
23
28
|
let _decodeHandleValue:
|
|
24
29
|
| typeof import("../../cache/handle-snapshot.js").decodeHandleValue
|
|
@@ -26,6 +31,18 @@ let _decodeHandleValue:
|
|
|
26
31
|
|
|
27
32
|
async function ensureStaticDeps(): Promise<void> {
|
|
28
33
|
if (_staticStore === undefined) {
|
|
34
|
+
// Evaluate the deferred manifest module (it assigns __STATIC_MANIFEST);
|
|
35
|
+
// support a pre-set __STATIC_MANIFEST too (tests, non-postprocess embeds).
|
|
36
|
+
if (
|
|
37
|
+
!globalThis.__STATIC_MANIFEST &&
|
|
38
|
+
globalThis.__loadStaticManifestModule
|
|
39
|
+
) {
|
|
40
|
+
await globalThis.__loadStaticManifestModule();
|
|
41
|
+
}
|
|
42
|
+
if (!globalThis.__STATIC_MANIFEST) {
|
|
43
|
+
_staticStore = null;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
29
46
|
const { createStaticStore } = await import("../../prerender/store.js");
|
|
30
47
|
_staticStore = createStaticStore();
|
|
31
48
|
}
|
|
@@ -52,8 +69,17 @@ export async function tryStaticLookup(
|
|
|
52
69
|
handlerId: string,
|
|
53
70
|
segmentId: string,
|
|
54
71
|
): Promise<ReactNode | undefined> {
|
|
55
|
-
// Fast path:
|
|
56
|
-
//
|
|
72
|
+
// Fast path: no manifest available (dev mode or no Static handlers).
|
|
73
|
+
// Latched SYNCHRONOUSLY — including on the very first call — so this path
|
|
74
|
+
// never awaits (an await here disrupts AsyncLocalStorage in
|
|
75
|
+
// workerd/Cloudflare runtime).
|
|
76
|
+
if (
|
|
77
|
+
_staticStore === undefined &&
|
|
78
|
+
!globalThis.__STATIC_MANIFEST &&
|
|
79
|
+
!globalThis.__loadStaticManifestModule
|
|
80
|
+
) {
|
|
81
|
+
_staticStore = null;
|
|
82
|
+
}
|
|
57
83
|
if (_staticStore === null) return undefined;
|
|
58
84
|
await ensureStaticDeps();
|
|
59
85
|
if (!_staticStore || !_deserializeComponent) return undefined;
|