@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1fa245e2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/README.md +76 -18
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +558 -319
- package/package.json +16 -15
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +45 -4
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +53 -43
- package/skills/middleware/SKILL.md +2 -0
- package/skills/parallel/SKILL.md +126 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/route/SKILL.md +31 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/__internal.ts +1 -1
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +19 -13
- package/src/browser/navigation-client.ts +115 -58
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +80 -15
- package/src/browser/prefetch/cache.ts +57 -5
- package/src/browser/prefetch/fetch.ts +38 -23
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +53 -9
- package/src/browser/react/NavigationProvider.tsx +40 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +134 -59
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +36 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +223 -74
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.tsx +2 -56
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +8 -0
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +22 -1
- package/src/route-definition/dsl-helpers.ts +73 -25
- package/src/route-definition/helpers-types.ts +10 -6
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +79 -23
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +122 -10
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-api.ts +124 -189
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +88 -16
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +22 -6
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +6 -8
- package/src/router/middleware.ts +4 -6
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +183 -20
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +412 -297
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/types.ts +1 -0
- package/src/router.ts +59 -6
- package/src/rsc/handler.ts +460 -368
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/rsc-rendering.ts +5 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +8 -1
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +140 -14
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +144 -18
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +137 -33
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-entry.ts +8 -1
- package/src/types/segments.ts +2 -0
- package/src/urls/path-helper-types.ts +9 -2
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +73 -4
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/discovery/prerender-collection.ts +14 -1
- package/src/vite/discovery/state.ts +13 -6
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +163 -211
- package/src/vite/router-discovery.ts +153 -42
- package/src/vite/utils/banner.ts +3 -3
- package/src/vite/utils/prerender-utils.ts +18 -0
- package/src/vite/utils/shared-utils.ts +3 -2
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import type { NonceProvider } from "../rsc/types.js";
|
|
9
9
|
import type { ExecutionContext } from "../server/request-context.js";
|
|
10
10
|
import type { UrlPatterns } from "../urls.js";
|
|
11
|
+
import type { UrlBuilder } from "../urls/pattern-types.js";
|
|
11
12
|
import type { NamedRouteEntry } from "./content-negotiation.js";
|
|
12
13
|
import type { TelemetrySink } from "./telemetry.js";
|
|
13
14
|
import type { RouterTimeouts, OnTimeoutCallback } from "./timeout.js";
|
|
@@ -95,6 +96,28 @@ export interface RSCRouterOptions<TEnv = any> {
|
|
|
95
96
|
*/
|
|
96
97
|
$$sourceFile?: string;
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* URL prefix applied to all routes registered with this router.
|
|
101
|
+
*
|
|
102
|
+
* Useful when the app is served under a sub-path (e.g. `/admin` or `/v2`).
|
|
103
|
+
* All `path()` patterns are automatically prefixed and `reverse()` returns
|
|
104
|
+
* full paths including the basename. Route names are NOT prefixed.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const router = createRouter({
|
|
109
|
+
* basename: "/admin",
|
|
110
|
+
* }).routes(({ path }) => [
|
|
111
|
+
* path("/", Dashboard, { name: "home" }), // matches /admin
|
|
112
|
+
* path("/users", Users, { name: "users" }), // matches /admin/users
|
|
113
|
+
* ]);
|
|
114
|
+
*
|
|
115
|
+
* router.reverse("home"); // "/admin"
|
|
116
|
+
* router.reverse("users"); // "/admin/users"
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
basename?: string;
|
|
120
|
+
|
|
98
121
|
/**
|
|
99
122
|
* Enable performance metrics collection
|
|
100
123
|
* When enabled, metrics are output to console and available via Server-Timing header
|
|
@@ -337,25 +360,28 @@ export interface RSCRouterOptions<TEnv = any> {
|
|
|
337
360
|
/**
|
|
338
361
|
* URL patterns to register with the router.
|
|
339
362
|
*
|
|
340
|
-
*
|
|
341
|
-
* directly
|
|
363
|
+
* Accepts either a `UrlPatterns` object from `urls()` or a builder function
|
|
364
|
+
* directly (urls() is called implicitly).
|
|
342
365
|
*
|
|
343
366
|
* @example
|
|
344
367
|
* ```typescript
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* const urlpatterns = urls(({ path, layout }) => [
|
|
348
|
-
* path("/", HomePage, { name: "home" }),
|
|
349
|
-
* path("/about", AboutPage, { name: "about" }),
|
|
350
|
-
* ]);
|
|
351
|
-
*
|
|
352
|
-
* const router = createRouter<AppEnv>({
|
|
368
|
+
* // With urls()
|
|
369
|
+
* createRouter<AppEnv>({
|
|
353
370
|
* document: Document,
|
|
354
371
|
* urls: urlpatterns,
|
|
355
372
|
* });
|
|
373
|
+
*
|
|
374
|
+
* // With builder function
|
|
375
|
+
* createRouter<AppEnv>({
|
|
376
|
+
* document: Document,
|
|
377
|
+
* urls: ({ path }) => [
|
|
378
|
+
* path("/", HomePage, { name: "home" }),
|
|
379
|
+
* path("/about", AboutPage, { name: "about" }),
|
|
380
|
+
* ],
|
|
381
|
+
* });
|
|
356
382
|
* ```
|
|
357
383
|
*/
|
|
358
|
-
urls?: UrlPatterns<TEnv, any>;
|
|
384
|
+
urls?: UrlPatterns<TEnv, any> | UrlBuilder<TEnv>;
|
|
359
385
|
|
|
360
386
|
/**
|
|
361
387
|
* Injected by the Vite transform at compile time.
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ReactNode } from "react";
|
|
9
9
|
import { invariant } from "../../errors";
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
getParallelEntries,
|
|
12
|
+
getParallelSlotEntries,
|
|
13
|
+
type EntryData,
|
|
14
|
+
} from "../../server/context";
|
|
11
15
|
import type {
|
|
12
16
|
HandlerContext,
|
|
13
17
|
InternalHandlerContext,
|
|
@@ -15,6 +19,8 @@ import type {
|
|
|
15
19
|
} from "../../types";
|
|
16
20
|
import type { SegmentResolutionDeps } from "../types.js";
|
|
17
21
|
import { resolveLoaderData } from "./loader-cache.js";
|
|
22
|
+
import { _getRequestContext } from "../../server/request-context.js";
|
|
23
|
+
import { appendMetric } from "../metrics.js";
|
|
18
24
|
import {
|
|
19
25
|
handleHandlerResult,
|
|
20
26
|
tryStaticHandler,
|
|
@@ -24,7 +30,11 @@ import {
|
|
|
24
30
|
} from "./helpers.js";
|
|
25
31
|
import { getRouterContext } from "../router-context.js";
|
|
26
32
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
27
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
track,
|
|
35
|
+
RSCRouterContext,
|
|
36
|
+
runInsideLoaderScope,
|
|
37
|
+
} from "../../server/context.js";
|
|
28
38
|
|
|
29
39
|
// ---------------------------------------------------------------------------
|
|
30
40
|
// Streamed handler telemetry
|
|
@@ -90,9 +100,11 @@ export async function resolveLoaders<TEnv>(
|
|
|
90
100
|
const shortCode = shortCodeOverride ?? entry.shortCode;
|
|
91
101
|
const hasLoading = "loading" in entry && entry.loading !== undefined;
|
|
92
102
|
const loadingDisabled = hasLoading && entry.loading === false;
|
|
103
|
+
const ms = _getRequestContext()?._metricsStore;
|
|
93
104
|
|
|
94
105
|
if (!loadingDisabled) {
|
|
95
|
-
|
|
106
|
+
// Streaming loaders: promises kick off now, settle during RSC serialization.
|
|
107
|
+
const segments = loaderEntries.map((loaderEntry, i) => {
|
|
96
108
|
const { loader } = loaderEntry;
|
|
97
109
|
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
98
110
|
return {
|
|
@@ -104,7 +116,9 @@ export async function resolveLoaders<TEnv>(
|
|
|
104
116
|
params: ctx.params,
|
|
105
117
|
loaderId: loader.$$id,
|
|
106
118
|
loaderData: deps.wrapLoaderPromise(
|
|
107
|
-
|
|
119
|
+
runInsideLoaderScope(() =>
|
|
120
|
+
resolveLoaderData(loaderEntry, ctx, ctx.pathname),
|
|
121
|
+
),
|
|
108
122
|
entry,
|
|
109
123
|
segmentId,
|
|
110
124
|
ctx.pathname,
|
|
@@ -112,18 +126,38 @@ export async function resolveLoaders<TEnv>(
|
|
|
112
126
|
belongsToRoute,
|
|
113
127
|
};
|
|
114
128
|
});
|
|
129
|
+
|
|
130
|
+
return segments;
|
|
115
131
|
}
|
|
116
132
|
|
|
117
133
|
// Loading disabled: still start all loaders in parallel, but only emit
|
|
118
134
|
// settled promises so handlers don't stream loading placeholders.
|
|
119
|
-
const pendingLoaderData = loaderEntries.map((loaderEntry) =>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
const pendingLoaderData = loaderEntries.map((loaderEntry) => {
|
|
136
|
+
const start = performance.now();
|
|
137
|
+
const promise = runInsideLoaderScope(() =>
|
|
138
|
+
resolveLoaderData(loaderEntry, ctx, ctx.pathname),
|
|
139
|
+
);
|
|
140
|
+
return { promise, start, loaderId: loaderEntry.loader.$$id };
|
|
141
|
+
});
|
|
142
|
+
await Promise.all(pendingLoaderData.map((p) => p.promise));
|
|
123
143
|
|
|
124
144
|
return loaderEntries.map((loaderEntry, i) => {
|
|
125
145
|
const { loader } = loaderEntry;
|
|
126
146
|
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
147
|
+
const pending = pendingLoaderData[i]!;
|
|
148
|
+
if (ms && !ms.metrics.some((m) => m.label === `loader:${loader.$$id}`)) {
|
|
149
|
+
// All loaders ran in parallel via Promise.all — each span covers
|
|
150
|
+
// from its own kickoff to the batch settlement, giving a ceiling
|
|
151
|
+
// on that loader's contribution to the overall wait.
|
|
152
|
+
const batchEnd = performance.now();
|
|
153
|
+
appendMetric(
|
|
154
|
+
ms,
|
|
155
|
+
`loader:${loader.$$id}`,
|
|
156
|
+
pending.start,
|
|
157
|
+
batchEnd - pending.start,
|
|
158
|
+
2,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
127
161
|
return {
|
|
128
162
|
id: segmentId,
|
|
129
163
|
namespace: entry.id,
|
|
@@ -133,7 +167,7 @@ export async function resolveLoaders<TEnv>(
|
|
|
133
167
|
params: ctx.params,
|
|
134
168
|
loaderId: loader.$$id,
|
|
135
169
|
loaderData: deps.wrapLoaderPromise(
|
|
136
|
-
|
|
170
|
+
pending.promise,
|
|
137
171
|
entry,
|
|
138
172
|
segmentId,
|
|
139
173
|
ctx.pathname,
|
|
@@ -197,7 +231,10 @@ export async function resolveSegment<TEnv>(
|
|
|
197
231
|
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
198
232
|
});
|
|
199
233
|
|
|
200
|
-
|
|
234
|
+
const resolvedParallelEntries = new Set<string>();
|
|
235
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
236
|
+
entry.parallel,
|
|
237
|
+
)) {
|
|
201
238
|
const parallelSegments = await resolveParallelEntry(
|
|
202
239
|
parallelEntry,
|
|
203
240
|
params,
|
|
@@ -207,8 +244,11 @@ export async function resolveSegment<TEnv>(
|
|
|
207
244
|
deps,
|
|
208
245
|
options,
|
|
209
246
|
routeKey,
|
|
247
|
+
[slot],
|
|
248
|
+
!resolvedParallelEntries.has(parallelEntry.id),
|
|
210
249
|
);
|
|
211
250
|
segments.push(...parallelSegments);
|
|
251
|
+
resolvedParallelEntries.add(parallelEntry.id);
|
|
212
252
|
}
|
|
213
253
|
|
|
214
254
|
for (const orphan of entry.layout) {
|
|
@@ -244,9 +284,14 @@ export async function resolveSegment<TEnv>(
|
|
|
244
284
|
entry.shortCode,
|
|
245
285
|
);
|
|
246
286
|
if (component === undefined) {
|
|
287
|
+
// For Passthrough routes at runtime, use the live handler instead of
|
|
288
|
+
// the build handler. At build time (context.build === true), always
|
|
289
|
+
// use the build handler from entry.handler.
|
|
290
|
+
const handler =
|
|
291
|
+
!context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
|
|
247
292
|
const doneRouteHandler = track(`handler:${entry.id}`, 2);
|
|
248
293
|
if (entry.loading) {
|
|
249
|
-
const result = handleHandlerResult(
|
|
294
|
+
const result = handleHandlerResult(handler(context));
|
|
250
295
|
if (result instanceof Promise) {
|
|
251
296
|
result.finally(doneRouteHandler).catch(() => {});
|
|
252
297
|
const tracked = deps.trackHandler(result, {
|
|
@@ -267,7 +312,7 @@ export async function resolveSegment<TEnv>(
|
|
|
267
312
|
component = result;
|
|
268
313
|
}
|
|
269
314
|
} else {
|
|
270
|
-
component = handleHandlerResult(await
|
|
315
|
+
component = handleHandlerResult(await handler(context));
|
|
271
316
|
doneRouteHandler();
|
|
272
317
|
}
|
|
273
318
|
}
|
|
@@ -282,11 +327,15 @@ export async function resolveSegment<TEnv>(
|
|
|
282
327
|
deps,
|
|
283
328
|
options,
|
|
284
329
|
routeKey,
|
|
330
|
+
entry,
|
|
285
331
|
);
|
|
286
332
|
segments.push(...orphanSegments);
|
|
287
333
|
}
|
|
288
334
|
|
|
289
|
-
|
|
335
|
+
const resolvedParallelEntries = new Set<string>();
|
|
336
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
337
|
+
entry.parallel,
|
|
338
|
+
)) {
|
|
290
339
|
const parallelSegments = await resolveParallelEntry(
|
|
291
340
|
parallelEntry,
|
|
292
341
|
params,
|
|
@@ -296,8 +345,11 @@ export async function resolveSegment<TEnv>(
|
|
|
296
345
|
deps,
|
|
297
346
|
options,
|
|
298
347
|
routeKey,
|
|
348
|
+
[slot],
|
|
349
|
+
!resolvedParallelEntries.has(parallelEntry.id),
|
|
299
350
|
);
|
|
300
351
|
segments.push(...parallelSegments);
|
|
352
|
+
resolvedParallelEntries.add(parallelEntry.id);
|
|
301
353
|
}
|
|
302
354
|
|
|
303
355
|
segments.push({
|
|
@@ -305,7 +357,7 @@ export async function resolveSegment<TEnv>(
|
|
|
305
357
|
namespace: entry.id,
|
|
306
358
|
type: "route",
|
|
307
359
|
index: 0,
|
|
308
|
-
component,
|
|
360
|
+
component: component ?? null,
|
|
309
361
|
loading: entry.loading === false ? null : entry.loading,
|
|
310
362
|
transition: entry.transition,
|
|
311
363
|
params,
|
|
@@ -331,6 +383,9 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
331
383
|
deps: SegmentResolutionDeps<TEnv>,
|
|
332
384
|
options?: ResolveSegmentOptions,
|
|
333
385
|
routeKey?: string,
|
|
386
|
+
/** Parent route entry — its loaders are inherited by the layout so
|
|
387
|
+
* parallel slots inside this layout can access them via useLoader(). */
|
|
388
|
+
parentRouteEntry?: EntryData,
|
|
334
389
|
): Promise<ResolvedSegment[]> {
|
|
335
390
|
invariant(
|
|
336
391
|
orphan.type === "layout" || orphan.type === "cache",
|
|
@@ -346,6 +401,26 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
346
401
|
deps,
|
|
347
402
|
);
|
|
348
403
|
segments.push(...loaderSegments);
|
|
404
|
+
|
|
405
|
+
// Inherit parent route's loaders so parallel slots inside this layout
|
|
406
|
+
// can access them via useLoader(). Without this, the route's loaders
|
|
407
|
+
// are only in the route's OutletProvider (rendered as <Outlet /> content),
|
|
408
|
+
// which is a child — not a parent — of the layout's context.
|
|
409
|
+
if (
|
|
410
|
+
parentRouteEntry &&
|
|
411
|
+
parentRouteEntry.loader &&
|
|
412
|
+
parentRouteEntry.loader.length > 0 &&
|
|
413
|
+
Object.keys(orphan.parallel).length > 0
|
|
414
|
+
) {
|
|
415
|
+
const inheritedLoaders = await resolveLoaders(
|
|
416
|
+
parentRouteEntry,
|
|
417
|
+
context,
|
|
418
|
+
belongsToRoute,
|
|
419
|
+
deps,
|
|
420
|
+
orphan.shortCode,
|
|
421
|
+
);
|
|
422
|
+
segments.push(...inheritedLoaders);
|
|
423
|
+
}
|
|
349
424
|
}
|
|
350
425
|
|
|
351
426
|
// Handler-first: orphan layout handler executes before its parallels
|
|
@@ -368,7 +443,10 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
368
443
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
369
444
|
});
|
|
370
445
|
|
|
371
|
-
|
|
446
|
+
const resolvedParallelEntries = new Set<string>();
|
|
447
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
448
|
+
orphan.parallel,
|
|
449
|
+
)) {
|
|
372
450
|
const parallelSegments = await resolveParallelEntry(
|
|
373
451
|
parallelEntry,
|
|
374
452
|
params,
|
|
@@ -378,8 +456,11 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
378
456
|
deps,
|
|
379
457
|
options,
|
|
380
458
|
routeKey,
|
|
459
|
+
[slot],
|
|
460
|
+
!resolvedParallelEntries.has(parallelEntry.id),
|
|
381
461
|
);
|
|
382
462
|
segments.push(...parallelSegments);
|
|
463
|
+
resolvedParallelEntries.add(parallelEntry.id);
|
|
383
464
|
}
|
|
384
465
|
|
|
385
466
|
return segments;
|
|
@@ -397,6 +478,8 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
397
478
|
deps: SegmentResolutionDeps<TEnv>,
|
|
398
479
|
options?: ResolveSegmentOptions,
|
|
399
480
|
routeKey?: string,
|
|
481
|
+
slotNames?: `@${string}`[],
|
|
482
|
+
includeLoaders: boolean = true,
|
|
400
483
|
): Promise<ResolvedSegment[]> {
|
|
401
484
|
invariant(
|
|
402
485
|
parallelEntry.type === "parallel",
|
|
@@ -411,7 +494,12 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
411
494
|
| ReactNode
|
|
412
495
|
>;
|
|
413
496
|
|
|
414
|
-
|
|
497
|
+
const slotsToResolve = slotNames ?? (Object.keys(slots) as `@${string}`[]);
|
|
498
|
+
|
|
499
|
+
for (const slot of slotsToResolve) {
|
|
500
|
+
// Try static lookup first — in production, handler bodies are evicted
|
|
501
|
+
// and replaced with stubs that have no .handler property (undefined).
|
|
502
|
+
// The static store holds the pre-rendered component for these slots.
|
|
415
503
|
let component: ReactNode | undefined = await tryStaticSlot(
|
|
416
504
|
parallelEntry,
|
|
417
505
|
slot,
|
|
@@ -419,6 +507,10 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
419
507
|
);
|
|
420
508
|
|
|
421
509
|
if (component === undefined) {
|
|
510
|
+
const handler = slots[slot];
|
|
511
|
+
if (handler === undefined) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
422
514
|
const doneParallelHandler = track(
|
|
423
515
|
`handler:${parallelEntry.id}.${slot}`,
|
|
424
516
|
2,
|
|
@@ -472,7 +564,7 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
472
564
|
});
|
|
473
565
|
}
|
|
474
566
|
|
|
475
|
-
if (!
|
|
567
|
+
if (!options?.skipLoaders && includeLoaders) {
|
|
476
568
|
const loaderSegments = await resolveLoaders(
|
|
477
569
|
parallelEntry,
|
|
478
570
|
context,
|
|
@@ -480,6 +572,15 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
480
572
|
deps,
|
|
481
573
|
parentShortCode,
|
|
482
574
|
);
|
|
575
|
+
// Tag parallel-owned loaders so renderSegments can stream them
|
|
576
|
+
// using the parallel's loading() instead of awaiting on the layout
|
|
577
|
+
const parallelLoading =
|
|
578
|
+
parallelEntry.loading === false ? undefined : parallelEntry.loading;
|
|
579
|
+
if (parallelLoading) {
|
|
580
|
+
for (const seg of loaderSegments) {
|
|
581
|
+
seg.parallelLoading = parallelLoading;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
483
584
|
segments.push(...loaderSegments);
|
|
484
585
|
}
|
|
485
586
|
|
|
@@ -515,6 +616,13 @@ export async function resolveAllSegments<TEnv>(
|
|
|
515
616
|
} catch {}
|
|
516
617
|
|
|
517
618
|
for (const entry of entries) {
|
|
619
|
+
// Set ALS flag when entering a cache() boundary so that ctx.get()
|
|
620
|
+
// can guard non-cacheable variable reads. Also guards response-level
|
|
621
|
+
// side effects (headers.set). Persists for all descendant entries.
|
|
622
|
+
if (entry.type === "cache") {
|
|
623
|
+
const store = RSCRouterContext.getStore();
|
|
624
|
+
if (store) store.insideCacheScope = true;
|
|
625
|
+
}
|
|
518
626
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
519
627
|
const resolvedSegments = await resolveWithErrorBoundary(
|
|
520
628
|
entry,
|
|
@@ -559,11 +667,66 @@ export async function resolveLoadersOnly<TEnv>(
|
|
|
559
667
|
deps: SegmentResolutionDeps<TEnv>,
|
|
560
668
|
): Promise<ResolvedSegment[]> {
|
|
561
669
|
const loaderSegments: ResolvedSegment[] = [];
|
|
670
|
+
const seenIds = new Set<string>();
|
|
671
|
+
|
|
672
|
+
async function collectEntryLoaders(
|
|
673
|
+
entry: EntryData,
|
|
674
|
+
belongsToRoute: boolean,
|
|
675
|
+
shortCodeOverride?: string,
|
|
676
|
+
): Promise<void> {
|
|
677
|
+
// Skip if all loaders from this entry have already been resolved
|
|
678
|
+
// via a parent (e.g., cache boundary wrapping a layout with shared loaders).
|
|
679
|
+
const entryLoaders = entry.loader ?? [];
|
|
680
|
+
const sc = shortCodeOverride ?? entry.shortCode;
|
|
681
|
+
const allAlreadySeen =
|
|
682
|
+
entryLoaders.length > 0 &&
|
|
683
|
+
entryLoaders.every((le, i) =>
|
|
684
|
+
seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
|
|
685
|
+
);
|
|
686
|
+
if (!allAlreadySeen) {
|
|
687
|
+
const segments = await resolveLoaders(
|
|
688
|
+
entry,
|
|
689
|
+
context,
|
|
690
|
+
belongsToRoute,
|
|
691
|
+
deps,
|
|
692
|
+
shortCodeOverride,
|
|
693
|
+
);
|
|
694
|
+
for (const seg of segments) {
|
|
695
|
+
if (!seenIds.has(seg.id)) {
|
|
696
|
+
seenIds.add(seg.id);
|
|
697
|
+
loaderSegments.push(seg);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const seenParallelEntryIds = new Set<string>();
|
|
703
|
+
for (const parallelEntry of getParallelEntries(entry.parallel)) {
|
|
704
|
+
if (seenParallelEntryIds.has(parallelEntry.id)) continue;
|
|
705
|
+
seenParallelEntryIds.add(parallelEntry.id);
|
|
706
|
+
await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const childBelongsToRoute = belongsToRoute || entry.type === "route";
|
|
710
|
+
for (const layoutEntry of entry.layout) {
|
|
711
|
+
await collectEntryLoaders(layoutEntry, childBelongsToRoute);
|
|
712
|
+
// Inherit route loaders for orphan layouts with parallels
|
|
713
|
+
if (
|
|
714
|
+
entry.type === "route" &&
|
|
715
|
+
entry.loader &&
|
|
716
|
+
entry.loader.length > 0 &&
|
|
717
|
+
Object.keys(layoutEntry.parallel).length > 0
|
|
718
|
+
) {
|
|
719
|
+
await collectEntryLoaders(
|
|
720
|
+
entry,
|
|
721
|
+
childBelongsToRoute,
|
|
722
|
+
layoutEntry.shortCode,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
562
727
|
|
|
563
728
|
for (const entry of entries) {
|
|
564
|
-
|
|
565
|
-
const segments = await resolveLoaders(entry, context, belongsToRoute, deps);
|
|
566
|
-
loaderSegments.push(...segments);
|
|
729
|
+
await collectEntryLoaders(entry, entry.type === "route");
|
|
567
730
|
}
|
|
568
731
|
|
|
569
732
|
return loaderSegments;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Error boundary segment creation
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type
|
|
11
|
+
import { createElement, type ReactNode } from "react";
|
|
12
12
|
import { DataNotFoundError } from "../../errors";
|
|
13
13
|
import {
|
|
14
14
|
createErrorInfo,
|
|
@@ -180,34 +180,39 @@ export function catchSegmentError<TEnv>(
|
|
|
180
180
|
|
|
181
181
|
if (error instanceof DataNotFoundError) {
|
|
182
182
|
const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
|
|
183
|
+
// Fall back to router's notFound component, then a plain default
|
|
184
|
+
const notFoundOption = deps.notFoundComponent;
|
|
185
|
+
const defaultFallback =
|
|
186
|
+
typeof notFoundOption === "function"
|
|
187
|
+
? notFoundOption({ pathname: pathname ?? "" })
|
|
188
|
+
: (notFoundOption ?? createElement("h1", null, "Not Found"));
|
|
189
|
+
const effectiveNotFoundFallback = notFoundFallback ?? defaultFallback;
|
|
183
190
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
);
|
|
191
|
+
const notFoundInfo = createNotFoundInfo(
|
|
192
|
+
error,
|
|
193
|
+
entry.shortCode,
|
|
194
|
+
entry.type,
|
|
195
|
+
pathname,
|
|
196
|
+
);
|
|
191
197
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
reportError(true, {
|
|
199
|
+
notFound: true,
|
|
200
|
+
message: notFoundInfo.message,
|
|
201
|
+
});
|
|
196
202
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
debugLog("segment", "notFound boundary handled error", {
|
|
204
|
+
segmentId: entry.shortCode,
|
|
205
|
+
message: notFoundInfo.message,
|
|
206
|
+
});
|
|
201
207
|
|
|
202
|
-
|
|
208
|
+
setResponseStatus(404);
|
|
203
209
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
210
|
+
return createNotFoundSegment(
|
|
211
|
+
notFoundInfo,
|
|
212
|
+
effectiveNotFoundFallback,
|
|
213
|
+
entry,
|
|
214
|
+
params,
|
|
215
|
+
);
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
const fallback = deps.findNearestErrorBoundary(entry);
|
|
@@ -147,6 +147,7 @@ export function resolveLoaderData<TEnv>(
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
const loaderId = loaderEntry.loader.$$id;
|
|
150
|
+
|
|
150
151
|
const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
|
|
151
152
|
const swrWindow = resolveSwrWindow(options.swr, store.defaults);
|
|
152
153
|
const swr = swrWindow || undefined;
|