@rangojs/router 0.10.1 → 0.12.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 (62) hide show
  1. package/dist/testing/vitest.js +1 -1
  2. package/dist/types/browser/partial-update.d.ts +1 -0
  3. package/dist/types/client-urls/navigation.d.ts +11 -0
  4. package/dist/types/client-urls/revalidate-chain.d.ts +33 -0
  5. package/dist/types/client-urls/types.d.ts +41 -14
  6. package/dist/types/client.d.ts +2 -0
  7. package/dist/types/deps/rsc-client.d.ts +1 -0
  8. package/dist/types/deps/rsc.d.ts +1 -1
  9. package/dist/types/deps/ssr.d.ts +1 -1
  10. package/dist/types/index.d.ts +1 -1
  11. package/dist/types/index.rsc.d.ts +1 -1
  12. package/dist/types/router/is-action.d.ts +34 -0
  13. package/dist/types/rsc/types.d.ts +9 -9
  14. package/dist/types/ssr/index.d.ts +27 -4
  15. package/dist/types/testing/flight.d.ts +4 -3
  16. package/dist/types/testing/index.d.ts +3 -1
  17. package/dist/types/testing/run-client-revalidate.d.ts +43 -0
  18. package/dist/types/testing/to-url.d.ts +2 -0
  19. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +2 -0
  20. package/dist/types/testing/vitest.d.ts +2 -1
  21. package/dist/types/types/handler-context.d.ts +12 -5
  22. package/dist/types/types/index.d.ts +1 -1
  23. package/dist/types/vite/plugins/expose-action-id.d.ts +14 -0
  24. package/dist/types/vite/plugins/virtual-entries.d.ts +3 -2
  25. package/dist/vite/index.js +66 -13
  26. package/package.json +8 -3
  27. package/skills/client-urls/SKILL.md +26 -4
  28. package/skills/loader/SKILL.md +1 -0
  29. package/skills/testing/SKILL.md +1 -0
  30. package/skills/testing/setup.md +6 -6
  31. package/skills/typesafety/route-types.md +1 -0
  32. package/src/browser/partial-update.ts +11 -2
  33. package/src/browser/server-action-bridge.ts +9 -2
  34. package/src/cache/cache-runtime.ts +1 -1
  35. package/src/cache/segment-codec.ts +2 -2
  36. package/src/client-urls/navigation.ts +40 -42
  37. package/src/client-urls/revalidate-chain.ts +83 -0
  38. package/src/client-urls/types.ts +41 -13
  39. package/src/client.tsx +5 -0
  40. package/src/deps/rsc-client.ts +8 -0
  41. package/src/deps/rsc.ts +4 -2
  42. package/src/deps/ssr.ts +1 -0
  43. package/src/index.rsc.ts +1 -0
  44. package/src/index.ts +1 -0
  45. package/src/router/is-action.ts +100 -0
  46. package/src/router/revalidation.ts +5 -48
  47. package/src/rsc/handler.ts +3 -3
  48. package/src/rsc/server-action.ts +2 -4
  49. package/src/rsc/types.ts +9 -9
  50. package/src/ssr/index.tsx +132 -51
  51. package/src/testing/flight.ts +4 -3
  52. package/src/testing/index.ts +4 -1
  53. package/src/testing/run-client-revalidate.ts +108 -0
  54. package/src/testing/run-transition-when.ts +1 -3
  55. package/src/testing/to-url.ts +5 -0
  56. package/src/testing/vitest-stubs/plugin-rsc.ts +13 -5
  57. package/src/testing/vitest.ts +3 -2
  58. package/src/types/handler-context.ts +13 -5
  59. package/src/types/index.ts +1 -0
  60. package/src/vite/plugins/expose-action-id.ts +29 -1
  61. package/src/vite/plugins/use-cache-transform.ts +65 -1
  62. package/src/vite/plugins/virtual-entries.ts +14 -11
@@ -14,7 +14,7 @@ function rangoTestAliases(opts = {}) {
14
14
  replacement: here("src/testing/vitest-stubs/version.ts")
15
15
  },
16
16
  {
17
- find: /^@vitejs\/plugin-rsc\/rsc$/,
17
+ find: /^@vitejs\/plugin-rsc\/rsc(\/(server|client))?$/,
18
18
  replacement: here("src/testing/vitest-stubs/plugin-rsc.ts")
19
19
  }
20
20
  ];
@@ -52,6 +52,7 @@ export type UpdateMode = {
52
52
  } | {
53
53
  type: "action";
54
54
  interceptSourceUrl?: string;
55
+ actionId?: string;
55
56
  };
