@rangojs/router 0.31.0 → 0.32.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.
Files changed (52) hide show
  1. package/dist/bin/rango.js +37 -7
  2. package/dist/types/browser/history-state.d.ts +13 -2
  3. package/dist/types/browser/react/Link.d.ts +10 -13
  4. package/dist/types/browser/react/location-state-shared.d.ts +2 -1
  5. package/dist/types/cache/document-cache-policy.d.ts +1 -1
  6. package/dist/types/route-definition/redirect.d.ts +5 -0
  7. package/dist/types/router/middleware-types.d.ts +6 -0
  8. package/dist/types/rsc/helpers.d.ts +5 -3
  9. package/dist/types/rsc/request-stages.d.ts +2 -0
  10. package/dist/types/server/document-location-state.d.ts +56 -0
  11. package/dist/types/server/request-context.d.ts +23 -6
  12. package/dist/types/testing/render-route.d.ts +17 -0
  13. package/dist/types/testing/serve-shell-request.d.ts +9 -0
  14. package/dist/types/types/handler-context.d.ts +18 -3
  15. package/dist/types/types/segments.d.ts +6 -0
  16. package/dist/types/vite/utils/forward-user-plugins.d.ts +32 -2
  17. package/dist/vite/index.js +3 -3
  18. package/package.json +1 -1
  19. package/skills/comparison/references/framework-comparison.md +3 -2
  20. package/skills/document-cache/SKILL.md +5 -0
  21. package/skills/hooks/state.md +110 -6
  22. package/skills/intercept/SKILL.md +2 -1
  23. package/skills/ppr/SKILL.md +12 -0
  24. package/skills/server-actions/SKILL.md +8 -0
  25. package/skills/testing/client-components.md +44 -19
  26. package/skills/testing/serve-shell-request.md +38 -13
  27. package/skills/ux-architecture/SKILL.md +6 -3
  28. package/src/browser/history-state.ts +24 -2
  29. package/src/browser/react/Link.tsx +10 -13
  30. package/src/browser/react/location-state-shared.ts +8 -1
  31. package/src/browser/rsc-router.tsx +5 -2
  32. package/src/build/runtime-discovery.ts +46 -8
  33. package/src/cache/document-cache-policy.ts +10 -1
  34. package/src/cache/document-cache.ts +7 -0
  35. package/src/route-definition/redirect.ts +5 -0
  36. package/src/router/match-middleware/cache-lookup.ts +23 -6
  37. package/src/router/match-result.ts +9 -1
  38. package/src/router/middleware-types.ts +6 -0
  39. package/src/router/revalidation.ts +23 -3
  40. package/src/rsc/helpers.ts +13 -4
  41. package/src/rsc/progressive-enhancement.ts +5 -0
  42. package/src/rsc/request-stages.ts +28 -1
  43. package/src/rsc/rsc-rendering.ts +39 -9
  44. package/src/server/document-location-state.ts +313 -0
  45. package/src/server/request-context.ts +24 -5
  46. package/src/testing/dispatch.ts +3 -0
  47. package/src/testing/render-route.tsx +40 -6
  48. package/src/testing/run-revalidate.ts +11 -1
  49. package/src/testing/serve-shell-request.ts +16 -0
  50. package/src/types/handler-context.ts +18 -3
  51. package/src/types/segments.ts +6 -0
  52. package/src/vite/utils/forward-user-plugins.ts +13 -5
