Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Commit b725b49

Browse files
authored
fix(vercel): output build directory (#437)
1 parent ff2e024 commit b725b49

File tree

3 files changed

+79
-74
lines changed

3 files changed

+79
-74
lines changed

.changeset/slow-mangos-call.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@astrojs/vercel': patch
3+
---
4+
5+
Fixes a regression where the `@astrojs/vercel` single entry point for the adapter was causing some regressions in users projects.

packages/vercel/src/index.ts

+68-72
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, readFileSync } from 'node:fs';
1+
import { cpSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
22
import { basename } from 'node:path';
33
import { pathToFileURL } from 'node:url';
44
import { emptyDir, removeDir, writeJson } from '@astrojs/internal-helpers/fs';
@@ -7,6 +7,7 @@ import type {
77
AstroConfig,
88
AstroIntegration,
99
AstroIntegrationLogger,
10+
HookParameters,
1011
IntegrationRouteData,
1112
} from 'astro';
1213
import glob from 'fast-glob';
@@ -188,13 +189,14 @@ export default function vercelAdapter({
188189
// Secret used to verify that the caller is the astro-generated edge middleware and not a third-party
189190
const middlewareSecret = crypto.randomUUID();
190191

191-
let buildOutput: 'server' | 'static';
192+
let _buildOutput: 'server' | 'static';
193+
194+
let staticDir: URL | undefined;
192195

193196
return {
194197
name: PACKAGE_NAME,
195198
hooks: {
196199
'astro:config:setup': async ({ command, config, updateConfig, injectScript, logger }) => {
197-
buildOutput = config.output;
198200
if (webAnalytics?.enabled) {
199201
injectScript(
200202
'head-inline',
@@ -204,7 +206,30 @@ export default function vercelAdapter({
204206
);
205207
}
206208

207-
if (buildOutput === 'server') {
209+
staticDir = new URL('./.vercel/output/static', config.root);
210+
updateConfig({
211+
build: {
212+
format: 'directory',
213+
redirects: false,
214+
},
215+
vite: {
216+
ssr: {
217+
external: ['@vercel/nft'],
218+
},
219+
},
220+
...getAstroImageConfig(
221+
imageService,
222+
imagesConfig,
223+
command,
224+
devImageService,
225+
config.image
226+
),
227+
});
228+
},
229+
'astro:config:done': ({ setAdapter, config, logger, buildOutput }) => {
230+
_buildOutput = buildOutput;
231+
232+
if (_buildOutput === 'server') {
208233
if (maxDuration && maxDuration > 900) {
209234
logger.warn(
210235
`maxDuration is set to ${maxDuration} seconds, which is longer than the maximum allowed duration of 900 seconds.`
@@ -214,7 +239,6 @@ export default function vercelAdapter({
214239
`Please make sure that your plan allows for this duration. See https://vercel.com/docs/functions/serverless-functions/runtimes#maxduration for more information.`
215240
);
216241
}
217-
218242
const vercelConfigPath = new URL('vercel.json', config.root);
219243
if (existsSync(vercelConfigPath)) {
220244
try {
@@ -225,65 +249,21 @@ export default function vercelAdapter({
225249
`\tYour "vercel.json" \`trailingSlash\` configuration (set to \`true\`) will conflict with your Astro \`trailinglSlash\` configuration (set to \`"always"\`).\n` +
226250
// biome-ignore lint/style/noUnusedTemplateLiteral: <explanation>
227251
`\tThis would cause infinite redirects under certain conditions and throw an \`ERR_TOO_MANY_REDIRECTS\` error.\n` +
228-
`\tTo prevent this, your Astro configuration is updated to \`"ignore"\` during builds.\n`
252+
`\tTo prevent this, change your Astro configuration and update \`"trailingSlash"\` to \`"ignore"\`.\n`
229253
);
230-
updateConfig({
231-
trailingSlash: 'ignore',
232-
});
233254
}
234255
} catch (_err) {
235256
logger.warn(`Your "vercel.json" config is not a valid json file.`);
236257
}
237258
}
238-
239-
updateConfig({
240-
outDir: new URL('./.vercel/output/', config.root),
241-
build: {
242-
client: new URL('./.vercel/output/static/', config.root),
243-
server: new URL('./.vercel/output/_functions/', config.root),
244-
redirects: false,
245-
},
246-
vite: {
247-
ssr: {
248-
external: ['@vercel/nft'],
249-
},
250-
},
251-
...getAstroImageConfig(
252-
imageService,
253-
imagesConfig,
254-
command,
255-
devImageService,
256-
config.image
257-
),
258-
});
259-
} else {
260-
const outDir = new URL('./.vercel/output/static/', config.root);
261-
updateConfig({
262-
outDir,
263-
build: {
264-
format: 'directory',
265-
redirects: false,
266-
},
267-
...getAstroImageConfig(
268-
imageService,
269-
imagesConfig,
270-
command,
271-
devImageService,
272-
config.image
273-
),
274-
});
275-
}
276-
},
277-
'astro:config:done': ({ setAdapter, config }) => {
278-
if (buildOutput === 'server') {
279-
setAdapter(getAdapter({ buildOutput, edgeMiddleware, middlewareSecret, skewProtection }));
259+
setAdapter(getAdapter({ buildOutput: _buildOutput, edgeMiddleware, middlewareSecret, skewProtection }));
280260
} else {
281261
setAdapter(
282262
getAdapter({
283263
edgeMiddleware: false,
284264
middlewareSecret: '',
285265
skewProtection,
286-
buildOutput,
266+
buildOutput: _buildOutput,
287267
})
288268
);
289269
}
@@ -292,27 +272,44 @@ export default function vercelAdapter({
292272
_serverEntry = config.build.serverEntry;
293273
},
294274
'astro:build:start': async () => {
295-
if (buildOutput !== 'static') {
296-
// Ensure to have `.vercel/output` empty.
297-
// This is because, when building to static, outDir = .vercel/output/static/,
298-
// so .vercel/output itself won't get cleaned.
299-
await emptyDir(new URL('./.vercel/output/', _config.root));
300-
}
275+
// Ensure to have `.vercel/output` empty.
276+
await emptyDir(new URL('./.vercel/output/', _config.root));
301277
},
302278
'astro:build:ssr': async ({ entryPoints, middlewareEntryPoint }) => {
303279
_entryPoints = new Map(
304280
Array.from(entryPoints).filter(([routeData]) => !routeData.prerender)
305281
);
306282
_middlewareEntryPoint = middlewareEntryPoint;
307283
},
308-
'astro:build:done': async ({ routes, logger }) => {
284+
'astro:build:done': async ({ routes, logger }: HookParameters<'astro:build:done'>) => {
285+
const outDir = new URL('./.vercel/output/', _config.root);
286+
if (staticDir) {
287+
if (existsSync(staticDir)) {
288+
emptyDir(staticDir);
289+
}
290+
mkdirSync(new URL('./.vercel/output/static/', _config.root), { recursive: true });
291+
292+
if (_buildOutput === 'static' && staticDir) {
293+
cpSync(_config.outDir, new URL('./.vercel/output/static/', _config.root), {
294+
recursive: true,
295+
});
296+
} else {
297+
cpSync(_config.build.client, new URL('./.vercel/output/static/', _config.root), {
298+
recursive: true,
299+
});
300+
cpSync(_config.build.server, new URL('./.vercel/output/_functions/', _config.root), {
301+
recursive: true,
302+
});
303+
}
304+
}
305+
309306
const routeDefinitions: Array<{
310307
src: string;
311308
dest: string;
312309
middlewarePath?: string;
313310
}> = [];
314311

315-
if (buildOutput === 'server') {
312+
if (_buildOutput === 'server') {
316313
// Merge any includes from `vite.assetsInclude
317314
if (_config.vite.assetsInclude) {
318315
const mergeGlobbedIncludes = (globPattern: unknown) => {
@@ -339,6 +336,7 @@ export default function vercelAdapter({
339336
excludeFiles,
340337
includeFiles,
341338
logger,
339+
outDir,
342340
maxDuration
343341
);
344342

@@ -399,10 +397,7 @@ export default function vercelAdapter({
399397
}
400398
}
401399
const fourOhFourRoute = routes.find((route) => route.pathname === '/404');
402-
const destination =
403-
buildOutput === 'server'
404-
? new URL('./config.json', _config.outDir)
405-
: new URL('./.vercel/output/config.json', _config.root);
400+
const destination = new URL('./.vercel/output/config.json', _config.root);
406401
const finalRoutes = [
407402
...getRedirects(routes, _config),
408403
{
@@ -412,12 +407,12 @@ export default function vercelAdapter({
412407
},
413408
{ handle: 'filesystem' },
414409
];
415-
if (buildOutput === 'server') {
410+
if (_buildOutput === 'server') {
416411
finalRoutes.push(...routeDefinitions);
417412
}
418413

419414
if (fourOhFourRoute) {
420-
if (buildOutput === 'server') {
415+
if (_buildOutput === 'server') {
421416
finalRoutes.push({
422417
src: '/.*',
423418
dest: fourOhFourRoute.prerender
@@ -464,7 +459,7 @@ export default function vercelAdapter({
464459
});
465460

466461
// Remove temporary folder
467-
if (buildOutput === 'server') {
462+
if (_buildOutput === 'server') {
468463
await removeDir(_buildTempFolder);
469464
}
470465
},
@@ -495,16 +490,17 @@ class VercelBuilder {
495490
readonly excludeFiles: URL[],
496491
readonly includeFiles: URL[],
497492
readonly logger: AstroIntegrationLogger,
493+
readonly outDir: URL,
498494
readonly maxDuration?: number,
499495
readonly runtime = getRuntime(process, logger)
500496
) {}
501497

502498
async buildServerlessFolder(entry: URL, functionName: string, root: URL) {
503499
const { config, includeFiles, excludeFiles, logger, NTF_CACHE, runtime, maxDuration } = this;
504500
// .vercel/output/functions/<name>.func/
505-
const functionFolder = new URL(`./functions/${functionName}.func/`, config.outDir);
506-
const packageJson = new URL(`./functions/${functionName}.func/package.json`, config.outDir);
507-
const vcConfig = new URL(`./functions/${functionName}.func/.vc-config.json`, config.outDir);
501+
const functionFolder = new URL(`./functions/${functionName}.func/`, this.outDir);
502+
const packageJson = new URL(`./functions/${functionName}.func/package.json`, this.outDir);
503+
const vcConfig = new URL(`./functions/${functionName}.func/.vc-config.json`, this.outDir);
508504

509505
// Copy necessary files (e.g. node_modules/)
510506
const { handler } = await copyDependenciesToFunction(
@@ -538,7 +534,7 @@ class VercelBuilder {
538534
await this.buildServerlessFolder(entry, functionName, root);
539535
const prerenderConfig = new URL(
540536
`./functions/${functionName}.prerender-config.json`,
541-
this.config.outDir
537+
this.outDir
542538
);
543539
// https://vercel.com/docs/build-output-api/v3/primitives#prerender-configuration-file
544540
await writeJson(prerenderConfig, {
@@ -550,7 +546,7 @@ class VercelBuilder {
550546
}
551547

552548
async buildMiddlewareFolder(entry: URL, functionName: string, middlewareSecret: string) {
553-
const functionFolder = new URL(`./functions/${functionName}.func/`, this.config.outDir);
549+
const functionFolder = new URL(`./functions/${functionName}.func/`, this.outDir);
554550

555551
await generateEdgeMiddleware(
556552
entry,

packages/vercel/test/serverless-prerender.test.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ describe('Serverless prerender', () => {
2222
const [file] = await fixture.glob(
2323
'../.vercel/output/functions/_render.func/packages/vercel/test/fixtures/serverless-prerender/.vercel/output/_functions/pages/_image.astro.mjs'
2424
);
25-
const contents = await fixture.readFile(file);
26-
assert.ok(!contents.includes('const outDir ='), "outDir is tree-shaken if it's not imported");
25+
try {
26+
await fixture.readFile(file);
27+
assert.fail();
28+
} catch {
29+
assert.ok('Function do be three-shaken');
30+
}
2731
});
2832

2933
// TODO: The path here seems to be inconsistent?

0 commit comments

Comments
 (0)