@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.dc2bd2b4
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 +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2151 -846
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +71 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +57 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +647 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +84 -11
- package/src/browser/navigation-client.ts +76 -28
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +64 -26
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +148 -16
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +64 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +92 -182
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +9 -4
- package/src/index.ts +53 -15
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -36
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +384 -257
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +100 -28
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +8 -8
- package/src/router/loader-resolution.ts +19 -2
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +38 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +143 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +20 -42
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +41 -7
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +101 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- 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 +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -28,11 +28,12 @@ import {
|
|
|
28
28
|
resolveLayoutComponent,
|
|
29
29
|
resolveWithErrorBoundary,
|
|
30
30
|
} from "./helpers.js";
|
|
31
|
+
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
31
32
|
import { getRouterContext } from "../router-context.js";
|
|
32
33
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
33
34
|
import {
|
|
34
35
|
track,
|
|
35
|
-
|
|
36
|
+
RangoContext,
|
|
36
37
|
runInsideLoaderScope,
|
|
37
38
|
} from "../../server/context.js";
|
|
38
39
|
|
|
@@ -224,7 +225,10 @@ export async function resolveSegment<TEnv>(
|
|
|
224
225
|
index: 0,
|
|
225
226
|
component,
|
|
226
227
|
loading: entry.loading === false ? null : entry.loading,
|
|
227
|
-
transition:
|
|
228
|
+
transition: applyViewTransitionDefault(
|
|
229
|
+
entry.transition,
|
|
230
|
+
deps.viewTransitionDefault,
|
|
231
|
+
),
|
|
228
232
|
params,
|
|
229
233
|
belongsToRoute: false,
|
|
230
234
|
layoutName: entry.id,
|
|
@@ -359,7 +363,10 @@ export async function resolveSegment<TEnv>(
|
|
|
359
363
|
index: 0,
|
|
360
364
|
component: component ?? null,
|
|
361
365
|
loading: entry.loading === false ? null : entry.loading,
|
|
362
|
-
transition:
|
|
366
|
+
transition: applyViewTransitionDefault(
|
|
367
|
+
entry.transition,
|
|
368
|
+
deps.viewTransitionDefault,
|
|
369
|
+
),
|
|
363
370
|
params,
|
|
364
371
|
belongsToRoute: true,
|
|
365
372
|
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
@@ -443,7 +450,10 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
443
450
|
belongsToRoute,
|
|
444
451
|
layoutName: orphan.id,
|
|
445
452
|
loading: orphan.loading === false ? null : orphan.loading,
|
|
446
|
-
transition:
|
|
453
|
+
transition: applyViewTransitionDefault(
|
|
454
|
+
orphan.transition,
|
|
455
|
+
deps.viewTransitionDefault,
|
|
456
|
+
),
|
|
447
457
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
448
458
|
});
|
|
449
459
|
|
|
@@ -515,6 +525,14 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
515
525
|
if (handler === undefined) {
|
|
516
526
|
continue;
|
|
517
527
|
}
|
|
528
|
+
// Pin `_currentSegmentId` to the slot's own id so handle pushes from
|
|
529
|
+
// inside the slot handler get their own bucket in the HandleStore.
|
|
530
|
+
// Parent-keying would collapse them into the parent layout's bucket;
|
|
531
|
+
// the partial-update merge then replaces the parent's bucket on a
|
|
532
|
+
// slot-only revalidation and drops layout-pushed Meta/Breadcrumbs.
|
|
533
|
+
// filterSegmentOrder() retains slot ids so the client preserves them.
|
|
534
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
535
|
+
`${parentShortCode}.${slot}`;
|
|
518
536
|
const doneParallelHandler = track(
|
|
519
537
|
`handler:${parallelEntry.id}.${slot}`,
|
|
520
538
|
2,
|
|
@@ -557,7 +575,10 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
557
575
|
index: 0,
|
|
558
576
|
component,
|
|
559
577
|
loading: parallelEntry.loading === false ? null : parallelEntry.loading,
|
|
560
|
-
transition:
|
|
578
|
+
transition: applyViewTransitionDefault(
|
|
579
|
+
parallelEntry.transition,
|
|
580
|
+
deps.viewTransitionDefault,
|
|
581
|
+
),
|
|
561
582
|
params,
|
|
562
583
|
slot,
|
|
563
584
|
belongsToRoute,
|
|
@@ -624,7 +645,7 @@ export async function resolveAllSegments<TEnv>(
|
|
|
624
645
|
// can guard non-cacheable variable reads. Also guards response-level
|
|
625
646
|
// side effects (headers.set). Persists for all descendant entries.
|
|
626
647
|
if (entry.type === "cache") {
|
|
627
|
-
const store =
|
|
648
|
+
const store = RangoContext.getStore();
|
|
628
649
|
if (store) store.insideCacheScope = true;
|
|
629
650
|
}
|
|
630
651
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
@@ -39,11 +39,12 @@ import {
|
|
|
39
39
|
resolveLayoutComponent,
|
|
40
40
|
resolveWithErrorBoundary,
|
|
41
41
|
} from "./helpers.js";
|
|
42
|
+
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
42
43
|
import { getRouterContext } from "../router-context.js";
|
|
43
44
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
44
45
|
import {
|
|
45
46
|
track,
|
|
46
|
-
|
|
47
|
+
RangoContext,
|
|
47
48
|
runInsideLoaderScope,
|
|
48
49
|
} from "../../server/context.js";
|
|
49
50
|
|
|
@@ -89,6 +90,27 @@ function observeStreamedHandler(
|
|
|
89
90
|
});
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Trace a parallel slot that's being force-rendered on a full refetch (client
|
|
95
|
+
* has no cached state). User revalidate fns are bypassed in this case — see
|
|
96
|
+
* the call sites for the load-bearing rationale.
|
|
97
|
+
*/
|
|
98
|
+
function traceFullRefetchedParallelSlot(
|
|
99
|
+
parallelId: string,
|
|
100
|
+
belongsToRoute: boolean,
|
|
101
|
+
): void {
|
|
102
|
+
if (!isTraceActive()) return;
|
|
103
|
+
pushRevalidationTraceEntry({
|
|
104
|
+
segmentId: parallelId,
|
|
105
|
+
segmentType: "parallel",
|
|
106
|
+
belongsToRoute,
|
|
107
|
+
source: "parallel",
|
|
108
|
+
defaultShouldRevalidate: true,
|
|
109
|
+
finalShouldRevalidate: true,
|
|
110
|
+
reason: "full-refetch",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
92
114
|
// ---------------------------------------------------------------------------
|
|
93
115
|
// Revalidation telemetry helper
|
|
94
116
|
// ---------------------------------------------------------------------------
|
|
@@ -448,44 +470,30 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
448
470
|
|
|
449
471
|
const isFullRefetch = clientSegmentIds.size === 0;
|
|
450
472
|
const isNewParent = !clientSegmentIds.has(entry.shortCode);
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
) {
|
|
457
|
-
matchedIds.push(parallelId);
|
|
458
|
-
}
|
|
473
|
+
// Always announce the slot in matchedIds — it's unconditionally appended
|
|
474
|
+
// to `segments` below, and a segment present in segments but missing from
|
|
475
|
+
// matched lets the client prune it (then it's missing from clientSegmentIds
|
|
476
|
+
// on the next request, perpetuating the staleness).
|
|
477
|
+
matchedIds.push(parallelId);
|
|
459
478
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
return true;
|
|
474
|
-
}
|
|
479
|
+
let shouldResolve: boolean;
|
|
480
|
+
if (isFullRefetch) {
|
|
481
|
+
// Client has nothing cached — slot MUST render. User revalidate fns are
|
|
482
|
+
// bypassed here because returning false would leave the segment blank
|
|
483
|
+
// with no client-side fallback.
|
|
484
|
+
traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
|
|
485
|
+
shouldResolve = true;
|
|
486
|
+
} else {
|
|
487
|
+
// For non-empty client sets, consult user revalidate fns. When the slot
|
|
488
|
+
// is unknown to the client, override the type-derived default so the
|
|
489
|
+
// soft chain seeds with the right "new segment" / "parent-chain" value.
|
|
490
|
+
let defaultOverride: { value: boolean; reason: string } | undefined;
|
|
475
491
|
if (!clientSegmentIds.has(parallelId)) {
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
belongsToRoute,
|
|
482
|
-
source: "parallel",
|
|
483
|
-
defaultShouldRevalidate: result,
|
|
484
|
-
finalShouldRevalidate: result,
|
|
485
|
-
reason: result ? "new-segment" : "skip-parent-chain",
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
return result;
|
|
492
|
+
const value = belongsToRoute || isNewParent;
|
|
493
|
+
defaultOverride = {
|
|
494
|
+
value,
|
|
495
|
+
reason: value ? "new-segment" : "skip-parent-chain",
|
|
496
|
+
};
|
|
489
497
|
}
|
|
490
498
|
|
|
491
499
|
const dummySegment: ResolvedSegment = {
|
|
@@ -503,7 +511,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
503
511
|
: {}),
|
|
504
512
|
};
|
|
505
513
|
|
|
506
|
-
|
|
514
|
+
shouldResolve = await evaluateRevalidation({
|
|
507
515
|
segment: dummySegment,
|
|
508
516
|
prevParams,
|
|
509
517
|
getPrevSegment: null,
|
|
@@ -519,8 +527,9 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
519
527
|
actionContext,
|
|
520
528
|
stale,
|
|
521
529
|
traceSource: "parallel",
|
|
530
|
+
defaultOverride,
|
|
522
531
|
});
|
|
523
|
-
}
|
|
532
|
+
}
|
|
524
533
|
emitRevalidationDecision(
|
|
525
534
|
parallelId,
|
|
526
535
|
context.pathname,
|
|
@@ -529,8 +538,11 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
529
538
|
);
|
|
530
539
|
|
|
531
540
|
let component: ReactNode | undefined;
|
|
541
|
+
let handlerRan = false;
|
|
532
542
|
if (shouldResolve) {
|
|
533
543
|
component = await tryStaticSlot(parallelEntry, slot, parallelId);
|
|
544
|
+
// tryStaticSlot returning a value means the static cache supplied the
|
|
545
|
+
// component — handler did NOT run. handlerRan stays false.
|
|
534
546
|
}
|
|
535
547
|
if (component === undefined) {
|
|
536
548
|
const hasLoadingFallback =
|
|
@@ -541,29 +553,37 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
541
553
|
// Handler evicted (production static slot) but static lookup missed.
|
|
542
554
|
// Nothing to render — use null so the client keeps its cached version.
|
|
543
555
|
component = null;
|
|
544
|
-
} else
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
556
|
+
} else {
|
|
557
|
+
// Slot-keyed pushes — slot owns its own bucket, parent layout owns
|
|
558
|
+
// its own. On slot-only revalidations the partial merge updates only
|
|
559
|
+
// the slot's bucket; the parent's bucket stays intact.
|
|
560
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
561
|
+
parallelId;
|
|
562
|
+
handlerRan = true;
|
|
563
|
+
if (hasLoadingFallback) {
|
|
564
|
+
const result =
|
|
565
|
+
typeof handler === "function" ? handler(context) : handler;
|
|
566
|
+
if (result instanceof Promise) {
|
|
567
|
+
const tracked = deps.trackHandler(result, {
|
|
568
|
+
segmentId: parallelId,
|
|
569
|
+
segmentType: "parallel",
|
|
570
|
+
});
|
|
571
|
+
observeStreamedHandler(
|
|
572
|
+
tracked,
|
|
573
|
+
parallelId,
|
|
574
|
+
"parallel",
|
|
575
|
+
context.pathname,
|
|
576
|
+
routeKey,
|
|
577
|
+
params,
|
|
578
|
+
);
|
|
579
|
+
component = tracked as ReactNode;
|
|
580
|
+
} else {
|
|
581
|
+
component = result as ReactNode;
|
|
582
|
+
}
|
|
561
583
|
} else {
|
|
562
|
-
component =
|
|
584
|
+
component =
|
|
585
|
+
typeof handler === "function" ? await handler(context) : handler;
|
|
563
586
|
}
|
|
564
|
-
} else {
|
|
565
|
-
component =
|
|
566
|
-
typeof handler === "function" ? await handler(context) : handler;
|
|
567
587
|
}
|
|
568
588
|
}
|
|
569
589
|
|
|
@@ -574,9 +594,13 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
574
594
|
index: 0,
|
|
575
595
|
component,
|
|
576
596
|
loading: parallelEntry.loading === false ? null : parallelEntry.loading,
|
|
577
|
-
transition:
|
|
597
|
+
transition: applyViewTransitionDefault(
|
|
598
|
+
parallelEntry.transition,
|
|
599
|
+
deps.viewTransitionDefault,
|
|
600
|
+
),
|
|
578
601
|
params,
|
|
579
602
|
slot,
|
|
603
|
+
_handlerRan: handlerRan,
|
|
580
604
|
belongsToRoute,
|
|
581
605
|
parallelName: `${parallelEntry.id}.${slot}`,
|
|
582
606
|
...(parallelEntry.mountPath
|
|
@@ -631,6 +655,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
631
655
|
): Promise<{ segment: ResolvedSegment; matchedId: string }> {
|
|
632
656
|
const matchedId = entry.shortCode;
|
|
633
657
|
|
|
658
|
+
let handlerRan = false;
|
|
634
659
|
const component = await revalidate(
|
|
635
660
|
async () => {
|
|
636
661
|
const hasSegment = clientSegmentIds.has(entry.shortCode);
|
|
@@ -707,6 +732,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
707
732
|
return shouldRevalidate;
|
|
708
733
|
},
|
|
709
734
|
async () => {
|
|
735
|
+
handlerRan = true;
|
|
710
736
|
const doneHandler = track(`handler:${entry.id}`, 2);
|
|
711
737
|
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
712
738
|
entry.shortCode;
|
|
@@ -781,13 +807,17 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
781
807
|
index: 0,
|
|
782
808
|
component: resolvedComponent,
|
|
783
809
|
loading: entry.loading === false ? null : entry.loading,
|
|
784
|
-
transition:
|
|
810
|
+
transition: applyViewTransitionDefault(
|
|
811
|
+
entry.transition,
|
|
812
|
+
deps.viewTransitionDefault,
|
|
813
|
+
),
|
|
785
814
|
params,
|
|
786
815
|
belongsToRoute,
|
|
787
816
|
...(entry.type === "layout" || entry.type === "cache"
|
|
788
817
|
? { layoutName: entry.id }
|
|
789
818
|
: {}),
|
|
790
819
|
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
820
|
+
_handlerRan: handlerRan,
|
|
791
821
|
};
|
|
792
822
|
|
|
793
823
|
return { segment, matchedId };
|
|
@@ -868,7 +898,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
868
898
|
prevUrl,
|
|
869
899
|
nextUrl,
|
|
870
900
|
routeKey,
|
|
871
|
-
loaderPromises,
|
|
872
901
|
true,
|
|
873
902
|
deps,
|
|
874
903
|
actionContext,
|
|
@@ -953,7 +982,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
953
982
|
prevUrl,
|
|
954
983
|
nextUrl,
|
|
955
984
|
routeKey,
|
|
956
|
-
loaderPromises,
|
|
957
985
|
false,
|
|
958
986
|
deps,
|
|
959
987
|
actionContext,
|
|
@@ -980,7 +1008,6 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
980
1008
|
prevUrl: URL,
|
|
981
1009
|
nextUrl: URL,
|
|
982
1010
|
routeKey: string,
|
|
983
|
-
loaderPromises: Map<string, Promise<any>>,
|
|
984
1011
|
belongsToRoute: boolean,
|
|
985
1012
|
deps: SegmentResolutionDeps<TEnv>,
|
|
986
1013
|
actionContext?: ActionContext,
|
|
@@ -1117,7 +1144,10 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1117
1144
|
belongsToRoute,
|
|
1118
1145
|
layoutName: orphan.id,
|
|
1119
1146
|
loading: orphan.loading === false ? null : orphan.loading,
|
|
1120
|
-
transition:
|
|
1147
|
+
transition: applyViewTransitionDefault(
|
|
1148
|
+
orphan.transition,
|
|
1149
|
+
deps.viewTransitionDefault,
|
|
1150
|
+
),
|
|
1121
1151
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
1122
1152
|
});
|
|
1123
1153
|
|
|
@@ -1166,21 +1196,20 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1166
1196
|
const parallelId = `${orphan.shortCode}.${slot}`;
|
|
1167
1197
|
matchedIds.push(parallelId);
|
|
1168
1198
|
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
}
|
|
1199
|
+
const isFullRefetch = clientSegmentIds.size === 0;
|
|
1200
|
+
let shouldResolve: boolean;
|
|
1201
|
+
if (isFullRefetch) {
|
|
1202
|
+
// Same load-bearing rationale as the main parallel path: full refetch
|
|
1203
|
+
// means the client has nothing to fall back to, so the slot must render.
|
|
1204
|
+
traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
|
|
1205
|
+
shouldResolve = true;
|
|
1206
|
+
} else {
|
|
1207
|
+
// When slot is unknown to the client, seed the soft chain with `true`
|
|
1208
|
+
// (orphan parallels always belong to the route — we want them rendered
|
|
1209
|
+
// unless the user explicitly opts out via revalidate()).
|
|
1210
|
+
const defaultOverride = clientSegmentIds.has(parallelId)
|
|
1211
|
+
? undefined
|
|
1212
|
+
: { value: true, reason: "new-segment" };
|
|
1184
1213
|
|
|
1185
1214
|
const dummySegment: ResolvedSegment = {
|
|
1186
1215
|
id: parallelId,
|
|
@@ -1197,7 +1226,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1197
1226
|
: {}),
|
|
1198
1227
|
};
|
|
1199
1228
|
|
|
1200
|
-
|
|
1229
|
+
shouldResolve = await evaluateRevalidation({
|
|
1201
1230
|
segment: dummySegment,
|
|
1202
1231
|
prevParams,
|
|
1203
1232
|
getPrevSegment: null,
|
|
@@ -1213,8 +1242,9 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1213
1242
|
actionContext,
|
|
1214
1243
|
stale,
|
|
1215
1244
|
traceSource: "parallel",
|
|
1245
|
+
defaultOverride,
|
|
1216
1246
|
});
|
|
1217
|
-
}
|
|
1247
|
+
}
|
|
1218
1248
|
emitRevalidationDecision(
|
|
1219
1249
|
parallelId,
|
|
1220
1250
|
context.pathname,
|
|
@@ -1223,6 +1253,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1223
1253
|
);
|
|
1224
1254
|
|
|
1225
1255
|
let component: ReactNode | undefined;
|
|
1256
|
+
let handlerRan = false;
|
|
1226
1257
|
if (shouldResolve) {
|
|
1227
1258
|
component = await tryStaticSlot(parallelEntry, slot, parallelId);
|
|
1228
1259
|
}
|
|
@@ -1234,29 +1265,35 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1234
1265
|
} else if (handler === undefined) {
|
|
1235
1266
|
// Handler evicted (production static slot) but static lookup missed.
|
|
1236
1267
|
component = null;
|
|
1237
|
-
} else
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
tracked,
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1268
|
+
} else {
|
|
1269
|
+
// Slot-keyed pushes — see resolveParallelSegmentsWithRevalidation.
|
|
1270
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
1271
|
+
parallelId;
|
|
1272
|
+
handlerRan = true;
|
|
1273
|
+
if (hasLoadingFallback) {
|
|
1274
|
+
const result =
|
|
1275
|
+
typeof handler === "function" ? handler(context) : handler;
|
|
1276
|
+
if (result instanceof Promise) {
|
|
1277
|
+
const tracked = deps.trackHandler(result, {
|
|
1278
|
+
segmentId: parallelId,
|
|
1279
|
+
segmentType: "parallel",
|
|
1280
|
+
});
|
|
1281
|
+
observeStreamedHandler(
|
|
1282
|
+
tracked,
|
|
1283
|
+
parallelId,
|
|
1284
|
+
"parallel",
|
|
1285
|
+
context.pathname,
|
|
1286
|
+
routeKey,
|
|
1287
|
+
params,
|
|
1288
|
+
);
|
|
1289
|
+
component = tracked as ReactNode;
|
|
1290
|
+
} else {
|
|
1291
|
+
component = result as ReactNode;
|
|
1292
|
+
}
|
|
1254
1293
|
} else {
|
|
1255
|
-
component =
|
|
1294
|
+
component =
|
|
1295
|
+
typeof handler === "function" ? await handler(context) : handler;
|
|
1256
1296
|
}
|
|
1257
|
-
} else {
|
|
1258
|
-
component =
|
|
1259
|
-
typeof handler === "function" ? await handler(context) : handler;
|
|
1260
1297
|
}
|
|
1261
1298
|
}
|
|
1262
1299
|
|
|
@@ -1267,9 +1304,13 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1267
1304
|
index: 0,
|
|
1268
1305
|
component,
|
|
1269
1306
|
loading: parallelEntry.loading === false ? null : parallelEntry.loading,
|
|
1270
|
-
transition:
|
|
1307
|
+
transition: applyViewTransitionDefault(
|
|
1308
|
+
parallelEntry.transition,
|
|
1309
|
+
deps.viewTransitionDefault,
|
|
1310
|
+
),
|
|
1271
1311
|
params,
|
|
1272
1312
|
slot,
|
|
1313
|
+
_handlerRan: handlerRan,
|
|
1273
1314
|
belongsToRoute,
|
|
1274
1315
|
parallelName: `${parallelEntry.id}.${slot}`,
|
|
1275
1316
|
...(parallelEntry.mountPath
|
|
@@ -1328,7 +1369,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1328
1369
|
|
|
1329
1370
|
const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
|
|
1330
1371
|
if (entry.type === "cache") {
|
|
1331
|
-
const store =
|
|
1372
|
+
const store = RangoContext.getStore();
|
|
1332
1373
|
if (store) store.insideCacheScope = true;
|
|
1333
1374
|
}
|
|
1334
1375
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View-transition boundary default resolution.
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own module (rather than helpers.ts) because several resolution
|
|
5
|
+
* tests mock helpers.ts with an explicit export list; a shared util here is
|
|
6
|
+
* never mocked, so the fresh and revalidation paths always get the real
|
|
7
|
+
* implementation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { EntryData } from "../../server/context";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the effective `viewTransition` for a segment's transition config.
|
|
14
|
+
*
|
|
15
|
+
* The per-segment value (set via the transition() DSL) always wins. When it is
|
|
16
|
+
* unset, the router-level createRouter({ viewTransition }) default is stamped
|
|
17
|
+
* in so the render gate reads the boundary decision off the segment — server
|
|
18
|
+
* and client, via the serialized segment — without the router option being
|
|
19
|
+
* threaded to the client. Only `false` is ever stamped; an unset (or "auto")
|
|
20
|
+
* value is left untouched because it already means "wrap" at the gate, which
|
|
21
|
+
* also avoids needless object allocation and payload growth. Used by both the
|
|
22
|
+
* fresh and revalidation resolution paths.
|
|
23
|
+
*/
|
|
24
|
+
export function applyViewTransitionDefault(
|
|
25
|
+
transition: EntryData["transition"],
|
|
26
|
+
viewTransitionDefault: "auto" | false | undefined,
|
|
27
|
+
): EntryData["transition"] {
|
|
28
|
+
if (!transition) return transition;
|
|
29
|
+
if (
|
|
30
|
+
transition.viewTransition === undefined &&
|
|
31
|
+
viewTransitionDefault === false
|
|
32
|
+
) {
|
|
33
|
+
return { ...transition, viewTransition: false };
|
|
34
|
+
}
|
|
35
|
+
return transition;
|
|
36
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { encodePathSegment } from "./url-params.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Substitute `:param` placeholders in a route pattern with values from
|
|
5
|
+
* `params`. Two-pass: optional params (`:name?`) first so absent values
|
|
6
|
+
* collapse cleanly, then required params (throws on missing). Constraint
|
|
7
|
+
* syntax (`:name(en|gb)`) is stripped from the result. Trailing-slash
|
|
8
|
+
* patterns like `/blog/` are preserved unless an optional segment was
|
|
9
|
+
* actually omitted.
|
|
10
|
+
*
|
|
11
|
+
* Shared by `ctx.reverse()` (server), `createReverse()` (typed runtime
|
|
12
|
+
* helper), and `useReverse()` (client hook). The behavior must stay
|
|
13
|
+
* identical across all three call sites.
|
|
14
|
+
*/
|
|
15
|
+
export function substitutePatternParams(
|
|
16
|
+
pattern: string,
|
|
17
|
+
params: Record<string, string | undefined>,
|
|
18
|
+
routeName: string,
|
|
19
|
+
): string {
|
|
20
|
+
let result = pattern;
|
|
21
|
+
let hadOmittedOptional = false;
|
|
22
|
+
|
|
23
|
+
result = result.replace(
|
|
24
|
+
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
|
|
25
|
+
(_match, key) => {
|
|
26
|
+
const value = params[key as string];
|
|
27
|
+
// The matcher omits absent optional params (so `value` is `undefined`
|
|
28
|
+
// here), but caller-supplied params or `getParams()` shapes may still
|
|
29
|
+
// pass `""` explicitly. Treat both as the absent form.
|
|
30
|
+
if (value === undefined || value === "") {
|
|
31
|
+
hadOmittedOptional = true;
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
return encodePathSegment(value);
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
result = result.replace(
|
|
39
|
+
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
|
|
40
|
+
(_match, key) => {
|
|
41
|
+
const value = params[key as string];
|
|
42
|
+
if (value === undefined) {
|
|
43
|
+
throw new Error(`Missing param "${key}" for route "${routeName}"`);
|
|
44
|
+
}
|
|
45
|
+
return encodePathSegment(value);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (hadOmittedOptional) {
|
|
50
|
+
const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
51
|
+
result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
|
|
52
|
+
if (hadTrailingSlash && !result.endsWith("/")) result += "/";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
}
|