rwsdk 1.0.0-beta.5 → 1.0.0-beta.50

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.
Files changed (157) hide show
  1. package/bin/rw-scripts.mjs +13 -13
  2. package/dist/lib/constants.d.mts +1 -0
  3. package/dist/lib/constants.mjs +7 -4
  4. package/dist/lib/e2e/browser.mjs +6 -2
  5. package/dist/lib/e2e/constants.d.mts +4 -0
  6. package/dist/lib/e2e/constants.mjs +49 -12
  7. package/dist/lib/e2e/dev.mjs +49 -57
  8. package/dist/lib/e2e/environment.d.mts +2 -0
  9. package/dist/lib/e2e/environment.mjs +201 -64
  10. package/dist/lib/e2e/index.d.mts +2 -0
  11. package/dist/lib/e2e/index.mjs +2 -0
  12. package/dist/lib/e2e/poll.d.mts +1 -1
  13. package/dist/lib/e2e/release.d.mts +1 -0
  14. package/dist/lib/e2e/release.mjs +57 -52
  15. package/dist/lib/e2e/tarball.mjs +2 -34
  16. package/dist/lib/e2e/testHarness.d.mts +39 -3
  17. package/dist/lib/e2e/testHarness.mjs +239 -92
  18. package/dist/lib/e2e/utils.d.mts +1 -0
  19. package/dist/lib/e2e/utils.mjs +15 -0
  20. package/dist/lib/normalizeModulePath.mjs +1 -1
  21. package/dist/runtime/client/client.d.ts +64 -2
  22. package/dist/runtime/client/client.js +156 -15
  23. package/dist/runtime/client/navigation.d.ts +45 -0
  24. package/dist/runtime/client/navigation.js +68 -14
  25. package/dist/runtime/client/navigationCache.d.ts +68 -0
  26. package/dist/runtime/client/navigationCache.js +294 -0
  27. package/dist/runtime/client/navigationCache.test.js +469 -0
  28. package/dist/runtime/client/types.d.ts +26 -5
  29. package/dist/runtime/client/types.js +8 -1
  30. package/dist/runtime/entries/no-react-server-ssr-bridge.d.ts +0 -0
  31. package/dist/runtime/entries/no-react-server-ssr-bridge.js +2 -0
  32. package/dist/runtime/entries/no-react-server.js +3 -1
  33. package/dist/runtime/entries/react-server-only.js +1 -1
  34. package/dist/runtime/entries/router.d.ts +1 -0
  35. package/dist/runtime/entries/routerClient.d.ts +1 -0
  36. package/dist/runtime/entries/routerClient.js +1 -0
  37. package/dist/runtime/entries/worker.d.ts +4 -0
  38. package/dist/runtime/entries/worker.js +4 -0
  39. package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
  40. package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
  41. package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
  42. package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
  43. package/dist/runtime/lib/db/createDb.d.ts +1 -2
  44. package/dist/runtime/lib/db/createDb.js +4 -0
  45. package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
  46. package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
  47. package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
  48. package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
  49. package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
  50. package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
  51. package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
  52. package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
  53. package/dist/runtime/lib/links.d.ts +21 -7
  54. package/dist/runtime/lib/links.js +84 -26
  55. package/dist/runtime/lib/links.test.d.ts +1 -0
  56. package/dist/runtime/lib/links.test.js +20 -0
  57. package/dist/runtime/lib/manifest.d.ts +1 -1
  58. package/dist/runtime/lib/manifest.js +7 -4
  59. package/dist/runtime/lib/realtime/client.js +28 -6
  60. package/dist/runtime/lib/realtime/worker.d.ts +1 -1
  61. package/dist/runtime/lib/router.d.ts +154 -35
  62. package/dist/runtime/lib/router.js +491 -105
  63. package/dist/runtime/lib/router.test.js +611 -1
  64. package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
  65. package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
  66. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
  67. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
  68. package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
  69. package/dist/runtime/lib/types.js +1 -0
  70. package/dist/runtime/register/client.d.ts +1 -1
  71. package/dist/runtime/register/client.js +10 -3
  72. package/dist/runtime/register/worker.js +13 -4
  73. package/dist/runtime/render/normalizeActionResult.js +8 -1
  74. package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
  75. package/dist/runtime/render/renderToStream.d.ts +4 -2
  76. package/dist/runtime/render/renderToStream.js +53 -24
  77. package/dist/runtime/render/renderToString.d.ts +3 -6
  78. package/dist/runtime/requestInfo/types.d.ts +5 -1
  79. package/dist/runtime/requestInfo/utils.d.ts +9 -0
  80. package/dist/runtime/requestInfo/utils.js +45 -0
  81. package/dist/runtime/requestInfo/worker.d.ts +0 -1
  82. package/dist/runtime/requestInfo/worker.js +5 -11
  83. package/dist/runtime/script.d.ts +1 -3
  84. package/dist/runtime/script.js +1 -10
  85. package/dist/runtime/server.d.ts +52 -0
  86. package/dist/runtime/server.js +88 -0
  87. package/dist/runtime/state.d.ts +3 -0
  88. package/dist/runtime/state.js +13 -0
  89. package/dist/runtime/worker.d.ts +3 -1
  90. package/dist/runtime/worker.js +45 -2
  91. package/dist/scripts/debug-sync.mjs +18 -20
  92. package/dist/scripts/worker-run.d.mts +1 -1
  93. package/dist/scripts/worker-run.mjs +59 -113
  94. package/dist/use-synced-state/SyncedStateServer.d.mts +36 -0
  95. package/dist/use-synced-state/SyncedStateServer.mjs +196 -0
  96. package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
  97. package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +116 -0
  98. package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
  99. package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
  100. package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
  101. package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
  102. package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
  103. package/dist/use-synced-state/__tests__/worker.test.mjs +70 -0
  104. package/dist/use-synced-state/client-core.d.ts +29 -0
  105. package/dist/use-synced-state/client-core.js +103 -0
  106. package/dist/use-synced-state/client.d.ts +3 -0
  107. package/dist/use-synced-state/client.js +4 -0
  108. package/dist/use-synced-state/constants.d.mts +1 -0
  109. package/dist/use-synced-state/constants.mjs +1 -0
  110. package/dist/use-synced-state/useSyncedState.d.ts +21 -0
  111. package/dist/use-synced-state/useSyncedState.js +64 -0
  112. package/dist/use-synced-state/worker.d.mts +14 -0
  113. package/dist/use-synced-state/worker.mjs +135 -0
  114. package/dist/vite/buildApp.mjs +34 -2
  115. package/dist/vite/cloudflarePreInitPlugin.d.mts +11 -0
  116. package/dist/vite/cloudflarePreInitPlugin.mjs +40 -0
  117. package/dist/vite/configPlugin.mjs +9 -14
  118. package/dist/vite/constants.d.mts +1 -0
  119. package/dist/vite/constants.mjs +1 -0
  120. package/dist/vite/createDirectiveLookupPlugin.mjs +10 -7
  121. package/dist/vite/devServerTimingPlugin.mjs +4 -0
  122. package/dist/vite/diagnosticAssetGraphPlugin.d.mts +4 -0
  123. package/dist/vite/diagnosticAssetGraphPlugin.mjs +41 -0
  124. package/dist/vite/directiveModulesDevPlugin.mjs +9 -1
  125. package/dist/vite/directivesPlugin.mjs +4 -4
  126. package/dist/vite/envResolvers.d.mts +11 -0
  127. package/dist/vite/envResolvers.mjs +20 -0
  128. package/dist/vite/getViteEsbuild.mjs +2 -1
  129. package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
  130. package/dist/vite/hmrStabilityPlugin.mjs +73 -0
  131. package/dist/vite/injectVitePreamblePlugin.mjs +0 -4
  132. package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
  133. package/dist/vite/knownDepsResolverPlugin.mjs +25 -17
  134. package/dist/vite/linkerPlugin.d.mts +2 -1
  135. package/dist/vite/linkerPlugin.mjs +11 -3
  136. package/dist/vite/linkerPlugin.test.mjs +15 -0
  137. package/dist/vite/miniflareHMRPlugin.mjs +6 -38
  138. package/dist/vite/moveStaticAssetsPlugin.mjs +35 -4
  139. package/dist/vite/redwoodPlugin.mjs +9 -11
  140. package/dist/vite/redwoodPlugin.test.mjs +4 -4
  141. package/dist/vite/runDirectivesScan.mjs +75 -19
  142. package/dist/vite/ssrBridgePlugin.mjs +132 -40
  143. package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
  144. package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
  145. package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
  146. package/dist/vite/staleDepRetryPlugin.mjs +74 -0
  147. package/dist/vite/statePlugin.d.mts +4 -0
  148. package/dist/vite/statePlugin.mjs +62 -0
  149. package/dist/vite/transformClientComponents.test.mjs +32 -0
  150. package/dist/vite/transformJsxScriptTagsPlugin.mjs +0 -5
  151. package/dist/vite/transformServerFunctions.mjs +66 -4
  152. package/dist/vite/transformServerFunctions.test.mjs +35 -0
  153. package/dist/vite/virtualPlugin.mjs +6 -7
  154. package/package.json +41 -19
  155. package/dist/vite/manifestPlugin.d.mts +0 -4
  156. package/dist/vite/manifestPlugin.mjs +0 -63
  157. /package/dist/runtime/{lib/rwContext.js → client/navigationCache.test.d.ts} +0 -0
