@rangojs/router 0.0.0-experimental.142 → 0.0.0-experimental.144
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/dist/vite/index.js +25 -6
- package/package.json +4 -2
- package/skills/cache-guide/SKILL.md +3 -1
- package/skills/caching/SKILL.md +41 -2
- package/skills/catalog.json +6 -0
- package/skills/composability/SKILL.md +32 -0
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/loader/SKILL.md +5 -0
- package/skills/migrate-nextjs/SKILL.md +4 -2
- package/skills/observability/SKILL.md +8 -0
- package/skills/parallel/SKILL.md +4 -0
- package/skills/ppr/SKILL.md +110 -20
- package/skills/rango/SKILL.md +10 -0
- package/skills/route/SKILL.md +8 -0
- package/skills/typesafety/SKILL.md +1 -0
- package/skills/typesafety/generated-files-and-cli.md +30 -0
- package/skills/use-cache/SKILL.md +12 -2
- package/src/browser/partial-update.ts +7 -0
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +54 -20
- package/src/cache/document-cache.ts +17 -11
- package/src/cache/vercel/vercel-cache-store.ts +9 -19
- package/src/cloudflare/tracing.ts +7 -8
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -8
- package/src/redirect-origin.ts +14 -0
- package/src/route-definition/helpers-types.ts +5 -4
- package/src/route-map-builder.ts +41 -4
- package/src/router/find-match.ts +15 -1
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +8 -2
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/match-handlers.ts +175 -133
- package/src/router/middleware.ts +40 -30
- package/src/router/router-interfaces.ts +9 -0
- package/src/router/segment-resolution/loader-snapshot.ts +98 -17
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router.ts +22 -14
- package/src/rsc/handler.ts +55 -32
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +35 -2
- package/src/rsc/shell-capture.ts +98 -20
- package/src/server/context.ts +47 -9
- package/src/server/cookie-store.ts +26 -5
- package/src/server/request-context.ts +22 -0
- package/src/ssr/index.tsx +145 -107
- package/src/testing/dispatch.ts +149 -37
- package/src/urls/path-helper-types.ts +9 -4
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/inject-client-debug.ts +64 -12
- package/src/vite/router-discovery.ts +9 -1
package/src/testing/dispatch.ts
CHANGED
|
@@ -55,6 +55,14 @@
|
|
|
55
55
|
* metadata.category, while the request still degrades-to-miss exactly as before.
|
|
56
56
|
* (A thrown response-route HANDLER error is the one onError path NOT covered —
|
|
57
57
|
* see "DOES NOT support" below.)
|
|
58
|
+
* - createRouter({ telemetry }) match-transaction lifecycle: request.start opens
|
|
59
|
+
* the transaction before the global middleware chain, request.end closes it
|
|
60
|
+
* after finalizeResponse (segmentCount 0 / cacheHit false — dispatch renders no
|
|
61
|
+
* RSC segments and holds no match-cache state), and a thrown non-Response error
|
|
62
|
+
* emits request.error with phase "routing". All three carry the same requestId
|
|
63
|
+
* (getRequestId). Emission is gated entirely on a configured sink; with none,
|
|
64
|
+
* dispatch does zero new work and stays byte-identical for existing callers.
|
|
65
|
+
* Lets a consumer unit-test their sink wiring in-process instead of only at e2e.
|
|
58
66
|
*
|
|
59
67
|
* What dispatch DOES NOT support (and why):
|
|
60
68
|
* - RSC component routes — rendering requires the Flight serializer + React
|
|
@@ -76,6 +84,13 @@
|
|
|
76
84
|
* merged cookies/headers all match production; only the Flight-embedded
|
|
77
85
|
* location-state entries are absent. Cover location-state restoration across a
|
|
78
86
|
* partial redirect with an e2e test.
|
|
87
|
+
* - Telemetry cache.decision / loader.* / handler.error events: these fire from
|
|
88
|
+
* the real match()/matchPartial() + RSC render + loader pipeline (match-
|
|
89
|
+
* handlers.ts, loader-resolution.ts, segment-resolution), none of which
|
|
90
|
+
* dispatch runs. Synthesizing them here would be faking (the events would not
|
|
91
|
+
* reflect a real cache lookup or loader run), so dispatch emits only the
|
|
92
|
+
* request.start/end/error lifecycle above; cover cache/loader telemetry with an
|
|
93
|
+
* e2e test driving a real RSC request.
|
|
79
94
|
*
|
|
80
95
|
* dispatch reuses router.previewMatch(), which itself runs content negotiation
|
|
81
96
|
* and resolves route middleware from the matched entry tree, so dispatch's
|
|
@@ -127,6 +142,8 @@ import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
|
127
142
|
import { invokeOnError } from "../router/error-handling.js";
|
|
128
143
|
import type { OnErrorCallback } from "../types/error-types.js";
|
|
129
144
|
import type { Rango } from "../router/router-interfaces.js";
|
|
145
|
+
import { getRequestId, resolveSink, safeEmit } from "../router/telemetry.js";
|
|
146
|
+
import type { TelemetrySink } from "../router/telemetry.js";
|
|
130
147
|
|
|
131
148
|
/**
|
|
132
149
|
* The internal subset of the router surface dispatch depends on. The public
|
|
@@ -140,6 +157,13 @@ interface DispatchableRouter<TEnv> {
|
|
|
140
157
|
routeMap: Record<string, unknown>;
|
|
141
158
|
middleware: MiddlewareEntry<TEnv>[];
|
|
142
159
|
onError?: OnErrorCallback<TEnv>;
|
|
160
|
+
/**
|
|
161
|
+
* Optional telemetry sink from createRouter({ telemetry }) (RangoInternal
|
|
162
|
+
* field). dispatch emits the match-transaction lifecycle events onto it so a
|
|
163
|
+
* consumer can unit-test their sink wiring in-process; undefined keeps dispatch
|
|
164
|
+
* byte-identical for every existing caller.
|
|
165
|
+
*/
|
|
166
|
+
telemetry?: TelemetrySink;
|
|
143
167
|
findMatch(pathname: string): Promise<{
|
|
144
168
|
redirectTo?: string;
|
|
145
169
|
routeKey?: string;
|
|
@@ -444,6 +468,19 @@ export async function dispatch<TEnv = any>(
|
|
|
444
468
|
const isPartial = url.searchParams.has("_rsc_partial");
|
|
445
469
|
const isAction = url.searchParams.has("_rsc_action");
|
|
446
470
|
|
|
471
|
+
// Telemetry: mirror the router's match-transaction lifecycle onto the
|
|
472
|
+
// configured sink so a consumer can unit-test createRouter({ telemetry })
|
|
473
|
+
// wiring in-process (the dogfood gap the RSC-free dispatch left — see
|
|
474
|
+
// tests/cloudflare-basic/test/cache-status.test.ts). Every emit is gated on
|
|
475
|
+
// `sink` truthiness: with no sink configured dispatch does zero new work and
|
|
476
|
+
// stays byte-identical for existing callers. Only request.start/end/error are
|
|
477
|
+
// reachable here — cache.decision and loader.* originate in the real match()/
|
|
478
|
+
// matchPartial() + RSC render pipeline dispatch deliberately does not run
|
|
479
|
+
// (module header), so fabricating them would violate the no-fake rule.
|
|
480
|
+
const sink = router.telemetry;
|
|
481
|
+
const telemetryRequestId = sink ? getRequestId(req) : undefined;
|
|
482
|
+
const telemetryStart = sink ? performance.now() : 0;
|
|
483
|
+
|
|
447
484
|
return runWithRequestContext(requestContext, async () => {
|
|
448
485
|
// Set params before middleware/handler run, so global middleware sees
|
|
449
486
|
// ctx.params (production sets them during matching, before middleware).
|
|
@@ -658,44 +695,119 @@ export async function dispatch<TEnv = any>(
|
|
|
658
695
|
return callResponseRoute();
|
|
659
696
|
};
|
|
660
697
|
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
:
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
);
|
|
675
|
-
|
|
676
|
-
// Match production's global-chain exit (handler.ts): on a partial/action
|
|
677
|
-
// request a middleware 3xx redirect is converted to a Flight-safe response
|
|
678
|
-
// so fetch() does not auto-follow it; every path then drains onResponse
|
|
679
|
-
// callbacks via finalizeResponse. dispatch is RSC-free, so the
|
|
680
|
-
// createRedirectFlightResponse stand-in falls back to the no-state
|
|
681
|
-
// 204 + X-RSC-Redirect (see the location-state divergence in the header).
|
|
682
|
-
let finalResponse: Response;
|
|
683
|
-
if (isPartial || isAction) {
|
|
684
|
-
const intercepted = interceptRedirectForPartial(
|
|
685
|
-
mwResponse,
|
|
686
|
-
(redirectUrl) => createSimpleRedirectResponse(redirectUrl),
|
|
687
|
-
);
|
|
688
|
-
finalResponse = finalizeResponse(intercepted ?? mwResponse);
|
|
689
|
-
} else {
|
|
690
|
-
finalResponse = finalizeResponse(mwResponse);
|
|
698
|
+
// request.start opens the match transaction, mirroring match-handlers.ts.
|
|
699
|
+
// transaction is always "match" (dispatch has no matchPartial split);
|
|
700
|
+
// isPartial carries the ?_rsc_partial signal the same way production does.
|
|
701
|
+
if (sink) {
|
|
702
|
+
safeEmit(resolveSink(sink), {
|
|
703
|
+
type: "request.start",
|
|
704
|
+
timestamp: telemetryStart,
|
|
705
|
+
requestId: telemetryRequestId,
|
|
706
|
+
method: req.method,
|
|
707
|
+
pathname: url.pathname,
|
|
708
|
+
transaction: "match",
|
|
709
|
+
isPartial,
|
|
710
|
+
});
|
|
691
711
|
}
|
|
692
712
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
713
|
+
try {
|
|
714
|
+
// Global (pattern-matched) middleware wraps coreHandler, exactly as
|
|
715
|
+
// production wraps coreHandler with executeMiddleware (handler.ts).
|
|
716
|
+
const globalMatches = matchMiddleware(url.pathname, router.middleware);
|
|
717
|
+
const mwResponse =
|
|
718
|
+
globalMatches.length === 0
|
|
719
|
+
? await coreHandler()
|
|
720
|
+
: await executeMiddleware<TEnv>(
|
|
721
|
+
globalMatches,
|
|
722
|
+
req,
|
|
723
|
+
env,
|
|
724
|
+
variables,
|
|
725
|
+
coreHandler,
|
|
726
|
+
reverse,
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
// Match production's global-chain exit (handler.ts): on a partial/action
|
|
730
|
+
// request a middleware 3xx redirect is converted to a Flight-safe response
|
|
731
|
+
// so fetch() does not auto-follow it; every path then drains onResponse
|
|
732
|
+
// callbacks via finalizeResponse. dispatch is RSC-free, so the
|
|
733
|
+
// createRedirectFlightResponse stand-in falls back to the no-state
|
|
734
|
+
// 204 + X-RSC-Redirect (see the location-state divergence in the header).
|
|
735
|
+
let finalResponse: Response;
|
|
736
|
+
if (isPartial || isAction) {
|
|
737
|
+
const intercepted = interceptRedirectForPartial(
|
|
738
|
+
mwResponse,
|
|
739
|
+
(redirectUrl) => createSimpleRedirectResponse(redirectUrl),
|
|
740
|
+
);
|
|
741
|
+
finalResponse = finalizeResponse(intercepted ?? mwResponse);
|
|
742
|
+
} else {
|
|
743
|
+
finalResponse = finalizeResponse(mwResponse);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// request.end closes the transaction. dispatch produces no RSC segments and
|
|
747
|
+
// holds no match-cache state, so segmentCount/cacheHit are 0/false — the
|
|
748
|
+
// same shape production emits for its own redirect (segment-less) result.
|
|
749
|
+
if (sink) {
|
|
750
|
+
safeEmit(resolveSink(sink), {
|
|
751
|
+
type: "request.end",
|
|
752
|
+
timestamp: performance.now(),
|
|
753
|
+
requestId: telemetryRequestId,
|
|
754
|
+
method: req.method,
|
|
755
|
+
pathname: url.pathname,
|
|
756
|
+
transaction: "match",
|
|
757
|
+
durationMs: performance.now() - telemetryStart,
|
|
758
|
+
segmentCount: 0,
|
|
759
|
+
cacheHit: false,
|
|
760
|
+
// dispatch's final response IS built before request.end (unlike
|
|
761
|
+
// match()/matchPartial(), whose Response is built after), so stamp its
|
|
762
|
+
// status — the same field a thrown-Response short-circuit carries.
|
|
763
|
+
status: finalResponse.status,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Mirror production's single open-redirect chokepoint (handler.ts): every
|
|
768
|
+
// browser-followed (3xx + Location) redirect is same-origin guarded before
|
|
769
|
+
// it leaves -- a cross-origin Location is rewritten to the basename root
|
|
770
|
+
// unless redirect(url, { external: true }) opted out. Soft partial/action
|
|
771
|
+
// redirects are 204 + X-RSC-Redirect and pass through untouched (the client
|
|
772
|
+
// validates them), so this is a no-op for them.
|
|
773
|
+
return guardOutgoingRedirect(finalResponse, url.origin, router.basename);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
if (sink) {
|
|
776
|
+
if (error instanceof Response) {
|
|
777
|
+
// executeMiddleware absorbs a middleware-thrown Response and returns it
|
|
778
|
+
// (middleware.ts:566), so a thrown Response never actually reaches this
|
|
779
|
+
// level in dispatch. Defensive: if one ever does it is a completed
|
|
780
|
+
// request from the consumer's seat (a short-circuit redirect), so mirror
|
|
781
|
+
// plan 002 / match-handlers.ts and emit request.end, not request.error.
|
|
782
|
+
safeEmit(resolveSink(sink), {
|
|
783
|
+
type: "request.end",
|
|
784
|
+
timestamp: performance.now(),
|
|
785
|
+
requestId: telemetryRequestId,
|
|
786
|
+
method: req.method,
|
|
787
|
+
pathname: url.pathname,
|
|
788
|
+
transaction: "match",
|
|
789
|
+
durationMs: performance.now() - telemetryStart,
|
|
790
|
+
segmentCount: 0,
|
|
791
|
+
cacheHit: false,
|
|
792
|
+
// Carry the short-circuit Response's status (parity with
|
|
793
|
+
// match-handlers.ts's thrown-Response request.end).
|
|
794
|
+
status: error.status,
|
|
795
|
+
});
|
|
796
|
+
} else {
|
|
797
|
+
safeEmit(resolveSink(sink), {
|
|
798
|
+
type: "request.error",
|
|
799
|
+
timestamp: performance.now(),
|
|
800
|
+
requestId: telemetryRequestId,
|
|
801
|
+
method: req.method,
|
|
802
|
+
pathname: url.pathname,
|
|
803
|
+
transaction: "match",
|
|
804
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
805
|
+
phase: "routing",
|
|
806
|
+
durationMs: performance.now() - telemetryStart,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
700
812
|
});
|
|
701
813
|
}
|
|
@@ -268,9 +268,16 @@ export type PathHelpers<TEnv> = {
|
|
|
268
268
|
* `{ handler, use? }` whose `use` is scoped to that slot only. Per-slot
|
|
269
269
|
* merge order is `handler.use` → shared `use` → slot-local `use`, with
|
|
270
270
|
* narrowest scope winning for last-write-wins items like `loading()`.
|
|
271
|
+
*
|
|
272
|
+
* Not generic over the slots record: an inferred type parameter makes the
|
|
273
|
+
* object literal an inference site, which suppresses contextual typing of
|
|
274
|
+
* arrow slot handlers (`(ctx) => ...` was implicit any). Bare handlers infer
|
|
275
|
+
* now; a descriptor's `handler:` arrow still needs an explicit ctx annotation
|
|
276
|
+
* because StaticHandlerDefinition's own `.handler` joins the contextual union
|
|
277
|
+
* (two callables — see parallel-slot-handler-types.test.ts).
|
|
271
278
|
*/
|
|
272
|
-
parallel:
|
|
273
|
-
|
|
279
|
+
parallel: (
|
|
280
|
+
slots: Record<
|
|
274
281
|
`@${string}`,
|
|
275
282
|
| Handler<any, any, TEnv>
|
|
276
283
|
| ReactNode
|
|
@@ -283,8 +290,6 @@ export type PathHelpers<TEnv> = {
|
|
|
283
290
|
use?: () => ParallelUseItem[];
|
|
284
291
|
}
|
|
285
292
|
>,
|
|
286
|
-
>(
|
|
287
|
-
slots: TSlots,
|
|
288
293
|
use?: () => ParallelUseItem[],
|
|
289
294
|
) => ParallelItem;
|
|
290
295
|
|
package/src/vercel/tracing.ts
CHANGED
|
@@ -44,15 +44,15 @@ import {
|
|
|
44
44
|
} from "../router/telemetry-otel.js";
|
|
45
45
|
import type {
|
|
46
46
|
RouterTracingConfig,
|
|
47
|
-
|
|
47
|
+
TracingToggleOptions,
|
|
48
48
|
} from "../router/tracing.js";
|
|
49
49
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Options for createVercelTracing. Extends the shared
|
|
52
|
+
* {@link TracingToggleOptions} (`enabled` + per-phase `spans`) with the
|
|
53
|
+
* Vercel-specific tracer selectors.
|
|
54
|
+
*/
|
|
55
|
+
export interface VercelTracingOptions extends TracingToggleOptions {
|
|
56
56
|
/**
|
|
57
57
|
* OTel instrumentation-scope name passed to `trace.getTracer()`. Defaults to
|
|
58
58
|
* `"rango"`. Ignored when `tracer` is provided.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Connect } from "vite";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Bake the resolved INTERNAL_RANGO_DEBUG value into the router's `internal-debug`
|
|
3
5
|
* module so the flag reaches the CLIENT debug logs by just setting the env var.
|
|
@@ -10,27 +12,77 @@
|
|
|
10
12
|
* runs on the module regardless of how (or whether) the define is delivered, in
|
|
11
13
|
* both dev and build and for every environment, so the discovery plugin uses this
|
|
12
14
|
* to replace the module with the resolved literal.
|
|
13
|
-
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Scope to the router's own internal-debug module: the published package
|
|
19
|
+
* (`/@rangojs/router/`, incl. pnpm's nested layout) or the monorepo workspace
|
|
20
|
+
* (`/packages/rangojs-router/`). The package-anchored path avoids matching a
|
|
21
|
+
* consumer file that merely sits under a directory named `rangojs-router`.
|
|
22
|
+
* Accepts module ids and dev-server URLs (`/@fs/...internal-debug.ts?v=abc`).
|
|
23
|
+
*/
|
|
24
|
+
export function isRouterInternalDebugId(id: string): boolean {
|
|
25
|
+
if (!id.includes("internal-debug")) return false;
|
|
26
|
+
const norm = id.replace(/\\/g, "/");
|
|
27
|
+
return (
|
|
28
|
+
/\/internal-debug\.[cm]?[jt]sx?(\?|$)/.test(norm) &&
|
|
29
|
+
(norm.includes("/@rangojs/router/") ||
|
|
30
|
+
norm.includes("/packages/rangojs-router/"))
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Transform: replace the module with the resolved literal.
|
|
14
36
|
* Returns null for any module that is not the router's internal-debug module.
|
|
15
37
|
*/
|
|
16
38
|
export function injectClientDebugFlag(
|
|
17
39
|
id: string,
|
|
18
40
|
): { code: string; map: null } | null {
|
|
19
41
|
// Cheap early-out: this hook runs on every module in every environment.
|
|
20
|
-
if (!id
|
|
21
|
-
const norm = id.replace(/\\/g, "/");
|
|
22
|
-
// Scope to the router's own internal-debug module: the published package
|
|
23
|
-
// (`/@rangojs/router/`, incl. pnpm's nested layout) or the monorepo workspace
|
|
24
|
-
// (`/packages/rangojs-router/`). The package-anchored path avoids matching a
|
|
25
|
-
// consumer file that merely sits under a directory named `rangojs-router`.
|
|
26
|
-
const isInternalDebug =
|
|
27
|
-
/\/internal-debug\.[cm]?[jt]sx?(\?|$)/.test(norm) &&
|
|
28
|
-
(norm.includes("/@rangojs/router/") ||
|
|
29
|
-
norm.includes("/packages/rangojs-router/"));
|
|
30
|
-
if (!isInternalDebug) return null;
|
|
42
|
+
if (!isRouterInternalDebugId(id)) return null;
|
|
31
43
|
// Emit the whole module: internal-debug.ts has a single export, kept in sync.
|
|
32
44
|
return {
|
|
33
45
|
code: `export const INTERNAL_RANGO_DEBUG = ${!!process.env.INTERNAL_RANGO_DEBUG};\n`,
|
|
34
46
|
map: null,
|
|
35
47
|
};
|
|
36
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Dev middleware companion: serve the internal-debug module `no-cache` so the
|
|
52
|
+
* browser revalidates it (etag) instead of trusting an immutable cache entry.
|
|
53
|
+
*
|
|
54
|
+
* Why: the transform bakes the flag into module CONTENT, but a published
|
|
55
|
+
* consumer resolves the module into node_modules, where dev serves it as
|
|
56
|
+
* `internal-debug.ts?v=<hash>` with `Cache-Control: max-age=31536000,immutable`.
|
|
57
|
+
* That `?v=` hash does not vary with env vars (verified on Vite 8: getConfigHash
|
|
58
|
+
* hashes NODE_ENV, resolve, plugin names, optimizeDeps -- not arbitrary env
|
|
59
|
+
* state), so toggling the flag changed the content under an unchanged immutable
|
|
60
|
+
* URL: a browser that ever loaded the app with the flag off kept the
|
|
61
|
+
* baked-`false` module across dev-server restarts, and INTERNAL_RANGO_DEBUG
|
|
62
|
+
* never reached the FE logs while the server logs worked. The monorepo was
|
|
63
|
+
* immune -- workspace source is outside node_modules and served no-cache --
|
|
64
|
+
* which is why this bit only npm consumers.
|
|
65
|
+
*
|
|
66
|
+
* internal-debug.ts is the ONLY flag-varying module in the graph (its importers
|
|
67
|
+
* are byte-identical across flag states), so forcing revalidation for this one
|
|
68
|
+
* tiny module is sufficient and costs one conditional request per session. The
|
|
69
|
+
* middleware is unconditional (not gated on the flag) so an already-poisoned
|
|
70
|
+
* cache heals in both directions. Alternatives that do not work: a plugin
|
|
71
|
+
* `resolveId` appending a flag query never fires in Vite 8 dev for
|
|
72
|
+
* fs-resolvable relative imports, and the pre-#621 `define` no longer rotates
|
|
73
|
+
* the optimizer hash (define contents are not part of getConfigHash).
|
|
74
|
+
*/
|
|
75
|
+
export function internalDebugNoCacheMiddleware(): Connect.NextHandleFunction {
|
|
76
|
+
return function rangoInternalDebugNoCache(req, res, next) {
|
|
77
|
+
if (req.url && isRouterInternalDebugId(req.url)) {
|
|
78
|
+
const setHeader = res.setHeader.bind(res);
|
|
79
|
+
res.setHeader = (name, value) => {
|
|
80
|
+
return setHeader(
|
|
81
|
+
name,
|
|
82
|
+
name.toLowerCase() === "cache-control" ? "no-cache" : value,
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
next();
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -19,7 +19,10 @@ import {
|
|
|
19
19
|
createScanFilter,
|
|
20
20
|
} from "../build/generate-route-types.js";
|
|
21
21
|
import { firstCodeMatchIndex } from "../build/route-types/source-scan.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
injectClientDebugFlag,
|
|
24
|
+
internalDebugNoCacheMiddleware,
|
|
25
|
+
} from "./inject-client-debug.js";
|
|
23
26
|
import { createVersionPlugin } from "./plugins/version-plugin.js";
|
|
24
27
|
import { createVirtualStubPlugin } from "./plugins/virtual-stub-plugin.js";
|
|
25
28
|
import {
|
|
@@ -392,6 +395,11 @@ export function createRouterDiscoveryPlugin(
|
|
|
392
395
|
if ((globalThis as any).__rscRouterDiscoveryActive) return;
|
|
393
396
|
s.devServer = server;
|
|
394
397
|
|
|
398
|
+
// Serve the internal-debug module no-cache: consumers resolve it into
|
|
399
|
+
// node_modules, where dev's immutable `?v=` caching pinned browsers to a
|
|
400
|
+
// stale baked INTERNAL_RANGO_DEBUG. See internalDebugNoCacheMiddleware.
|
|
401
|
+
server.middlewares.use(internalDebugNoCacheMiddleware());
|
|
402
|
+
|
|
395
403
|
// Discovery promise that the handler can await if requests arrive
|
|
396
404
|
// before discovery completes
|
|
397
405
|
let resolveDiscovery: () => void;
|