Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/prerender/prerender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export async function prerender(nitro: Nitro) {
const failedRoutes = new Set<PrerenderRoute>();
const skippedRoutes = new Set();
const displayedLengthWarns = new Set();
// Resolved output file -> the route that produced it. Two routes can resolve to
// one file (`/other` and `/other/index.html` both become `other/index.html`),
// which is only known once each of them has been rendered.
const routeByOutputFile = new Map<string, string>();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const publicAssetBases: string[] = nitro.options.publicAssets
.filter(
Expand Down Expand Up @@ -295,7 +299,18 @@ export async function prerender(nitro: Nitro) {

// Write to the disk
const filePath = join(nitro.options.output.publicDir, _route.fileName);
if (canWriteToDisk(_route) && filePath.startsWith(nitro.options.output.publicDir)) {
const writtenBy = routeByOutputFile.get(_route.fileName);
if (writtenBy !== undefined) {
// Writing again would replace the first route's output with this one, so the
// build result would depend on which of the two rendered first
nitro.logger.warn(
`Routes \`${writtenBy}\` and \`${route}\` both prerender to \`${_route.fileName}\`. Keeping the output of \`${writtenBy}\`.`
);
_route.skip = true;
} else if (canWriteToDisk(_route) && filePath.startsWith(nitro.options.output.publicDir)) {
// Claim the file before awaiting, so a concurrent render of a route resolving
// to the same file sees it as taken
routeByOutputFile.set(_route.fileName, route);
await writeFile(filePath, dataBuff!);
nitro._prerenderedRoutes!.push(_route);
} else {
Expand Down
54 changes: 54 additions & 0 deletions test/prerender/collision.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { mkdir, readFile, rm } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { join } from "pathe";
import { build, copyPublicAssets, createNitro, prepare, prerender } from "nitro/builder";
import { afterAll, describe, expect, it } from "vitest";

const fixtureDir = fileURLToPath(new URL("./fixture", import.meta.url));
const tmpDir = fileURLToPath(new URL("./.tmp", import.meta.url));

describe("prerender output collision", () => {
afterAll(async () => {
await rm(tmpDir, { recursive: true, force: true });
});

it("keeps the first route's output and warns instead of overwriting it", async () => {
const outDir = join(tmpDir, "output");
await rm(outDir, { recursive: true, force: true });
await mkdir(outDir, { recursive: true });

const nitro = await createNitro({
rootDir: fixtureDir,
preset: "static",
output: { dir: outDir },
prerender: {
crawlLinks: false,
// both resolve to `other/index.html`
routes: ["/other", "/other/index.html"],
},
});

const warnings: string[] = [];
nitro.logger.warn = ((...args: unknown[]) => {
warnings.push(args.map(String).join(" "));
}) as typeof nitro.logger.warn;

await prepare(nitro);
await copyPublicAssets(nitro);
await prerender(nitro);
await build(nitro);
await nitro.close();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const collisionWarnings = warnings.filter((w) => w.includes("both prerender to"));
expect(collisionWarnings).toHaveLength(1);
expect(collisionWarnings[0]).toContain("other/index.html");

// only one of the two routes may claim the file
const written = nitro._prerenderedRoutes!.filter((r) => r.fileName === "/other/index.html");
expect(written).toHaveLength(1);

// and the file holds that route's render, whole
const contents = await readFile(join(outDir, "public/other/index.html"), "utf8");
expect(contents).toBe(`<!DOCTYPE html><html><body>rendered ${written[0].route}</body></html>`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}, 120_000);
});
8 changes: 8 additions & 0 deletions test/prerender/fixture/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
fetch(req: Request) {
const { pathname } = new URL(req.url);
return new Response(`<!DOCTYPE html><html><body>rendered ${pathname}</body></html>`, {
headers: { "content-type": "text/html" },
});
},
};