[cli] Store build error in "builds.json" file in vc build (#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"
      }
    }
  ]
}
```
This commit is contained in:
Nathan Rajlich
2022-07-13 18:05:00 -07:00
committed by GitHub
parent bfdbe58675
commit 10c91c8579
8 changed files with 140 additions and 69 deletions

View File

@@ -1,4 +1,5 @@
# https://prettier.io/docs/en/ignore.html
# ignore this file with an intentional syntax error
# ignore these files with an intentional syntax error
packages/cli/test/dev/fixtures/edge-function-error/api/edge-error-syntax.js
packages/cli/test/fixtures/unit/commands/build/node-error/api/typescript.ts

View File

@@ -52,6 +52,13 @@ import { sortBuilders } from '../util/build/sort-builders';
type BuildResult = BuildResultV2 | BuildResultV3;
interface SerializedBuilder extends Builder {
error?: Error;
require?: string;
requirePath?: string;
apiVersion: number;
}
const help = () => {
return console.log(`
${chalk.bold(`${cli.logo} ${cli.name} build`)}
@@ -297,32 +304,36 @@ export default async function main(client: Client): Promise<number> {
const ops: Promise<Error | void>[] = [];
// Write the `detectedBuilders` result to output dir
ops.push(
fs.writeJSON(
join(outputDir, 'builds.json'),
{
'//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.',
target,
argv: process.argv,
builds: builds.map(build => {
const buildsJsonBuilds = new Map<Builder, SerializedBuilder>(
builds.map(build => {
const builderWithPkg = buildersWithPkgs.get(build.use);
if (!builderWithPkg) {
throw new Error(`Failed to load Builder "${build.use}"`);
}
const { builder, pkg: builderPkg } = builderWithPkg;
return {
return [
build,
{
require: builderPkg.name,
requirePath: builderWithPkg.path,
apiVersion: builder.version,
...build,
};
}),
},
{
spaces: 2,
}
)
];
})
);
const buildsJson = {
'//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.',
target,
argv: process.argv,
builds: Array.from(buildsJsonBuilds.values()),
};
const buildsJsonPath = join(outputDir, 'builds.json');
const writeBuildsJsonPromise = fs.writeJSON(buildsJsonPath, buildsJson, {
spaces: 2,
});
ops.push(writeBuildsJsonPromise);
// The `meta` config property is re-used for each Builder
// invocation so that Builders can share state between
@@ -347,6 +358,8 @@ export default async function main(client: Client): Promise<number> {
if (!builderWithPkg) {
throw new Error(`Failed to load Builder "${build.use}"`);
}
try {
const { builder, pkg: builderPkg } = builderWithPkg;
const buildConfig: Config = {
@@ -392,6 +405,25 @@ export default async function main(client: Client): Promise<number> {
err => err
)
);
} catch (err: any) {
await writeBuildsJsonPromise;
const buildJsonBuild = buildsJsonBuilds.get(build);
if (buildJsonBuild) {
buildJsonBuild.error = {
name: err.name,
message: err.message,
stack: err.stack,
...err,
};
await fs.writeJSON(buildsJsonPath, buildsJson, {
spaces: 2,
});
}
return 1;
}
}
if (corepackShimDir) {

View File

@@ -0,0 +1,7 @@
{
"orgId": ".",
"projectId": ".",
"settings": {
"framework": null
}
}

View File

@@ -0,0 +1 @@
export default (req, res) => res.end('Vercel');

View File

@@ -0,0 +1 @@
module.exports = (req, res) => res.end('Vercel');

View File

@@ -0,0 +1 @@
export default (req, res) => res.end('Vercel');

View File

@@ -0,0 +1,4 @@
import { IncomingMessage, ServerResponse } from 'http';
// Intentional syntax error to make the build fail
export default (req: IncomingMessage, res: ServerResponse => res.end('Vercel');

View File

@@ -589,8 +589,6 @@ describe('build', () => {
const output = join(cwd, '.vercel/output');
try {
process.chdir(cwd);
client.stderr.pipe(process.stderr);
client.setArgv('build');
const exitCode = await build(client);
expect(exitCode).toEqual(0);
@@ -619,4 +617,30 @@ describe('build', () => {
delete process.env.__VERCEL_BUILD_RUNNING;
}
});
it('should store Builder error in `builds.json`', async () => {
const cwd = fixture('node-error');
const output = join(cwd, '.vercel/output');
try {
process.chdir(cwd);
const exitCode = await build(client);
expect(exitCode).toEqual(1);
// `builds.json` contains "error" build
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds.builds).toHaveLength(4);
const errorBuilds = builds.builds.filter((b: any) => 'error' in b);
expect(errorBuilds).toHaveLength(1);
expect(errorBuilds[0].error.name).toEqual('Error');
expect(errorBuilds[0].error.message).toMatch(`TS1005`);
expect(errorBuilds[0].error.message).toMatch(`',' expected.`);
expect(errorBuilds[0].error.hideStackTrace).toEqual(true);
expect(errorBuilds[0].error.code).toEqual('NODE_TYPESCRIPT_ERROR');
} finally {
process.chdir(originalCwd);
delete process.env.__VERCEL_BUILD_RUNNING;
}
});
});