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 { getDistTag } from '../get-dist-tag' ; import { NoBuilderCacheError } from '../errors-ts' ; import * as staticBuilder from './static-builder' ; import { BuilderWithPackage } from './types' ; type CliPackageJson = typeof cliPkg ; const require_ = eval ( 'require' ) ; const registryTypes = new Set ( [ 'version' , 'tag' , 'range' ] ) ; const createStaticBuilder = ( scope ) : BuilderWithPackage => { return { runInProcess : true , requirePath : `${ scope }/static` , builder : Object . freeze ( staticBuilder ) , package : Object . freeze ( { name : `@${ scope }/static` , version : '' } ) , } ; } ; const localBuilders = { '@now/static' : createStaticBuilder ( 'now' ) , '@vercel/static' : createStaticBuilder ( 'vercel' ) , } ; const distTag = getDistTag ( cliPkg . version ) ; 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 ) ; try { const buildersPkg = join ( builderDir , 'package.json' ) ; await writeJSON ( buildersPkg , { private : true } , { flag : 'wx' } ) ; } catch ( err ) { if ( err . code !== 'EEXIST' ) { throw err ; } } return builderDir ; } function getNpmVersion ( use = '' ) { const parsed = npa ( use ) ; if ( registryTypes . has ( parsed . type ) ) { return parsed . fetchSpec || '' ; } return '' ; } export function getBuildUtils ( packages ) { const version = packages . map ( getNpmVersion ) . some ( ver => ver . includes ( 'canary' ) ) ? 'canary' : 'latest' ; return `@vercel/build-utils@${ version }` ; } function parseVersionSafe ( rawSpec ) { try { return semver . parse ( rawSpec ) ; } catch ( e ) { return null ; } } export function filterPackage ( builderSpec , distTag , buildersPkg , cliPkg ) { if ( builderSpec in localBuilders ) return false ; const parsed = npa ( builderSpec ) ; const parsedVersion = parseVersionSafe ( parsed . rawSpec ) ; if ( isBundledBuilder ( parsed , cliPkg ) ) { return false ; } if ( parsed . name && parsed . type === 'version' && parsedVersion && buildersPkg . dependencies && parsedVersion . version == buildersPkg . dependencies [ parsed . name ] ) { return false ; } if ( parsed . name && parsed . type === 'tag' && parsed . fetchSpec === distTag && buildersPkg . dependencies ) { const parsedInstalled = npa ( `${ parsed . name }@${ buildersPkg . dependencies [ parsed . name ] }` ) ; if ( parsedInstalled . type !== 'version' ) { return true ; } const semverInstalled = semver . parse ( parsedInstalled . rawSpec ) ; if ( ! semverInstalled ) { return true ; } if ( semverInstalled . prerelease . length > 0 ) { return semverInstalled . prerelease [ 0 ] !== distTag ; } if ( distTag === 'latest' ) { return false ; } } return true ; } /**
 * Install a list of builders to the cache directory.
 */ export async function installBuilders ( packagesSet , output , builderDir ? ) { const packages = Array . from ( packagesSet ) ; if ( packages . length === 0 || ( packages . length === 1 && Object . hasOwnProperty . call ( localBuilders , packages [ 0 ] ) ) ) { return ; } if ( ! builderDir ) { builderDir = await builderDirPromise ; } const buildersPkgPath = join ( builderDir , 'package.json' ) ; const buildersPkgBefore = await readJSON ( buildersPkgPath ) ; const depsBefore = { ... buildersPkgBefore . devDependencies , ... buildersPkgBefore . dependencies , } ; const packagesToInstall = packages . filter ( p => filterPackage ( p , distTag , 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 = [ ] ; 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 , output , packagesToInstall , silent ) { 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 ) { 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 , output , builderDir ? ) { if ( ! builderDir ) { builderDir = await builderDirPromise ; } const updatedPackages = [ ] ; 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 ( 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 , output , builderDir ? , isRetry = false ) { let builderWithPkg = localBuilders [ builderPkg ] ; if ( ! builderWithPkg ) { if ( ! builderDir ) { builderDir = await builderDirPromise ; } let requirePath ; const parsed = npa ( builderPkg ) ; 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 ) { if ( 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 ) ; return getBuilder ( builderPkg , output , builderDir , true ) ; } throw err ; } if ( bundledBuilder ) { localBuilders [ builderPkg ] = builderWithPkg ; } } return builderWithPkg ; } export function isBundledBuilder ( parsed , { dependencies = { } } ) { if ( ! parsed . name ) { return false ; } const bundledVersion = dependencies [ parsed . name ] ; if ( bundledVersion ) { if ( parsed . type === 'tag' ) { if ( parsed . fetchSpec === 'canary' ) { return bundledVersion . includes ( 'canary' ) ; } else if ( parsed . fetchSpec === 'latest' ) { return ! bundledVersion . includes ( 'canary' ) ; } } else if ( parsed . type === 'version' ) { return parsed . fetchSpec === bundledVersion ; } } return false ; } function getPackageName ( parsed , buildersPkg ) { if ( registryTypes . has ( parsed . type ) ) { return parsed . name ; } const deps = { ... 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 , builderDir , output ) { 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 ] ; } } } }