@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c
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 +172 -50
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +1160 -508
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +17 -16
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +49 -8
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +61 -51
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +91 -17
- package/skills/loader/SKILL.md +107 -24
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +24 -23
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +58 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +38 -24
- package/src/__internal.ts +92 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +175 -17
- package/src/browser/navigation-client.ts +177 -44
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +113 -17
- package/src/browser/prefetch/cache.ts +275 -28
- package/src/browser/prefetch/fetch.ts +191 -46
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +98 -14
- package/src/browser/react/NavigationProvider.tsx +89 -14
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/rsc-router.tsx +177 -66
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +73 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +67 -25
- 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 +455 -15
- 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.rsc.tsx +2 -1
- package/src/client.tsx +85 -276
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +9 -36
- package/src/index.ts +79 -70
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +27 -2
- package/src/route-definition/dsl-helpers.ts +240 -40
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +129 -26
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +10 -7
- package/src/router/loader-resolution.ts +160 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -193
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +94 -17
- 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 +103 -18
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +48 -27
- package/src/router/middleware.ts +201 -86
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +77 -11
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +50 -5
- package/src/router/router-options.ts +50 -19
- package/src/router/segment-resolution/fresh.ts +215 -19
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +454 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +30 -6
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +89 -17
- package/src/rsc/handler.ts +563 -364
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +37 -10
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +47 -44
- package/src/rsc/server-action.ts +24 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +11 -1
- package/src/search-params.ts +16 -13
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +109 -23
- package/src/server/context.ts +174 -19
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +218 -65
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +140 -72
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +2 -5
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +18 -16
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +61 -89
- package/src/vite/discovery/discover-routers.ts +7 -4
- package/src/vite/discovery/prerender-collection.ts +162 -88
- package/src/vite/discovery/state.ts +17 -13
- package/src/vite/index.ts +8 -3
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +190 -217
- package/src/vite/router-discovery.ts +241 -45
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/package-resolution.ts +34 -1
- package/src/vite/utils/prerender-utils.ts +97 -5
- package/src/vite/utils/shared-utils.ts +3 -2
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
|
@@ -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,6 +30,11 @@ import {
|
|
|
24
30
|
} from "./helpers.js";
|
|
25
31
|
import { getRouterContext } from "../router-context.js";
|
|
26
32
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
33
|
+
import {
|
|
34
|
+
track,
|
|
35
|
+
RSCRouterContext,
|
|
36
|
+
runInsideLoaderScope,
|
|
37
|
+
} from "../../server/context.js";
|
|
27
38
|
|
|
28
39
|
// ---------------------------------------------------------------------------
|
|
29
40
|
// Streamed handler telemetry
|
|
@@ -89,9 +100,11 @@ export async function resolveLoaders<TEnv>(
|
|
|
89
100
|
const shortCode = shortCodeOverride ?? entry.shortCode;
|
|
90
101
|
const hasLoading = "loading" in entry && entry.loading !== undefined;
|
|
91
102
|
const loadingDisabled = hasLoading && entry.loading === false;
|
|
103
|
+
const ms = _getRequestContext()?._metricsStore;
|
|
92
104
|
|
|
93
105
|
if (!loadingDisabled) {
|
|
94
|
-
|
|
106
|
+
// Streaming loaders: promises kick off now, settle during RSC serialization.
|
|
107
|
+
const segments = loaderEntries.map((loaderEntry, i) => {
|
|
95
108
|
const { loader } = loaderEntry;
|
|
96
109
|
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
97
110
|
return {
|
|
@@ -103,7 +116,9 @@ export async function resolveLoaders<TEnv>(
|
|
|
103
116
|
params: ctx.params,
|
|
104
117
|
loaderId: loader.$$id,
|
|
105
118
|
loaderData: deps.wrapLoaderPromise(
|
|
106
|
-
|
|
119
|
+
runInsideLoaderScope(() =>
|
|
120
|
+
resolveLoaderData(loaderEntry, ctx, ctx.pathname),
|
|
121
|
+
),
|
|
107
122
|
entry,
|
|
108
123
|
segmentId,
|
|
109
124
|
ctx.pathname,
|
|
@@ -111,18 +126,38 @@ export async function resolveLoaders<TEnv>(
|
|
|
111
126
|
belongsToRoute,
|
|
112
127
|
};
|
|
113
128
|
});
|
|
129
|
+
|
|
130
|
+
return segments;
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
// Loading disabled: still start all loaders in parallel, but only emit
|
|
117
134
|
// settled promises so handlers don't stream loading placeholders.
|
|
118
|
-
const pendingLoaderData = loaderEntries.map((loaderEntry) =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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));
|
|
122
143
|
|
|
123
144
|
return loaderEntries.map((loaderEntry, i) => {
|
|
124
145
|
const { loader } = loaderEntry;
|
|
125
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
|
+
}
|
|
126
161
|
return {
|
|
127
162
|
id: segmentId,
|
|
128
163
|
namespace: entry.id,
|
|
@@ -132,7 +167,7 @@ export async function resolveLoaders<TEnv>(
|
|
|
132
167
|
params: ctx.params,
|
|
133
168
|
loaderId: loader.$$id,
|
|
134
169
|
loaderData: deps.wrapLoaderPromise(
|
|
135
|
-
|
|
170
|
+
pending.promise,
|
|
136
171
|
entry,
|
|
137
172
|
segmentId,
|
|
138
173
|
ctx.pathname,
|
|
@@ -178,7 +213,9 @@ export async function resolveSegment<TEnv>(
|
|
|
178
213
|
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
179
214
|
entry.shortCode;
|
|
180
215
|
|
|
216
|
+
const doneLayoutHandler = track(`handler:${entry.id}`, 2);
|
|
181
217
|
const component = await resolveLayoutComponent(entry, context);
|
|
218
|
+
doneLayoutHandler();
|
|
182
219
|
|
|
183
220
|
segments.push({
|
|
184
221
|
id: entry.shortCode,
|
|
@@ -194,7 +231,10 @@ export async function resolveSegment<TEnv>(
|
|
|
194
231
|
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
195
232
|
});
|
|
196
233
|
|
|
197
|
-
|
|
234
|
+
const resolvedParallelEntries = new Set<string>();
|
|
235
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
236
|
+
entry.parallel,
|
|
237
|
+
)) {
|
|
198
238
|
const parallelSegments = await resolveParallelEntry(
|
|
199
239
|
parallelEntry,
|
|
200
240
|
params,
|
|
@@ -204,8 +244,11 @@ export async function resolveSegment<TEnv>(
|
|
|
204
244
|
deps,
|
|
205
245
|
options,
|
|
206
246
|
routeKey,
|
|
247
|
+
[slot],
|
|
248
|
+
!resolvedParallelEntries.has(parallelEntry.id),
|
|
207
249
|
);
|
|
208
250
|
segments.push(...parallelSegments);
|
|
251
|
+
resolvedParallelEntries.add(parallelEntry.id);
|
|
209
252
|
}
|
|
210
253
|
|
|
211
254
|
for (const orphan of entry.layout) {
|
|
@@ -241,9 +284,16 @@ export async function resolveSegment<TEnv>(
|
|
|
241
284
|
entry.shortCode,
|
|
242
285
|
);
|
|
243
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;
|
|
292
|
+
const doneRouteHandler = track(`handler:${entry.id}`, 2);
|
|
244
293
|
if (entry.loading) {
|
|
245
|
-
const result = handleHandlerResult(
|
|
294
|
+
const result = handleHandlerResult(handler(context));
|
|
246
295
|
if (result instanceof Promise) {
|
|
296
|
+
result.finally(doneRouteHandler).catch(() => {});
|
|
247
297
|
const tracked = deps.trackHandler(result, {
|
|
248
298
|
segmentId: entry.shortCode,
|
|
249
299
|
segmentType: entry.type,
|
|
@@ -258,10 +308,12 @@ export async function resolveSegment<TEnv>(
|
|
|
258
308
|
);
|
|
259
309
|
component = tracked;
|
|
260
310
|
} else {
|
|
311
|
+
doneRouteHandler();
|
|
261
312
|
component = result;
|
|
262
313
|
}
|
|
263
314
|
} else {
|
|
264
|
-
component = handleHandlerResult(await
|
|
315
|
+
component = handleHandlerResult(await handler(context));
|
|
316
|
+
doneRouteHandler();
|
|
265
317
|
}
|
|
266
318
|
}
|
|
267
319
|
|
|
@@ -275,11 +327,15 @@ export async function resolveSegment<TEnv>(
|
|
|
275
327
|
deps,
|
|
276
328
|
options,
|
|
277
329
|
routeKey,
|
|
330
|
+
entry,
|
|
278
331
|
);
|
|
279
332
|
segments.push(...orphanSegments);
|
|
280
333
|
}
|
|
281
334
|
|
|
282
|
-
|
|
335
|
+
const resolvedParallelEntries = new Set<string>();
|
|
336
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
337
|
+
entry.parallel,
|
|
338
|
+
)) {
|
|
283
339
|
const parallelSegments = await resolveParallelEntry(
|
|
284
340
|
parallelEntry,
|
|
285
341
|
params,
|
|
@@ -289,8 +345,11 @@ export async function resolveSegment<TEnv>(
|
|
|
289
345
|
deps,
|
|
290
346
|
options,
|
|
291
347
|
routeKey,
|
|
348
|
+
[slot],
|
|
349
|
+
!resolvedParallelEntries.has(parallelEntry.id),
|
|
292
350
|
);
|
|
293
351
|
segments.push(...parallelSegments);
|
|
352
|
+
resolvedParallelEntries.add(parallelEntry.id);
|
|
294
353
|
}
|
|
295
354
|
|
|
296
355
|
segments.push({
|
|
@@ -298,7 +357,7 @@ export async function resolveSegment<TEnv>(
|
|
|
298
357
|
namespace: entry.id,
|
|
299
358
|
type: "route",
|
|
300
359
|
index: 0,
|
|
301
|
-
component,
|
|
360
|
+
component: component ?? null,
|
|
302
361
|
loading: entry.loading === false ? null : entry.loading,
|
|
303
362
|
transition: entry.transition,
|
|
304
363
|
params,
|
|
@@ -324,6 +383,9 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
324
383
|
deps: SegmentResolutionDeps<TEnv>,
|
|
325
384
|
options?: ResolveSegmentOptions,
|
|
326
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,
|
|
327
389
|
): Promise<ResolvedSegment[]> {
|
|
328
390
|
invariant(
|
|
329
391
|
orphan.type === "layout" || orphan.type === "cache",
|
|
@@ -339,11 +401,37 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
339
401
|
deps,
|
|
340
402
|
);
|
|
341
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
|
+
// Tag as inherited so buildMatchResult can deduplicate when safe
|
|
423
|
+
for (const s of inheritedLoaders) {
|
|
424
|
+
s._inherited = true;
|
|
425
|
+
}
|
|
426
|
+
segments.push(...inheritedLoaders);
|
|
427
|
+
}
|
|
342
428
|
}
|
|
343
429
|
|
|
344
430
|
// Handler-first: orphan layout handler executes before its parallels
|
|
345
431
|
// so that ctx.set() values are visible to parallel children.
|
|
432
|
+
const doneOrphanHandler = track(`handler:${orphan.id}`, 2);
|
|
346
433
|
const component = await resolveLayoutComponent(orphan, context);
|
|
434
|
+
doneOrphanHandler();
|
|
347
435
|
|
|
348
436
|
segments.push({
|
|
349
437
|
id: orphan.shortCode,
|
|
@@ -359,7 +447,10 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
359
447
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
360
448
|
});
|
|
361
449
|
|
|
362
|
-
|
|
450
|
+
const resolvedParallelEntries = new Set<string>();
|
|
451
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
452
|
+
orphan.parallel,
|
|
453
|
+
)) {
|
|
363
454
|
const parallelSegments = await resolveParallelEntry(
|
|
364
455
|
parallelEntry,
|
|
365
456
|
params,
|
|
@@ -369,8 +460,11 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
369
460
|
deps,
|
|
370
461
|
options,
|
|
371
462
|
routeKey,
|
|
463
|
+
[slot],
|
|
464
|
+
!resolvedParallelEntries.has(parallelEntry.id),
|
|
372
465
|
);
|
|
373
466
|
segments.push(...parallelSegments);
|
|
467
|
+
resolvedParallelEntries.add(parallelEntry.id);
|
|
374
468
|
}
|
|
375
469
|
|
|
376
470
|
return segments;
|
|
@@ -388,6 +482,8 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
388
482
|
deps: SegmentResolutionDeps<TEnv>,
|
|
389
483
|
options?: ResolveSegmentOptions,
|
|
390
484
|
routeKey?: string,
|
|
485
|
+
slotNames?: `@${string}`[],
|
|
486
|
+
includeLoaders: boolean = true,
|
|
391
487
|
): Promise<ResolvedSegment[]> {
|
|
392
488
|
invariant(
|
|
393
489
|
parallelEntry.type === "parallel",
|
|
@@ -402,7 +498,12 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
402
498
|
| ReactNode
|
|
403
499
|
>;
|
|
404
500
|
|
|
405
|
-
|
|
501
|
+
const slotsToResolve = slotNames ?? (Object.keys(slots) as `@${string}`[]);
|
|
502
|
+
|
|
503
|
+
for (const slot of slotsToResolve) {
|
|
504
|
+
// Try static lookup first — in production, handler bodies are evicted
|
|
505
|
+
// and replaced with stubs that have no .handler property (undefined).
|
|
506
|
+
// The static store holds the pre-rendered component for these slots.
|
|
406
507
|
let component: ReactNode | undefined = await tryStaticSlot(
|
|
407
508
|
parallelEntry,
|
|
408
509
|
slot,
|
|
@@ -410,12 +511,21 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
410
511
|
);
|
|
411
512
|
|
|
412
513
|
if (component === undefined) {
|
|
514
|
+
const handler = slots[slot];
|
|
515
|
+
if (handler === undefined) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const doneParallelHandler = track(
|
|
519
|
+
`handler:${parallelEntry.id}.${slot}`,
|
|
520
|
+
2,
|
|
521
|
+
);
|
|
413
522
|
const hasLoadingFallback =
|
|
414
523
|
parallelEntry.loading !== undefined && parallelEntry.loading !== false;
|
|
415
524
|
if (hasLoadingFallback) {
|
|
416
525
|
const result =
|
|
417
526
|
typeof handler === "function" ? handler(context) : handler;
|
|
418
527
|
if (result instanceof Promise) {
|
|
528
|
+
result.finally(doneParallelHandler).catch(() => {});
|
|
419
529
|
const tracked = deps.trackHandler(result, {
|
|
420
530
|
segmentId: `${parentShortCode}.${slot}`,
|
|
421
531
|
segmentType: "parallel",
|
|
@@ -430,11 +540,13 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
430
540
|
);
|
|
431
541
|
component = tracked as ReactNode;
|
|
432
542
|
} else {
|
|
543
|
+
doneParallelHandler();
|
|
433
544
|
component = result as ReactNode;
|
|
434
545
|
}
|
|
435
546
|
} else {
|
|
436
547
|
component =
|
|
437
548
|
typeof handler === "function" ? await handler(context) : handler;
|
|
549
|
+
doneParallelHandler();
|
|
438
550
|
}
|
|
439
551
|
}
|
|
440
552
|
|
|
@@ -456,7 +568,7 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
456
568
|
});
|
|
457
569
|
}
|
|
458
570
|
|
|
459
|
-
if (!
|
|
571
|
+
if (!options?.skipLoaders && includeLoaders) {
|
|
460
572
|
const loaderSegments = await resolveLoaders(
|
|
461
573
|
parallelEntry,
|
|
462
574
|
context,
|
|
@@ -464,6 +576,15 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
464
576
|
deps,
|
|
465
577
|
parentShortCode,
|
|
466
578
|
);
|
|
579
|
+
// Tag parallel-owned loaders so renderSegments can stream them
|
|
580
|
+
// using the parallel's loading() instead of awaiting on the layout
|
|
581
|
+
const parallelLoading =
|
|
582
|
+
parallelEntry.loading === false ? undefined : parallelEntry.loading;
|
|
583
|
+
if (parallelLoading) {
|
|
584
|
+
for (const seg of loaderSegments) {
|
|
585
|
+
seg.parallelLoading = parallelLoading;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
467
588
|
segments.push(...loaderSegments);
|
|
468
589
|
}
|
|
469
590
|
|
|
@@ -499,6 +620,14 @@ export async function resolveAllSegments<TEnv>(
|
|
|
499
620
|
} catch {}
|
|
500
621
|
|
|
501
622
|
for (const entry of entries) {
|
|
623
|
+
// Set ALS flag when entering a cache() boundary so that ctx.get()
|
|
624
|
+
// can guard non-cacheable variable reads. Also guards response-level
|
|
625
|
+
// side effects (headers.set). Persists for all descendant entries.
|
|
626
|
+
if (entry.type === "cache") {
|
|
627
|
+
const store = RSCRouterContext.getStore();
|
|
628
|
+
if (store) store.insideCacheScope = true;
|
|
629
|
+
}
|
|
630
|
+
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
502
631
|
const resolvedSegments = await resolveWithErrorBoundary(
|
|
503
632
|
entry,
|
|
504
633
|
params,
|
|
@@ -518,6 +647,7 @@ export async function resolveAllSegments<TEnv>(
|
|
|
518
647
|
{ request: safeRequest, url: context.url, routeKey, telemetry },
|
|
519
648
|
context.pathname,
|
|
520
649
|
);
|
|
650
|
+
doneEntry();
|
|
521
651
|
// Deduplicate by segment ID. include() scopes can produce entries that
|
|
522
652
|
// resolve the same shared layout/loader segment. Duplicates in the segment
|
|
523
653
|
// array propagate to the client's matched[] and change the React tree depth.
|
|
@@ -541,11 +671,77 @@ export async function resolveLoadersOnly<TEnv>(
|
|
|
541
671
|
deps: SegmentResolutionDeps<TEnv>,
|
|
542
672
|
): Promise<ResolvedSegment[]> {
|
|
543
673
|
const loaderSegments: ResolvedSegment[] = [];
|
|
674
|
+
const seenIds = new Set<string>();
|
|
675
|
+
|
|
676
|
+
async function collectEntryLoaders(
|
|
677
|
+
entry: EntryData,
|
|
678
|
+
belongsToRoute: boolean,
|
|
679
|
+
shortCodeOverride?: string,
|
|
680
|
+
): Promise<void> {
|
|
681
|
+
// Skip if all loaders from this entry have already been resolved
|
|
682
|
+
// via a parent (e.g., cache boundary wrapping a layout with shared loaders).
|
|
683
|
+
const entryLoaders = entry.loader ?? [];
|
|
684
|
+
const sc = shortCodeOverride ?? entry.shortCode;
|
|
685
|
+
const allAlreadySeen =
|
|
686
|
+
entryLoaders.length > 0 &&
|
|
687
|
+
entryLoaders.every((le, i) =>
|
|
688
|
+
seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
|
|
689
|
+
);
|
|
690
|
+
if (!allAlreadySeen) {
|
|
691
|
+
const segments = await resolveLoaders(
|
|
692
|
+
entry,
|
|
693
|
+
context,
|
|
694
|
+
belongsToRoute,
|
|
695
|
+
deps,
|
|
696
|
+
shortCodeOverride,
|
|
697
|
+
);
|
|
698
|
+
for (const seg of segments) {
|
|
699
|
+
if (!seenIds.has(seg.id)) {
|
|
700
|
+
seenIds.add(seg.id);
|
|
701
|
+
loaderSegments.push(seg);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const seenParallelEntryIds = new Set<string>();
|
|
707
|
+
for (const parallelEntry of getParallelEntries(entry.parallel)) {
|
|
708
|
+
if (seenParallelEntryIds.has(parallelEntry.id)) continue;
|
|
709
|
+
seenParallelEntryIds.add(parallelEntry.id);
|
|
710
|
+
await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const childBelongsToRoute = belongsToRoute || entry.type === "route";
|
|
714
|
+
for (const layoutEntry of entry.layout) {
|
|
715
|
+
await collectEntryLoaders(layoutEntry, childBelongsToRoute);
|
|
716
|
+
// Inherit route loaders for orphan layouts with parallels.
|
|
717
|
+
// Resolve directly — do NOT re-enter collectEntryLoaders with the
|
|
718
|
+
// route entry, as that would re-iterate route.layout and loop.
|
|
719
|
+
if (
|
|
720
|
+
entry.type === "route" &&
|
|
721
|
+
entry.loader &&
|
|
722
|
+
entry.loader.length > 0 &&
|
|
723
|
+
Object.keys(layoutEntry.parallel).length > 0
|
|
724
|
+
) {
|
|
725
|
+
const inherited = await resolveLoaders(
|
|
726
|
+
entry,
|
|
727
|
+
context,
|
|
728
|
+
childBelongsToRoute,
|
|
729
|
+
deps,
|
|
730
|
+
layoutEntry.shortCode,
|
|
731
|
+
);
|
|
732
|
+
for (const seg of inherited) {
|
|
733
|
+
if (!seenIds.has(seg.id)) {
|
|
734
|
+
seenIds.add(seg.id);
|
|
735
|
+
seg._inherited = true;
|
|
736
|
+
loaderSegments.push(seg);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
544
742
|
|
|
545
743
|
for (const entry of entries) {
|
|
546
|
-
|
|
547
|
-
const segments = await resolveLoaders(entry, context, belongsToRoute, deps);
|
|
548
|
-
loaderSegments.push(...segments);
|
|
744
|
+
await collectEntryLoaders(entry, entry.type === "route");
|
|
549
745
|
}
|
|
550
746
|
|
|
551
747
|
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,
|
|
@@ -174,40 +174,45 @@ export function catchSegmentError<TEnv>(
|
|
|
174
174
|
const setResponseStatus = (status: number) => {
|
|
175
175
|
const reqCtx = getRequestContext();
|
|
176
176
|
if (reqCtx) {
|
|
177
|
-
reqCtx.
|
|
177
|
+
reqCtx._setStatus(status);
|
|
178
178
|
}
|
|
179
179
|
};
|
|
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;
|