@redwoodjs/sdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
116 lines (115 loc) • 4 kB
JavaScript
import { isValidElementType } from "react-is";
function matchPath(routePath, requestPath) {
const pattern = routePath
.replace(/:[a-zA-Z0-9]+/g, "([^/]+)") // Convert :param to capture group
.replace(/\*/g, "(.*)"); // Convert * to wildcard capture group
const regex = new RegExp(`^${pattern}$`);
const matches = requestPath.match(regex);
if (!matches) {
return null;
}
// Extract named parameters and wildcards
const params = {};
const paramNames = [...routePath.matchAll(/:[a-zA-Z0-9]+/g)].map((m) => m[0].slice(1));
const wildcardCount = (routePath.match(/\*/g) || []).length;
// Add named parameters
paramNames.forEach((name, i) => {
params[name] = matches[i + 1];
});
// Add wildcard parameters with numeric indices
for (let i = 0; i < wildcardCount; i++) {
const wildcardIndex = paramNames.length + i + 1;
params[`$${i}`] = matches[wildcardIndex];
}
return params;
}
function flattenRoutes(routes) {
return routes.reduce((acc, route) => {
if (Array.isArray(route)) {
return [...acc, ...flattenRoutes(route)];
}
return [...acc, route];
}, []);
}
export function defineRoutes(routes) {
const flattenedRoutes = flattenRoutes(routes);
return {
routes: flattenedRoutes,
async handle({ request, renderPage, getRequestInfo, runWithRequestInfoOverrides, }) {
const url = new URL(request.url);
let path = url.pathname;
// Must end with a trailing slash.
if (path !== "/" && !path.endsWith("/")) {
path = path + "/";
}
// Find matching route
let match = null;
for (const route of flattenedRoutes) {
if (typeof route === "function") {
const r = await route(getRequestInfo());
if (r instanceof Response) {
return r;
}
continue;
}
const params = matchPath(route.path, path);
if (params) {
match = { params, handler: route.handler };
break;
}
}
if (!match) {
// todo(peterp, 2025-01-28): Allow the user to define their own "not found" route.
return new Response("Not Found", { status: 404 });
}
let { params, handler } = match;
return runWithRequestInfoOverrides({ params }, async () => {
const handlers = Array.isArray(handler) ? handler : [handler];
for (const h of handlers) {
if (isRouteComponent(h)) {
return await renderPage(getRequestInfo(), h);
}
else {
const r = await h(getRequestInfo());
if (r instanceof Response) {
return r;
}
}
}
// Add fallback return
return new Response("Response not returned from route handler", {
status: 500,
});
});
},
};
}
export function route(path, handler) {
if (!path.endsWith("/")) {
path = path + "/";
}
return {
path,
handler,
};
}
export function index(handler) {
return route("/", handler);
}
export function prefix(prefix, routes) {
return routes.map((r) => {
return {
path: prefix + r.path,
handler: r.handler,
};
});
}
export function render(Document, routes) {
const documentMiddleware = ({ rw }) => {
rw.Document = Document;
};
return [documentMiddleware, ...routes];
}
function isRouteComponent(handler) {
return isValidElementType(handler) && handler.toString().includes("jsx");
}