rwsdk 1.0.0-beta.5 → 1.0.0-beta.51
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/bin/rw-scripts.mjs +13 -13
- package/dist/lib/constants.d.mts +1 -0
- package/dist/lib/constants.mjs +7 -4
- package/dist/lib/e2e/browser.mjs +6 -2
- package/dist/lib/e2e/constants.d.mts +4 -0
- package/dist/lib/e2e/constants.mjs +49 -12
- package/dist/lib/e2e/dev.mjs +49 -57
- package/dist/lib/e2e/environment.d.mts +2 -0
- package/dist/lib/e2e/environment.mjs +201 -64
- package/dist/lib/e2e/index.d.mts +2 -0
- package/dist/lib/e2e/index.mjs +2 -0
- package/dist/lib/e2e/poll.d.mts +1 -1
- package/dist/lib/e2e/release.d.mts +1 -0
- package/dist/lib/e2e/release.mjs +57 -52
- package/dist/lib/e2e/tarball.mjs +2 -34
- package/dist/lib/e2e/testHarness.d.mts +39 -3
- package/dist/lib/e2e/testHarness.mjs +239 -92
- package/dist/lib/e2e/utils.d.mts +1 -0
- package/dist/lib/e2e/utils.mjs +15 -0
- package/dist/lib/normalizeModulePath.mjs +1 -1
- package/dist/runtime/client/client.d.ts +64 -2
- package/dist/runtime/client/client.js +156 -15
- package/dist/runtime/client/navigation.d.ts +45 -0
- package/dist/runtime/client/navigation.js +68 -14
- package/dist/runtime/client/navigationCache.d.ts +68 -0
- package/dist/runtime/client/navigationCache.js +294 -0
- package/dist/runtime/client/navigationCache.test.js +469 -0
- package/dist/runtime/client/types.d.ts +26 -5
- package/dist/runtime/client/types.js +8 -1
- package/dist/runtime/entries/no-react-server-ssr-bridge.d.ts +0 -0
- package/dist/runtime/entries/no-react-server-ssr-bridge.js +2 -0
- package/dist/runtime/entries/no-react-server.js +3 -1
- package/dist/runtime/entries/react-server-only.js +1 -1
- package/dist/runtime/entries/router.d.ts +1 -0
- package/dist/runtime/entries/routerClient.d.ts +1 -0
- package/dist/runtime/entries/routerClient.js +1 -0
- package/dist/runtime/entries/worker.d.ts +4 -0
- package/dist/runtime/entries/worker.js +4 -0
- package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
- package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
- package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
- package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
- package/dist/runtime/lib/db/createDb.d.ts +1 -2
- package/dist/runtime/lib/db/createDb.js +4 -0
- package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
- package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
- package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
- package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
- package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
- package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
- package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
- package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
- package/dist/runtime/lib/links.d.ts +21 -7
- package/dist/runtime/lib/links.js +84 -26
- package/dist/runtime/lib/links.test.d.ts +1 -0
- package/dist/runtime/lib/links.test.js +20 -0
- package/dist/runtime/lib/manifest.d.ts +1 -1
- package/dist/runtime/lib/manifest.js +7 -4
- package/dist/runtime/lib/realtime/client.js +28 -6
- package/dist/runtime/lib/realtime/worker.d.ts +1 -1
- package/dist/runtime/lib/router.d.ts +154 -35
- package/dist/runtime/lib/router.js +491 -105
- package/dist/runtime/lib/router.test.js +611 -1
- package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
- package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
- package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
- package/dist/runtime/lib/types.js +1 -0
- package/dist/runtime/register/client.d.ts +1 -1
- package/dist/runtime/register/client.js +10 -3
- package/dist/runtime/register/worker.js +13 -4
- package/dist/runtime/render/normalizeActionResult.js +8 -1
- package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
- package/dist/runtime/render/renderToStream.d.ts +4 -2
- package/dist/runtime/render/renderToStream.js +53 -24
- package/dist/runtime/render/renderToString.d.ts +3 -6
- package/dist/runtime/requestInfo/types.d.ts +5 -1
- package/dist/runtime/requestInfo/utils.d.ts +9 -0
- package/dist/runtime/requestInfo/utils.js +45 -0
- package/dist/runtime/requestInfo/worker.d.ts +0 -1
- package/dist/runtime/requestInfo/worker.js +5 -11
- package/dist/runtime/script.d.ts +1 -3
- package/dist/runtime/script.js +1 -10
- package/dist/runtime/server.d.ts +52 -0
- package/dist/runtime/server.js +88 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +13 -0
- package/dist/runtime/worker.d.ts +3 -1
- package/dist/runtime/worker.js +45 -2
- package/dist/scripts/debug-sync.mjs +18 -20
- package/dist/scripts/worker-run.d.mts +1 -1
- package/dist/scripts/worker-run.mjs +59 -113
- package/dist/use-synced-state/SyncedStateServer.d.mts +36 -0
- package/dist/use-synced-state/SyncedStateServer.mjs +196 -0
- package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
- package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +116 -0
- package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
- package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
- package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
- package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
- package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
- package/dist/use-synced-state/__tests__/worker.test.mjs +70 -0
- package/dist/use-synced-state/client-core.d.ts +29 -0
- package/dist/use-synced-state/client-core.js +103 -0
- package/dist/use-synced-state/client.d.ts +3 -0
- package/dist/use-synced-state/client.js +4 -0
- package/dist/use-synced-state/constants.d.mts +1 -0
- package/dist/use-synced-state/constants.mjs +1 -0
- package/dist/use-synced-state/useSyncedState.d.ts +21 -0
- package/dist/use-synced-state/useSyncedState.js +64 -0
- package/dist/use-synced-state/worker.d.mts +14 -0
- package/dist/use-synced-state/worker.mjs +135 -0
- package/dist/vite/buildApp.mjs +34 -2
- package/dist/vite/cloudflarePreInitPlugin.d.mts +11 -0
- package/dist/vite/cloudflarePreInitPlugin.mjs +40 -0
- package/dist/vite/configPlugin.mjs +9 -14
- package/dist/vite/constants.d.mts +1 -0
- package/dist/vite/constants.mjs +1 -0
- package/dist/vite/createDirectiveLookupPlugin.mjs +10 -7
- package/dist/vite/devServerTimingPlugin.mjs +4 -0
- package/dist/vite/diagnosticAssetGraphPlugin.d.mts +4 -0
- package/dist/vite/diagnosticAssetGraphPlugin.mjs +41 -0
- package/dist/vite/directiveModulesDevPlugin.mjs +9 -1
- package/dist/vite/directivesPlugin.mjs +4 -4
- package/dist/vite/envResolvers.d.mts +11 -0
- package/dist/vite/envResolvers.mjs +20 -0
- package/dist/vite/getViteEsbuild.mjs +2 -1
- package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
- package/dist/vite/hmrStabilityPlugin.mjs +73 -0
- package/dist/vite/injectVitePreamblePlugin.mjs +0 -4
- package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
- package/dist/vite/knownDepsResolverPlugin.mjs +25 -17
- package/dist/vite/linkerPlugin.d.mts +2 -1
- package/dist/vite/linkerPlugin.mjs +11 -3
- package/dist/vite/linkerPlugin.test.mjs +15 -0
- package/dist/vite/miniflareHMRPlugin.mjs +6 -38
- package/dist/vite/moveStaticAssetsPlugin.mjs +35 -4
- package/dist/vite/redwoodPlugin.mjs +9 -11
- package/dist/vite/redwoodPlugin.test.mjs +4 -4
- package/dist/vite/runDirectivesScan.mjs +75 -19
- package/dist/vite/ssrBridgePlugin.mjs +132 -40
- package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
- package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
- package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
- package/dist/vite/staleDepRetryPlugin.mjs +74 -0
- package/dist/vite/statePlugin.d.mts +4 -0
- package/dist/vite/statePlugin.mjs +62 -0
- package/dist/vite/transformClientComponents.test.mjs +32 -0
- package/dist/vite/transformJsxScriptTagsPlugin.mjs +0 -5
- package/dist/vite/transformServerFunctions.mjs +66 -4
- package/dist/vite/transformServerFunctions.test.mjs +35 -0
- package/dist/vite/virtualPlugin.mjs +6 -7
- package/package.json +45 -20
- package/dist/vite/manifestPlugin.d.mts +0 -4
- package/dist/vite/manifestPlugin.mjs +0 -63
- /package/dist/runtime/{lib/rwContext.js → client/navigationCache.test.d.ts} +0 -0
|
@@ -1,11 +1,73 @@
|
|
|
1
1
|
import "./setWebpackRequire";
|
|
2
2
|
export { default as React } from "react";
|
|
3
|
+
export type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
|
3
4
|
export { ClientOnly } from "./ClientOnly.js";
|
|
4
5
|
export { initClientNavigation, navigate } from "./navigation.js";
|
|
5
|
-
|
|
6
|
+
export type { ActionResponseData } from "./types";
|
|
7
|
+
import type { ActionResponseData, HydrationOptions, Transport } from "./types";
|
|
6
8
|
export declare const fetchTransport: Transport;
|
|
7
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Initializes the React client and hydrates the RSC payload.
|
|
11
|
+
*
|
|
12
|
+
* This function sets up client-side hydration for React Server Components,
|
|
13
|
+
* making the page interactive. Call this from your client entry point.
|
|
14
|
+
*
|
|
15
|
+
* @param transport - Custom transport for server communication (defaults to fetchTransport)
|
|
16
|
+
* @param hydrateRootOptions - Options passed to React's `hydrateRoot`. Supports all React hydration options including:
|
|
17
|
+
* - `onUncaughtError`: Handler for uncaught errors (async errors, event handler errors).
|
|
18
|
+
* If not provided, defaults to logging errors to console.
|
|
19
|
+
* - `onCaughtError`: Handler for errors caught by error boundaries
|
|
20
|
+
* - `onRecoverableError`: Handler for recoverable errors
|
|
21
|
+
* @param handleResponse - Custom response handler for navigation errors (navigation GETs)
|
|
22
|
+
* @param onHydrated - Callback invoked after a new RSC payload has been committed on the client
|
|
23
|
+
* @param onActionResponse - Optional hook invoked when an action returns a Response;
|
|
24
|
+
* return true to signal that the response has been handled and
|
|
25
|
+
* default behaviour (e.g. redirects) should be skipped
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Basic usage
|
|
29
|
+
* import { initClient } from "rwsdk/client";
|
|
30
|
+
*
|
|
31
|
+
* initClient();
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // With client-side navigation
|
|
35
|
+
* import { initClient, initClientNavigation } from "rwsdk/client";
|
|
36
|
+
*
|
|
37
|
+
* const { handleResponse } = initClientNavigation();
|
|
38
|
+
* initClient({ handleResponse });
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // With error handling
|
|
42
|
+
* initClient({
|
|
43
|
+
* hydrateRootOptions: {
|
|
44
|
+
* onUncaughtError: (error, errorInfo) => {
|
|
45
|
+
* console.error("Uncaught error:", error);
|
|
46
|
+
* // Send to monitoring service
|
|
47
|
+
* sendToSentry(error, errorInfo);
|
|
48
|
+
* },
|
|
49
|
+
* onCaughtError: (error, errorInfo) => {
|
|
50
|
+
* console.error("Caught error:", error);
|
|
51
|
+
* // Handle errors from error boundaries
|
|
52
|
+
* sendToSentry(error, errorInfo);
|
|
53
|
+
* },
|
|
54
|
+
* },
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // With custom React hydration options
|
|
59
|
+
* initClient({
|
|
60
|
+
* hydrateRootOptions: {
|
|
61
|
+
* onRecoverableError: (error) => {
|
|
62
|
+
* console.warn("Recoverable error:", error);
|
|
63
|
+
* },
|
|
64
|
+
* },
|
|
65
|
+
* });
|
|
66
|
+
*/
|
|
67
|
+
export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, onHydrated, onActionResponse, }?: {
|
|
8
68
|
transport?: Transport;
|
|
9
69
|
hydrateRootOptions?: HydrationOptions;
|
|
10
70
|
handleResponse?: (response: Response) => boolean;
|
|
71
|
+
onHydrated?: () => void;
|
|
72
|
+
onActionResponse?: (actionResponse: ActionResponseData) => boolean | void;
|
|
11
73
|
}) => Promise<void>;
|
|
@@ -4,6 +4,7 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
|
4
4
|
// context(justinvdm, 14 Aug 2025): `react-server-dom-webpack` uses this global
|
|
5
5
|
// to load modules, so we need to define it here before importing
|
|
6
6
|
// "react-server-dom-webpack."
|
|
7
|
+
// prettier-ignore
|
|
7
8
|
import "./setWebpackRequire";
|
|
8
9
|
import React from "react";
|
|
9
10
|
import { hydrateRoot } from "react-dom/client";
|
|
@@ -12,18 +13,57 @@ import { rscStream } from "rsc-html-stream/client";
|
|
|
12
13
|
export { default as React } from "react";
|
|
13
14
|
export { ClientOnly } from "./ClientOnly.js";
|
|
14
15
|
export { initClientNavigation, navigate } from "./navigation.js";
|
|
16
|
+
import { getCachedNavigationResponse } from "./navigationCache.js";
|
|
17
|
+
import { isActionResponse } from "./types";
|
|
15
18
|
export const fetchTransport = (transportContext) => {
|
|
16
|
-
const fetchCallServer = async (id, args) => {
|
|
19
|
+
const fetchCallServer = async (id, args, source = "action", method = "POST") => {
|
|
17
20
|
const url = new URL(window.location.href);
|
|
18
21
|
url.searchParams.set("__rsc", "");
|
|
19
|
-
|
|
22
|
+
const isAction = id != null;
|
|
23
|
+
if (isAction) {
|
|
20
24
|
url.searchParams.set("__rsc_action_id", id);
|
|
25
|
+
// If args are provided and method is GET, serialize them into the query string
|
|
26
|
+
if (args != null && method === "GET") {
|
|
27
|
+
url.searchParams.set("args", JSON.stringify(args));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let fetchPromise;
|
|
31
|
+
if (!isAction && source === "navigation") {
|
|
32
|
+
// Try to get cached response first
|
|
33
|
+
const cachedResponse = await getCachedNavigationResponse(url);
|
|
34
|
+
if (cachedResponse) {
|
|
35
|
+
fetchPromise = Promise.resolve(cachedResponse);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// Fall back to network fetch on cache miss
|
|
39
|
+
fetchPromise = fetch(url, {
|
|
40
|
+
method: "GET",
|
|
41
|
+
redirect: "manual",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const headers = new Headers();
|
|
47
|
+
// Add x-rsc-data-only header if we want to skip the React tree render on the server
|
|
48
|
+
if (source === "query") {
|
|
49
|
+
headers.set("x-rsc-data-only", "true");
|
|
50
|
+
}
|
|
51
|
+
if (method === "GET") {
|
|
52
|
+
fetchPromise = fetch(url, {
|
|
53
|
+
method: "GET",
|
|
54
|
+
headers,
|
|
55
|
+
redirect: "manual",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
fetchPromise = fetch(url, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers,
|
|
62
|
+
redirect: "manual",
|
|
63
|
+
body: args != null ? await encodeReply(args) : null,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
21
66
|
}
|
|
22
|
-
const fetchPromise = fetch(url, {
|
|
23
|
-
method: "POST",
|
|
24
|
-
redirect: "manual",
|
|
25
|
-
body: args != null ? await encodeReply(args) : null,
|
|
26
|
-
});
|
|
27
67
|
// If there's a response handler, check the response first
|
|
28
68
|
if (transportContext.handleResponse) {
|
|
29
69
|
const response = await fetchPromise;
|
|
@@ -35,27 +75,121 @@ export const fetchTransport = (transportContext) => {
|
|
|
35
75
|
const streamData = createFromFetch(Promise.resolve(response), {
|
|
36
76
|
callServer: fetchCallServer,
|
|
37
77
|
});
|
|
38
|
-
|
|
78
|
+
if (source === "navigation" || source === "action") {
|
|
79
|
+
transportContext.setRscPayload(streamData);
|
|
80
|
+
}
|
|
39
81
|
const result = await streamData;
|
|
40
|
-
|
|
82
|
+
const rawActionResult = result.actionResult;
|
|
83
|
+
if (isActionResponse(rawActionResult)) {
|
|
84
|
+
const actionResponse = rawActionResult.__rw_action_response;
|
|
85
|
+
const handledByHook = transportContext.onActionResponse?.(actionResponse) === true;
|
|
86
|
+
if (!handledByHook) {
|
|
87
|
+
const location = actionResponse.headers["location"];
|
|
88
|
+
const isRedirect = actionResponse.status >= 300 && actionResponse.status < 400;
|
|
89
|
+
if (location && isRedirect) {
|
|
90
|
+
window.location.href = location;
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return rawActionResult;
|
|
95
|
+
}
|
|
96
|
+
return rawActionResult;
|
|
41
97
|
}
|
|
42
98
|
// Original behavior when no handler is present
|
|
43
99
|
const streamData = createFromFetch(fetchPromise, {
|
|
44
100
|
callServer: fetchCallServer,
|
|
45
101
|
});
|
|
46
|
-
|
|
102
|
+
if (source === "navigation" || source === "action") {
|
|
103
|
+
transportContext.setRscPayload(streamData);
|
|
104
|
+
}
|
|
47
105
|
const result = await streamData;
|
|
48
|
-
|
|
106
|
+
const rawActionResult = result.actionResult;
|
|
107
|
+
if (isActionResponse(rawActionResult)) {
|
|
108
|
+
const actionResponse = rawActionResult.__rw_action_response;
|
|
109
|
+
const handledByHook = transportContext.onActionResponse?.(actionResponse) === true;
|
|
110
|
+
if (!handledByHook) {
|
|
111
|
+
const location = actionResponse.headers["location"];
|
|
112
|
+
const isRedirect = actionResponse.status >= 300 && actionResponse.status < 400;
|
|
113
|
+
if (location && isRedirect) {
|
|
114
|
+
window.location.href = location;
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return rawActionResult;
|
|
119
|
+
}
|
|
120
|
+
return rawActionResult;
|
|
49
121
|
};
|
|
50
122
|
return fetchCallServer;
|
|
51
123
|
};
|
|
52
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Initializes the React client and hydrates the RSC payload.
|
|
126
|
+
*
|
|
127
|
+
* This function sets up client-side hydration for React Server Components,
|
|
128
|
+
* making the page interactive. Call this from your client entry point.
|
|
129
|
+
*
|
|
130
|
+
* @param transport - Custom transport for server communication (defaults to fetchTransport)
|
|
131
|
+
* @param hydrateRootOptions - Options passed to React's `hydrateRoot`. Supports all React hydration options including:
|
|
132
|
+
* - `onUncaughtError`: Handler for uncaught errors (async errors, event handler errors).
|
|
133
|
+
* If not provided, defaults to logging errors to console.
|
|
134
|
+
* - `onCaughtError`: Handler for errors caught by error boundaries
|
|
135
|
+
* - `onRecoverableError`: Handler for recoverable errors
|
|
136
|
+
* @param handleResponse - Custom response handler for navigation errors (navigation GETs)
|
|
137
|
+
* @param onHydrated - Callback invoked after a new RSC payload has been committed on the client
|
|
138
|
+
* @param onActionResponse - Optional hook invoked when an action returns a Response;
|
|
139
|
+
* return true to signal that the response has been handled and
|
|
140
|
+
* default behaviour (e.g. redirects) should be skipped
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* // Basic usage
|
|
144
|
+
* import { initClient } from "rwsdk/client";
|
|
145
|
+
*
|
|
146
|
+
* initClient();
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* // With client-side navigation
|
|
150
|
+
* import { initClient, initClientNavigation } from "rwsdk/client";
|
|
151
|
+
*
|
|
152
|
+
* const { handleResponse } = initClientNavigation();
|
|
153
|
+
* initClient({ handleResponse });
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* // With error handling
|
|
157
|
+
* initClient({
|
|
158
|
+
* hydrateRootOptions: {
|
|
159
|
+
* onUncaughtError: (error, errorInfo) => {
|
|
160
|
+
* console.error("Uncaught error:", error);
|
|
161
|
+
* // Send to monitoring service
|
|
162
|
+
* sendToSentry(error, errorInfo);
|
|
163
|
+
* },
|
|
164
|
+
* onCaughtError: (error, errorInfo) => {
|
|
165
|
+
* console.error("Caught error:", error);
|
|
166
|
+
* // Handle errors from error boundaries
|
|
167
|
+
* sendToSentry(error, errorInfo);
|
|
168
|
+
* },
|
|
169
|
+
* },
|
|
170
|
+
* });
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* // With custom React hydration options
|
|
174
|
+
* initClient({
|
|
175
|
+
* hydrateRootOptions: {
|
|
176
|
+
* onRecoverableError: (error) => {
|
|
177
|
+
* console.warn("Recoverable error:", error);
|
|
178
|
+
* },
|
|
179
|
+
* },
|
|
180
|
+
* });
|
|
181
|
+
*/
|
|
182
|
+
export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, onHydrated, onActionResponse, } = {}) => {
|
|
53
183
|
const transportContext = {
|
|
54
184
|
setRscPayload: () => { },
|
|
55
185
|
handleResponse,
|
|
186
|
+
onHydrated,
|
|
187
|
+
onActionResponse,
|
|
56
188
|
};
|
|
57
189
|
let transportCallServer = transport(transportContext);
|
|
58
|
-
const callServer = (id, args) =>
|
|
190
|
+
const callServer = (id, args, source, method) => {
|
|
191
|
+
return transportCallServer(id, args, source, method);
|
|
192
|
+
};
|
|
59
193
|
const upgradeToRealtime = async ({ key } = {}) => {
|
|
60
194
|
const { realtimeTransport } = await import("../lib/realtime/client");
|
|
61
195
|
const createRealtimeTransport = realtimeTransport({ key });
|
|
@@ -68,7 +202,7 @@ export const initClient = async ({ transport = fetchTransport, hydrateRootOption
|
|
|
68
202
|
};
|
|
69
203
|
const rootEl = document.getElementById("hydrate-root");
|
|
70
204
|
if (!rootEl) {
|
|
71
|
-
throw new Error('
|
|
205
|
+
throw new Error('RedwoodSDK: No element with id "hydrate-root" found in the document. This element is required for hydration. Ensure your Document component contains a <div id="hydrate-root">{children}</div>.');
|
|
72
206
|
}
|
|
73
207
|
let rscPayload;
|
|
74
208
|
// context(justinvdm, 18 Jun 2025): We inject the RSC payload
|
|
@@ -81,7 +215,14 @@ export const initClient = async ({ transport = fetchTransport, hydrateRootOption
|
|
|
81
215
|
function Content() {
|
|
82
216
|
const [streamData, setStreamData] = React.useState(rscPayload);
|
|
83
217
|
const [_isPending, startTransition] = React.useTransition();
|
|
84
|
-
transportContext.setRscPayload = (v) => startTransition(() =>
|
|
218
|
+
transportContext.setRscPayload = (v) => startTransition(() => {
|
|
219
|
+
setStreamData(v);
|
|
220
|
+
});
|
|
221
|
+
React.useEffect(() => {
|
|
222
|
+
if (!streamData)
|
|
223
|
+
return;
|
|
224
|
+
transportContext.onHydrated?.();
|
|
225
|
+
}, [streamData]);
|
|
85
226
|
return (_jsx(_Fragment, { children: streamData
|
|
86
227
|
? React.use(streamData).node
|
|
87
228
|
: null }));
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { type NavigationCache, type NavigationCacheStorage } from "./navigationCache.js";
|
|
2
|
+
export type { NavigationCache, NavigationCacheStorage };
|
|
1
3
|
export interface ClientNavigationOptions {
|
|
2
4
|
onNavigate?: () => void;
|
|
3
5
|
scrollToTop?: boolean;
|
|
4
6
|
scrollBehavior?: "auto" | "smooth" | "instant";
|
|
7
|
+
cacheStorage?: NavigationCacheStorage;
|
|
5
8
|
}
|
|
6
9
|
export declare function validateClickEvent(event: MouseEvent, target: HTMLElement): boolean;
|
|
7
10
|
export interface NavigateOptions {
|
|
@@ -12,6 +15,48 @@ export interface NavigateOptions {
|
|
|
12
15
|
};
|
|
13
16
|
}
|
|
14
17
|
export declare function navigate(href: string, options?: NavigateOptions): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Initializes client-side navigation for Single Page App (SPA) behavior.
|
|
20
|
+
*
|
|
21
|
+
* Intercepts clicks on internal links and fetches page content without full-page reloads.
|
|
22
|
+
* Returns a handleResponse function to pass to initClient.
|
|
23
|
+
*
|
|
24
|
+
* @param opts.scrollToTop - Scroll to top after navigation (default: true)
|
|
25
|
+
* @param opts.scrollBehavior - How to scroll: 'instant', 'smooth', or 'auto' (default: 'instant')
|
|
26
|
+
* @param opts.onNavigate - Callback executed after history push but before RSC fetch
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Basic usage
|
|
30
|
+
* import { initClient, initClientNavigation } from "rwsdk/client";
|
|
31
|
+
*
|
|
32
|
+
* const { handleResponse, onHydrated } = initClientNavigation();
|
|
33
|
+
* initClient({ handleResponse, onHydrated });
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // With custom scroll behavior
|
|
37
|
+
* const { handleResponse } = initClientNavigation({
|
|
38
|
+
* scrollBehavior: "smooth",
|
|
39
|
+
* scrollToTop: true,
|
|
40
|
+
* });
|
|
41
|
+
* initClient({ handleResponse });
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Preserve scroll position (e.g., for infinite scroll)
|
|
45
|
+
* const { handleResponse } = initClientNavigation({
|
|
46
|
+
* scrollToTop: false,
|
|
47
|
+
* });
|
|
48
|
+
* initClient({ handleResponse });
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // With navigation callback
|
|
52
|
+
* const { handleResponse } = initClientNavigation({
|
|
53
|
+
* onNavigate: () => {
|
|
54
|
+
* console.log("Navigating to:", window.location.href);
|
|
55
|
+
* },
|
|
56
|
+
* });
|
|
57
|
+
* initClient({ handleResponse });
|
|
58
|
+
*/
|
|
15
59
|
export declare function initClientNavigation(opts?: ClientNavigationOptions): {
|
|
16
60
|
handleResponse: (response: Response) => boolean;
|
|
61
|
+
onHydrated: () => void;
|
|
17
62
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { onNavigationCommit, preloadFromLinkTags, } from "./navigationCache.js";
|
|
1
2
|
export function validateClickEvent(event, target) {
|
|
2
3
|
// should this only work for left click?
|
|
3
4
|
if (event.button !== 0) {
|
|
@@ -44,7 +45,7 @@ export async function navigate(href, options = { history: "push" }) {
|
|
|
44
45
|
else {
|
|
45
46
|
window.history.replaceState({ path: href }, "", url);
|
|
46
47
|
}
|
|
47
|
-
await globalThis.__rsc_callServer;
|
|
48
|
+
await globalThis.__rsc_callServer(null, null, "navigation");
|
|
48
49
|
const scrollToTop = options.info?.scrollToTop ?? true;
|
|
49
50
|
const scrollBehavior = options.info?.scrollBehavior ?? "instant";
|
|
50
51
|
if (scrollToTop && history.scrollRestoration === "auto") {
|
|
@@ -63,6 +64,47 @@ function saveScrollPosition(x, y) {
|
|
|
63
64
|
scrollY: y,
|
|
64
65
|
}, "", window.location.href);
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Initializes client-side navigation for Single Page App (SPA) behavior.
|
|
69
|
+
*
|
|
70
|
+
* Intercepts clicks on internal links and fetches page content without full-page reloads.
|
|
71
|
+
* Returns a handleResponse function to pass to initClient.
|
|
72
|
+
*
|
|
73
|
+
* @param opts.scrollToTop - Scroll to top after navigation (default: true)
|
|
74
|
+
* @param opts.scrollBehavior - How to scroll: 'instant', 'smooth', or 'auto' (default: 'instant')
|
|
75
|
+
* @param opts.onNavigate - Callback executed after history push but before RSC fetch
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // Basic usage
|
|
79
|
+
* import { initClient, initClientNavigation } from "rwsdk/client";
|
|
80
|
+
*
|
|
81
|
+
* const { handleResponse, onHydrated } = initClientNavigation();
|
|
82
|
+
* initClient({ handleResponse, onHydrated });
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* // With custom scroll behavior
|
|
86
|
+
* const { handleResponse } = initClientNavigation({
|
|
87
|
+
* scrollBehavior: "smooth",
|
|
88
|
+
* scrollToTop: true,
|
|
89
|
+
* });
|
|
90
|
+
* initClient({ handleResponse });
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // Preserve scroll position (e.g., for infinite scroll)
|
|
94
|
+
* const { handleResponse } = initClientNavigation({
|
|
95
|
+
* scrollToTop: false,
|
|
96
|
+
* });
|
|
97
|
+
* initClient({ handleResponse });
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* // With navigation callback
|
|
101
|
+
* const { handleResponse } = initClientNavigation({
|
|
102
|
+
* onNavigate: () => {
|
|
103
|
+
* console.log("Navigating to:", window.location.href);
|
|
104
|
+
* },
|
|
105
|
+
* });
|
|
106
|
+
* initClient({ handleResponse });
|
|
107
|
+
*/
|
|
66
108
|
export function initClientNavigation(opts = {}) {
|
|
67
109
|
IS_CLIENT_NAVIGATION = true;
|
|
68
110
|
history.scrollRestoration = "auto";
|
|
@@ -74,22 +116,34 @@ export function initClientNavigation(opts = {}) {
|
|
|
74
116
|
const el = event.target;
|
|
75
117
|
const a = el.closest("a");
|
|
76
118
|
const href = a?.getAttribute("href");
|
|
77
|
-
navigate(href);
|
|
119
|
+
await navigate(href);
|
|
78
120
|
}, true);
|
|
79
121
|
window.addEventListener("popstate", async function handlePopState() {
|
|
80
|
-
|
|
81
|
-
await globalThis.__rsc_callServer();
|
|
122
|
+
await globalThis.__rsc_callServer(null, null, "navigation");
|
|
82
123
|
});
|
|
83
|
-
|
|
124
|
+
function handleResponse(response) {
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
// Redirect to the current page (window.location) to show the error
|
|
127
|
+
// This means the page that produced the error is called twice.
|
|
128
|
+
window.location.href = window.location.href;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
// Store cacheStorage globally for use in client.tsx
|
|
134
|
+
if (opts.cacheStorage && typeof globalThis !== "undefined") {
|
|
135
|
+
globalThis.__rsc_cacheStorage = opts.cacheStorage;
|
|
136
|
+
}
|
|
137
|
+
function onHydrated() {
|
|
138
|
+
// After each RSC hydration/update, increment generation and evict old caches,
|
|
139
|
+
// then warm the navigation cache based on any <link rel="x-prefetch"> tags
|
|
140
|
+
// rendered for the current location.
|
|
141
|
+
onNavigationCommit(undefined, opts.cacheStorage);
|
|
142
|
+
void preloadFromLinkTags(undefined, undefined, opts.cacheStorage);
|
|
143
|
+
}
|
|
144
|
+
// Return callbacks for use with initClient
|
|
84
145
|
return {
|
|
85
|
-
handleResponse
|
|
86
|
-
|
|
87
|
-
// Redirect to the current page (window.location) to show the error
|
|
88
|
-
// This means the page that produced the error is called twice.
|
|
89
|
-
window.location.href = window.location.href;
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
return true;
|
|
93
|
-
},
|
|
146
|
+
handleResponse,
|
|
147
|
+
onHydrated,
|
|
94
148
|
};
|
|
95
149
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface NavigationCacheEnvironment {
|
|
2
|
+
isSecureContext: boolean;
|
|
3
|
+
origin: string;
|
|
4
|
+
caches?: CacheStorage;
|
|
5
|
+
fetch: typeof fetch;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Interface for a single cache instance, mirroring the Cache API.
|
|
9
|
+
*/
|
|
10
|
+
export interface NavigationCache {
|
|
11
|
+
put(request: Request, response: Response): Promise<void>;
|
|
12
|
+
match(request: Request): Promise<Response | undefined>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Interface for cache storage, mirroring the CacheStorage API.
|
|
16
|
+
*/
|
|
17
|
+
export interface NavigationCacheStorage {
|
|
18
|
+
open(cacheName: string): Promise<NavigationCache>;
|
|
19
|
+
delete(cacheName: string): Promise<boolean>;
|
|
20
|
+
keys(): Promise<string[]>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates a default NavigationCacheStorage implementation that wraps the browser's CacheStorage API.
|
|
24
|
+
* This maintains the current generation-based cache naming and eviction logic.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createDefaultNavigationCacheStorage(env?: NavigationCacheEnvironment): NavigationCacheStorage | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Preloads the RSC navigation response for a given URL into the Cache API.
|
|
29
|
+
*
|
|
30
|
+
* This issues a GET request with the `__rsc` query parameter set, and, on a
|
|
31
|
+
* successful response, stores it in a versioned Cache using `cache.put`.
|
|
32
|
+
*
|
|
33
|
+
* See MDN for Cache interface semantics:
|
|
34
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/Cache
|
|
35
|
+
*/
|
|
36
|
+
export declare function preloadNavigationUrl(rawUrl: URL | string, env?: NavigationCacheEnvironment, cacheStorage?: NavigationCacheStorage): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Attempts to retrieve a cached navigation response for the given URL.
|
|
39
|
+
*
|
|
40
|
+
* Returns the cached Response if found, or undefined if not cached or if
|
|
41
|
+
* CacheStorage is unavailable.
|
|
42
|
+
*/
|
|
43
|
+
export declare function getCachedNavigationResponse(rawUrl: URL | string, env?: NavigationCacheEnvironment, cacheStorage?: NavigationCacheStorage): Promise<Response | undefined>;
|
|
44
|
+
/**
|
|
45
|
+
* Cleans up old generation caches for the current tab.
|
|
46
|
+
*
|
|
47
|
+
* This should be called after navigation commits to evict cache entries from
|
|
48
|
+
* previous navigations. It runs asynchronously via requestIdleCallback or
|
|
49
|
+
* setTimeout to avoid blocking the critical path.
|
|
50
|
+
*/
|
|
51
|
+
export declare function evictOldGenerationCaches(env?: NavigationCacheEnvironment, cacheStorage?: NavigationCacheStorage): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Increments the generation counter and schedules cleanup of old caches.
|
|
54
|
+
*
|
|
55
|
+
* This should be called after navigation commits to mark the current generation
|
|
56
|
+
* as complete and prepare for the next navigation cycle.
|
|
57
|
+
*/
|
|
58
|
+
export declare function onNavigationCommit(env?: NavigationCacheEnvironment, cacheStorage?: NavigationCacheStorage): void;
|
|
59
|
+
/**
|
|
60
|
+
* Scan the document for `<link rel="x-prefetch" href="...">` elements that point
|
|
61
|
+
* to same-origin paths and prefetch their RSC navigation responses into the
|
|
62
|
+
* Cache API.
|
|
63
|
+
*
|
|
64
|
+
* This is invoked after client navigations to warm the navigation cache in
|
|
65
|
+
* the background. We intentionally keep Cache usage write-only for now; reads
|
|
66
|
+
* still go through the normal fetch path.
|
|
67
|
+
*/
|
|
68
|
+
export declare function preloadFromLinkTags(doc?: Document, env?: NavigationCacheEnvironment, cacheStorage?: NavigationCacheStorage): Promise<void>;
|