@@ -1,54 +1,66 @@
1
1
  import React from "react";
2
2
  import { isValidElementType } from "react-is";
3
- export function matchPath(routePath, requestPath) {
3
+ const METHOD_VERBS = ["delete", "get", "head", "patch", "post", "put"];
4
+ const pathCache = new Map();
5
+ function compilePath(routePath) {
6
+ const cached = pathCache.get(routePath);
7
+ if (cached)
8
+ return cached;
4
9
  // Check for invalid pattern: multiple colons in a segment (e.g., /:param1:param2/)
5
10
  if (routePath.includes(":")) {
6
11
  const segments = routePath.split("/");
7
12
  for (const segment of segments) {
8
13
  if ((segment.match(/:/g) || []).length > 1) {
9
- throw new Error(`Invalid route pattern: segment "${segment}" in "${routePath}" contains multiple colons.`);
14
+ throw new Error(`RedwoodSDK: Invalid route pattern: segment "${segment}" in "${routePath}" contains multiple colons. Each route parameter should use a single colon (e.g., ":id"). Check for accidental double colons ("::").`);
10
15
  }
11
16
  }
12
17
  }
13
18
  // Check for invalid pattern: double wildcard (e.g., /**/)
14
19
  if (routePath.indexOf("**") !== -1) {
15
- throw new Error(`Invalid route pattern: "${routePath}" contains "**". Use "*" for a single wildcard segment.`);
20
+ throw new Error(`RedwoodSDK: Invalid route pattern: "${routePath}" contains "**". Use "*" for a single wildcard segment. Double wildcards are not supported.`);
16
21
  }
17
- const pattern = routePath
18
- .replace(/:[a-zA-Z0-9]+/g, "([^/]+)") // Convert :param to capture group
19
- .replace(/\*/g, "(.*)"); // Convert * to wildcard capture group
20
- const regex = new RegExp(`^${pattern}$`);
21
- const matches = requestPath.match(regex);
22
- if (!matches) {
23
- return null;
22
+ const isStatic = !routePath.includes(":") && !routePath.includes("*");
23
+ if (isStatic) {
24
+ const result = { isStatic: true, regex: null, paramMap: [] };
25
+ pathCache.set(routePath, result);
26
+ return result;
24
27
  }
25
- // Revised parameter extraction:
26
- const params = {};
27
- let currentMatchIndex = 1; // Regex matches are 1-indexed
28
- // This regex finds either a named parameter token (e.g., ":id") or a wildcard star token ("*").
28
+ const paramMap = [];
29
+ let wildcardCounter = 0;
29
30
  const tokenRegex = /:([a-zA-Z0-9_]+)|\*/g;
30
31
  let matchToken;
31
- let wildcardCounter = 0;
32
- // Ensure regex starts from the beginning of the routePath for each call if it's stateful (it is with /g)
33
- tokenRegex.lastIndex = 0;
34
32
  while ((matchToken = tokenRegex.exec(routePath)) !== null) {
35
- // Ensure we have a corresponding match from the regex execution
36
- if (matches[currentMatchIndex] === undefined) {
37
- // This case should ideally not be hit if routePath and pattern generation are correct
38
- // and all parts of the regex matched.
39
- // Consider logging a warning or throwing an error if critical.
40
- break;
41
- }
42
33
  if (matchToken[1]) {
43
- // This token is a named parameter (e.g., matchToken[1] is "id" for ":id")
44
- params[matchToken[1]] = matches[currentMatchIndex];
34
+ paramMap.push({ name: matchToken[1], isWildcard: false });
45
35
  }
46
36
  else {
47
- // This token is a wildcard "*"
48
- params[`$${wildcardCounter}`] = matches[currentMatchIndex];
49
- wildcardCounter++;
37
+ paramMap.push({ name: `$${wildcardCounter++}`, isWildcard: true });
50
38
  }
51
- currentMatchIndex++;
39
+ }
40
+ const pattern = routePath
41
+ .replace(/:[a-zA-Z0-9_]+/g, "([^/]+)") // Convert :param to capture group
42
+ .replace(/\*/g, "(.*)"); // Convert * to wildcard capture group
43
+ const result = {
44
+ isStatic: false,
45
+ regex: new RegExp(`^${pattern}$`),
46
+ paramMap,
47
+ };
48
+ pathCache.set(routePath, result);
49
+ return result;
50
+ }
51
+ export function matchPath(routePath, requestPath) {
52
+ const compiled = compilePath(routePath);
53
+ if (compiled.isStatic) {
54
+ return routePath === requestPath ? {} : null;
55
+ }
56
+ const matches = requestPath.match(compiled.regex);
57
+ if (!matches) {
58
+ return null;
59
+ }
60
+ const params = {};
61
+ for (let i = 0; i < compiled.paramMap.length; i++) {
62
+ const param = compiled.paramMap[i];
63
+ params[param.name] = matches[i + 1];
52
64
  }
53
65
  return params;
54
66
  }
@@ -57,20 +69,81 @@ function flattenRoutes(routes) {
57
69
  if (Array.isArray(route)) {
58
70
  return [...acc, ...flattenRoutes(route)];
59
71
  }
60
- return [...acc, route];
72
+ return [
73
+ ...acc,
74
+ route,
75
+ ];
61
76
  }, []);
62
77
  }
