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.
- 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 +41 -19
- 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,54 +1,66 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { isValidElementType } from "react-is";
|
|
3
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
44
|
-
params[matchToken[1]] = matches[currentMatchIndex];
|
|
34
|
+
paramMap.push({ name: matchToken[1], isWildcard: false });
|
|
45
35
|
}
|
|
46
36
|
else {
|
|
47
|
-
|
|
48
|
-
params[`$${wildcardCounter}`] = matches[currentMatchIndex];
|
|
49
|
-
wildcardCounter++;
|
|
37
|
+
paramMap.push({ name: `$${wildcardCounter++}`, isWildcard: true });
|
|
50
38
|
}
|
|
51
|
-
|
|
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 [
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
255
|
+
let params = null;
|
|
256
|
+
if (route.isStatic) {
|
|
257
|
+
if (route.path === path) {
|
|
258
|
+
params = {};
|
|
259
|
+
}
|
|
153
260
|
}
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
|
185
|
-
if
|
|
186
|
-
|
|
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
|
-
|
|
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:
|
|
199
|
-
handler:
|
|
200
|
-
...(
|
|
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
|
-
|
|
648
|
+
const routeDef = route;
|
|
257
649
|
return {
|
|
258
|
-
...
|
|
259
|
-
layouts: [LayoutComponent, ...(
|
|
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,
|