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