@rangojs/router 0.1.0 → 0.2.0
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/README.md +2 -1
- 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/document-cache.d.ts +3 -1
- 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 +25 -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-definition/helpers-types.d.ts +6 -6
- package/dist/types/route-map-builder.d.ts +4 -29
- package/dist/types/router/match-handlers.d.ts +2 -3
- package/dist/types/router/prerender-match.d.ts +4 -1
- package/dist/types/router/router-interfaces.d.ts +3 -1
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +3 -4
- package/dist/types/router/transition-when.d.ts +13 -0
- 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/shell-serve.d.ts +2 -0
- package/dist/types/rsc/transition-gate.d.ts +10 -14
- 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 +22 -8
- package/dist/types/server.d.ts +1 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/index.d.ts +2 -2
- package/dist/types/testing/run-transition-when.d.ts +6 -5
- package/dist/types/testing/shell-status.d.ts +23 -3
- package/dist/types/types/route-entry.d.ts +3 -0
- package/dist/types/types/segments.d.ts +27 -22
- package/dist/types/urls/path-helper-types.d.ts +4 -3
- 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 +24 -20
- package/skills/cache-guide/SKILL.md +6 -3
- package/skills/caching/SKILL.md +1 -1
- package/skills/catalog.json +7 -1
- package/skills/deployment-caching/SKILL.md +176 -0
- package/skills/document-cache/SKILL.md +30 -3
- package/skills/ppr/SKILL.md +90 -14
- package/skills/prerender/SKILL.md +15 -8
- package/skills/rango/SKILL.md +20 -17
- package/skills/testing/SKILL.md +1 -1
- package/skills/testing/cache-prerender.md +5 -1
- package/skills/vercel/SKILL.md +22 -1
- package/skills/view-transitions/SKILL.md +12 -8
- 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 +23 -7
- package/src/cache/cf/cf-cache-store.ts +63 -39
- package/src/cache/document-cache.ts +4 -2
- package/src/cache/memory-segment-store.ts +17 -6
- package/src/cache/shell-snapshot.ts +45 -15
- package/src/cache/types.ts +24 -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-definition/helpers-types.ts +6 -6
- 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/match-handlers.ts +44 -6
- package/src/router/match-middleware/cache-lookup.ts +10 -3
- 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/segment-resolution/view-transition-default.ts +9 -5
- package/src/router/transition-when.ts +76 -0
- 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/progressive-enhancement.ts +9 -14
- package/src/rsc/render-pipeline.ts +10 -2
- package/src/rsc/rsc-rendering.ts +319 -147
- package/src/rsc/shell-capture.ts +34 -11
- package/src/rsc/shell-serve.ts +3 -0
- package/src/rsc/transition-gate.ts +37 -40
- package/src/rsc/types.ts +2 -0
- package/src/server/context.ts +28 -0
- package/src/server/request-context.ts +38 -10
- package/src/server.ts +0 -2
- package/src/testing/dispatch.ts +1 -0
- package/src/testing/e2e/index.ts +5 -0
- package/src/testing/index.ts +9 -1
- package/src/testing/run-transition-when.ts +42 -9
- package/src/testing/shell-status.ts +92 -3
- package/src/types/route-entry.ts +3 -0
- package/src/types/segments.ts +27 -22
- package/src/urls/include-helper.ts +1 -0
- package/src/urls/path-helper-types.ts +4 -3
- 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
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,
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
3
3
|
import { sanitizeError } from "../errors";
|
|
4
|
-
import type { ErrorInfo, ErrorPhase, MatchResult } from "../types";
|
|
5
4
|
import type {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
ErrorBoundaryHandler,
|
|
6
|
+
ErrorInfo,
|
|
7
|
+
ErrorPhase,
|
|
8
|
+
MatchResult,
|
|
9
|
+
NotFoundBoundaryHandler,
|
|
10
|
+
} from "../types";
|
|
11
|
+
import {
|
|
12
|
+
isPprEntry,
|
|
13
|
+
type EntryData,
|
|
14
|
+
type InterceptEntry,
|
|
15
|
+
type InterceptSelectorContext,
|
|
16
|
+
} from "../server/context.js";
|
|
10
17
|
import type { MatchApiDeps } from "./types.js";
|
|
11
18
|
import type { RouterContext } from "./router-context.js";
|
|
12
19
|
import { runWithRouterContext } from "./router-context.js";
|
|
@@ -30,7 +37,6 @@ import {
|
|
|
30
37
|
startRevalidationTrace,
|
|
31
38
|
flushRevalidationTrace,
|
|
32
39
|
} from "./logging.js";
|
|
33
|
-
import type { ErrorBoundaryHandler, NotFoundBoundaryHandler } from "../types";
|
|
34
40
|
import type { MiddlewareFn } from "./middleware.js";
|
|
35
41
|
import {
|
|
36
42
|
type TelemetrySink,
|
|
@@ -41,6 +47,7 @@ import {
|
|
|
41
47
|
buildCacheSignalSegments,
|
|
42
48
|
} from "./telemetry.js";
|
|
43
49
|
import { _getRequestContext } from "../server/request-context.js";
|
|
50
|
+
import { evaluatePprTransitionWhen } from "./transition-when.js";
|
|
44
51
|
|
|
45
52
|
/**
|
|
46
53
|
* Per-call telemetry lifecycle emitter for match()/matchPartial(). Each method
|
|
@@ -227,6 +234,35 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
227
234
|
if (reqCtx) reqCtx._cacheSignal = segments;
|
|
228
235
|
};
|
|
229
236
|
|
|
237
|
+
const evaluatePprTransitionWhenForMatch = (
|
|
238
|
+
ctx: MatchContext<TEnv>,
|
|
239
|
+
isPartial: boolean,
|
|
240
|
+
): void => {
|
|
241
|
+
const reqCtx = _getRequestContext();
|
|
242
|
+
if (!reqCtx) return;
|
|
243
|
+
reqCtx._pprTransitionDecisions = undefined;
|
|
244
|
+
if (!isPprEntry(ctx.manifestEntry)) return;
|
|
245
|
+
|
|
246
|
+
evaluatePprTransitionWhen(
|
|
247
|
+
ctx.entries,
|
|
248
|
+
reqCtx,
|
|
249
|
+
{
|
|
250
|
+
params: ctx.matched.params,
|
|
251
|
+
routeName: ctx.interceptSelectorContext.toRouteName,
|
|
252
|
+
},
|
|
253
|
+
(error, segmentId) =>
|
|
254
|
+
callOnError(error, "rendering", {
|
|
255
|
+
request: ctx.request,
|
|
256
|
+
url: ctx.url,
|
|
257
|
+
env: ctx.env,
|
|
258
|
+
params: ctx.matched.params,
|
|
259
|
+
segmentId,
|
|
260
|
+
isPartial,
|
|
261
|
+
handledByBoundary: false,
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
|
|
230
266
|
async function createMatchContextForFull(
|
|
231
267
|
request: Request,
|
|
232
268
|
env: TEnv,
|
|
@@ -313,6 +349,7 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
313
349
|
}
|
|
314
350
|
|
|
315
351
|
const ctx = result as MatchContext<TEnv>;
|
|
352
|
+
evaluatePprTransitionWhenForMatch(ctx, false);
|
|
316
353
|
|
|
317
354
|
try {
|
|
318
355
|
const state = createPipelineState();
|
|
@@ -424,6 +461,7 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
424
461
|
emitter.end(0, false);
|
|
425
462
|
return null;
|
|
426
463
|
}
|
|
464
|
+
evaluatePprTransitionWhenForMatch(ctx, true);
|
|
427
465
|
|
|
428
466
|
if (isRouterDebugEnabled()) {
|
|
429
467
|
startRevalidationTrace({
|
|
@@ -460,6 +460,8 @@ export function withCacheLookup<TEnv>(
|
|
|
460
460
|
state.shouldRevalidate = cacheResult.shouldRevalidate;
|
|
461
461
|
state.cachedSegments = cacheResult.segments;
|
|
462
462
|
state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
|
|
463
|
+
const pprTransitionDecisions =
|
|
464
|
+
_getRequestContext()?._pprTransitionDecisions;
|
|
463
465
|
|
|
464
466
|
const canCheckSegmentRevalidation =
|
|
465
467
|
!ctx.isFullMatch &&
|
|
@@ -533,8 +535,13 @@ export function withCacheLookup<TEnv>(
|
|
|
533
535
|
reason: "cached-no-rules",
|
|
534
536
|
});
|
|
535
537
|
}
|
|
536
|
-
|
|
537
|
-
segment.
|
|
538
|
+
// A PPR transition decision must reach the client even when this cached
|
|
539
|
+
// segment otherwise needs no update. Keep its replayed component so the
|
|
540
|
+
// partial result retains the segment for gateTransitions().
|
|
541
|
+
if (!pprTransitionDecisions?.has(segment.id)) {
|
|
542
|
+
segment.component = null;
|
|
543
|
+
segment.loading = undefined;
|
|
544
|
+
}
|
|
538
545
|
yield segment;
|
|
539
546
|
continue;
|
|
540
547
|
}
|
|
@@ -566,7 +573,7 @@ export function withCacheLookup<TEnv>(
|
|
|
566
573
|
shouldRevalidate,
|
|
567
574
|
});
|
|
568
575
|
|
|
569
|
-
if (!shouldRevalidate) {
|
|
576
|
+
if (!shouldRevalidate && !pprTransitionDecisions?.has(segment.id)) {
|
|
570
577
|
segment.component = null;
|
|
571
578
|
segment.loading = undefined;
|
|
572
579
|
}
|
|
@@ -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;
|
|
@@ -23,10 +23,9 @@ import { getRequestContext } from "../../server/request-context.js";
|
|
|
23
23
|
* gate, which also avoids needless object allocation and payload growth.
|
|
24
24
|
*
|
|
25
25
|
* `when`: a server-only predicate. It is STRIPPED from the returned config (a
|
|
26
|
-
* function cannot cross Flight or the segment cache)
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* false. Used by both the fresh and revalidation resolution paths.
|
|
26
|
+
* function cannot cross Flight or the segment cache). PPR routes evaluate it
|
|
27
|
+
* from the manifest before the pipeline; other routes record it here for the
|
|
28
|
+
* existing post-handler gate. Used by both fresh and revalidation resolution.
|
|
30
29
|
*/
|
|
31
30
|
export function applyViewTransitionDefault(
|
|
32
31
|
transition: EntryData["transition"],
|
|
@@ -39,7 +38,12 @@ export function applyViewTransitionDefault(
|
|
|
39
38
|
if (segmentId !== undefined) {
|
|
40
39
|
try {
|
|
41
40
|
const ctx = getRequestContext();
|
|
42
|
-
(ctx.
|
|
41
|
+
if (!ctx._pprTransitionDecisions?.has(segmentId)) {
|
|
42
|
+
(ctx._transitionWhen ??= []).push({
|
|
43
|
+
id: segmentId,
|
|
44
|
+
when: result.when,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
43
47
|
} catch {
|
|
44
48
|
// No active request context (e.g. a unit test calling this util
|
|
45
49
|
// directly). Skip collection; the strip below still applies so the
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getParallelSlotEntries, type EntryData } from "../server/context.js";
|
|
2
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
3
|
+
import type { TransitionWhenContext } from "../types/segments.js";
|
|
4
|
+
|
|
5
|
+
export type TransitionWhenErrorReporter = (
|
|
6
|
+
error: unknown,
|
|
7
|
+
segmentId: string,
|
|
8
|
+
) => void;
|
|
9
|
+
|
|
10
|
+
export function createTransitionWhenContext<TEnv>(
|
|
11
|
+
ctx: RequestContext<TEnv>,
|
|
12
|
+
target?: {
|
|
13
|
+
params: Record<string, string>;
|
|
14
|
+
routeName?: string;
|
|
15
|
+
},
|
|
16
|
+
): TransitionWhenContext<Record<string, string>, TEnv> {
|
|
17
|
+
return {
|
|
18
|
+
currentUrl: ctx._gateCurrentUrl,
|
|
19
|
+
currentParams: ctx._gateCurrentParams,
|
|
20
|
+
fromRouteName: ctx._prevRouteKey as TransitionWhenContext["fromRouteName"],
|
|
21
|
+
nextUrl: ctx.url,
|
|
22
|
+
nextParams: target?.params ?? ctx.params,
|
|
23
|
+
toRouteName: (target?.routeName ??
|
|
24
|
+
ctx.routeName) as TransitionWhenContext["toRouteName"],
|
|
25
|
+
actionId: ctx._gateActionId,
|
|
26
|
+
actionUrl: ctx._gateActionUrl,
|
|
27
|
+
actionResult: ctx._gateActionResult,
|
|
28
|
+
formData: ctx._gateFormData,
|
|
29
|
+
method: ctx.request.method,
|
|
30
|
+
get: ctx.get,
|
|
31
|
+
env: ctx.env,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Evaluate server-only transition gates before handlers on a PPR route. */
|
|
36
|
+
export function evaluatePprTransitionWhen<TEnv>(
|
|
37
|
+
entries: EntryData[],
|
|
38
|
+
ctx: RequestContext<TEnv>,
|
|
39
|
+
target: {
|
|
40
|
+
params: Record<string, string>;
|
|
41
|
+
routeName?: string;
|
|
42
|
+
},
|
|
43
|
+
reportError: TransitionWhenErrorReporter,
|
|
44
|
+
): void {
|
|
45
|
+
const decisions = new Map<string, boolean>();
|
|
46
|
+
const whenContext = createTransitionWhenContext(ctx, target);
|
|
47
|
+
const visited = new Set<EntryData>();
|
|
48
|
+
|
|
49
|
+
const evaluate = (entry: EntryData, segmentId: string): void => {
|
|
50
|
+
const when = entry.transition?.when;
|
|
51
|
+
if (!when) return;
|
|
52
|
+
try {
|
|
53
|
+
decisions.set(segmentId, when(whenContext) !== false);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
decisions.set(segmentId, false);
|
|
56
|
+
reportError(error, segmentId);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const visitEntryAndOrphanLayouts = (entry: EntryData): void => {
|
|
61
|
+
if (visited.has(entry)) return;
|
|
62
|
+
visited.add(entry);
|
|
63
|
+
evaluate(entry, entry.shortCode);
|
|
64
|
+
|
|
65
|
+
for (const orphan of entry.layout) visitEntryAndOrphanLayouts(orphan);
|
|
66
|
+
// Rendered parallel slots are leaves identified by their owning segment.
|
|
67
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
68
|
+
entry.parallel,
|
|
69
|
+
)) {
|
|
70
|
+
evaluate(parallelEntry, `${entry.shortCode}.${slot}`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
for (const entry of entries) visitEntryAndOrphanLayouts(entry);
|
|
75
|
+
ctx._pprTransitionDecisions = decisions.size > 0 ? decisions : undefined;
|
|
76
|
+
}
|