18d3f3824SCedric van Puttenimport JsonFile from '@expo/json-file';
28d3f3824SCedric van Puttenimport npmPackageArg from 'npm-package-arg';
38d3f3824SCedric van Puttenimport path from 'path';
48d3f3824SCedric van Putten
58a424bebSJames Ideimport { BasePackageManager } from './BasePackageManager';
68d3f3824SCedric van Puttenimport { findYarnOrNpmWorkspaceRoot, NPM_LOCK_FILE } from '../utils/nodeWorkspaces';
78d3f3824SCedric van Puttenimport { createPendingSpawnAsync } from '../utils/spawn';
88d3f3824SCedric van Putten
98d3f3824SCedric van Puttenexport class NpmPackageManager extends BasePackageManager {
108d3f3824SCedric van Putten  readonly name = 'npm';
118d3f3824SCedric van Putten  readonly bin = 'npm';
128d3f3824SCedric van Putten  readonly lockFile = NPM_LOCK_FILE;
138d3f3824SCedric van Putten
148d3f3824SCedric van Putten  workspaceRoot() {
158d3f3824SCedric van Putten    const root = findYarnOrNpmWorkspaceRoot(this.ensureCwdDefined('workspaceRoot'));
168d3f3824SCedric van Putten    if (root) {
178d3f3824SCedric van Putten      return new NpmPackageManager({
188d3f3824SCedric van Putten        ...this.options,
198d3f3824SCedric van Putten        silent: this.silent,
208d3f3824SCedric van Putten        log: this.log,
218d3f3824SCedric van Putten        cwd: root,
228d3f3824SCedric van Putten      });
238d3f3824SCedric van Putten    }
248d3f3824SCedric van Putten
258d3f3824SCedric van Putten    return null;
268d3f3824SCedric van Putten  }
278d3f3824SCedric van Putten
288d3f3824SCedric van Putten  addAsync(namesOrFlags: string[] = []) {
298d3f3824SCedric van Putten    if (!namesOrFlags.length) {
308d3f3824SCedric van Putten      return this.installAsync();
318d3f3824SCedric van Putten    }
328d3f3824SCedric van Putten
338d3f3824SCedric van Putten    const { flags, versioned, unversioned } = this.parsePackageSpecs(namesOrFlags);
348d3f3824SCedric van Putten
358d3f3824SCedric van Putten    return createPendingSpawnAsync(
368d3f3824SCedric van Putten      () => this.updatePackageFileAsync(versioned, 'dependencies'),
378d3f3824SCedric van Putten      () =>
388d3f3824SCedric van Putten        !unversioned.length
398d3f3824SCedric van Putten          ? this.runAsync(['install', ...flags])
408d3f3824SCedric van Putten          : this.runAsync(['install', '--save', ...flags, ...unversioned.map((spec) => spec.raw)])
418d3f3824SCedric van Putten    );
428d3f3824SCedric van Putten  }
438d3f3824SCedric van Putten
448d3f3824SCedric van Putten  addDevAsync(namesOrFlags: string[] = []) {
458d3f3824SCedric van Putten    if (!namesOrFlags.length) {
468d3f3824SCedric van Putten      return this.installAsync();
478d3f3824SCedric van Putten    }
488d3f3824SCedric van Putten
498d3f3824SCedric van Putten    const { flags, versioned, unversioned } = this.parsePackageSpecs(namesOrFlags);
508d3f3824SCedric van Putten
518d3f3824SCedric van Putten    return createPendingSpawnAsync(
528d3f3824SCedric van Putten      () => this.updatePackageFileAsync(versioned, 'devDependencies'),
538d3f3824SCedric van Putten      () =>
548d3f3824SCedric van Putten        !unversioned.length
558d3f3824SCedric van Putten          ? this.runAsync(['install', ...flags])
568d3f3824SCedric van Putten          : this.runAsync([
578d3f3824SCedric van Putten              'install',
588d3f3824SCedric van Putten              '--save-dev',
598d3f3824SCedric van Putten              ...flags,
608d3f3824SCedric van Putten              ...unversioned.map((spec) => spec.raw),
618d3f3824SCedric van Putten            ])
628d3f3824SCedric van Putten    );
638d3f3824SCedric van Putten  }
648d3f3824SCedric van Putten
658d3f3824SCedric van Putten  addGlobalAsync(namesOrFlags: string[] = []) {
668d3f3824SCedric van Putten    if (!namesOrFlags.length) {
678d3f3824SCedric van Putten      return this.installAsync();
688d3f3824SCedric van Putten    }
698d3f3824SCedric van Putten
708d3f3824SCedric van Putten    return this.runAsync(['install', '--global', ...namesOrFlags]);
718d3f3824SCedric van Putten  }
728d3f3824SCedric van Putten
738d3f3824SCedric van Putten  removeAsync(namesOrFlags: string[]) {
748d3f3824SCedric van Putten    return this.runAsync(['uninstall', ...namesOrFlags]);
758d3f3824SCedric van Putten  }
768d3f3824SCedric van Putten
778d3f3824SCedric van Putten  removeDevAsync(namesOrFlags: string[]) {
788d3f3824SCedric van Putten    return this.runAsync(['uninstall', '--save-dev', ...namesOrFlags]);
798d3f3824SCedric van Putten  }
808d3f3824SCedric van Putten
818d3f3824SCedric van Putten  removeGlobalAsync(namesOrFlags: string[]) {
828d3f3824SCedric van Putten    return this.runAsync(['uninstall', '--global', ...namesOrFlags]);
838d3f3824SCedric van Putten  }
848d3f3824SCedric van Putten
858d3f3824SCedric van Putten  /**
868d3f3824SCedric van Putten   * Parse all package specifications from the names or flag list.
878d3f3824SCedric van Putten   * The result from this method can be used for `.updatePackageFileAsync`.
888d3f3824SCedric van Putten   */
898d3f3824SCedric van Putten  private parsePackageSpecs(namesOrFlags: string[]) {
908d3f3824SCedric van Putten    const result: {
918d3f3824SCedric van Putten      flags: string[];
928d3f3824SCedric van Putten      versioned: npmPackageArg.Result[];
938d3f3824SCedric van Putten      unversioned: npmPackageArg.Result[];
948d3f3824SCedric van Putten    } = { flags: [], versioned: [], unversioned: [] };
958d3f3824SCedric van Putten
968d3f3824SCedric van Putten    namesOrFlags
978d3f3824SCedric van Putten      .map((name) => {
988d3f3824SCedric van Putten        if (name.trim().startsWith('-')) {
998d3f3824SCedric van Putten          result.flags.push(name);
1008d3f3824SCedric van Putten          return null;
1018d3f3824SCedric van Putten        }
1028d3f3824SCedric van Putten
1038d3f3824SCedric van Putten        return npmPackageArg(name);
1048d3f3824SCedric van Putten      })
1058d3f3824SCedric van Putten      .forEach((spec) => {
1068d3f3824SCedric van Putten        // When using a dist-tag version of a library, we need to consider it as "unversioned".
1078d3f3824SCedric van Putten        // Doing so will install that version with `npm install --save(-dev)`, and resolve the dist-tag properly.
1088d3f3824SCedric van Putten        if (spec && spec.rawSpec && spec.type !== 'tag') {
1098d3f3824SCedric van Putten          result.versioned.push(spec);
1108d3f3824SCedric van Putten        } else if (spec) {
1118d3f3824SCedric van Putten          result.unversioned.push(spec);
1128d3f3824SCedric van Putten        }
1138d3f3824SCedric van Putten      });
1148d3f3824SCedric van Putten
1158d3f3824SCedric van Putten    return result;
1168d3f3824SCedric van Putten  }
1178d3f3824SCedric van Putten
1188d3f3824SCedric van Putten  /**
1198d3f3824SCedric van Putten   * Older npm versions have issues with mismatched nested dependencies when adding exact versions.
1208d3f3824SCedric van Putten   * This propagates as issues like mismatched `@expo/config-pugins` versions.
1218d3f3824SCedric van Putten   * As a workaround, we update the `package.json` directly and run `npm install`.
1228d3f3824SCedric van Putten   */
1238d3f3824SCedric van Putten  private async updatePackageFileAsync(
1248d3f3824SCedric van Putten    packageSpecs: npmPackageArg.Result[],
1258d3f3824SCedric van Putten    packageType: 'dependencies' | 'devDependencies'
1268d3f3824SCedric van Putten  ) {
1278d3f3824SCedric van Putten    if (!packageSpecs.length) {
1288d3f3824SCedric van Putten      return;
1298d3f3824SCedric van Putten    }
1308d3f3824SCedric van Putten
1318d3f3824SCedric van Putten    const pkgPath = path.join(this.options.cwd?.toString() || '.', 'package.json');
132*22f8ba00STomasz Sapeta    const pkg =
133*22f8ba00STomasz Sapeta      await JsonFile.readAsync<Record<typeof packageType, { [pkgName: string]: string }>>(pkgPath);
1348d3f3824SCedric van Putten
1358d3f3824SCedric van Putten    packageSpecs.forEach((spec) => {
1368d3f3824SCedric van Putten      pkg[packageType] = pkg[packageType] || {};
1378d3f3824SCedric van Putten      pkg[packageType][spec.name!] = spec.rawSpec;
1388d3f3824SCedric van Putten    });
1398d3f3824SCedric van Putten
1408d3f3824SCedric van Putten    await JsonFile.writeAsync(pkgPath, pkg, { json5: false });
1418d3f3824SCedric van Putten  }
1428d3f3824SCedric van Putten}
143