@rangojs/router 0.32.0 → 0.33.1
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 +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10173
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-3HH6BP3K.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-JALTIWLT.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +15 -6
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +5 -2
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +2 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +5 -2
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +43 -7
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +66 -2
- package/dist/types/testing/serve-shell-request.d.ts +6 -3
- package/dist/types/types/handler-context.d.ts +7 -5
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +15 -4
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/vite/index.js +449 -189
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +81 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +18 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +29 -11
- package/skills/comparison/references/framework-comparison.md +13 -8
- package/skills/document-cache/SKILL.md +8 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/outlets.md +4 -0
- package/skills/hooks/state.md +24 -8
- package/skills/intercept/SKILL.md +33 -12
- package/skills/layout/SKILL.md +60 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +26 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +41 -8
- package/skills/testing/client-components.md +29 -13
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +70 -19
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +20 -9
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +8 -1
- package/src/browser/server-action-bridge.ts +16 -11
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +27 -0
- package/src/cache/document-cache.ts +5 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +21 -13
- package/src/router/match-result.ts +39 -87
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +53 -23
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +8 -3
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/request-stages.ts +59 -0
- package/src/rsc/rsc-rendering.ts +49 -33
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +23 -2
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +113 -28
- package/src/testing/dispatch.ts +3 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +269 -4
- package/src/testing/serve-shell-request.ts +6 -3
- package/src/types/handler-context.ts +7 -5
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +15 -4
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
|
@@ -17,6 +17,7 @@ import type { CacheErrorCategory } from "../cache/cache-error.js";
|
|
|
17
17
|
import type { CookieOptions } from "../router/middleware.js";
|
|
18
18
|
import {
|
|
19
19
|
KEEP_CACHE_HEADER,
|
|
20
|
+
bumpStateWrite,
|
|
20
21
|
getRawCookieValue,
|
|
21
22
|
freshReadsCookieName,
|
|
22
23
|
mintStateValue,
|
|
@@ -83,7 +84,10 @@ import {
|
|
|
83
84
|
readGuardedTheme,
|
|
84
85
|
} from "./cookie-store.js";
|
|
85
86
|
import { requestHeaders } from "./request-headers.js";
|
|
86
|
-
import {
|
|
87
|
+
import {
|
|
88
|
+
requestKind,
|
|
89
|
+
type RequestKind,
|
|
90
|
+
} from "../router/segment-resolution/prefetch-deferral.js";
|
|
87
91
|
import {
|
|
88
92
|
createReverseFunction,
|
|
89
93
|
stripInternalParams,
|
|
@@ -203,8 +207,12 @@ export interface RequestContext<
|
|
|
203
207
|
setStatus(status: number): void;
|
|
204
208
|
/** @internal Set status bypassing cache-exec guard (for framework error handling) */
|
|
205
209
|
_setStatus(status: number): void;
|
|
206
|
-
/**
|
|
207
|
-
|
|
210
|
+
/**
|
|
211
|
+
* @internal Rotate the rango state cookie: its timestamp (the server seat of
|
|
212
|
+
* invalidateClientCache), or with `"write"` only its write part (a server
|
|
213
|
+
* action without a directive, cookie-store.ts runActionBody).
|
|
214
|
+
*/
|
|
215
|
+
_rotateStateCookie(part?: "write"): void;
|
|
208
216
|
/** @internal Set the keepClientCache() directive header on the response. */
|
|
209
217
|
_setKeepCacheDirective(): void;
|
|
210
218
|
/**
|
|
@@ -663,11 +671,30 @@ export interface RequestContext<
|
|
|
663
671
|
_payloadDeferred?: boolean;
|
|
664
672
|
|
|
665
673
|
/**
|
|
666
|
-
* @internal
|
|
667
|
-
*
|
|
668
|
-
*
|
|
674
|
+
* @internal Set by the RSC handler when a partial response carries an
|
|
675
|
+
* `x-rsc-prefetch-scope` (router/source-reads.ts prefetchScopeOf): its diff
|
|
676
|
+
* is right for its source page, or a group of them, only. The document
|
|
677
|
+
* cache keys a partial without its source, so it does not store that
|
|
678
|
+
* response, whatever Cache-Control it ends up with.
|
|
679
|
+
*/
|
|
680
|
+
_payloadSourceScoped?: boolean;
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* @internal Set when the answer has nothing live in it, so nothing in it can
|
|
684
|
+
* fail after the 200's headers left: the document cache middleware on a
|
|
685
|
+
* fresh HIT, and the RSC handler when the partial match came from storage
|
|
686
|
+
* (MatchResult.fromStorage). finalizeRequestResponse (rsc/request-stages.ts)
|
|
687
|
+
* sends such a prefetch the router's prefetch Cache-Control.
|
|
688
|
+
*/
|
|
689
|
+
_servedFromStorage?: boolean;
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* @internal What the request says it is: a speculative `<Link>` prefetch,
|
|
693
|
+
* an intent prefetch (hover, `prefetch()`) or a fill (`prefetch: false`),
|
|
694
|
+
* read once at creation (segment-resolution/prefetch-deferral.ts
|
|
695
|
+
* requestKind).
|
|
669
696
|
*/
|
|
670
|
-
_requestKind?:
|
|
697
|
+
_requestKind?: RequestKind;
|
|
671
698
|
|
|
672
699
|
/**
|
|
673
700
|
* @internal Set when a render shows the visitor's URL of a rewritten
|
|
@@ -691,6 +718,11 @@ export interface RequestContext<
|
|
|
691
718
|
* `Cache-Control: private, no-store` and never stored by the document cache
|
|
692
719
|
* or as a PPR shell.
|
|
693
720
|
*
|
|
721
|
+
* Call it from middleware, a handler or an action. A loader's call
|
|
722
|
+
* (through `getRequestContext()`) is not supported: the payload takes the
|
|
723
|
+
* state before loaders settle, so the write may be dropped, and a PPR
|
|
724
|
+
* shell HIT never sends it.
|
|
725
|
+
*
|
|
694
726
|
* Multiple calls accumulate entries across definitions. For one definition
|
|
695
727
|
* the last write wins: two entries of the same definition share its slot
|
|
696
728
|
* in history.state, so a later call (or a later entry in one array)
|
|
@@ -715,6 +747,16 @@ export interface RequestContext<
|
|
|
715
747
|
*/
|
|
716
748
|
_documentLocationState?: boolean;
|
|
717
749
|
|
|
750
|
+
/**
|
|
751
|
+
* @internal The `document.cookie` line that re-creates the location state
|
|
752
|
+
* cookie this request consumed (server/document-location-state.ts). The
|
|
753
|
+
* consuming response clears the cookie in its headers; a shell HIT that
|
|
754
|
+
* degrades into a reload after those headers left writes this line first
|
|
755
|
+
* (rsc/shell-serve.ts shellReloadScript), so the reload reads the state
|
|
756
|
+
* again.
|
|
757
|
+
*/
|
|
758
|
+
_carriedLocationStateCookie?: string;
|
|
759
|
+
|
|
718
760
|
/**
|
|
719
761
|
* The matched route name, if the route has an explicit name.
|
|
720
762
|
* Undefined before route matching or for unnamed routes.
|
|
@@ -1021,10 +1063,13 @@ export type PublicRequestContext<
|
|
|
1021
1063
|
| "_documentCacheRender"
|
|
1022
1064
|
| "_payloadVisitorTheme"
|
|
1023
1065
|
| "_payloadDeferred"
|
|
1066
|
+
| "_payloadSourceScoped"
|
|
1067
|
+
| "_servedFromStorage"
|
|
1024
1068
|
| "_requestKind"
|
|
1025
1069
|
| "_payloadVisitorLocation"
|
|
1026
1070
|
| "_locationState"
|
|
1027
1071
|
| "_documentLocationState"
|
|
1072
|
+
| "_carriedLocationStateCookie"
|
|
1028
1073
|
| "_routeName"
|
|
1029
1074
|
| "_prevRouteKey"
|
|
1030
1075
|
| "_visitorMoved"
|
|
@@ -1377,7 +1422,12 @@ export function createRequestContext<TEnv>(
|
|
|
1377
1422
|
// of this request's headers goes through requestHeaders().
|
|
1378
1423
|
guardRequestHeaders(request);
|
|
1379
1424
|
const cookieHeader = requestHeaders(request).get("Cookie");
|
|
1380
|
-
|
|
1425
|
+
// What this response's state Set-Cookie changed: nothing yet, the write
|
|
1426
|
+
// part only, or the timestamp (which covers a write).
|
|
1427
|
+
let rangoStateRotated: "write" | "timestamp" | null = null;
|
|
1428
|
+
// The state Set-Cookie line written, replaced when a timestamp rotation
|
|
1429
|
+
// follows a write bump in the same request.
|
|
1430
|
+
let stateCookieLine: string | null = null;
|
|
1381
1431
|
const freshCookie = stateCookieName
|
|
1382
1432
|
? freshReadsCookieName(stateCookieName)
|
|
1383
1433
|
: undefined;
|
|
@@ -1457,6 +1507,17 @@ export function createRequestContext<TEnv>(
|
|
|
1457
1507
|
rawStubHeaders = stubResponse.headers;
|
|
1458
1508
|
shadowStubHeaders(stubResponse);
|
|
1459
1509
|
};
|
|
1510
|
+
// Remove one Set-Cookie line an internal writer appended earlier in this
|
|
1511
|
+
// request (Headers has no per-value delete): the state cookie when a later
|
|
1512
|
+
// write replaces or withdraws it, the fresh-reads cookie when a longer
|
|
1513
|
+
// Max-Age replaces it.
|
|
1514
|
+
const dropSetCookieLine = (line: string): void => {
|
|
1515
|
+
const kept = rawStubHeaders
|
|
1516
|
+
.getSetCookie()
|
|
1517
|
+
.filter((cookie) => cookie !== line);
|
|
1518
|
+
rawStubHeaders.delete("Set-Cookie");
|
|
1519
|
+
for (const cookie of kept) rawStubHeaders.append("Set-Cookie", cookie);
|
|
1520
|
+
};
|
|
1460
1521
|
|
|
1461
1522
|
const handleStore = createHandleStore();
|
|
1462
1523
|
const loaderPromises = new Map<string, Promise<any>>();
|
|
@@ -1632,16 +1693,29 @@ export function createRequestContext<TEnv>(
|
|
|
1632
1693
|
stubResponse.headers.set(name, value);
|
|
1633
1694
|
},
|
|
1634
1695
|
|
|
1635
|
-
// Rotate the rango state cookie for the responding client
|
|
1636
|
-
//
|
|
1637
|
-
// value {version}:{timestamp}; the `:` stays raw (the cookie-name.ts
|
|
1696
|
+
// Rotate the rango state cookie for the responding client. Writes ONE
|
|
1697
|
+
// Set-Cookie per request; the `:` stays raw (the cookie-name.ts
|
|
1638
1698
|
// serializer), not the URL-encoded form serializeCookieValue would produce.
|
|
1639
|
-
//
|
|
1640
|
-
//
|
|
1641
|
-
//
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1699
|
+
// - No part (the server seat of invalidateClientCache): a new timestamp,
|
|
1700
|
+
// {version}:{timestamp}. It is strictly greater than the client's current
|
|
1701
|
+
// one (inbound X-Rango-State), so a same-millisecond server rotation
|
|
1702
|
+
// still differs from the client value and the divergence observer fires.
|
|
1703
|
+
// - "write" (a server action without a directive, after its body ran): the
|
|
1704
|
+
// client's version and timestamp kept and the write part bumped,
|
|
1705
|
+
// {version}:{timestamp}:{write}, so the browser HTTP cache misses on
|
|
1706
|
+
// every response it holds from before the write while the client's
|
|
1707
|
+
// in-memory caches (keyed by {version}:{timestamp}) only go stale.
|
|
1708
|
+
// Skipped after keepClientCache() (and withdrawn by a later one, below),
|
|
1709
|
+
// and when the request carries no state.
|
|
1710
|
+
// A timestamp rotation covers a write bump: the later one of the two
|
|
1711
|
+
// replaces the bump's line, and a bump after a rotation adds nothing.
|
|
1712
|
+
_rotateStateCookie(part?: "write"): void {
|
|
1713
|
+
const kind = part === "write" ? "write" : "timestamp";
|
|
1714
|
+
if (rangoStateRotated === "timestamp" || rangoStateRotated === kind) {
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
if (kind === "write" && rawStubHeaders.has(KEEP_CACHE_HEADER)) return;
|
|
1718
|
+
rangoStateRotated = kind;
|
|
1645
1719
|
if (!stateCookieName) return;
|
|
1646
1720
|
// The client's current value, for the monotonic guard: prefer the
|
|
1647
1721
|
// X-Rango-State header (router navigation/prefetch fetches send it), but
|
|
@@ -1656,22 +1730,38 @@ export function createRequestContext<TEnv>(
|
|
|
1656
1730
|
const prevRaw =
|
|
1657
1731
|
(requestHeaders(request).get("x-rango-state") || null) ??
|
|
1658
1732
|
getRawCookieValue(cookieHeader, stateCookieName);
|
|
1659
|
-
const value =
|
|
1733
|
+
const value =
|
|
1734
|
+
kind === "write"
|
|
1735
|
+
? bumpStateWrite(prevRaw)
|
|
1736
|
+
: mintStateValue(versions?.document ?? "0", prevRaw);
|
|
1737
|
+
// No state on the request: nothing is cached under one to miss.
|
|
1738
|
+
if (value === null) return;
|
|
1660
1739
|
// rawStubHeaders: guard-exempt internal writer — invalidateClientCache()
|
|
1661
1740
|
// is documented callable from loaders and during shell capture.
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1741
|
+
if (stateCookieLine !== null) dropSetCookieLine(stateCookieLine);
|
|
1742
|
+
stateCookieLine = serializeStateCookie(
|
|
1743
|
+
stateCookieName,
|
|
1744
|
+
value,
|
|
1745
|
+
url.protocol === "https:",
|
|
1665
1746
|
);
|
|
1747
|
+
rawStubHeaders.append("Set-Cookie", stateCookieLine);
|
|
1666
1748
|
invalidateResponseCookieCache();
|
|
1667
1749
|
},
|
|
1668
1750
|
|
|
1669
1751
|
// Set the keepClientCache() directive header. The action bridge reads it on
|
|
1670
1752
|
// the response and suppresses its automatic invalidation. `.set` makes this
|
|
1671
1753
|
// idempotent (one header regardless of call count). rawStubHeaders:
|
|
1672
|
-
// guard-exempt internal writer.
|
|
1754
|
+
// guard-exempt internal writer. A write bump already written (the action
|
|
1755
|
+
// body ended, then global middleware after next() or a loader of the
|
|
1756
|
+
// revalidation called it) is withdrawn: keepClientCache() bumps nothing.
|
|
1673
1757
|
_setKeepCacheDirective(): void {
|
|
1674
1758
|
rawStubHeaders.set(KEEP_CACHE_HEADER, "1");
|
|
1759
|
+
if (rangoStateRotated === "write" && stateCookieLine !== null) {
|
|
1760
|
+
dropSetCookieLine(stateCookieLine);
|
|
1761
|
+
stateCookieLine = null;
|
|
1762
|
+
rangoStateRotated = null;
|
|
1763
|
+
invalidateResponseCookieCache();
|
|
1764
|
+
}
|
|
1675
1765
|
},
|
|
1676
1766
|
|
|
1677
1767
|
// The fresh-reads cookie (browser/cookie-name.ts): one Set-Cookie per
|
|
@@ -1704,12 +1794,7 @@ export function createRequestContext<TEnv>(
|
|
|
1704
1794
|
`${freshCookie}=1; Max-Age=${age}; Path=/; HttpOnly; SameSite=Lax` +
|
|
1705
1795
|
(url.protocol === "https:" ? "; Secure" : "");
|
|
1706
1796
|
if (freshCookieMaxAge > 0) {
|
|
1707
|
-
|
|
1708
|
-
const kept = rawStubHeaders
|
|
1709
|
-
.getSetCookie()
|
|
1710
|
-
.filter((cookie) => cookie !== prior);
|
|
1711
|
-
rawStubHeaders.delete("Set-Cookie");
|
|
1712
|
-
for (const cookie of kept) rawStubHeaders.append("Set-Cookie", cookie);
|
|
1797
|
+
dropSetCookieLine(serialized(freshCookieMaxAge));
|
|
1713
1798
|
}
|
|
1714
1799
|
freshCookieMaxAge = maxAge;
|
|
1715
1800
|
rawStubHeaders.append("Set-Cookie", serialized(maxAge));
|
package/src/testing/dispatch.ts
CHANGED
|
@@ -179,6 +179,8 @@ interface DispatchableRouter<TEnv> {
|
|
|
179
179
|
basename?: string;
|
|
180
180
|
/** The rango state cookie name (RangoInternal field). */
|
|
181
181
|
resolvedStateCookieName: string;
|
|
182
|
+
/** The in-memory prefetch TTL in ms (RangoInternal field). */
|
|
183
|
+
prefetchCacheTTL: number;
|
|
182
184
|
cache?:
|
|
183
185
|
| {
|
|
184
186
|
enabled?: boolean;
|
|
@@ -295,6 +297,7 @@ export async function dispatch<TEnv = any>(
|
|
|
295
297
|
timeouts: router.timeouts,
|
|
296
298
|
onTimeout: router.onTimeout,
|
|
297
299
|
resolvedStateCookieName: router.resolvedStateCookieName,
|
|
300
|
+
prefetchCacheTTL: router.prefetchCacheTTL,
|
|
298
301
|
},
|
|
299
302
|
callOnError: stages.createErrorReporter<TEnv>(router),
|
|
300
303
|
// A response-route handler's own throw is serialized into the same typed
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
runCli,
|
|
11
11
|
type RunCliHandle,
|
|
12
12
|
type SpawnOptions,
|
|
13
|
+
stopServer,
|
|
13
14
|
waitForReady,
|
|
14
15
|
warmupDevServer,
|
|
15
16
|
} from "./server.js";
|
|
@@ -93,10 +94,7 @@ export function createUseFixture(
|
|
|
93
94
|
// throw (findPort/waitForReady). Otherwise a beforeAll failure leaves
|
|
94
95
|
// afterAll's cleanup a no-op and orphans the dev server (plus workerd
|
|
95
96
|
// children) for the rest of the run.
|
|
96
|
-
cleanup =
|
|
97
|
-
proc!.kill();
|
|
98
|
-
await proc!.done;
|
|
99
|
-
};
|
|
97
|
+
cleanup = () => stopServer(proc!);
|
|
100
98
|
const port = await proc.findPort();
|
|
101
99
|
baseURL = `http://localhost:${port}`;
|
|
102
100
|
const readyUrl = options.readyPath
|
|
@@ -128,10 +126,7 @@ export function createUseFixture(
|
|
|
128
126
|
});
|
|
129
127
|
// The build is a finite process, but assign cleanup before the await
|
|
130
128
|
// so a hung build is still killed if beforeAll is torn down.
|
|
131
|
-
cleanup =
|
|
132
|
-
buildProc.kill();
|
|
133
|
-
await buildProc.done;
|
|
134
|
-
};
|
|
129
|
+
cleanup = () => stopServer(buildProc);
|
|
135
130
|
// Fail loudly on a nonzero build exit. Without this a failed build is
|
|
136
131
|
// swallowed and preview serves the previous stale dist/ (green
|
|
137
132
|
// production tests against old code), or times out with "Server not
|
|
@@ -155,10 +150,7 @@ export function createUseFixture(
|
|
|
155
150
|
});
|
|
156
151
|
// Switch cleanup to the preview process immediately after spawn, before
|
|
157
152
|
// findPort/waitForReady can throw and orphan it.
|
|
158
|
-
cleanup =
|
|
159
|
-
proc!.kill();
|
|
160
|
-
await proc!.done;
|
|
161
|
-
};
|
|
153
|
+
cleanup = () => stopServer(proc!);
|
|
162
154
|
const port = await proc.findPort();
|
|
163
155
|
baseURL = `http://localhost:${port}`;
|
|
164
156
|
const buildReadyUrl = options.readyPath
|
|
@@ -19,7 +19,13 @@ export interface RunCliHandle {
|
|
|
19
19
|
*/
|
|
20
20
|
exitCode: Promise<number | null>;
|
|
21
21
|
findPort: (timeoutMs?: number) => Promise<number>;
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Stop the process and what it spawned (the whole process group). SIGTERM by
|
|
24
|
+
* default; `stopServer` escalates to SIGKILL for a process that outlives the
|
|
25
|
+
* wait.
|
|
26
|
+
*/
|
|
27
|
+
kill: (signal?: NodeJS.Signals) => void;
|
|
28
|
+
readonly pid: number | undefined;
|
|
23
29
|
stdout: () => string;
|
|
24
30
|
stderr: () => string;
|
|
25
31
|
}
|
|
@@ -34,9 +40,16 @@ export function runCli(
|
|
|
34
40
|
// hits EOF immediately, so without CI=true the dev/preview server shuts itself
|
|
35
41
|
// down before it finishes starting. Real CI runners set CI=true; mirror that for
|
|
36
42
|
// locally-spawned servers so they stay alive for the duration of the tests.
|
|
43
|
+
// detached makes the child a process-group leader (pgid === pid), so kill()
|
|
44
|
+
// can signal the whole tree. `pnpm dev` spawns vite as a grandchild; without
|
|
45
|
+
// it process.kill(-pid) throws ESRCH, the fallback child.kill() stops only
|
|
46
|
+
// pnpm, and vite keeps running after the suite. Not on Windows: there
|
|
47
|
+
// detached opens a console window per server, and kill() already ends the
|
|
48
|
+
// tree with `taskkill /t`.
|
|
37
49
|
const child = x(name!, args, {
|
|
38
50
|
nodeOptions: {
|
|
39
51
|
...options,
|
|
52
|
+
detached: process.platform !== "win32",
|
|
40
53
|
env: { ...process.env, CI: "true", ...options.env },
|
|
41
54
|
},
|
|
42
55
|
}).process!;
|
|
@@ -100,16 +113,16 @@ export function runCli(
|
|
|
100
113
|
});
|
|
101
114
|
}
|
|
102
115
|
|
|
103
|
-
function kill() {
|
|
116
|
+
function kill(signal: NodeJS.Signals = "SIGTERM") {
|
|
104
117
|
if (process.platform === "win32") {
|
|
105
118
|
spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"]);
|
|
106
119
|
} else {
|
|
107
120
|
// Kill entire process group (Vite spawns child processes like workerd).
|
|
108
121
|
// Falls back to direct kill if process group kill fails.
|
|
109
122
|
try {
|
|
110
|
-
process.kill(-child.pid!,
|
|
123
|
+
process.kill(-child.pid!, signal);
|
|
111
124
|
} catch {
|
|
112
|
-
child.kill();
|
|
125
|
+
child.kill(signal);
|
|
113
126
|
}
|
|
114
127
|
}
|
|
115
128
|
}
|
|
@@ -120,11 +133,85 @@ export function runCli(
|
|
|
120
133
|
exitCode,
|
|
121
134
|
findPort,
|
|
122
135
|
kill,
|
|
136
|
+
get pid() {
|
|
137
|
+
return child.pid;
|
|
138
|
+
},
|
|
123
139
|
stdout: () => stdout,
|
|
124
140
|
stderr: () => stderr,
|
|
125
141
|
};
|
|
126
142
|
}
|
|
127
143
|
|
|
144
|
+
/** How long `stopServer` waits for SIGTERM to take effect before SIGKILL. */
|
|
145
|
+
const STOP_WAIT_MS = 10_000;
|
|
146
|
+
/** How long `stopServer` waits for the SIGKILL to take effect. */
|
|
147
|
+
const KILL_WAIT_MS = 2_000;
|
|
148
|
+
|
|
149
|
+
/** Resolves true when `done` settled within `ms`, false when the timer won. */
|
|
150
|
+
async function exitedWithin(done: Promise<unknown>, ms: number) {
|
|
151
|
+
let timer: NodeJS.Timeout | undefined;
|
|
152
|
+
const timedOut = new Promise<false>((resolve) => {
|
|
153
|
+
timer = setTimeout(() => resolve(false), ms);
|
|
154
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
return await Promise.race([done.then(() => true as const), timedOut]);
|
|
157
|
+
} finally {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** True while a process of the group led by `pgid` exists (not on Windows). */
|
|
163
|
+
function groupAlive(pgid: number): boolean {
|
|
164
|
+
if (process.platform === "win32") return false;
|
|
165
|
+
try {
|
|
166
|
+
process.kill(-pgid, 0);
|
|
167
|
+
return true;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
// EPERM: the group exists but belongs to someone else. Only ESRCH is gone.
|
|
170
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolves true once `server.done` settled and no process of its group is left,
|
|
176
|
+
* false when `ms` ran out first. The leader (`pnpm`) exiting says nothing about
|
|
177
|
+
* the vite it spawned, which can outlive it, so the group is polled too.
|
|
178
|
+
*/
|
|
179
|
+
async function groupExitedWithin(
|
|
180
|
+
server: RunCliHandle,
|
|
181
|
+
ms: number,
|
|
182
|
+
): Promise<boolean> {
|
|
183
|
+
const deadline = Date.now() + ms;
|
|
184
|
+
if (!(await exitedWithin(server.done, ms))) return false;
|
|
185
|
+
const pgid = server.pid;
|
|
186
|
+
if (pgid === undefined) return true;
|
|
187
|
+
while (groupAlive(pgid)) {
|
|
188
|
+
if (Date.now() >= deadline) return false;
|
|
189
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Stop a server started by `runCli` and wait for its whole process group to
|
|
196
|
+
* exit: SIGTERM to the group, then SIGKILL (and a log naming the pid) for a
|
|
197
|
+
* process that outlives the wait, so a leaked server is visible rather than
|
|
198
|
+
* left holding its port.
|
|
199
|
+
*/
|
|
200
|
+
export async function stopServer(
|
|
201
|
+
server: RunCliHandle,
|
|
202
|
+
options: { exitWaitMs?: number } = {},
|
|
203
|
+
): Promise<void> {
|
|
204
|
+
const exitWaitMs = options.exitWaitMs ?? STOP_WAIT_MS;
|
|
205
|
+
server.kill();
|
|
206
|
+
if (await groupExitedWithin(server, exitWaitMs)) return;
|
|
207
|
+
console.error(
|
|
208
|
+
`[rango e2e] server (pid ${server.pid ?? "unknown"}) ignored SIGTERM for ` +
|
|
209
|
+
`${exitWaitMs} ms and was sent SIGKILL`,
|
|
210
|
+
);
|
|
211
|
+
server.kill("SIGKILL");
|
|
212
|
+
await groupExitedWithin(server, KILL_WAIT_MS);
|
|
213
|
+
}
|
|
214
|
+
|
|
128
215
|
export function tailOutput(text: string, maxChars = 4000): string {
|
|
129
216
|
if (!text) return "(empty)";
|
|
130
217
|
if (text.length <= maxChars) return text;
|