Skip to content

Commit 6ea83ef

Browse files
authored
fix: handle redirects for unmatched routes (#5846)
1 parent 6bf973d commit 6ea83ef

2 files changed

Lines changed: 63 additions & 0 deletions

File tree

packages/cli/src/prebuild.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import {
55
readdir,
66
readFile,
77
rm,
8+
symlink,
89
writeFile,
910
} from "node:fs/promises";
1011
import { join } from "node:path";
12+
import { pathToFileURL } from "node:url";
1113
import { tmpdir } from "node:os";
1214
import { bundleVersion } from "@webstudio-is/protocol";
1315
import { generateRedirectsModule, prebuild } from "./prebuild";
@@ -20,6 +22,35 @@ const rootFolderId = "root";
2022
const elementComponent = "ws:element";
2123
const slowPrebuildTestTimeout = 15_000;
2224
type Redirects = Array<{ old: string; new: string; status?: "301" | "302" }>;
25+
type GeneratedRouteModule = {
26+
loader: (args: { request: Request }) => Response | Promise<Response>;
27+
};
28+
29+
const importGeneratedRoute = async (path: string) => {
30+
await symlink(join(originalCwd, "node_modules"), "node_modules", "dir");
31+
return (await import(
32+
`${pathToFileURL(join(tempDir, path)).href}?test=${crypto.randomUUID()}`
33+
)) as GeneratedRouteModule;
34+
};
35+
36+
const expectGeneratedRedirectFallback = async (path: string) => {
37+
const routeModule = await importGeneratedRoute(path);
38+
const redirectResponse = await routeModule.loader({
39+
request: new Request("https://example.com/dl.php?filename=file.pdf"),
40+
});
41+
expect(redirectResponse.status).toBe(301);
42+
expect(redirectResponse.headers.get("Location")).toBe("/downloads/file.pdf");
43+
44+
try {
45+
await routeModule.loader({
46+
request: new Request("https://example.com/not-a-redirect"),
47+
});
48+
throw new Error("Expected unmatched request to throw a 404 response.");
49+
} catch (error) {
50+
expect(error).toBeInstanceOf(Response);
51+
expect((error as Response).status).toBe(404);
52+
}
53+
};
2354

2455
const getFilePaths = async (dir: string): Promise<string[]> => {
2556
const entries = await readdir(dir, { withFileTypes: true });
@@ -302,6 +333,7 @@ describe("prebuild", () => {
302333
expect(routeTemplate).toContain("../__generated__/_index.server");
303334
expect(routeTemplate).not.toContain("__CLIENT__");
304335
expect(routeTemplate).not.toContain("__SERVER__");
336+
await expectGeneratedRedirectFallback("app/routes/$.tsx");
305337

306338
await expect(
307339
readFile("app/__generated__/stale.ts", "utf8")

packages/cli/src/prebuild.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,28 @@ export const generateRedirectsModule = (pageRedirects: Pages["redirects"]) => {
196196
`;
197197
};
198198

199+
const generateRedirectFallbackRoute = (runtime: "remix" | "react-router") => {
200+
const loaderFunctionArgs =
201+
runtime === "react-router" ? "react-router" : "@remix-run/server-runtime";
202+
203+
return `
204+
import { type LoaderFunctionArgs } from ${JSON.stringify(loaderFunctionArgs)};
205+
import { redirectRequest } from "../redirect-url";
206+
// @todo think about how to make __generated__ typeable
207+
// @ts-ignore
208+
import { redirects } from "../__generated__/$resources.redirects";
209+
210+
export const loader = ({ request }: LoaderFunctionArgs) => {
211+
const redirectResponse = redirectRequest(request, redirects);
212+
if (redirectResponse !== undefined) {
213+
return redirectResponse;
214+
}
215+
216+
throw new Response("Not Found", { status: 404 });
217+
};
218+
`;
219+
};
220+
199221
export const prebuild = async (options: {
200222
/**
201223
* Do we need download assets
@@ -732,6 +754,15 @@ export const prebuild = async (options: {
732754
generateRedirectsModule(pages.redirects)
733755
);
734756

757+
if (pages.redirects !== undefined && pages.redirects.length > 0) {
758+
await createFileIfNotExists(
759+
join(routesDir, "$.tsx"),
760+
generateRedirectFallbackRoute(
761+
options.template.includes("react-router") ? "react-router" : "remix"
762+
)
763+
);
764+
}
765+
735766
if (options.assets === true && siteData.assets.length > 0) {
736767
const downloading = spinner();
737768
downloading.start("Downloading fonts and images");

0 commit comments

Comments
 (0)