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