import { queue } from "async" ; import * as chalk from "chalk" ; import * as fs from "fs" ; import * as path from "path" ; import sortPackageJson from "sort-package-json" ; import { logStatus , logVerbose } from "./logging" ; import { copyFileAsync , execWithErrorAsync , rimrafWithErrorAsync , unlinkAsync , writeFileAsync , ExecAsyncResult , readJsonSync , existsSync , lookUpDirSync , isSameFileOrDir , } from "./utils" ; import { MonoRepo } from "./monoRepo" ; import { options } from "../fluidBuild/options" ; interface IPerson { name : string ; email : string ; url : string ; } interface IPackage { name : string ; version : string ; private : boolean ; description : string ; keywords : string [ ] ; homepage : string ; bugs : { url : string ; email : string ; } ; license : string ; author : IPerson ; contributors : IPerson [ ] ; files : string [ ] ; main : string ; browser : string ; bin : { [ key ] : string ; } ; man : string | string [ ] ; repository : string | { type : string ; url : string ; } ; scripts : { [ key ] : string | undefined ; } ; config : { [ key ] : string ; } ; dependencies : { [ key ] : string ; } ; devDependencies : { [ key ] : string ; } ; peerDependencies : { [ key ] : string ; } ; bundledDependencies : { [ key ] : string ; } ; optionalDependencies : { [ key ] : string ; } ; engines : { node : string ; npm : string ; } ; os : string [ ] ; cpu : string [ ] ; [ key ] : any ; } ; export class Package { private static packageCount : number = 0 ; private static readonly chalkColor = [ chalk . default . red , chalk . default . green , chalk . default . yellow , chalk . default . blue , chalk . default . magenta , chalk . default . cyan , chalk . default . white , chalk . default . grey , chalk . default . redBright , chalk . default . greenBright , chalk . default . yellowBright , chalk . default . blueBright , chalk . default . magentaBright , chalk . default . cyanBright , chalk . default . whiteBright , ] ; public get packageJson ( ) : IPackage { return this . _packageJson ; } private readonly packageId = Package . packageCount ++ ; private _matched : boolean = false ; private _markForBuild : boolean = false ; private _packageJson : IPackage ; constructor ( private readonly packageJsonFileName , public readonly group , public readonly monoRepo ? ) { this . _packageJson = readJsonSync ( packageJsonFileName ) ; logVerbose ( `Package Loaded: ${ this . nameColored }` ) ; } public get name ( ) : string { return this . packageJson . name ; } public get nameColored ( ) : string { return this . color ( this . name ) ; } public get version ( ) : string { return this . packageJson . version ; } public get isPublished ( ) : boolean { return ! this . packageJson . private ; } public get matched ( ) { return this . _matched ; } public setMatched ( ) { this . _matched = true ; this . _markForBuild = true ; } public get markForBuild ( ) { return this . _markForBuild ; } public setMarkForBuild ( ) { this . _markForBuild = true ; } public get dependencies ( ) { return Object . keys ( this . packageJson . dependencies ?? { } ) ; } public get combinedDependencies ( ) { const it = function * ( packageJson ) { for ( const item in packageJson . dependencies ) { yield ( { name : item , version : packageJson . dependencies [ item ] , dev : false } ) ; } for ( const item in packageJson . devDependencies ) { yield ( { name : item , version : packageJson . devDependencies [ item ] , dev : true } ) ; } } ; return it ( this . packageJson ) ; } public get directory ( ) : string { return path . dirname ( this . packageJsonFileName ) ; } private get color ( ) { return Package . chalkColor [ this . packageId % Package . chalkColor . length ] ; } public getScript ( name ) { return this . packageJson . scripts ? this . packageJson . scripts [ name ] : undefined ; } public async cleanNodeModules ( ) { return rimrafWithErrorAsync ( path . join ( this . directory , "node_modules" ) , this . nameColored ) ; } public async savePackageJson ( ) { return writeFileAsync ( this . packageJsonFileName , `${ JSON . stringify ( sortPackageJson ( this . packageJson ) , undefined , 2 ) }\n` ) ; } public reload ( ) { this . _packageJson = readJsonSync ( this . packageJsonFileName ) ; } public async noHoistInstall ( repoRoot ) { const rootNpmRC = path . join ( repoRoot , ".npmrc" ) ; const npmRC = path . join ( this . directory , ".npmrc" ) ; const npmCommand = "npm i --no-package-lock --no-shrinkwrap" ; await copyFileAsync ( rootNpmRC , npmRC ) ; const result = await execWithErrorAsync ( npmCommand , { cwd : this . directory } , this . nameColored ) ; await unlinkAsync ( npmRC ) ; return result ; } public async checkInstall ( print = true ) { if ( this . combinedDependencies . next ( ) . done ) { return true ; } if ( ! existsSync ( path . join ( this . directory , "node_modules" ) ) ) { if ( print ) { console . error ( `${ this . nameColored }: node_modules not installed` ) ; } return false ; } let succeeded = true ; for ( const dep of this . combinedDependencies ) { if ( ! lookUpDirSync ( this . directory , ( currentDir ) => { return existsSync ( path . join ( currentDir , "node_modules" , dep . name ) ) ; } ) ) { succeeded = false ; if ( print ) { console . error ( `${ this . nameColored }: dependency ${ dep . name } not found` ) ; } } } return succeeded ; } public async install ( ) { if ( this . monoRepo ) { throw new Error ( "Package in a monorepo shouldn't be installed" ) ; } console . log ( `${ this . nameColored }: Installing - npm i` ) ; const installScript = "npm i" ; return execWithErrorAsync ( installScript , { cwd : this . directory } , this . directory ) ; } } ; interface TaskExec < TItem , TResult > { item : TItem ; resolve : ( result : TResult ) => void ; reject : ( reason ? : any ) => void ; } ; async function queueExec < TItem , TResult > ( items , exec , messageCallback ? ) { let numDone = 0 ; const timedExec = messageCallback ? async ( item ) => { const startTime = Date . now ( ) ; const result = await exec ( item ) ; const elapsedTime = ( Date . now ( ) - startTime ) / 1000 ; logStatus ( `[${ ++ numDone }/${ p . length }] ${ messageCallback ( item ) } - ${ elapsedTime . toFixed ( 3 ) }s` ) ; return result ; } : exec ; const q = queue ( async ( taskExec ) => { try { taskExec . resolve ( await timedExec ( taskExec . item ) ) ; } catch ( e ) { taskExec . reject ( e ) ; } } , options . concurrency ) ; const p = [ ] ; for ( const item of items ) { p . push ( new Promise < TResult > ( ( resolve , reject ) => q . push ( { item , resolve , reject } ) ) ) ; } return Promise . all ( p ) ; } export class Packages { public constructor ( public readonly packages ) { } public static loadDir ( dirFullPath , group , ignoredDirFullPaths , monoRepo ? ) { const packageJsonFileName = path . join ( dirFullPath , "package.json" ) ; if ( existsSync ( packageJsonFileName ) ) { return [ new Package ( packageJsonFileName , group , monoRepo ) ] ; } const packages = [ ] ; const files = fs . readdirSync ( dirFullPath , { withFileTypes : true } ) ; files . map ( ( dirent ) => { if ( dirent . isDirectory ( ) && dirent . name !== "node_modules" ) { const fullPath = path . join ( dirFullPath , dirent . name ) ; if ( ignoredDirFullPaths === undefined || ! ignoredDirFullPaths . some ( name => isSameFileOrDir ( name , fullPath ) ) ) { packages . push ( ... Packages . loadDir ( fullPath , group , ignoredDirFullPaths , monoRepo ) ) ; } } } ) ; return packages ; } public async cleanNodeModules ( ) { return this . queueExecOnAllPackage ( pkg => pkg . cleanNodeModules ( ) , "rimraf node_modules" ) ; } public async noHoistInstall ( repoRoot ) { return this . queueExecOnAllPackage ( pkg => pkg . noHoistInstall ( repoRoot ) , "npm i" ) ; } public async forEachAsync < TResult > ( exec , parallel , message ? ) { if ( parallel ) { return this . queueExecOnAllPackageCore ( exec , message ) ; } ; const results = [ ] ; for ( const pkg of this . packages ) { results . push ( await exec ( pkg ) ) ; } return results ; } public static async clean ( packages , status ) { const cleanP = [ ] ; let numDone = 0 ; const execCleanScript = async ( pkg , cleanScript ) => { const startTime = Date . now ( ) ; const result = await execWithErrorAsync ( cleanScript , { cwd : pkg . directory , env : { PATH : `${ process . env [ "PATH" ] }${ path . delimiter }${ path . join ( pkg . directory , "node_modules" , ".bin" ) }` } } , pkg . nameColored ) ; if ( status ) { const elapsedTime = ( Date . now ( ) - startTime ) / 1000 ; logStatus ( `[${ ++ numDone }/${ cleanP . length }] ${ pkg . nameColored }: ${ cleanScript } - ${ elapsedTime . toFixed ( 3 ) }s` ) ; } return result ; } ; for ( const pkg of packages ) { const cleanScript = pkg . getScript ( "clean" ) ; if ( cleanScript ) { cleanP . push ( execCleanScript ( pkg , cleanScript ) ) ; } } ; const results = await Promise . all ( cleanP ) ; return ! results . some ( result => result . error ) ; } private async queueExecOnAllPackageCore < TResult > ( exec , message ? ) { const messageCallback = message ? ( pkg ) => ` ${ pkg . nameColored }: ${ message }` : undefined ; return queueExec ( this . packages , exec , messageCallback ) ; } private async queueExecOnAllPackage ( exec , message ? ) { const results = await this . queueExecOnAllPackageCore ( exec , message ) ; return ! results . some ( result => result . error ) ; } }