package/dist/bin/rango.js CHANGED
@@ -1762,7 +1762,7 @@ var init_package = __esm({
1762
1762
  "package.json"() {
1763
1763
  package_default = {
1764
1764
  name: "@rangojs/router",
1765
- version: "0.31.0",
1765
+ version: "0.32.0",
1766
1766
  description: "Django-inspired RSC router with composable URL patterns",
1767
1767
  keywords: [
1768
1768
  "react",
@@ -9859,10 +9859,11 @@ var init_temp_rsc_server = __esm({
9859
9859
  // src/vite/utils/forward-user-plugins.ts
9860
9860
  var forward_user_plugins_exports = {};
9861
9861
  __export(forward_user_plugins_exports, {
9862
+ isFrameworkPlugin: () => isFrameworkPlugin,
9862
9863
  pickForwardedRunnerConfig: () => pickForwardedRunnerConfig,
9863
9864
  selectForwardableResolvePlugins: () => selectForwardableResolvePlugins
9864
9865
  });
9865
- function isDenied(name) {
9866
+ function isFrameworkPlugin(name) {
9866
9867
  return name.startsWith("vite:") || name === "rsc" || name.startsWith("rsc:") || name.startsWith("@rangojs/router") || name.startsWith("@cloudflare/vite-plugin") || name.startsWith("vite-plugin-cloudflare");
9867
9868
  }
9868
9869
  function isForwardable(p) {
@@ -9886,7 +9887,7 @@ function selectForwardableResolvePlugins(plugins) {
9886
9887
  const forwarded = [];
9887
9888
  for (const p of plugins) {
9888
9889
  const name = p?.name;
9889
- if (!name || isDenied(name)) continue;
9890
+ if (!name || isFrameworkPlugin(name)) continue;
9890
9891
  if (!isForwardable(p)) continue;
9891
9892
  forwarded.push(stripToResolutionHooks(p));
9892
9893
  }
@@ -9934,9 +9935,11 @@ import { basename, resolve as resolve4 } from "node:path";
9934
9935
  import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
9935
9936
  async function discoverAndWriteRouteTypes(opts) {
9936
9937
  let loadConfigFromFile;
9938
+ let resolveConfig;
9937
9939
  try {
9938
9940
  const vite = await import("vite");
9939
9941
  loadConfigFromFile = vite.loadConfigFromFile;
9942
+ resolveConfig = vite.resolveConfig;
9940
9943
  } catch {
9941
9944
  throw new Error(
9942
9945
  "Runtime discovery requires 'vite'. Install it with: pnpm add -D vite"
@@ -9952,7 +9955,11 @@ async function discoverAndWriteRouteTypes(opts) {
9952
9955
  const { createTempRscServer: createTempRscServer2 } = await Promise.resolve().then(() => (init_temp_rsc_server(), temp_rsc_server_exports));
9953
9956
  const { createDiscoveryState: createDiscoveryState2 } = await Promise.resolve().then(() => (init_state(), state_exports));
9954
9957
  const { discoverClientUrlProjections: discoverClientUrlProjections2 } = await Promise.resolve().then(() => (init_client_urls_projection(), client_urls_projection_exports));
9955
- const { pickForwardedRunnerConfig: pickForwardedRunnerConfig2, selectForwardableResolvePlugins: selectForwardableResolvePlugins2 } = await Promise.resolve().then(() => (init_forward_user_plugins(), forward_user_plugins_exports));
9958
+ const {
9959
+ isFrameworkPlugin: isFrameworkPlugin2,
9960
+ pickForwardedRunnerConfig: pickForwardedRunnerConfig2,
9961
+ selectForwardableResolvePlugins: selectForwardableResolvePlugins2
9962
+ } = await Promise.resolve().then(() => (init_forward_user_plugins(), forward_user_plugins_exports));
9956
9963
  const entryPath = resolve4(opts.entry);
9957
9964
  const state = createDiscoveryState2(entryPath, void 0);
9958
9965
  state.projectRoot = opts.root;
@@ -9966,9 +9973,32 @@ async function discoverAndWriteRouteTypes(opts) {
9966
9973
  );
9967
9974
  if (loaded) {
9968
9975
  state.userRunnerConfig = pickForwardedRunnerConfig2(loaded.config);
9969
- state.userResolvePlugins = selectForwardableResolvePlugins2(
9970
- await flattenPluginOptions(loaded.config.plugins)
9971
- );
9976
+ const userPlugins = (await flattenPluginOptions(loaded.config.plugins)).filter((p) => !isFrameworkPlugin2(p.name ?? ""));
9977
+ try {
9978
+ const resolved2 = await resolveConfig(
9979
+ {
9980
+ ...loaded.config,
9981
+ root: opts.root,
9982
+ configFile: false,
9983
+ plugins: userPlugins
9984
+ },
9985
+ "serve",
9986
+ "development"
9987
+ );
9988
+ state.userResolvePlugins = selectForwardableResolvePlugins2(
9989
+ resolved2.plugins
9990
+ );
9991
+ } catch (err) {
9992
+ console.warn(
9993
+ `[rango] Could not resolve the Vite config (${err?.message ?? err}); forwarding its resolver plugins without their config hooks. rango() and rsc() are left out of that resolve, so a plugin that needs the rsc environment fails there`
9994
+ );
9995
+ const serveEnv = { command: "serve", mode: "development" };
9996
+ state.userResolvePlugins = selectForwardableResolvePlugins2(
9997
+ userPlugins.filter(
9998
+ (p) => typeof p.apply === "function" ? p.apply(loaded.config, serveEnv) : p.apply !== "build"
9999
+ )
10000
+ );
10001
+ }
9972
10002
  }
9973
10003
  } catch (err) {
9974
10004
  console.warn(
@@ -35,8 +35,9 @@ export declare function stripShellMissMarker(): void;
35
35
  * the server rendered this document without them. One pass before hydration
36
36
  * (initBrowserApp; renderRoute's `hydrate` mode), so no reader needs to know
37
37
  * the page began as a document load, and a slot nobody reads is removed too.
38
- * No server-set state exists yet to lose: a document response carries none
39
- * (rsc-rendering.ts attaches it to partial payloads only).
38
+ * Server-set state is not lost to it: the state a document response carries
39
+ * is merged after this pass (applyDocumentLocationState), so a `~r` slot the
40
+ * server set for this load survives this load and no later one.
40
41
  *
41
42
  * A slot is recognised by its key alone (the `~r` suffix): at start-up the
42
43
  * module that defines it may not be loaded.
@@ -46,6 +47,16 @@ export declare function stripShellMissMarker(): void;
46
47
  * is a full page for later Back/Forward.
47
48
  */
48
49
  export declare function clearLocationStateOnDocumentLoad(): void;
50
+ /**
51
+ * Merge the location state a document response carried (its bootstrap
52
+ * payload's `metadata.locationState`, server/document-location-state.ts)
53
+ * into the entry the document loaded on. Start-up runs it after
54
+ * clearLocationStateOnDocumentLoad and the version is set, before the entry
55
+ * is committed for readers (initBrowserApp; renderRoute's `hydrate` mode),
56
+ * so readers get it once they have hydrated, as stored state. Only typed
57
+ * slots are taken.
58
+ */
59
+ export declare function applyDocumentLocationState(locationState: Record<string, unknown> | undefined): void;
49
60
  /**
50
61
  * Merge server-set location state into the current history entry and return
51
62
  * the entry state written. Readers see it when the caller commits it
@@ -66,23 +66,20 @@ export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorEle
66
66
  * keyed on Rango state + target URL. This is correct for routes whose
67
67
  * response shape doesn't depend on where the user navigates from.
68
68
  *
69
- * Set `":source"` when this Link's response would legitimately differ
70
- * based on the source page — typically when the target route (or one
71
- * of its layouts) uses a custom `revalidate()` handler that reads
72
- * `currentUrl` / `currentParams`, and the wildcard entry would
73
- * therefore serve the wrong diff to a navigation from a different
74
- * source.
69
+ * Set `":source"` to scope every prefetch of this Link to the page that
70
+ * sent it, when its response would differ by source page in a way the
71
+ * server cannot see.
75
72
  *
76
- * A route an intercept targets is scoped to the source automatically
77
- * (server-side `x-rsc-prefetch-scope: source`), whether or not the
78
- * intercept applies from this page: the modal and the full page are
79
- * both source-specific. Every other route stays shared, so `":source"`
80
- * is only needed for custom revalidation logic.
73
+ * The server scopes a prefetch to its source automatically
74
+ * (`x-rsc-prefetch-scope: source`) when an intercept targets the route,
75
+ * whether or not it applies from this page (the modal and the full page
76
+ * are both source-specific), and when a `revalidate()` of the target
77
+ * read `currentParams`, `currentUrl` or `fromRouteName` and kept the
78
+ * segment: that diff is the source page's (#1331). Every other response
79
+ * stays shared.
81
80
  *
82
81
  * @example
83
82
  * ```tsx
84
- * // Route uses a `revalidate()` that branches on currentUrl — opt in
85
- * // so prefetches don't bleed across source pages.
86
83
  * <Link to="/dashboard" prefetch="hover" prefetchKey=":source" />
87
84
  * ```
88
85
  */
@@ -269,7 +269,8 @@ export declare function isLocationStateDefinition(value: unknown): value is Loca
269
269
  */
270
270
  export declare function isLocationStateEntry(value: unknown): value is LocationStateEntry;
271
271
  /**
272
- * Resolve state entries into a flat object for history.state
272
+ * Resolve state entries into a flat object for history.state. One slot per
273
+ * definition: a later entry of the same definition replaces an earlier one.
273
274
  */
274
275
  export declare function resolveLocationStateEntries(entries: LocationStateEntry[]): Record<string, unknown>;
275
276
  export {};
@@ -12,7 +12,7 @@ export interface CacheDirectives {
12
12
  /**
13
13
  * Check if response should be cached based on Cache-Control headers
14
14
  */
15
- export declare function shouldCacheResponse(response: Response, requestCtx?: Pick<RequestContext, "_payloadVisitorTheme" | "_payloadDeferred" | "_payloadVisitorLocation">): CacheDirectives | null;
15
+ export declare function shouldCacheResponse(response: Response, requestCtx?: Pick<RequestContext, "_payloadVisitorTheme" | "_payloadDeferred" | "_payloadVisitorLocation" | "_documentLocationState">): CacheDirectives | null;
16
16
  /**
17
17
  * Whether this cache will store the response the current render produces, as
18
18
  * far as the render can tell: the middleware runs it (a MISS or a stale
@@ -9,6 +9,11 @@ import type { LocationStateEntry } from "../browser/react/location-state-shared.
9
9
  * Supports an optional state parameter to carry location state through the
10
10
  * redirect. On the target page, state can be read via useLocationState()
11
11
  * (use { flash: true } in createLocationState for read-once flash messages).
12
+ * On a client navigation or action the state travels in the redirect's
13
+ * payload. On a document request (a page load, a no-JS form post) it travels
14
+ * in a one-shot cookie the target's document request reads and clears, as
15
+ * JSON: give it plain JSON data there. `{ external: true }` carries none.
16
+ * Two entries of the same definition keep only the last.
12
17
  *
13
18
  * @param url - The URL to redirect to
14
19
  * @param statusOrOptions - HTTP status code (default: 302) or options object
@@ -56,6 +56,12 @@ export interface MiddlewareContext<TEnv = any, TParams = Record<string, string |
56
56
  */
57
57
  readonly theme?: Theme;
58
58
  setTheme?: (theme: Theme) => void;
59
+ /**
60
+ * Attach location state entries to this response (see the handler
61
+ * context's `setLocationState`). Entries accumulate across definitions;
62
+ * for one definition the last write wins. Set it here rather than in a
63
+ * handler on a `ppr` route: a shell HIT runs no handler.
64
+ */
59
65
  setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
60
66
  /** Global route names only; see {@link GlobalReverseFunction}. */
61
67
  reverse: GlobalReverseFunction<DefaultReverseRouteMap>;
@@ -95,10 +95,12 @@ export declare function interceptRedirectForPartial(response: Response, createRe
95
95
  /**
96
96
  * Attach location state set during a request to a payload's metadata.
97
97
  * No-op if no location state was set. Callers must ensure payload.metadata
98
- * is populated (the non-null assertion holds for the partial/action payloads
99
- * that reach this helper).
98
+ * is populated. A `document` payload (a page load's or a no-JS form post's
99
+ * bootstrap payload) that carries state marks the request
100
+ * `_documentLocationState`: it is the visitor's, so no shared cache keeps it
101
+ * (server/document-location-state.ts).
100
102
  */
101
- export declare function attachLocationStateIfPresent(payload: RscPayload): void;
103
+ export declare function attachLocationStateIfPresent(payload: RscPayload, document?: boolean): void;
102
104
  /**
103
105
  * Only cache successful responses. Non-200 statuses (errors, redirects) are
104
106
  * not cached -- notFound() produces 500 in response routes, and explicit
@@ -36,6 +36,8 @@ export interface StageRouter<TEnv = unknown> {
36
36
  readonly telemetry?: TelemetrySink;
37
37
  readonly timeouts?: ResolvedTimeouts;
38
38
  readonly onTimeout?: OnTimeoutCallback<TEnv>;
39
+ /** Names the cookie location state crosses a document redirect in. */
40
+ readonly resolvedStateCookieName: string;
39
41
  }
40
42
  /** Report an error through the router's onError. */
41
43
  export type ReportError<TEnv = unknown> = (error: unknown, phase: ErrorPhase, context: InvokeOnErrorContext<TEnv>) => void;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Location state on document responses (#1330).
3
+ *
4
+ * A client navigation or an action carries server-set location state in its
5
+ * Flight payload, and a redirect on either becomes a Flight redirect that
6
+ * carries it too. A document request has no such channel for a 3xx: the
7
+ * browser follows it with a plain GET. This module carries the state across
8
+ * that one hop in a short-lived cookie:
9
+ *
10
+ * - `carryLocationStateAcrossRedirect` (rsc/request-stages.ts
11
+ * finalizeRequestResponse) writes it on a document request's same-origin
12
+ * 3xx when the request set location state;
13
+ * - `consumeDocumentLocationState` (rsc/request-stages.ts runRequestPasses)
14
+ * reads it on the next GET document request that navigates to the target
15
+ * pathname (`Sec-Fetch-Dest` a browsing context, where sent), seeds
16
+ * `_locationState` with its entries (so the render attaches them to the
17
+ * bootstrap payload, rsc/rsc-rendering.ts) and clears it.
18
+ *
19
+ * A document that carries location state is per visitor: it is marked
20
+ * `_documentLocationState`, never stored by the document cache or as a PPR
21
+ * shell, and leaves with `Cache-Control: private, no-store`
22
+ * (`markDocumentResponsePrivate`).
23
+ *
24
+ * The value is JSON, not Flight: a cookie holds text, and the client merges
25
+ * it as `history.state` would hold it. `Path=/` because the `__Host-` prefix
26
+ * (https) requires it, which keeps a sibling subdomain from planting state;
27
+ * the target pathname inside the value decides who consumes it.
28
+ */
29
+ import { type RequestContext } from "./request-context.js";
30
+ /** The cookie's name for a router: `__Host-` on https. */
31
+ export declare function documentLocationStateCookieName(stateCookieName: string, secure: boolean): string;
32
+ /**
33
+ * Read the cookie a document redirect left for this request: seed
34
+ * `_locationState` with its slots and clear it. Only a GET document request
35
+ * of the target pathname that navigates consumes it (the request the browser
36
+ * makes for a 301/302/303, see isNavigationRequest); any other request, a
37
+ * form POST to that path or a prefetch of it included, leaves it to its
38
+ * expiry. A malformed value is cleared and ignored, and only `__rsc_ls_*`
39
+ * slots are taken from it (the value comes from the browser).
40
+ */
41
+ export declare function consumeDocumentLocationState(reqCtx: Pick<RequestContext, "_locationState" | "setCookie">, request: Request, url: URL, stateCookieName: string): void;
42
+ /**
43
+ * Write the cookie on a document request's redirect that set location state,
44
+ * and mark the redirect private. Not for a partial or action request (their
45
+ * redirect is a Flight payload), an external or cross-origin target (the
46
+ * cookie would never reach it) or a response that is not a 3xx with a
47
+ * Location. Returns the response to send: a new one when its headers are
48
+ * immutable.
49
+ */
50
+ export declare function carryLocationStateAcrossRedirect(response: Response, reqCtx: Pick<RequestContext, "_locationState">, request: Request, url: URL, stateCookieName: string): Response;
51
+ /**
52
+ * `Cache-Control: private, no-store` on a document that carries location
53
+ * state, after every middleware: a shared cache must never store or replay
54
+ * one visitor's state. Returns the response to send.
55
+ */
56
+ export declare function markDocumentResponsePrivate(response: Response, reqCtx: Pick<RequestContext, "_documentLocationState">): Response;
@@ -547,12 +547,22 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
547
547
  /**
548
548
  * Attach location state entries to the current response.
549
549
  *
550
- * For partial (SPA) requests, the state is included in the RSC payload
551
- * metadata and merged into history.pushState on the client. For redirect
552
- * responses, the state travels through the redirect payload so the target
553
- * page can read it via useLocationState.
550
+ * For a client navigation or an action, the state is included in the RSC
551
+ * payload metadata and merged into the entry's history.state on the
552
+ * client. For a document request (a page load, a no-JS form post), it rides
553
+ * the document's bootstrap payload and is merged into the entry when the
554
+ * client starts; readers get it once the page has hydrated. For a redirect,
555
+ * the state travels with the redirect: in its Flight payload on a
556
+ * navigation or action, in a one-shot cookie the target's document request
557
+ * reads on a document request. A document that carries state is sent
558
+ * `Cache-Control: private, no-store` and never stored by the document cache
559
+ * or as a PPR shell.
554
560
  *
555
- * Multiple calls accumulate entries.
561
+ * Multiple calls accumulate entries across definitions. For one definition
562
+ * the last write wins: two entries of the same definition share its slot
563
+ * in history.state, so a later call (or a later entry in one array)
564
+ * replaces an earlier one. To combine writes, make the state an array and
565
+ * write it once.
556
566
  *
557
567
  * @example
558
568
  * ```typescript
@@ -562,6 +572,13 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
562
572
  setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
563
573
  /** @internal Accumulated location state entries */
564
574
  _locationState?: LocationStateEntry[];
575
+ /**
576
+ * @internal Set when a document's payload carries location state
577
+ * (rsc/rsc-rendering.ts, rsc/progressive-enhancement.ts), a shell HIT's
578
+ * included. The document is per visitor: the document cache refuses it,
579
+ * and it leaves `private, no-store` (server/document-location-state.ts).
580
+ */
581
+ _documentLocationState?: boolean;
565
582
  /**
566
583
  * The matched route name, if the route has an explicit name.
567
584
  * Undefined before route matching or for unnamed routes.
@@ -795,7 +812,7 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
795
812
  * This is the type exported to library consumers. Internal code should
796
813
  * use the full RequestContext interface directly.
797
814
  */
798
- export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "_readCookie" | "_readCookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhenRefs" | "_pprReplayPostMatchReason" | "_cacheStore" | "_prerender" | "_onDemandProducer" | "_searchParamsFilter" | "_shellCaptureState" | "_shellImplicitCache" | "_shellLoaderSeed" | "_resolvedCacheKeys" | "_shellKey" | "_shellForcedMiss" | "_prerenderWarm" | "_runAtRequestEntry" | "_shellFragmentPayload" | "_explicitTaggedStores" | "_requestTags" | "_recordTagOwners" | "_recordLoaderTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_readTheme" | "_documentCacheRender" | "_payloadVisitorTheme" | "_payloadDeferred" | "_requestKind" | "_payloadVisitorLocation" | "_locationState" | "_routeName" | "_prevRouteKey" | "_visitorMoved" | "_inActionRevalidation" | "_actionRan" | "_reportedErrors" | "_throwers" | "_printedErrors" | "_renderErrors" | "_recoveredHandlerErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_awaitBeforeFlushLoaderIds" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_activeRoutine" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_handlerTiming" | "_tracing" | "_basename" | "_routerId" | "_versions" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_freshReads" | "_setFreshReadsCookie" | "_requestRoot" | "_requestStart" | "_responseSent" | "_variables" | "_classifiedRoute" | "_requestMode" | "_cacheSignal" | "_dynamic" | "res">;
815
+ export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "_readCookie" | "_readCookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhenRefs" | "_pprReplayPostMatchReason" | "_cacheStore" | "_prerender" | "_onDemandProducer" | "_searchParamsFilter" | "_shellCaptureState" | "_shellImplicitCache" | "_shellLoaderSeed" | "_resolvedCacheKeys" | "_shellKey" | "_shellForcedMiss" | "_prerenderWarm" | "_runAtRequestEntry" | "_shellFragmentPayload" | "_explicitTaggedStores" | "_requestTags" | "_recordTagOwners" | "_recordLoaderTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_readTheme" | "_documentCacheRender" | "_payloadVisitorTheme" | "_payloadDeferred" | "_requestKind" | "_payloadVisitorLocation" | "_locationState" | "_documentLocationState" | "_routeName" | "_prevRouteKey" | "_visitorMoved" | "_inActionRevalidation" | "_actionRan" | "_reportedErrors" | "_throwers" | "_printedErrors" | "_renderErrors" | "_recoveredHandlerErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_awaitBeforeFlushLoaderIds" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_activeRoutine" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_handlerTiming" | "_tracing" | "_basename" | "_routerId" | "_versions" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_freshReads" | "_setFreshReadsCookie" | "_requestRoot" | "_requestStart" | "_responseSent" | "_variables" | "_classifiedRoute" | "_requestMode" | "_cacheSignal" | "_dynamic" | "res">;
799
816
  /**
800
817
  * Marker for a waitUntil-scheduled fn whose task promise must NOT enter
801
818
  * _pendingBackgroundTasks. Used by the PPR shell capture for its own task:
@@ -185,6 +185,23 @@ export interface RenderRouteOptions {
185
185
  * });
186
186
  */
187
187
  locationState?: ReadonlyArray<readonly [LocationStateDefinition<any, any>, unknown]>;
188
+ /**
189
+ * Location state the document response carries, as `[def, value]` pairs:
190
+ * what a handler, middleware or no-JS form action set with
191
+ * `ctx.setLocationState()` (or a document redirect carried) for this page
192
+ * load. Merged into the entry at start-up as initBrowserApp merges the
193
+ * bootstrap payload's: after `locationState` and, with `hydrate: true`,
194
+ * after the `clearOnReload` pass, so a `clearOnReload` slot given here is
195
+ * applied. With `hydrate: true` a reader hydrates as `undefined` and gets
196
+ * the value once the root has hydrated.
197
+ *
198
+ * @example
199
+ * await renderRoute([{ path: "/", Component: FlashBanner }], {
200
+ * hydrate: true,
201
+ * documentLocationState: [[FlashMessage, { text: "Payment declined" }]],
202
+ * });
203
+ */
204
+ documentLocationState?: ReadonlyArray<readonly [LocationStateDefinition<any, any>, unknown]>;
188
205
  /**
189
206
  * Handles to seed by REFERENCE, for components that read handle output via
190
207
  * `useHandle(SomeHandle)` (e.g. a client `Breadcrumbs` trail). Each entry is
@@ -229,6 +229,15 @@ export interface ServeShellRequestResult {
229
229
  * hydrates without it.
230
230
  */
231
231
  readPayloadFormState(): Promise<unknown>;
232
+ /**
233
+ * The payload's location state as the browser reads it: the slots
234
+ * (`Def.__rsc_ls_key` to value) a document's start-up merges into its
235
+ * entry (or a navigation's commit, for a `partial` request). Undefined when
236
+ * the payload carries none (or no Flight was rendered). A redirect carries
237
+ * a document request's state in a `Set-Cookie` instead: pass that cookie
238
+ * on the request of its target to read what the target's payload carries.
239
+ */
240
+ readLocationState(): Promise<Record<string, unknown> | undefined>;
232
241
  /**
233
242
  * For an `action` request, the call's answer as the browser reads it: its
234
243
  * result and the `revalidationError` its loaders' reruns report.
@@ -274,9 +274,16 @@ export type HandlerContext<TParams = {}, TEnv = DefaultEnv, TSearch extends Sear
274
274
  */
275
275
  setTheme?: (theme: Theme) => void;
276
276
  /**
277
- * Attach location state entries to this response.
278
- * State is delivered to the client via history.pushState and accessible
279
- * through the useLocationState() hook.
277
+ * Attach location state entries to this response, for the useLocationState()
278
+ * hook to read on the entry this response renders. A client navigation
279
+ * delivers it with its payload; a document load (a link from outside the
280
+ * app, a no-JS form post) with its bootstrap payload, once the page has
281
+ * hydrated; a redirect carries it to its target (in a one-shot cookie on a
282
+ * document request). A document carrying it is sent
283
+ * `Cache-Control: private, no-store`.
284
+ *
285
+ * Entries accumulate across definitions; for one definition the last write
286
+ * wins (an earlier entry of it is replaced, not merged).
280
287
  *
281
288
  * @example
282
289
  * ```typescript
@@ -390,6 +397,14 @@ export type InternalHandlerContext<TParams = {}, TEnv = DefaultEnv, TSearch exte
390
397
  * request is a fill. See router/segment-resolution/prefetch-deferral.ts.
391
398
  */
392
399
  _prefetchDeferral?: import("../router/segment-resolution/prefetch-deferral.js").PrefetchDeferral;
400
+ /**
401
+ * @internal A revalidate() predicate that read the source page
402
+ * (`currentParams`, `currentUrl`, `fromRouteName`) kept a held segment, so
403
+ * this match's diff is right for that source only (#1331). Set by
404
+ * evaluateRevalidation (router/revalidation.ts); match-result.ts turns it
405
+ * into `sourceHeld`, which scopes a prefetch of the response to its source.
406
+ */
407
+ _sourceHeld?: true;
393
408
  /**
394
409
  * @internal DSL loaders registered and not started, by `$$id`: a prefetch
395
410
  * deferred them, or a pass that skips loaders (`skipLoaders`) did not run
@@ -274,6 +274,12 @@ export interface MatchResult {
274
274
  * source, so the RSC handler scopes its prefetch entry to it (#1007).
275
275
  */
276
276
  interceptTargeted?: true;
277
+ /**
278
+ * Set on a partial match when a revalidate() predicate read the source page
279
+ * and kept a held segment: the diff is that source's. The RSC handler
280
+ * scopes its prefetch entry to it, like `interceptTargeted` (#1331).
281
+ */
282
+ sourceHeld?: true;
277
283
  /**
278
284
  * Redirect URL for trailing slash normalization.
279
285
  * When set, the RSC handler should return a 308 redirect to this URL
@@ -1,4 +1,34 @@
1
1
  import type { Plugin, ResolvedConfig, UserConfig } from "vite";
2
+ /**
3
+ * Whether a user plugin must NOT be forwarded into the discovery temp server.
4
+ *
5
+ * Framework-owned plugins are matched precisely -- by exact name or a
6
+ * namespaced prefix -- rather than by a loose substring/word prefix, so an
7
+ * unrelated user resolver named e.g. `rsc-paths` or `cloudflare-kv-alias` is
8
+ * still forwarded (it would otherwise reproduce issue #500).
9
+ *
10
+ * - `vite:*` Vite core + official plugins (incl.
11
+ * @vitejs/plugin-react's `vite:react-*`). The temp
12
+ * server already provides its own core; forwarding
13
+ * these would duplicate or conflict with it.
14
+ * - `rsc` / `rsc:*` @vitejs/plugin-rsc. createTempRscServer instantiates
15
+ * its own rsc() plugin; forwarding would duplicate it.
16
+ * Matched exactly (`rsc`) or by the `rsc:` namespace --
17
+ * NOT every `rsc`-prefixed name.
18
+ * - `@rangojs/router*` Our own plugins. The discovery plugin spawns the
19
+ * temp server, so forwarding would recurse infinitely.
20
+ * - `@cloudflare/vite-plugin*` / @cloudflare/vite-plugin (emits the scoped
21
+ * `vite-plugin-cloudflare*` `@cloudflare/vite-plugin` and unscoped
22
+ * `vite-plugin-cloudflare` / `vite-plugin-cloudflare:*`).
23
+ * Forwarding re-inits workerd, defeating the Node temp
24
+ * server. Matched specifically so a scoped user resolver
25
+ * like `@cloudflare/kv-alias` is still forwarded.
26
+ *
27
+ * `rango generate --runtime` drops these before it resolves the user's config,
28
+ * so rango()'s configResolved (which writes the static gen files) and the
29
+ * Cloudflare plugin's config hooks never run there.
30
+ */
31
+ export declare function isFrameworkPlugin(name: string): boolean;
2
32
  /**
3
33
  * Pick the user's resolution plugins from the resolved plugin list, denylist
4
34
  * framework-owned plugins, keep only those with resolution hooks, and strip
@@ -20,8 +50,8 @@ export interface ForwardedRunnerConfig {
20
50
  /**
21
51
  * Extract the data-only config slice to mirror into the discovery temp server.
22
52
  * `alias` is included here so callers no longer need to thread it separately.
23
- * Accepts the unresolved config too: `rango generate --runtime` must not
24
- * resolve it (that runs rango()'s configResolved, which writes gen files).
53
+ * Accepts the unresolved config too: `rango generate --runtime` passes the
54
+ * config file's own values, without what plugin `config` hooks add.
25
55
  *
26
56
  * `tsconfigPaths` is forwarded so Vite 8's native tsconfig `paths` resolution
27
57
  * (a top-level `resolve` flag, off by default) reaches the temp server. The
@@ -7146,7 +7146,7 @@ function resolveVersionsFrom(table, routerId2) {
7146
7146
  // package.json
7147
7147
  var package_default = {
7148
7148
  name: "@rangojs/router",
7149
- version: "0.31.0",
7149
+ version: "0.32.0",
7150
7150
  description: "Django-inspired RSC router with composable URL patterns",
7151
7151
  keywords: [
7152
7152
  "react",
@@ -14666,7 +14666,7 @@ function bundleScanPhase(s, context, bundle) {
14666
14666
  }
14667
14667
 
14668
14668
  // src/vite/utils/forward-user-plugins.ts
14669
- function isDenied(name) {
14669
+ function isFrameworkPlugin(name) {
14670
14670
  return name.startsWith("vite:") || name === "rsc" || name.startsWith("rsc:") || name.startsWith("@rangojs/router") || name.startsWith("@cloudflare/vite-plugin") || name.startsWith("vite-plugin-cloudflare");
14671
14671
  }
14672
14672
  var IMPORT_REWRITE_PLUGINS = /* @__PURE__ */ new Set([
@@ -14693,7 +14693,7 @@ function selectForwardableResolvePlugins(plugins) {
14693
14693
  const forwarded = [];
14694
14694
  for (const p of plugins) {
14695
14695
  const name = p?.name;
14696
- if (!name || isDenied(name)) continue;
14696
+ if (!name || isFrameworkPlugin(name)) continue;
14697
14697
  if (!isForwardable(p)) continue;
14698
14698
  forwarded.push(stripToResolutionHooks(p));
14699
14699
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -558,8 +558,9 @@ analog but operates on loader/query reload, not RSC-segment render.
558
558
 
559
559
  `<Link prefetch="hover|viewport|render|adaptive|none">` (default: the router's
560
560
  `defaultPrefetch`, `"none"` in development and `"viewport"` in production), plus
561
- `prefetchKey` (`":source"` scopes a prefetch to the originating page for routes
562
- whose response branches on `currentUrl`). Intercepted plain `<a href>` elements
561
+ `prefetchKey` (`":source"` scopes a prefetch to the originating page; the
562
+ router does so itself for an intercept target and for a `revalidate()` that
563
+ read the source page and kept a segment). Intercepted plain `<a href>` elements
563
564
  inside the router basename follow the same default; common static-resource
564
565
  extensions are excluded unless `data-prefetch="true"` identifies an application
565
566
  route, and `data-prefetch="false"` or `"none"` opts out unsafe GET links. A
@@ -228,6 +228,11 @@ suffixes are still appended.
228
228
  (`rango-state-fresh` by default), so it is not stored. A stale entry's
229
229
  background re-run sets no fresh-reads cookie (no visitor made that
230
230
  invalidation), so its invalidation does not keep the refreshed page out.
231
+ - A document that carries location state (`ctx.setLocationState()`, or the
232
+ one-shot cookie a document redirect left for it): it is the visitor's, so it
233
+ is not stored, leaves `private, no-store`, and a request that arrives with
234
+ that cookie, or whose earlier middleware set state, is not answered from a
235
+ stored entry (see `/hooks` `state.md`, "State on a document response")
231
236
  - Non-200 responses
232
237
  - A 200 whose render reported an error: a component that threw after the
233
238
  response started streaming, such as an async server component whose fetch