@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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 +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader-family capture snapshot: elide + overlay for bake-lane loader
|
|
3
|
+
* containers (docs/design/loader-container-bake.md).
|
|
4
|
+
*
|
|
5
|
+
* A loader on an entry with NO renderable loading() executes during shell
|
|
6
|
+
* capture; its settled container bakes into the prelude while every promise
|
|
7
|
+
* still pending at the quiet window postpones as a hole. To keep a HIT's fresh
|
|
8
|
+
* payload byte-identical to that frozen prelude, the capture pins the container
|
|
9
|
+
* in the shell snapshot:
|
|
10
|
+
*
|
|
11
|
+
* - elide: deep-walk the settled container; a PENDING nested promise is a
|
|
12
|
+
* hole, replaced by {@link LOADER_HOLE_KEY} — and since
|
|
13
|
+
* loader-cache masks every nested thenable at capture
|
|
14
|
+
* ({@link maskNestedContainerThenables}), nested promises are
|
|
15
|
+
* ALWAYS pending here for new captures. The SETTLED-marker branch
|
|
16
|
+
* below is legacy: it fires only if a settled thenable reaches
|
|
17
|
+
* elide anyway, and the overlay keeps decoding SETTLED markers
|
|
18
|
+
* from pre-mask snapshots. The result is promise-free and
|
|
19
|
+
* Flight-serializable.
|
|
20
|
+
* - overlay: on a HIT the loader runs fresh (only the loader body can mint
|
|
21
|
+
* the live nested promises), then the recorded container is laid
|
|
22
|
+
* over it: recorded paths win (they are what the prelude froze),
|
|
23
|
+
* hole-marker paths take the fresh run's value (the live hole),
|
|
24
|
+
* SETTLED-marker paths become Promise.resolve(pinned) — consumers
|
|
25
|
+
* wrote use(data.x) against a promise-shaped container, and
|
|
26
|
+
* handing them the raw value throws React #438 on every HIT (the
|
|
27
|
+
* storefront PDP regression) — and fresh-only paths pass through
|
|
28
|
+
* (they cannot contradict prelude bytes that never rendered them).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { isThenable } from "../../handles/is-thenable.js";
|
|
32
|
+
|
|
33
|
+
// Capture-side nested-thenable masking lives in the LEAF module mask-nested.ts
|
|
34
|
+
// (request-context also needs it for handle pushes and cannot import through
|
|
35
|
+
// loader-mask without a cycle). Re-exported here so loader-cache and the unit
|
|
36
|
+
// tests keep one import site for the snapshot family.
|
|
37
|
+
export { maskNestedContainerThenables } from "./mask-nested.js";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Marker object standing in for a pending nested promise in a recorded loader
|
|
41
|
+
* container. Shape-checked (not identity-checked) because the record round-trips
|
|
42
|
+
* through Flight serialization and a JSON-embedding store envelope.
|
|
43
|
+
*/
|
|
44
|
+
export const LOADER_HOLE_KEY = "$rangoLoaderHole" as const;
|
|
45
|
+
|
|
46
|
+
export interface LoaderHoleMarker {
|
|
47
|
+
[LOADER_HOLE_KEY]: 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isLoaderHoleMarker(value: unknown): value is LoaderHoleMarker {
|
|
51
|
+
return (
|
|
52
|
+
typeof value === "object" &&
|
|
53
|
+
value !== null &&
|
|
54
|
+
(value as Record<string, unknown>)[LOADER_HOLE_KEY] === 1
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Marker wrapping the inlined value of a NESTED promise that settled during
|
|
60
|
+
* capture. The value baked (physics), but the container key was a promise —
|
|
61
|
+
* the overlay must hand consumers a Promise.resolve(value), not the raw value,
|
|
62
|
+
* or an unconditional use(data.x) throws React #438 on every HIT. The ROOT
|
|
63
|
+
* container is never wrapped: loader-cache overlays against the awaited fresh
|
|
64
|
+
* container value.
|
|
65
|
+
*/
|
|
66
|
+
export const LOADER_SETTLED_KEY = "$rangoLoaderSettled" as const;
|
|
67
|
+
|
|
68
|
+
export interface LoaderSettledMarker {
|
|
69
|
+
[LOADER_SETTLED_KEY]: 1;
|
|
70
|
+
value: unknown;
|
|
71
|
+
/**
|
|
72
|
+
* Present when the pinned subtree contains hole markers. Computed once at
|
|
73
|
+
* capture (elide already visits every node) so the per-HIT overlay never
|
|
74
|
+
* rescans the pinned structure to pick its rehydration path.
|
|
75
|
+
*/
|
|
76
|
+
holes?: 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isLoaderSettledMarker(
|
|
80
|
+
value: unknown,
|
|
81
|
+
): value is LoaderSettledMarker {
|
|
82
|
+
return (
|
|
83
|
+
typeof value === "object" &&
|
|
84
|
+
value !== null &&
|
|
85
|
+
(value as Record<string, unknown>)[LOADER_SETTLED_KEY] === 1
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
90
|
+
if (typeof value !== "object" || value === null) return false;
|
|
91
|
+
const proto = Object.getPrototypeOf(value);
|
|
92
|
+
return proto === Object.prototype || proto === null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Probe whether a promise is already settled without waiting for it: races it
|
|
97
|
+
* against an immediately-resolved sentinel across two microtask hops (then
|
|
98
|
+
* chaining means a resolved inner value needs one extra hop to surface).
|
|
99
|
+
* Returns the settled state, or "pending" if it has not settled by then.
|
|
100
|
+
*/
|
|
101
|
+
async function probeSettled(
|
|
102
|
+
p: PromiseLike<unknown>,
|
|
103
|
+
): Promise<
|
|
104
|
+
| { state: "fulfilled"; value: unknown }
|
|
105
|
+
| { state: "rejected" }
|
|
106
|
+
| { state: "pending" }
|
|
107
|
+
> {
|
|
108
|
+
const PENDING = Symbol("pending");
|
|
109
|
+
// Two sentinel hops: Promise.resolve(p) adoption costs a microtask, so a
|
|
110
|
+
// single-hop sentinel would misreport an already-fulfilled promise as pending.
|
|
111
|
+
const sentinel = Promise.resolve()
|
|
112
|
+
.then(() => undefined)
|
|
113
|
+
.then(() => PENDING as unknown);
|
|
114
|
+
try {
|
|
115
|
+
const raced = await Promise.race([Promise.resolve(p), sentinel]);
|
|
116
|
+
if (raced === PENDING) return { state: "pending" };
|
|
117
|
+
return { state: "fulfilled", value: raced };
|
|
118
|
+
} catch {
|
|
119
|
+
return { state: "rejected" };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type ElideResult =
|
|
124
|
+
| { state: "ok"; value: unknown; hasHole: boolean }
|
|
125
|
+
| { state: "rejected" };
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Deep-elide a settled bake-lane container for recording. Settled nested
|
|
129
|
+
* promises pin their value behind a settled marker (they baked, but consumers
|
|
130
|
+
* hold a promise-shaped key); pending ones become hole markers; a REJECTED
|
|
131
|
+
* nested promise poisons the record (error UI must never bake into a shared
|
|
132
|
+
* shell) — the caller refuses the capture. Only plain objects/arrays are
|
|
133
|
+
* traversed; anything else (Date, Map, class instance) is a pinned leaf.
|
|
134
|
+
* Cycles are cut as pinned references (best effort — Flight rejects true
|
|
135
|
+
* cycles later regardless).
|
|
136
|
+
*
|
|
137
|
+
* The ROOT container promise-chain unwraps with NO marker: loader-cache
|
|
138
|
+
* overlays against the AWAITED fresh container, so the recorded root must be
|
|
139
|
+
* the unwrapped structure. Everything below it goes through elideNested.
|
|
140
|
+
*/
|
|
141
|
+
export async function elideLoaderContainer(
|
|
142
|
+
value: unknown,
|
|
143
|
+
seen: Set<object> = new Set(),
|
|
144
|
+
): Promise<ElideResult> {
|
|
145
|
+
if (isThenable(value)) {
|
|
146
|
+
const probed = await probeSettled(value);
|
|
147
|
+
if (probed.state === "pending") {
|
|
148
|
+
return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
|
|
149
|
+
}
|
|
150
|
+
if (probed.state === "rejected") return { state: "rejected" };
|
|
151
|
+
return elideLoaderContainer(probed.value, seen);
|
|
152
|
+
}
|
|
153
|
+
return elideNested(value, seen);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function elideNested(
|
|
157
|
+
value: unknown,
|
|
158
|
+
seen: Set<object>,
|
|
159
|
+
): Promise<ElideResult> {
|
|
160
|
+
if (isThenable(value)) {
|
|
161
|
+
const probed = await probeSettled(value);
|
|
162
|
+
if (probed.state === "pending") {
|
|
163
|
+
return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
|
|
164
|
+
}
|
|
165
|
+
if (probed.state === "rejected") return { state: "rejected" };
|
|
166
|
+
const inner = await elideNested(probed.value, seen);
|
|
167
|
+
if (inner.state === "rejected") return inner;
|
|
168
|
+
const marker: LoaderSettledMarker = inner.hasHole
|
|
169
|
+
? { [LOADER_SETTLED_KEY]: 1, value: inner.value, holes: 1 }
|
|
170
|
+
: { [LOADER_SETTLED_KEY]: 1, value: inner.value };
|
|
171
|
+
return { state: "ok", value: marker, hasHole: inner.hasHole };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
if (seen.has(value)) return { state: "ok", value, hasHole: false };
|
|
176
|
+
seen.add(value);
|
|
177
|
+
const out: unknown[] = new Array(value.length);
|
|
178
|
+
let hasHole = false;
|
|
179
|
+
for (let i = 0; i < value.length; i++) {
|
|
180
|
+
const r = await elideNested(value[i], seen);
|
|
181
|
+
if (r.state === "rejected") return r;
|
|
182
|
+
out[i] = r.value;
|
|
183
|
+
hasHole ||= r.hasHole;
|
|
184
|
+
}
|
|
185
|
+
return { state: "ok", value: out, hasHole };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (isPlainObject(value)) {
|
|
189
|
+
if (seen.has(value)) return { state: "ok", value, hasHole: false };
|
|
190
|
+
seen.add(value);
|
|
191
|
+
const out: Record<string, unknown> = {};
|
|
192
|
+
let hasHole = false;
|
|
193
|
+
for (const key of Object.keys(value)) {
|
|
194
|
+
const r = await elideNested(value[key], seen);
|
|
195
|
+
if (r.state === "rejected") return r;
|
|
196
|
+
out[key] = r.value;
|
|
197
|
+
hasHole ||= r.hasHole;
|
|
198
|
+
}
|
|
199
|
+
return { state: "ok", value: out, hasHole };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { state: "ok", value, hasHole: false };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Overlay a recorded (elided) container onto the fresh run's container for a
|
|
207
|
+
* shell HIT. Recorded wins per path; hole markers take the fresh value (the
|
|
208
|
+
* live nested promise); fresh-only object keys pass through. Where the shapes
|
|
209
|
+
* disagree structurally, recorded wins wholesale — it is what the prelude
|
|
210
|
+
* froze, and parity beats freshness inside the shell.
|
|
211
|
+
*/
|
|
212
|
+
export function overlayLoaderContainer(
|
|
213
|
+
fresh: unknown,
|
|
214
|
+
recorded: unknown,
|
|
215
|
+
): unknown {
|
|
216
|
+
if (isLoaderHoleMarker(recorded)) return fresh;
|
|
217
|
+
|
|
218
|
+
if (isLoaderSettledMarker(recorded)) {
|
|
219
|
+
const pinned = recorded.value;
|
|
220
|
+
// Deep holes inside a settled container (capture-computed `holes` bit)
|
|
221
|
+
// need the fresh promise's resolved value to fill them; a fully-pinned
|
|
222
|
+
// container resolves immediately (the prelude already shows it — never
|
|
223
|
+
// gate it on fresh latency). A rejecting fresh run degrades holes to
|
|
224
|
+
// undefined instead of poisoning the pin.
|
|
225
|
+
if (recorded.holes === 1) {
|
|
226
|
+
return Promise.resolve(fresh).then(
|
|
227
|
+
(freshValue) => overlayLoaderContainer(freshValue, pinned),
|
|
228
|
+
() => overlayLoaderContainer(undefined, pinned),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return Promise.resolve(overlayLoaderContainer(undefined, pinned));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (Array.isArray(recorded)) {
|
|
235
|
+
const freshArr = Array.isArray(fresh) ? fresh : [];
|
|
236
|
+
return recorded.map((item, i) => overlayLoaderContainer(freshArr[i], item));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (isPlainObject(recorded)) {
|
|
240
|
+
if (!isPlainObject(fresh)) {
|
|
241
|
+
// Shape drift: still honor markers (holes fall back to undefined).
|
|
242
|
+
const out: Record<string, unknown> = {};
|
|
243
|
+
for (const key of Object.keys(recorded)) {
|
|
244
|
+
out[key] = overlayLoaderContainer(undefined, recorded[key]);
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
const out: Record<string, unknown> = {};
|
|
249
|
+
for (const key of Object.keys(recorded)) {
|
|
250
|
+
out[key] = overlayLoaderContainer(fresh[key], recorded[key]);
|
|
251
|
+
}
|
|
252
|
+
for (const key of Object.keys(fresh)) {
|
|
253
|
+
if (!(key in out)) out[key] = fresh[key];
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return recorded;
|
|
259
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture-side nested-thenable masking — the mechanism behind "nested-promise
|
|
3
|
+
* shape is the liveness declaration" for BOTH bake-lane loader containers
|
|
4
|
+
* (loader-cache.ts) and pushed handle containers (request-context.ts
|
|
5
|
+
* createUseFunction).
|
|
6
|
+
*
|
|
7
|
+
* Deliberately a LEAF module: request-context needs the mask for handle
|
|
8
|
+
* pushes, and loader-mask (the other natural home) imports request-context —
|
|
9
|
+
* importing from there would cycle. This module imports only is-thenable.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { isThenable } from "../../handles/is-thenable.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A promise that never settles — the masked stand-in for a per-request value
|
|
16
|
+
* during shell capture. The consuming Suspense subtree suspends forever, so
|
|
17
|
+
* the static prerender postpones it as a hole instead of baking a per-request
|
|
18
|
+
* value into the shared shell. The capture abort (`maxWaitMs` in
|
|
19
|
+
* captureShellHTML) bounds how long the prerender waits before it freezes the
|
|
20
|
+
* prelude, so this never hangs the request.
|
|
21
|
+
*/
|
|
22
|
+
export function createMaskedLoaderPromise<T = unknown>(): Promise<T> {
|
|
23
|
+
return new Promise<T>(() => {});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
27
|
+
if (typeof value !== "object" || value === null) return false;
|
|
28
|
+
const proto = Object.getPrototypeOf(value);
|
|
29
|
+
return proto === Object.prototype || proto === null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Deep-copy a container with every NESTED thenable replaced by a masked
|
|
34
|
+
* (never-resolving) promise. Applied during shell capture to (a) bake-lane
|
|
35
|
+
* loader containers (loader-cache.ts) and (b) pushed handle containers
|
|
36
|
+
* (createUseFunction) — the two rango-owned funnels where consumers declare
|
|
37
|
+
* per-request data by promise SHAPE.
|
|
38
|
+
*
|
|
39
|
+
* Why: a nested promise that happened to SETTLE before the capture's quiet
|
|
40
|
+
* window closed used to bake its value into the SHARED shell (and, for
|
|
41
|
+
* loaders, the snapshot pinned it for every HIT) — per-request data frozen
|
|
42
|
+
* and served cross-session (found live: a storefront basket, carrying the
|
|
43
|
+
* capturing session's basketId/customer identifiers, served to anonymous
|
|
44
|
+
* visitors). The window waits for the slowest shared material on the page, so
|
|
45
|
+
* any real data source (a 5ms SQL read, a 200ms basket API) lost the race.
|
|
46
|
+
* Masking makes the consuming subtree postpone as a hole no matter when the
|
|
47
|
+
* promise settles: liveness by declaration, not by racing the window.
|
|
48
|
+
*
|
|
49
|
+
* Only plain objects/arrays are traversed; other values are leaves. The INPUT
|
|
50
|
+
* IS NEVER MUTATED — handler-side loader consumption (the consumption-lane
|
|
51
|
+
* rule, semantic-matrix PPR3) shares the raw container and must keep real
|
|
52
|
+
* values. Cycles are preserved as cycles in the copy.
|
|
53
|
+
*/
|
|
54
|
+
export function maskNestedContainerThenables(
|
|
55
|
+
value: unknown,
|
|
56
|
+
seen: Map<object, unknown> = new Map(),
|
|
57
|
+
): unknown {
|
|
58
|
+
if (isThenable(value)) return createMaskedLoaderPromise();
|
|
59
|
+
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
const cached = seen.get(value);
|
|
62
|
+
if (cached !== undefined) return cached;
|
|
63
|
+
const out: unknown[] = new Array(value.length);
|
|
64
|
+
seen.set(value, out);
|
|
65
|
+
for (let i = 0; i < value.length; i++) {
|
|
66
|
+
out[i] = maskNestedContainerThenables(value[i], seen);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isPlainObject(value)) {
|
|
72
|
+
const cached = seen.get(value);
|
|
73
|
+
if (cached !== undefined) return cached;
|
|
74
|
+
const out: Record<string, unknown> = {};
|
|
75
|
+
seen.set(value, out);
|
|
76
|
+
for (const key of Object.keys(value)) {
|
|
77
|
+
out[key] = maskNestedContainerThenables(value[key], seen);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
@@ -660,6 +660,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
660
660
|
transition: applyViewTransitionDefault(
|
|
661
661
|
parallelEntry.transition,
|
|
662
662
|
deps.viewTransitionDefault,
|
|
663
|
+
parallelId,
|
|
663
664
|
),
|
|
664
665
|
params,
|
|
665
666
|
slot,
|
|
@@ -861,6 +862,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
861
862
|
transition: applyViewTransitionDefault(
|
|
862
863
|
entry.transition,
|
|
863
864
|
deps.viewTransitionDefault,
|
|
865
|
+
entry.shortCode,
|
|
864
866
|
),
|
|
865
867
|
params,
|
|
866
868
|
belongsToRoute,
|
|
@@ -1197,6 +1199,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1197
1199
|
transition: applyViewTransitionDefault(
|
|
1198
1200
|
orphan.transition,
|
|
1199
1201
|
deps.viewTransitionDefault,
|
|
1202
|
+
orphan.shortCode,
|
|
1200
1203
|
),
|
|
1201
1204
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
1202
1205
|
});
|
|
@@ -8,29 +8,49 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { EntryData } from "../../server/context";
|
|
11
|
+
import { getRequestContext } from "../../server/request-context.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* Resolve
|
|
14
|
+
* Resolve a segment's transition config: stamp the `viewTransition` default and
|
|
15
|
+
* peel off a transition({ when }) predicate.
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
* unset, the router-level createRouter({ viewTransition })
|
|
17
|
-
* in so the render gate reads the boundary decision off the
|
|
18
|
-
* and client, via the serialized segment — without the router
|
|
19
|
-
* threaded to the client. Only `false` is ever stamped; an unset
|
|
20
|
-
* value is left untouched because it already means "wrap" at the
|
|
21
|
-
* also avoids needless object allocation and payload growth.
|
|
22
|
-
*
|
|
17
|
+
* `viewTransition`: the per-segment value (set via the transition() DSL) always
|
|
18
|
+
* wins. When it is unset, the router-level createRouter({ viewTransition })
|
|
19
|
+
* default is stamped in so the render gate reads the boundary decision off the
|
|
20
|
+
* segment — server and client, via the serialized segment — without the router
|
|
21
|
+
* option being threaded to the client. Only `false` is ever stamped; an unset
|
|
22
|
+
* (or "auto") value is left untouched because it already means "wrap" at the
|
|
23
|
+
* gate, which also avoids needless object allocation and payload growth.
|
|
24
|
+
*
|
|
25
|
+
* `when`: a server-only predicate. It is STRIPPED from the returned config (a
|
|
26
|
+
* function cannot cross Flight or the segment cache) and recorded on the request
|
|
27
|
+
* context keyed by `segmentId`, so rsc-rendering can evaluate it post-handler —
|
|
28
|
+
* outside any cache scope — and drop this segment's transition when it returns
|
|
29
|
+
* false. Used by both the fresh and revalidation resolution paths.
|
|
23
30
|
*/
|
|
24
31
|
export function applyViewTransitionDefault(
|
|
25
32
|
transition: EntryData["transition"],
|
|
26
33
|
viewTransitionDefault: "auto" | false | undefined,
|
|
34
|
+
segmentId?: string,
|
|
27
35
|
): EntryData["transition"] {
|
|
28
36
|
if (!transition) return transition;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
let result = transition;
|
|
38
|
+
if (result.when) {
|
|
39
|
+
if (segmentId !== undefined) {
|
|
40
|
+
try {
|
|
41
|
+
const ctx = getRequestContext();
|
|
42
|
+
(ctx._transitionWhen ??= []).push({ id: segmentId, when: result.when });
|
|
43
|
+
} catch {
|
|
44
|
+
// No active request context (e.g. a unit test calling this util
|
|
45
|
+
// directly). Skip collection; the strip below still applies so the
|
|
46
|
+
// serialized config never carries the function.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const { when: _when, ...rest } = result;
|
|
50
|
+
result = rest;
|
|
51
|
+
}
|
|
52
|
+
if (result.viewTransition === undefined && viewTransitionDefault === false) {
|
|
53
|
+
return { ...result, viewTransition: false };
|
|
34
54
|
}
|
|
35
|
-
return
|
|
55
|
+
return result;
|
|
36
56
|
}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import { encodePathSegment } from "./url-params.js";
|
|
1
|
+
import { encodePathSegment, encodePathRemainder } from "./url-params.js";
|
|
2
|
+
import { parsePattern } from "./parse-pattern.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Substitute `:param` placeholders in a route pattern with values from
|
|
5
|
-
* `params
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* `params`, producing a URL. Built by walking the SAME parsed segments the
|
|
7
|
+
* matcher uses (`parsePattern`) and emitting one piece per segment — so a
|
|
8
|
+
* substituted value is never re-scanned as if it were another placeholder (a
|
|
9
|
+
* catch-all value like `sha:abc/x` used to make the "required" pass read `:abc`
|
|
10
|
+
* and throw). Constraint syntax (`:name(en|gb)`) is stripped; trailing-slash
|
|
11
|
+
* patterns like `/blog/` are preserved unless an optional segment was omitted.
|
|
12
|
+
*
|
|
13
|
+
* Semantics per segment:
|
|
14
|
+
* - static -> emitted verbatim.
|
|
15
|
+
* - `:name` -> required; `undefined` throws, `""` yields an empty segment.
|
|
16
|
+
* - `:name?` -> optional; `undefined`/`""` omitted.
|
|
17
|
+
* - `:name*` / `:name+`-> catch-all; the value is multi-segment, so each segment
|
|
18
|
+
* is encoded and the `/` separators are preserved. `+`
|
|
19
|
+
* (one-or-more) throws when absent; `*` (and bare `*`)
|
|
20
|
+
* omit when absent.
|
|
10
21
|
*
|
|
11
22
|
* Shared by `ctx.reverse()` (server), `createReverse()` (typed runtime
|
|
12
23
|
* helper), and `useReverse()` (client hook). The behavior must stay
|
|
@@ -17,40 +28,48 @@ export function substitutePatternParams(
|
|
|
17
28
|
params: Record<string, string | undefined>,
|
|
18
29
|
routeName: string,
|
|
19
30
|
): string {
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
const hasTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
32
|
+
const normalized = hasTrailingSlash ? pattern.slice(0, -1) : pattern;
|
|
33
|
+
const segments = parsePattern(normalized);
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// pass `""` explicitly. Treat both as the absent form.
|
|
35
|
+
const parts: string[] = [];
|
|
36
|
+
for (const seg of segments) {
|
|
37
|
+
if (seg.type === "static") {
|
|
38
|
+
parts.push("/" + seg.value);
|
|
39
|
+
} else if (seg.type === "wildcard") {
|
|
40
|
+
const value = params[seg.value];
|
|
30
41
|
if (value === undefined || value === "") {
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
// `:name+` requires at least one segment; bare `*` / `:name*` collapse.
|
|
43
|
+
if (seg.oneOrMore) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Missing param "${seg.value}" for route "${routeName}"`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
parts.push("/" + encodePathRemainder(value));
|
|
33
50
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
} else {
|
|
52
|
+
// Plain param. Constraint (`seg.constraint`) is intentionally not re-emitted.
|
|
53
|
+
const value = params[seg.value];
|
|
54
|
+
const suffix = seg.suffix ?? "";
|
|
55
|
+
if (seg.optional) {
|
|
56
|
+
// The matcher omits absent optionals (`undefined`); callers/getParams()
|
|
57
|
+
// may pass `""` explicitly — treat both as absent.
|
|
58
|
+
if (value !== undefined && value !== "") {
|
|
59
|
+
parts.push("/" + encodePathSegment(value) + suffix);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
if (value === undefined) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Missing param "${seg.value}" for route "${routeName}"`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
parts.push("/" + encodePathSegment(value) + suffix);
|
|
44
68
|
}
|
|
45
|
-
|
|
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 += "/";
|
|
69
|
+
}
|
|
53
70
|
}
|
|
54
71
|
|
|
72
|
+
let result = parts.join("") || "/";
|
|
73
|
+
if (hasTrailingSlash && !result.endsWith("/")) result += "/";
|
|
55
74
|
return result;
|
|
56
75
|
}
|
|
@@ -41,7 +41,7 @@ import { runThenSettle } from "./tracing.js";
|
|
|
41
41
|
import type {
|
|
42
42
|
RouterTracingConfig,
|
|
43
43
|
SpanRunner,
|
|
44
|
-
|
|
44
|
+
TracingToggleOptions,
|
|
45
45
|
} from "./tracing.js";
|
|
46
46
|
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
@@ -92,13 +92,11 @@ const STATUS_ERROR = 2;
|
|
|
92
92
|
// Tracing adapter: phase spans via startActiveSpan (the `tracing` slot)
|
|
93
93
|
// ---------------------------------------------------------------------------
|
|
94
94
|
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
spans?: TracePhaseToggles;
|
|
101
|
-
}
|
|
95
|
+
/**
|
|
96
|
+
* Options for createOTelTracing. Alias of the shared {@link TracingToggleOptions}
|
|
97
|
+
* (`enabled` master switch + per-phase `spans` toggles); the name is public API.
|
|
98
|
+
*/
|
|
99
|
+
export type OTelTracingOptions = TracingToggleOptions;
|
|
102
100
|
|
|
103
101
|
/**
|
|
104
102
|
* Create the tracing config that maps the router's phases onto OTel spans via
|
package/src/router/telemetry.ts
CHANGED
|
@@ -38,6 +38,14 @@ export interface RequestEndEvent extends BaseEvent {
|
|
|
38
38
|
durationMs: number;
|
|
39
39
|
segmentCount: number;
|
|
40
40
|
cacheHit: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* HTTP status when a Response ended the transaction — a thrown-Response
|
|
43
|
+
* short-circuit (redirect / auth gate carries the Response's status, e.g. 302),
|
|
44
|
+
* or dispatch()'s final response status. Absent for a normal render completion:
|
|
45
|
+
* the Response is built after match(), so match()/matchPartial() have no status
|
|
46
|
+
* to stamp there. Lets a sink split 3xx short-circuits from 2xx completions.
|
|
47
|
+
*/
|
|
48
|
+
status?: number;
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
export interface RequestErrorEvent extends BaseEvent {
|
|
@@ -320,7 +328,7 @@ export function createConsoleSink(): TelemetrySink {
|
|
|
320
328
|
break;
|
|
321
329
|
case "request.end":
|
|
322
330
|
console.log(
|
|
323
|
-
`[telemetry] ${event.type} ${event.method} ${event.pathname} ${event.durationMs.toFixed(1)}ms segments=${event.segmentCount} cache=${event.cacheHit}`,
|
|
331
|
+
`[telemetry] ${event.type} ${event.method} ${event.pathname} ${event.durationMs.toFixed(1)}ms segments=${event.segmentCount} cache=${event.cacheHit}${event.status !== undefined ? ` status=${event.status}` : ""}`,
|
|
324
332
|
);
|
|
325
333
|
break;
|
|
326
334
|
case "request.error":
|
package/src/router/tracing.ts
CHANGED
|
@@ -81,17 +81,26 @@ export interface TracePhaseToggles {
|
|
|
81
81
|
ssr?: boolean;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* The option pair shared by every tracing factory (enabled master switch +
|
|
86
|
+
* per-phase span toggles). Extended by OTelTracingOptions,
|
|
87
|
+
* CloudflareTracingOptions, VercelTracingOptions, and RouterTracingConfig so
|
|
88
|
+
* a phase added to TracePhaseToggles propagates everywhere from one place.
|
|
89
|
+
*/
|
|
90
|
+
export interface TracingToggleOptions {
|
|
91
|
+
/** Master switch. Defaults to true. */
|
|
92
|
+
enabled?: boolean;
|
|
93
|
+
/** Per-phase span toggles. Omitted phases default to enabled. */
|
|
94
|
+
spans?: TracePhaseToggles;
|
|
95
|
+
}
|
|
96
|
+
|
|
84
97
|
/**
|
|
85
98
|
* Value passed to `createRouter({ tracing })`. Produced by a platform factory
|
|
86
99
|
* such as `createCloudflareTracing()`.
|
|
87
100
|
*/
|
|
88
|
-
export interface RouterTracingConfig {
|
|
101
|
+
export interface RouterTracingConfig extends TracingToggleOptions {
|
|
89
102
|
/** Platform span runner. */
|
|
90
103
|
runner: SpanRunner;
|
|
91
|
-
/** Master switch. Defaults to true when a config object is provided. */
|
|
92
|
-
enabled?: boolean;
|
|
93
|
-
/** Per-phase span toggles. */
|
|
94
|
-
spans?: TracePhaseToggles;
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
/**
|