56
57
  /**
57
58
  * Type for the fetchPartialUpdate function
@@ -31,6 +31,17 @@ export declare function beginClientUrlNavigation(targetUrl: URL, signal: AbortSi
31
31
  export declare function collectClientRevalidationDecisions(options: {
32
32
  currentUrl: URL;
33
33
  nextUrl: URL;
34
+ /**
35
+ * True only when the decisions ride the action POST itself — the one
36
+ * request the server evaluates with actionContext (locked default true).
37
+ * Action-triggered refetch GETs (partial-update terminals) pass false:
38
+ * the server gives those navigation defaults, and the delta gate below
39
+ * must diff against the default the SERVER will use, or a force decision
40
+ * on the refetch would be silently swallowed as "equals default".
41
+ */
42
+ actionRequest: boolean;
43
+ /** Action TRUTH for the predicates' isAction() matcher; may be true on
44
+ * refetch GETs where actionRequest is false. */
34
45
  isAction: boolean;
35
46
  actionId?: string;
36
47
  stale: boolean;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The clientUrls revalidate() chain evaluator, shared by the browser
3
+ * collector (navigation.ts) and the public testing primitive
4
+ * (testing/run-client-revalidate.ts) so the two can never drift.
5
+ *
6
+ * Semantics mirror the server's evaluateRevalidation
7
+ * (src/router/revalidation.ts): a boolean verdict is a hard decision and
8
+ * short-circuits the rest of the chain; a `{ defaultShouldRevalidate }`
9
+ * object updates the running suggestion, which later predicates receive as
10
+ * their `defaultShouldRevalidate`; null/undefined defers; a throwing
11
+ * predicate fails open to the current suggestion (logged). One deliberate
12
+ * divergence: the object form is accepted only with a boolean value — the
13
+ * server is laxer, but never re-compares the value, while this decision
14
+ * feeds a strict-equality delta gate and the wire encoding
15
+ * (navigation.ts), where a truthy non-boolean would invert intent.
16
+ */
17
+ import type { ClientRevalidateArgs, ClientRevalidateFn } from "./types.js";
18
+ /**
19
+ * The locked default the server will apply to the request these decisions
20
+ * ride on. `actionRequest` is about the REQUEST, not the user gesture: only
21
+ * the action POST itself is evaluated server-side with actionContext
22
+ * (default `true`); the follow-up refetch GETs an action can trigger carry
23
+ * no actionContext and get navigation defaults — even though their
24
+ * predicates still see `isAction()` as true.
25
+ */
26
+ export declare function lockedClientDefault(options: {
27
+ actionRequest: boolean;
28
+ currentParams: Record<string, string>;
29
+ nextParams: Record<string, string>;
30
+ currentUrl: URL;
31
+ nextUrl: URL;
32
+ }): boolean;
33
+ export declare function runClientRevalidateChain(fns: readonly ClientRevalidateFn[], baseArgs: Omit<ClientRevalidateArgs, "defaultShouldRevalidate">, lockedDefault: boolean, label: string): boolean;
@@ -1,5 +1,5 @@
1
1
  import type { ComponentType, ReactNode } from "react";
2
- import type { LoaderDefinition, LoaderOptions, TransitionConfig } from "../types.js";
2
+ import type { IsActionFn, LoaderDefinition, LoaderOptions, TransitionConfig } from "../types.js";
3
3
  import type { TrieMatchResult } from "../router/trie-matching.js";
4
4
  import type { PathOptions } from "../urls/pattern-types.js";
5
5
  import type { SearchSchema } from "../search-params.js";
