1import JsonFile from '@expo/json-file'; 2import npmPackageArg from 'npm-package-arg'; 3import path from 'path'; 4 5import { findYarnOrNpmWorkspaceRoot, NPM_LOCK_FILE } from '../utils/nodeWorkspaces'; 6import { createPendingSpawnAsync } from '../utils/spawn'; 7import { BasePackageManager } from './BasePackageManager'; 8 9export class NpmPackageManager extends BasePackageManager { 10 readonly name = 'npm'; 11 readonly bin = 'npm'; 12 readonly lockFile = NPM_LOCK_FILE; 13 14 workspaceRoot() { 15 const root = findYarnOrNpmWorkspaceRoot(this.ensureCwdDefined('workspaceRoot')); 16 if (root) { 17 return new NpmPackageManager({ 18 ...this.options, 19 silent: this.silent, 20 log: this.log, 21 cwd: root, 22 }); 23 } 24 25 return null; 26 } 27 28 addAsync(namesOrFlags: string[] = []) { 29 if (!namesOrFlags.length) { 30 return this.installAsync(); 31 } 32 33 const { flags, versioned, unversioned } = this.parsePackageSpecs(namesOrFlags); 34 35 return createPendingSpawnAsync( 36 () => this.updatePackageFileAsync(versioned, 'dependencies'), 37 () => 38 !unversioned.length 39 ? this.runAsync(['install', ...flags]) 40 : this.runAsync(['install', '--save', ...flags, ...unversioned.map((spec) => spec.raw)]) 41 ); 42 } 43 44 addDevAsync(namesOrFlags: string[] = []) { 45 if (!namesOrFlags.length) { 46 return this.installAsync(); 47 } 48 49 const { flags, versioned, unversioned } = this.parsePackageSpecs(namesOrFlags); 50 51 return createPendingSpawnAsync( 52 () => this.updatePackageFileAsync(versioned, 'devDependencies'), 53 () => 54 !unversioned.length 55 ? this.runAsync(['install', ...flags]) 56 : this.runAsync([ 57 'install', 58 '--save-dev', 59 ...flags, 60 ...unversioned.map((spec) => spec.raw), 61 ]) 62 ); 63 } 64 65 addGlobalAsync(namesOrFlags: string[] = []) { 66 if (!namesOrFlags.length) { 67 return this.installAsync(); 68 } 69 70 return this.runAsync(['install', '--global', ...namesOrFlags]); 71 } 72 73 removeAsync(namesOrFlags: string[]) { 74 return this.runAsync(['uninstall', ...namesOrFlags]); 75 } 76 77 removeDevAsync(namesOrFlags: string[]) { 78 return this.runAsync(['uninstall', '--save-dev', ...namesOrFlags]); 79 } 80 81 removeGlobalAsync(namesOrFlags: string[]) { 82 return this.runAsync(['uninstall', '--global', ...namesOrFlags]); 83 } 84 85 /** 86 * Parse all package specifications from the names or flag list. 87 * The result from this method can be used for `.updatePackageFileAsync`. 88 */ 89 private parsePackageSpecs(namesOrFlags: string[]) { 90 const result: { 91 flags: string[]; 92 versioned: npmPackageArg.Result[]; 93 unversioned: npmPackageArg.Result[]; 94 } = { flags: [], versioned: [], unversioned: [] }; 95 96 namesOrFlags 97 .map((name) => { 98 if (name.trim().startsWith('-')) { 99 result.flags.push(name); 100 return null; 101 } 102 103 return npmPackageArg(name); 104 }) 105 .forEach((spec) => { 106 // When using a dist-tag version of a library, we need to consider it as "unversioned". 107 // Doing so will install that version with `npm install --save(-dev)`, and resolve the dist-tag properly. 108 if (spec && spec.rawSpec && spec.type !== 'tag') { 109 result.versioned.push(spec); 110 } else if (spec) { 111 result.unversioned.push(spec); 112 } 113 }); 114 115 return result; 116 } 117 118 /** 119 * Older npm versions have issues with mismatched nested dependencies when adding exact versions. 120 * This propagates as issues like mismatched `@expo/config-pugins` versions. 121 * As a workaround, we update the `package.json` directly and run `npm install`. 122 */ 123 private async updatePackageFileAsync( 124 packageSpecs: npmPackageArg.Result[], 125 packageType: 'dependencies' | 'devDependencies' 126 ) { 127 if (!packageSpecs.length) { 128 return; 129 } 130 131 const pkgPath = path.join(this.options.cwd?.toString() || '.', 'package.json'); 132 const pkg = await JsonFile.readAsync<Record<typeof packageType, { [pkgName: string]: string }>>( 133 pkgPath 134 ); 135 136 packageSpecs.forEach((spec) => { 137 pkg[packageType] = pkg[packageType] || {}; 138 pkg[packageType][spec.name!] = spec.rawSpec; 139 }); 140 141 await JsonFile.writeAsync(pkgPath, pkg, { json5: false }); 142 } 143} 144