import assert from 'assert' ; import fs from 'fs-extra' ; import path from 'path' ; import debug from '../debug' ; import spawn from 'cross-spawn' ; import { SpawnOptions } from 'child_process' ; import { deprecate } from 'util' ; import { NowBuildError } from '../errors' ; import { Meta , PackageJson , NodeVersion , Config } from '../types' ; import { getSupportedNodeVersion , getLatestNodeVersion } from './node-version' ; export type CliType = 'yarn' | 'npm' ; export interface ScanParentDirsResult { /**
     * "yarn" or "npm", depending on the presence of lockfiles.
     */ cliType : CliType ; /**
     * The contents of found `package.json` file, when the `readPackageJson`
     * option is enabled.
     */ packageJson ? : PackageJson ; /**
     * The `lockfileVersion` number from the `package-lock.json` file,
     * when present.
     */ lockfileVersion ? : number ; } export interface WalkParentDirsProps { /**
     * The highest directory, typically the workPath root of the project.
     * If this directory is reached and it doesn't contain the file, null is returned.
     */ base : string ; /**
     * The directory to start searching, typically the same directory of the entrypoint.
     * If this directory doesn't contain the file, the parent is checked, etc.
     */ start : string ; /**
     * The name of the file to search for, typically `package.json` or `Gemfile`.
     */ filename : string ; } export interface SpawnOptionsExtended extends SpawnOptions { /**
     * Pretty formatted command that is being spawned for logging purposes.
     */ prettyCommand ? : string ; } export function spawnAsync ( command , args , opts = { } ) { return new Promise < void > ( ( resolve , reject ) => { const stderrLogs = [ ] ; opts = { stdio : 'inherit' , ... opts } ; const child = spawn ( command , args , opts ) ; if ( opts . stdio === 'pipe' && child . stderr ) { child . stderr . on ( 'data' , data => stderrLogs . push ( data ) ) ; } child . on ( 'error' , reject ) ; child . on ( 'close' , ( code , signal ) => { if ( code === 0 ) { return resolve ( ) ; } const cmd = opts . prettyCommand ? `Command "${ opts . prettyCommand }"` : 'Command' ; reject ( new NowBuildError ( { code : `BUILD_UTILS_SPAWN_${ code || signal }` , message : opts . stdio === 'inherit' ? `${ cmd } exited with ${ code || signal }` : stderrLogs . map ( line => line . toString ( ) ) . join ( '' ) , } ) ) ; } ) ; } ) ; } export function execAsync ( command , args , opts = { } ) { return new Promise < { stdout : string ; stderr : string ; code : number ; } > ( ( resolve , reject ) => { opts . stdio = 'pipe' ; const stdoutList = [ ] ; const stderrList = [ ] ; const child = spawn ( command , args , opts ) ; child . stderr ! . on ( 'data' , data => { stderrList . push ( data ) ; } ) ; child . stdout ! . on ( 'data' , data => { stdoutList . push ( data ) ; } ) ; child . on ( 'error' , reject ) ; child . on ( 'close' , ( code , signal ) => { if ( code !== 0 ) { const cmd = opts . prettyCommand ? `Command "${ opts . prettyCommand }"` : 'Command' ; return reject ( new NowBuildError ( { code : `BUILD_UTILS_EXEC_${ code || signal }` , message : `${ cmd } exited with ${ code || signal }` , } ) ) ; } return resolve ( { code , stdout : Buffer . concat ( stdoutList ) . toString ( ) , stderr : Buffer . concat ( stderrList ) . toString ( ) , } ) ; } ) ; } ) ; } export function spawnCommand ( command , options = { } ) { const opts = { ... options , prettyCommand : command } ; if ( process . platform === 'win32' ) { return spawn ( 'cmd.exe' , [ '/C' , command ] , opts ) ; } return spawn ( 'sh' , [ '-c' , command ] , opts ) ; } export async function execCommand ( command , options = { } ) { const opts = { ... options , prettyCommand : command } ; if ( process . platform === 'win32' ) { await spawnAsync ( 'cmd.exe' , [ '/C' , command ] , opts ) ; } else { await spawnAsync ( 'sh' , [ '-c' , command ] , opts ) ; } return true ; } export async function getNodeBinPath ( { cwd } ) { const { stdout } = await execAsync ( 'npm' , [ 'bin' ] , { cwd } ) ; return stdout . trim ( ) ; } async function chmodPlusX ( fsPath ) { const s = await fs . stat ( fsPath ) ; const newMode = s . mode | 64 | 8 | 1 ; if ( s . mode === newMode ) return ; const base8 = newMode . toString ( 8 ) . slice ( - 3 ) ; await fs . chmod ( fsPath , base8 ) ; } export async function runShellScript ( fsPath , args = [ ] , spawnOpts ? ) { assert ( path . isAbsolute ( fsPath ) ) ; const destPath = path . dirname ( fsPath ) ; await chmodPlusX ( fsPath ) ; const command = `./${ path . basename ( fsPath ) }` ; await spawnAsync ( command , args , { ... spawnOpts , cwd : destPath , prettyCommand : command , } ) ; return true ; } export function getSpawnOptions ( meta , nodeVersion ) { const opts = { env : { ... process . env } , } ; if ( ! meta . isDev ) { opts . env . PATH = `/node${ nodeVersion . major }/bin:${ opts . env . PATH }` ; } return opts ; } export async function getNodeVersion ( destPath , _nodeVersion ? , config = { } , meta = { } ) { if ( meta && meta . isDev ) { const latest = getLatestNodeVersion ( ) ; return { ... latest , runtime : 'nodejs' } ; } const { packageJson } = await scanParentDirs ( destPath , true ) ; let { nodeVersion } = config ; let isAuto = true ; if ( packageJson && packageJson . engines && packageJson . engines . node ) { const { node } = packageJson . engines ; if ( nodeVersion && nodeVersion !== node && ! meta . isDev ) { console . warn ( `Warning: Due to "engines": { "node": "${ node }" } in your \`package.json\` file, the Node.js Version defined in your Project Settings ("${ nodeVersion }") will not apply. Learn More: http://vercel.link/node-version` ) ; } nodeVersion = node ; isAuto = false ; } return getSupportedNodeVersion ( nodeVersion , isAuto ) ; } export async function scanParentDirs ( destPath , readPackageJson = false ) { assert ( path . isAbsolute ( destPath ) ) ; let cliType = 'yarn' ; let packageJson ; let currentDestPath = destPath ; let lockfileVersion ; while ( true ) { const packageJsonPath = path . join ( currentDestPath , 'package.json' ) ; if ( await fs . pathExists ( packageJsonPath ) ) { if ( readPackageJson && ! packageJson ) { packageJson = JSON . parse ( await fs . readFile ( packageJsonPath , 'utf8' ) ) ; } const [ packageLockJson , hasYarnLock ] = await Promise . all ( [ fs . readJson ( path . join ( currentDestPath , 'package-lock.json' ) ) . catch ( error => { if ( error . code === 'ENOENT' ) { return null ; } throw error ; } ) , fs . pathExists ( path . join ( currentDestPath , 'yarn.lock' ) ) , ] ) ; if ( packageLockJson && ! hasYarnLock ) { cliType = 'npm' ; lockfileVersion = packageLockJson . lockfileVersion ; } if ( packageLockJson || hasYarnLock ) { break ; } } const newDestPath = path . dirname ( currentDestPath ) ; if ( currentDestPath === newDestPath ) break ; currentDestPath = newDestPath ; } return { cliType , packageJson , lockfileVersion } ; } export async function walkParentDirs ( { base , start , filename , } ) { assert ( path . isAbsolute ( base ) , 'Expected "base" to be absolute path' ) ; assert ( path . isAbsolute ( start ) , 'Expected "start" to be absolute path' ) ; let parent = '' ; for ( let current = start ; base . length <= current . length ; current = parent ) { const fullPath = path . join ( current , filename ) ; if ( await fs . pathExists ( fullPath ) ) { return fullPath ; } parent = path . dirname ( current ) ; } return null ; } export async function runNpmInstall ( destPath , args = [ ] , spawnOpts ? , meta ? , nodeVersion ? ) { if ( meta ?. isDev ) { debug ( 'Skipping dependency installation because dev mode is enabled' ) ; return ; } assert ( path . isAbsolute ( destPath ) ) ; debug ( `Installing to ${ destPath }` ) ; const { cliType , lockfileVersion } = await scanParentDirs ( destPath ) ; const opts = { cwd : destPath , ... spawnOpts } ; const env = opts . env ? { ... opts . env } : { ... process . env } ; delete env . NODE_ENV ; opts . env = env ; let commandArgs ; if ( cliType === 'npm' ) { opts . prettyCommand = 'npm install' ; commandArgs = args . filter ( a => a !== '--prefer-offline' ) . concat ( [ 'install' , '--no-audit' , '--unsafe-perm' ] ) ; if ( typeof lockfileVersion === 'number' && lockfileVersion >= 2 && ( nodeVersion ?. major || 0 ) < 16 ) { env . PATH = `/node16/bin-npm7:${ env . PATH }` ; console . log ( 'Detected `package-lock.json` generated by npm 7...' ) ; } } else { opts . prettyCommand = 'yarn install' ; commandArgs = [ 'install' , ... args ] ; if ( ! env . YARN_NODE_LINKER ) { env . YARN_NODE_LINKER = 'node-modules' ; } } if ( process . env . NPM_ONLY_PRODUCTION ) { commandArgs . push ( '--production' ) ; } return spawnAsync ( cliType , commandArgs , opts ) ; } export async function runPackageJsonScript ( destPath , scriptNames , spawnOpts ? ) { assert ( path . isAbsolute ( destPath ) ) ; const { packageJson , cliType , lockfileVersion } = await scanParentDirs ( destPath , true ) ; const scriptName = getScriptName ( packageJson , typeof scriptNames === 'string' ? [ scriptNames ] : scriptNames ) ; if ( ! scriptName ) return false ; debug ( 'Running user script...' ) ; const runScriptTime = Date . now ( ) ; const opts = { cwd : destPath , ... spawnOpts } ; const env = ( opts . env = { ... process . env , ... opts . env } ) ; if ( cliType === 'npm' ) { opts . prettyCommand = `npm run ${ scriptName }` ; if ( typeof lockfileVersion === 'number' && lockfileVersion >= 2 ) { env . PATH = `/node16/bin-npm7:${ env . PATH }` ; } } else { opts . prettyCommand = `yarn run ${ scriptName }` ; if ( ! env . YARN_NODE_LINKER ) { env . YARN_NODE_LINKER = 'node-modules' ; } } console . log ( `Running "${ opts . prettyCommand }"` ) ; await spawnAsync ( cliType , [ 'run' , scriptName ] , opts ) ; debug ( `Script complete [${ Date . now ( ) - runScriptTime }ms]` ) ; return true ; } export async function runBundleInstall ( destPath , args = [ ] , spawnOpts ? , meta ? ) { if ( meta && meta . isDev ) { debug ( 'Skipping dependency installation because dev mode is enabled' ) ; return ; } assert ( path . isAbsolute ( destPath ) ) ; const opts = { ... spawnOpts , cwd : destPath , prettyCommand : 'bundle install' } ; await spawnAsync ( 'bundle' , args . concat ( [ 'install' ] ) , opts ) ; } export async function runPipInstall ( destPath , args = [ ] , spawnOpts ? , meta ? ) { if ( meta && meta . isDev ) { debug ( 'Skipping dependency installation because dev mode is enabled' ) ; return ; } assert ( path . isAbsolute ( destPath ) ) ; const opts = { ... spawnOpts , cwd : destPath , prettyCommand : 'pip3 install' } ; await spawnAsync ( 'pip3' , [ 'install' , '--disable-pip-version-check' , ... args ] , opts ) ; } export function getScriptName ( pkg , possibleNames ) { if ( pkg ?. scripts ) { for ( const name of possibleNames ) { if ( name in pkg . scripts ) { return name ; } } } return null ; } deprecate export const installDependencies = deprecate ( runNpmInstall , 'installDependencies() is deprecated. Please use runNpmInstall() instead.' ) ;