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