import { AsyncPriorityQueue } from "async" ; import * as assert from "assert" ; import * as path from "path" ; import { BuildResult , BuildPackage , summarizeBuildResult } from "../../buildGraph" ; import { logStatus , logVerbose } from "../../../common/logging" ; import { options } from "../../options" ; import { Task , TaskExec } from "../task" ; import { getExecutableFromCommand , writeFileAsync , unlinkAsync , readFileAsync , execAsync , existsSync , ExecAsyncResult } from "../../../common/utils" ; import chalk from "chalk" ; export abstract class LeafTask extends Task { private dependentTasks ? : LeafTask [ ] ; private parentCount : number = 0 ; constructor ( node , command ) { super ( node , command ) ; if ( ! this . isDisabled ) { this . node . buildContext . taskStats . leafTotalCount ++ ; } } public get isLeaf ( ) : boolean { return true ; } public get isDisabled ( ) { const isLintTask = this . executable === "eslint" || this . executable === "tsfmt" ; return ( options . nolint && isLintTask ) || ( options . lintonly && ! isLintTask ) ; } public get executable ( ) { return getExecutableFromCommand ( this . command ) ; } public initializeDependentTask ( ) { this . dependentTasks = new Array < LeafTask > ( ) ; this . addDependentTasks ( this . dependentTasks ) ; } public matchTask ( command , options ? ) { return ( this . command === command ) ? this : undefined ; } public collectLeafTasks ( leafTasks ) { leafTasks . push ( this ) ; this . parentCount ++ ; } protected get useWorker ( ) { return false ; } public async exec ( ) { if ( this . isDisabled ) { return BuildResult . UpToDate ; } if ( options . showExec ) { this . node . buildContext . taskStats . leafBuiltCount ++ ; const taskNum = this . node . buildContext . taskStats . leafBuiltCount . toString ( ) . padStart ( 3 , " " ) ; const totalTask = this . node . buildContext . taskStats . leafTotalCount - this . node . buildContext . taskStats . leafUpToDateCount ; logStatus ( `[${ taskNum }/${ totalTask }] ${ this . node . pkg . nameColored }: ${ this . command }` ) ; } const startTime = Date . now ( ) ; if ( this . recheckLeafIsUpToDate && ! this . forced && await this . checkLeafIsUpToDate ( ) ) { return this . execDone ( startTime , BuildResult . UpToDate ) ; } const ret = await this . execCore ( ) ; if ( ret . error ) { const codeStr = ret . error . code !== undefined ? ` (exit code ${ ret . error . code })` : "" ; console . error ( `${ this . node . pkg . nameColored }: error during command '${ this . command }'` ) ; console . error ( this . getExecErrors ( ret ) ) ; return this . execDone ( startTime , BuildResult . Failed ) ; } if ( ret . stderr ) { console . warn ( `${ this . node . pkg . nameColored }: warning during command '${ this . command }'` ) ; console . warn ( this . getExecErrors ( ret ) ) ; } await this . markExecDone ( ) ; return this . execDone ( startTime , BuildResult . Success ) ; } private async execCore ( ) { const workerPool = this . node . buildContext . workerPool ; if ( workerPool && this . useWorker ) { const workerResult = await workerPool . runOnWorker ( this . executable , this . command , this . node . pkg . directory ) ; if ( workerResult . code === 0 || ! workerResult . error ) { return { error : workerResult . code === 0 ? null : { name : "Worker error" , message : "Worker error" , cmd : this . command , code : workerResult . code } , stdout : workerResult . stdout ?? "" , stderr : workerResult . stderr ?? "" , } ; } const result = await this . execCommand ( ) ; if ( ! result . error ) { console . warn ( `${ this . node . pkg . nameColored }: warning: worker failed with code ${ workerResult . code } but succeeded directly '${ this . command }'` ) ; if ( workerResult . error ) { if ( workerResult . error . stack ) { console . warn ( workerResult . error . stack ) ; } else { console . warn ( `${ workerResult . error . name }: ${ workerResult . error . message }` ) ; } } } return result ; } return this . execCommand ( ) ; } private async execCommand ( ) { return execAsync ( this . command , { cwd : this . node . pkg . directory , env : { PATH : `${ path . join ( this . node . pkg . directory , "node_modules" , ".bin" ) }${ path . delimiter }${ process . env [ "PATH" ] }` } } ) ; } private getExecErrors ( ret ) { let errorMessages = ret . stdout ; if ( ret . stderr ) { errorMessages = `${ errorMessages }\n${ ret . stderr }` ; } errorMessages = errorMessages . trim ( ) ; if ( options . vscode ) { errorMessages = this . getVsCodeErrorMessages ( errorMessages ) ; } else { errorMessages = errorMessages . replace ( /\n/g , `\n${ this . node . pkg . nameColored }: ` ) ; errorMessages = `${ this . node . pkg . nameColored }: ${ errorMessages }` ; } return errorMessages ; } private execDone ( startTime , status ) { if ( ! options . showExec ) { let statusCharacter = " " ; switch ( status ) { case BuildResult . Success : statusCharacter = chalk . greenBright ( "\u2713" ) ; break ; case BuildResult . UpToDate : statusCharacter = chalk . cyanBright ( "-" ) ; break ; case BuildResult . Failed : statusCharacter = chalk . redBright ( "x" ) ; break ; } this . node . buildContext . taskStats . leafBuiltCount ++ ; const taskNum = this . node . buildContext . taskStats . leafBuiltCount . toString ( ) . padStart ( 3 , " " ) ; const totalTask = this . node . buildContext . taskStats . leafTotalCount - this . node . buildContext . taskStats . leafUpToDateCount ; const elapsedTime = ( Date . now ( ) - startTime ) / 1000 ; const statusString = `[${ taskNum }/${ totalTask }] ${ statusCharacter } ${ this . node . pkg . nameColored }: ${ this . command } - ${ elapsedTime . toFixed ( 3 ) }s` ; logStatus ( statusString ) ; if ( status === BuildResult . Failed ) { this . node . buildContext . failedTaskLines . push ( statusString ) ; } this . node . buildContext . taskStats . leafExecTimeTotal += elapsedTime ; } return status ; } protected async runTask ( q ) { this . logVerboseTask ( "Begin Leaf Task" ) ; const result = await this . buildDependentTask ( q ) ; if ( result === BuildResult . Failed ) { return BuildResult . Failed ; } return new Promise ( ( resolve , reject ) => { this . logVerboseTask ( `[${ this . parentCount }] Queue Leaf Task` ) ; q . push ( { task : this , resolve } , - this . parentCount ) ; } ) ; } protected async checkIsUpToDate ( ) { if ( this . isDisabled ) { return true ; } if ( options . lintonly ) { return false ; } const leafIsUpToDate = await this . checkDependentTasksIsUpToDate ( ) && await this . checkLeafIsUpToDate ( ) ; if ( leafIsUpToDate ) { this . node . buildContext . taskStats . leafUpToDateCount ++ ; this . logVerboseTask ( `Skipping Leaf Task` ) ; } return leafIsUpToDate ; } protected addChildTask ( dependentTasks , node , command , options ? ) { const task = node . findTask ( command , options ) ; if ( task ) { task . collectLeafTasks ( dependentTasks ) ; return task ; } return undefined ; } private async checkDependentTasksIsUpToDate ( ) { const dependentTasks = this . getDependentTasks ( ) ; for ( const dependentTask of dependentTasks ) { if ( ! await dependentTask . isUpToDate ( ) ) { this . logVerboseTrigger ( `dependent task ${ dependentTask . toString ( ) } not up to date` ) ; return false ; } } return true ; } private getDependentTasks ( ) { assert . notStrictEqual ( this . dependentTasks , undefined ) ; return this . dependentTasks ! ; } private async buildDependentTask ( q ) { const p = new Array < Promise < BuildResult > > ( ) ; for ( const dependentTask of this . getDependentTasks ( ) ) { p . push ( dependentTask . run ( q ) ) ; } return summarizeBuildResult ( await Promise . all ( p ) ) ; } protected getPackageFileFullPath ( filePath ) { return path . join ( this . node . pkg . directory , filePath ) ; } protected get allDependentTasks ( ) { return ( function * ( dependentTasks ) { const pending = [ ... dependentTasks ] ; const seen = new Set < LeafTask > ( ) ; while ( true ) { const leafTask = pending . pop ( ) ; if ( ! leafTask ) { return ; } if ( seen . has ( leafTask ) ) { continue ; } seen . add ( leafTask ) ; yield leafTask ; pending . push ( ... leafTask . getDependentTasks ( ) ) ; } } ) ( this . getDependentTasks ( ) ) ; } /**
     * Subclass should override these to configure the leaf task
     */ protected abstract addDependentTasks ( dependentTasks ) ; protected abstract checkLeafIsUpToDate ( ) ; protected get recheckLeafIsUpToDate ( ) : boolean { return false ; } protected async markExecDone ( ) { } protected getVsCodeErrorMessages ( errorMessages ) { return errorMessages ; } protected logVerboseNotUpToDate ( ) { this . logVerboseTrigger ( "not up to date" ) ; } protected logVerboseTrigger ( reason ) { logVerbose ( `Triggering Leaf Task: [${ reason }] ${ this . node . pkg . nameColored } - ${ this . command }` ) ; } protected logVerboseDependency ( child , dep ) { logVerbose ( `Task Dependency: ${ this . node . pkg . nameColored } ${ this . executable } -> ${ child . pkg . nameColored } ${ dep }` ) ; } protected logVerboseTask ( msg ) { logVerbose ( `Task: ${ this . node . pkg . nameColored } ${ this . executable }: ${ msg }` ) ; } protected addAllDependentPackageTasks ( dependentTasks ) { for ( const child of this . node . dependentPackages ) { if ( child . task ) { child . task . collectLeafTasks ( dependentTasks ) ; this . logVerboseDependency ( child , "*" ) ; } } } } ; export abstract class LeafWithDoneFileTask extends LeafTask { protected get doneFileFullPath ( ) { return this . getPackageFileFullPath ( this . doneFile ) ; } protected async checkIsUpToDate ( ) { const leafIsUpToDate = await super . checkIsUpToDate ( ) ; if ( ! leafIsUpToDate && ! this . recheckLeafIsUpToDate ) { const doneFileFullPath = this . doneFileFullPath ; try { if ( existsSync ( doneFileFullPath ) ) { await unlinkAsync ( doneFileFullPath ) ; } } catch { console . warn ( `${ this . node . pkg . nameColored }: warning: unable to unlink ${ doneFileFullPath }` ) ; } } return leafIsUpToDate ; } protected async markExecDone ( ) { const doneFileFullPath = this . doneFileFullPath ; try { let content = await this . getDoneFileContent ( ) ; if ( content !== undefined ) { await writeFileAsync ( doneFileFullPath , content ) ; } else { console . warn ( `${ this . node . pkg . nameColored }: warning: unable to generate content for ${ doneFileFullPath }` ) ; } } catch { console . warn ( `${ this . node . pkg . nameColored }: warning: unable to write ${ doneFileFullPath }` ) ; } } protected async checkLeafIsUpToDate ( ) { const doneFileFullPath = this . doneFileFullPath ! ; try { const doneFileExpectedContent = await this . getDoneFileContent ( ) ; if ( doneFileExpectedContent ) { const doneFileContent = await readFileAsync ( doneFileFullPath , "utf8" ) ; if ( doneFileContent === doneFileExpectedContent ) { return true ; } this . logVerboseTrigger ( "mismatched compare file" ) ; } else { this . logVerboseTrigger ( "unable to generate done file expected content" ) ; } } catch { this . logVerboseTrigger ( "unable to read compare file" ) ; } return false ; } /**
     * Subclass should override these to configure the leaf with done file task
     */ protected abstract get doneFile ( ) : string ; protected async getDoneFileContent ( ) { return "" ; } } export class UnknownLeafTask extends LeafTask { protected addDependentTasks ( dependentTasks ) { this . addAllDependentPackageTasks ( dependentTasks ) ; } protected async checkLeafIsUpToDate ( ) { return false ; } }