@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1
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 +9 -9
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +914 -485
- package/package.json +55 -11
- 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 +3 -1
- package/skills/hooks/SKILL.md +214 -18
- package/skills/host-router/SKILL.md +45 -20
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +149 -6
- package/skills/middleware/SKILL.md +13 -9
- package/skills/migrate-nextjs/SKILL.md +1 -1
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +5 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/route/SKILL.md +13 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +53 -41
- package/skills/testing/SKILL.md +599 -0
- package/skills/typesafety/SKILL.md +310 -26
- 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/event-controller.ts +42 -66
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +6 -6
- package/src/browser/navigation-client.ts +12 -15
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +9 -19
- package/src/browser/react/NavigationProvider.tsx +29 -40
- 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-params.ts +3 -4
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +14 -1
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +30 -16
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +2 -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-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 +49 -6
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +10 -8
- 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 +6 -4
- package/src/index.ts +13 -6
- 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/prerender.ts +4 -4
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -41
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +238 -263
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +37 -14
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -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 +4 -42
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/loader-resolution.ts +16 -2
- package/src/router/match-handlers.ts +62 -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 +32 -30
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/middleware.ts +46 -78
- 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 +43 -1
- 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 +19 -6
- package/src/router/segment-resolution/revalidation.ts +19 -6
- 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/types.ts +8 -0
- package/src/router.ts +37 -21
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +20 -65
- package/src/rsc/helpers.ts +22 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/response-route-handler.ts +32 -52
- package/src/rsc/rsc-rendering.ts +27 -53
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +13 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +121 -65
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +10 -0
- 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 +56 -11
- package/src/types/index.ts +1 -0
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- 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 +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +70 -48
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +40 -84
- 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 +3 -7
- package/src/vite/plugins/client-ref-hashing.ts +12 -1
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
- package/src/vite/plugins/expose-action-id.ts +2 -2
- 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-internal-ids.ts +47 -67
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +13 -11
- package/src/vite/plugins/version-injector.ts +2 -12
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +67 -15
- package/src/vite/router-discovery.ts +208 -63
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- 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/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -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
|
/**
|
|
@@ -353,8 +356,19 @@ function createLoaderExecutor<TEnv>(
|
|
|
353
356
|
};
|
|
354
357
|
|
|
355
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.
|
|
356
368
|
const promise = Promise.resolve(
|
|
357
|
-
|
|
369
|
+
runInsideLoaderBodyScope(() =>
|
|
370
|
+
loaderFn(loaderCtx as LoaderContext<any, TEnv>),
|
|
371
|
+
),
|
|
358
372
|
).finally(() => {
|
|
359
373
|
pendingLoaders.delete(loader.$$id);
|
|
360
374
|
doneLoader();
|
|
@@ -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,
|
|
@@ -208,17 +236,24 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
208
236
|
const state = createPipelineState();
|
|
209
237
|
const pipeline = createMatchPartialPipeline(ctx, state);
|
|
210
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
|
+
}
|
|
211
256
|
if (hasTelemetry) {
|
|
212
|
-
safeEmit(telemetry, {
|
|
213
|
-
type: "cache.decision",
|
|
214
|
-
timestamp: performance.now(),
|
|
215
|
-
requestId,
|
|
216
|
-
pathname,
|
|
217
|
-
routeKey: ctx.routeKey,
|
|
218
|
-
hit: state.cacheHit,
|
|
219
|
-
shouldRevalidate: !!state.shouldRevalidate,
|
|
220
|
-
source: state.cacheSource,
|
|
221
|
-
});
|
|
222
257
|
safeEmit(telemetry, {
|
|
223
258
|
type: "request.end",
|
|
224
259
|
timestamp: performance.now(),
|
|
@@ -363,17 +398,24 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
363
398
|
state,
|
|
364
399
|
);
|
|
365
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
|
+
}
|
|
366
418
|
if (hasTelemetry) {
|
|
367
|
-
safeEmit(telemetry, {
|
|
368
|
-
type: "cache.decision",
|
|
369
|
-
timestamp: performance.now(),
|
|
370
|
-
requestId: partialRequestId,
|
|
371
|
-
pathname,
|
|
372
|
-
routeKey: ctx.routeKey,
|
|
373
|
-
hit: state.cacheHit,
|
|
374
|
-
shouldRevalidate: !!state.shouldRevalidate,
|
|
375
|
-
source: state.cacheSource,
|
|
376
|
-
});
|
|
377
419
|
safeEmit(telemetry, {
|
|
378
420
|
type: "request.end",
|
|
379
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();
|
|
@@ -138,34 +138,38 @@ export async function collectSegments(
|
|
|
138
138
|
function deduplicateLoaderSegments(
|
|
139
139
|
segments: ResolvedSegment[],
|
|
140
140
|
logPrefix: string,
|
|
141
|
-
): ResolvedSegment[] {
|
|
142
|
-
//
|
|
143
|
-
// and
|
|
141
|
+
): { segments: ResolvedSegment[]; removedIds: Set<string> } {
|
|
142
|
+
// Single pass: original (non-inherited) loaderIds, all loaderIds grouped by
|
|
143
|
+
// namespace, and namespaces of segments that declare loading().
|
|
144
144
|
const originalLoaders = new Set<string>();
|
|
145
|
-
const
|
|
145
|
+
const loaderIdsByNamespace = new Map<string, string[]>();
|
|
146
|
+
const namespacesWithLoading = new Set<string>();
|
|
146
147
|
for (const s of segments) {
|
|
147
|
-
if (s.type === "loader" && s.loaderId
|
|
148
|
-
originalLoaders.add(s.loaderId);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
if (s.type === "loader" && s.loaderId) {
|
|
149
|
+
if (!s._inherited) originalLoaders.add(s.loaderId);
|
|
150
|
+
const ids = loaderIdsByNamespace.get(s.namespace);
|
|
151
|
+
if (ids) ids.push(s.loaderId);
|
|
152
|
+
else loaderIdsByNamespace.set(s.namespace, [s.loaderId]);
|
|
153
|
+
} else if (
|
|
154
|
+
s.type !== "loader" &&
|
|
155
|
+
s.loading !== undefined &&
|
|
156
|
+
s.loading !== false
|
|
157
|
+
) {
|
|
158
|
+
namespacesWithLoading.add(s.namespace);
|
|
152
159
|
}
|
|
153
160
|
}
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
loadersWithLoading.add(l.loaderId);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
161
|
+
|
|
162
|
+
// An inherited loader is needed when it shares a namespace with a
|
|
163
|
+
// loading-bearing segment (its data sits behind that LoaderBoundary).
|
|
164
|
+
const loadersWithLoading = new Set<string>();
|
|
165
|
+
for (const ns of namespacesWithLoading) {
|
|
166
|
+
for (const id of loaderIdsByNamespace.get(ns) ?? []) {
|
|
167
|
+
loadersWithLoading.add(id);
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
const result: ResolvedSegment[] = [];
|
|
168
|
-
|
|
172
|
+
const removedIds = new Set<string>();
|
|
169
173
|
|
|
170
174
|
for (const s of segments) {
|
|
171
175
|
if (
|
|
@@ -175,17 +179,20 @@ function deduplicateLoaderSegments(
|
|
|
175
179
|
originalLoaders.has(s.loaderId) &&
|
|
176
180
|
!loadersWithLoading.has(s.loaderId)
|
|
177
181
|
) {
|
|
178
|
-
|
|
182
|
+
removedIds.add(s.id);
|
|
179
183
|
continue;
|
|
180
184
|
}
|
|
181
185
|
result.push(s);
|
|
182
186
|
}
|
|
183
187
|
|
|
184
|
-
if (
|
|
185
|
-
debugLog(
|
|
188
|
+
if (removedIds.size > 0) {
|
|
189
|
+
debugLog(
|
|
190
|
+
logPrefix,
|
|
191
|
+
`deduped ${removedIds.size} inherited loader segment(s)`,
|
|
192
|
+
);
|
|
186
193
|
}
|
|
187
194
|
|
|
188
|
-
return result;
|
|
195
|
+
return { segments: result, removedIds };
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
/**
|
|
@@ -244,7 +251,7 @@ export function buildMatchResult<TEnv>(
|
|
|
244
251
|
);
|
|
245
252
|
}
|
|
246
253
|
|
|
247
|
-
const dedupedSegments = deduplicateLoaderSegments(
|
|
254
|
+
const { segments: dedupedSegments, removedIds } = deduplicateLoaderSegments(
|
|
248
255
|
segmentsToRender,
|
|
249
256
|
logPrefix,
|
|
250
257
|
);
|
|
@@ -262,11 +269,6 @@ export function buildMatchResult<TEnv>(
|
|
|
262
269
|
|
|
263
270
|
// Remove deduped loader IDs from matched so the client doesn't treat
|
|
264
271
|
// them as missing segments and trigger a fallback refetch.
|
|
265
|
-
const removedIds = new Set(
|
|
266
|
-
segmentsToRender
|
|
267
|
-
.filter((s) => !dedupedSegments.includes(s))
|
|
268
|
-
.map((s) => s.id),
|
|
269
|
-
);
|
|
270
272
|
const matchedIds =
|
|
271
273
|
removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
|
|
272
274
|
|
package/src/router/metrics.ts
CHANGED
|
@@ -140,7 +140,7 @@ export interface MiddlewareContext<
|
|
|
140
140
|
* @template TEnv - Environment type - defaults to any for internal flexibility
|
|
141
141
|
* @template TParams - URL params type (typed for route middleware)
|
|
142
142
|
*
|
|
143
|
-
* When using middleware with global augmentation (
|
|
143
|
+
* When using middleware with global augmentation (Rango.Env), explicitly
|
|
144
144
|
* annotate your middleware functions, or the types will be inferred from context:
|
|
145
145
|
*
|
|
146
146
|
* @example
|
package/src/router/middleware.ts
CHANGED
|
@@ -307,6 +307,46 @@ export function matchMiddleware<TEnv>(
|
|
|
307
307
|
return matches;
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
// Set-Cookie is appended; for other headers stubOverridesNonCookie=true
|
|
311
|
+
// overwrites (chain ran to completion), false fills only missing slots (an
|
|
312
|
+
// explicit short-circuit Response's own headers win).
|
|
313
|
+
function mergeStubHeaders(
|
|
314
|
+
target: Headers,
|
|
315
|
+
stub: Headers,
|
|
316
|
+
stubOverridesNonCookie: boolean,
|
|
317
|
+
): void {
|
|
318
|
+
stub.forEach((value, name) => {
|
|
319
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
320
|
+
target.append(name, value);
|
|
321
|
+
} else if (stubOverridesNonCookie || !target.has(name)) {
|
|
322
|
+
target.set(name, value);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Set-Cookie is deduped so a nested inner executeMiddleware that already merged
|
|
328
|
+
// the same reqCtx cookies does not duplicate them; other headers fill if missing.
|
|
329
|
+
function mergeReqCtxStub(
|
|
330
|
+
target: Headers,
|
|
331
|
+
reqCtx: ReturnType<typeof _getRequestContext>,
|
|
332
|
+
): void {
|
|
333
|
+
if (!reqCtx) return;
|
|
334
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
335
|
+
if (stubCookies.length > 0) {
|
|
336
|
+
const existing = new Set(target.getSetCookie());
|
|
337
|
+
for (const cookie of stubCookies) {
|
|
338
|
+
if (!existing.has(cookie)) {
|
|
339
|
+
target.append("set-cookie", cookie);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
344
|
+
if (name !== "set-cookie" && !target.has(name)) {
|
|
345
|
+
target.set(name, value);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
310
350
|
/**
|
|
311
351
|
* Execute middleware chain
|
|
312
352
|
*
|
|
@@ -345,36 +385,9 @@ export async function executeMiddleware<TEnv>(
|
|
|
345
385
|
// End of chain - call actual RSC handler
|
|
346
386
|
const response = await finalHandler();
|
|
347
387
|
|
|
348
|
-
// Merge headers set on stub into the real response.
|
|
349
|
-
// Use append for Set-Cookie to preserve multiple cookies.
|
|
350
388
|
const mergedHeaders = new Headers(response.headers);
|
|
351
|
-
stubResponse.headers
|
|
352
|
-
|
|
353
|
-
mergedHeaders.append(name, value);
|
|
354
|
-
} else {
|
|
355
|
-
mergedHeaders.set(name, value);
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
// Also merge shared RequestContext stub (cookies written via cookies().set()).
|
|
359
|
-
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
360
|
-
// may have already merged the same reqCtx cookies into the response.
|
|
361
|
-
const reqCtx = _getRequestContext();
|
|
362
|
-
if (reqCtx) {
|
|
363
|
-
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
364
|
-
if (stubCookies.length > 0) {
|
|
365
|
-
const existing = new Set(mergedHeaders.getSetCookie());
|
|
366
|
-
for (const cookie of stubCookies) {
|
|
367
|
-
if (!existing.has(cookie)) {
|
|
368
|
-
mergedHeaders.append("set-cookie", cookie);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
373
|
-
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
374
|
-
mergedHeaders.set(name, value);
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
}
|
|
389
|
+
mergeStubHeaders(mergedHeaders, stubResponse.headers, true);
|
|
390
|
+
mergeReqCtxStub(mergedHeaders, _getRequestContext());
|
|
378
391
|
|
|
379
392
|
if (isWebSocketUpgradeResponse(response)) {
|
|
380
393
|
responseHolder.response = response;
|
|
@@ -485,33 +498,8 @@ export async function executeMiddleware<TEnv>(
|
|
|
485
498
|
return result;
|
|
486
499
|
}
|
|
487
500
|
const mergedHeaders = new Headers(result.headers);
|
|
488
|
-
stubResponse.headers
|
|
489
|
-
|
|
490
|
-
mergedHeaders.append(name, value);
|
|
491
|
-
} else if (!mergedHeaders.has(name)) {
|
|
492
|
-
mergedHeaders.set(name, value);
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
// Also merge shared RequestContext stub (cookies written via setCookie).
|
|
496
|
-
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
497
|
-
// may have already merged the same reqCtx cookies into the response.
|
|
498
|
-
const reqCtx = _getRequestContext();
|
|
499
|
-
if (reqCtx) {
|
|
500
|
-
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
501
|
-
if (stubCookies.length > 0) {
|
|
502
|
-
const existing = new Set(mergedHeaders.getSetCookie());
|
|
503
|
-
for (const cookie of stubCookies) {
|
|
504
|
-
if (!existing.has(cookie)) {
|
|
505
|
-
mergedHeaders.append("set-cookie", cookie);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
510
|
-
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
511
|
-
mergedHeaders.set(name, value);
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
}
|
|
501
|
+
mergeStubHeaders(mergedHeaders, stubResponse.headers, false);
|
|
502
|
+
mergeReqCtxStub(mergedHeaders, _getRequestContext());
|
|
515
503
|
const merged = new Response(result.body, {
|
|
516
504
|
status: result.status,
|
|
517
505
|
statusText: result.statusText,
|
|
@@ -565,21 +553,7 @@ export async function executeMiddleware<TEnv>(
|
|
|
565
553
|
// set-cookie on an upgrade is not meaningful.
|
|
566
554
|
const reqCtx = _getRequestContext();
|
|
567
555
|
if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
|
|
568
|
-
|
|
569
|
-
if (stubCookies.length > 0) {
|
|
570
|
-
const existingCookies = new Set(finalResponse.headers.getSetCookie());
|
|
571
|
-
for (const cookie of stubCookies) {
|
|
572
|
-
if (!existingCookies.has(cookie)) {
|
|
573
|
-
finalResponse.headers.append("set-cookie", cookie);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
// Fill in non-cookie headers that aren't already on the response
|
|
578
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
579
|
-
if (name !== "set-cookie" && !finalResponse.headers.has(name)) {
|
|
580
|
-
finalResponse.headers.set(name, value);
|
|
581
|
-
}
|
|
582
|
-
});
|
|
556
|
+
mergeReqCtxStub(finalResponse.headers, reqCtx);
|
|
583
557
|
}
|
|
584
558
|
|
|
585
559
|
return finalResponse;
|
|
@@ -688,13 +662,7 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
688
662
|
// Only fill in missing headers — the returned Response's explicit
|
|
689
663
|
// headers take precedence, matching executeMiddleware behavior.
|
|
690
664
|
const mergedHeaders = new Headers(response.headers);
|
|
691
|
-
stubResponse.headers
|
|
692
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
693
|
-
mergedHeaders.append(name, value);
|
|
694
|
-
} else if (!mergedHeaders.has(name)) {
|
|
695
|
-
mergedHeaders.set(name, value);
|
|
696
|
-
}
|
|
697
|
-
});
|
|
665
|
+
mergeStubHeaders(mergedHeaders, stubResponse.headers, false);
|
|
698
666
|
return new Response(response.body, {
|
|
699
667
|
status: response.status,
|
|
700
668
|
statusText: response.statusText,
|
|
@@ -126,7 +126,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
126
126
|
get env() {
|
|
127
127
|
if (buildEnv !== undefined) return buildEnv;
|
|
128
128
|
throw new Error(
|
|
129
|
-
"[
|
|
129
|
+
"[rango] ctx.env is not available during dev-mode getParams(). " +
|
|
130
130
|
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
131
131
|
);
|
|
132
132
|
},
|
|
@@ -67,9 +67,11 @@ export async function previewMatch<TEnv = any>(
|
|
|
67
67
|
responseType: negotiation.responseType,
|
|
68
68
|
handler: negotiation.handler,
|
|
69
69
|
params: matched.params,
|
|
70
|
-
negotiated: true,
|
|
71
70
|
manifestEntry: negotiation.manifestEntry,
|
|
72
71
|
routeKey: matched.routeKey,
|
|
72
|
+
// omitted unless a variant negotiated, preserving the prior public
|
|
73
|
+
// shape (absent for plain response routes, not negotiated:false)
|
|
74
|
+
...(negotiation.negotiated ? { negotiated: true } : {}),
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -278,33 +278,9 @@ async function classifyResponseRoute<TEnv>(
|
|
|
278
278
|
pathname: string,
|
|
279
279
|
snapshot: RouteSnapshot<TEnv>,
|
|
280
280
|
): Promise<ResponseRoutePlan<TEnv> | null> {
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
// negotiateRoute returns the response plan (variant or plain) or null for RSC.
|
|
283
282
|
const negotiation = await negotiateRoute(request, pathname, snapshot);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
route: snapshot,
|
|
288
|
-
...negotiation,
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Non-negotiated response route (no variants, or RSC won negotiation)
|
|
293
|
-
if (responseType) {
|
|
294
|
-
const handler =
|
|
295
|
-
manifestEntry.type === "route" ? manifestEntry.handler : undefined;
|
|
296
|
-
if (handler) {
|
|
297
|
-
return {
|
|
298
|
-
mode: "response",
|
|
299
|
-
route: snapshot,
|
|
300
|
-
handler,
|
|
301
|
-
responseType,
|
|
302
|
-
negotiated: false,
|
|
303
|
-
manifestEntry,
|
|
304
|
-
routeMiddleware: snapshot.routeMiddleware,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return null;
|
|
283
|
+
return negotiation
|
|
284
|
+
? { mode: "response", route: snapshot, ...negotiation }
|
|
285
|
+
: null;
|
|
310
286
|
}
|