[cli] Switch to npm and add more context to install errors during Builder import (#8545)

Before:

<img width="1012" alt="Screen Shot 2022-09-09 at 7 03 01 PM" src="https://user-images.githubusercontent.com/71256/189464732-8cf6398a-9432-423b-8509-a52bd714333e.png">

After:

<img width="579" alt="Screen Shot 2022-09-12 at 12 27 31 PM" src="https://user-images.githubusercontent.com/71256/189739091-86399428-d9b8-4d03-b0e6-7d27a1037bce.png">

This is a precursor to https://github.com/vercel/vercel/pull/8485, since the last remaining test failure there is related to specialized messaging we had for these same cases.
This commit is contained in:
Nathan Rajlich
2022-09-12 15:05:38 -07:00
committed by GitHub
parent b5c3610113
commit 2cb008e8ed
2 changed files with 88 additions and 8 deletions

View File

@@ -1,3 +1,5 @@
import { URL } from 'url';
import plural from 'pluralize';
import npa from 'npm-package-arg';
import { satisfies } from 'semver';
import { dirname, join } from 'path';
@@ -13,6 +15,9 @@ import { VERCEL_DIR } from '../projects/link';
import { Output } from '../output';
import readJSONFile from '../read-json-file';
import { CantParseJSONFile } from '../errors-ts';
import { errorToString, isErrnoException, isError } from '../is-error';
import cmd from '../output/cmd';
import code from '../output/code';
export interface BuilderWithPkg {
path: string;
@@ -201,15 +206,54 @@ async function installBuilders(
if (err.code !== 'EEXIST') throw err;
}
output.debug(`Installing Builders: ${Array.from(buildersToAdd).join(', ')}`);
await spawnAsync('yarn', ['add', '@vercel/build-utils', ...buildersToAdd], {
cwd: buildersDir,
});
output.log(
`Installing ${plural('Builder', buildersToAdd.size)}: ${Array.from(
buildersToAdd
).join(', ')}`
);
try {
await spawnAsync(
'npm',
['install', '@vercel/build-utils', ...buildersToAdd],
{
cwd: buildersDir,
stdio: 'pipe',
}
);
} catch (err: unknown) {
if (isError(err)) {
(err as any).link =
'https://vercel.link/builder-dependencies-install-failed';
if (isErrnoException(err) && err.code === 'ENOENT') {
// `npm` is not installed
err.message = `Please install ${cmd('npm')} before continuing`;
} else {
const message = errorToString(err);
const notFound = /GET (.*) - Not found/.exec(message);
if (notFound) {
const url = new URL(notFound[1]);
const packageName = decodeURIComponent(url.pathname.slice(1));
err.message = `The package ${code(
packageName
)} is not published on the npm registry`;
}
}
}
throw err;
}
// Symlink `@now/build-utils` -> `@vercel/build-utils` to support legacy Builders
const nowScopePath = join(buildersDir, 'node_modules/@now');
await mkdirp(nowScopePath);
await symlink('../@vercel/build-utils', join(nowScopePath, 'build-utils'));
try {
await symlink('../@vercel/build-utils', join(nowScopePath, 'build-utils'));
} catch (err: unknown) {
if (!isErrnoException(err) || err.code !== 'EEXIST') {
// Throw unless the error is due to the symlink already existing
throw err;
}
}
// Cross-reference any builderSpecs from the saved `package.json` file,
// in case they were installed from a URL

View File

@@ -37,20 +37,56 @@ describe('importBuilders()', () => {
const cwd = await getWriteableDirectory();
try {
const spec = 'vercel-deno@2.0.1';
const specs = new Set([spec]);
const tarballSpec = 'https://test2020-h5hdll5dz-tootallnate.vercel.app';
const specs = new Set([spec, tarballSpec]);
const builders = await importBuilders(specs, cwd, client.output);
expect(builders.size).toEqual(1);
expect(builders.size).toEqual(2);
expect(builders.get(spec)?.pkg.name).toEqual('vercel-deno');
expect(builders.get(spec)?.pkg.version).toEqual('2.0.1');
expect(builders.get(spec)?.pkgPath).toEqual(
join(cwd, '.vercel/builders/node_modules/vercel-deno/package.json')
);
expect(typeof builders.get(spec)?.builder.build).toEqual('function');
expect(builders.get(tarballSpec)?.pkg.name).toEqual('vercel-bash');
expect(builders.get(tarballSpec)?.pkg.version).toEqual('4.1.0');
expect(builders.get(tarballSpec)?.pkgPath).toEqual(
join(cwd, '.vercel/builders/node_modules/vercel-bash/package.json')
);
expect(typeof builders.get(tarballSpec)?.builder.build).toEqual(
'function'
);
await expect(client.stderr).toOutput(
'> Installing Builders: vercel-deno@2.0.1, https://test2020-h5hdll5dz-tootallnate.vercel.app'
);
} finally {
await remove(cwd);
}
});
it('should throw when importing a Builder that is not on npm registry', async () => {
let err: Error | undefined;
const cwd = await getWriteableDirectory();
try {
const spec = '@vercel/does-not-exist@0.0.1';
const specs = new Set([spec]);
await importBuilders(specs, cwd, client.output);
} catch (_err) {
err = _err;
} finally {
await remove(cwd);
}
if (!err) {
throw new Error('Expected `err` to be defined');
}
expect(err.message).toEqual(
'The package `@vercel/does-not-exist` is not published on the npm registry'
);
expect((err as any).link).toEqual(
'https://vercel.link/builder-dependencies-install-failed'
);
});
it('should import legacy `@now/build-utils` Builders', async () => {
if (process.platform === 'win32') {
// this test creates symlinks which require admin by default on Windows