@rangojs/router 0.31.0 → 0.33.0
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 -10143
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-QRR6HD6T.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/history-state.d.ts +13 -2
- 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 +19 -13
- package/dist/types/browser/react/location-state-shared.d.ts +2 -1
- 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/route-definition/redirect.d.ts +5 -0
- 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/middleware-types.d.ts +6 -0
- 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 +8 -3
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +4 -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 +59 -0
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +65 -12
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +83 -2
- package/dist/types/testing/serve-shell-request.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +21 -4
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +18 -1
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/types/vite/utils/forward-user-plugins.d.ts +32 -2
- package/dist/vite/index.js +451 -191
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +74 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +15 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +18 -11
- package/skills/comparison/references/framework-comparison.md +16 -10
- package/skills/document-cache/SKILL.md +13 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +132 -12
- package/skills/intercept/SKILL.md +33 -11
- package/skills/layout/SKILL.md +11 -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 +38 -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 +49 -8
- package/skills/testing/client-components.md +73 -32
- package/skills/testing/serve-shell-request.md +45 -20
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +67 -15
- 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/history-state.ts +24 -2
- 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 +24 -16
- package/src/browser/react/location-state-shared.ts +8 -1
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +13 -3
- package/src/browser/server-action-bridge.ts +3 -3
- 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/build/runtime-discovery.ts +46 -8
- 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 +37 -1
- package/src/cache/document-cache.ts +12 -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/route-definition/redirect.ts +5 -0
- 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 +39 -14
- package/src/router/match-result.ts +45 -85
- package/src/router/middleware-types.ts +6 -0
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +63 -13
- 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 +20 -6
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +8 -2
- package/src/rsc/request-stages.ts +87 -1
- package/src/rsc/rsc-rendering.ts +78 -32
- 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 +334 -0
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +137 -33
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +309 -10
- package/src/testing/run-revalidate.ts +11 -1
- package/src/testing/serve-shell-request.ts +22 -3
- package/src/types/handler-context.ts +21 -4
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +18 -1
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
- package/src/vite/utils/forward-user-plugins.ts +13 -5
|
@@ -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.
|
|
669
688
|
*/
|
|
670
|
-
|
|
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).
|
|
696
|
+
*/
|
|
697
|
+
_requestKind?: RequestKind;
|
|
671
698
|
|
|
672
699
|
/**
|
|
673
700
|
* @internal Set when a render shows the visitor's URL of a rewritten
|
|
@@ -680,12 +707,27 @@ export interface RequestContext<
|
|
|
680
707
|
/**
|
|
681
708
|
* Attach location state entries to the current response.
|
|
682
709
|
*
|
|
683
|
-
* For
|
|
684
|
-
* metadata and merged into history.
|
|
685
|
-
*
|
|
686
|
-
*
|
|
710
|
+
* For a client navigation or an action, the state is included in the RSC
|
|
711
|
+
* payload metadata and merged into the entry's history.state on the
|
|
712
|
+
* client. For a document request (a page load, a no-JS form post), it rides
|
|
713
|
+
* the document's bootstrap payload and is merged into the entry when the
|
|
714
|
+
* client starts; readers get it once the page has hydrated. For a redirect,
|
|
715
|
+
* the state travels with the redirect: in its Flight payload on a
|
|
716
|
+
* navigation or action, in a one-shot cookie the target's document request
|
|
717
|
+
* reads on a document request. A document that carries state is sent
|
|
718
|
+
* `Cache-Control: private, no-store` and never stored by the document cache
|
|
719
|
+
* or as a PPR shell.
|
|
687
720
|
*
|
|
688
|
-
*
|
|
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
|
+
*
|
|
726
|
+
* Multiple calls accumulate entries across definitions. For one definition
|
|
727
|
+
* the last write wins: two entries of the same definition share its slot
|
|
728
|
+
* in history.state, so a later call (or a later entry in one array)
|
|
729
|
+
* replaces an earlier one. To combine writes, make the state an array and
|
|
730
|
+
* write it once.
|
|
689
731
|
*
|
|
690
732
|
* @example
|
|
691
733
|
* ```typescript
|
|
@@ -697,6 +739,24 @@ export interface RequestContext<
|
|
|
697
739
|
/** @internal Accumulated location state entries */
|
|
698
740
|
_locationState?: LocationStateEntry[];
|
|
699
741
|
|
|
742
|
+
/**
|
|
743
|
+
* @internal Set when a document's payload carries location state
|
|
744
|
+
* (rsc/rsc-rendering.ts, rsc/progressive-enhancement.ts), a shell HIT's
|
|
745
|
+
* included. The document is per visitor: the document cache refuses it,
|
|
746
|
+
* and it leaves `private, no-store` (server/document-location-state.ts).
|
|
747
|
+
*/
|
|
748
|
+
_documentLocationState?: boolean;
|
|
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
|
+
|
|
700
760
|
/**
|
|
701
761
|
* The matched route name, if the route has an explicit name.
|
|
702
762
|
* Undefined before route matching or for unnamed routes.
|
|
@@ -1003,9 +1063,13 @@ export type PublicRequestContext<
|
|
|
1003
1063
|
| "_documentCacheRender"
|
|
1004
1064
|
| "_payloadVisitorTheme"
|
|
1005
1065
|
| "_payloadDeferred"
|
|
1066
|
+
| "_payloadSourceScoped"
|
|
1067
|
+
| "_servedFromStorage"
|
|
1006
1068
|
| "_requestKind"
|
|
1007
1069
|
| "_payloadVisitorLocation"
|
|
1008
1070
|
| "_locationState"
|
|
1071
|
+
| "_documentLocationState"
|
|
1072
|
+
| "_carriedLocationStateCookie"
|
|
1009
1073
|
| "_routeName"
|
|
1010
1074
|
| "_prevRouteKey"
|
|
1011
1075
|
| "_visitorMoved"
|
|
@@ -1358,7 +1422,12 @@ export function createRequestContext<TEnv>(
|
|
|
1358
1422
|
// of this request's headers goes through requestHeaders().
|
|
1359
1423
|
guardRequestHeaders(request);
|
|
1360
1424
|
const cookieHeader = requestHeaders(request).get("Cookie");
|
|
1361
|
-
|
|
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;
|
|
1362
1431
|
const freshCookie = stateCookieName
|
|
1363
1432
|
? freshReadsCookieName(stateCookieName)
|
|
1364
1433
|
: undefined;
|
|
@@ -1438,6 +1507,17 @@ export function createRequestContext<TEnv>(
|
|
|
1438
1507
|
rawStubHeaders = stubResponse.headers;
|
|
1439
1508
|
shadowStubHeaders(stubResponse);
|
|
1440
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
|
+
};
|
|
1441
1521
|
|
|
1442
1522
|
const handleStore = createHandleStore();
|
|
1443
1523
|
const loaderPromises = new Map<string, Promise<any>>();
|
|
@@ -1613,16 +1693,29 @@ export function createRequestContext<TEnv>(
|
|
|
1613
1693
|
stubResponse.headers.set(name, value);
|
|
1614
1694
|
},
|
|
1615
1695
|
|
|
1616
|
-
// Rotate the rango state cookie for the responding client
|
|
1617
|
-
//
|
|
1618
|
-
// 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
|
|
1619
1698
|
// serializer), not the URL-encoded form serializeCookieValue would produce.
|
|
1620
|
-
//
|
|
1621
|
-
//
|
|
1622
|
-
//
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
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;
|
|
1626
1719
|
if (!stateCookieName) return;
|
|
1627
1720
|
// The client's current value, for the monotonic guard: prefer the
|
|
1628
1721
|
// X-Rango-State header (router navigation/prefetch fetches send it), but
|
|
@@ -1637,22 +1730,38 @@ export function createRequestContext<TEnv>(
|
|
|
1637
1730
|
const prevRaw =
|
|
1638
1731
|
(requestHeaders(request).get("x-rango-state") || null) ??
|
|
1639
1732
|
getRawCookieValue(cookieHeader, stateCookieName);
|
|
1640
|
-
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;
|
|
1641
1739
|
// rawStubHeaders: guard-exempt internal writer — invalidateClientCache()
|
|
1642
1740
|
// is documented callable from loaders and during shell capture.
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1741
|
+
if (stateCookieLine !== null) dropSetCookieLine(stateCookieLine);
|
|
1742
|
+
stateCookieLine = serializeStateCookie(
|
|
1743
|
+
stateCookieName,
|
|
1744
|
+
value,
|
|
1745
|
+
url.protocol === "https:",
|
|
1646
1746
|
);
|
|
1747
|
+
rawStubHeaders.append("Set-Cookie", stateCookieLine);
|
|
1647
1748
|
invalidateResponseCookieCache();
|
|
1648
1749
|
},
|
|
1649
1750
|
|
|
1650
1751
|
// Set the keepClientCache() directive header. The action bridge reads it on
|
|
1651
1752
|
// the response and suppresses its automatic invalidation. `.set` makes this
|
|
1652
1753
|
// idempotent (one header regardless of call count). rawStubHeaders:
|
|
1653
|
-
// 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.
|
|
1654
1757
|
_setKeepCacheDirective(): void {
|
|
1655
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
|
+
}
|
|
1656
1765
|
},
|
|
1657
1766
|
|
|
1658
1767
|
// The fresh-reads cookie (browser/cookie-name.ts): one Set-Cookie per
|
|
@@ -1685,12 +1794,7 @@ export function createRequestContext<TEnv>(
|
|
|
1685
1794
|
`${freshCookie}=1; Max-Age=${age}; Path=/; HttpOnly; SameSite=Lax` +
|
|
1686
1795
|
(url.protocol === "https:" ? "; Secure" : "");
|
|
1687
1796
|
if (freshCookieMaxAge > 0) {
|
|
1688
|
-
|
|
1689
|
-
const kept = rawStubHeaders
|
|
1690
|
-
.getSetCookie()
|
|
1691
|
-
.filter((cookie) => cookie !== prior);
|
|
1692
|
-
rawStubHeaders.delete("Set-Cookie");
|
|
1693
|
-
for (const cookie of kept) rawStubHeaders.append("Set-Cookie", cookie);
|
|
1797
|
+
dropSetCookieLine(serialized(freshCookieMaxAge));
|
|
1694
1798
|
}
|
|
1695
1799
|
freshCookieMaxAge = maxAge;
|
|
1696
1800
|
rawStubHeaders.append("Set-Cookie", serialized(maxAge));
|
package/src/testing/dispatch.ts
CHANGED
|
@@ -177,6 +177,10 @@ interface DispatchableRouter<TEnv> {
|
|
|
177
177
|
onTimeout?: OnTimeoutCallback<TEnv>;
|
|
178
178
|
findMatch: ClassifyRequestDeps<TEnv>["findMatch"];
|
|
179
179
|
basename?: string;
|
|
180
|
+
/** The rango state cookie name (RangoInternal field). */
|
|
181
|
+
resolvedStateCookieName: string;
|
|
182
|
+
/** The in-memory prefetch TTL in ms (RangoInternal field). */
|
|
183
|
+
prefetchCacheTTL: number;
|
|
180
184
|
cache?:
|
|
181
185
|
| {
|
|
182
186
|
enabled?: boolean;
|
|
@@ -292,6 +296,8 @@ export async function dispatch<TEnv = any>(
|
|
|
292
296
|
telemetry: router.telemetry,
|
|
293
297
|
timeouts: router.timeouts,
|
|
294
298
|
onTimeout: router.onTimeout,
|
|
299
|
+
resolvedStateCookieName: router.resolvedStateCookieName,
|
|
300
|
+
prefetchCacheTTL: router.prefetchCacheTTL,
|
|
295
301
|
},
|
|
296
302
|
callOnError: stages.createErrorReporter<TEnv>(router),
|
|
297
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;
|