rwsdk 1.0.0-beta.54 → 1.0.0-beta.56

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.
@@ -379,7 +379,7 @@ export async function runRelease(cwd, projectDir, resourceUniqueKey) {
379
379
  const stdout = result.stdout;
380
380
  // Extract deployment URL from output
381
381
  log("Extracting deployment URL from output");
382
- const urlMatch = stdout.match(/https:\/\/([a-zA-Z0-9-]+)\.redwoodjs\.workers\.dev/);
382
+ const urlMatch = stdout.match(/https:\/\/([a-zA-Z0-9-]+)(?:\.[a-zA-Z0-9-]+)?\.workers\.dev/);
383
383
  if (!urlMatch || !urlMatch[0]) {
384
384
  log("ERROR: Could not extract deployment URL from release output");
385
385
  // Log more details about the output for debugging
@@ -64,22 +64,7 @@ export const fetchTransport = (transportContext) => {
64
64
  });
65
65
  }
66
66
  }
67
- // If there's a response handler, check the response first
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,46 @@ 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 streamData = createFromFetch(fetchPromise, {
104
+ const response = await fetchPromise;
105
+ const location = response.headers.get("Location");
106
+ if (response.status >= 300 && response.status < 400 && location) {
107
+ window.location.href = location;
108
+ return undefined;
109
+ }
110
+ const streamData = createFromFetch(Promise.resolve(response), {
100
111
  callServer: fetchCallServer,
101
112
  });
102
113
  if (source === "navigation" || source === "action") {
103
114
  transportContext.setRscPayload(streamData);
104
115
  }
105
116
  const result = await streamData;
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;
117
+ return processActionResponse(result.actionResult);
121
118
  };
122
119
  return fetchCallServer;
123
120
  };
