@rangojs/router 0.16.0 → 0.17.0
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 +254 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-scope.d.ts +34 -5
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +53 -13
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +15 -3
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +3 -2
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
- package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +10 -0
- package/dist/types/server/context.d.ts +33 -3
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +24 -21
- package/dist/types/ssr/index.d.ts +15 -0
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/vite/index.js +1381 -1346
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +65 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +207 -97
- package/skills/caching/SKILL.md +295 -93
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +53 -31
- package/skills/cloudflare/SKILL.md +72 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +39 -33
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +2 -1
- package/skills/deployment-caching/SKILL.md +6 -1
- package/skills/document-cache/SKILL.md +92 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +53 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +31 -16
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +37 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +242 -106
- package/skills/middleware/SKILL.md +119 -23
- package/skills/migrate-nextjs/SKILL.md +100 -45
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +49 -38
- package/skills/migrate-react-router/component-migration.md +25 -6
- package/skills/migrate-react-router/data-and-actions.md +18 -5
- package/skills/migrate-react-router/route-mapping.md +12 -6
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +28 -14
- package/skills/parallel/SKILL.md +75 -39
- package/skills/ppr/SKILL.md +345 -150
- package/skills/prerender/SKILL.md +189 -100
- package/skills/rango/SKILL.md +65 -34
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +165 -60
- package/skills/router-setup/SKILL.md +418 -240
- package/skills/scripts/SKILL.md +31 -20
- package/skills/server-actions/SKILL.md +220 -59
- package/skills/shell-manifest/SKILL.md +12 -7
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +68 -32
- package/skills/testing/cache-prerender.md +55 -29
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +8 -5
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +106 -29
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +126 -47
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +200 -82
- package/skills/vercel/SKILL.md +21 -11
- package/skills/view-transitions/SKILL.md +41 -20
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +1 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +1 -6
- package/src/cache/cache-runtime.ts +230 -61
- package/src/cache/cache-scope.ts +76 -12
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +132 -28
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/tag-invalidation.ts +4 -3
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +16 -3
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +117 -30
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +34 -25
- package/src/router/intercept-resolution.ts +125 -39
- package/src/router/loader-resolution.ts +56 -11
- package/src/router/match-api.ts +12 -17
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +93 -10
- package/src/router/match-middleware/cache-store.ts +17 -90
- package/src/router/middleware-types.ts +3 -5
- package/src/router/middleware.ts +52 -9
- package/src/router/prerender-match.ts +53 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +44 -12
- package/src/router/segment-resolution/loader-cache.ts +205 -45
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +6 -6
- package/src/router/segment-resolution/mask-nested.ts +7 -7
- package/src/router/segment-resolution/revalidation.ts +34 -6
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +8 -45
- package/src/rsc/shell-capture-constants.ts +13 -0
- package/src/rsc/shell-capture.ts +111 -90
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +11 -0
- package/src/server/context.ts +88 -11
- package/src/server/cookie-store.ts +5 -4
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +35 -30
- package/src/ssr/index.tsx +40 -7
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +8 -6
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "../context-var.js";
|
|
17
17
|
import {
|
|
18
18
|
assertCachedHeaderWriteAllowed,
|
|
19
|
-
|
|
19
|
+
assertNonCacheableReadAllowed,
|
|
20
20
|
} from "../server/context.js";
|
|
21
21
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
22
22
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
@@ -29,6 +29,32 @@ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
|
29
29
|
// once at module load instead of per createHandlerContext (per-request) call.
|
|
30
30
|
const MUTATING_HEADERS_METHODS = new Set(["set", "append", "delete"]);
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* `headers` whose mutating methods run `assertWrite(method)` first. Shared by
|
|
34
|
+
* the handler ctx (createHandlerContext) and the middleware ctx
|
|
35
|
+
* (createMiddlewareContext).
|
|
36
|
+
*/
|
|
37
|
+
export function guardHeaderWrites(
|
|
38
|
+
headers: Headers,
|
|
39
|
+
assertWrite: (method: string | symbol) => void,
|
|
40
|
+
): Headers {
|
|
41
|
+
return new Proxy(headers, {
|
|
42
|
+
get(target, prop, receiver) {
|
|
43
|
+
const value = Reflect.get(target, prop, receiver);
|
|
44
|
+
if (typeof value === "function") {
|
|
45
|
+
if (MUTATING_HEADERS_METHODS.has(prop as string)) {
|
|
46
|
+
return (...args: any[]) => {
|
|
47
|
+
assertWrite(prop);
|
|
48
|
+
return value.apply(target, args);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return value.bind(target);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
32
58
|
/**
|
|
33
59
|
* Strip internal _rsc* query params from a URL.
|
|
34
60
|
* Returns a new URL with only user-facing params.
|
|
@@ -224,23 +250,10 @@ export function createHandlerContext<TEnv>(
|
|
|
224
250
|
// assertCachedHeaderWriteAllowed, the unified #713 guard).
|
|
225
251
|
// Uses lazy `ctx` reference (assigned below) — only the specific handler ctx
|
|
226
252
|
// is stamped by cache-runtime, not the shared request context.
|
|
227
|
-
// MUTATING_HEADERS_METHODS is hoisted to module scope (constant, read-only).
|
|
228
253
|
let ctx: InternalHandlerContext<any, TEnv>;
|
|
229
|
-
const guardedHeaders =
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (typeof value === "function") {
|
|
233
|
-
if (MUTATING_HEADERS_METHODS.has(prop as string)) {
|
|
234
|
-
return (...args: any[]) => {
|
|
235
|
-
assertNotInsideCacheExec(ctx, "headers");
|
|
236
|
-
assertCachedHeaderWriteAllowed("ctx.headers", prop);
|
|
237
|
-
return value.apply(target, args);
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
return value.bind(target);
|
|
241
|
-
}
|
|
242
|
-
return value;
|
|
243
|
-
},
|
|
254
|
+
const guardedHeaders = guardHeaderWrites(stubResponse.headers, (prop) => {
|
|
255
|
+
assertNotInsideCacheExec(ctx, "headers");
|
|
256
|
+
assertCachedHeaderWriteAllowed("ctx.headers", prop);
|
|
244
257
|
});
|
|
245
258
|
|
|
246
259
|
ctx = {
|
|
@@ -263,14 +276,10 @@ export function createHandlerContext<TEnv>(
|
|
|
263
276
|
executionContext: requestContext?.executionContext,
|
|
264
277
|
_variables: variables,
|
|
265
278
|
get: ((keyOrVar: any) => {
|
|
266
|
-
// Read-time guard: non-cacheable var inside cache() →
|
|
267
|
-
// Works for both ContextVar tokens and string keys.
|
|
268
|
-
if (isNonCacheable(variables, keyOrVar)
|
|
269
|
-
|
|
270
|
-
`ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
|
|
271
|
-
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
272
|
-
`and its value would be stale on cache hit. Move the read outside the cached scope.`,
|
|
273
|
-
);
|
|
279
|
+
// Read-time guard: non-cacheable var inside "use cache" or cache() →
|
|
280
|
+
// throw. Works for both ContextVar tokens and string keys.
|
|
281
|
+
if (isNonCacheable(variables, keyOrVar)) {
|
|
282
|
+
assertNonCacheableReadAllowed(keyOrVar);
|
|
274
283
|
}
|
|
275
284
|
return contextGet(variables, keyOrVar);
|
|
276
285
|
}) as HandlerContext<any, TEnv>["get"],
|
|
@@ -26,8 +26,12 @@ import {
|
|
|
26
26
|
warnOnStreamedResponse,
|
|
27
27
|
buildLoaderErrorContext,
|
|
28
28
|
} from "./segment-resolution.js";
|
|
29
|
+
import { catchSegmentError } from "./segment-resolution/helpers.js";
|
|
30
|
+
import { findInOrphans } from "./error-handling.js";
|
|
31
|
+
import { resolveLoaderData } from "./segment-resolution/loader-cache.js";
|
|
29
32
|
import type { SegmentResolutionDeps } from "./types.js";
|
|
30
33
|
import { debugLog } from "./logging.js";
|
|
34
|
+
import { getRouterContext } from "./router-context.js";
|
|
31
35
|
import {
|
|
32
36
|
RangoContext,
|
|
33
37
|
latchPprHeaderScopeForEntries,
|
|
@@ -101,15 +105,20 @@ export function collectInterceptTargetNames(
|
|
|
101
105
|
fromEntry: EntryData | null,
|
|
102
106
|
): string[] {
|
|
103
107
|
const names = new Set<string>();
|
|
108
|
+
// Tolerate partial entries (unit-test mocks omit the arrays); real
|
|
109
|
+
// registration always populates layout/intercept.
|
|
110
|
+
const add = (source: EntryData): undefined => {
|
|
111
|
+
for (const intercept of source.intercept ?? []) {
|
|
112
|
+
names.add(intercept.routeName);
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
};
|
|
104
116
|
let current: EntryData | null = fromEntry;
|
|
117
|
+
let prev: EntryData | null = null;
|
|
105
118
|
while (current) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
for (const intercept of source.intercept ?? []) {
|
|
110
|
-
names.add(intercept.routeName);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
119
|
+
add(current);
|
|
120
|
+
findInOrphans(current, add, prev);
|
|
121
|
+
prev = current;
|
|
113
122
|
current = current.parent;
|
|
114
123
|
}
|
|
115
124
|
return [...names];
|
|
@@ -121,27 +130,62 @@ export function findInterceptForRoute(
|
|
|
121
130
|
selectorContext: InterceptSelectorContext | null = null,
|
|
122
131
|
isAction: boolean = false,
|
|
123
132
|
): { intercept: InterceptEntry; entry: EntryData } | null {
|
|
133
|
+
const match = (source: EntryData) => {
|
|
134
|
+
for (const intercept of source.intercept) {
|
|
135
|
+
if (
|
|
136
|
+
intercept.routeName === targetRouteKey &&
|
|
137
|
+
evaluateInterceptWhen(intercept, selectorContext, isAction)
|
|
138
|
+
) {
|
|
139
|
+
return { intercept, entry: source };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
};
|
|
124
144
|
let current: EntryData | null = fromEntry;
|
|
145
|
+
let prev: EntryData | null = null;
|
|
125
146
|
|
|
126
147
|
while (current) {
|
|
127
|
-
// current first, then its
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
intercept.routeName === targetRouteKey &&
|
|
132
|
-
evaluateInterceptWhen(intercept, selectorContext, isAction)
|
|
133
|
-
) {
|
|
134
|
-
return { intercept, entry: source };
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
148
|
+
// current first, then its orphans (nested ones included) in render order.
|
|
149
|
+
const found = match(current) ?? findInOrphans(current, match, prev);
|
|
150
|
+
if (found) return found;
|
|
138
151
|
|
|
152
|
+
prev = current;
|
|
139
153
|
current = current.parent;
|
|
140
154
|
}
|
|
141
155
|
|
|
142
156
|
return null;
|
|
143
157
|
}
|
|
144
158
|
|
|
159
|
+
/**
|
|
160
|
+
* An intercept handler's throw or notFound() renders the declaring entry's
|
|
161
|
+
* boundary as the slot component (catchSegmentError, as for route handlers on
|
|
162
|
+
* partial navigations: 500/404 status, onError "handler"). Responses and every
|
|
163
|
+
* error on the background re-render (skipMiddleware) rethrow, so a failed
|
|
164
|
+
* re-render aborts its cache write instead of storing the fallback.
|
|
165
|
+
*/
|
|
166
|
+
function renderInterceptHandlerError<TEnv>(
|
|
167
|
+
error: unknown,
|
|
168
|
+
parentEntry: EntryData,
|
|
169
|
+
params: Record<string, string>,
|
|
170
|
+
context: HandlerContext<any, TEnv>,
|
|
171
|
+
deps: SegmentResolutionDeps<TEnv>,
|
|
172
|
+
skipMiddleware: boolean | undefined,
|
|
173
|
+
): ReactNode {
|
|
174
|
+
if (error instanceof Response || skipMiddleware) throw error;
|
|
175
|
+
return catchSegmentError(
|
|
176
|
+
error,
|
|
177
|
+
parentEntry,
|
|
178
|
+
params,
|
|
179
|
+
deps,
|
|
180
|
+
{
|
|
181
|
+
...buildLoaderErrorContext(context),
|
|
182
|
+
isPartial: true,
|
|
183
|
+
telemetry: getRouterContext()?.telemetry,
|
|
184
|
+
},
|
|
185
|
+
context.pathname,
|
|
186
|
+
).component;
|
|
187
|
+
}
|
|
188
|
+
|
|
145
189
|
/**
|
|
146
190
|
* Resolve an intercept entry and emit segment with the slot name.
|
|
147
191
|
*/
|
|
@@ -210,8 +254,8 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
210
254
|
const loaderIds: string[] = [];
|
|
211
255
|
|
|
212
256
|
for (let i = 0; i < interceptEntry.loader.length; i++) {
|
|
213
|
-
const
|
|
214
|
-
|
|
257
|
+
const loaderEntry = interceptEntry.loader[i];
|
|
258
|
+
const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
|
|
215
259
|
const segmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}D${i}.${loader.$$id}`;
|
|
216
260
|
|
|
217
261
|
if (revalidationContext) {
|
|
@@ -273,7 +317,11 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
273
317
|
loaderIds.push(loader.$$id);
|
|
274
318
|
loaderPromises.push(
|
|
275
319
|
deps.wrapLoaderPromise(
|
|
276
|
-
|
|
320
|
+
// The DSL binding's funnel, as for route loaders: a loader's own
|
|
321
|
+
// cache() applies here too.
|
|
322
|
+
runInsideLoaderScope(() =>
|
|
323
|
+
resolveLoaderData(loaderEntry, context, context.pathname),
|
|
324
|
+
),
|
|
277
325
|
parentEntry,
|
|
278
326
|
segmentId,
|
|
279
327
|
context.pathname,
|
|
@@ -284,10 +332,26 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
284
332
|
);
|
|
285
333
|
}
|
|
286
334
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
335
|
+
let handlerResult: ReactNode;
|
|
336
|
+
try {
|
|
337
|
+
handlerResult =
|
|
338
|
+
typeof interceptEntry.handler === "function"
|
|
339
|
+
? handleHandlerResult(interceptEntry.handler(context))
|
|
340
|
+
: interceptEntry.handler;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
handlerResult = renderInterceptHandlerError(
|
|
343
|
+
error,
|
|
344
|
+
parentEntry,
|
|
345
|
+
params,
|
|
346
|
+
context,
|
|
347
|
+
deps,
|
|
348
|
+
options?.skipMiddleware,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
// Consumed only after the layout and loader awaits below; a rejection in that
|
|
352
|
+
// window would be an unhandledRejection (process crash on Node's default
|
|
353
|
+
// mode). The later await / trackHandler still observes it.
|
|
354
|
+
if (handlerResult instanceof Promise) handlerResult.catch(() => {});
|
|
291
355
|
|
|
292
356
|
let layoutElement: ReactNode | undefined;
|
|
293
357
|
if (interceptEntry.layout) {
|
|
@@ -304,6 +368,14 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
304
368
|
|
|
305
369
|
let component: ReactNode;
|
|
306
370
|
let loaderDataPromise: Promise<any[]> | any[] | undefined;
|
|
371
|
+
// A streamed (loading()) handler resolves after the 200 has started, so its
|
|
372
|
+
// rejection stays with the client; track it like route handlers do
|
|
373
|
+
// (onError report, handle-store completion).
|
|
374
|
+
const trackStreamedHandler = (promise: Promise<unknown>): ReactNode =>
|
|
375
|
+
deps.trackHandler(promise, {
|
|
376
|
+
segmentId: `${parentEntry.shortCode}.${interceptEntry.slotName}`,
|
|
377
|
+
segmentType: "parallel",
|
|
378
|
+
}) as ReactNode;
|
|
307
379
|
|
|
308
380
|
if (interceptEntry.loading && loaderPromises.length > 0) {
|
|
309
381
|
if (handlerResult instanceof Promise) {
|
|
@@ -314,20 +386,30 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
314
386
|
}
|
|
315
387
|
component =
|
|
316
388
|
handlerResult instanceof Promise
|
|
317
|
-
? handlerResult
|
|
389
|
+
? trackStreamedHandler(handlerResult)
|
|
318
390
|
: (Promise.resolve(handlerResult) as ReactNode);
|
|
319
391
|
loaderDataPromise = Promise.all(loaderPromises);
|
|
320
|
-
} else if (
|
|
321
|
-
|
|
322
|
-
component =
|
|
323
|
-
handlerResult instanceof Promise ? await handlerResult : handlerResult;
|
|
392
|
+
} else if (interceptEntry.loading && handlerResult instanceof Promise) {
|
|
393
|
+
component = trackStreamedHandler(handlerResult);
|
|
324
394
|
} else {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
395
|
+
if (loaderPromises.length > 0) {
|
|
396
|
+
loaderDataPromise = await Promise.all(loaderPromises);
|
|
397
|
+
}
|
|
398
|
+
component = handlerResult;
|
|
399
|
+
if (handlerResult instanceof Promise) {
|
|
400
|
+
try {
|
|
401
|
+
component = await handlerResult;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
component = renderInterceptHandlerError(
|
|
404
|
+
error,
|
|
405
|
+
parentEntry,
|
|
406
|
+
params,
|
|
407
|
+
context,
|
|
408
|
+
deps,
|
|
409
|
+
options?.skipMiddleware,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
331
413
|
}
|
|
332
414
|
|
|
333
415
|
const interceptSegment = {
|
|
@@ -401,8 +483,8 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
401
483
|
} = revalidationContext;
|
|
402
484
|
|
|
403
485
|
for (let i = 0; i < interceptEntry.loader.length; i++) {
|
|
404
|
-
const
|
|
405
|
-
|
|
486
|
+
const loaderEntry = interceptEntry.loader[i];
|
|
487
|
+
const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
|
|
406
488
|
const segmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}D${i}.${loader.$$id}`;
|
|
407
489
|
|
|
408
490
|
const interceptSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
|
|
@@ -451,7 +533,11 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
451
533
|
loaderIds.push(loader.$$id);
|
|
452
534
|
loaderPromises.push(
|
|
453
535
|
deps.wrapLoaderPromise(
|
|
454
|
-
|
|
536
|
+
// The DSL binding's funnel, as for route loaders: a loader's own
|
|
537
|
+
// cache() applies here too.
|
|
538
|
+
runInsideLoaderScope(() =>
|
|
539
|
+
resolveLoaderData(loaderEntry, context, context.pathname),
|
|
540
|
+
),
|
|
455
541
|
parentEntry,
|
|
456
542
|
segmentId,
|
|
457
543
|
context.pathname,
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import type { ReactNode } from "react";
|
|
8
8
|
import type { EntryData } from "../server/context";
|
|
9
9
|
import { observePhase, PHASES } from "./instrument.js";
|
|
10
|
-
import { contextGet } from "../context-var.js";
|
|
10
|
+
import { contextGet, isNonCacheable } from "../context-var.js";
|
|
11
|
+
import { NOCACHE_SYMBOL } from "../cache/taint.js";
|
|
11
12
|
import type {
|
|
12
13
|
ResolvedSegment,
|
|
13
14
|
HandlerContext,
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
|
|
44
45
|
import { _getRequestContext } from "../server/request-context.js";
|
|
45
46
|
import {
|
|
47
|
+
assertNonCacheableReadAllowed,
|
|
46
48
|
isInsideLoaderScope,
|
|
47
49
|
runInsideLoaderBodyScope,
|
|
48
50
|
isInsidePushCallbackScope,
|
|
@@ -394,6 +396,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
394
396
|
// Capture RequestContext eagerly for cookie access (ALS protection on Cloudflare)
|
|
395
397
|
const reqCtxRef = _getRequestContext();
|
|
396
398
|
const silentHandles = executorOptions?.silentHandles === true;
|
|
399
|
+
const internal = ctx as InternalHandlerContext<any, TEnv>;
|
|
397
400
|
|
|
398
401
|
// Dependency graph: loaderId -> set of loader IDs it directly depends on.
|
|
399
402
|
const dependsOn = new Map<string, Set<string>>();
|
|
@@ -429,7 +432,12 @@ function createLoaderExecutor<TEnv>(
|
|
|
429
432
|
deps.add(loader.$$id);
|
|
430
433
|
}
|
|
431
434
|
|
|
432
|
-
//
|
|
435
|
+
// Every loader execution funnels here (handler and loader-to-loader
|
|
436
|
+
// reads, uncached DSL bindings, cache misses): memoized per request, so a
|
|
437
|
+
// loader runs at most once however many reads, with no store cache. A
|
|
438
|
+
// loader's cache() read-through lives in loader-cache.ts
|
|
439
|
+
// (executeLoaderData), whose ctx.use override serves a later handler read
|
|
440
|
+
// the binding's result.
|
|
433
441
|
if (loaderPromises.has(loader.$$id)) {
|
|
434
442
|
return loaderPromises.get(loader.$$id)!;
|
|
435
443
|
}
|
|
@@ -452,7 +460,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
452
460
|
pendingLoaders.add(loader.$$id);
|
|
453
461
|
|
|
454
462
|
const currentLoaderId = loader.$$id;
|
|
455
|
-
const variables =
|
|
463
|
+
const variables = internal._variables;
|
|
456
464
|
|
|
457
465
|
// Capture whether this loader is being started from a DSL loader scope
|
|
458
466
|
// (runInsideLoaderScope in fresh.ts). Handler-invoked loaders are NOT
|
|
@@ -466,18 +474,26 @@ function createLoaderExecutor<TEnv>(
|
|
|
466
474
|
// stale. Attributing to the owning route/layout segment gives handle
|
|
467
475
|
// pushes the same granularity handler pushes have (segmentOrder-correct
|
|
468
476
|
// for breadcrumb accumulation).
|
|
469
|
-
const owningSegmentId =
|
|
470
|
-
._currentSegmentId;
|
|
477
|
+
const owningSegmentId = internal._currentSegmentId;
|
|
471
478
|
|
|
472
479
|
let renderedResolved = false;
|
|
473
480
|
let renderedPromise: Promise<void> | null = null;
|
|
474
481
|
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
// (
|
|
480
|
-
|
|
482
|
+
// Request-scoped like the handler ctx it derives from (issue #940): the
|
|
483
|
+
// NOCACHE_SYMBOL brand below makes a "use cache" function taking it key by
|
|
484
|
+
// the route fields (_routeName, _responseType) and replay its handle
|
|
485
|
+
// pushes into the owning segment, claimed per loader like a handler ctx
|
|
486
|
+
// (_claimLoaderPushes, _runLoaderIsolated). Non-cacheable ctx.get reads
|
|
487
|
+
// run the shared guard; the body itself stays exempt (loader body scope).
|
|
488
|
+
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> &
|
|
489
|
+
Pick<
|
|
490
|
+
InternalHandlerContext<any, TEnv>,
|
|
491
|
+
| "_routeName"
|
|
492
|
+
| "_responseType"
|
|
493
|
+
| "_currentSegmentId"
|
|
494
|
+
| "_claimLoaderPushes"
|
|
495
|
+
| "_runLoaderIsolated"
|
|
496
|
+
> = {
|
|
481
497
|
params: ctx.params,
|
|
482
498
|
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
483
499
|
request: ctx.request,
|
|
@@ -517,6 +533,9 @@ function createLoaderExecutor<TEnv>(
|
|
|
517
533
|
buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
|
|
518
534
|
return collectHandleData(keyOrVar, snapshot, segmentOrder);
|
|
519
535
|
}
|
|
536
|
+
if (isNonCacheable(variables, keyOrVar)) {
|
|
537
|
+
assertNonCacheableReadAllowed(keyOrVar);
|
|
538
|
+
}
|
|
520
539
|
return contextGet(variables, keyOrVar);
|
|
521
540
|
}) as typeof ctx.get,
|
|
522
541
|
use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
@@ -552,6 +571,11 @@ function createLoaderExecutor<TEnv>(
|
|
|
552
571
|
method: "GET",
|
|
553
572
|
body: undefined,
|
|
554
573
|
reverse: ctx.reverse as LoaderContext["reverse"],
|
|
574
|
+
_routeName: internal._routeName,
|
|
575
|
+
_responseType: internal._responseType,
|
|
576
|
+
_currentSegmentId: owningSegmentId,
|
|
577
|
+
_claimLoaderPushes: internal._claimLoaderPushes,
|
|
578
|
+
_runLoaderIsolated: internal._runLoaderIsolated,
|
|
555
579
|
rendered: (): Promise<void> => {
|
|
556
580
|
// Guard: only DSL loaders may use rendered()
|
|
557
581
|
if (!isDslLoader) {
|
|
@@ -639,6 +663,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
639
663
|
return renderedPromise;
|
|
640
664
|
},
|
|
641
665
|
};
|
|
666
|
+
(loaderCtx as any)[NOCACHE_SYMBOL] = true;
|
|
642
667
|
|
|
643
668
|
// Meter this loader once via observePhase (loader:<id> perf metric +
|
|
644
669
|
// rango.loader span); loaderFn runs inside the span callback so its KV/D1/
|
|
@@ -736,6 +761,26 @@ export function setupLoaderAccess<TEnv>(
|
|
|
736
761
|
|
|
737
762
|
const useLoader = createLoaderExecutor(ctx, loaderPromises);
|
|
738
763
|
|
|
764
|
+
// Loader-cache HIT replay (loader-cache.ts replayLoaderHandles): a cached
|
|
765
|
+
// entry records its ctx.use dependencies' pushes too, and a dependency is
|
|
766
|
+
// memoized here and shared with live readers. A loader that already ran
|
|
767
|
+
// here keeps its live pushes; otherwise one replay delivers its recorded
|
|
768
|
+
// pushes, and a later run here replaces them (HandleStore.pushReplayed).
|
|
769
|
+
const claimed = new Set<string>();
|
|
770
|
+
const internal = ctx as InternalHandlerContext<any, TEnv>;
|
|
771
|
+
internal._claimLoaderPushes = (loaderId) => {
|
|
772
|
+
if (loaderPromises.has(loaderId) || claimed.has(loaderId)) return false;
|
|
773
|
+
claimed.add(loaderId);
|
|
774
|
+
return true;
|
|
775
|
+
};
|
|
776
|
+
// A refresh's pushes are diverted, so its loader ctxs never claim
|
|
777
|
+
// (cache-runtime.ts refreshView).
|
|
778
|
+
internal._runLoaderIsolated = (loader) =>
|
|
779
|
+
createLoaderExecutor(
|
|
780
|
+
Object.create(ctx, { _claimLoaderPushes: { value: undefined } }),
|
|
781
|
+
new Map(),
|
|
782
|
+
)(loader, null);
|
|
783
|
+
|
|
739
784
|
ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
740
785
|
if (isHandle(item)) {
|
|
741
786
|
const handle = item;
|
package/src/router/match-api.ts
CHANGED
|
@@ -7,7 +7,9 @@ import { RouteNotFoundError } from "../errors";
|
|
|
7
7
|
import {
|
|
8
8
|
createErrorInfo,
|
|
9
9
|
createErrorSegment,
|
|
10
|
+
findInOrphans,
|
|
10
11
|
findNearestErrorBoundary as findErrorBoundary,
|
|
12
|
+
withErrorBoundary,
|
|
11
13
|
} from "./error-handling.js";
|
|
12
14
|
import {
|
|
13
15
|
createHandlerContext,
|
|
@@ -481,24 +483,21 @@ export async function matchError<TEnv>(
|
|
|
481
483
|
segmentType,
|
|
482
484
|
);
|
|
483
485
|
|
|
486
|
+
// Same search order as findNearestErrorBoundary, so the fallback above and
|
|
487
|
+
// the entry that positions it agree.
|
|
484
488
|
let entryWithBoundary: EntryData | null = null;
|
|
485
489
|
let current: EntryData | null = manifestEntry;
|
|
490
|
+
let prev: EntryData | null = null;
|
|
486
491
|
while (current) {
|
|
487
492
|
if (current.errorBoundary && current.errorBoundary.length > 0) {
|
|
488
493
|
entryWithBoundary = current;
|
|
489
494
|
break;
|
|
490
495
|
}
|
|
491
496
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (orphan.errorBoundary && orphan.errorBoundary.length > 0) {
|
|
495
|
-
entryWithBoundary = orphan;
|
|
496
|
-
break;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
if (entryWithBoundary) break;
|
|
500
|
-
}
|
|
497
|
+
entryWithBoundary = findInOrphans(current, withErrorBoundary, prev) ?? null;
|
|
498
|
+
if (entryWithBoundary) break;
|
|
501
499
|
|
|
500
|
+
prev = current;
|
|
502
501
|
current = current.parent;
|
|
503
502
|
}
|
|
504
503
|
|
|
@@ -507,23 +506,19 @@ export async function matchError<TEnv>(
|
|
|
507
506
|
|
|
508
507
|
if (entryWithBoundary) {
|
|
509
508
|
boundaryEntry = entryWithBoundary;
|
|
509
|
+
// The chain entry holding the boundary, itself or in its orphan tree: the
|
|
510
|
+
// error segment takes the place of its child in the chain.
|
|
511
|
+
const boundaryOwner = current;
|
|
510
512
|
|
|
511
513
|
outletEntry = manifestEntry;
|
|
512
514
|
current = manifestEntry;
|
|
513
515
|
|
|
514
516
|
while (current) {
|
|
515
|
-
if (current.parent ===
|
|
517
|
+
if (current.parent === boundaryOwner) {
|
|
516
518
|
outletEntry = current;
|
|
517
519
|
break;
|
|
518
520
|
}
|
|
519
521
|
|
|
520
|
-
if (current.parent && current.parent.layout) {
|
|
521
|
-
if (current.parent.layout.includes(boundaryEntry)) {
|
|
522
|
-
outletEntry = current;
|
|
523
|
-
break;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
522
|
current = current.parent;
|
|
528
523
|
}
|
|
529
524
|
} else {
|