Skip to content

Commit 4592be5

Browse files
authored
Fix public files taking priority over API routes in build (#15331)
1 parent 7c55f80 commit 4592be5

5 files changed

Lines changed: 77 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fixes an issue where API routes would overwrite public files during build. Public files now correctly take priority over generated routes in both dev and build modes.

packages/astro/src/core/build/generate.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'node:fs';
22
import os from 'node:os';
3+
import { fileURLToPath } from 'node:url';
34
import PLimit from 'p-limit';
45
import PQueue from 'p-queue';
56
import colors from 'piccolore';
@@ -35,6 +36,8 @@ import type { BuildApp } from './app.js';
3536
import { getOutFile, getOutFolder } from './common.js';
3637
import { type BuildInternals, hasPrerenderedPages } from './internal.js';
3738
import type { StaticBuildOptions } from './types.js';
39+
import type { AstroSettings } from '../../types/astro.js';
40+
import type { Logger } from '../logger/core.js';
3841
import { getTimeStat, shouldAppendForwardSlash } from './util.js';
3942

4043
export async function generatePages(
@@ -617,6 +620,9 @@ async function generatePath(
617620
routeToHeaders.set(pathname, { headers: responseHeaders, route: integrationRoute });
618621
}
619622

623+
// Public files take priority over generated routes
624+
if (checkPublicConflict(outFile, route, settings, logger)) return false;
625+
620626
await fs.promises.mkdir(outFolder, { recursive: true });
621627
await fs.promises.writeFile(outFile, body);
622628

@@ -634,3 +640,29 @@ function getPrettyRouteName(route: RouteData): string {
634640
}
635641
return route.component;
636642
}
643+
644+
/**
645+
* Check if a file exists in the public directory that would conflict with the output file.
646+
* Public files take priority over generated routes. Returns true if there's a conflict.
647+
*/
648+
function checkPublicConflict(
649+
outFile: URL,
650+
route: RouteData,
651+
settings: AstroSettings,
652+
logger: Logger,
653+
): boolean {
654+
const outFilePath = fileURLToPath(outFile);
655+
const outRoot = fileURLToPath(
656+
settings.buildOutput === 'static' ? settings.config.outDir : settings.config.build.client,
657+
);
658+
const relativePath = outFilePath.slice(outRoot.length);
659+
const publicFilePath = new URL(relativePath, settings.config.publicDir);
660+
if (fs.existsSync(publicFilePath)) {
661+
logger.warn(
662+
'build',
663+
`Skipping ${route.component} because a file with the same name exists in the public folder: ${relativePath}`,
664+
);
665+
return true;
666+
}
667+
return false;
668+
}

packages/astro/test/astro-public.test.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from 'node:assert/strict';
22
import { Writable } from 'node:stream';
3-
import { before, describe, it } from 'node:test';
3+
import { after, before, describe, it } from 'node:test';
44
import { Logger } from '../dist/core/logger/core.js';
55
import { loadFixture } from './test-utils.js';
66

@@ -47,4 +47,36 @@ describe('Public', () => {
4747
assert.fail(`Should not have empty chunk warning, but got: ${emptyChunkWarning.message}`);
4848
}
4949
});
50+
51+
it('public file takes priority over API route in build', async () => {
52+
// When both public/robots.txt and src/pages/robots.txt.ts exist,
53+
// the public file should take priority
54+
const robotsTxt = await fixture.readFile('/robots.txt');
55+
assert.match(robotsTxt, /Disallow: \/admin\//, 'Should contain public file content');
56+
assert.doesNotMatch(robotsTxt, /Disallow: \/\n/, 'Should not contain API route content');
57+
});
58+
});
59+
60+
describe('Public (dev)', () => {
61+
let fixture;
62+
let devServer;
63+
64+
before(async () => {
65+
fixture = await loadFixture({ root: './fixtures/astro-public/' });
66+
devServer = await fixture.startDevServer();
67+
});
68+
69+
after(async () => {
70+
await devServer.stop();
71+
});
72+
73+
it('public file takes priority over API route in dev', async () => {
74+
// When both public/robots.txt and src/pages/robots.txt.ts exist,
75+
// the public file should take priority
76+
const response = await fixture.fetch('/robots.txt');
77+
assert.equal(response.status, 200);
78+
const text = await response.text();
79+
assert.match(text, /Disallow: \/admin\//, 'Should contain public file content');
80+
assert.doesNotMatch(text, /Disallow: \/\n/, 'Should not contain API route content');
81+
});
5082
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
User-agent: *
2+
Disallow: /admin/
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { APIRoute } from 'astro';
2+
3+
export const GET: APIRoute = () => {
4+
return new Response('User-agent: *\nDisallow: /\n');
5+
};

0 commit comments

Comments
 (0)