/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import { spawn, spawnSync } from 'child_process'; import { existsSync, mkdtempSync, readFileSync, realpathSync, rmdirSync, writeFileSync } from 'fs'; import { tmpdir } from 'os'; import { join, resolve } from 'path'; import { PackageManager } from '../lib/config/workspace-schema'; import { NgAddSaveDepedency } from '../utilities/package-metadata'; import { Spinner } from './spinner'; interface PackageManagerOptions { silent: string; saveDev: string; install: string; installAll?: string; prefix: string; noLockfile: string; } export async function installAllPackages( packageManager: PackageManager = PackageManager.Npm, extraArgs: string[] = [], cwd = process.cwd(), ): Promise<1 | 0> { const packageManagerArgs = getPackageManagerArguments(packageManager); const installArgs: string[] = []; if (packageManagerArgs.installAll) { installArgs.push(packageManagerArgs.installAll); } installArgs.push(packageManagerArgs.silent); const spinner = new Spinner(); spinner.start('Installing packages...'); const bufferedOutput: { stream: NodeJS.WriteStream; data: Buffer }[] = []; return new Promise((resolve, reject) => { const childProcess = spawn(packageManager, [...installArgs, ...extraArgs], { stdio: 'pipe', shell: true, cwd, }).on('close', (code: number) => { if (code === 0) { spinner.succeed('Packages successfully installed.'); resolve(0); } else { spinner.stop(); bufferedOutput.forEach(({ stream, data }) => stream.write(data)); spinner.fail('Package install failed, see above.'); reject(1); } }); childProcess.stdout?.on('data', (data: Buffer) => bufferedOutput.push({ stream: process.stdout, data: data }), ); childProcess.stderr?.on('data', (data: Buffer) => bufferedOutput.push({ stream: process.stderr, data: data }), ); }); } export async function installPackage( packageName: string, packageManager: PackageManager = PackageManager.Npm, save: Exclude = true, extraArgs: string[] = [], cwd = process.cwd(), ): Promise<1 | 0> { const packageManagerArgs = getPackageManagerArguments(packageManager); const installArgs: string[] = [ packageManagerArgs.install, packageName, packageManagerArgs.silent, ]; const spinner = new Spinner(); spinner.start('Installing package...'); if (save === 'devDependencies') { installArgs.push(packageManagerArgs.saveDev); } const bufferedOutput: { stream: NodeJS.WriteStream; data: Buffer }[] = []; return new Promise((resolve, reject) => { const childProcess = spawn(packageManager, [...installArgs, ...extraArgs], { stdio: 'pipe', shell: true, cwd, }).on('close', (code: number) => { if (code === 0) { spinner.succeed('Package successfully installed.'); resolve(0); } else { spinner.stop(); bufferedOutput.forEach(({ stream, data }) => stream.write(data)); spinner.fail('Package install failed, see above.'); reject(1); } }); childProcess.stdout?.on('data', (data: Buffer) => bufferedOutput.push({ stream: process.stdout, data: data }), ); childProcess.stderr?.on('data', (data: Buffer) => bufferedOutput.push({ stream: process.stderr, data: data }), ); }); } export async function installTempPackage( packageName: string, packageManager: PackageManager = PackageManager.Npm, extraArgs?: string[], ): Promise<{ status: 1 | 0; tempNodeModules: string; }> { const tempPath = mkdtempSync(join(realpathSync(tmpdir()), 'angular-cli-packages-')); // clean up temp directory on process exit process.on('exit', () => { try { rmdirSync(tempPath, { recursive: true, maxRetries: 3 }); } catch {} }); // NPM will warn when a `package.json` is not found in the install directory // Example: // npm WARN enoent ENOENT: no such file or directory, open '/tmp/.ng-temp-packages-84Qi7y/package.json' // npm WARN .ng-temp-packages-84Qi7y No description // npm WARN .ng-temp-packages-84Qi7y No repository field. // npm WARN .ng-temp-packages-84Qi7y No license field. // While we can use `npm init -y` we will end up needing to update the 'package.json' anyways // because of missing fields. writeFileSync( join(tempPath, 'package.json'), JSON.stringify({ name: 'temp-cli-install', description: 'temp-cli-install', repository: 'temp-cli-install', license: 'MIT', }), ); // setup prefix/global modules path const packageManagerArgs = getPackageManagerArguments(packageManager); const tempNodeModules = join(tempPath, 'node_modules'); // Yarn will not append 'node_modules' to the path const prefixPath = packageManager === PackageManager.Yarn ? tempNodeModules : tempPath; const installArgs: string[] = [ ...(extraArgs || []), `${packageManagerArgs.prefix}="${prefixPath}"`, packageManagerArgs.noLockfile, ]; return { status: await installPackage(packageName, packageManager, true, installArgs, tempPath), tempNodeModules, }; } export async function runTempPackageBin( packageName: string, packageManager: PackageManager = PackageManager.Npm, args: string[] = [], ): Promise { const { status: code, tempNodeModules } = await installTempPackage(packageName, packageManager); if (code !== 0) { return code; } // Remove version/tag etc... from package name // Ex: @angular/cli@latest -> @angular/cli const packageNameNoVersion = packageName.substring(0, packageName.lastIndexOf('@')); const pkgLocation = join(tempNodeModules, packageNameNoVersion); const packageJsonPath = join(pkgLocation, 'package.json'); // Get a binary location for this package let binPath: string | undefined; if (existsSync(packageJsonPath)) { const content = readFileSync(packageJsonPath, 'utf-8'); if (content) { const { bin = {} } = JSON.parse(content); const binKeys = Object.keys(bin); if (binKeys.length) { binPath = resolve(pkgLocation, bin[binKeys[0]]); } } } if (!binPath) { throw new Error(`Cannot locate bin for temporary package: ${packageNameNoVersion}.`); } const { status, error } = spawnSync(process.execPath, [binPath, ...args], { stdio: 'inherit', env: { ...process.env, NG_DISABLE_VERSION_CHECK: 'true', NG_CLI_ANALYTICS: 'false', }, }); if (status === null && error) { throw error; } return status || 0; } function getPackageManagerArguments(packageManager: PackageManager): PackageManagerOptions { switch (packageManager) { case PackageManager.Yarn: return { silent: '--silent', saveDev: '--dev', install: 'add', prefix: '--modules-folder', noLockfile: '--no-lockfile', }; case PackageManager.Pnpm: return { silent: '--silent', saveDev: '--save-dev', install: 'add', installAll: 'install', prefix: '--prefix', noLockfile: '--no-lockfile', }; default: return { silent: '--quiet', saveDev: '--save-dev', install: 'install', installAll: 'install', prefix: '--prefix', noLockfile: '--no-package-lock', }; } }