[cli] Use Builder importing logic from vc build in vc dev (#8485)

This makes `vc dev` utilize the same Builder installing and importing logic that `vc build` uses. So now the Builders will be installed locally on a per-project basis, rather than being installed to a shared global installation directory.
This commit is contained in:
Nathan Rajlich
2022-09-13 14:35:01 -07:00
committed by GitHub
parent f3f3d7df5b
commit cc7577a648
9 changed files with 57 additions and 797 deletions

View File

@@ -342,6 +342,7 @@ export interface BuilderV2 {
version: 2;
build: BuildV2;
prepareCache?: PrepareCache;
shouldServe?: ShouldServe;
}
export interface BuilderV3 {

View File

@@ -1,5 +1,6 @@
import minimatch from 'minimatch';
import { BuildV2, Files } from '@vercel/build-utils';
import { shouldServe as defaultShouldServe } from '@vercel/build-utils';
import type { BuildV2, Files, ShouldServe } from '@vercel/build-utils';
export const version = 2;
@@ -39,3 +40,18 @@ export const build: BuildV2 = async ({ entrypoint, files, config }) => {
return { output };
};
export const shouldServe: ShouldServe = _opts => {
const opts = { ..._opts };
const {
config: { zeroConfig, outputDirectory },
} = opts;
// Add the output directory prefix
if (zeroConfig && outputDirectory) {
opts.entrypoint = `${outputDirectory}/${opts.entrypoint}`;
opts.requestPath = `${outputDirectory}/${opts.requestPath}`;
}
return defaultShouldServe(opts);
};

View File

@@ -1,412 +0,0 @@
import chalk from 'chalk';
import execa from 'execa';
import semver from 'semver';
import npa from 'npm-package-arg';
import pluralize from 'pluralize';
import { basename, join } from 'path';
import XDGAppPaths from 'xdg-app-paths';
import { mkdirp, readJSON, writeJSON } from 'fs-extra';
import { NowBuildError, PackageJson } from '@vercel/build-utils';
import cliPkg from '../pkg';
import cmd from '../output/cmd';
import { Output } from '../output';
import { NoBuilderCacheError } from '../errors-ts';
import * as staticBuilder from './static-builder';
import { BuilderWithPackage } from './types';
import { isErrnoException } from '../is-error';
const require_: typeof require = eval('require');
const registryTypes = new Set(['version', 'tag', 'range']);
const createStaticBuilder = (scope: string): BuilderWithPackage => {
return {
runInProcess: true,
requirePath: `${scope}/static`,
builder: Object.freeze(staticBuilder),
package: Object.freeze({ name: `@${scope}/static`, version: '' }),
};
};
const localBuilders: { [key: string]: BuilderWithPackage } = {
'@now/static': createStaticBuilder('now'),
'@vercel/static': createStaticBuilder('vercel'),
};
export const cacheDirPromise = prepareCacheDir();
export const builderDirPromise = prepareBuilderDir();
/**
* Prepare cache directory for installing Vercel runtimes.
*/
export async function prepareCacheDir() {
const designated = XDGAppPaths('com.vercel.cli').cache();
if (!designated) {
throw new NoBuilderCacheError();
}
const cacheDir = join(designated, 'dev');
await mkdirp(cacheDir);
return cacheDir;
}
export async function prepareBuilderDir() {
const builderDir = join(await cacheDirPromise, 'builders');
await mkdirp(builderDir);
// Create an empty `package.json` file, only if one does not already exist
try {
const buildersPkg = join(builderDir, 'package.json');
await writeJSON(buildersPkg, { private: true }, { flag: 'wx' });
} catch (err: unknown) {
if (!isErrnoException(err) || err.code !== 'EEXIST') {
throw err;
}
}
return builderDir;
}
function getNpmVersion(use = ''): string {
const parsed = npa(use);
if (registryTypes.has(parsed.type)) {
return parsed.fetchSpec || '';
}
return '';
}
export function getBuildUtils(packages: string[]): string {
const version = packages
.map(getNpmVersion)
.some(ver => ver.includes('canary'))
? 'canary'
: 'latest';
return `@vercel/build-utils@${version}`;
}
function parseVersionSafe(rawSpec: string) {
try {
return semver.parse(rawSpec);
} catch (e) {
return null;
}
}
export function filterPackage(
builderSpec: string,
buildersPkg: PackageJson,
cliPkg: Partial<PackageJson>
) {
if (builderSpec in localBuilders) return false;
const parsed = npa(builderSpec);
const parsedVersion = parseVersionSafe(parsed.rawSpec);
// Skip install of Runtimes that are part of Vercel CLI's `dependencies`
if (isBundledBuilder(parsed, cliPkg)) {
return false;
}
// Skip install of already installed Runtime with exact version match
if (
parsed.name &&
parsed.type === 'version' &&
parsedVersion &&
buildersPkg.dependencies &&
parsedVersion.version == buildersPkg.dependencies[parsed.name]
) {
return false;
}
return true;
}
/**
* Install a list of builders to the cache directory.
*/
export async function installBuilders(
packagesSet: Set<string>,
output: Output,
builderDir?: string
): Promise<void> {
const packages = Array.from(packagesSet);
if (
packages.length === 0 ||
(packages.length === 1 &&
Object.hasOwnProperty.call(localBuilders, packages[0]))
) {
// Static deployment, no builders to install
return;
}
if (!builderDir) {
builderDir = await builderDirPromise;
}
const buildersPkgPath = join(builderDir, 'package.json');
const buildersPkgBefore = await readJSON(buildersPkgPath);
const depsBefore = {
...buildersPkgBefore.devDependencies,
...buildersPkgBefore.dependencies,
};
// Filter out any packages that come packaged with Vercel CLI
const packagesToInstall = packages.filter(p =>
filterPackage(p, buildersPkgBefore, cliPkg)
);
if (packagesToInstall.length === 0) {
output.debug('No Runtimes need to be installed');
return;
}
packagesToInstall.push(getBuildUtils(packages));
await npmInstall(builderDir, output, packagesToInstall, false);
const updatedPackages: string[] = [];
const buildersPkgAfter = await readJSON(buildersPkgPath);
const depsAfter = {
...buildersPkgAfter.devDependencies,
...buildersPkgAfter.dependencies,
};
for (const [name, version] of Object.entries(depsAfter)) {
if (version !== depsBefore[name]) {
output.debug(`Runtime "${name}" updated to version \`${version}\``);
updatedPackages.push(name);
}
}
purgeRequireCache(updatedPackages, builderDir, output);
}
async function npmInstall(
cwd: string,
output: Output,
packagesToInstall: string[],
silent: boolean
) {
const sortedPackages = packagesToInstall.sort();
if (!silent) {
output.spinner(
`Installing ${pluralize(
'Runtime',
sortedPackages.length
)}: ${sortedPackages.join(', ')}`
);
}
output.debug(`Running npm install in ${cwd}`);
try {
const args = [
'install',
'--save-exact',
'--no-package-lock',
'--no-audit',
'--no-progress',
];
if (process.stderr.isTTY) {
// Force colors in the npm child process
// https://docs.npmjs.com/misc/config#color
args.push('--color=always');
}
args.push(...sortedPackages);
const result = await execa('npm', args, {
cwd,
reject: false,
stdio: output.isDebugEnabled() ? 'inherit' : 'pipe',
});
if (result.failed) {
output.stopSpinner();
if (result.stdout) {
console.log(result.stdout);
}
if (result.stderr) {
console.error(result.stderr);
}
throw new NowBuildError({
message:
(result as any).code === 'ENOENT'
? `Command not found: ${chalk.cyan(
'npm'
)}\nPlease ensure that ${cmd('npm')} is properly installed`
: 'Failed to install `vercel dev` dependencies',
code: 'NPM_INSTALL_ERROR',
link: 'https://vercel.link/npm-install-failed-dev',
});
}
} finally {
output.stopSpinner();
}
}
export async function updateBuilders(
packagesSet: Set<string>,
output: Output,
builderDir?: string
): Promise<string[]> {
if (!builderDir) {
builderDir = await builderDirPromise;
}
const updatedPackages: string[] = [];
const packages = Array.from(packagesSet);
const buildersPkgPath = join(builderDir, 'package.json');
const buildersPkgBefore = await readJSON(buildersPkgPath);
const depsBefore = {
...buildersPkgBefore.devDependencies,
...buildersPkgBefore.dependencies,
};
const packagesToUpdate = packages.filter(p => {
if (p in localBuilders) return false;
// If it's a builder that is part of Vercel CLI's
// `dependencies` then don't update it
if (isBundledBuilder(npa(p), cliPkg)) {
return false;
}
return true;
});
if (packagesToUpdate.length > 0) {
packagesToUpdate.push(getBuildUtils(packages));
await npmInstall(builderDir, output, packagesToUpdate, true);
const buildersPkgAfter = await readJSON(buildersPkgPath);
const depsAfter = {
...buildersPkgAfter.devDependencies,
...buildersPkgAfter.dependencies,
};
for (const [name, version] of Object.entries(depsAfter)) {
if (version !== depsBefore[name]) {
output.debug(`Runtime "${name}" updated to version \`${version}\``);
updatedPackages.push(name);
}
}
purgeRequireCache(updatedPackages, builderDir, output);
}
return updatedPackages;
}
/**
* Get a builder from the cache directory.
*/
export async function getBuilder(
builderPkg: string,
output: Output,
builderDir?: string,
isRetry = false
): Promise<BuilderWithPackage> {
let builderWithPkg: BuilderWithPackage = localBuilders[builderPkg];
if (!builderWithPkg) {
if (!builderDir) {
builderDir = await builderDirPromise;
}
let requirePath: string;
const parsed = npa(builderPkg);
// First check if it's a bundled Runtime in Vercel CLI's `node_modules`
const bundledBuilder = isBundledBuilder(parsed, cliPkg);
if (bundledBuilder && parsed.name) {
requirePath = parsed.name;
} else {
const buildersPkg = await readJSON(join(builderDir, 'package.json'));
const pkgName = getPackageName(parsed, buildersPkg) || builderPkg;
requirePath = join(builderDir, 'node_modules', pkgName);
}
try {
output.debug(`Requiring runtime: "${requirePath}"`);
const mod = require_(requirePath);
const pkg = require_(join(requirePath, 'package.json'));
builderWithPkg = {
requirePath,
builder: Object.freeze(mod),
package: Object.freeze(pkg),
};
} catch (err: unknown) {
if (
isErrnoException(err) &&
err.code === 'MODULE_NOT_FOUND' &&
!isRetry
) {
output.debug(
`Attempted to require ${requirePath}, but it is not installed`
);
const pkgSet = new Set([builderPkg]);
await installBuilders(pkgSet, output, builderDir);
// Run `getBuilder()` again now that the builder has been installed
return getBuilder(builderPkg, output, builderDir, true);
}
throw err;
}
// If it's a bundled builder, then cache the require call
if (bundledBuilder) {
localBuilders[builderPkg] = builderWithPkg;
}
}
return builderWithPkg;
}
export function isBundledBuilder(
parsed: npa.Result,
{ dependencies = {} }: PackageJson
): boolean {
if (!parsed.name) {
return false;
}
const inCliDependencyList = !!dependencies[parsed.name];
const inScope = parsed.scope === '@vercel';
const isVersionedReference = ['tag', 'version', 'range'].includes(
parsed.type
);
return inCliDependencyList && inScope && isVersionedReference;
}
function getPackageName(
parsed: npa.Result,
buildersPkg: PackageJson
): string | null {
if (registryTypes.has(parsed.type)) {
return parsed.name;
}
const deps: PackageJson.DependencyMap = {
...buildersPkg.devDependencies,
...buildersPkg.dependencies,
};
for (const [name, dep] of Object.entries(deps)) {
if (dep === parsed.raw || basename(dep) === basename(parsed.raw)) {
return name;
}
}
return null;
}
function purgeRequireCache(
packages: string[],
builderDir: string,
output: Output
) {
// The `require()` cache for the builder's assets must be purged
const packagesPaths = packages.map(b => join(builderDir, 'node_modules', b));
for (const id of Object.keys(require_.cache)) {
for (const path of packagesPaths) {
if (id.startsWith(path)) {
output.debug(`Purging require cache for "${id}"`);
delete require_.cache[id];
}
}
}
}

View File

@@ -15,7 +15,7 @@ import {
FileFsRef,
normalizePath,
} from '@vercel/build-utils';
import { isOfficialRuntime } from '@vercel/fs-detectors';
import { isStaticRuntime } from '@vercel/fs-detectors';
import plural from 'pluralize';
import minimatch from 'minimatch';
@@ -26,7 +26,6 @@ import { relative } from '../path-helpers';
import { LambdaSizeExceededError } from '../errors-ts';
import DevServer from './server';
import { getBuilder } from './builder-cache';
import {
VercelConfig,
BuildMatch,
@@ -41,6 +40,7 @@ import {
import { normalizeRoutes } from '@vercel/routing-utils';
import getUpdateCommand from '../get-update-command';
import { getTitleName } from '../pkg-name';
import { importBuilders } from '../build/import-builders';
interface BuildMessage {
type: string;
@@ -108,18 +108,18 @@ export async function executeBuild(
filesRemoved?: string[]
): Promise<void> {
const {
builderWithPkg: { runInProcess, requirePath, builder, package: pkg },
builderWithPkg: { path: requirePath, builder, pkg },
} = match;
const { entrypoint } = match;
const { entrypoint, use } = match;
const isStatic = isStaticRuntime(use);
const { envConfigs, cwd: workPath, devCacheDir } = devServer;
const debug = devServer.output.isDebugEnabled();
const startTime = Date.now();
const showBuildTimestamp =
!isOfficialRuntime('static', match.use) && (!isInitialBuild || debug);
const showBuildTimestamp = !isStatic && (!isInitialBuild || debug);
if (showBuildTimestamp) {
devServer.output.log(`Building ${match.use}:${entrypoint}`);
devServer.output.log(`Building ${use}:${entrypoint}`);
devServer.output.debug(
`Using \`${pkg.name}${pkg.version ? `@${pkg.version}` : ''}\``
);
@@ -130,7 +130,7 @@ export async function executeBuild(
let result: BuildResult;
let { buildProcess } = match;
if (!runInProcess && !buildProcess) {
if (!isStatic && !buildProcess) {
buildProcess = await createBuildProcess(
match,
envConfigs,
@@ -158,7 +158,7 @@ export async function executeBuild(
},
};
let buildResultOrOutputs: BuilderOutputs | BuildResult | BuildResultV3;
let buildResultOrOutputs;
if (buildProcess) {
buildProcess.send({
type: 'build',
@@ -198,16 +198,12 @@ export async function executeBuild(
}
// Sort out build result to builder v2 shape
if (!builder.version || builder.version === 1) {
if (!builder.version || (builder as any).version === 1) {
// `BuilderOutputs` map was returned (Now Builder v1 behavior)
result = {
output: buildResultOrOutputs as BuilderOutputs,
routes: [],
watch: [],
distPath:
typeof buildResultOrOutputs.distPath === 'string'
? buildResultOrOutputs.distPath
: undefined,
};
} else if (builder.version === 2) {
result = buildResultOrOutputs as BuildResult;
@@ -253,7 +249,7 @@ export async function executeBuild(
} else {
throw new Error(
`${getTitleName()} CLI does not support builder version ${
builder.version
(builder as any).version
}.\nPlease run \`${await getUpdateCommand()}\` to update to the latest CLI.`
);
}
@@ -383,7 +379,7 @@ export async function executeBuild(
if (showBuildTimestamp) {
const endTime = Date.now();
devServer.output.log(
`Built ${match.use}:${entrypoint} [${ms(endTime - startTime)}]`
`Built ${use}:${entrypoint} [${ms(endTime - startTime)}]`
);
}
}
@@ -405,6 +401,8 @@ export async function getBuildMatches(
const noMatches: Builder[] = [];
const builds = vercelConfig.builds || [{ src: '**', use: '@vercel/static' }];
const builderSpecs = new Set(builds.map(b => b.use).filter(Boolean));
const buildersWithPkgs = await importBuilders(builderSpecs, cwd, output);
for (const buildConfig of builds) {
let { src = '**', use, config = {} } = buildConfig;
@@ -439,6 +437,8 @@ export async function getBuildMatches(
for (const file of files) {
src = relative(cwd, file);
const entrypoint = mapToEntrypoint.get(src) || src;
// Remove the output directory prefix
if (config.zeroConfig && config.outputDirectory) {
const outputMatch = config.outputDirectory + '/';
@@ -447,11 +447,15 @@ export async function getBuildMatches(
}
}
const builderWithPkg = await getBuilder(use, output);
const builderWithPkg = buildersWithPkgs.get(use);
if (!builderWithPkg) {
throw new Error(`Failed to load Builder "${use}"`);
}
matches.push({
...buildConfig,
src,
entrypoint: mapToEntrypoint.get(src) || src,
entrypoint,
builderWithPkg,
buildOutput: {},
buildResults: new Map(),

View File

@@ -1,4 +1,3 @@
import ms from 'ms';
import url, { URL } from 'url';
import http from 'http';
import fs from 'fs-extra';
@@ -62,11 +61,6 @@ import { devRouter, getRoutesTypes } from './router';
import getMimeType from './mime-type';
import { executeBuild, getBuildMatches, shutdownBuilder } from './builder';
import { generateErrorMessage, generateHttpStatusDescription } from './errors';
import {
installBuilders,
updateBuilders,
builderDirPromise,
} from './builder-cache';
// HTML templates
import errorTemplate from './templates/error';
@@ -171,8 +165,6 @@ export default class DevServer {
private vercelConfigWarning: boolean;
private getVercelConfigPromise: Promise<VercelConfig> | null;
private blockingBuildsPromise: Promise<void> | null;
private updateBuildersPromise: Promise<void> | null;
private updateBuildersTimeout: NodeJS.Timeout | undefined;
private startPromise: Promise<void> | null;
private systemEnvValues: string[];
@@ -211,7 +203,6 @@ export default class DevServer {
this.vercelConfigWarning = false;
this.getVercelConfigPromise = null;
this.blockingBuildsPromise = null;
this.updateBuildersPromise = null;
this.startPromise = null;
this.watchAggregationId = null;
@@ -493,33 +484,6 @@ export default class DevServer {
);
}
async invalidateBuildMatches(
vercelConfig: VercelConfig,
updatedBuilders: string[]
): Promise<void> {
if (updatedBuilders.length === 0) {
this.output.debug('No builders were updated');
return;
}
// Delete any build matches that have the old builder required already
for (const buildMatch of this.buildMatches.values()) {
const {
src,
builderWithPkg: { package: pkg },
} = buildMatch;
if (isOfficialRuntime('static', pkg.name)) continue;
if (pkg.name && updatedBuilders.includes(pkg.name)) {
shutdownBuilder(buildMatch, this.output);
this.buildMatches.delete(src);
this.output.debug(`Invalidated build match for "${src}"`);
}
}
// Re-add the build matches that were just removed, but with the new builder
await this.updateBuildMatches(vercelConfig);
}
async getLocalEnv(fileName: string, base?: Env): Promise<Env> {
// TODO: use the file watcher to only invalidate the env `dotfile`
// once a change to the `fileName` occurs
@@ -948,30 +912,8 @@ export default class DevServer {
}
}
const builders = new Set<string>(
(vercelConfig.builds || [])
.filter((b: Builder) => b.use)
.map((b: Builder) => b.use)
);
await installBuilders(builders, this.output);
await this.updateBuildMatches(vercelConfig, true);
// Updating builders happens lazily, and any builders that were updated
// get their "build matches" invalidated so that the new version is used.
this.updateBuildersTimeout = setTimeout(() => {
this.updateBuildersPromise = updateBuilders(builders, this.output)
.then(updatedBuilders => {
this.updateBuildersPromise = null;
this.invalidateBuildMatches(vercelConfig, updatedBuilders);
})
.catch(err => {
this.updateBuildersPromise = null;
this.output.prettyError(err);
this.output.debug(err.stack);
});
}, ms('30s'));
// Builders that do not define a `shouldServe()` function need to be
// executed at boot-up time in order to get the initial assets and/or routes
// that can be served by the builder.
@@ -1047,16 +989,11 @@ export default class DevServer {
* Shuts down the `vercel dev` server, and cleans up any temporary resources.
*/
async stop(exitCode?: number): Promise<void> {
const { devProcess } = this;
const { debug } = this.output;
if (this.stopping) return;
this.stopping = true;
if (typeof this.updateBuildersTimeout !== 'undefined') {
clearTimeout(this.updateBuildersTimeout);
}
const { devProcess } = this;
const { debug } = this.output;
const ops: Promise<any>[] = [];
for (const match of this.buildMatches.values()) {
@@ -1074,18 +1011,10 @@ export default class DevServer {
ops.push(this.watcher.close());
}
if (this.updateBuildersPromise) {
debug(`Waiting for builders update to complete`);
ops.push(this.updateBuildersPromise);
}
for (const pid of this.devServerPids) {
ops.push(this.killBuilderDevServer(pid));
}
// Ensure that the builders module cache is created
ops.push(builderDirPromise);
try {
await Promise.all(ops);
} catch (err: unknown) {
@@ -1484,8 +1413,9 @@ export default class DevServer {
// the middleware server for every HTTP request?
const { envConfigs, files, devCacheDir, cwd: workPath } = this;
try {
startMiddlewareResult =
await middleware.builderWithPkg.builder.startDevServer?.({
const { builder } = middleware.builderWithPkg;
if (builder.version === 3) {
startMiddlewareResult = await builder.startDevServer?.({
files,
entrypoint: middleware.entrypoint,
workPath,
@@ -1499,6 +1429,7 @@ export default class DevServer {
buildEnv: { ...envConfigs.buildEnv },
},
});
}
if (startMiddlewareResult) {
const { port, pid } = startMiddlewareResult;
@@ -1911,8 +1842,8 @@ export default class DevServer {
// up a single-serve dev HTTP server that vercel dev will proxy this HTTP request
// to. Once the proxied request is finished, vercel dev shuts down the dev
// server child process.
const { builder, package: builderPkg } = match.builderWithPkg;
if (typeof builder.startDevServer === 'function') {
const { builder, pkg: builderPkg } = match.builderWithPkg;
if (builder.version === 3 && typeof builder.startDevServer === 'function') {
let devServerResult: StartDevServerResult = null;
try {
const { envConfigs, files, devCacheDir, cwd: workPath } = this;

View File

@@ -1,42 +0,0 @@
import {
FileFsRef,
BuildOptions,
shouldServe as defaultShouldServe,
ShouldServeOptions,
} from '@vercel/build-utils';
import { BuildResult } from './types';
export const version = 2;
export function build({
files,
entrypoint,
config: { zeroConfig, outputDirectory },
}: BuildOptions): BuildResult {
const path =
zeroConfig && outputDirectory
? `${outputDirectory}/${entrypoint}`
: entrypoint;
return {
output: {
[entrypoint]: files[path] as FileFsRef,
},
routes: [],
watch: [path],
};
}
export function shouldServe(_opts: ShouldServeOptions) {
const opts = { ..._opts };
let {
config: { zeroConfig, outputDirectory },
} = opts;
// Add the output directory prefix
if (zeroConfig && outputDirectory) {
opts.entrypoint = `${outputDirectory}/${opts.entrypoint}`;
opts.requestPath = `${outputDirectory}/${opts.requestPath}`;
}
return defaultShouldServe(opts);
}

View File

@@ -12,12 +12,12 @@ import {
FileBlob,
FileFsRef,
Lambda,
PackageJson,
} from '@vercel/build-utils';
import { VercelConfig } from '@vercel/client';
import { HandleValue, Route } from '@vercel/routing-utils';
import { Output } from '../output';
import { ProjectEnvVariable, ProjectSettings } from '../../types';
import { BuilderWithPkg } from '../build/import-builders';
export { VercelConfig };
@@ -48,7 +48,7 @@ export interface EnvConfigs {
export interface BuildMatch extends BuildConfig {
entrypoint: string;
src: string;
builderWithPkg: BuilderWithPackage;
builderWithPkg: BuilderWithPkg;
buildOutput: BuilderOutputs;
buildResults: Map<string | null, BuildResult>;
buildTimestamp: number;
@@ -122,13 +122,6 @@ export interface BuildResultV4 {
distPath?: string;
}
export interface BuilderWithPackage {
runInProcess?: boolean;
requirePath: string;
builder: Readonly<Builder>;
package: Readonly<PackageJson>;
}
export interface HttpHeadersConfig {
[name: string]: string;
}

View File

@@ -3,6 +3,7 @@ import url from 'url';
import fs from 'fs-extra';
import { join } from 'path';
import listen from 'async-listen';
import stripAnsi from 'strip-ansi';
import { createServer } from 'http';
const {
@@ -723,12 +724,15 @@ test('[vercel dev] should support custom 404 routes', async () => {
test('[vercel dev] prints `npm install` errors', async () => {
const dir = fixture('runtime-not-installed');
const result = await exec(dir);
expect(result.stderr.includes('npm ERR! 404')).toBeTruthy();
expect(
result.stderr.includes('Failed to install `vercel dev` dependencies')
stripAnsi(result.stderr).includes(
'Error: The package `@vercel/does-not-exist` is not published on the npm registry'
)
).toBeTruthy();
expect(
result.stderr.includes('https://vercel.link/npm-install-failed-dev')
result.stderr.includes(
'https://vercel.link/builder-dependencies-install-failed'
)
).toBeTruthy();
});

View File

@@ -1,235 +0,0 @@
import npa from 'npm-package-arg';
import {
filterPackage,
getBuildUtils,
isBundledBuilder,
} from '../../../../src/util/dev/builder-cache';
describe('filterPackage', () => {
const cliPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1',
},
};
it('should filter package that does not appear in CLI package.json', () => {
const result = filterPackage('@vercel/other', {}, cliPkg);
expect(result).toEqual(true);
});
it('should not filter "latest", cached canary', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage('@vercel/build-utils', buildersPkg, cliPkg);
expect(result).toEqual(false);
});
it('should filter install "canary", cached stable', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1',
},
};
const result = filterPackage(
'@vercel/build-utils@canary',
buildersPkg,
cliPkg
);
expect(result).toEqual(false);
});
it('should filter install "latest", cached stable', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1',
},
};
const result = filterPackage('@vercel/build-utils', buildersPkg, cliPkg);
expect(result).toEqual(false);
});
it('should filter install "canary", cached canary', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage(
'@vercel/build-utils@canary',
buildersPkg,
cliPkg
);
expect(result).toEqual(false);
});
it('should filter install URL, cached stable', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1',
},
};
const result = filterPackage('https://tarball.now.sh', buildersPkg, cliPkg);
expect(result).toEqual(true);
});
it('should filter install URL, cached canary', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage('https://tarball.now.sh', buildersPkg, cliPkg);
expect(result).toEqual(true);
});
it('should filter install "latest", cached URL - canary', () => {
const buildersPkg = {
dependencies: {
'@vercel/build-utils': 'https://tarball.now.sh',
},
};
const result = filterPackage('@vercel/build-utils', buildersPkg, cliPkg);
expect(result).toEqual(false);
});
it('should filter install not bundled version, cached same version', () => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.1',
},
};
const result = filterPackage(
'not-bundled-package@0.0.1',
buildersPkg,
cliPkg
);
expect(result).toEqual(false);
});
it('should filter install not bundled version, cached different version', () => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.9',
},
};
const result = filterPackage(
'not-bundled-package@0.0.1',
buildersPkg,
cliPkg
);
expect(result).toEqual(true);
});
it('should filter install not bundled stable, cached version', () => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.1',
},
};
const result = filterPackage('not-bundled-package', buildersPkg, cliPkg);
expect(result).toEqual(true);
});
it('should filter install not bundled tagged, cached tagged', () => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '16.9.0-alpha.0',
},
};
const result = filterPackage(
'not-bundled-package@alpha',
buildersPkg,
cliPkg
);
expect(result).toEqual(true);
});
});
describe('getBuildUtils', () => {
const tests: [string[], string][] = [
[['@vercel/static', '@vercel/node@canary'], 'canary'],
[['@vercel/static', '@vercel/node@0.7.4-canary.0'], 'canary'],
[['@vercel/static', '@vercel/node@0.8.0'], 'latest'],
[['@vercel/static', '@vercel/node'], 'latest'],
[['@vercel/static'], 'latest'],
[['@vercel/md@canary'], 'canary'],
[['custom-builder'], 'latest'],
[['custom-builder@canary'], 'canary'],
[['canary-bird'], 'latest'],
[['canary-bird@4.0.0'], 'latest'],
[['canary-bird@canary'], 'canary'],
[['@canary/bird'], 'latest'],
[['@canary/bird@0.1.0'], 'latest'],
[['@canary/bird@canary'], 'canary'],
[['https://example.com'], 'latest'],
[[''], 'latest'],
];
for (const [input, expected] of tests) {
it(`should install "${expected}" with input ${JSON.stringify(
input
)}`, () => {
const result = getBuildUtils(input);
expect(result).toEqual(`@vercel/build-utils@${expected}`);
});
}
});
describe('isBundledBuilder', () => {
const cliPkg = {
dependencies: {
'@vercel/node': '0.0.1',
},
};
it('should not detect when dependency does not appear in CLI package.json', () => {
const parsed = npa('@vercel/node');
const result = isBundledBuilder(parsed, {});
expect(result).toEqual(false);
});
it('should detect "canary" tagged releases', () => {
const parsed = npa('@vercel/node@canary');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(true);
});
it('should detect "canary" versioned releases', () => {
const parsed = npa('@vercel/node@1.6.1-canary.0');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(true);
});
it('should detect latest releases', () => {
const parsed = npa('@vercel/node');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(true);
});
it('should detect "latest" tagged releases', () => {
const parsed = npa('@vercel/node@latest');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(true);
});
it('should detect versioned releases', () => {
const parsed = npa('@vercel/node@1.6.1');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(true);
});
it('should NOT detect URL releases', () => {
const parsed = npa('https://example.com');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(false);
});
it('should NOT detect git url releases', () => {
const parsed = npa('git://example.com/repo.git');
const result = isBundledBuilder(parsed, cliPkg);
expect(result).toEqual(false);
});
});