@rangojs/router 0.0.0-experimental.143 → 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 +23 -6
- package/package.json +2 -2
- package/skills/cache-guide/SKILL.md +3 -1
- package/skills/caching/SKILL.md +23 -2
- package/skills/catalog.json +6 -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/parallel/SKILL.md +2 -0
- package/skills/ppr/SKILL.md +34 -10
- package/skills/rango/SKILL.md +10 -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/redirect-origin.ts +14 -0
- package/src/route-map-builder.ts +17 -3
- package/src/router/lazy-includes.ts +8 -2
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/match-handlers.ts +11 -6
- package/src/router/middleware.ts +4 -1
- package/src/router/telemetry.ts +9 -1
- package/src/router.ts +7 -8
- package/src/rsc/handler.ts +9 -2
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +35 -2
- package/src/rsc/shell-capture.ts +93 -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 +7 -0
- package/src/vite/inject-client-debug.ts +64 -12
- package/src/vite/router-discovery.ts +9 -1
package/src/ssr/index.tsx
CHANGED
|
@@ -247,11 +247,22 @@ async function readStreamToUint8Array(
|
|
|
247
247
|
const reader = stream.getReader();
|
|
248
248
|
const chunks: Uint8Array[] = [];
|
|
249
249
|
let total = 0;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
250
|
+
try {
|
|
251
|
+
while (true) {
|
|
252
|
+
const { done, value } = await reader.read();
|
|
253
|
+
if (done) break;
|
|
254
|
+
chunks.push(value);
|
|
255
|
+
total += value.length;
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
// Mid-read abort path (documented): the prelude stream errors with our
|
|
259
|
+
// abort reason while we are still reading it. Cancel the source before
|
|
260
|
+
// rethrowing so it is not left uncancelled; releaseLock always runs in
|
|
261
|
+
// finally. Mirrors src/rsc/rsc-rendering.ts's serve-side reader cleanup.
|
|
262
|
+
reader.cancel(error).catch(() => {});
|
|
263
|
+
throw error;
|
|
264
|
+
} finally {
|
|
265
|
+
reader.releaseLock();
|
|
255
266
|
}
|
|
256
267
|
const out = new Uint8Array(total);
|
|
257
268
|
let offset = 0;
|
|
@@ -434,113 +445,140 @@ export function createShellCaptureHandler<TEnv = unknown>(
|
|
|
434
445
|
): Promise<ShellCaptureResult | null> {
|
|
435
446
|
const maxWaitMs = opts.maxWaitMs ?? DEFAULT_SHELL_CAPTURE_MAX_WAIT_MS;
|
|
436
447
|
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
// Start prerender first, then run the abort schedule concurrently. When
|
|
446
|
-
// holes are pending, prerender's promise settles only after abort(); when
|
|
447
|
-
// the shell completes with no holes it settles on its own and the later
|
|
448
|
-
// abort() is a harmless no-op (the DATA variant).
|
|
449
|
-
const controller = new AbortController();
|
|
450
|
-
const prerenderPromise = prerender(<SsrRoot />, {
|
|
451
|
-
signal: controller.signal,
|
|
452
|
-
bootstrapScriptContent,
|
|
453
|
-
// Abort is how capture WORKS: once the shell is quiet we abort() to freeze
|
|
454
|
-
// the prelude and let the still-pending holes postpone. React reports the
|
|
455
|
-
// abort reason for each pending boundary through onError. Without an onError
|
|
456
|
-
// here React falls back to console.error, so every capture that still has a
|
|
457
|
-
// live hole at abort time (the normal case, and every cold-module capture
|
|
458
|
-
// where the shell is not yet done) dumps a DOMException [AbortError] stack —
|
|
459
|
-
// once per pending boundary. That is EXPECTED degradation, not a failure, so
|
|
460
|
-
// swallow the abort here. Genuine shell render errors (a component throwing)
|
|
461
|
-
// are NOT the abort and still surface through the deps.onError channel, the
|
|
462
|
-
// same one renderHTML uses. See docs/design/ppr-shell-resume.md.
|
|
463
|
-
onError: (error: unknown) => {
|
|
464
|
-
if (
|
|
465
|
-
controller.signal.aborted &&
|
|
466
|
-
(error as { name?: string } | null)?.name === "AbortError"
|
|
467
|
-
) {
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
reportRenderError(onError, error);
|
|
471
|
-
},
|
|
472
|
-
});
|
|
473
|
-
// Pre-attach a no-op catch: the real await sits AFTER quiesce + the
|
|
474
|
-
// post-quiesce hops, so an early prerender rejection (e.g. a bake-lane
|
|
475
|
-
// loader tripping the identity guard within milliseconds) would otherwise
|
|
476
|
-
// spend several turns handler-less and crash the worker as an unhandled
|
|
477
|
-
// rejection. The actual rejection handling still happens at the await
|
|
478
|
-
// below; this parallel handler only keeps the gap crash-free.
|
|
479
|
-
prerenderPromise.catch(() => {});
|
|
480
|
-
|
|
481
|
-
// Wait for the caller's quiesce signal. By the time it resolves the Flight
|
|
482
|
-
// input is byte-quiet and FROZEN by the capture gate (shell-capture.ts
|
|
483
|
-
// gateFlightForCapture), so there is no wall-clock debounce here — maxWaitMs
|
|
484
|
-
// is only the pathological guard for a shell that never goes quiet (a root
|
|
485
|
-
// postpone / hung handle), and should never fire in tests.
|
|
486
|
-
const timer = createCancelableTimeout(maxWaitMs);
|
|
487
|
-
try {
|
|
488
|
-
await Promise.race([opts.quiesce, timer.promise]);
|
|
489
|
-
} finally {
|
|
490
|
-
timer.cancel();
|
|
491
|
-
}
|
|
492
|
-
// Fixed task hops before the abort: give React's fizz worker turns to flush
|
|
493
|
-
// the now-complete shell and mark the still-pending boundaries as POSTPONED
|
|
494
|
-
// rather than errored. Deterministic (the byte set is already frozen), so a
|
|
495
|
-
// fixed count of turns suffices — no wall-clock.
|
|
496
|
-
for (let i = 0; i < POST_QUIESCE_TASK_HOPS; i++) {
|
|
497
|
-
await macrotask();
|
|
498
|
-
}
|
|
499
|
-
controller.abort();
|
|
500
|
-
|
|
501
|
-
// A hard prerender rejection (fatal shell error) propagates. Expected
|
|
502
|
-
// degradation surfaces three ways and all return null: a trivial prelude
|
|
503
|
-
// (sanity gate below), the prerender REJECTING with an AbortError, or the
|
|
504
|
-
// prelude STREAM erroring with the abort reason mid-read — both abort
|
|
505
|
-
// shapes happen when our own abort lands before the shell completed (seen
|
|
506
|
-
// on dev cold paths, where module transform / first-render latency
|
|
507
|
-
// outlasts flight quiesce; a later request re-captures against warm
|
|
508
|
-
// modules and succeeds).
|
|
509
|
-
let prelude: Uint8Array;
|
|
510
|
-
let postponed: unknown;
|
|
448
|
+
// Arm the maxWaitMs deadline BEFORE the first await so it bounds the ENTIRE
|
|
449
|
+
// capture, the bootstrap-script load included. loadBootstrapScriptContent()
|
|
450
|
+
// used to run before the timer, so a hung/slow bootstrap load hung
|
|
451
|
+
// captureShellHTML with no upper bound and held the background capture task
|
|
452
|
+
// open. One deadline, shared by the bootstrap race below and the quiesce
|
|
453
|
+
// race, keeps the whole path "bounded by maxWaitMs like every quiesce input".
|
|
454
|
+
const deadline = createCancelableTimeout(maxWaitMs);
|
|
511
455
|
try {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
// the
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
456
|
+
// No nonce (nonce'd requests never reach capture); no formState.
|
|
457
|
+
const SsrRoot = createSsrRootComponent({
|
|
458
|
+
createFromReadableStream,
|
|
459
|
+
rscStream,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Bootstrap load raced against the deadline. A load that never resolves
|
|
463
|
+
// within maxWaitMs is the same bounded no-shell degrade as a shell that
|
|
464
|
+
// never goes quiet: return null, do not hang. A load that REJECTS is a
|
|
465
|
+
// genuine error and still propagates (it is not the deadline). `null` is
|
|
466
|
+
// the deadline sentinel — disjoint from the load's `Promise<string>`, so
|
|
467
|
+
// the race narrows to `string | null` with no wrapper. The no-op catch
|
|
468
|
+
// keeps a late rejection off the unhandledRejection path when the deadline
|
|
469
|
+
// already won; a rejection that lands first still propagates out.
|
|
470
|
+
const load = loadBootstrapScriptContent();
|
|
471
|
+
load.catch(() => {});
|
|
472
|
+
const bootstrapScriptContent = await Promise.race([
|
|
473
|
+
load,
|
|
474
|
+
deadline.promise.then(() => null),
|
|
475
|
+
]);
|
|
476
|
+
if (bootstrapScriptContent === null) {
|
|
523
477
|
return null;
|
|
524
478
|
}
|
|
525
|
-
throw error;
|
|
526
|
-
}
|
|
527
479
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
480
|
+
// Start prerender first, then run the abort schedule concurrently. When
|
|
481
|
+
// holes are pending, prerender's promise settles only after abort(); when
|
|
482
|
+
// the shell completes with no holes it settles on its own and the later
|
|
483
|
+
// abort() is a harmless no-op (the DATA variant).
|
|
484
|
+
const controller = new AbortController();
|
|
485
|
+
// Private reason object: the deliberate abort is identified by object
|
|
486
|
+
// IDENTITY in both the onError below and the post-await catch. React
|
|
487
|
+
// propagates this EXACT object to onError for every still-pending boundary
|
|
488
|
+
// (verified identity-preserving), and rejects/errors the prelude with it.
|
|
489
|
+
const abortReason = { rangoShellCaptureAbort: true };
|
|
490
|
+
const prerenderPromise = prerender(<SsrRoot />, {
|
|
491
|
+
signal: controller.signal,
|
|
492
|
+
bootstrapScriptContent,
|
|
493
|
+
// Abort is how capture WORKS: once the shell is quiet we abort() to
|
|
494
|
+
// freeze the prelude and let the still-pending holes postpone. React
|
|
495
|
+
// reports the abort reason for each pending boundary through onError.
|
|
496
|
+
// Without an onError here React falls back to console.error, so every
|
|
497
|
+
// capture that still has a live hole at abort time (the normal case)
|
|
498
|
+
// dumps a stack once per pending boundary. That is EXPECTED degradation,
|
|
499
|
+
// so swallow OUR abort — matched by IDENTITY (error === abortReason).
|
|
500
|
+
// Discriminate by identity, NOT error.name: capture aborts before
|
|
501
|
+
// awaiting, so signal.aborted is unconditionally true and a name check
|
|
502
|
+
// swallowed genuine AbortError-named throws (a component's own
|
|
503
|
+
// fetch/AbortController cancellation) as if they were our abort. Genuine
|
|
504
|
+
// render errors are NOT our sentinel and still surface through
|
|
505
|
+
// deps.onError, the same channel renderHTML uses. See
|
|
506
|
+
// docs/design/ppr-shell-resume.md.
|
|
507
|
+
onError: (error: unknown) => {
|
|
508
|
+
if (error === abortReason) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
reportRenderError(onError, error);
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
// Pre-attach a no-op catch: the real await sits AFTER quiesce + the
|
|
515
|
+
// post-quiesce hops, so an early prerender rejection (e.g. a bake-lane
|
|
516
|
+
// loader tripping the identity guard within milliseconds) would otherwise
|
|
517
|
+
// spend several turns handler-less and crash the worker as an unhandled
|
|
518
|
+
// rejection. The actual rejection handling still happens at the await
|
|
519
|
+
// below; this parallel handler only keeps the gap crash-free.
|
|
520
|
+
prerenderPromise.catch(() => {});
|
|
521
|
+
|
|
522
|
+
// Wait for the caller's quiesce signal, bounded by the SAME deadline. By
|
|
523
|
+
// the time it resolves the Flight input is byte-quiet and FROZEN by the
|
|
524
|
+
// capture gate (shell-capture.ts gateFlightForCapture), so there is no
|
|
525
|
+
// wall-clock debounce here — maxWaitMs is only the pathological guard for a
|
|
526
|
+
// shell that never goes quiet (a root postpone / hung handle).
|
|
527
|
+
await Promise.race([opts.quiesce, deadline.promise]);
|
|
528
|
+
// Fixed task hops before the abort: give React's fizz worker turns to flush
|
|
529
|
+
// the now-complete shell and mark the still-pending boundaries as POSTPONED
|
|
530
|
+
// rather than errored. Deterministic (the byte set is already frozen), so a
|
|
531
|
+
// fixed count of turns suffices — no wall-clock.
|
|
532
|
+
for (let i = 0; i < POST_QUIESCE_TASK_HOPS; i++) {
|
|
533
|
+
await macrotask();
|
|
534
|
+
}
|
|
535
|
+
controller.abort(abortReason);
|
|
536
|
+
|
|
537
|
+
// A hard prerender rejection (fatal shell error) propagates. Expected
|
|
538
|
+
// degradation surfaces three ways and all return null: a trivial prelude
|
|
539
|
+
// (sanity gate below), the prerender REJECTING with our abort reason, or
|
|
540
|
+
// the prelude STREAM erroring with our abort reason mid-read — both abort
|
|
541
|
+
// shapes happen when our own abort lands before the shell completed (seen
|
|
542
|
+
// on dev cold paths, where module transform / first-render latency
|
|
543
|
+
// outlasts flight quiesce; a later request re-captures against warm
|
|
544
|
+
// modules and succeeds).
|
|
545
|
+
let prelude: Uint8Array;
|
|
546
|
+
let postponed: unknown;
|
|
547
|
+
try {
|
|
548
|
+
const result = await prerenderPromise;
|
|
549
|
+
prelude = await readStreamToUint8Array(result.prelude);
|
|
550
|
+
postponed = result.postponed;
|
|
551
|
+
} catch (error) {
|
|
552
|
+
// Identity match: swallow ONLY our own deliberate abort
|
|
553
|
+
// (error === abortReason). Not error.name — capture aborts before this
|
|
554
|
+
// await, so signal.aborted is always true, and a name check let a
|
|
555
|
+
// genuine AbortError-named throw masquerade as our abort and degrade
|
|
556
|
+
// into a retryable no-shell, hiding real failures from reportCacheError.
|
|
557
|
+
if (error === abortReason) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Sanity gate: a prelude with no `<body` is the no-shell failure mode.
|
|
564
|
+
// Return null and store nothing; the request falls back to axis 1 and a
|
|
565
|
+
// later request re-captures. The dominant real-world cause is a loader
|
|
566
|
+
// route WITHOUT a route-level loading() boundary: renderSegments' loading-
|
|
567
|
+
// less branch awaits loader data at TREE-BUILD, so the masked loader pins
|
|
568
|
+
// the whole tree above <body> (root postpone). Root-postponing layouts and
|
|
569
|
+
// hung handles degrade the same way. shell-capture.ts logs a once-per-key
|
|
570
|
+
// warning so the eternal-MISS shape is diagnosable.
|
|
571
|
+
if (!new TextDecoder().decode(prelude).includes("<body")) {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
539
574
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
575
|
+
return {
|
|
576
|
+
prelude,
|
|
577
|
+
postponed: postponed == null ? null : JSON.stringify(postponed),
|
|
578
|
+
};
|
|
579
|
+
} finally {
|
|
580
|
+
deadline.cancel();
|
|
581
|
+
}
|
|
544
582
|
};
|
|
545
583
|
}
|
|
546
584
|
|
package/src/testing/dispatch.ts
CHANGED
|
@@ -757,6 +757,10 @@ export async function dispatch<TEnv = any>(
|
|
|
757
757
|
durationMs: performance.now() - telemetryStart,
|
|
758
758
|
segmentCount: 0,
|
|
759
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,
|
|
760
764
|
});
|
|
761
765
|
}
|
|
762
766
|
|
|
@@ -785,6 +789,9 @@ export async function dispatch<TEnv = any>(
|
|
|
785
789
|
durationMs: performance.now() - telemetryStart,
|
|
786
790
|
segmentCount: 0,
|
|
787
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,
|
|
788
795
|
});
|
|
789
796
|
} else {
|
|
790
797
|
safeEmit(resolveSink(sink), {
|
|
@@ -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;
|