@rangojs/router 0.0.0-experimental.fa8a383a → 0.0.0-experimental.fb4fdc18
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 +188 -35
- package/dist/bin/rango.js +130 -47
- package/dist/vite/index.js +1884 -537
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +7 -5
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +8 -0
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +33 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +93 -17
- package/skills/loader/SKILL.md +123 -46
- package/skills/middleware/SKILL.md +36 -3
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/parallel/SKILL.md +133 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/rango/SKILL.md +26 -22
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +75 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +19 -1
- package/src/__internal.ts +1 -1
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +44 -4
- package/src/browser/navigation-bridge.ts +95 -7
- package/src/browser/navigation-client.ts +128 -53
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/partial-update.ts +93 -12
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +156 -18
- package/src/browser/prefetch/queue.ts +92 -29
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +72 -8
- package/src/browser/react/NavigationProvider.tsx +82 -21
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- 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 +17 -4
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/rsc-router.tsx +60 -9
- package/src/browser/scroll-restoration.ts +10 -8
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +46 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +46 -5
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/cache/taint.ts +55 -0
- package/src/client.tsx +84 -230
- package/src/context-var.ts +72 -2
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +6 -1
- package/src/index.ts +49 -6
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +28 -2
- package/src/route-definition/dsl-helpers.ts +210 -35
- package/src/route-definition/helpers-types.ts +73 -20
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +9 -1
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +102 -25
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/lazy-includes.ts +6 -6
- package/src/router/loader-resolution.ts +159 -21
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +128 -192
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-middleware/background-revalidation.ts +12 -1
- package/src/router/match-middleware/cache-lookup.ts +74 -14
- package/src/router/match-middleware/cache-store.ts +21 -4
- package/src/router/match-middleware/segment-resolution.ts +53 -0
- package/src/router/match-result.ts +112 -9
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +20 -33
- package/src/router/middleware.ts +56 -12
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +101 -17
- 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/revalidation.ts +15 -1
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +114 -18
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +257 -127
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +55 -7
- package/src/rsc/handler.ts +478 -383
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +18 -2
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +20 -1
- package/src/rsc/server-action.ts +12 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +15 -1
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +22 -62
- package/src/server/context.ts +76 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +185 -57
- package/src/ssr/index.tsx +8 -1
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +145 -68
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +12 -1
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +47 -12
- 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/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +36 -4
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +175 -74
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +13 -4
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +60 -5
- package/src/vite/plugins/cjs-to-esm.ts +5 -0
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +16 -4
- 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 +52 -28
- 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-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +563 -316
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/use-cache-transform.ts +56 -43
- package/src/vite/plugins/version-injector.ts +37 -11
- package/src/vite/rango.ts +63 -11
- package/src/vite/router-discovery.ts +732 -86
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +38 -5
- package/src/vite/utils/shared-utils.ts +3 -2
|
@@ -41,7 +41,11 @@ import {
|
|
|
41
41
|
} from "./helpers.js";
|
|
42
42
|
import { getRouterContext } from "../router-context.js";
|
|
43
43
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
track,
|
|
46
|
+
RSCRouterContext,
|
|
47
|
+
runInsideLoaderScope,
|
|
48
|
+
} from "../../server/context.js";
|
|
45
49
|
|
|
46
50
|
// ---------------------------------------------------------------------------
|
|
47
51
|
// Telemetry helpers
|
|
@@ -85,6 +89,27 @@ function observeStreamedHandler(
|
|
|
85
89
|
});
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Trace a parallel slot that's being force-rendered on a full refetch (client
|
|
94
|
+
* has no cached state). User revalidate fns are bypassed in this case — see
|
|
95
|
+
* the call sites for the load-bearing rationale.
|
|
96
|
+
*/
|
|
97
|
+
function traceFullRefetchedParallelSlot(
|
|
98
|
+
parallelId: string,
|
|
99
|
+
belongsToRoute: boolean,
|
|
100
|
+
): void {
|
|
101
|
+
if (!isTraceActive()) return;
|
|
102
|
+
pushRevalidationTraceEntry({
|
|
103
|
+
segmentId: parallelId,
|
|
104
|
+
segmentType: "parallel",
|
|
105
|
+
belongsToRoute,
|
|
106
|
+
source: "parallel",
|
|
107
|
+
defaultShouldRevalidate: true,
|
|
108
|
+
finalShouldRevalidate: true,
|
|
109
|
+
reason: "full-refetch",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
88
113
|
// ---------------------------------------------------------------------------
|
|
89
114
|
// Revalidation telemetry helper
|
|
90
115
|
// ---------------------------------------------------------------------------
|
|
@@ -232,7 +257,9 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
232
257
|
params: ctx.params,
|
|
233
258
|
loaderId: loader.$$id,
|
|
234
259
|
loaderData: deps.wrapLoaderPromise(
|
|
235
|
-
|
|
260
|
+
runInsideLoaderScope(() =>
|
|
261
|
+
resolveLoaderData(loaderEntry, ctx, ctx.pathname),
|
|
262
|
+
),
|
|
236
263
|
entry,
|
|
237
264
|
segmentId,
|
|
238
265
|
ctx.pathname,
|
|
@@ -262,29 +289,46 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
|
|
|
262
289
|
): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
|
|
263
290
|
const allLoaderSegments: ResolvedSegment[] = [];
|
|
264
291
|
const allMatchedIds: string[] = [];
|
|
292
|
+
const seenIds = new Set<string>();
|
|
265
293
|
|
|
266
294
|
async function collectEntryLoaders(
|
|
267
295
|
entry: EntryData,
|
|
268
296
|
belongsToRoute: boolean,
|
|
269
297
|
shortCodeOverride?: string,
|
|
270
298
|
): Promise<void> {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
299
|
+
// Skip if all loaders from this entry have already been resolved
|
|
300
|
+
// via a parent (e.g., cache boundary wrapping a layout with shared loaders).
|
|
301
|
+
const loaderEntries = entry.loader ?? [];
|
|
302
|
+
const sc = shortCodeOverride ?? entry.shortCode;
|
|
303
|
+
const allAlreadySeen =
|
|
304
|
+
loaderEntries.length > 0 &&
|
|
305
|
+
loaderEntries.every((le, i) =>
|
|
306
|
+
seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
|
|
307
|
+
);
|
|
308
|
+
if (!allAlreadySeen) {
|
|
309
|
+
const { segments, matchedIds } = await resolveLoadersWithRevalidation(
|
|
310
|
+
entry,
|
|
311
|
+
context,
|
|
312
|
+
belongsToRoute,
|
|
313
|
+
clientSegmentIds,
|
|
314
|
+
prevParams,
|
|
315
|
+
request,
|
|
316
|
+
prevUrl,
|
|
317
|
+
nextUrl,
|
|
318
|
+
routeKey,
|
|
319
|
+
deps,
|
|
320
|
+
actionContext,
|
|
321
|
+
shortCodeOverride,
|
|
322
|
+
stale,
|
|
323
|
+
);
|
|
324
|
+
for (const seg of segments) {
|
|
325
|
+
if (!seenIds.has(seg.id)) {
|
|
326
|
+
seenIds.add(seg.id);
|
|
327
|
+
allLoaderSegments.push(seg);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
allMatchedIds.push(...matchedIds);
|
|
331
|
+
}
|
|
288
332
|
|
|
289
333
|
const seenParallelEntryIds = new Set<string>();
|
|
290
334
|
for (const parallelEntry of getParallelEntries(entry.parallel)) {
|
|
@@ -296,6 +340,39 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
|
|
|
296
340
|
const childBelongsToRoute = belongsToRoute || entry.type === "route";
|
|
297
341
|
for (const layoutEntry of entry.layout) {
|
|
298
342
|
await collectEntryLoaders(layoutEntry, childBelongsToRoute);
|
|
343
|
+
// Inherit route loaders for orphan layouts with parallels.
|
|
344
|
+
// Resolve directly — do NOT re-enter collectEntryLoaders with the
|
|
345
|
+
// route entry, as that would re-iterate route.layout and loop.
|
|
346
|
+
if (
|
|
347
|
+
entry.type === "route" &&
|
|
348
|
+
entry.loader &&
|
|
349
|
+
entry.loader.length > 0 &&
|
|
350
|
+
Object.keys(layoutEntry.parallel).length > 0
|
|
351
|
+
) {
|
|
352
|
+
const inherited = await resolveLoadersWithRevalidation(
|
|
353
|
+
entry,
|
|
354
|
+
context,
|
|
355
|
+
childBelongsToRoute,
|
|
356
|
+
clientSegmentIds,
|
|
357
|
+
prevParams,
|
|
358
|
+
request,
|
|
359
|
+
prevUrl,
|
|
360
|
+
nextUrl,
|
|
361
|
+
routeKey,
|
|
362
|
+
deps,
|
|
363
|
+
actionContext,
|
|
364
|
+
layoutEntry.shortCode,
|
|
365
|
+
stale,
|
|
366
|
+
);
|
|
367
|
+
for (const seg of inherited.segments) {
|
|
368
|
+
if (!seenIds.has(seg.id)) {
|
|
369
|
+
seenIds.add(seg.id);
|
|
370
|
+
seg._inherited = true;
|
|
371
|
+
allLoaderSegments.push(seg);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
allMatchedIds.push(...inherited.matchedIds);
|
|
375
|
+
}
|
|
299
376
|
}
|
|
300
377
|
}
|
|
301
378
|
|
|
@@ -392,44 +469,30 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
392
469
|
|
|
393
470
|
const isFullRefetch = clientSegmentIds.size === 0;
|
|
394
471
|
const isNewParent = !clientSegmentIds.has(entry.shortCode);
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
) {
|
|
401
|
-
matchedIds.push(parallelId);
|
|
402
|
-
}
|
|
472
|
+
// Always announce the slot in matchedIds — it's unconditionally appended
|
|
473
|
+
// to `segments` below, and a segment present in segments but missing from
|
|
474
|
+
// matched lets the client prune it (then it's missing from clientSegmentIds
|
|
475
|
+
// on the next request, perpetuating the staleness).
|
|
476
|
+
matchedIds.push(parallelId);
|
|
403
477
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
return true;
|
|
418
|
-
}
|
|
478
|
+
let shouldResolve: boolean;
|
|
479
|
+
if (isFullRefetch) {
|
|
480
|
+
// Client has nothing cached — slot MUST render. User revalidate fns are
|
|
481
|
+
// bypassed here because returning false would leave the segment blank
|
|
482
|
+
// with no client-side fallback.
|
|
483
|
+
traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
|
|
484
|
+
shouldResolve = true;
|
|
485
|
+
} else {
|
|
486
|
+
// For non-empty client sets, consult user revalidate fns. When the slot
|
|
487
|
+
// is unknown to the client, override the type-derived default so the
|
|
488
|
+
// soft chain seeds with the right "new segment" / "parent-chain" value.
|
|
489
|
+
let defaultOverride: { value: boolean; reason: string } | undefined;
|
|
419
490
|
if (!clientSegmentIds.has(parallelId)) {
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
belongsToRoute,
|
|
426
|
-
source: "parallel",
|
|
427
|
-
defaultShouldRevalidate: result,
|
|
428
|
-
finalShouldRevalidate: result,
|
|
429
|
-
reason: result ? "new-segment" : "skip-parent-chain",
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
return result;
|
|
491
|
+
const value = belongsToRoute || isNewParent;
|
|
492
|
+
defaultOverride = {
|
|
493
|
+
value,
|
|
494
|
+
reason: value ? "new-segment" : "skip-parent-chain",
|
|
495
|
+
};
|
|
433
496
|
}
|
|
434
497
|
|
|
435
498
|
const dummySegment: ResolvedSegment = {
|
|
@@ -447,7 +510,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
447
510
|
: {}),
|
|
448
511
|
};
|
|
449
512
|
|
|
450
|
-
|
|
513
|
+
shouldResolve = await evaluateRevalidation({
|
|
451
514
|
segment: dummySegment,
|
|
452
515
|
prevParams,
|
|
453
516
|
getPrevSegment: null,
|
|
@@ -463,8 +526,9 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
463
526
|
actionContext,
|
|
464
527
|
stale,
|
|
465
528
|
traceSource: "parallel",
|
|
529
|
+
defaultOverride,
|
|
466
530
|
});
|
|
467
|
-
}
|
|
531
|
+
}
|
|
468
532
|
emitRevalidationDecision(
|
|
469
533
|
parallelId,
|
|
470
534
|
context.pathname,
|
|
@@ -473,8 +537,11 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
473
537
|
);
|
|
474
538
|
|
|
475
539
|
let component: ReactNode | undefined;
|
|
540
|
+
let handlerRan = false;
|
|
476
541
|
if (shouldResolve) {
|
|
477
542
|
component = await tryStaticSlot(parallelEntry, slot, parallelId);
|
|
543
|
+
// tryStaticSlot returning a value means the static cache supplied the
|
|
544
|
+
// component — handler did NOT run. handlerRan stays false.
|
|
478
545
|
}
|
|
479
546
|
if (component === undefined) {
|
|
480
547
|
const hasLoadingFallback =
|
|
@@ -485,29 +552,37 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
485
552
|
// Handler evicted (production static slot) but static lookup missed.
|
|
486
553
|
// Nothing to render — use null so the client keeps its cached version.
|
|
487
554
|
component = null;
|
|
488
|
-
} else
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
555
|
+
} else {
|
|
556
|
+
// Slot-keyed pushes — slot owns its own bucket, parent layout owns
|
|
557
|
+
// its own. On slot-only revalidations the partial merge updates only
|
|
558
|
+
// the slot's bucket; the parent's bucket stays intact.
|
|
559
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
560
|
+
parallelId;
|
|
561
|
+
handlerRan = true;
|
|
562
|
+
if (hasLoadingFallback) {
|
|
563
|
+
const result =
|
|
564
|
+
typeof handler === "function" ? handler(context) : handler;
|
|
565
|
+
if (result instanceof Promise) {
|
|
566
|
+
const tracked = deps.trackHandler(result, {
|
|
567
|
+
segmentId: parallelId,
|
|
568
|
+
segmentType: "parallel",
|
|
569
|
+
});
|
|
570
|
+
observeStreamedHandler(
|
|
571
|
+
tracked,
|
|
572
|
+
parallelId,
|
|
573
|
+
"parallel",
|
|
574
|
+
context.pathname,
|
|
575
|
+
routeKey,
|
|
576
|
+
params,
|
|
577
|
+
);
|
|
578
|
+
component = tracked as ReactNode;
|
|
579
|
+
} else {
|
|
580
|
+
component = result as ReactNode;
|
|
581
|
+
}
|
|
505
582
|
} else {
|
|
506
|
-
component =
|
|
583
|
+
component =
|
|
584
|
+
typeof handler === "function" ? await handler(context) : handler;
|
|
507
585
|
}
|
|
508
|
-
} else {
|
|
509
|
-
component =
|
|
510
|
-
typeof handler === "function" ? await handler(context) : handler;
|
|
511
586
|
}
|
|
512
587
|
}
|
|
513
588
|
|
|
@@ -521,6 +596,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
521
596
|
transition: parallelEntry.transition,
|
|
522
597
|
params,
|
|
523
598
|
slot,
|
|
599
|
+
_handlerRan: handlerRan,
|
|
524
600
|
belongsToRoute,
|
|
525
601
|
parallelName: `${parallelEntry.id}.${slot}`,
|
|
526
602
|
...(parallelEntry.mountPath
|
|
@@ -575,6 +651,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
575
651
|
): Promise<{ segment: ResolvedSegment; matchedId: string }> {
|
|
576
652
|
const matchedId = entry.shortCode;
|
|
577
653
|
|
|
654
|
+
let handlerRan = false;
|
|
578
655
|
const component = await revalidate(
|
|
579
656
|
async () => {
|
|
580
657
|
const hasSegment = clientSegmentIds.has(entry.shortCode);
|
|
@@ -651,6 +728,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
651
728
|
return shouldRevalidate;
|
|
652
729
|
},
|
|
653
730
|
async () => {
|
|
731
|
+
handlerRan = true;
|
|
654
732
|
const doneHandler = track(`handler:${entry.id}`, 2);
|
|
655
733
|
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
656
734
|
entry.shortCode;
|
|
@@ -665,13 +743,20 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
665
743
|
return staticComponent;
|
|
666
744
|
}
|
|
667
745
|
const routeEntry = entry as Extract<EntryData, { type: "route" }>;
|
|
746
|
+
// For Passthrough routes at runtime, use the live handler instead of
|
|
747
|
+
// the build handler. At build time (context.build === true), always
|
|
748
|
+
// use the build handler from routeEntry.handler.
|
|
749
|
+
const handler =
|
|
750
|
+
!context.build && routeEntry.liveHandler
|
|
751
|
+
? routeEntry.liveHandler
|
|
752
|
+
: routeEntry.handler;
|
|
668
753
|
if (!routeEntry.loading) {
|
|
669
|
-
const result = handleHandlerResult(await
|
|
754
|
+
const result = handleHandlerResult(await handler(context));
|
|
670
755
|
doneHandler();
|
|
671
756
|
return result;
|
|
672
757
|
}
|
|
673
758
|
if (!actionContext) {
|
|
674
|
-
const result = handleHandlerResult(
|
|
759
|
+
const result = handleHandlerResult(handler(context));
|
|
675
760
|
if (result instanceof Promise) {
|
|
676
761
|
result.finally(doneHandler).catch(() => {});
|
|
677
762
|
const tracked = deps.trackHandler(result, {
|
|
@@ -694,9 +779,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
694
779
|
debugLog("segment.action", "resolving action route with awaited value", {
|
|
695
780
|
entryId: entry.id,
|
|
696
781
|
});
|
|
697
|
-
const actionResult = handleHandlerResult(
|
|
698
|
-
await routeEntry.handler(context),
|
|
699
|
-
);
|
|
782
|
+
const actionResult = handleHandlerResult(await handler(context));
|
|
700
783
|
doneHandler();
|
|
701
784
|
return {
|
|
702
785
|
content: Promise.resolve(actionResult),
|
|
@@ -705,10 +788,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
705
788
|
() => null,
|
|
706
789
|
);
|
|
707
790
|
|
|
791
|
+
// Normalize void handlers (undefined) to null so the reconciler's
|
|
792
|
+
// component === null checks work consistently for both void and explicit null.
|
|
708
793
|
const resolvedComponent =
|
|
709
794
|
component && typeof component === "object" && "content" in component
|
|
710
|
-
? (component as { content: ReactNode }).content
|
|
711
|
-
: component;
|
|
795
|
+
? ((component as { content: ReactNode }).content ?? null)
|
|
796
|
+
: (component ?? null);
|
|
712
797
|
|
|
713
798
|
const segment: ResolvedSegment = {
|
|
714
799
|
id: entry.shortCode,
|
|
@@ -725,6 +810,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
725
810
|
? { layoutName: entry.id }
|
|
726
811
|
: {}),
|
|
727
812
|
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
813
|
+
_handlerRan: handlerRan,
|
|
728
814
|
};
|
|
729
815
|
|
|
730
816
|
return { segment, matchedId };
|
|
@@ -805,11 +891,11 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
805
891
|
prevUrl,
|
|
806
892
|
nextUrl,
|
|
807
893
|
routeKey,
|
|
808
|
-
loaderPromises,
|
|
809
894
|
true,
|
|
810
895
|
deps,
|
|
811
896
|
actionContext,
|
|
812
897
|
stale,
|
|
898
|
+
entry,
|
|
813
899
|
);
|
|
814
900
|
segments.push(...orphanResult.segments);
|
|
815
901
|
matchedIds.push(...orphanResult.matchedIds);
|
|
@@ -889,7 +975,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
889
975
|
prevUrl,
|
|
890
976
|
nextUrl,
|
|
891
977
|
routeKey,
|
|
892
|
-
loaderPromises,
|
|
893
978
|
false,
|
|
894
979
|
deps,
|
|
895
980
|
actionContext,
|
|
@@ -916,11 +1001,12 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
916
1001
|
prevUrl: URL,
|
|
917
1002
|
nextUrl: URL,
|
|
918
1003
|
routeKey: string,
|
|
919
|
-
loaderPromises: Map<string, Promise<any>>,
|
|
920
1004
|
belongsToRoute: boolean,
|
|
921
1005
|
deps: SegmentResolutionDeps<TEnv>,
|
|
922
1006
|
actionContext?: ActionContext,
|
|
923
1007
|
stale?: boolean,
|
|
1008
|
+
/** Parent route entry — its loaders are inherited so parallel slots can access them. */
|
|
1009
|
+
parentRouteEntry?: EntryData,
|
|
924
1010
|
): Promise<SegmentRevalidationResult> {
|
|
925
1011
|
invariant(
|
|
926
1012
|
orphan.type === "layout" || orphan.type === "cache",
|
|
@@ -948,6 +1034,37 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
948
1034
|
segments.push(...loaderResult.segments);
|
|
949
1035
|
matchedIds.push(...loaderResult.matchedIds);
|
|
950
1036
|
|
|
1037
|
+
// Inherit parent route's loaders so parallel slots inside this layout
|
|
1038
|
+
// can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
|
|
1039
|
+
if (
|
|
1040
|
+
parentRouteEntry &&
|
|
1041
|
+
parentRouteEntry.loader &&
|
|
1042
|
+
parentRouteEntry.loader.length > 0 &&
|
|
1043
|
+
Object.keys(orphan.parallel).length > 0
|
|
1044
|
+
) {
|
|
1045
|
+
const inheritedResult = await resolveLoadersWithRevalidation(
|
|
1046
|
+
parentRouteEntry,
|
|
1047
|
+
context,
|
|
1048
|
+
belongsToRoute,
|
|
1049
|
+
clientSegmentIds,
|
|
1050
|
+
prevParams,
|
|
1051
|
+
request,
|
|
1052
|
+
prevUrl,
|
|
1053
|
+
nextUrl,
|
|
1054
|
+
routeKey,
|
|
1055
|
+
deps,
|
|
1056
|
+
actionContext,
|
|
1057
|
+
orphan.shortCode,
|
|
1058
|
+
stale,
|
|
1059
|
+
);
|
|
1060
|
+
// Tag as inherited so buildMatchResult can deduplicate when safe
|
|
1061
|
+
for (const s of inheritedResult.segments) {
|
|
1062
|
+
s._inherited = true;
|
|
1063
|
+
}
|
|
1064
|
+
segments.push(...inheritedResult.segments);
|
|
1065
|
+
matchedIds.push(...inheritedResult.matchedIds);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
951
1068
|
// Handler-first: resolve orphan layout handler before its parallels
|
|
952
1069
|
// so ctx.set() values are visible to parallel children.
|
|
953
1070
|
matchedIds.push(orphan.shortCode);
|
|
@@ -1034,6 +1151,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1034
1151
|
);
|
|
1035
1152
|
|
|
1036
1153
|
if (!resolvedParallelEntries.has(parallelEntry.id)) {
|
|
1154
|
+
// shortCodeOverride must match the parent layout, not the parallel entry.
|
|
1037
1155
|
const loaderResult = await resolveLoadersWithRevalidation(
|
|
1038
1156
|
parallelEntry,
|
|
1039
1157
|
context,
|
|
@@ -1046,7 +1164,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1046
1164
|
routeKey,
|
|
1047
1165
|
deps,
|
|
1048
1166
|
actionContext,
|
|
1049
|
-
|
|
1167
|
+
orphan.shortCode,
|
|
1050
1168
|
stale,
|
|
1051
1169
|
);
|
|
1052
1170
|
segments.push(...loaderResult.segments);
|
|
@@ -1068,21 +1186,20 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1068
1186
|
const parallelId = `${orphan.shortCode}.${slot}`;
|
|
1069
1187
|
matchedIds.push(parallelId);
|
|
1070
1188
|
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
}
|
|
1189
|
+
const isFullRefetch = clientSegmentIds.size === 0;
|
|
1190
|
+
let shouldResolve: boolean;
|
|
1191
|
+
if (isFullRefetch) {
|
|
1192
|
+
// Same load-bearing rationale as the main parallel path: full refetch
|
|
1193
|
+
// means the client has nothing to fall back to, so the slot must render.
|
|
1194
|
+
traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
|
|
1195
|
+
shouldResolve = true;
|
|
1196
|
+
} else {
|
|
1197
|
+
// When slot is unknown to the client, seed the soft chain with `true`
|
|
1198
|
+
// (orphan parallels always belong to the route — we want them rendered
|
|
1199
|
+
// unless the user explicitly opts out via revalidate()).
|
|
1200
|
+
const defaultOverride = clientSegmentIds.has(parallelId)
|
|
1201
|
+
? undefined
|
|
1202
|
+
: { value: true, reason: "new-segment" };
|
|
1086
1203
|
|
|
1087
1204
|
const dummySegment: ResolvedSegment = {
|
|
1088
1205
|
id: parallelId,
|
|
@@ -1099,7 +1216,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1099
1216
|
: {}),
|
|
1100
1217
|
};
|
|
1101
1218
|
|
|
1102
|
-
|
|
1219
|
+
shouldResolve = await evaluateRevalidation({
|
|
1103
1220
|
segment: dummySegment,
|
|
1104
1221
|
prevParams,
|
|
1105
1222
|
getPrevSegment: null,
|
|
@@ -1115,8 +1232,9 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1115
1232
|
actionContext,
|
|
1116
1233
|
stale,
|
|
1117
1234
|
traceSource: "parallel",
|
|
1235
|
+
defaultOverride,
|
|
1118
1236
|
});
|
|
1119
|
-
}
|
|
1237
|
+
}
|
|
1120
1238
|
emitRevalidationDecision(
|
|
1121
1239
|
parallelId,
|
|
1122
1240
|
context.pathname,
|
|
@@ -1125,6 +1243,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1125
1243
|
);
|
|
1126
1244
|
|
|
1127
1245
|
let component: ReactNode | undefined;
|
|
1246
|
+
let handlerRan = false;
|
|
1128
1247
|
if (shouldResolve) {
|
|
1129
1248
|
component = await tryStaticSlot(parallelEntry, slot, parallelId);
|
|
1130
1249
|
}
|
|
@@ -1136,29 +1255,35 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1136
1255
|
} else if (handler === undefined) {
|
|
1137
1256
|
// Handler evicted (production static slot) but static lookup missed.
|
|
1138
1257
|
component = null;
|
|
1139
|
-
} else
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
tracked,
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1258
|
+
} else {
|
|
1259
|
+
// Slot-keyed pushes — see resolveParallelSegmentsWithRevalidation.
|
|
1260
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
1261
|
+
parallelId;
|
|
1262
|
+
handlerRan = true;
|
|
1263
|
+
if (hasLoadingFallback) {
|
|
1264
|
+
const result =
|
|
1265
|
+
typeof handler === "function" ? handler(context) : handler;
|
|
1266
|
+
if (result instanceof Promise) {
|
|
1267
|
+
const tracked = deps.trackHandler(result, {
|
|
1268
|
+
segmentId: parallelId,
|
|
1269
|
+
segmentType: "parallel",
|
|
1270
|
+
});
|
|
1271
|
+
observeStreamedHandler(
|
|
1272
|
+
tracked,
|
|
1273
|
+
parallelId,
|
|
1274
|
+
"parallel",
|
|
1275
|
+
context.pathname,
|
|
1276
|
+
routeKey,
|
|
1277
|
+
params,
|
|
1278
|
+
);
|
|
1279
|
+
component = tracked as ReactNode;
|
|
1280
|
+
} else {
|
|
1281
|
+
component = result as ReactNode;
|
|
1282
|
+
}
|
|
1156
1283
|
} else {
|
|
1157
|
-
component =
|
|
1284
|
+
component =
|
|
1285
|
+
typeof handler === "function" ? await handler(context) : handler;
|
|
1158
1286
|
}
|
|
1159
|
-
} else {
|
|
1160
|
-
component =
|
|
1161
|
-
typeof handler === "function" ? await handler(context) : handler;
|
|
1162
1287
|
}
|
|
1163
1288
|
}
|
|
1164
1289
|
|
|
@@ -1172,6 +1297,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1172
1297
|
transition: parallelEntry.transition,
|
|
1173
1298
|
params,
|
|
1174
1299
|
slot,
|
|
1300
|
+
_handlerRan: handlerRan,
|
|
1175
1301
|
belongsToRoute,
|
|
1176
1302
|
parallelName: `${parallelEntry.id}.${slot}`,
|
|
1177
1303
|
...(parallelEntry.mountPath
|
|
@@ -1229,6 +1355,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1229
1355
|
}
|
|
1230
1356
|
|
|
1231
1357
|
const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
|
|
1358
|
+
if (entry.type === "cache") {
|
|
1359
|
+
const store = RSCRouterContext.getStore();
|
|
1360
|
+
if (store) store.insideCacheScope = true;
|
|
1361
|
+
}
|
|
1232
1362
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
1233
1363
|
const resolved = await resolveWithErrorBoundary(
|
|
1234
1364
|
nonParallelEntry,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { TrieNode, TrieLeaf } from "../build/route-trie.js";
|
|
9
|
+
import { safeDecodeURIComponent } from "./url-params.js";
|
|
9
10
|
|
|
10
11
|
export interface TrieMatchResult {
|
|
11
12
|
/** Route name */
|
|
@@ -14,7 +15,9 @@ export interface TrieMatchResult {
|
|
|
14
15
|
sp: string;
|
|
15
16
|
/** Matched route params */
|
|
16
17
|
params: Record<string, string>;
|
|
17
|
-
/** Optional param names
|
|
18
|
+
/** Optional param names declared on the route. Absent params are omitted
|
|
19
|
+
* from `params` (read as `undefined`), matching the
|
|
20
|
+
* `ExtractParams<"/:locale?/...">` type. */
|
|
18
21
|
optionalParams?: string[];
|
|
19
22
|
/** Ancestry shortCodes for layout pruning */
|
|
20
23
|
ancestry: string[];
|
|
@@ -173,20 +176,25 @@ function validateAndBuild(
|
|
|
173
176
|
originalPathname: string,
|
|
174
177
|
pathnameHasTrailingSlash: boolean,
|
|
175
178
|
): TrieMatchResult | null {
|
|
176
|
-
// Build named params by zipping leaf.pa with positional paramValues
|
|
179
|
+
// Build named params by zipping leaf.pa with positional paramValues.
|
|
180
|
+
// Params are URL-decoded at this boundary so ctx.params holds the values
|
|
181
|
+
// apps expect (matching Express/React Router) and round-trip cleanly
|
|
182
|
+
// through ctx.reverse.
|
|
177
183
|
const params: Record<string, string> = {};
|
|
178
184
|
if (leaf.pa) {
|
|
179
185
|
for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
|
|
180
|
-
params[leaf.pa[i]] = paramValues[i];
|
|
186
|
+
params[leaf.pa[i]] = safeDecodeURIComponent(paramValues[i]);
|
|
181
187
|
}
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
// Add wildcard param (wildcard leaves have pn from TrieNode.w type)
|
|
185
191
|
if (wildcardValue !== undefined && "pn" in leaf) {
|
|
186
|
-
params[(leaf as TrieLeaf & { pn: string }).pn] =
|
|
192
|
+
params[(leaf as TrieLeaf & { pn: string }).pn] =
|
|
193
|
+
safeDecodeURIComponent(wildcardValue);
|
|
187
194
|
}
|
|
188
195
|
|
|
189
|
-
// Validate constraints
|
|
196
|
+
// Validate constraints against decoded values so constraint lists can be
|
|
197
|
+
// written in decoded form (e.g. ["en-GB", "en US"]).
|
|
190
198
|
if (leaf.cv) {
|
|
191
199
|
for (const paramName in leaf.cv) {
|
|
192
200
|
const allowed = leaf.cv[paramName]!;
|
|
@@ -197,14 +205,11 @@ function validateAndBuild(
|
|
|
197
205
|
}
|
|
198
206
|
}
|
|
199
207
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
+
// Optional params that weren't matched are left absent from `params` so
|
|
209
|
+
// `ctx.params.locale` reads as `undefined`, matching the
|
|
210
|
+
// `ExtractParams<"/:locale?/...">` type (`{ locale?: string }`). Both
|
|
211
|
+
// internal consumers — the constraint check above and `reverse()` —
|
|
212
|
+
// already treat missing/undefined as the absent form.
|
|
208
213
|
|
|
209
214
|
// Trailing slash handling
|
|
210
215
|
const tsMode = leaf.ts as "never" | "always" | "ignore" | undefined;
|