@rangojs/router 0.6.0 → 0.7.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/dist/types/client-urls/server-projection.d.ts +10 -1
- package/dist/types/client-urls/types.d.ts +5 -2
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -9
- package/dist/types/urls/path-helper-types.d.ts +4 -1
- package/dist/types/vite/plugins/client-ref-dedup.d.ts +0 -11
- package/dist/vite/index.js +360 -108
- package/package.json +6 -5
- package/skills/client-urls/SKILL.md +9 -9
- package/src/client-urls/client-urls.ts +78 -0
- package/src/client-urls/server-projection.ts +95 -4
- package/src/client-urls/types.ts +8 -2
- package/src/router/segment-resolution/fresh.ts +8 -2
- package/src/router/segment-resolution/loader-cache.ts +34 -13
- package/src/router/segment-resolution/loader-mask.ts +9 -9
- package/src/router/segment-resolution/revalidation.ts +3 -1
- package/src/urls/path-helper-types.ts +4 -1
- package/src/vite/plugins/client-ref-dedup.ts +281 -19
- package/src/vite/plugins/expose-action-id.ts +45 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -182,6 +182,7 @@
|
|
|
182
182
|
"prepublishOnly": "pnpm build",
|
|
183
183
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
184
184
|
"test": "playwright test",
|
|
185
|
+
"test:preload": "playwright test --config=playwright.preload.config.ts",
|
|
185
186
|
"test:ui": "playwright test --ui",
|
|
186
187
|
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-basename --project=hmr-prerender --no-deps --workers=1 && RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
187
188
|
"test:hmr-routes-local": "RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
@@ -191,7 +192,7 @@
|
|
|
191
192
|
},
|
|
192
193
|
"dependencies": {
|
|
193
194
|
"@types/debug": "^4.1.12",
|
|
194
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
195
|
+
"@vitejs/plugin-rsc": "^0.5.31",
|
|
195
196
|
"debug": "^4.4.1",
|
|
196
197
|
"magic-string": "^0.30.17",
|
|
197
198
|
"picomatch": "^4.0.4",
|
|
@@ -224,9 +225,9 @@
|
|
|
224
225
|
"@playwright/test": "^1.49.1",
|
|
225
226
|
"@testing-library/react": ">=16",
|
|
226
227
|
"@vercel/functions": "^3.0.0",
|
|
227
|
-
"@vitejs/plugin-rsc": "^0.5.
|
|
228
|
-
"react": ">=19.2.
|
|
229
|
-
"react-dom": ">=19.2.
|
|
228
|
+
"@vitejs/plugin-rsc": "^0.5.31",
|
|
229
|
+
"react": ">=19.2.8 <20",
|
|
230
|
+
"react-dom": ">=19.2.8 <20",
|
|
230
231
|
"vite": "^8.0.16",
|
|
231
232
|
"vitest": ">=3"
|
|
232
233
|
},
|
|
@@ -118,15 +118,15 @@ typing work exactly as for server routes (`/typesafety`).
|
|
|
118
118
|
|
|
119
119
|
## Helpers: what exists inside clientUrls()
|
|
120
120
|
|
|
121
|
-
| Helper | Notes
|
|
122
|
-
| -------------- |
|
|
123
|
-
| `path()` | Options are `name`, `search`, `trailingSlash`
|
|
124
|
-
| `layout()` | Must contain at least one `path()`
|
|
125
|
-
| `loader()` | `loader(Def, use?)` or `loader(Def, { stream: "navigation" }, use?)` — see below
|
|
126
|
-
| `loading()` | Route/layout-level pending UI; inline `<Suspense>` at read sites is usually better
|
|
127
|
-
| `revalidate()` | Valid **inside a loader() use callback only**; runs in the browser
|
|
128
|
-
| `transition()` | Data-only ViewTransition config — no `when` (that is a server-executed predicate)
|
|
129
|
-
| `intercept()` | Dot-local named target in the SAME definition; use may contain `loader()`/`loading()`
|
|
121
|
+
| Helper | Notes |
|
|
122
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
|
+
| `path()` | Options are `name`, `search`, `trailingSlash`, `ppr` (shell caching — see /ppr skill; loader routes need `loading()` or capture refuses); no response variants |
|
|
124
|
+
| `layout()` | Must contain at least one `path()` |
|
|
125
|
+
| `loader()` | `loader(Def, use?)` or `loader(Def, { stream: "navigation" }, use?)` — see below |
|
|
126
|
+
| `loading()` | Route/layout-level pending UI; inline `<Suspense>` at read sites is usually better |
|
|
127
|
+
| `revalidate()` | Valid **inside a loader() use callback only**; runs in the browser |
|
|
128
|
+
| `transition()` | Data-only ViewTransition config — no `when` (that is a server-executed predicate) |
|
|
129
|
+
| `intercept()` | Dot-local named target in the SAME definition; use may contain `loader()`/`loading()` |
|
|
130
130
|
|
|
131
131
|
`include`, `parallel`, `cache`, `middleware`, `errorBoundary`,
|
|
132
132
|
`notFoundBoundary` **throw** — and that is a design position, not a gap.
|
|
@@ -31,6 +31,56 @@ const TRANSITION_CONFIG_KEYS = new Set<PropertyKey>([
|
|
|
31
31
|
"viewTransition",
|
|
32
32
|
]);
|
|
33
33
|
|
|
34
|
+
const PPR_NUMBER_KEYS = new Set<PropertyKey>([
|
|
35
|
+
"ttl",
|
|
36
|
+
"swr",
|
|
37
|
+
"captureTimeout",
|
|
38
|
+
"maxSnapshotBytes",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* DSL-time shape check for the projected `ppr` path option. Mirrors
|
|
43
|
+
* PartialPrerenderProps but validates by hand: the value must survive the
|
|
44
|
+
* JSON projection to the server, so only `true` or a plain object of finite
|
|
45
|
+
* numbers plus a string[] `tags` is legal. The projection serializer
|
|
46
|
+
* (server-projection.ts) re-validates independently — this copy exists so a
|
|
47
|
+
* bad value fails in the authoring module with a DSL-shaped message.
|
|
48
|
+
*/
|
|
49
|
+
function validateClientPpr(value: unknown): void {
|
|
50
|
+
if (value === true) return;
|
|
51
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"clientUrls() path() ppr must be true or a PartialPrerenderProps object",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
57
|
+
if (PPR_NUMBER_KEYS.has(key)) {
|
|
58
|
+
const n = (value as Record<PropertyKey, unknown>)[key];
|
|
59
|
+
if (typeof n !== "number" || !Number.isFinite(n)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`clientUrls() path() ppr.${String(key)} must be a finite number`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (key === "tags") {
|
|
67
|
+
const tags = (value as { tags: unknown }).tags;
|
|
68
|
+
if (
|
|
69
|
+
!Array.isArray(tags) ||
|
|
70
|
+
tags.some((tag) => typeof tag !== "string" || tag === "")
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"clientUrls() path() ppr.tags must be an array of non-empty strings",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
throw new Error(
|
|
79
|
+
`clientUrls() path() ppr received unsupported key ${JSON.stringify(String(key))}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
34
84
|
const UNSUPPORTED_HELPERS = new Set([
|
|
35
85
|
"include",
|
|
36
86
|
"parallel",
|
|
@@ -173,6 +223,31 @@ function assertNamedComponent(
|
|
|
173
223
|
component: unknown,
|
|
174
224
|
helper: "path" | "layout" | "intercept",
|
|
175
225
|
): asserts component is ComponentType {
|
|
226
|
+
// Build-time handler wrappers get a TARGETED rejection ahead of the generic
|
|
227
|
+
// shape check: their handler code cannot live in a "use client" bundle and
|
|
228
|
+
// their render runs at build, so accepting one could only fail later and
|
|
229
|
+
// further from the mistake. Brand strings checked directly — importing
|
|
230
|
+
// static-handler.ts/prerender.ts here would pull server modules into the
|
|
231
|
+
// client graph for an error message.
|
|
232
|
+
const brand =
|
|
233
|
+
component !== null &&
|
|
234
|
+
(typeof component === "object" || typeof component === "function")
|
|
235
|
+
? (component as { __brand?: unknown }).__brand
|
|
236
|
+
: undefined;
|
|
237
|
+
if (
|
|
238
|
+
brand === "staticHandler" ||
|
|
239
|
+
brand === "prerenderHandler" ||
|
|
240
|
+
brand === "prerenderPassthrough"
|
|
241
|
+
) {
|
|
242
|
+
const wrapper = brand === "staticHandler" ? "Static()" : "Prerender()";
|
|
243
|
+
throw new Error(
|
|
244
|
+
`clientUrls() ${helper}() received a ${wrapper} handler. Build-time ` +
|
|
245
|
+
"handlers are server-DSL surface and cannot mount inside a client " +
|
|
246
|
+
"group — routes in a group render client components. For shell " +
|
|
247
|
+
"caching of a group route use the `ppr` path option; build-time " +
|
|
248
|
+
"prerendering belongs to the server tree around the include.",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
176
251
|
if (typeof component !== "function" || component.name.trim() === "") {
|
|
177
252
|
throw new Error(
|
|
178
253
|
`clientUrls() ${helper}() expects a named client component value`,
|
|
@@ -249,6 +324,9 @@ function createHelpers(): ClientUrlHelpers {
|
|
|
249
324
|
'clientUrls() path() trailingSlash must be "always", "never", or "ignore"',
|
|
250
325
|
);
|
|
251
326
|
}
|
|
327
|
+
if (options?.ppr !== undefined) {
|
|
328
|
+
validateClientPpr(options.ppr);
|
|
329
|
+
}
|
|
252
330
|
|
|
253
331
|
const items = use ? runUse(use, "path use") : [];
|
|
254
332
|
rejectItems(items, new Set(["loader", "loading", "transition"]), "path");
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
type InterceptSelectorContext,
|
|
8
8
|
} from "../server/context.js";
|
|
9
9
|
import type { LoaderDefinition, TrailingSlashMode } from "../types.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
PartialPrerenderProps,
|
|
12
|
+
PathOptions,
|
|
13
|
+
UrlPatterns,
|
|
14
|
+
} from "../urls/pattern-types.js";
|
|
11
15
|
import type { PathHelpers } from "../urls/path-helper-types.js";
|
|
12
16
|
import type { AllUseItems } from "../route-types.js";
|
|
13
17
|
import { urls } from "../urls/urls-function.js";
|
|
@@ -45,6 +49,14 @@ const PROJECTION_OPTIONS = new Set<PropertyKey>([
|
|
|
45
49
|
"name",
|
|
46
50
|
"search",
|
|
47
51
|
"trailingSlash",
|
|
52
|
+
"ppr",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const PPR_NUMBER_KEYS = new Set<PropertyKey>([
|
|
56
|
+
"ttl",
|
|
57
|
+
"swr",
|
|
58
|
+
"captureTimeout",
|
|
59
|
+
"maxSnapshotBytes",
|
|
48
60
|
]);
|
|
49
61
|
|
|
50
62
|
const clientUrlProjections = new Map<string, ClientUrlProjection>();
|
|
@@ -59,6 +71,15 @@ export type ClientUrlDefinitionSource = ClientUrlReference | ClientUrlPatterns;
|
|
|
59
71
|
export interface ClientUrlProjectionOptions {
|
|
60
72
|
readonly search?: Readonly<Record<string, SearchSchemaValue>>;
|
|
61
73
|
readonly trailingSlash?: TrailingSlashMode;
|
|
74
|
+
/**
|
|
75
|
+
* The `ppr` path option, JSON-projected. Materialization passes it onto
|
|
76
|
+
* the group route's server `path()` verbatim, so the manifest entry
|
|
77
|
+
* carries it and the runtime shell capture/serve lanes engage exactly as
|
|
78
|
+
* for a hand-written server route (search is part of shell identity —
|
|
79
|
+
* the capture render seeds the key's own search, so group static parts
|
|
80
|
+
* may read useSearchParams).
|
|
81
|
+
*/
|
|
82
|
+
readonly ppr?: true | Readonly<PartialPrerenderProps>;
|
|
62
83
|
}
|
|
63
84
|
|
|
64
85
|
export interface ClientUrlProjectionRoute {
|
|
@@ -138,9 +159,6 @@ function serializeOptions(
|
|
|
138
159
|
if (!source) return Object.freeze({});
|
|
139
160
|
|
|
140
161
|
for (const key of Reflect.ownKeys(source)) {
|
|
141
|
-
if (key === "ppr") {
|
|
142
|
-
throw projectionError(route, 'path option "ppr" is not supported');
|
|
143
|
-
}
|
|
144
162
|
if (!PROJECTION_OPTIONS.has(key)) {
|
|
145
163
|
throw projectionError(
|
|
146
164
|
route,
|
|
@@ -152,6 +170,7 @@ function serializeOptions(
|
|
|
152
170
|
const options: {
|
|
153
171
|
search?: Readonly<Record<string, SearchSchemaValue>>;
|
|
154
172
|
trailingSlash?: TrailingSlashMode;
|
|
173
|
+
ppr?: true | Readonly<PartialPrerenderProps>;
|
|
155
174
|
} = {};
|
|
156
175
|
if (source.search !== undefined) {
|
|
157
176
|
options.search = serializeSearchSchema(route, source.search);
|
|
@@ -159,9 +178,72 @@ function serializeOptions(
|
|
|
159
178
|
if (source.trailingSlash !== undefined) {
|
|
160
179
|
options.trailingSlash = source.trailingSlash;
|
|
161
180
|
}
|
|
181
|
+
if (source.ppr !== undefined) {
|
|
182
|
+
options.ppr = serializePpr(route, source.ppr);
|
|
183
|
+
}
|
|
162
184
|
return Object.freeze(options);
|
|
163
185
|
}
|
|
164
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Serialize the `ppr` path option for the projection. The DSL already
|
|
189
|
+
* validated shape in the authoring module (validateClientPpr in
|
|
190
|
+
* client-urls.ts); this re-validates independently and re-copies
|
|
191
|
+
* defensively — the projection must carry only JSON, and the copy keeps a
|
|
192
|
+
* later mutation of the consumer's options object from leaking into the
|
|
193
|
+
* frozen projection.
|
|
194
|
+
*/
|
|
195
|
+
function serializePpr(
|
|
196
|
+
route: ClientUrlRouteRecord,
|
|
197
|
+
source: unknown,
|
|
198
|
+
): true | Readonly<PartialPrerenderProps> {
|
|
199
|
+
if (source === true) return true;
|
|
200
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) {
|
|
201
|
+
throw projectionError(
|
|
202
|
+
route,
|
|
203
|
+
'path option "ppr" must be true or a PartialPrerenderProps object',
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const out: {
|
|
207
|
+
ttl?: number;
|
|
208
|
+
swr?: number;
|
|
209
|
+
captureTimeout?: number;
|
|
210
|
+
maxSnapshotBytes?: number;
|
|
211
|
+
tags?: readonly string[];
|
|
212
|
+
} = {};
|
|
213
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
214
|
+
if (PPR_NUMBER_KEYS.has(key)) {
|
|
215
|
+
const value = (source as Record<PropertyKey, unknown>)[key];
|
|
216
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
217
|
+
throw projectionError(
|
|
218
|
+
route,
|
|
219
|
+
`path option "ppr.${String(key)}" must be a finite number`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
out[key as "ttl" | "swr" | "captureTimeout" | "maxSnapshotBytes"] = value;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (key === "tags") {
|
|
226
|
+
const tags = (source as { tags: unknown }).tags;
|
|
227
|
+
if (
|
|
228
|
+
!Array.isArray(tags) ||
|
|
229
|
+
tags.some((tag) => typeof tag !== "string" || tag === "")
|
|
230
|
+
) {
|
|
231
|
+
throw projectionError(
|
|
232
|
+
route,
|
|
233
|
+
'path option "ppr.tags" must be an array of non-empty strings',
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
out.tags = Object.freeze([...tags]);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
throw projectionError(
|
|
240
|
+
route,
|
|
241
|
+
`path option "ppr.${String(key)}" is not supported`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return Object.freeze(out) as Readonly<PartialPrerenderProps>;
|
|
245
|
+
}
|
|
246
|
+
|
|
165
247
|
function serializeTransition(
|
|
166
248
|
route: ClientUrlRouteRecord,
|
|
167
249
|
): Readonly<ClientTransitionConfig> | undefined {
|
|
@@ -393,6 +475,15 @@ function materializedPathOptions(route: ClientUrlProjectionRoute): PathOptions {
|
|
|
393
475
|
...(route.options.trailingSlash
|
|
394
476
|
? { trailingSlash: route.options.trailingSlash }
|
|
395
477
|
: {}),
|
|
478
|
+
// ppr rides onto the materialized server path() verbatim: path-helper
|
|
479
|
+
// stamps it on the manifest entry, so resolvePprConfig classifies the
|
|
480
|
+
// group route exactly like a hand-written ppr page. Shallow-copy the
|
|
481
|
+
// object form — path-helper receives a mutable PathOptions.
|
|
482
|
+
...(route.options.ppr !== undefined
|
|
483
|
+
? {
|
|
484
|
+
ppr: route.options.ppr === true ? true : { ...route.options.ppr },
|
|
485
|
+
}
|
|
486
|
+
: {}),
|
|
396
487
|
};
|
|
397
488
|
}
|
|
398
489
|
|
package/src/client-urls/types.ts
CHANGED
|
@@ -28,7 +28,10 @@ export type ClientUrlUse = () => ClientUrlItems;
|
|
|
28
28
|
export type ClientPathOptions<
|
|
29
29
|
TName extends string = string,
|
|
30
30
|
TSearch extends SearchSchema = SearchSchema,
|
|
31
|
-
> = Pick<
|
|
31
|
+
> = Pick<
|
|
32
|
+
PathOptions<TName, TSearch>,
|
|
33
|
+
"name" | "search" | "trailingSlash" | "ppr"
|
|
34
|
+
>;
|
|
32
35
|
|
|
33
36
|
export type ClientPathFn = <
|
|
34
37
|
const TPattern extends string,
|
|
@@ -141,7 +144,10 @@ export interface ClientUrlHelpers {
|
|
|
141
144
|
* Pass `{ stream: "navigation" }` to await this loader before first flush
|
|
142
145
|
* on DOCUMENT requests (see {@link LoaderOptions}) — the opt-in for loaders
|
|
143
146
|
* whose data, handle pushes, or thrown notFound()/redirect() must be in the
|
|
144
|
-
* SSR'd HTML. Per-loader: a dynamic sibling keeps streaming.
|
|
147
|
+
* SSR'd HTML. Per-loader: a dynamic sibling keeps streaming. Under a
|
|
148
|
+
* `ppr` group route the flag BAKES: the loader executes at shell capture
|
|
149
|
+
* and its settled return freezes into the shell (nested promises stay
|
|
150
|
+
* live holes).
|
|
145
151
|
*/
|
|
146
152
|
readonly loader: <TData>(
|
|
147
153
|
definition: LoaderDefinition<TData>,
|
|
@@ -148,7 +148,13 @@ export async function resolveLoaders<TEnv>(
|
|
|
148
148
|
loaderEntry,
|
|
149
149
|
ctx,
|
|
150
150
|
ctx.pathname,
|
|
151
|
-
|
|
151
|
+
// The bake key rides for flagged loaders too: stream:"navigation"
|
|
152
|
+
// bakes at capture regardless of the entry's loading() lane
|
|
153
|
+
// (loader-cache.ts capture branch) and its HIT-tail seed
|
|
154
|
+
// overlay needs the same key.
|
|
155
|
+
bakeLane || loaderEntry.awaitBeforeFlush === true
|
|
156
|
+
? segmentId
|
|
157
|
+
: null,
|
|
152
158
|
),
|
|
153
159
|
),
|
|
154
160
|
entry,
|
|
@@ -191,7 +197,7 @@ export async function resolveLoaders<TEnv>(
|
|
|
191
197
|
loaderEntry,
|
|
192
198
|
ctx,
|
|
193
199
|
ctx.pathname,
|
|
194
|
-
bakeLane ? segmentId : null,
|
|
200
|
+
bakeLane || loaderEntry.awaitBeforeFlush === true ? segmentId : null,
|
|
195
201
|
),
|
|
196
202
|
),
|
|
197
203
|
entry,
|
|
@@ -157,19 +157,40 @@ export function resolveLoaderData<TEnv>(
|
|
|
157
157
|
// shell HIT the recorded container is overlaid onto the fresh run so the
|
|
158
158
|
// payload matches the frozen prelude byte-for-byte.
|
|
159
159
|
if (isShellCaptureActive(reqCtx)) {
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
// the
|
|
160
|
+
// Capture lane, per LOADER (not per entry):
|
|
161
|
+
//
|
|
162
|
+
// - `stream: "navigation"` (awaitBeforeFlush) — the BAKE lane. The flag's
|
|
163
|
+
// document promise is "this loader's data is in the HTML before first
|
|
164
|
+
// flush"; under ppr the pre-flush HTML IS the frozen prelude, so the
|
|
165
|
+
// loader executes at capture and its SETTLED return bakes into the
|
|
166
|
+
// shell. Nested-promise SHAPE stays the liveness declaration: nested
|
|
167
|
+
// thenables are masked so their subtrees postpone as holes (per-request
|
|
168
|
+
// material must be promise-shaped — the #692 cross-session scar). The
|
|
169
|
+
// masked container registers on _shellCaptureLoaderRecords: it holds
|
|
170
|
+
// the capture gate (bounded by ppr.captureTimeout) and pins into the
|
|
171
|
+
// shell snapshot, so a HIT tail replays the baked value and the
|
|
172
|
+
// hydration payload matches the frozen prelude byte-for-byte.
|
|
173
|
+
// - Every other loader — LIVE unconditionally: masked with a
|
|
174
|
+
// never-resolving promise, postponed at its boundary (loading() or an
|
|
175
|
+
// inline Suspense), streamed fresh per request. A masked reader with NO
|
|
176
|
+
// boundary above it root-postpones and the <body> sanity gate refuses
|
|
177
|
+
// the shell (eternal-MISS warning) — the degrade for boundary-less ppr
|
|
178
|
+
// routes. A route whose loaders are ALL flagged therefore needs no
|
|
179
|
+
// loading() at all: nothing masks, the shell captures complete.
|
|
180
|
+
if (loaderEntry.awaitBeforeFlush && bakeSegmentKey) {
|
|
181
|
+
const containerPromise = executeLoaderData(loaderEntry, ctx, pathname);
|
|
182
|
+
// Pre-attach a no-op catch: a bake-lane rejection during capture must
|
|
183
|
+
// surface through the drain's refusal (and the wrapper's error
|
|
184
|
+
// boundary), never as an unhandled rejection that can kill the worker
|
|
185
|
+
// before the drain probes this record.
|
|
186
|
+
containerPromise.catch(() => {});
|
|
187
|
+
const maskedPromise = containerPromise.then((container: unknown) =>
|
|
188
|
+
maskNestedContainerThenables(container),
|
|
189
|
+
);
|
|
190
|
+
maskedPromise.catch(() => {});
|
|
191
|
+
reqCtx?._shellCaptureLoaderRecords?.set(bakeSegmentKey, maskedPromise);
|
|
192
|
+
return maskedPromise;
|
|
193
|
+
}
|
|
173
194
|
return createMaskedLoaderPromise();
|
|
174
195
|
}
|
|
175
196
|
|
|
@@ -42,15 +42,15 @@ export function isShellCaptureActive(
|
|
|
42
42
|
export { createMaskedLoaderPromise } from "./mask-nested.js";
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
* docs/design/loader-container-bake.md)
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
45
|
+
* Entry-level lane input for an entry's loaders under PPR (the loading()
|
|
46
|
+
* value; docs/design/loader-container-bake.md). The CAPTURE decision itself
|
|
47
|
+
* is per LOADER in loader-cache.ts: a `stream: "navigation"`
|
|
48
|
+
* (awaitBeforeFlush) loader BAKES at capture regardless of this value — the
|
|
49
|
+
* flag's document promise ("data in the HTML before first flush") maps to
|
|
50
|
+
* the frozen prelude — while every other loader is LIVE (masked at capture,
|
|
51
|
+
* fresh on every serve), postponing at loading() or an inline Suspense.
|
|
52
|
+
* This helper still gates which entries pass a bake segment key for the
|
|
53
|
+
* legacy loading()-less shape and the HIT-tail seed overlay.
|
|
54
54
|
*
|
|
55
55
|
* Mirrors segment-system's isRenderableLoading so the mask decision and the
|
|
56
56
|
* tree's boundary placement can never disagree.
|
|
@@ -350,7 +350,10 @@ export type PathHelpers<TEnv> = {
|
|
|
350
350
|
* Pass `{ stream: "navigation" }` to await this loader before first flush
|
|
351
351
|
* on DOCUMENT requests (see {@link LoaderOptions}) — the opt-in for loaders
|
|
352
352
|
* whose data, handle pushes, or thrown notFound()/redirect() must be in the
|
|
353
|
-
* SSR'd HTML. Per-loader: a dynamic sibling keeps streaming.
|
|
353
|
+
* SSR'd HTML. Per-loader: a dynamic sibling keeps streaming. Under a
|
|
354
|
+
* `ppr` route the flag BAKES: the loader executes at shell capture and
|
|
355
|
+
* its settled return freezes into the shell (nested promises stay live
|
|
356
|
+
* holes) — the pre-flush promise applied to the prelude.
|
|
354
357
|
*/
|
|
355
358
|
loader: <TData>(
|
|
356
359
|
loaderDef: LoaderDefinition<TData>,
|