@rangojs/router 0.24.4 → 0.25.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 +1 -0
- package/dist/bin/rango.js +363 -39
- package/dist/testing/vitest.js +37 -4
- package/dist/types/browser/event-controller.d.ts +24 -5
- package/dist/types/browser/intercept-utils.d.ts +5 -2
- package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
- package/dist/types/browser/navigation-transaction.d.ts +5 -5
- package/dist/types/browser/partial-update.d.ts +21 -13
- package/dist/types/browser/prefetch/cache.d.ts +8 -0
- package/dist/types/browser/rango-marker.d.ts +7 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
- package/dist/types/browser/react/use-handle.d.ts +20 -0
- package/dist/types/browser/react/use-href.d.ts +6 -0
- package/dist/types/browser/react/use-router.d.ts +6 -1
- package/dist/types/browser/response-adapter.d.ts +5 -3
- package/dist/types/browser/scroll-restoration.d.ts +9 -3
- package/dist/types/browser/segment-reconciler.d.ts +10 -13
- package/dist/types/browser/types.d.ts +38 -8
- package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/types.d.ts +8 -0
- package/dist/types/client-urls/client-urls.d.ts +6 -2
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +9 -5
- package/dist/types/client-urls/types.d.ts +29 -16
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.rsc.d.ts +2 -2
- package/dist/types/decode-loader-results.d.ts +28 -7
- package/dist/types/href-client.d.ts +3 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +2 -3
- package/dist/types/outlet-context.d.ts +7 -6
- package/dist/types/prefetch-markers.d.ts +14 -5
- package/dist/types/route-content-wrapper.d.ts +17 -14
- package/dist/types/route-definition/helpers-types.d.ts +7 -2
- package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
- package/dist/types/route-definition/rewrite.d.ts +48 -5
- package/dist/types/route-types.d.ts +2 -2
- package/dist/types/router/handler-context.d.ts +14 -2
- package/dist/types/router/intercept-resolution.d.ts +19 -46
- package/dist/types/router/lazy-includes.d.ts +7 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
- package/dist/types/router/middleware.d.ts +0 -16
- package/dist/types/router/navigation-snapshot.d.ts +2 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
- package/dist/types/router/revalidation.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +4 -16
- package/dist/types/router/router-interfaces.d.ts +0 -6
- package/dist/types/router/router-options.d.ts +4 -7
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
- package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
- package/dist/types/router/segment-resolution.d.ts +1 -1
- package/dist/types/router/segment-wrappers.d.ts +3 -5
- package/dist/types/router/substitute-pattern-params.d.ts +3 -2
- package/dist/types/router/tracing.d.ts +2 -2
- package/dist/types/rsc/full-payload.d.ts +14 -2
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/manifest-init.d.ts +20 -0
- package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
- package/dist/types/rsc/rewrite-passes.d.ts +12 -2
- package/dist/types/rsc/server-action.d.ts +6 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
- package/dist/types/rsc/shell-capture.d.ts +7 -0
- package/dist/types/rsc/types.d.ts +5 -0
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/context.d.ts +64 -4
- package/dist/types/server/request-context.d.ts +41 -4
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
- package/dist/types/testing/render-route.d.ts +21 -11
- package/dist/types/testing/run-loader.d.ts +7 -0
- package/dist/types/testing/run-middleware.d.ts +7 -0
- package/dist/types/testing/run-revalidate.d.ts +5 -4
- package/dist/types/testing/serve-shell-request.d.ts +41 -4
- package/dist/types/testing/shell-status.d.ts +17 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +10 -1
- package/dist/types/thenable-status.d.ts +0 -6
- package/dist/types/types/handler-context.d.ts +12 -1
- package/dist/types/types/loader-types.d.ts +4 -3
- package/dist/types/types/route-entry.d.ts +2 -1
- package/dist/types/urls/path-helper-types.d.ts +4 -2
- package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
- package/dist/types/vite/discovery/state.d.ts +20 -0
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
- package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
- package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
- package/dist/vite/index.js +6816 -6547
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +255 -0
- package/skills/app-structure/content-site.md +333 -0
- package/skills/app-structure/e-commerce.md +435 -0
- package/skills/app-structure/large-app.md +885 -0
- package/skills/app-structure/multi-app.md +280 -0
- package/skills/breadcrumbs/SKILL.md +6 -1
- package/skills/caching/SKILL.md +3 -1
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +185 -32
- package/skills/composability/SKILL.md +10 -8
- package/skills/handler-use/SKILL.md +48 -9
- package/skills/hooks/handle-and-actions.md +104 -0
- package/skills/hooks/navigation.md +24 -10
- package/skills/hooks/urls.md +1 -1
- package/skills/i18n/SKILL.md +2 -1
- package/skills/intercept/SKILL.md +60 -29
- package/skills/layout/SKILL.md +6 -3
- package/skills/links/SKILL.md +151 -17
- package/skills/loader/SKILL.md +32 -10
- package/skills/middleware/SKILL.md +111 -15
- package/skills/observability/SKILL.md +11 -0
- package/skills/parallel/SKILL.md +5 -5
- package/skills/ppr/SKILL.md +43 -8
- package/skills/prerender/SKILL.md +1 -2
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/scripts/SKILL.md +12 -1
- package/skills/server-actions/SKILL.md +60 -20
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +13 -10
- package/skills/testing/loader.md +25 -24
- package/skills/testing/middleware.md +11 -11
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +85 -21
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/view-transitions/SKILL.md +43 -10
- package/src/__augment-tests__/augmented.check.ts +28 -0
- package/src/browser/event-controller.ts +44 -18
- package/src/browser/intercept-utils.ts +3 -2
- package/src/browser/merge-segment-loaders.ts +0 -85
- package/src/browser/navigation-bridge.ts +112 -84
- package/src/browser/navigation-client.ts +20 -15
- package/src/browser/navigation-store.ts +4 -2
- package/src/browser/navigation-transaction.ts +9 -8
- package/src/browser/partial-update.ts +90 -32
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +13 -3
- package/src/browser/rango-marker.ts +13 -0
- package/src/browser/react/NavigationProvider.tsx +25 -7
- package/src/browser/react/filter-segment-order.ts +13 -0
- package/src/browser/react/use-action.ts +9 -15
- package/src/browser/react/use-handle.ts +63 -71
- package/src/browser/react/use-href.tsx +24 -5
- package/src/browser/react/use-link-status.ts +1 -1
- package/src/browser/react/use-navigation.ts +30 -40
- package/src/browser/react/use-router.ts +37 -6
- package/src/browser/react/use-segments.ts +8 -12
- package/src/browser/response-adapter.ts +11 -5
- package/src/browser/rsc-router.tsx +3 -1
- package/src/browser/scroll-restoration.ts +34 -20
- package/src/browser/segment-reconciler.ts +11 -29
- package/src/browser/server-action-bridge.ts +52 -16
- package/src/browser/types.ts +40 -8
- package/src/build/generate-manifest.ts +12 -1
- package/src/build/route-types/codegen.ts +6 -2
- package/src/build/route-types/per-module-writer.ts +94 -61
- package/src/cache/cache-runtime.ts +9 -0
- package/src/cache/cache-scope.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +2 -0
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/types.ts +8 -0
- package/src/cache/vercel/vercel-cache-store.ts +6 -1
- package/src/client-urls/client-urls.ts +202 -153
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +137 -66
- package/src/client-urls/types.ts +30 -15
- package/src/client.rsc.tsx +11 -1
- package/src/client.tsx +9 -1
- package/src/decode-loader-results.ts +64 -31
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -4
- package/src/loader-redirect.tsx +2 -3
- package/src/outlet-context.ts +7 -6
- package/src/prefetch-markers.ts +15 -5
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +27 -7
- package/src/route-definition/helpers-types.ts +7 -2
- package/src/route-definition/resolve-handler-use.ts +23 -10
- package/src/route-definition/rewrite.ts +86 -19
- package/src/route-types.ts +1 -2
- package/src/router/find-match.ts +37 -1
- package/src/router/handler-context.ts +85 -40
- package/src/router/intercept-resolution.ts +161 -307
- package/src/router/lazy-includes.ts +43 -27
- package/src/router/loader-resolution.ts +21 -1
- package/src/router/manifest.ts +6 -8
- package/src/router/match-api.ts +11 -1
- package/src/router/match-middleware/background-revalidation.ts +2 -4
- package/src/router/match-middleware/cache-lookup.ts +16 -20
- package/src/router/match-middleware/intercept-resolution.ts +41 -60
- package/src/router/match-result.ts +31 -2
- package/src/router/middleware.ts +6 -147
- package/src/router/navigation-snapshot.ts +38 -2
- package/src/router/prefetch-cache-ttl.ts +10 -18
- package/src/router/prerender-match.ts +2 -1
- package/src/router/revalidation.ts +24 -3
- package/src/router/router-context.ts +5 -18
- package/src/router/router-interfaces.ts +0 -7
- package/src/router/router-options.ts +4 -7
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
- package/src/router/segment-resolution/revalidation.ts +75 -35
- package/src/router/segment-resolution.ts +0 -1
- package/src/router/segment-wrappers.ts +8 -13
- package/src/router/substitute-pattern-params.ts +3 -2
- package/src/router/tracing.ts +2 -2
- package/src/router.ts +2 -9
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +83 -41
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/manifest-init.ts +56 -3
- package/src/rsc/prefetch-render-error.ts +38 -0
- package/src/rsc/progressive-enhancement.ts +161 -211
- package/src/rsc/rewrite-passes.ts +56 -9
- package/src/rsc/rsc-rendering.ts +79 -23
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture-constants.ts +22 -0
- package/src/rsc/shell-capture.ts +11 -0
- package/src/rsc/types.ts +5 -0
- package/src/segment-system.tsx +125 -119
- package/src/server/context.ts +96 -5
- package/src/server/handle-store.ts +3 -2
- package/src/server/request-context.ts +42 -3
- package/src/server/request-headers.ts +32 -0
- package/src/server/rewrite-trail.ts +55 -9
- package/src/ssr/ssr-root.tsx +6 -10
- package/src/testing/dispatch.ts +4 -5
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/internal/client-urls-projection.ts +31 -11
- package/src/testing/render-route.tsx +83 -40
- package/src/testing/run-loader.ts +26 -2
- package/src/testing/run-middleware.ts +10 -0
- package/src/testing/run-revalidate.ts +13 -6
- package/src/testing/serve-shell-request.ts +77 -4
- package/src/testing/shell-status.ts +40 -0
- package/src/testing/vitest-stubs/version.ts +1 -0
- package/src/testing/vitest.ts +74 -7
- package/src/thenable-status.ts +0 -11
- package/src/types/handler-context.ts +12 -1
- package/src/types/loader-types.ts +4 -3
- package/src/types/route-entry.ts +2 -1
- package/src/urls/include-helper.ts +38 -11
- package/src/urls/path-helper-types.ts +4 -2
- package/src/vite/discovery/client-urls-projection.ts +205 -16
- package/src/vite/discovery/discover-routers.ts +2 -2
- package/src/vite/discovery/prerender-collection.ts +2 -6
- package/src/vite/discovery/state.ts +26 -0
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
- package/src/vite/plugins/expose-internal-ids.ts +46 -40
- package/src/vite/plugins/hoist/source.ts +8 -7
- package/src/vite/plugins/transition-when-hoist.ts +1 -0
- package/src/vite/plugins/version-plugin.ts +7 -0
- package/src/vite/plugins/version.d.ts +7 -0
- package/src/vite/plugins/virtual-entries.ts +20 -0
- package/src/vite/rango.ts +4 -1
- package/src/vite/router-discovery.ts +39 -7
- package/src/vite/utils/prerender-utils.ts +17 -0
|
@@ -8,8 +8,9 @@ import type { ReactNode } from "react";
|
|
|
8
8
|
import * as React from "react";
|
|
9
9
|
import { startTransition } from "react";
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
|
|
11
|
+
// React 19.3+ and experimental builds. Call it only inside startTransition:
|
|
12
|
+
// outside one, React starts a transition of its own.
|
|
13
|
+
export const addTransitionType: ((type: string) => void) | undefined =
|
|
13
14
|
"addTransitionType" in React ? (React as any).addTransitionType : undefined;
|
|
14
15
|
|
|
15
16
|
import type { RenderSegmentsOptions } from "../segment-system.js";
|
|
@@ -32,7 +33,12 @@ import {
|
|
|
32
33
|
IS_BROWSER_DEBUG,
|
|
33
34
|
} from "./logging.js";
|
|
34
35
|
import { validatePayloadRedirect } from "./validate-redirect-origin.js";
|
|
35
|
-
import type {
|
|
36
|
+
import type {
|
|
37
|
+
HandleData,
|
|
38
|
+
NavigationUpdate,
|
|
39
|
+
RscMetadata,
|
|
40
|
+
ShownEntry,
|
|
41
|
+
} from "./types.js";
|
|
36
42
|
import { OPTIMISTIC_COMMIT_TRANSITION_TYPE } from "./optimistic-commit.js";
|
|
37
43
|
import type { LoaderStore } from "../loader-store.js";
|
|
38
44
|
import { setActiveInterceptTargets } from "../client-urls/navigation.js";
|
|
@@ -45,6 +51,7 @@ import { buildHistoryState } from "./history-state.js";
|
|
|
45
51
|
import { addLocationState } from "./react/location-state-shared.js";
|
|
46
52
|
import { isPlaceholder, type TransitionWhenKind } from "../types/segments.js";
|
|
47
53
|
import { auditTreeCause, type TreeUpdateCause } from "../suspense-audit.js";
|
|
54
|
+
import { revalidationErrorOf } from "../decode-loader-results.js";
|
|
48
55
|
|
|
49
56
|
/**
|
|
50
57
|
* Hold/animate decision for a commit: a non-intercept segment carries a
|
|
@@ -79,19 +86,24 @@ function treeCause(mode: UpdateMode, traversal?: boolean): TreeUpdateCause {
|
|
|
79
86
|
* settled/cached segments make it a no-op. Urgent commits call onUpdate
|
|
80
87
|
* directly: nothing is held there, a reader of a pending stream suspends to
|
|
81
88
|
* its fallback. Also used by renderRoute's navigate() (testing/render-route.tsx).
|
|
89
|
+
* Every commit is tagged with its transition({ when }) `kind` (#1165), then
|
|
90
|
+
* `transitionTypes`.
|
|
82
91
|
*/
|
|
83
92
|
export function commitInTransition(
|
|
84
93
|
loaders: LoaderStore,
|
|
85
94
|
onUpdate: UpdateSubscriber,
|
|
86
95
|
segments: readonly ResolvedSegment[],
|
|
87
96
|
update: NavigationUpdate,
|
|
97
|
+
kind: TransitionWhenKind,
|
|
88
98
|
transitionTypes: readonly string[],
|
|
89
99
|
): void {
|
|
90
100
|
startTransition(() => {
|
|
91
101
|
loaders.announcePendingStreams(segments);
|
|
92
102
|
if (addTransitionType) {
|
|
103
|
+
addTransitionType(kind);
|
|
93
104
|
for (const type of transitionTypes) addTransitionType(type);
|
|
94
105
|
}
|
|
106
|
+
update.inTransition = true;
|
|
95
107
|
onUpdate(update);
|
|
96
108
|
});
|
|
97
109
|
}
|
|
@@ -124,13 +136,15 @@ export interface PartialUpdateConfig {
|
|
|
124
136
|
/** What the response's handlers pushed, to the handle store. */
|
|
125
137
|
handles(metadata: RscMetadata): void;
|
|
126
138
|
/**
|
|
127
|
-
* Deliver the navigation state to its readers now. A page
|
|
128
|
-
* holes commits in the task of its click, before the
|
|
129
|
-
* click has gone out (event-controller.ts
|
|
130
|
-
* React holds that commit where a hole is
|
|
131
|
-
* this useNavigation() reads idle, on the
|
|
132
|
-
* wait.
|
|
133
|
-
* handed over, and stays while React holds it,
|
|
139
|
+
* Deliver the navigation state to its readers now, and commit it. A page
|
|
140
|
+
* built with holes commits in the task of its click, before the
|
|
141
|
+
* notification of the click has gone out (event-controller.ts
|
|
142
|
+
* makeDebouncedNotifier), and React holds that commit where a hole is
|
|
143
|
+
* read with no boundary: without this useNavigation() reads idle, on the
|
|
144
|
+
* page being left, for the whole wait. Committed first, `loading` is on
|
|
145
|
+
* screen when the commit is handed over, and stays while React holds it,
|
|
146
|
+
* as for a plain click. Delivered at default priority it joins the
|
|
147
|
+
* commit's urgent batch, which brings idle with the URL, and never shows.
|
|
134
148
|
*/
|
|
135
149
|
flush(): void;
|
|
136
150
|
};
|
|
@@ -185,8 +199,8 @@ export interface CommitOverrides {
|
|
|
185
199
|
interceptSourceUrl?: string;
|
|
186
200
|
/** Server-set location state to merge into history.pushState */
|
|
187
201
|
serverState?: Record<string, unknown>;
|
|
188
|
-
/** The
|
|
189
|
-
|
|
202
|
+
/** The payload's metadata: its route name and rewrite target, remembered per history entry. */
|
|
203
|
+
metadata?: ShownEntry;
|
|
190
204
|
}
|
|
191
205
|
|
|
192
206
|
/**
|
|
@@ -233,7 +247,9 @@ export type UpdateMode =
|
|
|
233
247
|
};
|
|
234
248
|
|
|
235
249
|
/**
|
|
236
|
-
* Type for the fetchPartialUpdate function
|
|
250
|
+
* Type for the fetchPartialUpdate function. In `action` mode it resolves with
|
|
251
|
+
* the error of a loader that threw while it re-ran (revalidationErrorOf);
|
|
252
|
+
* the loader throws at its read.
|
|
237
253
|
*/
|
|
238
254
|
export type PartialUpdater = (
|
|
239
255
|
targetUrl: string,
|
|
@@ -242,7 +258,7 @@ export type PartialUpdater = (
|
|
|
242
258
|
signal: AbortSignal | undefined,
|
|
243
259
|
tx: BoundTransaction,
|
|
244
260
|
mode?: UpdateMode,
|
|
245
|
-
) => Promise<
|
|
261
|
+
) => Promise<Error | undefined>;
|
|
246
262
|
|
|
247
263
|
export function createPartialUpdater(
|
|
248
264
|
config: PartialUpdateConfig,
|
|
@@ -545,7 +561,7 @@ export function createPartialUpdater(
|
|
|
545
561
|
signal: AbortSignal | undefined,
|
|
546
562
|
tx: BoundTransaction,
|
|
547
563
|
mode: UpdateMode = { type: "navigate" },
|
|
548
|
-
): Promise<
|
|
564
|
+
): Promise<Error | undefined> {
|
|
549
565
|
const segmentState = store.getSegmentState();
|
|
550
566
|
const url = targetUrl || window.location.href;
|
|
551
567
|
|
|
@@ -666,6 +682,11 @@ export function createPartialUpdater(
|
|
|
666
682
|
targetUrl: url,
|
|
667
683
|
segmentIds: segments,
|
|
668
684
|
previousUrl,
|
|
685
|
+
// The entry being left (a popstate has not moved it yet), unless
|
|
686
|
+
// previousUrl names an intercept's source.
|
|
687
|
+
rewriteTarget: interceptSourceUrl
|
|
688
|
+
? undefined
|
|
689
|
+
: store.getHistoryEntryMemory()?.rewriteTarget,
|
|
669
690
|
staleRevalidation:
|
|
670
691
|
mode.type === "stale-revalidation" || segments.length === 0,
|
|
671
692
|
version: getVersion(),
|
|
@@ -755,7 +776,7 @@ export function createPartialUpdater(
|
|
|
755
776
|
const { scroll: commitScroll } = tx.commit(
|
|
756
777
|
matchedIds,
|
|
757
778
|
existingSegments,
|
|
758
|
-
{
|
|
779
|
+
{ metadata: payload.metadata },
|
|
759
780
|
);
|
|
760
781
|
if (cachedHandleData) {
|
|
761
782
|
store.updateCacheHandleData(
|
|
@@ -781,6 +802,7 @@ export function createPartialUpdater(
|
|
|
781
802
|
onUpdate,
|
|
782
803
|
existingSegments,
|
|
783
804
|
cachedUpdate,
|
|
805
|
+
whenKind,
|
|
784
806
|
["navigation"],
|
|
785
807
|
);
|
|
786
808
|
} else {
|
|
@@ -804,7 +826,7 @@ export function createPartialUpdater(
|
|
|
804
826
|
const { scroll: leaveScroll } = tx.commit(
|
|
805
827
|
matchedIds,
|
|
806
828
|
existingSegments,
|
|
807
|
-
{
|
|
829
|
+
{ metadata: payload.metadata },
|
|
808
830
|
);
|
|
809
831
|
|
|
810
832
|
if (process.env.NODE_ENV !== "production") {
|
|
@@ -824,7 +846,7 @@ export function createPartialUpdater(
|
|
|
824
846
|
"[Browser] No changes - all revalidations returned false, keeping existing UI",
|
|
825
847
|
);
|
|
826
848
|
tx.commit(matchedIds, existingSegments, {
|
|
827
|
-
|
|
849
|
+
metadata: payload.metadata,
|
|
828
850
|
treeless: true,
|
|
829
851
|
});
|
|
830
852
|
debugLog("[Browser] Navigation complete (no re-render)");
|
|
@@ -910,6 +932,13 @@ export function createPartialUpdater(
|
|
|
910
932
|
// prefetch is decided against the real source.
|
|
911
933
|
const gatedOff = decideGatedOff(reconciled.segments, payload.metadata);
|
|
912
934
|
|
|
935
|
+
const isInterceptResponse = hasActiveInterceptSlots(
|
|
936
|
+
payload.metadata?.slots,
|
|
937
|
+
);
|
|
938
|
+
// The source the commit records for an intercept, and its fill's.
|
|
939
|
+
const effectiveInterceptSource =
|
|
940
|
+
interceptSourceUrl || segmentState.currentUrl;
|
|
941
|
+
|
|
913
942
|
// The fill: one request for what the prefetch deferred, listing what
|
|
914
943
|
// the client holds once this payload commits. It leaves now, before
|
|
915
944
|
// the tree is rendered, with this navigation's signal.
|
|
@@ -921,8 +950,15 @@ export function createPartialUpdater(
|
|
|
921
950
|
answer: client.fetchPartial({
|
|
922
951
|
targetUrl: url,
|
|
923
952
|
segmentIds: heldIds,
|
|
924
|
-
// The page the fill completes is the page it is on
|
|
925
|
-
|
|
953
|
+
// The page the fill completes is the page it is on: from the
|
|
954
|
+
// target itself nothing intercepts, so an intercept's fill names
|
|
955
|
+
// its source, as a request from the open modal does.
|
|
956
|
+
previousUrl: isInterceptResponse ? effectiveInterceptSource : url,
|
|
957
|
+
...(isInterceptResponse && {
|
|
958
|
+
interceptSourceUrl: effectiveInterceptSource,
|
|
959
|
+
}),
|
|
960
|
+
// The server ignores it beside an intercept source.
|
|
961
|
+
rewriteTarget: payload.metadata?.rewriteTarget,
|
|
926
962
|
fill: true,
|
|
927
963
|
signal,
|
|
928
964
|
version: getVersion(),
|
|
@@ -1029,15 +1065,19 @@ export function createPartialUpdater(
|
|
|
1029
1065
|
newTree = await renderSegments(reconciled.mainSegments, renderOptions);
|
|
1030
1066
|
}
|
|
1031
1067
|
|
|
1068
|
+
// A background refresh keeps the stale page on screen when a loader it
|
|
1069
|
+
// re-ran threw (its entries settled in the build): nothing is committed,
|
|
1070
|
+
// and navigation-bridge logs the rejection (dc-t65). Every other lane
|
|
1071
|
+
// throws it at the read. Before the abort check: the await yields.
|
|
1072
|
+
if (mode.type === "stale-revalidation") {
|
|
1073
|
+
const failed = await revalidationErrorOf(serverSegments);
|
|
1074
|
+
if (failed) throw failed;
|
|
1075
|
+
}
|
|
1032
1076
|
if (signal?.aborted) {
|
|
1033
1077
|
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
1034
1078
|
return;
|
|
1035
1079
|
}
|
|
1036
1080
|
|
|
1037
|
-
const isInterceptResponse = hasActiveInterceptSlots(
|
|
1038
|
-
payload.metadata?.slots,
|
|
1039
|
-
);
|
|
1040
|
-
|
|
1041
1081
|
// Same-structure revalidation: every segment this commit will render
|
|
1042
1082
|
// already exists on screen (by id) — the navigation mounts nothing new,
|
|
1043
1083
|
// it only refreshes what the user is looking at. The hold this enables
|
|
@@ -1053,8 +1093,6 @@ export function createPartialUpdater(
|
|
|
1053
1093
|
const isSameStructureNav =
|
|
1054
1094
|
mode.type === "navigate" && holdsAll(reconciled.segments);
|
|
1055
1095
|
|
|
1056
|
-
const effectiveInterceptSource =
|
|
1057
|
-
interceptSourceUrl || segmentState.currentUrl;
|
|
1058
1096
|
if (mode.type !== "action" && mode.type !== "stale-revalidation") {
|
|
1059
1097
|
if (isInterceptResponse) {
|
|
1060
1098
|
store.setInterceptSourceUrl(effectiveInterceptSource);
|
|
@@ -1065,7 +1103,7 @@ export function createPartialUpdater(
|
|
|
1065
1103
|
|
|
1066
1104
|
const serverLocationState = payload.metadata?.locationState;
|
|
1067
1105
|
const overrides: CommitOverrides = {
|
|
1068
|
-
|
|
1106
|
+
metadata: payload.metadata,
|
|
1069
1107
|
...(isInterceptResponse && {
|
|
1070
1108
|
scroll: false,
|
|
1071
1109
|
intercept: true,
|
|
@@ -1148,6 +1186,7 @@ export function createPartialUpdater(
|
|
|
1148
1186
|
onUpdate,
|
|
1149
1187
|
reconciled.mainSegments,
|
|
1150
1188
|
update,
|
|
1189
|
+
whenKind,
|
|
1151
1190
|
hasTransition ? ["action"] : [],
|
|
1152
1191
|
);
|
|
1153
1192
|
} else if (hasTransition) {
|
|
@@ -1156,6 +1195,7 @@ export function createPartialUpdater(
|
|
|
1156
1195
|
onUpdate,
|
|
1157
1196
|
reconciled.mainSegments,
|
|
1158
1197
|
update,
|
|
1198
|
+
whenKind,
|
|
1159
1199
|
optimisticPresented
|
|
1160
1200
|
? ["navigation", OPTIMISTIC_COMMIT_TRANSITION_TYPE]
|
|
1161
1201
|
: ["navigation"],
|
|
@@ -1181,7 +1221,7 @@ export function createPartialUpdater(
|
|
|
1181
1221
|
// flash). Inside a transition React holds the current UI until the
|
|
1182
1222
|
// suspended data lands, the action treatment; nothing new mounts (by
|
|
1183
1223
|
// the isSameStructureNav definition), so no fallback is being withheld
|
|
1184
|
-
// from a first paint.
|
|
1224
|
+
// from a first paint. Its kind is its only type (#1165): a React
|
|
1185
1225
|
// content-hold, not a view transition. Deliberate trade-off (#622
|
|
1186
1226
|
// introduced, #624 reverted, then reinstated): a client component that
|
|
1187
1227
|
// suspends during its FIRST render (use() of a promise created at
|
|
@@ -1199,6 +1239,7 @@ export function createPartialUpdater(
|
|
|
1199
1239
|
onUpdate,
|
|
1200
1240
|
reconciled.mainSegments,
|
|
1201
1241
|
update,
|
|
1242
|
+
whenKind,
|
|
1202
1243
|
optimisticPresented ? [OPTIMISTIC_COMMIT_TRANSITION_TYPE] : [],
|
|
1203
1244
|
);
|
|
1204
1245
|
} else {
|
|
@@ -1223,6 +1264,7 @@ export function createPartialUpdater(
|
|
|
1223
1264
|
if (holes) await fillHoles(holes, signal, url, gatedOff);
|
|
1224
1265
|
|
|
1225
1266
|
debugLog("[Browser] Navigation complete");
|
|
1267
|
+
if (mode.type === "action") return revalidationErrorOf(serverSegments);
|
|
1226
1268
|
return;
|
|
1227
1269
|
} else {
|
|
1228
1270
|
console.warn(`[Browser] Full update (fallback)`);
|
|
@@ -1235,6 +1277,12 @@ export function createPartialUpdater(
|
|
|
1235
1277
|
transitionGatedOff: fullGatedOff,
|
|
1236
1278
|
});
|
|
1237
1279
|
|
|
1280
|
+
// A background refresh keeps the page, as in the partial branch: its
|
|
1281
|
+
// loaders streamed, so this waits for them before anything commits.
|
|
1282
|
+
if (mode.type === "stale-revalidation") {
|
|
1283
|
+
const failed = await revalidationErrorOf(segments);
|
|
1284
|
+
if (failed) throw failed;
|
|
1285
|
+
}
|
|
1238
1286
|
if (signal?.aborted) {
|
|
1239
1287
|
debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
|
|
1240
1288
|
return;
|
|
@@ -1242,7 +1290,7 @@ export function createPartialUpdater(
|
|
|
1242
1290
|
|
|
1243
1291
|
const fullUpdateServerState = payload.metadata?.locationState;
|
|
1244
1292
|
const { scroll: fullScroll } = tx.commit(segmentIds, segments, {
|
|
1245
|
-
|
|
1293
|
+
metadata: payload.metadata,
|
|
1246
1294
|
...(fullUpdateServerState && { serverState: fullUpdateServerState }),
|
|
1247
1295
|
});
|
|
1248
1296
|
|
|
@@ -1283,6 +1331,7 @@ export function createPartialUpdater(
|
|
|
1283
1331
|
onUpdate,
|
|
1284
1332
|
segments,
|
|
1285
1333
|
fullUpdate,
|
|
1334
|
+
whenKind,
|
|
1286
1335
|
fullHasTransition ? ["action"] : [],
|
|
1287
1336
|
);
|
|
1288
1337
|
}
|
|
@@ -1292,16 +1341,25 @@ export function createPartialUpdater(
|
|
|
1292
1341
|
onUpdate,
|
|
1293
1342
|
segments,
|
|
1294
1343
|
fullUpdate,
|
|
1344
|
+
whenKind,
|
|
1295
1345
|
fullHasTransition ? ["action"] : [],
|
|
1296
1346
|
);
|
|
1297
1347
|
} else if (fullHasTransition) {
|
|
1298
|
-
commitInTransition(
|
|
1299
|
-
|
|
1300
|
-
|
|
1348
|
+
commitInTransition(
|
|
1349
|
+
store.loaders,
|
|
1350
|
+
onUpdate,
|
|
1351
|
+
segments,
|
|
1352
|
+
fullUpdate,
|
|
1353
|
+
whenKind,
|
|
1354
|
+
["navigation"],
|
|
1355
|
+
);
|
|
1301
1356
|
} else {
|
|
1302
1357
|
onUpdate(fullUpdate);
|
|
1303
1358
|
}
|
|
1304
1359
|
|
|
1360
|
+
// This render streamed its loaders (no isAction); the pending action
|
|
1361
|
+
// call waits for them only to report, as the partial lane's commit does.
|
|
1362
|
+
if (mode.type === "action") return revalidationErrorOf(segments);
|
|
1305
1363
|
return;
|
|
1306
1364
|
}
|
|
1307
1365
|
}
|
|
@@ -91,6 +91,14 @@ export interface DecodedPrefetch {
|
|
|
91
91
|
* one prefetch serves unlimited adoptions within TTL/state validity.
|
|
92
92
|
*/
|
|
93
93
|
respawn?: () => DecodedPrefetch;
|
|
94
|
+
/**
|
|
95
|
+
* The prefetch's stall timer (executePrefetchFetch). Navigation clears it on
|
|
96
|
+
* the entry it serves: from then on the click owns the stream, which runs to
|
|
97
|
+
* its end (a Flight response that arrived is never aborted mid-stream). A
|
|
98
|
+
* prefetch nobody adopted keeps it, so a stalled one is still aborted.
|
|
99
|
+
* Absent on a respawned entry, which has no timer.
|
|
100
|
+
*/
|
|
101
|
+
stallTimer?: ReturnType<typeof setTimeout>;
|
|
94
102
|
/**
|
|
95
103
|
* Release the buffered reserve bytes. Called when the entry is evicted
|
|
96
104
|
* without being consumed (expiry, FIFO, sweep, cache clear) so the tee
|
|
@@ -37,6 +37,7 @@ import { teeWithCompletion, isForeignRouterId } from "../response-adapter.js";
|
|
|
37
37
|
import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../../segment-fragments.js";
|
|
38
38
|
import { PREFETCH_HEADER } from "../../prefetch-markers.js";
|
|
39
39
|
import type { RscPayload } from "../types.js";
|
|
40
|
+
import { getRegisteredStore } from "../navigation-store-handle.js";
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Decoder injected at app startup (see setPrefetchDecoder). This is
|
|
@@ -162,6 +163,13 @@ function buildPrefetchUrl(
|
|
|
162
163
|
if (routerId) {
|
|
163
164
|
targetUrl.searchParams.set("_rsc_rid", routerId);
|
|
164
165
|
}
|
|
166
|
+
// The page's rewrite target, so the server diffs against the route that
|
|
167
|
+
// rendered it, as for the navigation.
|
|
168
|
+
const rewriteTarget =
|
|
169
|
+
getRegisteredStore()?.getHistoryEntryMemory()?.rewriteTarget;
|
|
170
|
+
if (rewriteTarget) {
|
|
171
|
+
targetUrl.searchParams.set("_rsc_rt", rewriteTarget);
|
|
172
|
+
}
|
|
165
173
|
return targetUrl;
|
|
166
174
|
}
|
|
167
175
|
|
|
@@ -218,8 +226,9 @@ function executePrefetchFetch(
|
|
|
218
226
|
// hover/direct path (no caller signal) and the queue-driven path (whose caller
|
|
219
227
|
// signal only aborts on navigation, never on a stall). On fire it aborts the
|
|
220
228
|
// fetch/stream and evicts the published entry if one exists; it is NOT cleared
|
|
221
|
-
// when headers arrive (see below) — it is cleared when the stream completes,
|
|
222
|
-
//
|
|
229
|
+
// when headers arrive (see below) — it is cleared when the stream completes,
|
|
230
|
+
// when a navigation serves the entry (DecodedPrefetch.stallTimer), or in
|
|
231
|
+
// `.finally()` for paths that publish no streaming entry.
|
|
223
232
|
let publishedKey: string | undefined;
|
|
224
233
|
let publishedEntry: DecodedPrefetch | undefined;
|
|
225
234
|
const timeoutController = new AbortController();
|
|
@@ -341,6 +350,7 @@ function executePrefetchFetch(
|
|
|
341
350
|
streamComplete,
|
|
342
351
|
scope,
|
|
343
352
|
complete: false,
|
|
353
|
+
stallTimer: timeoutId,
|
|
344
354
|
...(reserve && {
|
|
345
355
|
dispose: () => {
|
|
346
356
|
reserve.cancel().catch(() => {});
|
|
@@ -369,6 +379,7 @@ function executePrefetchFetch(
|
|
|
369
379
|
// reserve would replay broken bytes, so it stays one-shot (and its
|
|
370
380
|
// buffered reserve is released via the removePrefetch dispose above).
|
|
371
381
|
Promise.allSettled([payload, streamComplete]).then(([decode]) => {
|
|
382
|
+
clearTimeout(timeoutId);
|
|
372
383
|
if (decode.status === "fulfilled" && endedCleanly) {
|
|
373
384
|
entry.complete = true;
|
|
374
385
|
if (reserve) {
|
|
@@ -388,7 +399,6 @@ function executePrefetchFetch(
|
|
|
388
399
|
}
|
|
389
400
|
}
|
|
390
401
|
}
|
|
391
|
-
clearTimeout(timeoutId);
|
|
392
402
|
});
|
|
393
403
|
return entry;
|
|
394
404
|
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from "@rangojs/router:version";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Publishes `window.__rango = { version }` so outside tools can tell a page is
|
|
5
|
+
* a Rango app and which router release serves it. Write-only and informational:
|
|
6
|
+
* nothing in the router reads it (rango-marker.test.ts greps src/ for reads),
|
|
7
|
+
* and only the npm semver is exposed, no build hash, router id or config.
|
|
8
|
+
*/
|
|
9
|
+
export function publishRangoMarker(): void {
|
|
10
|
+
(window as { __rango?: unknown }).__rango = Object.freeze({
|
|
11
|
+
version: PACKAGE_VERSION,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -505,9 +505,16 @@ export function NavigationProvider({
|
|
|
505
505
|
|
|
506
506
|
// The last navigation's scroll decision, until React commits its update.
|
|
507
507
|
const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
|
|
508
|
+
// The last payload handed to React, until it commits: route state that
|
|
509
|
+
// arrived behind it waits for that commit (flushRouteState("anchor")).
|
|
510
|
+
const committingRef = useRef<typeof payload | undefined>(undefined);
|
|
508
511
|
|
|
509
512
|
// Apply scroll after React commits the new content to the DOM
|
|
510
513
|
useLayoutEffect(() => {
|
|
514
|
+
if (payload === committingRef.current) {
|
|
515
|
+
committingRef.current = undefined;
|
|
516
|
+
eventController.releaseRouteState();
|
|
517
|
+
}
|
|
511
518
|
const scrollAction = pendingScrollRef.current;
|
|
512
519
|
if (!scrollAction) return;
|
|
513
520
|
pendingScrollRef.current = undefined;
|
|
@@ -518,6 +525,7 @@ export function NavigationProvider({
|
|
|
518
525
|
restore: scrollAction.restore,
|
|
519
526
|
scroll: scrollAction.enabled,
|
|
520
527
|
isStreaming: scrollAction.isStreaming,
|
|
528
|
+
committed: true,
|
|
521
529
|
});
|
|
522
530
|
});
|
|
523
531
|
|
|
@@ -544,7 +552,9 @@ export function NavigationProvider({
|
|
|
544
552
|
eventController.releaseHydrationLock();
|
|
545
553
|
}
|
|
546
554
|
|
|
547
|
-
|
|
555
|
+
const next = { root: update.root, metadata: update.metadata };
|
|
556
|
+
committingRef.current = next;
|
|
557
|
+
setPayload(next);
|
|
548
558
|
// The state of the entry this payload renders, as its commit recorded
|
|
549
559
|
// it: one update, so one lane, so one React commit with the tree.
|
|
550
560
|
setLocationState(eventController.getLocationState());
|
|
@@ -556,9 +566,23 @@ export function NavigationProvider({
|
|
|
556
566
|
eventController.setParams(update.metadata.params);
|
|
557
567
|
}
|
|
558
568
|
|
|
569
|
+
// tx.commit() and the metadata updates above mutate the controller
|
|
570
|
+
// synchronously, but its ordinary notifications are task-debounced. Flush
|
|
571
|
+
// them here so hook setState calls inherit this payload update's lane. In
|
|
572
|
+
// a transition that suspends, the source tree therefore keeps its source
|
|
573
|
+
// pathname/params until the destination payload commits with them.
|
|
574
|
+
// A page in a transition: route state that arrives from here waits for
|
|
575
|
+
// this payload's commit (useCommitAnchoredState). An urgent page: it is
|
|
576
|
+
// set at default priority, in the page's lane, and commits with it.
|
|
577
|
+
eventController.flushRouteState(
|
|
578
|
+
update.inTransition ? "anchor" : "release",
|
|
579
|
+
);
|
|
580
|
+
|
|
559
581
|
// A page rendered from a history-cache entry (back/forward, or a link
|
|
560
582
|
// to it) starts from the entry's pushes, and its response's handles
|
|
561
583
|
// merge onto them: the page on screen's pushes are not its segments'.
|
|
584
|
+
// After the flush: a crumb whose content is still pending would hold
|
|
585
|
+
// the page in its transition; it waits and lands in the page's task.
|
|
562
586
|
const { matched, cachedHandleData } = update.metadata;
|
|
563
587
|
if (matched && cachedHandleData) {
|
|
564
588
|
eventController.setHandleData(cachedHandleData, matched);
|
|
@@ -575,12 +599,6 @@ export function NavigationProvider({
|
|
|
575
599
|
update.metadata.resolvedIds,
|
|
576
600
|
);
|
|
577
601
|
}
|
|
578
|
-
|
|
579
|
-
// tx.commit() and the metadata updates above mutate the controller
|
|
580
|
-
// synchronously, but its ordinary notifications are task-debounced. Flush
|
|
581
|
-
// them here so hook setState calls inherit this payload update's lane. In
|
|
582
|
-
// a transition that suspends, the source tree therefore keeps its source
|
|
583
|
-
// pathname/params until the destination payload commits with them.
|
|
584
602
|
eventController.flushRouteState();
|
|
585
603
|
});
|
|
586
604
|
|
|
@@ -9,6 +9,19 @@ export function isLoaderSegmentId(id: string): boolean {
|
|
|
9
9
|
return LOADER_SEGMENT_ID.test(id);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The parent of a loader sub-id (`${parentId}D${index}.${loaderId}`); any
|
|
14
|
+
* other id unchanged. parentId is a shortCode (never "D"), or
|
|
15
|
+
* `${shortCode}.${slotName}` for an intercept slot's loaders, where the
|
|
16
|
+
* user-chosen name may hold an uppercase "D" ("@Detail"). Strip from the first
|
|
17
|
+
* `D<index>.`: splitting on a bare "D" mis-cut "@Detail" to "@" and silently
|
|
18
|
+
* dropped the loader's data. Correct only because slot names cannot contain
|
|
19
|
+
* "." (assertValidSlotName, route-definition/dsl-helpers.ts).
|
|
20
|
+
*/
|
|
21
|
+
export function loaderParentId(id: string): string {
|
|
22
|
+
return id.replace(/D\d+\..*$/, "");
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
/**
|
|
13
26
|
* Build the handle-collection segment order from a raw `matched` list.
|
|
14
27
|
*
|
|
@@ -11,19 +11,9 @@ import {
|
|
|
11
11
|
import { NavigationStoreContext } from "./context.js";
|
|
12
12
|
import { shallowEqual } from "./shallow-equal.js";
|
|
13
13
|
import type { TrackedActionState } from "../types.js";
|
|
14
|
+
import { DEFAULT_ACTION_STATE } from "../event-controller.js";
|
|
14
15
|
import { invariant } from "../../errors.js";
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
* Default action state (idle with no payload)
|
|
18
|
-
*/
|
|
19
|
-
const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
20
|
-
state: "idle",
|
|
21
|
-
actionId: null,
|
|
22
|
-
payload: null,
|
|
23
|
-
error: null,
|
|
24
|
-
result: null,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
17
|
/**
|
|
28
18
|
* Extract action ID from a server action function or string.
|
|
29
19
|
*
|
|
@@ -49,10 +39,13 @@ function getActionId(action: ServerActionFunction | string): string {
|
|
|
49
39
|
return action;
|
|
50
40
|
}
|
|
51
41
|
|
|
52
|
-
// If we get here, this is likely an action passed as prop from a server
|
|
53
|
-
//
|
|
42
|
+
// If we get here, this is likely an action passed as prop from a server
|
|
43
|
+
// component: these lose their metadata during RSC serialization. The
|
|
44
|
+
// guidance is dev-only: it was about 225 B gzip of every client build that
|
|
45
|
+
// calls useAction.
|
|
54
46
|
throw new Error(
|
|
55
|
-
|
|
47
|
+
process.env.NODE_ENV !== "production"
|
|
48
|
+
? `useAction: Cannot extract action ID from function reference.
|
|
56
49
|
|
|
57
50
|
This typically happens when an action is passed as a prop from a server component.
|
|
58
51
|
Actions passed through RSC lose their metadata during serialization.
|
|
@@ -65,7 +58,8 @@ Solutions:
|
|
|
65
58
|
2. Use the action name as a string:
|
|
66
59
|
const state = useAction("myAction");
|
|
67
60
|
|
|
68
|
-
The string must match the exported function name from your "use server" file
|
|
61
|
+
The string must match the exported function name from your "use server" file.`
|
|
62
|
+
: "useAction: Cannot extract action ID from function reference.",
|
|
69
63
|
);
|
|
70
64
|
}
|
|
71
65
|
|