78
+ function isMethodHandlers(handler) {
79
+ return (typeof handler === "object" && handler !== null && !Array.isArray(handler));
80
+ }
81
+ function handleOptionsRequest(methodHandlers) {
82
+ const methods = new Set([
83
+ ...(methodHandlers.config?.disableOptions ? [] : ["OPTIONS"]),
84
+ ...METHOD_VERBS.filter((verb) => methodHandlers[verb]).map((verb) => verb.toUpperCase()),
85
+ ...Object.keys(methodHandlers.custom ?? {}).map((method) => method.toUpperCase()),
86
+ ]);
87
+ return new Response(null, {
88
+ status: 204,
89
+ headers: {
90
+ Allow: Array.from(methods).sort().join(", "),
91
+ },
92
+ });
93
+ }
94
+ function handleMethodNotAllowed(methodHandlers) {
95
+ const optionsResponse = handleOptionsRequest(methodHandlers);
96
+ return new Response("Method Not Allowed", {
97
+ status: 405,
98
+ headers: optionsResponse.headers,
99
+ });
100
+ }
101
+ function getHandlerForMethod(methodHandlers, method) {
102
+ const lowerMethod = method.toLowerCase();
103
+ // Check standard method verbs
104
+ if (METHOD_VERBS.includes(lowerMethod)) {
105
+ return methodHandlers[lowerMethod];
106
+ }
107
+ // Check custom methods (already normalized to lowercase)
108
+ return methodHandlers.custom?.[lowerMethod];
109
+ }
63
110
  export function defineRoutes(routes) {
64
111
  const flattenedRoutes = flattenRoutes(routes);
112
+ const compiledRoutes = flattenedRoutes.map((route) => {
113
+ if (typeof route === "function") {
114
+ return { type: "middleware", handler: route };
115
+ }
116
+ if (typeof route === "object" &&
117
+ route !== null &&
118
+ "__rwExcept" in route &&
119
+ route.__rwExcept === true) {
120
+ return { type: "except", handler: route };
121
+ }
122
+ const routeDef = route;
123
+ const compiledPath = compilePath(routeDef.path);
124
+ return {
125
+ type: "definition",
126
+ path: routeDef.path,
127
+ handler: routeDef.handler,
128
+ layouts: routeDef.layouts,
129
+ isStatic: compiledPath.isStatic,
130
+ regex: compiledPath.regex ?? undefined,
131
+ paramNames: compiledPath.paramMap
132
+ .filter((p) => !p.isWildcard)
133
+ .map((p) => p.name),
134
+ wildcardCount: compiledPath.paramMap.filter((p) => p.isWildcard).length,
135
+ };
136
+ });
65
137
  return {
66
138
  routes: flattenedRoutes,
67
139
  async handle({ request, renderPage, getRequestInfo, onError, runWithRequestInfoOverrides, rscActionHandler, }) {
140
+ const requestInfo = getRequestInfo();
68
141
  const url = new URL(request.url);
69
142
  let path = url.pathname;
70
- // Must end with a trailing slash.
71
143
  if (path !== "/" && !path.endsWith("/")) {
72
144
  path = path + "/";
73
145
  }
146
+ requestInfo.path = path;
74
147
  // --- Helpers ---
75
148
  // (Hoisted for readability)
76
149
  function parseHandlers(handler) {
@@ -84,7 +157,17 @@ export function defineRoutes(routes) {
84
157
  }
85
158
  function renderElement(element) {
86
159
  const requestInfo = getRequestInfo();
160
+ // Try to preserve the component name from the element's type
161
+ const elementType = element.type;
162
+ const componentName = typeof elementType === "function" && elementType.name
163
+ ? elementType.name
164
+ : "Element";
87
165
  const Element = () => element;
166
+ // Set the name for better debugging
167
+ Object.defineProperty(Element, "name", {
168
+ value: componentName,
169
+ configurable: true,
170
+ });
88
171
  return renderPage(requestInfo, Element, onError);
89
172
  }
90
173
  async function handleMiddlewareResult(result) {
@@ -96,110 +179,393 @@ export function defineRoutes(routes) {
96
179
  }
97
180
  return undefined;
98
181
  }
182
+ function isExceptHandler(route) {
183
+ return route.type === "except";
184
+ }
185
+ async function executeExceptHandlers(error, startIndex) {
186
+ // Search backwards from startIndex to find the most recent except handler
187
+ for (let i = startIndex; i >= 0; i--) {
188
+ const route = compiledRoutes[i];
189
+ if (isExceptHandler(route)) {
190
+ try {
191
+ const result = await route.handler.handler(error, getRequestInfo());
192
+ const handled = await handleMiddlewareResult(result);
193
+ if (handled) {
194
+ return handled;
195
+ }
196
+ // If the handler didn't return a Response or JSX, continue to next handler (further back)
197
+ }
198
+ catch (nextError) {
199
+ // If the except handler itself throws, try the next one (further back)
200
+ return await executeExceptHandlers(nextError, i - 1);
201
+ }
202
+ }
203
+ }
204
+ // No handler found, throw to top-level onError
205
+ onError(error);
206
+ throw error;
207
+ }
99
208
  // --- Main flow ---
100
209
  let firstRouteDefinitionEncountered = false;
101
210
  let actionHandled = false;
102
211
  const handleAction = async () => {
103
- if (!actionHandled && url.searchParams.has("__rsc_action_id")) {
104
- getRequestInfo().rw.actionResult = await rscActionHandler(request);
212
+ // Handle RSC actions once per request, based on the incoming URL.
213
+ if (!actionHandled) {
214
+ const url = new URL(request.url);
215
+ if (url.searchParams.has("__rsc_action_id")) {
216
+ requestInfo.rw.actionResult = await rscActionHandler(request);
217
+ }
105
218
  actionHandled = true;
106
219
  }
107
220
  };
108
- for (const route of flattenedRoutes) {
109
- if (typeof route === "function") {
110
- // This is a global middleware.
111
- const result = await route(getRequestInfo());
112
- const handled = await handleMiddlewareResult(result);
113
- if (handled) {
114
- return handled; // Short-circuit
221
+ try {
222
+ let currentRouteIndex = 0;
223
+ for (const route of compiledRoutes) {
224
+ // Skip except handlers during normal execution
225
+ if (route.type === "except") {
226
+ currentRouteIndex++;
227
+ continue;
115
228
  }
116
- continue;
117
- }
118
- // This is a RouteDefinition.
119
- // The first time we see one, we handle any RSC actions.
120
- if (!firstRouteDefinitionEncountered) {
121
- firstRouteDefinitionEncountered = true;
122
- await handleAction();
123
- }
124
- const params = matchPath(route.path, path);
125
- if (!params) {
126
- continue; // Not a match, keep going.
127
- }
128
- // Found a match: run route-specific middlewares, then the final component, then stop.
129
- return await runWithRequestInfoOverrides({ params }, async () => {
130
- const { routeMiddlewares, componentHandler } = parseHandlers(route.handler);
131
- // Route-specific middlewares
132
- for (const mw of routeMiddlewares) {
133
- const result = await mw(getRequestInfo());
134
- const handled = await handleMiddlewareResult(result);
135
- if (handled) {
136
- return handled;
229
+ if (route.type === "middleware") {
230
+ // This is a global middleware.
231
+ try {
232
+ const result = await route.handler(getRequestInfo());
233
+ const handled = await handleMiddlewareResult(result);
234
+ if (handled) {
235
+ return handled; // Short-circuit
236
+ }
237
+ }
238
+ catch (error) {
239
+ return await executeExceptHandlers(error, currentRouteIndex);
137
240
  }
241
+ currentRouteIndex++;
242
+ continue;
138
243
  }
139
- // Final component/handler
140
- if (isRouteComponent(componentHandler)) {
141
- const requestInfo = getRequestInfo();
142
- const WrappedComponent = wrapWithLayouts(wrapHandlerToThrowResponses(componentHandler), route.layouts || [], requestInfo);
143
- if (!isClientReference(componentHandler)) {
144
- requestInfo.rw.pageRouteResolved = Promise.withResolvers();
244
+ // This is a RouteDefinition (route.type === "definition").
245
+ // The first time we see one, we handle any RSC actions.
246
+ if (!firstRouteDefinitionEncountered) {
247
+ firstRouteDefinitionEncountered = true;
248
+ try {
249
+ await handleAction();
250
+ }
251
+ catch (error) {
252
+ return await executeExceptHandlers(error, currentRouteIndex);
145
253
  }
146
- return await renderPage(requestInfo, WrappedComponent, onError);
147
254
  }
148
- // Handle non-component final handler (e.g., returns new Response)
149
- const tailResult = await componentHandler(getRequestInfo());
150
- const handledTail = await handleMiddlewareResult(tailResult);
151
- if (handledTail) {
152
- return handledTail;
255
+ let params = null;
256
+ if (route.isStatic) {
257
+ if (route.path === path) {
258
+ params = {};
259
+ }
153
260
  }
154
- return new Response("Response not returned from route handler", {
155
- status: 500,
156
- });
157
- });
261
+ else if (route.regex) {
262
+ const matches = path.match(route.regex);
263
+ if (matches) {
264
+ params = {};
265
+ for (let i = 0; i < route.paramNames.length; i++) {
266
+ params[route.paramNames[i]] = matches[i + 1];
267
+ }
268
+ for (let i = 0; i < route.wildcardCount; i++) {
269
+ params[`$${i}`] = matches[route.paramNames.length + i + 1];
270
+ }
271
+ }
272
+ }
273
+ if (!params) {
274
+ currentRouteIndex++;
275
+ continue; // Not a match, keep going.
276
+ }
277
+ // Resolve handler if method-based routing
278
+ let handler;
279
+ if (isMethodHandlers(route.handler)) {
280
+ const requestMethod = request.method;
281
+ // Handle OPTIONS request
282
+ if (requestMethod === "OPTIONS" &&
283
+ !route.handler.config?.disableOptions) {
284
+ return handleOptionsRequest(route.handler);
285
+ }
286
+ // Try to find handler for the request method
287
+ handler = getHandlerForMethod(route.handler, requestMethod);
288
+ if (!handler) {
289
+ // Method not supported for this route
290
+ if (!route.handler.config?.disable405) {
291
+ return handleMethodNotAllowed(route.handler);
292
+ }
293
+ // If 405 is disabled, continue to next route
294
+ currentRouteIndex++;
295
+ continue;
296
+ }
297
+ }
298
+ else {
299
+ handler = route.handler;
300
+ }
301
+ // Found a match: run route-specific middlewares, then the final component, then stop.
302
+ try {
303
+ return await runWithRequestInfoOverrides({ params }, async () => {
304
+ const { routeMiddlewares, componentHandler } = parseHandlers(handler);
305
+ // Route-specific middlewares
306
+ for (const mw of routeMiddlewares) {
307
+ const result = await mw(getRequestInfo());
308
+ const handled = await handleMiddlewareResult(result);
309
+ if (handled) {
310
+ return handled;
311
+ }
312
+ }
313
+ // Final component/handler
314
+ if (isRouteComponent(componentHandler)) {
315
+ const requestInfo = getRequestInfo();
316
+ const WrappedComponent = wrapWithLayouts(wrapHandlerToThrowResponses(componentHandler), route.layouts || [], requestInfo);
317
+ if (!isClientReference(componentHandler) && !requestInfo.rw.pageRouteResolved) {
318
+ requestInfo.rw.pageRouteResolved = Promise.withResolvers();
319
+ }
320
+ return await renderPage(requestInfo, WrappedComponent, onError);
321
+ }
322
+ // Handle non-component final handler (e.g., returns new Response)
323
+ const tailResult = await componentHandler(getRequestInfo());
324
+ const handledTail = await handleMiddlewareResult(tailResult);
325
+ if (handledTail) {
326
+ return handledTail;
327
+ }
328
+ const handlerName = typeof componentHandler === "function" &&
329
+ componentHandler.name
330
+ ? componentHandler.name
331
+ : "anonymous";
332
+ const errorMessage = `Route handler did not return a Response or React element.
333
+
334
+ Route: ${route.path}
335
+ Matched path: ${path}
336
+ Method: ${request.method}
337
+ Handler: ${handlerName}
338
+
339
+ Route handlers must return one of:
340
+ - A Response object (e.g., \`new Response("OK")\`)
341
+ - A React element (e.g., \`<div>Hello</div>\`)
342
+ - \`void\` (if handled by middleware earlier in the chain)`;
343
+ return new Response(errorMessage, {
344
+ status: 500,
345
+ headers: { "Content-Type": "text/plain" },
346
+ });
347
+ });
348
+ }
349
+ catch (error) {
350
+ return await executeExceptHandlers(error, currentRouteIndex);
351
+ }
352
+ }
353
+ // If we've gotten this far, no route was matched.
354
+ // We still need to handle a possible action if the app has no route definitions at all.
355
+ if (!firstRouteDefinitionEncountered) {
356
+ try {
357
+ await handleAction();
358
+ }
359
+ catch (error) {
360
+ return await executeExceptHandlers(error, compiledRoutes.length - 1);
361
+ }
362
+ }
363
+ return new Response("Not Found", { status: 404 });
158
364
  }
159
- // If we've gotten this far, no route was matched.
160
- // We still need to handle a possible action if the app has no route definitions at all.
161
- if (!firstRouteDefinitionEncountered) {
162
- await handleAction();
365
+ catch (error) {
366
+ // Top-level catch for any unhandled errors
367
+ return await executeExceptHandlers(error, compiledRoutes.length - 1);
163
368
  }
164
- return new Response("Not Found", { status: 404 });
165
369
  },
166
370
  };
167
371
  }
372
+ /**
373
+ * Defines a route handler for a path pattern.
374
+ *
375
+ * Supports three types of path patterns:
376
+ * - Static: /about, /contact
377
+ * - Parameters: /users/:id, /posts/:postId/edit
378
+ * - Wildcards: /files/\*, /api/\*\/download
379
+ *
380
+ * @example
381
+ * // Static route
382
+ * route("/about", () => <AboutPage />)
383
+ *
384
+ * @example
385
+ * // Route with parameters
386
+ * route("/users/:id", ({ params }) => {
387
+ * return <UserProfile userId={params.id} />
388
+ * })
389
+ *
390
+ * @example
391
+ * // Route with wildcards
392
+ * route("/files/*", ({ params }) => {
393
+ * const filePath = params.$0
394
+ * return <FileViewer path={filePath} />
395
+ * })
396
+ *
397
+ * @example
398
+ * // Method-based routing
399
+ * route("/api/users", {
400
+ * get: () => Response.json(users),
401
+ * post: ({ request }) => Response.json({ status: "created" }, { status: 201 }),
402
+ * delete: () => new Response(null, { status: 204 }),
403
+ * })
404
+ *
405
+ * @example
406
+ * // Route with middleware array
407
+ * route("/admin", [isAuthenticated, isAdmin, () => <AdminDashboard />])
408
+ */
168
409
  export function route(path, handler) {
169
- if (!path.endsWith("/")) {
170
- path = path + "/";
410
+ let normalizedPath = path;
411
+ if (!normalizedPath.startsWith("/")) {
412
+ normalizedPath = "/" + normalizedPath;
413
+ }
414
+ // Special case: wildcard route "*" should normalize to "/*" (not "/*/")
415
+ // to allow it to match the root path "/"
416
+ if (normalizedPath !== "/*" && !normalizedPath.endsWith("/")) {
417
+ normalizedPath = normalizedPath + "/";
418
+ }
419
+ // Normalize custom method keys to lowercase
420
+ if (isMethodHandlers(handler) && handler.custom) {
421
+ handler = {
422
+ ...handler,
423
+ custom: Object.fromEntries(Object.entries(handler.custom).map(([method, methodHandler]) => [
424
+ method.toLowerCase(),
425
+ methodHandler,
426
+ ])),
427
+ };
171
428
  }
172
429
  return {
173
- path,
430
+ path: normalizedPath,
174
431
  handler,
432
+ __rwPath: normalizedPath,
175
433
  };
176
434
  }
435
+ /**
436
+ * Defines a route handler for the root path "/".
437
+ *
438
+ * @example
439
+ * // Homepage
440
+ * index(() => <HomePage />)
441
+ *
442
+ * @example
443
+ * // With middleware
444
+ * index([logRequest, () => <HomePage />])
445
+ */
177
446
  export function index(handler) {
178
447
  return route("/", handler);
179
448
  }
449
+ /**
450
+ * Defines an error handler that catches errors from routes, middleware, and RSC actions.
451
+ *
452
+ * @example
453
+ * // Global error handler
454
+ * except((error, requestInfo) => {
455
+ * console.error(error);
456
+ * return new Response("Internal Server Error", { status: 500 });
457
+ * })
458
+ *
459
+ * @example
460
+ * // Error handler that returns a React component
461
+ * except((error) => {
462
+ * return <ErrorPage error={error} />;
463
+ * })
464
+ */
465
+ export function except(handler) {
466
+ return { __rwExcept: true, handler };
467
+ }
468
+ /**
469
+ * Prefixes a group of routes with a path.
470
+ *
471
+ * @example
472
+ * // Organize blog routes under /blog
473
+ * const blogRoutes = [
474
+ * route("/", () => <BlogIndex />),
475
+ * route("/post/:id", ({ params }) => <BlogPost id={params.id} />),
476
+ * route("/admin", [isAuthenticated, () => <BlogAdmin />]),
477
+ * ]
478
+ *
479
+ * // In worker.tsx
480
+ * defineApp([
481
+ * render(Document, [
482
+ * route("/", () => <HomePage />),
483
+ * prefix("/blog", blogRoutes),
484
+ * ]),
485
+ * ])
486
+ */
487
+ function joinPaths(p1, p2) {
488
+ // Normalize p1: ensure it doesn't end with / (except if it's just "/")
489
+ const part1 = p1 === "/" ? "/" : p1.endsWith("/") ? p1.slice(0, -1) : p1;
490
+ // Normalize p2: ensure it starts with /
491
+ const part2 = p2.startsWith("/") ? p2 : `/${p2}`;
492
+ return part1 + part2;
493
+ }
180
494
  export function prefix(prefixPath, routes) {
181
- return routes.map((r) => {
495
+ // Normalize prefix path
496
+ let normalizedPrefix = prefixPath;
497
+ if (!normalizedPrefix.startsWith("/")) {
498
+ normalizedPrefix = "/" + normalizedPrefix;
499
+ }
500
+ if (!normalizedPrefix.endsWith("/")) {
501
+ normalizedPrefix = normalizedPrefix + "/";
502
+ }
503
+ // Check if prefix has parameters
504
+ const hasParams = normalizedPrefix.includes(":") || normalizedPrefix.includes("*");
505
+ // Create a pattern for matching: if prefix has params, append wildcard to match any path under it
506
+ const matchPattern = hasParams
507
+ ? normalizedPrefix.endsWith("/")
508
+ ? normalizedPrefix.slice(0, -1) + "/*"
509
+ : normalizedPrefix + "/*"
510
+ : normalizedPrefix;
511
+ const prefixed = routes.map((r) => {
182
512
  if (typeof r === "function") {
183
513
  const middleware = (requestInfo) => {
184
- const url = new URL(requestInfo.request.url);
185
- if (url.pathname.startsWith(prefixPath)) {
186
- return r(requestInfo);
514
+ const path = requestInfo.path;
515
+ // Check if path matches the prefix pattern
516
+ let matches = false;
517
+ let prefixParams = {};
518
+ if (hasParams) {
519
+ // Use matchPath to check if path matches and extract params
520
+ const params = matchPath(matchPattern, path);
521
+ if (params) {
522
+ matches = true;
523
+ prefixParams = params;
524
+ }
525
+ }
526
+ else {
527
+ // For static prefixes, use simple string matching
528
+ if (path === normalizedPrefix || path.startsWith(normalizedPrefix)) {
529
+ matches = true;
530
+ }
531
+ }
532
+ if (matches) {
533
+ // Merge prefix params with existing params
534
+ const mergedParams = { ...requestInfo.params, ...prefixParams };
535
+ // Create a new requestInfo with merged params
536
+ const modifiedRequestInfo = {
537
+ ...requestInfo,
538
+ params: mergedParams,
539
+ };
540
+ return r(modifiedRequestInfo);
187
541
  }
188
542
  return;
189
543
  };
190
544
  return middleware;
191
545
  }
546
+ if (typeof r === "object" &&
547
+ r !== null &&
548
+ "__rwExcept" in r &&
549
+ r.__rwExcept === true) {
550
+ // Pass through ExceptHandler as-is
551
+ return r;
552
+ }
192
553
  if (Array.isArray(r)) {
193
554
  // Recursively process nested route arrays
194
555
  return prefix(prefixPath, r);
195
556
  }
196
- // For RouteDefinition objects, update the path and preserve layouts
557
+ const routeDef = r;
558
+ // Use joinPaths to properly combine paths
559
+ const combinedPath = joinPaths(prefixPath, routeDef.path);
560
+ // Normalize double slashes
561
+ const normalizedCombinedPath = combinedPath.replace(/\/+/g, "/");
197
562
  return {
198
- path: prefixPath + r.path,
199
- handler: r.handler,
200
- ...(r.layouts && { layouts: r.layouts }),
563
+ path: normalizedCombinedPath,
564
+ handler: routeDef.handler,
565
+ ...(routeDef.layouts && { layouts: routeDef.layouts }),
201
566
  };
202
567
  });
568
+ return prefixed;
203
569
  }
204
570
  function wrapWithLayouts(Component, layouts = [], requestInfo) {
205
571
  if (layouts.length === 0) {
@@ -242,31 +608,51 @@ export const wrapHandlerToThrowResponses = (handler) => {
242
608
  ComponentWrappedToThrowResponses.__rwsdk_route_component = true;
243
609
  return ComponentWrappedToThrowResponses;
244
610
  };
611
+ /**
612
+ * Wraps routes with a layout component.
613
+ *
614
+ * @example
615
+ * // Define a layout component
616
+ * function BlogLayout({ children }: { children?: React.ReactNode }) {
617
+ * return (
618
+ * <div>
619
+ * <nav>Blog Navigation</nav>
620
+ * <main>{children}</main>
621
+ * </div>
622
+ * )
623
+ * }
624
+ *
625
+ * // Apply layout to routes
626
+ * const blogRoutes = layout(BlogLayout, [
627
+ * route("/", () => <BlogIndex />),
628
+ * route("/post/:id", ({ params }) => <BlogPost id={params.id} />),
629
+ * ])
630
+ */
245
631
  export function layout(LayoutComponent, routes) {
246
- // Attach layouts directly to route definitions
247
632
  return routes.map((route) => {
248
633
  if (typeof route === "function") {
249
634
  // Pass through middleware as-is
250
635
  return route;
251
636
  }
637
+ if (typeof route === "object" &&
638
+ route !== null &&
639
+ "__rwExcept" in route &&
640
+ route.__rwExcept === true) {
641
+ // Pass through ExceptHandler as-is
642
+ return route;
643
+ }
252
644
  if (Array.isArray(route)) {
253
645
  // Recursively process nested route arrays
254
646
  return layout(LayoutComponent, route);
255
647
  }
256
- // For RouteDefinition objects, prepend the layout so outer layouts come first
648
+ const routeDef = route;
257
649
  return {
258
- ...route,
259
- layouts: [LayoutComponent, ...(route.layouts || [])],
650
+ ...routeDef,
651
+ layouts: [LayoutComponent, ...(routeDef.layouts || [])],
260
652
  };
261
653
  });
262
654
  }
263
- export function render(Document, routes,
264
- /**
265
- * @param options - Configuration options for rendering.
266
- * @param options.rscPayload - Toggle the RSC payload that's appended to the Document. Disabling this will mean that interactivity no longer works.
267
- * @param options.ssr - Disable sever side rendering for all these routes. This only allow client side rendering`, which requires `rscPayload` to be enabled.
268
- */
269
- options = {}) {
655
+ export function render(Document, routes, options = {}) {
270
656
  options = {
271
657
  rscPayload: true,
272
658
  ssr: true,