@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
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* PPR shell-status testing primitives for @rangojs/router consumers.
|
|
3
3
|
*
|
|
4
4
|
* Companion to `cache-status.ts` (segment/document cache): this module covers
|
|
5
|
-
* the **shell axis** (`ppr` path option → `x-rango-shell: HIT | MISS`)
|
|
5
|
+
* the **shell axis** (`ppr` path option → `x-rango-shell: HIT | MISS`) and
|
|
6
|
+
* partial-navigation segment replay (`x-rango-ppr-replay: HIT`).
|
|
6
7
|
*
|
|
7
8
|
* ## Spike conclusions (plan 009)
|
|
8
9
|
*
|
|
@@ -11,8 +12,9 @@
|
|
|
11
12
|
* + `ppr: true` on a response route is a no-op for shell serve/capture. The
|
|
12
13
|
* production path lives in `rsc/rsc-rendering.ts` + `rsc/shell-capture.ts`.
|
|
13
14
|
*
|
|
14
|
-
* 2. **Smallest
|
|
15
|
-
*
|
|
15
|
+
* 2. **Smallest status signals:** `x-rango-shell` (`HIT` | `MISS`) on document
|
|
16
|
+
* GETs, and `x-rango-ppr-replay` on partial requests to ppr routes. A replay
|
|
17
|
+
* HIT is reported only when matching consumes the captured segment record.
|
|
16
18
|
* Secondary unit signal: `store.getShell(shellCacheKey(url))` after a real
|
|
17
19
|
* capture flush (background `putShell`). There is no Flight flag for shell HIT.
|
|
18
20
|
*
|
|
@@ -31,9 +33,37 @@ import { sortedSearchString } from "../cache/cache-key-utils.js";
|
|
|
31
33
|
/** Production header name (`rsc/shell-serve.ts` `SHELL_STATUS_HEADER`). */
|
|
32
34
|
export const SHELL_STATUS_HEADER: string = "x-rango-shell";
|
|
33
35
|
|
|
36
|
+
/** Partial-navigation replay decision header from `rsc/rsc-rendering.ts`. */
|
|
37
|
+
export const PPR_REPLAY_STATUS_HEADER: string = "x-rango-ppr-replay";
|
|
38
|
+
|
|
34
39
|
/** Values the serve path writes on `x-rango-shell`. */
|
|
35
40
|
export type ShellStatus = "HIT" | "MISS";
|
|
36
41
|
|
|
42
|
+
const PPR_REPLAY_BYPASS_REASONS = [
|
|
43
|
+
"method",
|
|
44
|
+
"dynamic",
|
|
45
|
+
"nonce",
|
|
46
|
+
"store-unavailable",
|
|
47
|
+
"passive-read-unsupported",
|
|
48
|
+
"read-error",
|
|
49
|
+
"no-entry",
|
|
50
|
+
"invalid-version",
|
|
51
|
+
"corrupt-entry",
|
|
52
|
+
"handler-live-holes",
|
|
53
|
+
"transition-when",
|
|
54
|
+
"no-segment-snapshot",
|
|
55
|
+
"snapshot-miss",
|
|
56
|
+
"stale-build-entry",
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
/** Bounded reasons a PPR partial request can fall open to ordinary matching. */
|
|
60
|
+
export type PprReplayBypassReason = (typeof PPR_REPLAY_BYPASS_REASONS)[number];
|
|
61
|
+
|
|
62
|
+
/** Parsed `x-rango-ppr-replay` value. */
|
|
63
|
+
export type PprReplayStatus =
|
|
64
|
+
| { outcome: "HIT"; freshness: "fresh" | "stale" }
|
|
65
|
+
| { outcome: "BYPASS"; reason: PprReplayBypassReason };
|
|
66
|
+
|
|
37
67
|
/** A target carrying response headers (a Response or a `{ headers }` object). */
|
|
38
68
|
export type ShellStatusTarget = Response | { headers: Headers };
|
|
39
69
|
|
|
@@ -58,6 +88,8 @@ function getHeaders(target: ShellStatusTarget): Headers {
|
|
|
58
88
|
return target.headers;
|
|
59
89
|
}
|
|
60
90
|
|
|
91
|
+
const PPR_REPLAY_BYPASS_REASON_SET = new Set<string>(PPR_REPLAY_BYPASS_REASONS);
|
|
92
|
+
|
|
61
93
|
/**
|
|
62
94
|
* Read `x-rango-shell` from a response. Returns `null` when the header is
|
|
63
95
|
* absent (axis-1 / non-ppr / ineligible request).
|
|
@@ -98,3 +130,60 @@ export function assertShellStatus(
|
|
|
98
130
|
);
|
|
99
131
|
}
|
|
100
132
|
}
|
|
133
|
+
|
|
134
|
+
/** Parse the dedicated PPR partial-navigation replay decision header. */
|
|
135
|
+
export function parsePprReplayStatus(
|
|
136
|
+
target: ShellStatusTarget,
|
|
137
|
+
): PprReplayStatus | null {
|
|
138
|
+
const raw = getHeaders(target).get(PPR_REPLAY_STATUS_HEADER)?.trim();
|
|
139
|
+
if (!raw) return null;
|
|
140
|
+
|
|
141
|
+
const [outcome, detail, ...extra] = raw.split(";").map((part) => part.trim());
|
|
142
|
+
if (extra.length > 0 || !detail) return null;
|
|
143
|
+
|
|
144
|
+
if (outcome === "HIT") {
|
|
145
|
+
const freshness = detail.match(/^freshness=(fresh|stale)$/)?.[1];
|
|
146
|
+
if (freshness === "fresh" || freshness === "stale") {
|
|
147
|
+
return { outcome, freshness };
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (outcome === "BYPASS") {
|
|
153
|
+
const reason = detail.match(/^reason=(.+)$/)?.[1] as
|
|
154
|
+
| PprReplayBypassReason
|
|
155
|
+
| undefined;
|
|
156
|
+
if (reason && PPR_REPLAY_BYPASS_REASON_SET.has(reason)) {
|
|
157
|
+
return { outcome, reason };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Assert a partial response's PPR replay outcome and freshness/reason. */
|
|
165
|
+
export function assertPprReplayStatus(
|
|
166
|
+
target: ShellStatusTarget,
|
|
167
|
+
expected: PprReplayStatus,
|
|
168
|
+
): void {
|
|
169
|
+
const raw = getHeaders(target).get(PPR_REPLAY_STATUS_HEADER);
|
|
170
|
+
if (raw === null) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`assertPprReplayStatus: response has no ${PPR_REPLAY_STATUS_HEADER} header. ` +
|
|
173
|
+
"The header is set on partial requests to ppr-declared routes.",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const actual = parsePprReplayStatus(target);
|
|
178
|
+
if (!actual) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`assertPprReplayStatus: unrecognized ${PPR_REPLAY_STATUS_HEADER} value "${raw}".`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`assertPprReplayStatus: expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}.`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
package/src/types/route-entry.ts
CHANGED
|
@@ -13,6 +13,9 @@ export interface LazyIncludeContext {
|
|
|
13
13
|
>;
|
|
14
14
|
/** Root scope flag for dot-local reverse resolution */
|
|
15
15
|
rootScoped?: boolean;
|
|
16
|
+
/** Owning router id; scopes search-schema/root-scope registration during
|
|
17
|
+
* lazy include evaluation (route-map-builder.ts registries) */
|
|
18
|
+
routerId?: string;
|
|
16
19
|
/**
|
|
17
20
|
* Positional include scope token composed from the parent scope plus this
|
|
18
21
|
* include's sibling index (`${parentScope}I${idx}`). Applied to direct-
|
package/src/types/segments.ts
CHANGED
|
@@ -14,11 +14,12 @@ export type ViewTransitionClass = Record<string, string> | string;
|
|
|
14
14
|
*
|
|
15
15
|
* It mirrors the {@link ShouldRevalidateFn} args a `revalidate()` predicate
|
|
16
16
|
* gets — the same navigation/action metadata — so the two read the same shape,
|
|
17
|
-
* plus `get`/`env` for
|
|
17
|
+
* plus `get`/`env` for request-context reads. There is no full `HandlerContext`
|
|
18
18
|
* here: the gate runs at the RSC-payload layer with the request context, not a
|
|
19
19
|
* handler context, so handler-only sugar (`search`/`build`/`dev`/`headers`) is
|
|
20
|
-
* absent by design. `get`
|
|
21
|
-
* via `ctx.set(...)`
|
|
20
|
+
* absent by design. On ordinary routes, `get` can read what handlers or
|
|
21
|
+
* middleware set via `ctx.set(...)`. On `ppr` routes the gate runs before route
|
|
22
|
+
* handlers, so only middleware-established values are available.
|
|
22
23
|
*
|
|
23
24
|
* Field availability (all source fields are optional — never fabricated):
|
|
24
25
|
* - `currentUrl` / `currentParams` / `fromRouteName` (the navigation SOURCE) are
|
|
@@ -43,11 +44,12 @@ export type ViewTransitionClass = Record<string, string> | string;
|
|
|
43
44
|
* time, so `currentUrl`/`currentParams`/`fromRouteName` reflect the page the
|
|
44
45
|
* prefetch fired from, NOT necessarily the page the user actually navigates from
|
|
45
46
|
* — the decision is baked into the stored Flight payload and replayed verbatim.
|
|
46
|
-
* A `cache()`/prerender hit replays the stored transition with the
|
|
47
|
-
* re-run
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* A non-PPR `cache()`/prerender hit replays the stored transition with the
|
|
48
|
+
* predicate NOT re-run. A `ppr` route instead evaluates the predicate before
|
|
49
|
+
* handlers on every match, including cache, prerender, and PPR replay. Prefetch
|
|
50
|
+
* still freezes the server decision into its Flight payload. If the gate must
|
|
51
|
+
* reflect the exact click-time source, source-scope the prefetch
|
|
52
|
+
* (`<Link prefetchKey=":source">`).
|
|
51
53
|
*/
|
|
52
54
|
export type TransitionWhenContext<
|
|
53
55
|
TParams = Record<string, string>,
|
|
@@ -74,17 +76,19 @@ export type TransitionWhenContext<
|
|
|
74
76
|
/**
|
|
75
77
|
* Predicate that gates whether a transition() applies for the current request.
|
|
76
78
|
*
|
|
77
|
-
* Evaluated server-side
|
|
78
|
-
* handler
|
|
79
|
-
*
|
|
79
|
+
* Evaluated server-side outside any cache scope. On ordinary routes it runs
|
|
80
|
+
* AFTER the route's handler, so `get(...)` can read handler/middleware state. A
|
|
81
|
+
* route with `ppr` automatically hoists it before route handlers and reevaluates
|
|
82
|
+
* it on every cache/prerender/PPR replay; there `get(...)` can read middleware
|
|
83
|
+
* state, but not values set by route handlers. Return false to drop this
|
|
84
|
+
* segment's transition for the request; return true to apply it. The
|
|
80
85
|
* context ({@link TransitionWhenContext}) carries the same navigation/action
|
|
81
86
|
* metadata a `revalidate()` predicate sees plus `get`/`env`. If it throws, the
|
|
82
87
|
* error is reported to the router's onError (phase "rendering") and the
|
|
83
88
|
* transition is dropped (the navigation does not hold).
|
|
84
89
|
*
|
|
85
90
|
* Distinct from intercept()'s `when` config selector, which runs at MATCH time
|
|
86
|
-
* over `{ from, to, params, segments, … }
|
|
87
|
-
* post-handler over the resolved payload.
|
|
91
|
+
* over `{ from, to, params, segments, … }`.
|
|
88
92
|
*
|
|
89
93
|
* Scope: dropping a transition removes only THIS segment's contribution to the
|
|
90
94
|
* navigation's hold. The startTransition hold is navigation-wide — it engages if
|
|
@@ -92,10 +96,10 @@ export type TransitionWhenContext<
|
|
|
92
96
|
* navigation stream its loading fallback only when no other matched segment
|
|
93
97
|
* keeps a transition (the common case: a single transition on the route).
|
|
94
98
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* {@link TransitionWhenContext}.
|
|
99
|
+
* On non-PPR routes it runs only during fresh resolution. On PPR routes it runs
|
|
100
|
+
* before handlers for every match, including runtime cache, build-time
|
|
101
|
+
* prerender, and PPR segment replay. A prefetched navigation still freezes the
|
|
102
|
+
* result to prefetch time; see {@link TransitionWhenContext}.
|
|
99
103
|
*/
|
|
100
104
|
export type TransitionWhenFn = (ctx: TransitionWhenContext) => boolean;
|
|
101
105
|
|
|
@@ -129,11 +133,12 @@ export interface TransitionConfig {
|
|
|
129
133
|
viewTransition?: "auto" | false;
|
|
130
134
|
/**
|
|
131
135
|
* Optional server-side predicate that gates this transition per request. When
|
|
132
|
-
* present and it returns false
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
136
|
+
* present and it returns false, the router drops this segment's transition for
|
|
137
|
+
* the request, so the navigation streams its loading fallback instead of
|
|
138
|
+
* holding. PPR routes evaluate it before route handlers and on every replay;
|
|
139
|
+
* other routes evaluate it after handlers on fresh resolution. The predicate
|
|
140
|
+
* is server-only and never serialized to the client; only its resolved effect
|
|
141
|
+
* crosses. See {@link TransitionWhenFn}.
|
|
137
142
|
*/
|
|
138
143
|
when?: TransitionWhenFn;
|
|
139
144
|
}
|
|
@@ -394,9 +394,10 @@ export type PathHelpers<TEnv> = {
|
|
|
394
394
|
* transition cannot fire without a startTransition. See
|
|
395
395
|
* skills/view-transitions for the startTransition x ViewTransition matrix.
|
|
396
396
|
*
|
|
397
|
-
* Pass `when: (ctx) => boolean` to gate the transition per request
|
|
398
|
-
*
|
|
399
|
-
*
|
|
397
|
+
* Pass `when: (ctx) => boolean` to gate the transition per request. It normally
|
|
398
|
+
* runs after the route handler; `ppr` routes automatically run it before route
|
|
399
|
+
* handlers and on every replay, where `ctx.get(...)` sees middleware state but
|
|
400
|
+
* not handler-set values.
|
|
400
401
|
*/
|
|
401
402
|
transition: {
|
|
402
403
|
(): TransitionItem;
|
package/src/urls/path-helper.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getNamePrefix,
|
|
7
7
|
getRootScoped,
|
|
8
8
|
requireDslContext,
|
|
9
|
+
stampStaticDefScope,
|
|
9
10
|
} from "../server/context";
|
|
10
11
|
import { invariant, DataNotFoundError } from "../errors";
|
|
11
12
|
import { validateUserRouteName } from "../route-name.js";
|
|
@@ -182,8 +183,11 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
182
183
|
: {}),
|
|
183
184
|
};
|
|
184
185
|
|
|
185
|
-
if (isStaticHandler(handler) && handler.$$id
|
|
186
|
-
(
|
|
186
|
+
if (isStaticHandler(handler) && handler.$$id) {
|
|
187
|
+
if (ctx.namePrefix) {
|
|
188
|
+
(handler as any).$$routePrefix = ctx.namePrefix;
|
|
189
|
+
}
|
|
190
|
+
stampStaticDefScope(handler, routeName);
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
invariant(
|
|
@@ -193,7 +197,7 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
193
197
|
|
|
194
198
|
ctx.manifest.set(routeName, entry);
|
|
195
199
|
|
|
196
|
-
registerRouteRootScope(routeName, getRootScoped());
|
|
200
|
+
registerRouteRootScope(routeName, getRootScoped(), ctx.routerId);
|
|
197
201
|
|
|
198
202
|
if (ctx.patterns) {
|
|
199
203
|
ctx.patterns.set(routeName, prefixedPattern);
|
|
@@ -215,7 +219,7 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
215
219
|
if (ctx.searchSchemas) {
|
|
216
220
|
ctx.searchSchemas.set(routeName, options.search);
|
|
217
221
|
}
|
|
218
|
-
registerSearchSchema(routeName, options.search);
|
|
222
|
+
registerSearchSchema(routeName, options.search, ctx.routerId);
|
|
219
223
|
}
|
|
220
224
|
|
|
221
225
|
if (mergedUse) {
|
|
@@ -133,7 +133,7 @@ export function postprocessBundle(state: DiscoveryState): void {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
// 3. Write static handler data as separate importable asset modules
|
|
136
|
-
// and inject a
|
|
136
|
+
// and inject a lazy loader thunk into the RSC entry.
|
|
137
137
|
if (hasStaticData && existsSync(rscEntryPath)) {
|
|
138
138
|
const rscCode = readFileSync(rscEntryPath, "utf-8");
|
|
139
139
|
if (!rscCode.includes("__static-manifest.js")) {
|
|
@@ -152,9 +152,10 @@ export function postprocessBundle(state: DiscoveryState): void {
|
|
|
152
152
|
);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
155
|
+
// The module assigns the global on evaluation; evaluation itself is
|
|
156
|
+
// DEFERRED behind the loader thunk below (mirroring the prerender and
|
|
157
|
+
// shell manifests), so the thunk table stays off the worker's eager
|
|
158
|
+
// cold-start path (issue #760) — it grows O(#Static handlers).
|
|
158
159
|
const manifestCode = `const m={${manifestEntries.join(",")}};globalThis.__STATIC_MANIFEST=m;export default m;\n`;
|
|
159
160
|
const manifestPath = resolve(
|
|
160
161
|
state.projectRoot,
|
|
@@ -163,9 +164,11 @@ export function postprocessBundle(state: DiscoveryState): void {
|
|
|
163
164
|
writeFileSync(manifestPath, manifestCode);
|
|
164
165
|
totalBytes += Buffer.byteLength(manifestCode);
|
|
165
166
|
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
|
|
167
|
+
// Lazy thunk, NOT an eager import: static-store.ts resolves it on the
|
|
168
|
+
// first Static() lookup (a first-call latch — the entry body assigning
|
|
169
|
+
// this global runs after hoisted imports, so an eval-time read in
|
|
170
|
+
// static-store.ts could never see it).
|
|
171
|
+
const injection = `globalThis.__loadStaticManifestModule = () => import("./__static-manifest.js");\n`;
|
|
169
172
|
writeFileSync(rscEntryPath, injection + rscCode);
|
|
170
173
|
|
|
171
174
|
const totalKB = (totalBytes / 1024).toFixed(1);
|
|
@@ -19,12 +19,9 @@ import {
|
|
|
19
19
|
// boundary tree regardless of which realm imported the walker. Only
|
|
20
20
|
// generateManifestFull must stay on the runner (it invokes user handlers via
|
|
21
21
|
// RangoContext from the runner realm) — see the runner.import below.
|
|
22
|
-
import {
|
|
22
|
+
import { buildPerRouterTrie } from "../../build/route-trie.js";
|
|
23
23
|
import { collectFallbackClientRefs } from "../../build/collect-fallback-refs.js";
|
|
24
|
-
import {
|
|
25
|
-
flattenLeafEntries,
|
|
26
|
-
buildRouteToStaticPrefix,
|
|
27
|
-
} from "../utils/manifest-utils.js";
|
|
24
|
+
import { flattenLeafEntries } from "../utils/manifest-utils.js";
|
|
28
25
|
import type { DiscoveryState, PrecomputedEntry } from "./state.js";
|
|
29
26
|
import {
|
|
30
27
|
expandPrerenderRoutes,
|
|
@@ -145,12 +142,10 @@ export async function discoverRouters(
|
|
|
145
142
|
// full pass succeeds, so a failed re-discovery preserves the last
|
|
146
143
|
// known-good state instead of leaving it partially wiped.
|
|
147
144
|
const newMergedRouteManifest: Record<string, string> = {};
|
|
148
|
-
const newMergedPrecomputedEntries: PrecomputedEntry[] = [];
|
|
149
145
|
const newPerRouterManifests: typeof state.perRouterManifests = [];
|
|
150
146
|
const newPerRouterManifestDataMap = new Map<string, any>();
|
|
151
147
|
const newPerRouterPrecomputedMap = new Map<string, PrecomputedEntry[]>();
|
|
152
148
|
const newPerRouterTrieMap = new Map<string, any>();
|
|
153
|
-
let mergedRouteTrailingSlash: Record<string, string> = {};
|
|
154
149
|
|
|
155
150
|
let routerMountIndex = 0;
|
|
156
151
|
// Collect all manifests for trie building (avoid re-running generateManifest)
|
|
@@ -189,6 +184,7 @@ export async function discoverRouters(
|
|
|
189
184
|
router.urlpatterns,
|
|
190
185
|
routerMountIndex,
|
|
191
186
|
{
|
|
187
|
+
routerId: id,
|
|
192
188
|
...(router.__basename ? { urlPrefix: router.__basename } : {}),
|
|
193
189
|
...(collectClientFallbackRef ? { collectClientFallbackRef } : {}),
|
|
194
190
|
},
|
|
@@ -239,29 +235,21 @@ export async function discoverRouters(
|
|
|
239
235
|
newPerRouterManifests.push({
|
|
240
236
|
id,
|
|
241
237
|
routeManifest: manifest.routeManifest,
|
|
238
|
+
routeTrailingSlash: manifest.routeTrailingSlash,
|
|
242
239
|
routeSearchSchemas: manifest.routeSearchSchemas,
|
|
243
240
|
sourceFile: router.__sourceFile,
|
|
244
241
|
factoryOnlyPrefixes,
|
|
245
242
|
});
|
|
246
243
|
|
|
247
|
-
// Merge trailing slash config
|
|
248
|
-
if (manifest.routeTrailingSlash) {
|
|
249
|
-
Object.assign(mergedRouteTrailingSlash, manifest.routeTrailingSlash);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
244
|
// Flatten prefix tree leaf nodes into precomputed entries.
|
|
253
245
|
// Leaf nodes (no children) can have their routes used directly by
|
|
254
246
|
// evaluateLazyEntry() without running the handler at runtime.
|
|
255
|
-
// Walk once into a per-router array, then fold it into the merged array;
|
|
256
|
-
// the merged and per-router entries are identical, so a second walk is
|
|
257
|
-
// redundant. Append order is preserved within and across routers.
|
|
258
247
|
const routerPrecomputed: PrecomputedEntry[] = [];
|
|
259
248
|
flattenLeafEntries(
|
|
260
249
|
manifest.prefixTree,
|
|
261
250
|
manifest.routeManifest,
|
|
262
251
|
routerPrecomputed,
|
|
263
252
|
);
|
|
264
|
-
newMergedPrecomputedEntries.push(...routerPrecomputed);
|
|
265
253
|
|
|
266
254
|
// Store per-router manifest and precomputed entries for isolated virtual modules.
|
|
267
255
|
newPerRouterManifestDataMap.set(id, manifest.routeManifest);
|
|
@@ -298,53 +286,11 @@ export async function discoverRouters(
|
|
|
298
286
|
(performance.now() - manifestGenStart).toFixed(1),
|
|
299
287
|
);
|
|
300
288
|
|
|
301
|
-
//
|
|
302
|
-
|
|
289
|
+
// No merged trie is built: find-match.ts consumes per-router tries only
|
|
290
|
+
// (getRouterTrie(routerId)), falling back to regex over live routes on a
|
|
291
|
+
// gap — the global merged trie was never read by any matcher.
|
|
303
292
|
const trieStart = debug ? performance.now() : 0;
|
|
304
293
|
if (Object.keys(newMergedRouteManifest).length > 0) {
|
|
305
|
-
// Build routeToStaticPrefix from saved manifests
|
|
306
|
-
const routeToStaticPrefix: Record<string, string> = {};
|
|
307
|
-
for (const { manifest } of allManifests) {
|
|
308
|
-
// Root-level routes have empty static prefix
|
|
309
|
-
for (const name of Object.keys(manifest.routeManifest)) {
|
|
310
|
-
if (!(name in routeToStaticPrefix)) {
|
|
311
|
-
routeToStaticPrefix[name] = "";
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
buildRouteToStaticPrefix(manifest.prefixTree, routeToStaticPrefix);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Collect prerender route names and response type routes from all manifests
|
|
318
|
-
const prerenderRouteNames = new Set<string>();
|
|
319
|
-
const passthroughRouteNames = new Set<string>();
|
|
320
|
-
const mergedResponseTypeRoutes: Record<string, string> = {};
|
|
321
|
-
for (const { manifest } of allManifests) {
|
|
322
|
-
if (manifest.prerenderRoutes) {
|
|
323
|
-
for (const name of manifest.prerenderRoutes) {
|
|
324
|
-
prerenderRouteNames.add(name);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
if (manifest.passthroughRoutes) {
|
|
328
|
-
for (const name of manifest.passthroughRoutes) {
|
|
329
|
-
passthroughRouteNames.add(name);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
if (manifest.responseTypeRoutes) {
|
|
333
|
-
Object.assign(mergedResponseTypeRoutes, manifest.responseTypeRoutes);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// buildRouteTrie reads these via ?.has / ?.[] — empty is observationally
|
|
338
|
-
// identical to undefined, so no empty->undefined coercion is needed.
|
|
339
|
-
newMergedRouteTrie = buildRouteTrie(
|
|
340
|
-
newMergedRouteManifest,
|
|
341
|
-
routeToStaticPrefix,
|
|
342
|
-
mergedRouteTrailingSlash,
|
|
343
|
-
prerenderRouteNames,
|
|
344
|
-
passthroughRouteNames,
|
|
345
|
-
mergedResponseTypeRoutes,
|
|
346
|
-
);
|
|
347
|
-
|
|
348
294
|
// Build per-router tries for multi-router isolation. Uses the single
|
|
349
295
|
// shared buildPerRouterTrie so the production serialized trie is built by
|
|
350
296
|
// exactly the same code as the dev/HMR runtime rebuild (manifest-init.ts).
|
|
@@ -366,12 +312,10 @@ export async function discoverRouters(
|
|
|
366
312
|
// This ensures a failed re-discovery (e.g. from a transient module
|
|
367
313
|
// evaluation error) preserves the last known-good state.
|
|
368
314
|
state.mergedRouteManifest = newMergedRouteManifest;
|
|
369
|
-
state.mergedPrecomputedEntries = newMergedPrecomputedEntries;
|
|
370
315
|
state.perRouterManifests = newPerRouterManifests;
|
|
371
316
|
state.perRouterManifestDataMap = newPerRouterManifestDataMap;
|
|
372
317
|
state.perRouterPrecomputedMap = newPerRouterPrecomputedMap;
|
|
373
318
|
state.perRouterTrieMap = newPerRouterTrieMap;
|
|
374
|
-
state.mergedRouteTrie = newMergedRouteTrie;
|
|
375
319
|
|
|
376
320
|
// Install the route tries into the RSC realm BEFORE prerender collection.
|
|
377
321
|
// matchForPrerender resolves each enumerated URL via findMatch, and without
|
|
@@ -386,9 +330,6 @@ export async function discoverRouters(
|
|
|
386
330
|
// collection was the one findMatch consumer running trieless. Mirrors the
|
|
387
331
|
// dev perRouterSetters loop; deliberately does NOT markRouterTrieAuthoritative
|
|
388
332
|
// so a genuine trie gap keeps the regex fallback, exactly as in dev.
|
|
389
|
-
if (serverMod.setRouteTrie && newMergedRouteTrie) {
|
|
390
|
-
serverMod.setRouteTrie(newMergedRouteTrie);
|
|
391
|
-
}
|
|
392
333
|
const perRouterSetters: Array<[Map<string, unknown>, string]> = [
|
|
393
334
|
[newPerRouterManifestDataMap, "setRouterManifest"],
|
|
394
335
|
[newPerRouterTrieMap, "setRouterTrie"],
|
|
@@ -415,15 +415,26 @@ export async function renderStaticHandlers(
|
|
|
415
415
|
|
|
416
416
|
const startHandler = performance.now();
|
|
417
417
|
let handled = false;
|
|
418
|
-
|
|
418
|
+
// Owner-first: path() stamped the mounting router's id on the def
|
|
419
|
+
// ($$routerId). Sweeping the whole registry hands the FIRST router's
|
|
420
|
+
// id and route map to every static handler — wrong scoping in
|
|
421
|
+
// multi-router apps. The full sweep remains only as the fallback for
|
|
422
|
+
// an unstamped def (evaluated outside a router scope).
|
|
423
|
+
const owner = (def as any).$$routerId as string | undefined;
|
|
424
|
+
const owned = owner !== undefined ? registry.get(owner) : undefined;
|
|
425
|
+
const candidates = owned ? [owned] : [...registry.values()];
|
|
426
|
+
for (const routerInstance of candidates) {
|
|
419
427
|
if (!routerInstance.renderStaticSegment) continue;
|
|
420
428
|
try {
|
|
421
429
|
const result = await routerInstance.renderStaticSegment(
|
|
422
430
|
def.handler,
|
|
423
431
|
def.$$id,
|
|
424
|
-
|
|
432
|
+
// Full route name when stamped; the historical $$routePrefix is a
|
|
433
|
+
// name PREFIX (never in the root-scope registry) kept as fallback.
|
|
434
|
+
(def as any).$$routeName ?? (def as any).$$routePrefix,
|
|
425
435
|
state.resolvedBuildEnv,
|
|
426
436
|
!state.isBuildMode,
|
|
437
|
+
(def as any).$$rootScoped,
|
|
427
438
|
);
|
|
428
439
|
if (result) {
|
|
429
440
|
// result.handles is the pre-encoded handle string ("" when none).
|
|
@@ -59,6 +59,7 @@ export interface ChunkInfo {
|
|
|
59
59
|
export interface PerRouterManifestEntry {
|
|
60
60
|
id: string;
|
|
61
61
|
routeManifest: Record<string, string>;
|
|
62
|
+
routeTrailingSlash?: Record<string, string>;
|
|
62
63
|
routeSearchSchemas?: Record<string, Record<string, string>>;
|
|
63
64
|
sourceFile?: string;
|
|
64
65
|
factoryOnlyPrefixes?: Set<string>;
|
|
@@ -104,8 +105,6 @@ export interface DiscoveryState {
|
|
|
104
105
|
|
|
105
106
|
mergedRouteManifest: Record<string, string> | null;
|
|
106
107
|
perRouterManifests: PerRouterManifestEntry[];
|
|
107
|
-
mergedPrecomputedEntries: PrecomputedEntry[] | null;
|
|
108
|
-
mergedRouteTrie: any;
|
|
109
108
|
|
|
110
109
|
perRouterTrieMap: Map<string, any>;
|
|
111
110
|
perRouterPrecomputedMap: Map<string, PrecomputedEntry[]>;
|
|
@@ -141,6 +140,8 @@ export interface DiscoveryState {
|
|
|
141
140
|
resolvedStaticModules: Map<string, string[]> | undefined;
|
|
142
141
|
|
|
143
142
|
discoveryDone: Promise<void> | null;
|
|
143
|
+
/** Monotonic Cloudflare-dev generation installed by the routes manifest. */
|
|
144
|
+
devDiscoveryEpoch?: number;
|
|
144
145
|
devServerOrigin: string | null;
|
|
145
146
|
devServer: any;
|
|
146
147
|
selfWrittenGenFiles: Map<string, { at: number; hash: string }>;
|
|
@@ -177,8 +178,6 @@ export function createDiscoveryState(
|
|
|
177
178
|
|
|
178
179
|
mergedRouteManifest: null,
|
|
179
180
|
perRouterManifests: [],
|
|
180
|
-
mergedPrecomputedEntries: null,
|
|
181
|
-
mergedRouteTrie: null,
|
|
182
181
|
|
|
183
182
|
perRouterTrieMap: new Map(),
|
|
184
183
|
perRouterPrecomputedMap: new Map(),
|
|
@@ -196,6 +195,7 @@ export function createDiscoveryState(
|
|
|
196
195
|
resolvedStaticModules: undefined,
|
|
197
196
|
|
|
198
197
|
discoveryDone: null,
|
|
198
|
+
devDiscoveryEpoch: opts?.preset === "cloudflare" ? Date.now() : undefined,
|
|
199
199
|
devServerOrigin: null,
|
|
200
200
|
devServer: null,
|
|
201
201
|
selfWrittenGenFiles: new Map(),
|