import { FileSystem , PackageJsonLookup , Sort , Text } from '@rushstack/node-core-library' ; import * as child_process from 'child_process' ; import * as path from 'path' ; import stringArgv from 'string-argv' ; import { IpcMessage } from './LauncherTypes' ; export class Rundown { private _importedModuleMap : Map < string , string > = new Map ( ) ; public async invokeAsync ( scriptPath , args , quiet , ignoreExitCode ) { if ( ! FileSystem . exists ( scriptPath ) ) { throw new Error ( 'The specified script path does not exist: ' + scriptPath ) ; } const absoluteScriptPath = path . resolve ( scriptPath ) ; const expandedArgs = args === undefined ? [ ] : stringArgv ( args ) ; console . log ( 'Starting process: ' + [ absoluteScriptPath , ... expandedArgs ] . join ( ' ' ) ) ; console . log ( ) ; const nodeArgs = [ path . join ( __dirname , 'launcher.js' ) , absoluteScriptPath , ... expandedArgs ] ; await this . _spawnLauncherAsync ( nodeArgs , quiet , ignoreExitCode ) ; if ( ! quiet ) { console . log ( ) ; } } public writeSnapshotReport ( ) { const reportPath = 'rundown-snapshot.log' ; console . log ( 'Writing report file: ' + reportPath ) ; const packageJsonLookup = new PackageJsonLookup ( ) ; const importedPaths = [ ... this . _importedModuleMap . keys ( ) ] ; const importedPackageFolders = new Set ( ) ; for ( const importedPath of importedPaths ) { const importedPackageFolder = packageJsonLookup . tryGetPackageFolderFor ( importedPath ) ; if ( importedPackageFolder ) { if ( /[\\/]node_modules[\\/]/i . test ( importedPackageFolder ) ) { importedPackageFolders . add ( path . basename ( importedPackageFolder ) ) ; } else { const relativePath = path . relative ( process . cwd ( ) , importedPackageFolder ) ; importedPackageFolders . add ( Text . replaceAll ( relativePath , '\\' , '/' ) ) ; } } else { } } Sort . sortSet ( importedPackageFolders ) ; const data = [ ... importedPackageFolders ] . join ( '\n' ) + '\n' ; FileSystem . writeFile ( reportPath , data ) ; } public writeInspectReport ( traceImports ) { const reportPath = 'rundown-inspect.log' ; console . log ( 'Writing report file: ' + reportPath ) ; const importedPaths = [ ... this . _importedModuleMap . keys ( ) ] ; importedPaths . sort ( ) ; let data = '' ; if ( traceImports ) { for ( const importedPath of importedPaths ) { data += importedPath + '\n' ; let current = importedPath ; const visited = new Set ( ) ; for ( ; ; ) { const callerPath = this . _importedModuleMap . get ( current ) ; if ( ! callerPath ) { break ; } if ( visited . has ( callerPath ) ) { break ; } visited . add ( callerPath ) ; data += '  imported by ' + callerPath + '\n' ; current = callerPath ; } data += '\n' ; } } else { data = importedPaths . join ( '\n' ) + '\n' ; } FileSystem . writeFile ( reportPath , data ) ; } private async _spawnLauncherAsync ( nodeArgs , quiet , ignoreExitCode ) { const childProcess = child_process . spawn ( process . execPath , nodeArgs , { stdio : quiet ? [ 'inherit' , 'ignore' , 'ignore' , 'ipc' ] : [ 'inherit' , 'inherit' , 'inherit' , 'ipc' ] } ) ; let completedNormally = false ; childProcess . on ( 'message' , ( message ) : void => { switch ( message . id ) { case 'trace' : for ( const record of message . records ) { this . _importedModuleMap . set ( record . importedModule , record . callingModule ) ; } break ; case 'done' : completedNormally = true ; break ; default : throw new Error ( 'Unknown IPC message: ' + JSON . stringify ( message ) ) ; } } ) ; await new Promise < void > ( ( resolve , reject ) => { childProcess . on ( 'exit' , ( code , signal ) : void => { if ( code !== 0 && ! ignoreExitCode ) { reject ( new Error ( 'Child process terminated with exit code ' + code ) ) ; } else if ( ! completedNormally ) { reject ( new Error ( 'Child process terminated without completing IPC handshake' ) ) ; } else { resolve ( ) ; } } ) ; } ) ; } }