@@ -23,7 +23,8 @@ export type ClientLayoutFn = <const TItems extends ClientUrlItems>(component: Co
23
23
  * subset of the server ShouldRevalidateFn args — the predicate RUNS IN THE
24
24
  * BROWSER (it is declared in a "use client" module and never crosses the
25
25
  * projection boundary); only its decision is sent to the server. There is no
26
- * `context` — no server handler context exists where this executes.
26
+ * `context` — no server handler context exists where this executes. `isAction`
27
+ * is the same callable matcher as on the server, not a boolean.
27
28
  */
28
29
  export interface ClientRevalidateArgs {
29
30
  /** Full URL of the page being navigated away from (current location). */
@@ -35,20 +36,45 @@ export interface ClientRevalidateArgs {
35
36
  /** Route params for the navigation target (definition-local match). */
36
37
  readonly nextParams: Record<string, string>;
37
38
  /**
38
- * The locked default decision for this loader, computed client-side with
39
- * the same rules the server would apply: `true` on actions and when
40
- * params/search changed, `false` otherwise. Return it for default behavior
41
- * plus your own conditions.
39
+ * The current default decision for this loader, computed client-side with
40
+ * the same rules the server applies to the request the decisions ride on:
41
+ * `true` when they ride the action POST itself, otherwise `true` when
42
+ * params/search changed. (An action-triggered refetch GET gets navigation
43
+ * defaults — matching the server — even though `isAction()` is true.)
44
+ * Earlier predicates' `{ defaultShouldRevalidate }` verdicts thread into
45
+ * this value, exactly like the server chain. Return it for default
46
+ * behavior plus your own conditions.
42
47
  */
43
48
  readonly defaultShouldRevalidate: boolean;
44
49
  /** True when this is a stale history-entry background revalidation. */
45
50
  readonly stale: boolean;
46
- /** True when revalidation is triggered by a server action. */
47
- readonly isAction: boolean;
48
- /** The triggering server action's id, when isAction. */
51
+ /**
52
+ * Same {@link IsActionFn} the server `revalidate()` predicate receives.
53
+ * In the browser the match is against the action stub's hashed `$$id`
54
+ * (the id the action request carries) — not the RSC file-path `$id`.
55
+ */
56
+ readonly isAction: IsActionFn;
57
+ /**
58
+ * The triggering server action's id, when this is an action. In the
59
+ * browser this is the hashed `hash#export` form (`$$id`), not the RSC
60
+ * file-path `src/...#export`. Prefer `isAction(ref)` — a substring of
61
+ * `path#export` will not match here in production.
62
+ */
49
63
  readonly actionId?: string;
50
64
  }
51
- export type ClientRevalidateFn = (args: ClientRevalidateArgs) => boolean;
65
+ /**
66
+ * Client-run per-loader predicate, with the same chain semantics as the
67
+ * server's `revalidate()` (src/router/revalidation.ts): a boolean is a HARD
68
+ * decision that short-circuits the rest of the chain; a
69
+ * `{ defaultShouldRevalidate }` object updates the running suggestion, which
70
+ * later predicates receive as their `defaultShouldRevalidate`; `void` /
71
+ * `null` / `undefined` defers to the current suggestion — so
72
+ * `isAction(CartActions) || undefined` defers to the locked default.
73
+ * Predicates must be synchronous; the object form requires a boolean value.
74
+ */
75
+ export type ClientRevalidateFn = (args: ClientRevalidateArgs) => boolean | {
76
+ defaultShouldRevalidate: boolean;
77
+ } | null | void;
52
78
  export interface ClientUrlLoaderRecord {
53
79
  readonly loader: LoaderDefinition<any, any>;
54
80
  /** Client-run per-loader revalidation predicates; empty = locked defaults. */
@@ -114,10 +140,11 @@ export interface ClientUrlHelpers {
114
140
  readonly loading: (component: ReactNode) => ClientUrlItem;
115
141
  /**
116
142
  * Per-loader revalidation predicate, valid inside a loader() use callback
117
- * only. Runs IN THE BROWSER with client-computable args; return true to
118
- * re-run the loader, false to keep held data. Absent predicates (and
119
- * requests that carry no decisions: no-JS, PE, prefetch, document loads)
120
- * follow the locked server defaults.
143
+ * only. Runs IN THE BROWSER with client-computable args (including the
144
+ * callable `isAction(...refs)` matcher); return true to re-run the loader,
145
+ * false to keep held data. Absent predicates (and requests that carry no
146
+ * decisions: no-JS, PE, prefetch, document loads) follow the locked server
147
+ * defaults.
121
148
  */
122
149
  readonly revalidate: (fn: ClientRevalidateFn) => ClientUrlItem;
123
150
  /**
@@ -190,3 +190,5 @@ export { useHref } from "./browser/react/use-href.js";
190
190
  export { useReverse } from "./browser/react/use-reverse.js";
191
191
  export type { ScopedReverseFunction, LocalReverseFunction } from "./reverse.js";
192
192
  export type { LoaderDefinition } from "./types.js";
193
+ export type { ActionRef, IsActionFn } from "./types.js";
194
+ export type { ClientRevalidateArgs, ClientRevalidateFn, } from "./client-urls/types.js";
@@ -0,0 +1 @@
1
+ export { createFromReadableStream, encodeReply, createClientTemporaryReferenceSet, } from "@vitejs/plugin-rsc/rsc/client";
@@ -1 +1 @@
1
- export { renderToReadableStream, decodeReply, createTemporaryReferenceSet, loadServerAction, decodeAction, decodeFormState, } from "@vitejs/plugin-rsc/rsc";
1
+ export { renderToReadableStream, decodeReply, createTemporaryReferenceSet, loadServerAction, decodeAction, decodeFormState, } from "@vitejs/plugin-rsc/rsc/server";
@@ -1 +1 @@
1
- export { createFromReadableStream, setOnClientReference, } from "@vitejs/plugin-rsc/ssr";
1
+ export { createFromReadableStream, setOnClientReference, getClientEntryUrl, } from "@vitejs/plugin-rsc/ssr";
@@ -11,7 +11,7 @@
11
11
  */
12
12
  export { RouteNotFoundError, DataNotFoundError, notFound, MiddlewareError, HandlerError, BuildError, DslContextError, InvalidHandlerError, RouterError, Skip, isSkip, } from "./errors.js";
13
13
  export type { DocumentProps, DefaultEnv, RouteDefinition, RouteConfig, RouteDefinitionOptions, TrailingSlashMode, Handler, // Supports params object, path pattern, or route name
14
- HandlerContext, ExtractParams, GenericParams, Middleware, RevalidateParams, Revalidate, ActionRef, RouteKeys, LoaderDefinition, LoaderFn, LoaderContext, LoaderOptions, FetchableLoaderOptions, LoadOptions, ErrorInfo, ErrorBoundaryFallbackProps, ErrorBoundaryHandler, ClientErrorBoundaryFallbackProps, NotFoundInfo, NotFoundBoundaryFallbackProps, NotFoundBoundaryHandler, ErrorPhase, OnErrorContext, OnErrorCallback, } from "./types.js";
14
+ HandlerContext, ExtractParams, GenericParams, Middleware, RevalidateParams, Revalidate, ActionRef, IsActionFn, RouteKeys, LoaderDefinition, LoaderFn, LoaderContext, LoaderOptions, FetchableLoaderOptions, LoadOptions, ErrorInfo, ErrorBoundaryFallbackProps, ErrorBoundaryHandler, ClientErrorBoundaryFallbackProps, NotFoundInfo, NotFoundBoundaryFallbackProps, NotFoundBoundaryHandler, ErrorPhase, OnErrorContext, OnErrorCallback, } from "./types.js";
15
15
  export type { SearchSchema, SearchSchemaValue, ResolveSearchSchema, RouteSearchParams, RouteParams, } from "./search-params.js";
16
16
  export { TRACKING_SEARCH_PARAMS, type CacheSearchParams, } from "./cache/search-params-filter.js";
17
17
  export { createLoader } from "./loader.js";
@@ -9,7 +9,7 @@
9
9
  * in RSC context, while the regular index.ts is used in client components.
10
10
  */
11
11
  export { RouteNotFoundError, DataNotFoundError, notFound, MiddlewareError, HandlerError, BuildError, DslContextError, InvalidHandlerError, RouterError, Skip, isSkip, } from "./index.js";
12
- export type { DocumentProps, DefaultEnv, RouteDefinition, RouteConfig, RouteDefinitionOptions, TrailingSlashMode, Handler, HandlerContext, ExtractParams, GenericParams, Middleware, RevalidateParams, Revalidate, ActionRef, RouteKeys, LoaderDefinition, LoaderFn, LoaderContext, FetchableLoaderOptions, LoadOptions, ErrorInfo, ErrorBoundaryFallbackProps, ErrorBoundaryHandler, ClientErrorBoundaryFallbackProps, NotFoundInfo, NotFoundBoundaryFallbackProps, NotFoundBoundaryHandler, ErrorPhase, OnErrorContext, OnErrorCallback, TransitionConfig, TransitionWhenFn, TransitionWhenContext, ViewTransitionClass, } from "./types.js";
12
+ export type { DocumentProps, DefaultEnv, RouteDefinition, RouteConfig, RouteDefinitionOptions, TrailingSlashMode, Handler, HandlerContext, ExtractParams, GenericParams, Middleware, RevalidateParams, Revalidate, ActionRef, IsActionFn, RouteKeys, LoaderDefinition, LoaderFn, LoaderContext, FetchableLoaderOptions, LoadOptions, ErrorInfo, ErrorBoundaryFallbackProps, ErrorBoundaryHandler, ClientErrorBoundaryFallbackProps, NotFoundInfo, NotFoundBoundaryFallbackProps, NotFoundBoundaryHandler, ErrorPhase, OnErrorContext, OnErrorCallback, TransitionConfig, TransitionWhenFn, TransitionWhenContext, ViewTransitionClass, } from "./types.js";
13
13
  export type { RangoOptions, SSRStreamMode, SSROptions, ResolveStreamingContext, } from "./router.js";
14
14
  export type { OriginCheckConfig, OriginCheckContext, OriginCheckPhase, } from "./rsc/origin-guard.js";
15
15
  export type { ShellCaptureDebug, ShellCaptureDebugEvent, } from "./rsc/shell-capture.js";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared `isAction()` matcher for revalidate predicates (server + clientUrls).
3
+ *
4
+ * Action identity is `actionId`. The helper resolves an imported reference the
5
+ * same way the action boundary derives that id (`$id ?? $$id`), so a
6
+ * rename-safe `isAction(fn)` / `isAction(namespace)` match works in both the
7
+ * RSC environment (file-path `$id`) and the browser (hashed `$$id`).
8
+ */
9
+ import type { IsActionFn } from "../types.js";
10
+ /**
11
+ * Resolve a server-action reference's stable id, mirroring how the action
12
+ * boundary derives `actionContext.actionId` in `rsc/server-action.ts`
13
+ * (`$id ?? $$id`): the file-path `$id` set by the expose-action-id plugin in a
14
+ * production RSC build when present, otherwise React's `$$id`. Resolving both
15
+ * the incoming `actionId` and the reference with the same precedence makes
16
+ * `isAction()` form-agnostic across dev and production.
17
+ */
18
+ export declare function resolveActionRefId(ref: unknown): string | undefined;
19
+ /**
20
+ * Build the `isAction()` helper bound to the current action. Called with no
21
+ * arguments it answers "is this request an action at all?" — `true` during
22
+ * action handling (including action-triggered refetches that carry no id
23
+ * yet), `false` on plain navigation. Called with one or more action
24
+ * references it narrows to those: a single imported action, several
25
+ * (variadic), a namespace import (`import * as Mod`), an object literal of
26
+ * actions (`{ addToCart, removeFromCart }`), or a grouped namespace object.
27
+ * Returns `false` when there is no action or nothing matches.
28
+ *
29
+ * `inAction` is the request kind. It defaults to "an id is present" so
30
+ * existing server call sites stay a single argument. The client passes the
31
+ * explicit flag so a refetch terminal with `isAction: true` still answers
32
+ * bare `isAction()` even if `actionId` was not threaded.
33
+ */
34
+ export declare function makeIsAction(currentActionId: string | undefined, inAction?: boolean): IsActionFn;
@@ -107,37 +107,37 @@ export interface RscPayload {
107
107
  */
108
108
  export type ReactFormState = unknown;
109
109
  /**
110
- * RSC dependencies from @vitejs/plugin-rsc/rsc
110
+ * RSC dependencies from @vitejs/plugin-rsc/rsc/server
111
111
  */
112
112
  export interface RSCDependencies {
113
113
  /**
114
- * renderToReadableStream from @vitejs/plugin-rsc/rsc
114
+ * renderToReadableStream from @vitejs/plugin-rsc/rsc/server
115
115
  */
116
116
  renderToReadableStream: <T>(payload: T, options?: {
117
117
  temporaryReferences?: unknown;
118
118
  onError?: (error: unknown) => string | void;
119
119
  }) => ReadableStream<Uint8Array>;
120
120
  /**
121
- * decodeReply from @vitejs/plugin-rsc/rsc
121
+ * decodeReply from @vitejs/plugin-rsc/rsc/server
122
122
  */
123
123
  decodeReply: (body: FormData | string, options?: {
124
124
  temporaryReferences?: unknown;
125
125
  }) => Promise<unknown[]>;
126
126
  /**
127
- * createTemporaryReferenceSet from @vitejs/plugin-rsc/rsc
127
+ * createTemporaryReferenceSet from @vitejs/plugin-rsc/rsc/server
128
128
  */
129
129
  createTemporaryReferenceSet: () => unknown;
130
130
  /**
131
- * loadServerAction from @vitejs/plugin-rsc/rsc
131
+ * loadServerAction from @vitejs/plugin-rsc/rsc/server
132
132
  */
133
133
  loadServerAction: (actionId: string) => Promise<Function>;
134
134
  /**
135
- * decodeAction from @vitejs/plugin-rsc/rsc
135
+ * decodeAction from @vitejs/plugin-rsc/rsc/server
136
136
  * Decodes a FormData into a bound action function (for useActionState forms)
137
137
  */
138
138
  decodeAction: (body: FormData) => Promise<() => Promise<unknown>>;
139
139
  /**
140
- * decodeFormState from @vitejs/plugin-rsc/rsc
140
+ * decodeFormState from @vitejs/plugin-rsc/rsc/server
141
141
  * Decodes the action result into a ReactFormState for useActionState progressive enhancement
142
142
  */
143
143
  decodeFormState: (actionResult: unknown, body: FormData) => Promise<ReactFormState | null>;
@@ -268,8 +268,8 @@ export interface CreateRSCHandlerOptions<TEnv = unknown, TRoutes extends Record<
268
268
  */
269
269
  router: RangoInternal<TEnv, TRoutes>;
270
270
  /**
271
- * RSC dependencies from @vitejs/plugin-rsc/rsc.
272
- * Defaults to the exports from @vitejs/plugin-rsc/rsc.
271
+ * RSC dependencies from @vitejs/plugin-rsc/rsc/server.
272
+ * Defaults to the exports from @vitejs/plugin-rsc/rsc/server.
273
273
  */
274
274
  deps?: RSCDependencies;
275
275
  /**
@@ -117,10 +117,19 @@ export interface SSRDependencies<TEnv = unknown> {
117
117
  */
118
118
  injectRSCPayload: (rscStream: ReadableStream<Uint8Array>, options?: InjectRSCPayloadOptions) => TransformStream<Uint8Array, Uint8Array>;
119
119
  /**
120
- * Function to load bootstrap script content
121
- * Typically: () => import.meta.viteRsc.loadBootstrapScriptContent("index")
120
+ * Function to load bootstrap script content.
121
+ * Required unless `getClientEntryUrl` is provided with `headScripts: "preinit"`.
122
+ * Custom SSR entries typically: `() => import.meta.viteRsc.loadBootstrapScriptContent("index")`
123
+ * (deprecated in `@vitejs/plugin-rsc` 0.5.33 in favor of `getClientEntryUrl`).
122
124
  */
123
- loadBootstrapScriptContent: () => Promise<string>;
125
+ loadBootstrapScriptContent?: () => Promise<string>;
126
+ /**
127
+ * Client entry URL from `@vitejs/plugin-rsc/ssr` `getClientEntryUrl()`.
128
+ * Preferred when `headScripts` is `"preinit"`: Fizz receives `bootstrapModules`
129
+ * without the deprecated `loadBootstrapScriptContent` round-trip. Custom SSR
130
+ * entries can omit this and keep the inline bootstrap path.
131
+ */
132
+ getClientEntryUrl?: () => string;
124
133
  /**
125
134
  * Document script strategy; the generated virtual SSR entry threads the
126
135
  * `rango({ headScripts })` plugin option here (canonical docs on
@@ -251,7 +260,10 @@ interface ShellResumeOptions {
251
260
  * @example
252
261
  * ```tsx
253
262
  * import { createSSRHandler } from "@rangojs/router/ssr";
254
- * import { createFromReadableStream } from "@rangojs/router/internal/deps/ssr";
263
+ * import {
264
+ * createFromReadableStream,
265
+ * getClientEntryUrl,
266
+ * } from "@rangojs/router/internal/deps/ssr";
255
267
  * import { renderToReadableStream } from "react-dom/server.edge";
256
268
  * import { injectRSCPayload } from "@rangojs/router/internal/deps/html-stream-server";
257
269
  *
@@ -259,6 +271,17 @@ interface ShellResumeOptions {
259
271
  * createFromReadableStream,
260
272
  * renderToReadableStream,
261
273
  * injectRSCPayload,
274
+ * getClientEntryUrl,
275
+ * headScripts: "preinit", // getClientEntryUrl is only used under "preinit"
276
+ * });
277
+ * ```
278
+ *
279
+ * Custom SSR entries that still use the deprecated bootstrap helper:
280
+ * ```tsx
281
+ * export const renderHTML = createSSRHandler({
282
+ * createFromReadableStream,
283
+ * renderToReadableStream,
284
+ * injectRSCPayload,
262
285
  * loadBootstrapScriptContent: () =>
263
286
  * import.meta.viteRsc.loadBootstrapScriptContent("index"),
264
287
  * });
@@ -6,14 +6,15 @@
6
6
  * the same react-server-dom serializer the router uses at runtime. It runs in
7
7
  * plain node (no Vite, no browser), but ONLY under the `react-server` export
8
8
  * condition. The serializer is the VENDORED build shipped with
9
- * @vitejs/plugin-rsc — the public `@vitejs/plugin-rsc/rsc` entry top-level
10
- * imports Vite virtual modules and is not usable outside a Vite build.
9
+ * @vitejs/plugin-rsc — the public `@vitejs/plugin-rsc/rsc/server` entry
10
+ * top-level imports Vite virtual modules and is not usable outside a Vite
11
+ * build.
11
12
  *
12
13
  * Run the example/tests for this module via the dedicated rsc vitest project
13
14
  * (vitest.rsc.config.ts), which forces `--conditions=react-server` on the
14
15
  * worker. The main vitest project must NOT use that condition (it would flip
15
16
  * React to the no-hooks server build and break the ~50 tests that mock
16
- * @vitejs/plugin-rsc/rsc).
17
+ * @vitejs/plugin-rsc/rsc/server).
17
18
  *
18
19
  * Scope / limitations (v1):
19
20
  * - Server-only / leaf trees. A tree containing a CLIENT component emits an
@@ -26,7 +26,7 @@
26
26
  * condition and would throw if pulled into this barrel.
27
27
  *
28
28
  * Layers:
29
- * - Unit: runMiddleware, runLoader
29
+ * - Unit: runMiddleware, runLoader, runClientRevalidate
30
30
  * - Integration: dispatch (request -> Response)
31
31
  * - Cross-cut: assertCacheStatus, assertShellStatus, assertGeneratedRoutesMatch
32
32
  * - Component: see @rangojs/router/testing/dom (renderRoute)
@@ -39,6 +39,8 @@ export { runLoader, runLoaderResult } from "./run-loader.js";
39
39
  export type { RunLoaderOptions, RunLoaderResult, UseResolver, TestLoaderContext, } from "./run-loader.js";
40
40
  export { runTransitionWhen } from "./run-transition-when.js";
41
41
  export type { RunTransitionWhenOptions, RunTransitionWhenResult, } from "./run-transition-when.js";
42
+ export { runClientRevalidate } from "./run-client-revalidate.js";
43
+ export type { RunClientRevalidateOptions } from "./run-client-revalidate.js";
42
44
  export { dispatch } from "./dispatch.js";
43
45
  export type { DispatchOptions } from "./dispatch.js";
44
46
  export { assertCacheStatus, assertCacheDecision, parseCacheHeader, createCacheSink, filterCacheDecisions, } from "./cache-status.js";
@@ -0,0 +1,43 @@
1
+ /**
2
+ * runClientRevalidate — unit-test clientUrls() revalidate() predicates.
3
+ *
4
+ * Builds the same {@link ClientRevalidateArgs} the browser collector passes
5
+ * and evaluates the predicate(s) through the SAME chain evaluator production
6
+ * uses (client-urls/revalidate-chain.ts) — locked default, boolean
7
+ * short-circuit, soft-verdict threading, and fail-open are the production
8
+ * code paths, not a re-implementation. Pass an array to test a chain.
9
+ *
10
+ * Synchronous: client revalidate functions must be sync.
11
+ */
12
+ import type { ClientRevalidateFn } from "../client-urls/types.js";
13
+ /**
14
+ * Options for {@link runClientRevalidate}. Defaults model a same-URL
15
+ * navigation with no action (locked default `false`).
16
+ */
17
+ export interface RunClientRevalidateOptions {
18
+ currentUrl?: string | URL;
19
+ nextUrl?: string | URL;
20
+ currentParams?: Record<string, string>;
21
+ nextParams?: Record<string, string>;
22
+ stale?: boolean;
23
+ /**
24
+ * The triggering action: a single imported reference (id resolved via
25
+ * `$id ?? $$id`; throws if the function carries neither) or a raw actionId
26
+ * string. A namespace/object is rejected — it cannot identify the ONE
27
+ * action that triggered the request. Omit for a plain navigation.
28
+ */
29
+ action?: ((...args: never[]) => unknown) | string;
30
+ /**
31
+ * Model an action-triggered refetch GET: predicates see `isAction()` as
32
+ * true, but the locked default stays the navigation default, matching how
33
+ * the server evaluates that request (no actionContext). Defaults to
34
+ * treating a provided `action` as the action POST itself.
35
+ */
36
+ actionRequest?: boolean;
37
+ }
38
+ /**
39
+ * Run one clientUrls `revalidate()` predicate — or a chain, in declaration
40
+ * order — against production-built args. Returns the final boolean decision
41
+ * (locked default if every predicate defers or throws).
42
+ */
43
+ export declare function runClientRevalidate(fn: ClientRevalidateFn | readonly ClientRevalidateFn[], opts?: RunClientRevalidateOptions): boolean;
@@ -0,0 +1,2 @@
1
+ /** Shared string|URL coercion for the testing primitives. */
2
+ export declare function toURL(value: string | URL | undefined, fallback: URL): URL;
@@ -5,3 +5,5 @@ export declare const decodeReply: () => undefined;
5
5
  export declare const decodeAction: () => undefined;
6
6
  export declare const decodeFormState: () => undefined;
7
7
  export declare const createTemporaryReferenceSet: () => Record<string, never>;
8
+ export declare const encodeReply: () => never;
9
+ export declare const createClientTemporaryReferenceSet: () => Record<string, never>;
@@ -18,7 +18,8 @@
18
18
  * `@rangojs/router` specifier to its react-server entry (real impls) while
19
19
  * leaving React as the client build — which is exactly what this helper does.
20
20
  * - The build-only `@rangojs/router:version` virtual and `@vitejs/plugin-rsc/rsc`
21
- * (whose real body imports unresolvable Vite virtuals) are stubbed.
21
+ * plus `/rsc/server`, `/rsc/client` (whose real body imports unresolvable
22
+ * Vite virtuals) are stubbed.
22
23
  * - Cloudflare apps additionally import the `cloudflare:workers` /
23
24
  * `cloudflare:email` runtime virtuals; pass `{ preset: "cloudflare" }` to stub them.
24
25
  *
@@ -418,13 +418,17 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<S
418
418
  /**
419
419
  * A reference to a server action, used by `isAction()` in a revalidate predicate.
420
420
  *
421
- * Either a directly imported action (`import { addToCart }`) or a namespace
422
- * import of an action module (`import * as CartActions`). Matching resolves the
421
+ * Either a directly imported action (`import { addToCart }`), a namespace
422
+ * import of an action module (`import * as CartActions`), an object
423
+ * literal of actions (`{ addToCart, removeFromCart }`), or a grouped
424
+ * namespace (`{ Cart: CartActions, Order: OrderActions }`). Matching resolves the
423
425
  * action's build-injected id (`path#export`) — the same identity the router uses
424
426
  * for `actionId` — so a renamed or moved action breaks at compile time instead
425
427
  * of silently failing to match.
426
428
  */
427
429
  export type ActionRef = ((...args: never[]) => unknown) | Record<string, unknown>;
430
+ /** The `isAction()` matcher passed to server and client `revalidate()` predicates. */
431
+ export type IsActionFn = (...actions: ActionRef[]) => boolean;
428
432
  /**
429
433
  * Revalidation function called during client-side navigation to decide whether
430
434
  * a segment (layout, route, parallel slot, or loader) should be re-rendered.
@@ -511,8 +515,10 @@ export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
511
515
  /**
512
516
  * Typed, rename-safe action matching. Returns `true` when the action that
513
517
  * triggered this revalidation is one of the given references — or, for a
514
- * namespace import (`import * as CartActions`), any export of that module —
515
- * and `false` otherwise (including plain navigation with no action).
518
+ * namespace import (`import * as CartActions`), object literal
519
+ * (`{ addToCart, removeFromCart }`), or grouped namespaces
520
+ * (`{ Cart: CartActions }`), any of those exports — and `false`
521
+ * otherwise (including plain navigation with no action).
516
522
  *
517
523
  * Called with NO arguments it answers "is this request an action at all?":
518
524
  * `true` for any action, `false` on plain navigation. Use the bare form when
@@ -536,9 +542,10 @@ export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
536
542
  * revalidate((ctx) => ctx.isAction(addToCart) || undefined); // one action
537
543
  * revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
538
544
  * revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any in the module
545
+ * revalidate((ctx) => ctx.isAction({ addToCart, removeFromCart }) || undefined); // object form
539
546
  * ```
540
547
  */
541
- isAction: (...actions: ActionRef[]) => boolean;
548
+ isAction: IsActionFn;
542
549
  /** URL where the action was executed (the page the user was on when they triggered the action). */
543
550
  actionUrl?: URL;
544
551
  /** Return value from the action execution. Can be used to conditionally revalidate based on the action's outcome. */
@@ -3,7 +3,7 @@ import "./global-namespace.js";
3
3
  export type { DocumentProps, ExtractParams, TrailingSlashMode, RouteConfig, RouteDefinitionOptions, RouteDefinition, ResolvedRouteMap, } from "./route-config.js";
4
4
  export type { ErrorInfo, ErrorBoundaryFallbackProps, ErrorBoundaryHandler, ClientErrorBoundaryFallbackProps, LoaderDataResult, NotFoundInfo, NotFoundBoundaryFallbackProps, NotFoundBoundaryHandler, } from "./boundaries.js";
5
5
  export { isLoaderDataResult } from "./boundaries.js";
6
- export type { MiddlewareFn, ScopedRouteMap, Handler, HandlerContext, InternalHandlerContext, GenericParams, RevalidateParams, ShouldRevalidateFn, ActionRef, RouteKeys, ExtractRouteParams, HandlersForRouteMap, Revalidate, Middleware, } from "./handler-context.js";
6
+ export type { MiddlewareFn, ScopedRouteMap, Handler, HandlerContext, InternalHandlerContext, GenericParams, RevalidateParams, ShouldRevalidateFn, ActionRef, IsActionFn, RouteKeys, ExtractRouteParams, HandlersForRouteMap, Revalidate, Middleware, } from "./handler-context.js";
7
7
  export type { ViewTransitionClass, TransitionConfig, TransitionWhenFn, TransitionWhenContext, ResolvedSegment, SegmentMetadata, SlotState, RootLayoutProps, MatchResult, } from "./segments.js";
8
8
  export type { LazyIncludeContext, RouteEntry } from "./route-entry.js";
9
9
  export type { LoaderContext, LoaderFn, FetchableLoaderOptions, LoaderOptions, LoadOptions, LoaderDefinition, } from "./loader-types.js";
@@ -1,4 +1,18 @@
1
1
  import type { Plugin } from "vite";
2
+ /**
3
+ * Per-reference own `bind` injected next to `$$id`. React's client.browser
4
+ * build carries no server-reference metadata across `.bind()` (the
5
+ * edge/node/server builds install an own `bind` on each reference that
6
+ * does), so `isAction(boundStub)` would silently miss in the browser only.
7
+ * Installing the same per-reference own `bind` here — scoped to the stubs
8
+ * this plugin already wraps, guarded to never override an existing own
9
+ * `bind` — closes that without mutating the global Function.prototype
10
+ * (which would re-wrap once per Vite environment/HMR pass and break
11
+ * native-function detection for co-loaded code). The helper re-installs
12
+ * itself on the bound result so chained binds keep the metadata too.
13
+ */
14
+ export declare const ACTION_BIND_HELPER_NAME: string;
15
+ export declare const ACTION_BIND_HELPER_SOURCE: string;
2
16
  /**
3
17
  * Vite plugin that exposes action IDs on server reference functions.
4
18
  *
@@ -3,8 +3,9 @@ export declare const VIRTUAL_ENTRY_BROWSER: string;
3
3
  /**
4
4
  * Generate the virtual SSR entry. `headScripts` mirrors the rango() plugin
5
5
  * option: "preinit" (default) installs the client-reference preinit hook and
6
- * lets the SSR handlers convert the bootstrap to `bootstrapModules`;
7
- * "preload" omits the hook and pins the handlers to the hint-only strategy.
6
+ * threads `getClientEntryUrl` so Fizz emits `bootstrapModules`;
7
+ * "preload" omits the hook and uses the deprecated inline
8
+ * `loadBootstrapScriptContent` bootstrap.
8
9
  */
9
10
  export declare function getVirtualEntrySSR(headScripts?: HeadScriptsOption, progressiveChunkSize?: number): string;
10
11
  /**