import ms from 'ms' ; import bytes from 'bytes' ; import { delimiter , dirname , join } from 'path' ; import { fork , ChildProcess } from 'child_process' ; import { createFunction } from '@zeit/fun' ; import { Builder , BuildOptions , Env , File , Lambda , FileBlob , FileFsRef , isOfficialRuntime , } from '@vercel/build-utils' ; import plural from 'pluralize' ; import minimatch from 'minimatch' ; import { Output } from '../output' ; import highlight from '../output/highlight' ; import { treeKill } from '../tree-kill' ; import { relative } from '../path-helpers' ; import { LambdaSizeExceededError } from '../errors-ts' ; import DevServer from './server' ; import { getBuilder } from './builder-cache' ; import { VercelConfig , BuildMatch , BuildResult , BuilderInputs , BuilderOutput , BuildResultV3 , BuilderOutputs , EnvConfigs , } from './types' ; import { normalizeRoutes } from '@vercel/routing-utils' ; import getUpdateCommand from '../get-update-command' ; import { getTitleName } from '../pkg-name' ; interface BuildMessage { type : string ; } interface BuildMessageResult extends BuildMessage { type : 'buildResult' ; result ? : BuilderOutputs | BuildResult ; error ? : object ; } async function createBuildProcess ( match , envConfigs , workPath , output ) { output . debug ( `Creating build process for "${ match . entrypoint }"` ) ; const builderWorkerPath = join ( __dirname , 'builder-worker.js' ) ; let PATH = `${ dirname ( process . execPath ) }${ delimiter }${ process . env . PATH }` ; const env = { ... process . env , PATH , ... envConfigs . allEnv , } ; const buildProcess = fork ( builderWorkerPath , [ ] , { cwd : workPath , execArgv : [ ] , env , } ) ; match . buildProcess = buildProcess ; buildProcess . on ( 'exit' , ( code , signal ) => { output . debug ( `Build process for "${ match . entrypoint }" exited with ${ signal || code }` ) ; match . buildProcess = undefined ; } ) ; return new Promise ( ( resolve , reject ) => { buildProcess . once ( 'message' , ( { type } ) => { if ( type !== 'ready' ) { reject ( new Error ( 'Did not get "ready" event from builder' ) ) ; } else { resolve ( buildProcess ) ; } } ) ; } ) ; } export async function executeBuild ( vercelConfig , devServer , files , match , requestPath , isInitialBuild , filesChanged ? , filesRemoved ? ) { const { builderWithPkg : { runInProcess , requirePath , builder , package : pkg } , } = match ; const { entrypoint } = match ; const { envConfigs , cwd : workPath , devCacheDir } = devServer ; const debug = devServer . output . isDebugEnabled ( ) ; const startTime = Date . now ( ) ; const showBuildTimestamp = ! isOfficialRuntime ( 'static' , match . use ) && ( ! isInitialBuild || debug ) ; if ( showBuildTimestamp ) { devServer . output . log ( `Building ${ match . use }:${ entrypoint }` ) ; devServer . output . debug ( `Using \`${ pkg . name }${ pkg . version ? `@${ pkg . version }` : '' }\`` ) ; } const config = match . config || { } ; let result ; let { buildProcess } = match ; if ( ! runInProcess && ! buildProcess ) { buildProcess = await createBuildProcess ( match , envConfigs , workPath , devServer . output ) ; } const buildOptions = { files , entrypoint , workPath , config , meta : { isDev : true , requestPath , devCacheDir , filesChanged , filesRemoved , env : { ... envConfigs . runEnv } , buildEnv : { ... envConfigs . buildEnv } , } , } ; let buildResultOrOutputs ; if ( buildProcess ) { buildProcess . send ( { type : 'build' , requirePath , buildOptions , } ) ; buildResultOrOutputs = await new Promise ( ( resolve , reject ) => { function onMessage ( { type , result , error } ) { cleanup ( ) ; if ( type === 'buildResult' ) { if ( result ) { resolve ( result ) ; } else if ( error ) { reject ( Object . assign ( new Error ( ) , error ) ) ; } } else { reject ( new Error ( `Got unexpected message type: ${ type }` ) ) ; } } function onExit ( code , signal ) { cleanup ( ) ; const err = new Error ( `Builder exited with ${ signal || code } before sending build result` ) ; reject ( err ) ; } function cleanup ( ) { buildProcess ! . removeListener ( 'exit' , onExit ) ; buildProcess ! . removeListener ( 'message' , onMessage ) ; } buildProcess ! . on ( 'exit' , onExit ) ; buildProcess ! . on ( 'message' , onMessage ) ; } ) ; } else { buildResultOrOutputs = await builder . build ( buildOptions ) ; } if ( ! builder . version || builder . version === 1 ) { result = { output : buildResultOrOutputs as BuilderOutputs , routes : [ ] , watch : [ ] , distPath : typeof buildResultOrOutputs . distPath === 'string' ? buildResultOrOutputs . distPath : undefined , } ; } else if ( builder . version === 2 ) { result = buildResultOrOutputs as BuildResult ; } else if ( builder . version === 3 ) { const { output , ... rest } = buildResultOrOutputs as BuildResultV3 ; if ( ! output || ( output as BuilderOutput ) . type !== 'Lambda' ) { throw new Error ( 'The result of "builder.build()" must be a `Lambda`' ) ; } if ( output . maxDuration ) { throw new Error ( 'The result of "builder.build()" must not contain `maxDuration`' ) ; } if ( output . memory ) { throw new Error ( 'The result of "builder.build()" must not contain `memory`' ) ; } for ( const [ src , func ] of Object . entries ( config . functions || { } ) ) { if ( src === entrypoint || minimatch ( entrypoint , src ) ) { if ( func . maxDuration ) { output . maxDuration = func . maxDuration ; } if ( func . memory ) { output . memory = func . memory ; } break ; } } result = { ... rest , output : { [ entrypoint ] : output , } , } as BuildResult ; } else { throw new Error ( `${ getTitleName ( ) } CLI does not support builder version ${ builder . version }.\nPlease run \`${ await getUpdateCommand ( ) }\` to update to the latest CLI.` ) ; } const normalized = normalizeRoutes ( result . routes ) ; if ( normalized . error ) { throw new Error ( normalized . error . message ) ; } else { result . routes = normalized . routes || [ ] ; } const { output } = result ; const { cleanUrls } = vercelConfig ; Object . entries ( output ) . forEach ( ( [ path , value ] ) => { if ( cleanUrls && path . endsWith ( '.html' ) ) { path = path . slice ( 0 , - 5 ) ; if ( value . type === 'FileBlob' || value . type === 'FileFsRef' ) { value . contentType = value . contentType || 'text/html; charset=utf-8' ; } } const extensionless = devServer . getExtensionlessFile ( path ) ; if ( extensionless ) { path = extensionless ; } output [ path ] = value ; } ) ; for ( const name of Object . keys ( output ) ) { const obj = output [ name ] as File ; let lambda ; let fileRef ; let fileBlob ; switch ( obj . type ) { case 'FileFsRef' : fileRef = Object . assign ( Object . create ( FileFsRef . prototype ) , obj ) ; output [ name ] = fileRef ; break ; case 'FileBlob' : fileBlob = Object . assign ( Object . create ( FileBlob . prototype ) , obj ) ; fileBlob . data = Buffer . from ( ( obj as any ) . data . data ) ; output [ name ] = fileBlob ; break ; case 'Lambda' : lambda = Object . assign ( Object . create ( Lambda . prototype ) , obj ) as Lambda ; lambda . zipBuffer = Buffer . from ( ( obj as any ) . zipBuffer . data ) ; output [ name ] = lambda ; break ; default : throw new Error ( `Unknown file type: ${ obj . type }` ) ; } } result . watch = ( result . watch || [ ] ) . map ( ( w ) => { if ( w . startsWith ( './' ) ) { return w . substring ( 2 ) ; } return w ; } ) ; if ( ! result . watch . includes ( entrypoint ) ) { result . watch . push ( entrypoint ) ; } const maxLambdaBytes = bytes ( '50mb' ) ; for ( const asset of Object . values ( result . output ) ) { if ( asset . type === 'Lambda' ) { const size = asset . zipBuffer . length ; if ( size > maxLambdaBytes ) { throw new LambdaSizeExceededError ( size , maxLambdaBytes ) ; } } } await Promise . all ( Object . entries ( result . output ) . map ( async ( entry ) => { const path = entry [ 0 ] ; const asset = entry [ 1 ] ; if ( asset . type === 'Lambda' ) { const oldAsset = match . buildOutput && match . buildOutput [ path ] ; if ( oldAsset && oldAsset . type === 'Lambda' && oldAsset . fn ) { await oldAsset . fn . destroy ( ) ; } asset . fn = await createFunction ( { Code : { ZipFile : asset . zipBuffer } , Handler : asset . handler , Runtime : asset . runtime , MemorySize : asset . memory || 3008 , Environment : { Variables : { ... vercelConfig . env , ... asset . environment , ... envConfigs . runEnv , } , } , } ) ; } match . buildTimestamp = Date . now ( ) ; } ) ) ; match . buildResults . set ( requestPath , result ) ; Object . assign ( match . buildOutput , result . output ) ; if ( showBuildTimestamp ) { const endTime = Date . now ( ) ; devServer . output . log ( `Built ${ match . use }:${ entrypoint } [${ ms ( endTime - startTime ) }]` ) ; } } export async function getBuildMatches ( vercelConfig , cwd , output , devServer , fileList ) { const matches = [ ] ; if ( fileList . length === 0 ) { return matches ; } const noMatches = [ ] ; const builds = vercelConfig . builds || [ { src : '**' , use : '@vercel/static' } ] ; for ( const buildConfig of builds ) { let { src = '**' , use , config = { } } = buildConfig ; if ( ! use ) { continue ; } if ( src [ 0 ] === '/' ) { src = src . substring ( 1 ) ; } src = src . replace ( /(\[|\])/g , '[$1]' ) ; const mapToEntrypoint = new Map < string , string > ( ) ; const extensionless = devServer . getExtensionlessFile ( src ) ; if ( extensionless ) { mapToEntrypoint . set ( extensionless , src ) ; src = extensionless ; } const files = fileList . filter ( name => name === src || minimatch ( name , src , { dot : true } ) ) . map ( name => join ( cwd , name ) ) ; if ( files . length === 0 ) { noMatches . push ( buildConfig ) ; } for ( const file of files ) { src = relative ( cwd , file ) ; if ( config . zeroConfig && config . outputDirectory ) { const outputMatch = config . outputDirectory + '/' ; if ( src . startsWith ( outputMatch ) ) { src = src . slice ( outputMatch . length ) ; } } const builderWithPkg = await getBuilder ( use , output ) ; matches . push ( { ... buildConfig , src , entrypoint : mapToEntrypoint . get ( src ) || src , builderWithPkg , buildOutput : { } , buildResults : new Map ( ) , buildTimestamp : 0 , } ) ; } } if ( noMatches . length > 0 ) { output . warn ( `You defined ${ plural ( 'build' , noMatches . length , true ) } that did not match any source files (please ensure they are NOT defined in ${ highlight ( '.vercelignore' ) }):` ) ; for ( const buildConfig of noMatches ) { output . print ( `- ${ JSON . stringify ( buildConfig ) }\n` ) ; } } return matches ; } export async function shutdownBuilder ( match , { debug } ) { const ops = [ ] ; if ( match . buildProcess ) { const { pid } = match . buildProcess ; debug ( `Killing builder sub-process with PID ${ pid }` ) ; const killPromise = treeKill ( pid ) . then ( ( ) => { debug ( `Killed builder with PID ${ pid }` ) ; } ) . catch ( ( err ) => { debug ( `Failed to kill builder with PID ${ pid }: ${ err }` ) ; } ) ; ops . push ( killPromise ) ; delete match . buildProcess ; } if ( match . buildOutput ) { for ( const asset of Object . values ( match . buildOutput ) ) { if ( asset . type === 'Lambda' && asset . fn ) { debug ( `Shutting down Lambda function` ) ; ops . push ( asset . fn . destroy ( ) ) ; } } } await Promise . all ( ops ) ; }