rwsdk 1.0.0-beta.54 → 1.0.0-beta.55
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/dist/runtime/client/client.js +32 -32
- package/dist/runtime/client/navigation.js +7 -0
- package/dist/runtime/client/navigation.test.js +53 -3
- package/dist/runtime/server.d.ts +4 -4
- package/dist/runtime/server.js +4 -8
- package/dist/vite/transformServerFunctions.test.mjs +30 -0
- package/package.json +1 -1
|
@@ -64,22 +64,7 @@ export const fetchTransport = (transportContext) => {
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
if (transportContext.handleResponse) {
|
|
69
|
-
const response = await fetchPromise;
|
|
70
|
-
const shouldContinue = transportContext.handleResponse(response);
|
|
71
|
-
if (!shouldContinue) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
// Continue with the response if handler returned true
|
|
75
|
-
const streamData = createFromFetch(Promise.resolve(response), {
|
|
76
|
-
callServer: fetchCallServer,
|
|
77
|
-
});
|
|
78
|
-
if (source === "navigation" || source === "action") {
|
|
79
|
-
transportContext.setRscPayload(streamData);
|
|
80
|
-
}
|
|
81
|
-
const result = await streamData;
|
|
82
|
-
const rawActionResult = result.actionResult;
|
|
67
|
+
const processActionResponse = (rawActionResult) => {
|
|
83
68
|
if (isActionResponse(rawActionResult)) {
|
|
84
69
|
const actionResponse = rawActionResult.__rw_action_response;
|
|
85
70
|
const handledByHook = transportContext.onActionResponse?.(actionResponse) === true;
|
|
@@ -90,34 +75,49 @@ export const fetchTransport = (transportContext) => {
|
|
|
90
75
|
window.location.href = location;
|
|
91
76
|
return undefined;
|
|
92
77
|
}
|
|
78
|
+
if (actionResponse.status >= 400) {
|
|
79
|
+
throw new Error(`Server function returned status ${actionResponse.status}`);
|
|
80
|
+
}
|
|
93
81
|
}
|
|
94
82
|
return rawActionResult;
|
|
95
83
|
}
|
|
96
84
|
return rawActionResult;
|
|
85
|
+
};
|
|
86
|
+
// If there's a response handler, check the response first
|
|
87
|
+
if (transportContext.handleResponse) {
|
|
88
|
+
const response = await fetchPromise;
|
|
89
|
+
const shouldContinue = transportContext.handleResponse(response);
|
|
90
|
+
if (!shouldContinue) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
// Continue with the response if handler returned true
|
|
94
|
+
const streamData = createFromFetch(Promise.resolve(response), {
|
|
95
|
+
callServer: fetchCallServer,
|
|
96
|
+
});
|
|
97
|
+
if (source === "navigation" || source === "action") {
|
|
98
|
+
transportContext.setRscPayload(streamData);
|
|
99
|
+
}
|
|
100
|
+
const result = await streamData;
|
|
101
|
+
return processActionResponse(result.actionResult);
|
|
97
102
|
}
|
|
98
103
|
// Original behavior when no handler is present
|
|
99
|
-
const
|
|
104
|
+
const response = await fetchPromise;
|
|
105
|
+
console.log("[fetchTransport] Response status:", response.status);
|
|
106
|
+
const location = response.headers.get("Location");
|
|
107
|
+
console.log("[fetchTransport] Location header:", location);
|
|
108
|
+
if (response.status >= 300 && response.status < 400 && location) {
|
|
109
|
+
console.log("[fetchTransport] Redirecting to:", location);
|
|
110
|
+
window.location.href = location;
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const streamData = createFromFetch(Promise.resolve(response), {
|
|
100
114
|
callServer: fetchCallServer,
|
|
101
115
|
});
|
|
102
116
|
if (source === "navigation" || source === "action") {
|
|
103
117
|
transportContext.setRscPayload(streamData);
|
|
104
118
|
}
|
|
105
119
|
const result = await streamData;
|
|
106
|
-
|
|
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;
|
|
120
|
+
return processActionResponse(result.actionResult);
|
|
121
121
|
};
|
|
122
122
|
return fetchCallServer;
|
|
123
123
|
};
|
|
@@ -122,6 +122,13 @@ export function initClientNavigation(opts = {}) {
|
|
|
122
122
|
await globalThis.__rsc_callServer(null, null, "navigation");
|
|
123
123
|
});
|
|
124
124
|
function handleResponse(response) {
|
|
125
|
+
if (response.status >= 300 && response.status < 400) {
|
|
126
|
+
const location = response.headers.get("Location");
|
|
127
|
+
if (location) {
|
|
128
|
+
window.location.href = location;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
125
132
|
if (!response.ok) {
|
|
126
133
|
// Redirect to the current page (window.location) to show the error
|
|
127
134
|
// This means the page that produced the error is called twice.
|
|
@@ -1,8 +1,31 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { validateClickEvent } from "./navigation";
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
+
import { validateClickEvent, initClientNavigation } from "./navigation";
|
|
3
|
+
// Mocking browser globals
|
|
4
|
+
vi.stubGlobal("window", {
|
|
5
|
+
location: { href: "http://localhost/" },
|
|
6
|
+
addEventListener: vi.fn(),
|
|
7
|
+
history: { scrollRestoration: "auto" },
|
|
8
|
+
});
|
|
9
|
+
vi.stubGlobal("document", {
|
|
10
|
+
addEventListener: vi.fn(),
|
|
11
|
+
});
|
|
12
|
+
vi.stubGlobal("history", {
|
|
13
|
+
scrollRestoration: "auto",
|
|
14
|
+
});
|
|
15
|
+
vi.stubGlobal("Headers", class {
|
|
16
|
+
constructor(init) {
|
|
17
|
+
this.map = {};
|
|
18
|
+
if (init && init.Location) {
|
|
19
|
+
this.map.location = init.Location;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
get(name) {
|
|
23
|
+
return this.map[name.toLowerCase()] || null;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
3
26
|
describe("clientNavigation", () => {
|
|
4
27
|
let mockEvent = {
|
|
5
|
-
button: 0,
|
|
28
|
+
button: 0,
|
|
6
29
|
metaKey: false,
|
|
7
30
|
altKey: false,
|
|
8
31
|
shiftKey: false,
|
|
@@ -52,3 +75,30 @@ describe("clientNavigation", () => {
|
|
|
52
75
|
})).toBe(true);
|
|
53
76
|
});
|
|
54
77
|
});
|
|
78
|
+
describe("initClientNavigation", () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
window.location.href = "http://localhost/";
|
|
81
|
+
vi.clearAllMocks();
|
|
82
|
+
});
|
|
83
|
+
it("handleResponse should follow redirects", () => {
|
|
84
|
+
const { handleResponse } = initClientNavigation();
|
|
85
|
+
const mockResponse = {
|
|
86
|
+
status: 302,
|
|
87
|
+
headers: new Headers({ Location: "/new-page" }),
|
|
88
|
+
ok: false,
|
|
89
|
+
};
|
|
90
|
+
const result = handleResponse(mockResponse);
|
|
91
|
+
expect(result).toBe(false);
|
|
92
|
+
expect(window.location.href).toBe("/new-page");
|
|
93
|
+
});
|
|
94
|
+
it("handleResponse should reload on error", () => {
|
|
95
|
+
const { handleResponse } = initClientNavigation();
|
|
96
|
+
const mockResponse = {
|
|
97
|
+
status: 500,
|
|
98
|
+
ok: false,
|
|
99
|
+
};
|
|
100
|
+
const result = handleResponse(mockResponse);
|
|
101
|
+
expect(result).toBe(false);
|
|
102
|
+
expect(window.location.href).toBe("http://localhost/");
|
|
103
|
+
});
|
|
104
|
+
});
|
package/dist/runtime/server.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
type Interruptor<TArgs extends any[] = any[]
|
|
1
|
+
type Interruptor<TArgs extends any[] = any[]> = (context: {
|
|
2
2
|
request: Request;
|
|
3
3
|
ctx: Record<string, any>;
|
|
4
4
|
args: TArgs;
|
|
5
|
-
}) => Promise<Response | void
|
|
5
|
+
}) => Promise<Response | void> | Response | void;
|
|
6
6
|
type ServerFunction<TArgs extends any[] = any[], TResult = any> = (...args: TArgs) => Promise<TResult>;
|
|
7
7
|
type ServerFunctionOptions = {
|
|
8
8
|
method?: "GET" | "POST";
|
|
@@ -29,7 +29,7 @@ type WrappedServerFunction<TArgs extends any[] = any[], TResult = any> = {
|
|
|
29
29
|
* })
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
|
-
export declare function serverQuery<TArgs extends any[]
|
|
32
|
+
export declare function serverQuery<TArgs extends any[], TResult>(fnsOrFn: ServerFunction<TArgs, TResult> | [...Interruptor<TArgs>[], ServerFunction<TArgs, TResult>], options?: ServerFunctionOptions): WrappedServerFunction<TArgs, TResult>;
|
|
33
33
|
/**
|
|
34
34
|
* Wrap a function to be used as a server action.
|
|
35
35
|
*
|
|
@@ -48,5 +48,5 @@ export declare function serverQuery<TArgs extends any[] = any[], TResult = any>(
|
|
|
48
48
|
* })
|
|
49
49
|
* ```
|
|
50
50
|
*/
|
|
51
|
-
export declare function serverAction<TArgs extends any[]
|
|
51
|
+
export declare function serverAction<TArgs extends any[], TResult>(fnsOrFn: ServerFunction<TArgs, TResult> | [...Interruptor<TArgs>[], ServerFunction<TArgs, TResult>], options?: ServerFunctionOptions): WrappedServerFunction<TArgs, TResult>;
|
|
52
52
|
export {};
|
package/dist/runtime/server.js
CHANGED
|
@@ -6,18 +6,14 @@ function createServerFunction(fns, mainFn, options) {
|
|
|
6
6
|
for (const fn of fns) {
|
|
7
7
|
const result = await fn({ request, ctx, args });
|
|
8
8
|
if (result instanceof Response) {
|
|
9
|
-
// We
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
// In the RSC context, throwing a Response is a common pattern to short-circuit.
|
|
13
|
-
throw result;
|
|
9
|
+
// We return the Response so it can be handled by the action handler
|
|
10
|
+
// and serialized into the RSC stream via normalizeActionResult.
|
|
11
|
+
return result;
|
|
14
12
|
}
|
|
15
13
|
}
|
|
16
14
|
return mainFn(...args);
|
|
17
15
|
};
|
|
18
|
-
wrapped.method = options?.method ?? "POST";
|
|
19
|
-
// User said: "export const getProject = serverQuery(...) // Defaults to GET"
|
|
20
|
-
// So serverQuery defaults to GET, serverAction defaults to POST?
|
|
16
|
+
wrapped.method = options?.method ?? "POST";
|
|
21
17
|
return wrapped;
|
|
22
18
|
}
|
|
23
19
|
/**
|
|
@@ -119,6 +119,33 @@ export default serverQuery(async (id) => {
|
|
|
119
119
|
export default serverAction(async (id) => {
|
|
120
120
|
return { id, name: "Project X" };
|
|
121
121
|
});
|
|
122
|
+
`;
|
|
123
|
+
let SERVER_QUERY_ARRAY_CODE = `
|
|
124
|
+
"use server";
|
|
125
|
+
export const getProject = serverQuery([
|
|
126
|
+
auth,
|
|
127
|
+
async (id) => {
|
|
128
|
+
return { id, name: "Project X" };
|
|
129
|
+
}
|
|
130
|
+
]);
|
|
131
|
+
`;
|
|
132
|
+
let SERVER_ACTION_ARRAY_CODE = `
|
|
133
|
+
"use server";
|
|
134
|
+
export const upvote = serverAction([
|
|
135
|
+
auth,
|
|
136
|
+
async (id) => {
|
|
137
|
+
return { id, count: 1 };
|
|
138
|
+
}
|
|
139
|
+
]);
|
|
140
|
+
`;
|
|
141
|
+
let SERVER_QUERY_ARRAY_POST_CODE = `
|
|
142
|
+
"use server";
|
|
143
|
+
export const getProject = serverQuery([
|
|
144
|
+
auth,
|
|
145
|
+
async (id) => {
|
|
146
|
+
return { id, name: "Project X" };
|
|
147
|
+
}
|
|
148
|
+
], { method: "POST" });
|
|
122
149
|
`;
|
|
123
150
|
const TEST_CASES = {
|
|
124
151
|
COMMENT_CODE,
|
|
@@ -137,6 +164,9 @@ export default serverAction(async (id) => {
|
|
|
137
164
|
SERVER_ACTION_CODE,
|
|
138
165
|
SERVER_QUERY_DEFAULT_CODE,
|
|
139
166
|
SERVER_ACTION_DEFAULT_CODE,
|
|
167
|
+
SERVER_QUERY_ARRAY_CODE,
|
|
168
|
+
SERVER_ACTION_ARRAY_CODE,
|
|
169
|
+
SERVER_QUERY_ARRAY_POST_CODE,
|
|
140
170
|
};
|
|
141
171
|
describe("TRANSFORMS", () => {
|
|
142
172
|
for (const [key, CODE] of Object.entries(TEST_CASES)) {
|