@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
|
@@ -18,6 +18,8 @@ import { isInsideCacheScope } from "../server/context.js";
|
|
|
18
18
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
19
19
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
20
20
|
import { PRERENDER_PASSTHROUGH } from "../prerender.js";
|
|
21
|
+
import { substitutePatternParams } from "./substitute-pattern-params.js";
|
|
22
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Strip internal _rsc* query params from a URL.
|
|
@@ -158,48 +160,14 @@ export function createReverseFunction(
|
|
|
158
160
|
);
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
let result = pattern;
|
|
162
|
-
|
|
163
163
|
// Merge current request params as defaults, explicit params override
|
|
164
164
|
const effectiveParams = currentParams
|
|
165
165
|
? { ...currentParams, ...hrefParams }
|
|
166
166
|
: hrefParams;
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
let hadOmittedOptional = false;
|
|
172
|
-
// First pass: optional params (trailing ?)
|
|
173
|
-
result = result.replace(
|
|
174
|
-
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
|
|
175
|
-
(_, key) => {
|
|
176
|
-
const value = effectiveParams[key];
|
|
177
|
-
if (value === undefined) {
|
|
178
|
-
hadOmittedOptional = true;
|
|
179
|
-
return "";
|
|
180
|
-
}
|
|
181
|
-
return encodeURIComponent(value);
|
|
182
|
-
},
|
|
183
|
-
);
|
|
184
|
-
// Second pass: required params (no trailing ?)
|
|
185
|
-
result = result.replace(
|
|
186
|
-
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
|
|
187
|
-
(_, key) => {
|
|
188
|
-
const value = effectiveParams[key];
|
|
189
|
-
if (value === undefined) {
|
|
190
|
-
throw new Error(`Missing param "${key}" for route "${name}"`);
|
|
191
|
-
}
|
|
192
|
-
return encodeURIComponent(value);
|
|
193
|
-
},
|
|
194
|
-
);
|
|
195
|
-
// Clean up slashes only when an optional param was actually omitted,
|
|
196
|
-
// so intentional trailing-slash patterns like "/blog/" are preserved.
|
|
197
|
-
if (hadOmittedOptional) {
|
|
198
|
-
const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
199
|
-
result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
|
|
200
|
-
if (hadTrailingSlash && !result.endsWith("/")) result += "/";
|
|
201
|
-
}
|
|
202
|
-
}
|
|
168
|
+
let result = effectiveParams
|
|
169
|
+
? substitutePatternParams(pattern, effectiveParams, name)
|
|
170
|
+
: pattern;
|
|
203
171
|
|
|
204
172
|
// Append search params as query string
|
|
205
173
|
if (search) {
|
|
@@ -278,8 +246,12 @@ export function createHandlerContext<TEnv>(
|
|
|
278
246
|
search: searchSchema ? resolvedSearchParams : {},
|
|
279
247
|
pathname,
|
|
280
248
|
url,
|
|
281
|
-
originalUrl: new URL(request.url),
|
|
249
|
+
originalUrl: requestContext?.originalUrl ?? new URL(request.url),
|
|
282
250
|
env: bindings,
|
|
251
|
+
waitUntil: requestContext
|
|
252
|
+
? requestContext.waitUntil.bind(requestContext)
|
|
253
|
+
: fireAndForgetWaitUntil,
|
|
254
|
+
executionContext: requestContext?.executionContext,
|
|
283
255
|
_variables: variables,
|
|
284
256
|
get: ((keyOrVar: any) => {
|
|
285
257
|
// Read-time guard: non-cacheable var inside cache() → throw.
|
|
@@ -384,6 +356,12 @@ export function createPrerenderContext<TEnv>(
|
|
|
384
356
|
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
385
357
|
);
|
|
386
358
|
},
|
|
359
|
+
// Build-time prerender has no live request. waitUntil is a true no-op
|
|
360
|
+
// (running fn() here would fire side effects during build, which is
|
|
361
|
+
// incorrect — these are meant to outlive the live response).
|
|
362
|
+
// executionContext is absent for the same reason.
|
|
363
|
+
waitUntil: () => {},
|
|
364
|
+
executionContext: undefined,
|
|
387
365
|
_variables: variables,
|
|
388
366
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
389
367
|
set: ((keyOrVar: any, value: any) => {
|
|
@@ -473,6 +451,11 @@ export function createStaticContext<TEnv>(
|
|
|
473
451
|
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
474
452
|
);
|
|
475
453
|
},
|
|
454
|
+
// Static() handlers have no live request. waitUntil is a true no-op
|
|
455
|
+
// (running fn() here would fire side effects during build, which is
|
|
456
|
+
// incorrect). executionContext is absent for the same reason.
|
|
457
|
+
waitUntil: () => {},
|
|
458
|
+
executionContext: undefined,
|
|
476
459
|
_variables: variables,
|
|
477
460
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
478
461
|
set: ((keyOrVar: any, value: any) => {
|
|
@@ -66,28 +66,14 @@ export function findInterceptForRoute(
|
|
|
66
66
|
let current: EntryData | null = fromEntry;
|
|
67
67
|
|
|
68
68
|
while (current) {
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// current first, then its sibling layouts — same order as before.
|
|
70
|
+
for (const source of [current, ...current.layout]) {
|
|
71
|
+
for (const intercept of source.intercept) {
|
|
71
72
|
if (
|
|
72
73
|
intercept.routeName === targetRouteKey &&
|
|
73
74
|
evaluateInterceptWhen(intercept, selectorContext, isAction)
|
|
74
75
|
) {
|
|
75
|
-
return { intercept, entry:
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (current.layout && current.layout.length > 0) {
|
|
81
|
-
for (const siblingLayout of current.layout) {
|
|
82
|
-
if (siblingLayout.intercept && siblingLayout.intercept.length > 0) {
|
|
83
|
-
for (const intercept of siblingLayout.intercept) {
|
|
84
|
-
if (
|
|
85
|
-
intercept.routeName === targetRouteKey &&
|
|
86
|
-
evaluateInterceptWhen(intercept, selectorContext, isAction)
|
|
87
|
-
) {
|
|
88
|
-
return { intercept, entry: siblingLayout };
|
|
89
|
-
}
|
|
90
|
-
}
|
|
76
|
+
return { intercept, entry: source };
|
|
91
77
|
}
|
|
92
78
|
}
|
|
93
79
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { registerRouteMap } from "../route-map-builder.js";
|
|
2
2
|
import { extractStaticPrefix } from "./pattern-matching.js";
|
|
3
3
|
import {
|
|
4
|
-
EntryData,
|
|
5
|
-
|
|
4
|
+
type EntryData,
|
|
5
|
+
RangoContext,
|
|
6
6
|
runWithPrefixes,
|
|
7
7
|
getIsolatedLazyParent,
|
|
8
8
|
} from "../server/context";
|
|
@@ -125,14 +125,13 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
125
125
|
// Merge captured counters from include() to maintain consistent
|
|
126
126
|
// shortCode indices with sibling entries from pattern extraction
|
|
127
127
|
const lazyCounters: Record<string, number> = {};
|
|
128
|
-
if (lazyContext
|
|
129
|
-
const
|
|
130
|
-
for (const [key, value] of Object.entries(captured)) {
|
|
128
|
+
if (lazyContext?.counters) {
|
|
129
|
+
for (const [key, value] of Object.entries(lazyContext.counters)) {
|
|
131
130
|
lazyCounters[key] = value;
|
|
132
131
|
}
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
|
|
134
|
+
RangoContext.run(
|
|
136
135
|
{
|
|
137
136
|
manifest,
|
|
138
137
|
patterns,
|
|
@@ -141,8 +140,9 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
141
140
|
namespace: "lazy",
|
|
142
141
|
parent: getIsolatedLazyParent(lazyContext?.parent as EntryData | null),
|
|
143
142
|
counters: lazyCounters,
|
|
144
|
-
cacheProfiles:
|
|
145
|
-
rootScoped:
|
|
143
|
+
cacheProfiles: lazyContext?.cacheProfiles,
|
|
144
|
+
rootScoped: lazyContext?.rootScoped,
|
|
145
|
+
includeScope: lazyContext?.includeScope,
|
|
146
146
|
},
|
|
147
147
|
() => {
|
|
148
148
|
// Run the lazy patterns handler with the original context prefixes
|
|
@@ -24,7 +24,10 @@ import { isHandle, collectHandleData, type Handle } from "../handle.js";
|
|
|
24
24
|
import { buildHandleSnapshot } from "../server/handle-store.js";
|
|
25
25
|
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
|
|
26
26
|
import { _getRequestContext } from "../server/request-context.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
isInsideLoaderScope,
|
|
29
|
+
runInsideLoaderBodyScope,
|
|
30
|
+
} from "../server/context.js";
|
|
28
31
|
import { debugLog } from "./logging.js";
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -266,7 +269,10 @@ function createLoaderExecutor<TEnv>(
|
|
|
266
269
|
search: (ctx as any).search,
|
|
267
270
|
pathname: ctx.pathname,
|
|
268
271
|
url: ctx.url,
|
|
272
|
+
originalUrl: ctx.originalUrl,
|
|
269
273
|
env: ctx.env,
|
|
274
|
+
waitUntil: ctx.waitUntil.bind(ctx),
|
|
275
|
+
executionContext: ctx.executionContext,
|
|
270
276
|
get: ((keyOrVar: any) =>
|
|
271
277
|
contextGet(variables, keyOrVar)) as typeof ctx.get,
|
|
272
278
|
use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
@@ -350,8 +356,19 @@ function createLoaderExecutor<TEnv>(
|
|
|
350
356
|
};
|
|
351
357
|
|
|
352
358
|
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
|
359
|
+
// Run the loader body inside loader scope so request-scoped reads
|
|
360
|
+
// (cookies()/headers() and non-cacheable ctx.get) are exempt from the
|
|
361
|
+
// cache-purity guards: loaders always run fresh, so their reads never leak
|
|
362
|
+
// into a cached segment. DSL loaders are already wrapped by fresh.ts; this
|
|
363
|
+
// also covers handler-invoked loaders (ctx.use(Loader) from a handler),
|
|
364
|
+
// which otherwise execute in the caller's cache scope and would wrongly
|
|
365
|
+
// throw. rendered() gating uses the captured isDslLoader (above), so this
|
|
366
|
+
// does not grant rendered() to handler-invoked loaders. Uses a body-only
|
|
367
|
+
// scope, so isInsideLoaderScope() / barrier / deadlock gating is unchanged.
|
|
353
368
|
const promise = Promise.resolve(
|
|
354
|
-
|
|
369
|
+
runInsideLoaderBodyScope(() =>
|
|
370
|
+
loaderFn(loaderCtx as LoaderContext<any, TEnv>),
|
|
371
|
+
),
|
|
355
372
|
).finally(() => {
|
|
356
373
|
pendingLoaders.delete(loader.$$id);
|
|
357
374
|
doneLoader();
|
package/src/router/manifest.ts
CHANGED
|
@@ -126,28 +126,37 @@ export async function loadManifest(
|
|
|
126
126
|
// were created during pattern extraction. This prevents shortCode
|
|
127
127
|
// collisions between lazy and non-lazy entries under the same parent
|
|
128
128
|
// (e.g., ArticlesLayout and BlogLayout both under NavLayout).
|
|
129
|
-
if (lazyContext
|
|
130
|
-
const
|
|
131
|
-
for (const [key, value] of Object.entries(captured)) {
|
|
129
|
+
if (lazyContext?.counters) {
|
|
130
|
+
for (const [key, value] of Object.entries(lazyContext.counters)) {
|
|
132
131
|
Store.counters[key] = Math.max(Store.counters[key] ?? 0, value);
|
|
133
132
|
}
|
|
134
133
|
}
|
|
135
134
|
|
|
136
135
|
// Propagate cache profiles for DSL-time cache("profileName") resolution.
|
|
137
136
|
// Non-lazy entries carry profiles directly; lazy entries carry them
|
|
138
|
-
// in the captured lazyContext from include() time.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
137
|
+
// in the captured lazyContext from include() time. Always write
|
|
138
|
+
// (including clearing to undefined) so a prior lazy build's profile
|
|
139
|
+
// map cannot leak into a later non-lazy build on the same ALS-backed
|
|
140
|
+
// Store — which would otherwise let cache("name") resolve a profile
|
|
141
|
+
// from an unrelated entry.
|
|
142
|
+
Store.cacheProfiles = entry.cacheProfiles ?? lazyContext?.cacheProfiles;
|
|
144
143
|
|
|
145
144
|
// Propagate rootScoped from lazyContext so that routes inside
|
|
146
145
|
// nested { name: "sub" } under { name: "" } keep inherited root scope
|
|
147
|
-
// when the manifest is rebuilt on each request.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
// when the manifest is rebuilt on each request. Always write
|
|
147
|
+
// (including clearing to undefined, which makes getRootScoped()
|
|
148
|
+
// return its true default) so a prior lazy build's scope cannot leak
|
|
149
|
+
// into a later non-lazy build on the same ALS-backed Store — which
|
|
150
|
+
// would otherwise mis-register plain routes as non-root-scoped and
|
|
151
|
+
// break dot-local reverse resolution.
|
|
152
|
+
Store.rootScoped = lazyContext?.rootScoped;
|
|
153
|
+
|
|
154
|
+
// Propagate includeScope from lazyContext so that direct-descendant
|
|
155
|
+
// shortCodes of this include use the correct scoped counter namespace
|
|
156
|
+
// on every manifest rebuild. Always write (including clearing to
|
|
157
|
+
// undefined) so a prior lazy build's scope cannot leak into a later
|
|
158
|
+
// non-lazy build on the same ALS-backed Store.
|
|
159
|
+
Store.includeScope = lazyContext?.includeScope;
|
|
151
160
|
|
|
152
161
|
const handlerExecStart = performance.now();
|
|
153
162
|
const useItems = await getContext().runWithStore(
|
package/src/router/match-api.ts
CHANGED
|
@@ -22,10 +22,10 @@ import { collectRouteMiddleware } from "./middleware.js";
|
|
|
22
22
|
import { traverseBack } from "./pattern-matching.js";
|
|
23
23
|
import { DefaultErrorFallback } from "../default-error-boundary.js";
|
|
24
24
|
import {
|
|
25
|
-
EntryData,
|
|
26
|
-
LoaderEntry,
|
|
25
|
+
type EntryData,
|
|
26
|
+
type LoaderEntry,
|
|
27
27
|
getContext,
|
|
28
|
-
InterceptSelectorContext,
|
|
28
|
+
type InterceptSelectorContext,
|
|
29
29
|
} from "../server/context";
|
|
30
30
|
import type { ErrorBoundaryHandler, ErrorInfo, MatchResult } from "../types";
|
|
31
31
|
import type { ReactNode } from "react";
|
|
@@ -550,6 +550,7 @@ export async function matchError<TEnv>(
|
|
|
550
550
|
segments: [errorSegment],
|
|
551
551
|
matched: matchedIds,
|
|
552
552
|
diff: [errorSegment.id],
|
|
553
|
+
resolvedIds: [errorSegment.id],
|
|
553
554
|
params: matched.params,
|
|
554
555
|
};
|
|
555
556
|
}
|
|
@@ -33,10 +33,13 @@ import type { ErrorBoundaryHandler, NotFoundBoundaryHandler } from "../types";
|
|
|
33
33
|
import type { MiddlewareFn } from "./middleware.js";
|
|
34
34
|
import {
|
|
35
35
|
type TelemetrySink,
|
|
36
|
+
type CacheSegmentSignal,
|
|
36
37
|
safeEmit,
|
|
37
38
|
resolveSink,
|
|
38
39
|
getRequestId,
|
|
40
|
+
buildCacheSignalSegments,
|
|
39
41
|
} from "./telemetry.js";
|
|
42
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
40
43
|
|
|
41
44
|
export interface MatchHandlerDeps<TEnv = any> {
|
|
42
45
|
buildRouterContext: () => RouterContext<TEnv>;
|
|
@@ -51,6 +54,12 @@ export interface MatchHandlerDeps<TEnv = any> {
|
|
|
51
54
|
isAction: boolean,
|
|
52
55
|
) => { intercept: InterceptEntry; entry: EntryData } | null;
|
|
53
56
|
telemetry?: TelemetrySink;
|
|
57
|
+
/**
|
|
58
|
+
* DEVELOPMENT/TEST ONLY gate for the X-Rango-Cache debug header. When true,
|
|
59
|
+
* match/matchPartial stash a coarse route-level cache signal on the request
|
|
60
|
+
* context for the response-finalization path to emit. Default off.
|
|
61
|
+
*/
|
|
62
|
+
cacheSignalEnabled?: boolean;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
export interface MatchHandlers<TEnv = any> {
|
|
@@ -113,6 +122,25 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
113
122
|
} = deps;
|
|
114
123
|
const hasTelemetry = !!deps.telemetry;
|
|
115
124
|
const telemetry = resolveSink(deps.telemetry);
|
|
125
|
+
const cacheSignalEnabled = !!deps.cacheSignalEnabled;
|
|
126
|
+
// Compute the coarse cache signal when EITHER telemetry needs it (for the
|
|
127
|
+
// cache.decision event) OR the debug header gate is on. When neither is set,
|
|
128
|
+
// this is never called — zero extra work on the hot path.
|
|
129
|
+
const buildSignal = (
|
|
130
|
+
routeKey: string,
|
|
131
|
+
state: {
|
|
132
|
+
cacheHit: boolean;
|
|
133
|
+
cacheSource?: "runtime" | "prerender";
|
|
134
|
+
shouldRevalidate?: boolean;
|
|
135
|
+
},
|
|
136
|
+
): CacheSegmentSignal[] => buildCacheSignalSegments(routeKey, state);
|
|
137
|
+
// Stash the signal on the request context for the response path to emit as
|
|
138
|
+
// the X-Rango-Cache header. Only when the debug gate is on.
|
|
139
|
+
const recordSignalIfEnabled = (segments: CacheSegmentSignal[]): void => {
|
|
140
|
+
if (!cacheSignalEnabled) return;
|
|
141
|
+
const reqCtx = _getRequestContext();
|
|
142
|
+
if (reqCtx) reqCtx._cacheSignal = segments;
|
|
143
|
+
};
|
|
116
144
|
|
|
117
145
|
async function createMatchContextForFull(
|
|
118
146
|
request: Request,
|
|
@@ -196,6 +224,7 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
196
224
|
segments: [],
|
|
197
225
|
matched: [],
|
|
198
226
|
diff: [],
|
|
227
|
+
resolvedIds: [],
|
|
199
228
|
params: {},
|
|
200
229
|
redirect: result.redirectUrl,
|
|
201
230
|
};
|
|
@@ -207,17 +236,24 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
207
236
|
const state = createPipelineState();
|
|
208
237
|
const pipeline = createMatchPartialPipeline(ctx, state);
|
|
209
238
|
const matchResult = await collectMatchResult(pipeline, ctx, state);
|
|
239
|
+
if (hasTelemetry || cacheSignalEnabled) {
|
|
240
|
+
const signalSegments = buildSignal(ctx.routeKey, state);
|
|
241
|
+
recordSignalIfEnabled(signalSegments);
|
|
242
|
+
if (hasTelemetry) {
|
|
243
|
+
safeEmit(telemetry, {
|
|
244
|
+
type: "cache.decision",
|
|
245
|
+
timestamp: performance.now(),
|
|
246
|
+
requestId,
|
|
247
|
+
pathname,
|
|
248
|
+
routeKey: ctx.routeKey,
|
|
249
|
+
hit: state.cacheHit,
|
|
250
|
+
shouldRevalidate: !!state.shouldRevalidate,
|
|
251
|
+
source: state.cacheSource,
|
|
252
|
+
segments: signalSegments,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
210
256
|
if (hasTelemetry) {
|
|
211
|
-
safeEmit(telemetry, {
|
|
212
|
-
type: "cache.decision",
|
|
213
|
-
timestamp: performance.now(),
|
|
214
|
-
requestId,
|
|
215
|
-
pathname,
|
|
216
|
-
routeKey: ctx.routeKey,
|
|
217
|
-
hit: state.cacheHit,
|
|
218
|
-
shouldRevalidate: !!state.shouldRevalidate,
|
|
219
|
-
source: state.cacheSource,
|
|
220
|
-
});
|
|
221
257
|
safeEmit(telemetry, {
|
|
222
258
|
type: "request.end",
|
|
223
259
|
timestamp: performance.now(),
|
|
@@ -362,17 +398,24 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
362
398
|
state,
|
|
363
399
|
);
|
|
364
400
|
flushRevalidationTrace();
|
|
401
|
+
if (hasTelemetry || cacheSignalEnabled) {
|
|
402
|
+
const signalSegments = buildSignal(ctx.routeKey, state);
|
|
403
|
+
recordSignalIfEnabled(signalSegments);
|
|
404
|
+
if (hasTelemetry) {
|
|
405
|
+
safeEmit(telemetry, {
|
|
406
|
+
type: "cache.decision",
|
|
407
|
+
timestamp: performance.now(),
|
|
408
|
+
requestId: partialRequestId,
|
|
409
|
+
pathname,
|
|
410
|
+
routeKey: ctx.routeKey,
|
|
411
|
+
hit: state.cacheHit,
|
|
412
|
+
shouldRevalidate: !!state.shouldRevalidate,
|
|
413
|
+
source: state.cacheSource,
|
|
414
|
+
segments: signalSegments,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
365
418
|
if (hasTelemetry) {
|
|
366
|
-
safeEmit(telemetry, {
|
|
367
|
-
type: "cache.decision",
|
|
368
|
-
timestamp: performance.now(),
|
|
369
|
-
requestId: partialRequestId,
|
|
370
|
-
pathname,
|
|
371
|
-
routeKey: ctx.routeKey,
|
|
372
|
-
hit: state.cacheHit,
|
|
373
|
-
shouldRevalidate: !!state.shouldRevalidate,
|
|
374
|
-
source: state.cacheSource,
|
|
375
|
-
});
|
|
376
419
|
safeEmit(telemetry, {
|
|
377
420
|
type: "request.end",
|
|
378
421
|
timestamp: performance.now(),
|
|
@@ -282,6 +282,38 @@ async function* yieldFromStore<TEnv>(
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Look up a prerendered (build-time cached) entry for the current route and, on
|
|
287
|
+
* a hit, yield its segments. Returns true when an entry was served (the caller
|
|
288
|
+
* should stop the pipeline) and false on a miss. Intercept navigations consult
|
|
289
|
+
* only the intercept-specific entry (`paramHash + "/i"`); a miss there falls
|
|
290
|
+
* through to the normal pipeline so intercept-resolution can run. Callers must
|
|
291
|
+
* guard on `prerenderStoreInstance` after `ensurePrerenderDeps()`.
|
|
292
|
+
*/
|
|
293
|
+
async function* tryPrerenderLookup<TEnv>(
|
|
294
|
+
ctx: MatchContext<TEnv>,
|
|
295
|
+
state: MatchPipelineState,
|
|
296
|
+
pipelineStart: number,
|
|
297
|
+
handleStoreRef?: HandleStore,
|
|
298
|
+
): AsyncGenerator<ResolvedSegment, boolean> {
|
|
299
|
+
const paramHash = _hashParams!(ctx.matched.params);
|
|
300
|
+
const isPassthroughPrerenderRoute = ctx.entries.some(
|
|
301
|
+
(entry) => entry.type === "route" && entry.isPassthrough === true,
|
|
302
|
+
);
|
|
303
|
+
const lookupHash = ctx.isIntercept ? paramHash + "/i" : paramHash;
|
|
304
|
+
const entry = await prerenderStoreInstance!.get(
|
|
305
|
+
ctx.matched.routeKey,
|
|
306
|
+
lookupHash,
|
|
307
|
+
{
|
|
308
|
+
pathname: ctx.pathname,
|
|
309
|
+
isPassthroughRoute: isPassthroughPrerenderRoute,
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
if (!entry) return false;
|
|
313
|
+
yield* yieldFromStore(entry, ctx, state, pipelineStart, handleStoreRef);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
285
317
|
/**
|
|
286
318
|
* Async generator middleware type
|
|
287
319
|
*/
|
|
@@ -334,54 +366,13 @@ export function withCacheLookup<TEnv>(
|
|
|
334
366
|
if (!ctx.isAction && !isHmr && ctx.matched.pr) {
|
|
335
367
|
await ensurePrerenderDeps();
|
|
336
368
|
if (prerenderStoreInstance) {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
369
|
+
const served = yield* tryPrerenderLookup(
|
|
370
|
+
ctx,
|
|
371
|
+
state,
|
|
372
|
+
pipelineStart,
|
|
373
|
+
handleStoreRef,
|
|
340
374
|
);
|
|
341
|
-
|
|
342
|
-
if (ctx.isIntercept) {
|
|
343
|
-
// Intercept navigation: try intercept-specific prerender entry
|
|
344
|
-
const entry = await prerenderStoreInstance.get(
|
|
345
|
-
ctx.matched.routeKey,
|
|
346
|
-
paramHash + "/i",
|
|
347
|
-
{
|
|
348
|
-
pathname: ctx.pathname,
|
|
349
|
-
isPassthroughRoute: isPassthroughPrerenderRoute,
|
|
350
|
-
},
|
|
351
|
-
);
|
|
352
|
-
if (entry) {
|
|
353
|
-
yield* yieldFromStore(
|
|
354
|
-
entry,
|
|
355
|
-
ctx,
|
|
356
|
-
state,
|
|
357
|
-
pipelineStart,
|
|
358
|
-
handleStoreRef,
|
|
359
|
-
);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
// No intercept prerender -- fall through to normal pipeline
|
|
363
|
-
// (skip non-intercept prerender to let intercept-resolution run)
|
|
364
|
-
} else {
|
|
365
|
-
// Normal navigation: existing behavior
|
|
366
|
-
const entry = await prerenderStoreInstance.get(
|
|
367
|
-
ctx.matched.routeKey,
|
|
368
|
-
paramHash,
|
|
369
|
-
{
|
|
370
|
-
pathname: ctx.pathname,
|
|
371
|
-
isPassthroughRoute: isPassthroughPrerenderRoute,
|
|
372
|
-
},
|
|
373
|
-
);
|
|
374
|
-
if (entry) {
|
|
375
|
-
yield* yieldFromStore(
|
|
376
|
-
entry,
|
|
377
|
-
ctx,
|
|
378
|
-
state,
|
|
379
|
-
pipelineStart,
|
|
380
|
-
handleStoreRef,
|
|
381
|
-
);
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
375
|
+
if (served) return;
|
|
385
376
|
}
|
|
386
377
|
}
|
|
387
378
|
|
|
@@ -404,51 +395,13 @@ export function withCacheLookup<TEnv>(
|
|
|
404
395
|
if (hasStatic) {
|
|
405
396
|
await ensurePrerenderDeps();
|
|
406
397
|
if (prerenderStoreInstance) {
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
398
|
+
const served = yield* tryPrerenderLookup(
|
|
399
|
+
ctx,
|
|
400
|
+
state,
|
|
401
|
+
pipelineStart,
|
|
402
|
+
handleStoreRef,
|
|
410
403
|
);
|
|
411
|
-
|
|
412
|
-
if (ctx.isIntercept) {
|
|
413
|
-
const entry = await prerenderStoreInstance.get(
|
|
414
|
-
ctx.matched.routeKey,
|
|
415
|
-
paramHash + "/i",
|
|
416
|
-
{
|
|
417
|
-
pathname: ctx.pathname,
|
|
418
|
-
isPassthroughRoute: isPassthroughPrerenderRoute,
|
|
419
|
-
},
|
|
420
|
-
);
|
|
421
|
-
if (entry) {
|
|
422
|
-
yield* yieldFromStore(
|
|
423
|
-
entry,
|
|
424
|
-
ctx,
|
|
425
|
-
state,
|
|
426
|
-
pipelineStart,
|
|
427
|
-
handleStoreRef,
|
|
428
|
-
);
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
// No intercept prerender -- fall through to normal pipeline
|
|
432
|
-
} else {
|
|
433
|
-
const entry = await prerenderStoreInstance.get(
|
|
434
|
-
ctx.matched.routeKey,
|
|
435
|
-
paramHash,
|
|
436
|
-
{
|
|
437
|
-
pathname: ctx.pathname,
|
|
438
|
-
isPassthroughRoute: isPassthroughPrerenderRoute,
|
|
439
|
-
},
|
|
440
|
-
);
|
|
441
|
-
if (entry) {
|
|
442
|
-
yield* yieldFromStore(
|
|
443
|
-
entry,
|
|
444
|
-
ctx,
|
|
445
|
-
state,
|
|
446
|
-
pipelineStart,
|
|
447
|
-
handleStoreRef,
|
|
448
|
-
);
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
404
|
+
if (served) return;
|
|
452
405
|
}
|
|
453
406
|
}
|
|
454
407
|
}
|
|
@@ -169,10 +169,11 @@ export function withCacheStore<TEnv>(
|
|
|
169
169
|
// skip (client already had them). Segments where the handler intentionally
|
|
170
170
|
// returned null are not revalidation skips — re-rendering them will still
|
|
171
171
|
// produce null, so proactive caching would be wasted work.
|
|
172
|
-
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
173
172
|
const hasNullComponents = allSegmentsToCache.some(
|
|
174
173
|
(s) =>
|
|
175
|
-
s.component === null &&
|
|
174
|
+
s.component === null &&
|
|
175
|
+
s.type !== "loader" &&
|
|
176
|
+
ctx.clientSegmentSet.has(s.id),
|
|
176
177
|
);
|
|
177
178
|
|
|
178
179
|
const requestCtx = getRequestContext();
|