import { packageJson , install , uninstall , NpmOptions } from 'mrm-core' ; import { spawnSync , SpawnSyncReturns , StdioOptions , spawn } from 'child_process' ; import { File } from '../Base/File' ; type InstallerFns = 'install' | 'uninstall' ; type InstallerNotifier = ( list : string [ ] , dev : boolean ) => void ; type Dependencies = { list : string [ ] ; versions ? : any ; dev : boolean ; } ; type SupportedPackageManager = 'yarn' | 'pnpm' | 'npm' ; /**
 * Exposes the API to work with `package.json` file. The file is
 * same as a standard JSON file, but with some special methods
 * related to package file itself.
 */ export class PackageJsonFile extends File { public filePointer : ReturnType < typeof packageJson > ; /**
     * Collection of actions to be executed on package file
     */ protected actions = [ ] ; /**
     * A copy of install instructions
     */ protected packages : { install : { dependency : string ; version : string ; dev : boolean ; } [ ] ; uninstall : { dependency : string ; dev : boolean ; } [ ] ; } = { install : [ ] , uninstall : [ ] , } ; /**
     * Explicitly force to use another client instead of npm
     */ private packageManager : SupportedPackageManager ; /**
     * Method invoked before installing dependencies
     */ private beforeInstallHooks ? : InstallerNotifier ; /**
     * Method invoked before uninstalling dependencies
     */ private beforeUninstallHooks ? : InstallerNotifier ; constructor ( basePath , private installerOutput = 'pipe' ) { super ( basePath ) ; this . cdIn ( ) ; this . filePointer = packageJson ( ) ; this . cdOut ( ) ; } /**
     * Run hooks for action or uninstall action
     */ private runHooks ( action , list , dev ) { if ( action === 'install' && typeof this . beforeInstallHooks === 'function' ) { this . beforeInstallHooks ( list , dev ) ; } else if ( action === 'uninstall' && typeof this . beforeUninstallHooks === 'function' ) { this . beforeUninstallHooks ( list , dev ) ; } } /**
     * Sets installation client
     */ private setClient ( options ) { if ( this . packageManager === 'yarn' ) { options . yarn = true ; } else if ( this . packageManager === 'pnpm' ) { options . pnpm = true ; } } /**
     * Executes the installer `install` or `uninstall` action. Use
     * `this.installerFnAsync` for async version
     */ private installerFn ( action , list , options ) { if ( ! list . length ) { return ; } this . setClient ( options ) ; this . runHooks ( action , list , options . dev ! ) ; const fn = action === 'install' ? install : uninstall ; return fn ( list , options , ( command , args ) => { return spawnSync ( command , args , { stdio : this . installerOutput } ) ; } ) as SpawnSyncReturns < Buffer > ; } /**
     * Executes the installer `install` or `uninstall` action. Use
     * `this.installerFn` for sync version
     */ private installerFnAsync ( action , list , options ) { return new Promise < undefined | SpawnSyncReturns < Buffer > > ( ( resolve ) => { if ( ! list . length ) { resolve ( undefined ) ; return ; } this . setClient ( options ) ; this . runHooks ( action , list , options . dev ! ) ; let response ; const fn = action === 'install' ? install : uninstall ; let callbackInvoked = false ; fn ( list , options , ( command , args ) => { callbackInvoked = true ; const runner = spawn ( command , args , { stdio : 'pipe' } ) ; response = { pid : runner . pid ! , output : [ ] , stdout : Buffer . from ( '' ) , stderr : Buffer . from ( '' ) , status : null , signal : null , } ; runner . stdout . on ( 'data' , ( chunk ) => { response . stdout = Buffer . concat ( [ response . stdout , chunk ] ) ; } ) ; runner . stderr . on ( 'data' , ( chunk ) => { response . stderr = Buffer . concat ( [ response . stderr , chunk ] ) ; } ) ; runner . on ( 'close' , ( code , signal ) => { response . status = code ; response . signal = signal ; resolve ( response ) ; } ) ; } ) ; if ( ! callbackInvoked ) { resolve ( undefined ) ; } } ) ; } /**
     * Install and uninstall packages defined via `this.install`
     * and `this.uninstall`
     */ private commitDependencies ( installs , uninstalls ) { let response ; for ( let { list , versions , dev } of installs ) { response = this . installerFn ( 'install' , list , { versions , dev } ) ; if ( response && response . status === 1 ) { return response ; } } for ( let { list , dev } of uninstalls ) { response = this . installerFn ( 'uninstall' , list , { dev } ) ; if ( response && response . status === 1 ) { return response ; } } } /**
     * Performing uninstalling as a rollback step. Which means, this method
     * will remove packages marked for installation.
     */ private rollbackDependencies ( installs ) { let response ; for ( let { list , dev } of installs ) { response = this . installerFn ( 'uninstall' , list , { dev } ) ; if ( response && response . status === 1 ) { return response ; } } } /**
     * Same as `commitInstalls` but async
     */ private async commitDependenciesAsync ( installs , uninstalls ) { let response ; for ( let { list , versions , dev } of installs ) { response = await this . installerFnAsync ( 'install' , list , { versions , dev } ) ; if ( response && response . status === 1 ) { return response ; } } for ( let { list , dev } of uninstalls ) { response = await this . installerFnAsync ( 'uninstall' , list , { dev } ) ; if ( response && response . status === 1 ) { return response ; } } } /**
     * Same as `rollbackInstalls` but async.
     */ private async rollbackDependenciesAsync ( installs ) { let response ; for ( let { list , dev } of installs ) { response = await this . installerFnAsync ( 'uninstall' , list , { dev } ) ; if ( response && response . status === 1 ) { return response ; } } } /**
     * Commits actions defined on the given file
     */ private commitActions ( ) { const actions = this . getCommitActions ( ) ; const deleteFile = actions . find ( ( { action } ) => action === 'delete' ) ; /**
         * In case of `delete` action. There is no point running
         * other actions and we can simply delete the file
         */ if ( deleteFile ) { this . filePointer . delete ( ) ; this . cdOut ( ) ; return false ; } /**
         * Executing all actions
         */ actions . forEach ( ( { action , body } ) => { if ( [ 'set' , 'unset' ] . indexOf ( action ) > - 1 ) { this . filePointer [ action ] ( body . key , body . value ) ; return ; } if ( [ 'prependScript' , 'appendScript' , 'setScript' , 'removeScript' ] . indexOf ( action ) > - 1 ) { this . filePointer [ action ] ( body . name , body . script ) ; return ; } } ) ; /**
         * Save the file to the disk before starting install process.
         */ this . filePointer . save ( ) ; return true ; } /**
     * Rollsback actions defined on the package file
     */ private rollbackActions ( ) { const actions = this . getCommitActions ( ) ; /**
         * Executing actions in reverse.
         */ actions . forEach ( ( { action , body } ) => { if ( action === 'set' ) { this . filePointer . unset ( body . key ) ; return ; } if ( action === 'setScript' ) { this . filePointer . removeScript ( body . name ) ; return ; } if ( [ 'prependScript' , 'appendScript' ] . indexOf ( action ) > - 1 ) { this . filePointer . removeScript ( body . name , new RegExp ( body . script ) ) ; return ; } } ) ; /**
         * Write file to the disk
         */ this . filePointer . save ( ) ; return true ; } /**
     * Set key/value pair in the package.json file
     */ public set ( key , value ) { this . addAction ( 'set' , { key , value } ) ; return this ; } /**
     * Set a specific client to be used
     */ public useClient ( client ) { this . packageManager = client ; return this ; } deprecated public yarn ( _useYarn ) { this . packageManager = 'yarn' ; return this ; } /**
     * Unset key/value pair from the package.json file
     */ public unset ( key ) { this . addAction ( 'unset' , { key } ) ; return this ; } /**
     * Set package.json script
     */ public setScript ( name , script ) { this . addAction ( 'setScript' , { name , script } ) ; return this ; } /**
     * Append to existing package.json script
     */ public appendScript ( name , script ) { this . addAction ( 'appendScript' , { name , script } ) ; return this ; } /**
     * Prepend to existing package.json script
     */ public prependScript ( name , script ) { this . addAction ( 'prependScript' , { name , script } ) ; return this ; } /**
     * Remove existing script or remove a given action from an
     * existing script
     */ public removeScript ( name , script ? ) { this . addAction ( 'removeScript' , { name , script } ) ; return this ; } /**
     * Install dependencies
     */ public install ( dependency , version = 'latest' , dev = true ) { this . packages . install . push ( { dependency , version , dev } ) ; return this ; } /**
     * Uninstall dependencies
     */ public uninstall ( dependency , dev = true ) { this . packages . uninstall . push ( { dependency , dev } ) ; return this ; } /**
     * Remove file
     */ public delete ( ) { this . addAction ( 'delete' ) ; return this ; } /**
     * Returns value for a given key from the file
     */ public get ( ) ; public get ( address , defaultValue ? ) ; public get ( address ? , defaultValue ? ) { return address ? this . filePointer . get ( address , defaultValue ) : this . filePointer . get ( ) ; } /**
     * A boolean telling if the file already exists
     */ public exists ( ) { return this . filePointer . exists ( ) ; } /**
     * Returns a list of dependencies along with specific versions (if any)
     */ public getInstalls ( dev = true ) { const dependencies = { versions : { } , list : [ ] , dev } ; return this . packages . install . reduce ( ( result , dependency ) => { if ( dependency . dev && dev ) { result . list . push ( dependency . dependency ) ; if ( dependency . version !== 'latest' ) { result . versions [ dependency . dependency ] = dependency . version ; } } else if ( ! dependency . dev && ! dev ) { result . list . push ( dependency . dependency ) ; if ( dependency . version !== 'latest' ) { result . versions [ dependency . dependency ] = dependency . version ; } } return result ; } , dependencies ) ; } /**
     * Returns uninstalls list for prod or development
     * dependencies.
     */ public getUninstalls ( dev ) { const dependencies = { list : [ ] , dev } ; return this . packages . uninstall . reduce ( ( result , dependency ) => { if ( dependency . dev && dev ) { result . list . push ( dependency . dependency ) ; } else if ( ! dependency . dev && ! dev ) { result . list . push ( dependency . dependency ) ; } return result ; } , dependencies ) ; } /**
     * Define a function to be called before installing dependencies
     */ public beforeInstall ( callback ) { this . beforeInstallHooks = callback ; return this ; } /**
     * Define a function to be called before uninstalling dependencies
     */ public beforeUninstall ( callback ) { this . beforeUninstallHooks = callback ; return this ; } /**
     * Commit mutations
     */ public commit ( ) { this . cdIn ( ) ; const success = this . commitActions ( ) ; if ( ! success ) { return ; } /**
         * Install/uninstall dependencies
         */ const response = this . commitDependencies ( [ this . getInstalls ( true ) , this . getInstalls ( false ) ] , [ this . getUninstalls ( true ) , this . getUninstalls ( false ) ] ) ; this . cdOut ( ) ; return response ; } /**
     * Commits async. The files are still written using synchronous
     * API. However, the install and uninstall becomes async.
     */ public async commitAsync ( ) { this . cdIn ( ) ; const success = this . commitActions ( ) ; if ( ! success ) { return ; } /**
         * Install/uninstall dependencies
         */ const response = await this . commitDependenciesAsync ( [ this . getInstalls ( true ) , this . getInstalls ( false ) ] , [ this . getUninstalls ( true ) , this . getUninstalls ( false ) ] ) ; this . cdOut ( ) ; return response ; } /**
     * Rollback mutations
     */ public rollback ( ) { this . cdIn ( ) ; const success = this . rollbackActions ( ) ; if ( ! success ) { return ; } /**
         * Uninstalling installed packages
         */ const response = this . rollbackDependencies ( [ this . getInstalls ( true ) , this . getInstalls ( false ) ] ) ; this . cdOut ( ) ; return response ; } /**
     * Rollsback async. The files are still written using synchronous
     * API. However, the uninstall becomes async.
     */ public async rollbackAsync ( ) { this . cdIn ( ) ; const success = this . rollbackActions ( ) ; if ( ! success ) { return ; } /**
         * Uninstalling installed packages
         */ const response = await this . rollbackDependenciesAsync ( [ this . getInstalls ( true ) , this . getInstalls ( false ) , ] ) ; this . cdOut ( ) ; return response ; } }