@@ -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, // right click
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
+ });
@@ -1,8 +1,8 @@
1
- type Interruptor<TArgs extends any[] = any[], TResult = any> = (context: {
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 | TResult> | Response | void | TResult;
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[] = any[], TResult = any>(fnsOrFn: ServerFunction<TArgs, TResult> | [...Interruptor<TArgs, TResult>[], ServerFunction<TArgs, TResult>], options?: ServerFunctionOptions): WrappedServerFunction<TArgs, TResult>;
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[] = any[], TResult = any>(fnsOrFn: ServerFunction<TArgs, TResult> | [...Interruptor<TArgs, TResult>[], ServerFunction<TArgs, TResult>], options?: ServerFunctionOptions): WrappedServerFunction<TArgs, TResult>;
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 {};
@@ -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 can't easily return a Response from a server action function
10
- // because the return type is expected to be TResult.
11
- // However, if the interruptor returns a Response, it usually means "stop and return this HTTP response".
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"; // Default to POST if not specified, though user said serverQuery defaults to GET?
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)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "1.0.0-beta.54",
3
+ "version": "1.0.0-beta.56",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {
@@ -153,68 +153,68 @@
153
153
  "author": "RedwoodSDK <peter@redwoodjs.com>",
154
154
  "license": "MIT",
155
155
  "dependencies": {
156
- "@ast-grep/napi": "~0.39.0",
157
- "@cloudflare/workers-types": "~4.20260218.0",
156
+ "@ast-grep/napi": "~0.41.0",
157
+ "@cloudflare/workers-types": "~4.20260305.1",
158
158
  "@mdx-js/mdx": "~3.1.1",
159
- "@puppeteer/browsers": "~2.10.0",
159
+ "@puppeteer/browsers": "~2.13.0",
160
160
  "@types/decompress": "~4.2.7",
161
161
  "@types/fs-extra": "~11.0.4",
162
- "@types/glob": "^8.1.0",
162
+ "@types/glob": "^9.0.0",
163
163
  "@types/react": "~19.2.14",
164
164
  "@types/react-dom": "~19.2.3",
165
- "@types/react-is": "~19.0.0",
166
- "@vitejs/plugin-react": "~5.0.0",
167
- "chokidar": "~4.0.0",
168
- "debug": "~4.4.0",
165
+ "@types/react-is": "~19.2.0",
166
+ "@vitejs/plugin-react": "~5.1.4",
167
+ "chokidar": "~5.0.0",
168
+ "debug": "~4.4.3",
169
169
  "decompress": "~4.2.1",
170
- "enhanced-resolve": "~5.18.1",
171
- "eventsource-parser": "~3.0.0",
172
- "execa": "~9.6.0",
170
+ "enhanced-resolve": "~5.20.0",
171
+ "eventsource-parser": "~3.0.6",
172
+ "execa": "~9.6.1",
173
173
  "find-up": "~8.0.0",
174
- "fs-extra": "~11.3.0",
174
+ "fs-extra": "~11.3.4",
175
175
  "get-port": "^7.1.0",
176
- "glob": "~11.1.0",
177
- "ignore": "~7.0.4",
176
+ "glob": "~13.0.6",
177
+ "ignore": "~7.0.5",
178
178
  "jsonc-parser": "~3.3.1",
179
- "kysely": "~0.28.2",
179
+ "kysely": "~0.28.11",
180
180
  "kysely-do": "~0.0.1-rc.1",
181
181
  "lodash": "~4.17.23",
182
- "magic-string": "~0.30.17",
182
+ "magic-string": "~0.30.21",
183
183
  "picocolors": "~1.1.1",
184
184
  "proper-lockfile": "~4.1.2",
185
- "puppeteer-core": "~24.22.0",
186
- "react-is": "~19.1.0",
187
- "rsc-html-stream": "~0.0.6",
185
+ "puppeteer-core": "~24.38.0",
186
+ "react-is": "~19.2.4",
187
+ "rsc-html-stream": "~0.0.7",
188
188
  "server-only": "^0.0.1",
189
189
  "tmp-promise": "~3.0.3",
190
- "ts-morph": "~27.0.0",
190
+ "ts-morph": "~27.0.2",
191
191
  "unique-names-generator": "~4.7.1",
192
- "vibe-rules": "~0.3.0",
193
- "vite-tsconfig-paths": "~5.1.4"
192
+ "vibe-rules": "~0.3.91",
193
+ "vite-tsconfig-paths": "~6.1.1"
194
194
  },
195
195
  "peerDependencies": {
196
- "@cloudflare/vite-plugin": "^1.25.1",
197
- "capnweb": "~0.2.0",
196
+ "@cloudflare/vite-plugin": "^1.26.0",
197
+ "capnweb": "~0.5.0",
198
198
  "react": ">=19.2.0-0 <19.3.0 || >=19.3.0-0 <20.0.0",
199
199
  "react-dom": ">=19.2.0-0 <19.3.0 || >=19.3.0-0 <20.0.0",
200
200
  "react-server-dom-webpack": ">=19.2.0-0 <19.3.0 || >=19.3.0-0 <20.0.0",
201
201
  "vite": "^6.2.6 || 7.x",
202
- "wrangler": "^4.66.0"
202
+ "wrangler": "^4.70.0"
203
203
  },
204
204
  "packageManager": "pnpm@10.28.2",
205
205
  "devDependencies": {
206
- "@cloudflare/vite-plugin": "1.25.1",
207
- "capnweb": "~0.2.0",
206
+ "@cloudflare/vite-plugin": "1.26.0",
207
+ "capnweb": "~0.5.0",
208
208
  "@types/debug": "~4.1.12",
209
209
  "@types/js-beautify": "~1.14.3",
210
- "@types/lodash": "~4.17.16",
211
- "@types/node": "~24.10.0",
210
+ "@types/lodash": "~4.17.24",
211
+ "@types/node": "~25.3.3",
212
212
  "@types/proper-lockfile": "~4.1.4",
213
213
  "js-beautify": "~1.15.4",
214
- "semver": "~7.7.1",
214
+ "semver": "~7.7.4",
215
215
  "tsx": "~4.21.0",
216
- "typescript": "~5.9.0",
216
+ "typescript": "~5.9.3",
217
217
  "vite": "~7.3.1",
218
- "vitest": "~3.2.0"
218
+ "vitest": "~4.0.18"
219
219
  }
220
220
  }