mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-09 12:57:46 +00:00
[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:
@@ -342,6 +342,7 @@ export interface BuilderV2 {
|
||||
version: 2;
|
||||
build: BuildV2;
|
||||
prepareCache?: PrepareCache;
|
||||
shouldServe?: ShouldServe;
|
||||
}
|
||||
|
||||
export interface BuilderV3 {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user