rwsdk 1.0.0-beta.43 → 1.0.0-beta.45

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.
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { describe, expect, it } from "vitest";
3
- import { defineRoutes, layout, matchPath, prefix, render, route, } from "./router";
3
+ import { defineRoutes, except, layout, matchPath, prefix, render, route, } from "./router";
4
4
  describe("matchPath", () => {
5
5
  // Test case 1: Static paths
6
6
  it("should match static paths", () => {
@@ -34,6 +34,11 @@ describe("matchPath", () => {
34
34
  it("should match empty wildcard", () => {
35
35
  expect(matchPath("/files/*/", "/files//")).toEqual({ $0: "" });
36
36
  });
37
+ it("should match wildcard route against root path", () => {
38
+ // Wildcard route should match the root path "/"
39
+ // This tests the regression where route("*") stopped matching "/"
40
+ expect(matchPath("/*", "/")).toEqual({ $0: "" });
41
+ });
37
42
  // Test case 4: Paths with both parameters and wildcards
38
43
  it("should match paths with both parameters and wildcards", () => {
39
44
  expect(matchPath("/products/:productId/*/", "/products/abc/details/more/")).toEqual({ productId: "abc", $0: "details/more" });
@@ -62,6 +67,7 @@ describe("defineRoutes - Request Handling Behavior", () => {
62
67
  const createMockDependencies = () => {
63
68
  const mockRequestInfo = {
64
69
  request: new Request("http://localhost:3000/"),
70
+ path: "/",
65
71
  params: {},
66
72
  ctx: {},
67
73
  rw: {
@@ -293,6 +299,109 @@ describe("defineRoutes - Request Handling Behavior", () => {
293
299
  expect(executionOrder).toEqual(["prefixedMiddleware"]);
294
300
  expect(await response.text()).toBe("From prefixed middleware");
295
301
  });
302
+ it("should pass prefix parameters to route handlers", async () => {
303
+ let capturedParams = null;
304
+ const TaskDetailPage = (requestInfo) => {
305
+ capturedParams = requestInfo.params;
306
+ return React.createElement("div", {}, "Task Detail");
307
+ };
308
+ const router = defineRoutes([
309
+ ...prefix("/tasks/:containerId", [route("/", TaskDetailPage)]),
310
+ ]);
311
+ const deps = createMockDependencies();
312
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/tasks/123/");
313
+ const request = new Request("http://localhost:3000/tasks/123/");
314
+ await router.handle({
315
+ request,
316
+ renderPage: deps.mockRenderPage,
317
+ getRequestInfo: deps.getRequestInfo,
318
+ onError: deps.onError,
319
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
320
+ rscActionHandler: deps.mockRscActionHandler,
321
+ });
322
+ expect(capturedParams).toEqual({ containerId: "123" });
323
+ });
324
+ it("should pass prefix parameters to middlewares within a parameterized prefix", async () => {
325
+ const executionOrder = [];
326
+ let capturedParams = null;
327
+ const prefixedMiddleware = (requestInfo) => {
328
+ executionOrder.push("prefixedMiddleware");
329
+ capturedParams = requestInfo.params;
330
+ };
331
+ const PageComponent = () => {
332
+ executionOrder.push("PageComponent");
333
+ return React.createElement("div", {}, "Page");
334
+ };
335
+ const router = defineRoutes([
336
+ ...prefix("/tasks/:containerId", [
337
+ prefixedMiddleware,
338
+ route("/", PageComponent),
339
+ ]),
340
+ ]);
341
+ const deps = createMockDependencies();
342
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/tasks/123/");
343
+ const request = new Request("http://localhost:3000/tasks/123/");
344
+ await router.handle({
345
+ request,
346
+ renderPage: deps.mockRenderPage,
347
+ getRequestInfo: deps.getRequestInfo,
348
+ onError: deps.onError,
349
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
350
+ rscActionHandler: deps.mockRscActionHandler,
351
+ });
352
+ expect(executionOrder).toEqual(["prefixedMiddleware", "PageComponent"]);
353
+ // The wildcard captures the trailing slash as an empty string
354
+ expect(capturedParams).toEqual({ containerId: "123", $0: "" });
355
+ });
356
+ it("should pass prefix parameters to route handlers (array)", async () => {
357
+ let capturedParamsInMiddleware = null;
358
+ let capturedParamsInComponent = null;
359
+ const middleware = (requestInfo) => {
360
+ capturedParamsInMiddleware = requestInfo.params;
361
+ };
362
+ const Component = (requestInfo) => {
363
+ capturedParamsInComponent = requestInfo.params;
364
+ return React.createElement("div", {}, "Component");
365
+ };
366
+ const router = defineRoutes([
367
+ ...prefix("/tasks/:containerId", [route("/", [middleware, Component])]),
368
+ ]);
369
+ const deps = createMockDependencies();
370
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/tasks/123/");
371
+ const request = new Request("http://localhost:3000/tasks/123/");
372
+ await router.handle({
373
+ request,
374
+ renderPage: deps.mockRenderPage,
375
+ getRequestInfo: deps.getRequestInfo,
376
+ onError: deps.onError,
377
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
378
+ rscActionHandler: deps.mockRscActionHandler,
379
+ });
380
+ expect(capturedParamsInMiddleware).toEqual({ containerId: "123" });
381
+ expect(capturedParamsInComponent).toEqual({ containerId: "123" });
382
+ });
383
+ it("should match even if prefix has a trailing slash", async () => {
384
+ let capturedParams = null;
385
+ const TaskDetailPage = (requestInfo) => {
386
+ capturedParams = requestInfo.params;
387
+ return React.createElement("div", {}, "Task Detail");
388
+ };
389
+ const router = defineRoutes([
390
+ ...prefix("/tasks/:containerId/", [route("/", TaskDetailPage)]),
391
+ ]);
392
+ const deps = createMockDependencies();
393
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/tasks/123/");
394
+ const request = new Request("http://localhost:3000/tasks/123/");
395
+ await router.handle({
396
+ request,
397
+ renderPage: deps.mockRenderPage,
398
+ getRequestInfo: deps.getRequestInfo,
399
+ onError: deps.onError,
400
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
401
+ rscActionHandler: deps.mockRscActionHandler,
402
+ });
403
+ expect(capturedParams).toEqual({ containerId: "123" });
404
+ });
296
405
  });
297
406
  describe("RSC Action Handling", () => {
298
407
  it("should handle RSC actions before the first route definition", async () => {
@@ -437,6 +546,28 @@ describe("defineRoutes - Request Handling Behavior", () => {
437
546
  expect(response.status).toBe(404);
438
547
  expect(await response.text()).toBe("Not Found");
439
548
  });
549
+ it("should match wildcard route against root path", async () => {
550
+ // Regression test: wildcard route should match the root path "/"
551
+ const executionOrder = [];
552
+ const WildcardPage = () => {
553
+ executionOrder.push("WildcardPage");
554
+ return React.createElement("div", {}, "Wildcard Page");
555
+ };
556
+ const router = defineRoutes([route("*", WildcardPage)]);
557
+ const deps = createMockDependencies();
558
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/");
559
+ deps.mockRequestInfo.path = "/";
560
+ const request = new Request("http://localhost:3000/");
561
+ await router.handle({
562
+ request,
563
+ renderPage: deps.mockRenderPage,
564
+ getRequestInfo: deps.getRequestInfo,
565
+ onError: deps.onError,
566
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
567
+ rscActionHandler: deps.mockRscActionHandler,
568
+ });
569
+ expect(executionOrder).toEqual(["WildcardPage"]);
570
+ });
440
571
  });
441
572
  describe("Multiple Render Blocks with SSR Configuration", () => {
442
573
  it("should short-circuit on first matching render block and not apply later configurations", async () => {
@@ -884,4 +1015,242 @@ describe("defineRoutes - Request Handling Behavior", () => {
884
1015
  expect(await response.text()).toBe("Rendered: Element");
885
1016
  });
886
1017
  });
1018
+ describe("except - Error Handling", () => {
1019
+ it("should catch errors from global middleware", async () => {
1020
+ const errorMessage = "Middleware error";
1021
+ const middleware = () => {
1022
+ throw new Error(errorMessage);
1023
+ };
1024
+ const errorHandler = except((error) => {
1025
+ return new Response(`Caught: ${error instanceof Error ? error.message : String(error)}`, {
1026
+ status: 500,
1027
+ });
1028
+ });
1029
+ const router = defineRoutes([
1030
+ middleware,
1031
+ errorHandler,
1032
+ route("/test/", () => React.createElement("div")),
1033
+ ]);
1034
+ const deps = createMockDependencies();
1035
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/test/");
1036
+ const request = new Request("http://localhost:3000/test/");
1037
+ const response = await router.handle({
1038
+ request,
1039
+ renderPage: deps.mockRenderPage,
1040
+ getRequestInfo: deps.getRequestInfo,
1041
+ onError: deps.onError,
1042
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
1043
+ rscActionHandler: deps.mockRscActionHandler,
1044
+ });
1045
+ expect(response.status).toBe(500);
1046
+ expect(await response.text()).toBe(`Caught: ${errorMessage}`);
1047
+ });
1048
+ it("should catch errors from route handlers (components)", async () => {
1049
+ const errorMessage = "Component error";
1050
+ const PageComponent = () => {
1051
+ throw new Error(errorMessage);
1052
+ };
1053
+ const errorHandler = except((error) => {
1054
+ return new Response(`Caught: ${error instanceof Error ? error.message : String(error)}`, {
1055
+ status: 500,
1056
+ });
1057
+ });
1058
+ const router = defineRoutes([
1059
+ errorHandler,
1060
+ route("/test/", PageComponent),
1061
+ ]);
1062
+ const deps = createMockDependencies();
1063
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/test/");
1064
+ const request = new Request("http://localhost:3000/test/");
1065
+ const response = await router.handle({
1066
+ request,
1067
+ renderPage: deps.mockRenderPage,
1068
+ getRequestInfo: deps.getRequestInfo,
1069
+ onError: deps.onError,
1070
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
1071
+ rscActionHandler: deps.mockRscActionHandler,
1072
+ });
1073
+ expect(response.status).toBe(500);
1074
+ expect(await response.text()).toBe(`Caught: ${errorMessage}`);
1075
+ });
1076
+ it("should catch errors from route handlers (functions)", async () => {
1077
+ const errorMessage = "Handler error";
1078
+ const routeHandler = () => {
1079
+ throw new Error(errorMessage);
1080
+ };
1081
+ const errorHandler = except((error) => {
1082
+ return new Response(`Caught: ${error instanceof Error ? error.message : String(error)}`, {
1083
+ status: 500,
1084
+ });
1085
+ });
1086
+ const router = defineRoutes([
1087
+ errorHandler,
1088
+ route("/test/", routeHandler),
1089
+ ]);
1090
+ const deps = createMockDependencies();
1091
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/test/");
1092
+ const request = new Request("http://localhost:3000/test/");
1093
+ const response = await router.handle({
1094
+ request,
1095
+ renderPage: deps.mockRenderPage,
1096
+ getRequestInfo: deps.getRequestInfo,
1097
+ onError: deps.onError,
1098
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
1099
+ rscActionHandler: deps.mockRscActionHandler,
1100
+ });
1101
+ expect(response.status).toBe(500);
1102
+ expect(await response.text()).toBe(`Caught: ${errorMessage}`);
1103
+ });
1104
+ it("should catch errors from RSC actions", async () => {
1105
+ const errorMessage = "RSC action error";
1106
+ const errorHandler = except((error) => {
1107
+ return new Response(`Caught: ${error instanceof Error ? error.message : String(error)}`, {
1108
+ status: 500,
1109
+ });
1110
+ });
1111
+ const router = defineRoutes([
1112
+ errorHandler,
1113
+ route("/test/", () => React.createElement("div")),
1114
+ ]);
1115
+ const deps = createMockDependencies();
1116
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/test/?__rsc_action_id=test");
1117
+ deps.mockRscActionHandler = async () => {
1118
+ throw new Error(errorMessage);
1119
+ };
1120
+ const request = new Request("http://localhost:3000/test/?__rsc_action_id=test");
1121
+ const response = await router.handle({
1122
+ request,
1123
+ renderPage: deps.mockRenderPage,
1124
+ getRequestInfo: deps.getRequestInfo,
1125
+ onError: deps.onError,
1126
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
1127
+ rscActionHandler: deps.mockRscActionHandler,
1128
+ });
1129
+ expect(response.status).toBe(500);
1130
+ expect(await response.text()).toBe(`Caught: ${errorMessage}`);
1131
+ });
1132
+ it("should use the most recent except handler before the error", async () => {
1133
+ const errorMessage = "Route error";
1134
+ const firstHandler = except((error) => {
1135
+ return new Response("First handler", { status: 500 });
1136
+ });
1137
+ const secondHandler = except((error) => {
1138
+ return new Response("Second handler", { status: 500 });
1139
+ });
1140
+ const PageComponent = () => {
1141
+ throw new Error(errorMessage);
1142
+ };
1143
+ const router = defineRoutes([
1144
+ firstHandler,
1145
+ secondHandler,
1146
+ route("/test/", PageComponent),
1147
+ ]);
1148
+ const deps = createMockDependencies();
1149
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/test/");
1150
+ const request = new Request("http://localhost:3000/test/");
1151
+ const response = await router.handle({
1152
+ request,
1153
+ renderPage: deps.mockRenderPage,
1154
+ getRequestInfo: deps.getRequestInfo,
1155
+ onError: deps.onError,
1156
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
1157
+ rscActionHandler: deps.mockRscActionHandler,
1158
+ });
1159
+ // Should use the second handler (most recent before the route)
1160
+ expect(response.status).toBe(500);
1161
+ expect(await response.text()).toBe("Second handler");
1162
+ });
1163
+ it("should try next except handler if current one throws", async () => {
1164
+ const errorMessage = "Route error";
1165
+ const firstHandler = except(() => {
1166
+ throw new Error("First handler error");
1167
+ });
1168
+ const secondHandler = except((error) => {
1169
+ return new Response(`Caught by second: ${error instanceof Error ? error.message : String(error)}`, {
1170
+ status: 500,
1171
+ });
1172
+ });
1173
+ const PageComponent = () => {
1174
+ throw new Error(errorMessage);
1175
+ };
1176
+ const router = defineRoutes([
1177
+ secondHandler, // Outer handler
1178
+ firstHandler, // Inner handler (closer to route)
1179
+ route("/test/", PageComponent),
1180
+ ]);
1181
+ const deps = createMockDependencies();
1182
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/test/");
1183
+ const request = new Request("http://localhost:3000/test/");
1184
+ const response = await router.handle({
1185
+ request,
1186
+ renderPage: deps.mockRenderPage,
1187
+ getRequestInfo: deps.getRequestInfo,
1188
+ onError: deps.onError,
1189
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
1190
+ rscActionHandler: deps.mockRscActionHandler,
1191
+ });
1192
+ // Should catch the error from the first handler with the second handler
1193
+ expect(response.status).toBe(500);
1194
+ expect(await response.text()).toBe("Caught by second: First handler error");
1195
+ });
1196
+ it("should return JSX element from except handler", async () => {
1197
+ const errorMessage = "Route error";
1198
+ function ErrorComponent() {
1199
+ return React.createElement("div", {}, "Error Page");
1200
+ }
1201
+ const errorHandler = except(() => {
1202
+ return React.createElement(ErrorComponent);
1203
+ });
1204
+ const PageComponent = () => {
1205
+ throw new Error(errorMessage);
1206
+ };
1207
+ const router = defineRoutes([
1208
+ errorHandler,
1209
+ route("/test/", PageComponent),
1210
+ ]);
1211
+ const deps = createMockDependencies();
1212
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/test/");
1213
+ const request = new Request("http://localhost:3000/test/");
1214
+ const response = await router.handle({
1215
+ request,
1216
+ renderPage: deps.mockRenderPage,
1217
+ getRequestInfo: deps.getRequestInfo,
1218
+ onError: deps.onError,
1219
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
1220
+ rscActionHandler: deps.mockRscActionHandler,
1221
+ });
1222
+ expect(await response.text()).toBe("Rendered: ErrorComponent");
1223
+ });
1224
+ it("should work with prefix and layout", async () => {
1225
+ const errorMessage = "Route error";
1226
+ const errorHandler = except((error) => {
1227
+ return new Response(`Caught: ${error instanceof Error ? error.message : String(error)}`, {
1228
+ status: 500,
1229
+ });
1230
+ });
1231
+ const PageComponent = () => {
1232
+ throw new Error(errorMessage);
1233
+ };
1234
+ const Layout = ({ children }) => {
1235
+ return React.createElement("div", {}, children);
1236
+ };
1237
+ const router = defineRoutes([
1238
+ errorHandler,
1239
+ layout(Layout, [prefix("/api", [route("/test/", PageComponent)])]),
1240
+ ]);
1241
+ const deps = createMockDependencies();
1242
+ deps.mockRequestInfo.request = new Request("http://localhost:3000/api/test/");
1243
+ const request = new Request("http://localhost:3000/api/test/");
1244
+ const response = await router.handle({
1245
+ request,
1246
+ renderPage: deps.mockRenderPage,
1247
+ getRequestInfo: deps.getRequestInfo,
1248
+ onError: deps.onError,
1249
+ runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides,
1250
+ rscActionHandler: deps.mockRscActionHandler,
1251
+ });
1252
+ expect(response.status).toBe(500);
1253
+ expect(await response.text()).toBe(`Caught: ${errorMessage}`);
1254
+ });
1255
+ });
887
1256
  });
@@ -3,6 +3,7 @@ export interface DefaultAppContext {
3
3
  }
4
4
  export interface RequestInfo<Params = any, AppContext = DefaultAppContext> {
5
5
  request: Request;
6
+ path: string;
6
7
  params: Params;
7
8
  ctx: AppContext;
8
9
  rw: RwContext;
@@ -9,6 +9,7 @@ export const constructWithDefaultRequestInfo = (overrides = {}) => {
9
9
  const { rw: rwOverrides, ...otherRequestInfoOverrides } = overrides;
10
10
  const defaultRequestInfo = {
11
11
  request: new Request("http://localhost/"),
12
+ path: "/",
12
13
  params: {},
13
14
  ctx: {},
14
15
  cf: {
@@ -17,7 +17,8 @@ export const requestInfo = Object.freeze(requestInfoBase);
17
17
  export function getRequestInfo() {
18
18
  const store = requestInfoStore.getStore();
19
19
  if (!store) {
20
- throw new Error("Request context not found");
20
+ throw new Error("RedwoodSDK: Request context not found. getRequestInfo() can only be called within the request lifecycle (e.g., in a route handler, middleware, or server action).\n\n" +
21
+ "For detailed troubleshooting steps, see: https://docs.rwsdk.com/guides/troubleshooting#request-context-errors");
21
22
  }
22
23
  return store;
23
24
  }
@@ -11,11 +11,11 @@ import { defineRoutes } from "./lib/router";
11
11
  import { generateNonce } from "./lib/utils";
12
12
  export * from "./requestInfo/types";
13
13
  export const defineApp = (routes) => {
14
+ const router = defineRoutes(routes);
14
15
  return {
15
16
  __rwRoutes: routes,
16
17
  fetch: async (request, env, cf) => {
17
18
  globalThis.__webpack_require__ = ssrWebpackRequire;
18
- const router = defineRoutes(routes);
19
19
  // context(justinvdm, 5 Feb 2025): Serve assets requests using the assets service binding
20
20
  // todo(justinvdm, 5 Feb 2025): Find a way to avoid this so asset requests are served directly
21
21
  // rather than first needing to go through the worker
@@ -63,6 +63,10 @@ export const defineApp = (routes) => {
63
63
  }
64
64
  try {
65
65
  const url = new URL(request.url);
66
+ let path = url.pathname;
67
+ if (path !== "/" && !path.endsWith("/")) {
68
+ path = path + "/";
69
+ }
66
70
  const isRSCRequest = url.searchParams.has("__rsc") ||
67
71
  request.headers.get("accept")?.includes("text/x-component");
68
72
  const isAction = url.searchParams.has("__rsc_action_id");
@@ -83,6 +87,7 @@ export const defineApp = (routes) => {
83
87
  };
84
88
  const outerRequestInfo = {
85
89
  request,
90
+ path,
86
91
  cf,
87
92
  params: {},
88
93
  ctx: {},
@@ -298,7 +298,9 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
298
298
  });
299
299
  }
300
300
  catch (e) {
301
- throw new Error(`RWSDK directive scan failed:\n${e.stack}`);
301
+ throw new Error(`RedwoodSDK: Directive scan failed. This often happens due to syntax errors in files using "use client" or "use server". Check your directive files for issues.\n\n` +
302
+ `For detailed troubleshooting steps, see: https://docs.rwsdk.com/guides/troubleshooting#directive-scan-errors\n\n` +
303
+ `${e.stack}`);
302
304
  }
303
305
  finally {
304
306
  deferredLog("✔ (rwsdk) Done scanning for 'use client' and 'use server' directives.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "1.0.0-beta.43",
3
+ "version": "1.0.0-beta.45",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {
@@ -182,6 +182,7 @@
182
182
  "puppeteer-core": "~24.22.0",
183
183
  "react-is": "~19.1.0",
184
184
  "rsc-html-stream": "~0.0.6",
185
+ "server-only": "^0.0.1",
185
186
  "tmp-promise": "~3.0.3",
186
187
  "ts-morph": "~27.0.0",
187
188
  "unique-names-generator": "~4.7.1",