1import spawnAsync, { SpawnPromise, SpawnResult } from '@expo/spawn-async'; 2import assert from 'assert'; 3import fs from 'fs'; 4import path from 'path'; 5 6import { PackageManager, PackageManagerOptions } from '../PackageManager'; 7import { PendingSpawnPromise } from '../utils/spawn'; 8 9export abstract class BasePackageManager implements PackageManager { 10 readonly silent: boolean; 11 readonly log?: (...args: any) => void; 12 readonly options: PackageManagerOptions; 13 14 constructor({ silent, log, env = process.env, ...options }: PackageManagerOptions = {}) { 15 this.silent = !!silent; 16 this.log = log ?? (!silent ? console.log : undefined); 17 this.options = { 18 ...options, 19 env: { ...this.getDefaultEnvironment(), ...env }, 20 }; 21 } 22 23 /** Get the name of the package manager */ 24 abstract readonly name: string; 25 /** Get the executable binary of the package manager */ 26 abstract readonly bin: string; 27 /** Get the lockfile for this package manager */ 28 abstract readonly lockFile: string; 29 30 /** Get the default environment variables used when running the package manager. */ 31 protected getDefaultEnvironment(): Record<string, string> { 32 return { 33 ADBLOCK: '1', 34 DISABLE_OPENCOLLECTIVE: '1', 35 }; 36 } 37 38 abstract addAsync( 39 namesOrFlags: string[] 40 ): SpawnPromise<SpawnResult> | PendingSpawnPromise<SpawnResult>; 41 abstract addDevAsync( 42 namesOrFlags: string[] 43 ): SpawnPromise<SpawnResult> | PendingSpawnPromise<SpawnResult>; 44 abstract addGlobalAsync( 45 namesOrFlags: string[] 46 ): SpawnPromise<SpawnResult> | PendingSpawnPromise<SpawnResult>; 47 48 abstract removeAsync( 49 namesOrFlags: string[] 50 ): SpawnPromise<SpawnResult> | PendingSpawnPromise<SpawnResult>; 51 abstract removeDevAsync( 52 namesOrFlags: string[] 53 ): SpawnPromise<SpawnResult> | PendingSpawnPromise<SpawnResult>; 54 abstract removeGlobalAsync( 55 namesOrFlags: string[] 56 ): SpawnPromise<SpawnResult> | PendingSpawnPromise<SpawnResult>; 57 58 abstract workspaceRoot(): PackageManager | null; 59 60 /** Ensure the CWD is set to a non-empty string */ 61 protected ensureCwdDefined(method?: string): string { 62 const cwd = this.options.cwd?.toString(); 63 const className = (this.constructor as typeof BasePackageManager).name; 64 const methodName = method ? `.${method}` : ''; 65 assert(cwd, `cwd is required for ${className}${methodName}`); 66 return cwd; 67 } 68 69 runAsync(command: string[]) { 70 this.log?.(`> ${this.name} ${command.join(' ')}`); 71 const spawn = spawnAsync(this.bin, command, this.options); 72 73 if (!this.silent) { 74 spawn.child.stderr?.pipe(process.stderr); 75 } 76 77 return spawn; 78 } 79 80 async versionAsync() { 81 return await this.runAsync(['--version']).then(({ stdout }) => stdout.trim()); 82 } 83 84 async getConfigAsync(key: string) { 85 return await this.runAsync(['config', 'get', key]).then(({ stdout }) => stdout.trim()); 86 } 87 88 async removeLockfileAsync() { 89 const cwd = this.ensureCwdDefined('removeLockFile'); 90 const filePath = path.join(cwd, this.lockFile); 91 await fs.promises.rm(filePath, { force: true }); 92 } 93 94 installAsync(flags: string[] = []): SpawnPromise<SpawnResult> | PendingSpawnPromise<SpawnResult> { 95 return this.runAsync(['install', ...flags]); 96 } 97 98 async uninstallAsync() { 99 const cwd = this.ensureCwdDefined('uninstallAsync'); 100 const modulesPath = path.join(cwd, 'node_modules'); 101 await fs.promises.rm(modulesPath, { force: true, recursive: true }); 102 } 103} 104