Skip to content

Commit 10c91c8

Browse files
authored
[cli] Store build error in "builds.json" file in vc build (vercel#8148)
When a build fails, store the serialized Error in the "builds.json" file under the "build" object of the Builder that failed. Example: ```json { "//": "This file was generated by the `vercel build` command. It is not part of the Build Output API.", "target": "preview", "argv": [ "/usr/local/bin/node", "/Users/nrajlich/Code/vercel/vercel/packages/cli/src/index.ts", "build", "--cwd", "/Users/nrajlich/Downloads/vc-build-next-repro/" ], "builds": [ { "require": "@vercel/next", "requirePath": "/Users/nrajlich/Code/vercel/vercel/packages/next/dist/index", "apiVersion": 2, "src": "package.json", "use": "@vercel/next", "config": { "zeroConfig": true, "framework": "nextjs" }, "error": { "name": "Error", "message": "Command \"pnpm run build\" exited with 1", "stack": "Error: Command \"pnpm run build\" exited with 1\n at ChildProcess.<anonymous> (/Users/nrajlich/Code/vercel/vercel/packages/build-utils/dist/index.js:20591:20)\n at ChildProcess.emit (node:events:527:28)\n at ChildProcess.emit (node:domain:475:12)\n at maybeClose (node:internal/child_process:1092:16)\n at Process.ChildProcess._handle.onexit (node:internal/child_process:302:5)", "hideStackTrace": true, "code": "BUILD_UTILS_SPAWN_1" } } ] } ```
1 parent bfdbe58 commit 10c91c8

File tree

8 files changed

+141
-70
lines changed

8 files changed

+141
-70
lines changed

.prettierignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# https://prettier.io/docs/en/ignore.html
22

3-
# ignore this file with an intentional syntax error
3+
# ignore these files with an intentional syntax error
44
packages/cli/test/dev/fixtures/edge-function-error/api/edge-error-syntax.js
5+
packages/cli/test/fixtures/unit/commands/build/node-error/api/typescript.ts

packages/cli/src/commands/build.ts

+99-67
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ import { sortBuilders } from '../util/build/sort-builders';
5252

5353
type BuildResult = BuildResultV2 | BuildResultV3;
5454

55+
interface SerializedBuilder extends Builder {
56+
error?: Error;
57+
require?: string;
58+
requirePath?: string;
59+
apiVersion: number;
60+
}
61+
5562
const help = () => {
5663
return console.log(`
5764
${chalk.bold(`${cli.logo} ${cli.name} build`)}
@@ -297,32 +304,36 @@ export default async function main(client: Client): Promise<number> {
297304
const ops: Promise<Error | void>[] = [];
298305

299306
// Write the `detectedBuilders` result to output dir
300-
ops.push(
301-
fs.writeJSON(
302-
join(outputDir, 'builds.json'),
303-
{
304-
'//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.',
305-
target,
306-
argv: process.argv,
307-
builds: builds.map(build => {
308-
const builderWithPkg = buildersWithPkgs.get(build.use);
309-
if (!builderWithPkg) {
310-
throw new Error(`Failed to load Builder "${build.use}"`);
311-
}
312-
const { builder, pkg: builderPkg } = builderWithPkg;
313-
return {
314-
require: builderPkg.name,
315-
requirePath: builderWithPkg.path,
316-
apiVersion: builder.version,
317-
...build,
318-
};
319-
}),
320-
},
321-
{
322-
spaces: 2,
307+
const buildsJsonBuilds = new Map<Builder, SerializedBuilder>(
308+
builds.map(build => {
309+
const builderWithPkg = buildersWithPkgs.get(build.use);
310+
if (!builderWithPkg) {
311+
throw new Error(`Failed to load Builder "${build.use}"`);
323312
}
324-
)
313+
const { builder, pkg: builderPkg } = builderWithPkg;
314+
return [
315+
build,
316+
{
317+
require: builderPkg.name,
318+
requirePath: builderWithPkg.path,
319+
apiVersion: builder.version,
320+
...build,
321+
},
322+
];
323+
})
325324
);
325+
const buildsJson = {
326+
'//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.',
327+
target,
328+
argv: process.argv,
329+
builds: Array.from(buildsJsonBuilds.values()),
330+
};
331+
const buildsJsonPath = join(outputDir, 'builds.json');
332+
const writeBuildsJsonPromise = fs.writeJSON(buildsJsonPath, buildsJson, {
333+
spaces: 2,
334+
});
335+
336+
ops.push(writeBuildsJsonPromise);
326337

327338
// The `meta` config property is re-used for each Builder
328339
// invocation so that Builders can share state between
@@ -347,51 +358,72 @@ export default async function main(client: Client): Promise<number> {
347358
if (!builderWithPkg) {
348359
throw new Error(`Failed to load Builder "${build.use}"`);
349360
}
350-
const { builder, pkg: builderPkg } = builderWithPkg;
351-
352-
const buildConfig: Config = {
353-
outputDirectory: project.settings.outputDirectory ?? undefined,
354-
...build.config,
355-
projectSettings: project.settings,
356-
installCommand: project.settings.installCommand ?? undefined,
357-
devCommand: project.settings.devCommand ?? undefined,
358-
buildCommand: project.settings.buildCommand ?? undefined,
359-
framework: project.settings.framework,
360-
nodeVersion: project.settings.nodeVersion,
361-
};
362-
const buildOptions: BuildOptions = {
363-
files: filesMap,
364-
entrypoint: build.src,
365-
workPath,
366-
repoRootPath,
367-
config: buildConfig,
368-
meta,
369-
};
370-
output.debug(
371-
`Building entrypoint "${build.src}" with "${builderPkg.name}"`
372-
);
373-
const buildResult = await builder.build(buildOptions);
374361

375-
// Store the build result to generate the final `config.json` after
376-
// all builds have completed
377-
buildResults.set(build, buildResult);
362+
try {
363+
const { builder, pkg: builderPkg } = builderWithPkg;
364+
365+
const buildConfig: Config = {
366+
outputDirectory: project.settings.outputDirectory ?? undefined,
367+
...build.config,
368+
projectSettings: project.settings,
369+
installCommand: project.settings.installCommand ?? undefined,
370+
devCommand: project.settings.devCommand ?? undefined,
371+
buildCommand: project.settings.buildCommand ?? undefined,
372+
framework: project.settings.framework,
373+
nodeVersion: project.settings.nodeVersion,
374+
};
375+
const buildOptions: BuildOptions = {
376+
files: filesMap,
377+
entrypoint: build.src,
378+
workPath,
379+
repoRootPath,
380+
config: buildConfig,
381+
meta,
382+
};
383+
output.debug(
384+
`Building entrypoint "${build.src}" with "${builderPkg.name}"`
385+
);
386+
const buildResult = await builder.build(buildOptions);
387+
388+
// Store the build result to generate the final `config.json` after
389+
// all builds have completed
390+
buildResults.set(build, buildResult);
391+
392+
// Start flushing the file outputs to the filesystem asynchronously
393+
ops.push(
394+
writeBuildResult(
395+
outputDir,
396+
buildResult,
397+
build,
398+
builder,
399+
builderPkg,
400+
vercelConfig?.cleanUrls
401+
).then(
402+
override => {
403+
if (override) overrides.push(override);
404+
},
405+
err => err
406+
)
407+
);
408+
} catch (err: any) {
409+
await writeBuildsJsonPromise;
410+
411+
const buildJsonBuild = buildsJsonBuilds.get(build);
412+
if (buildJsonBuild) {
413+
buildJsonBuild.error = {
414+
name: err.name,
415+
message: err.message,
416+
stack: err.stack,
417+
...err,
418+
};
419+
420+
await fs.writeJSON(buildsJsonPath, buildsJson, {
421+
spaces: 2,
422+
});
423+
}
378424

379-
// Start flushing the file outputs to the filesystem asynchronously
380-
ops.push(
381-
writeBuildResult(
382-
outputDir,
383-
buildResult,
384-
build,
385-
builder,
386-
builderPkg,
387-
vercelConfig?.cleanUrls
388-
).then(
389-
override => {
390-
if (override) overrides.push(override);
391-
},
392-
err => err
393-
)
394-
);
425+
return 1;
426+
}
395427
}
396428

397429
if (corepackShimDir) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"orgId": ".",
3+
"projectId": ".",
4+
"settings": {
5+
"framework": null
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default (req, res) => res.end('Vercel');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = (req, res) => res.end('Vercel');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default (req, res) => res.end('Vercel');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { IncomingMessage, ServerResponse } from 'http';
2+
3+
// Intentional syntax error to make the build fail
4+
export default (req: IncomingMessage, res: ServerResponse => res.end('Vercel');

packages/cli/test/unit/commands/build.test.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -589,8 +589,6 @@ describe('build', () => {
589589
const output = join(cwd, '.vercel/output');
590590
try {
591591
process.chdir(cwd);
592-
client.stderr.pipe(process.stderr);
593-
client.setArgv('build');
594592
const exitCode = await build(client);
595593
expect(exitCode).toEqual(0);
596594

@@ -619,4 +617,30 @@ describe('build', () => {
619617
delete process.env.__VERCEL_BUILD_RUNNING;
620618
}
621619
});
620+
621+
it('should store Builder error in `builds.json`', async () => {
622+
const cwd = fixture('node-error');
623+
const output = join(cwd, '.vercel/output');
624+
try {
625+
process.chdir(cwd);
626+
const exitCode = await build(client);
627+
expect(exitCode).toEqual(1);
628+
629+
// `builds.json` contains "error" build
630+
const builds = await fs.readJSON(join(output, 'builds.json'));
631+
expect(builds.builds).toHaveLength(4);
632+
633+
const errorBuilds = builds.builds.filter((b: any) => 'error' in b);
634+
expect(errorBuilds).toHaveLength(1);
635+
636+
expect(errorBuilds[0].error.name).toEqual('Error');
637+
expect(errorBuilds[0].error.message).toMatch(`TS1005`);
638+
expect(errorBuilds[0].error.message).toMatch(`',' expected.`);
639+
expect(errorBuilds[0].error.hideStackTrace).toEqual(true);
640+
expect(errorBuilds[0].error.code).toEqual('NODE_TYPESCRIPT_ERROR');
641+
} finally {
642+
process.chdir(originalCwd);
643+
delete process.env.__VERCEL_BUILD_RUNNING;
644+
}
645+
});
622646
});

0 commit comments

Comments
 (0)