import type {PackageInstaller, InstallerOptions} from '@atlaspack/types'; import path from 'path'; import fs from 'fs'; // @ts-expect-error TS7016 import commandExists from 'command-exists'; // @ts-expect-error TS7016 import spawn from 'cross-spawn'; import {registerSerializableClass} from '@atlaspack/build-cache'; import logger from '@atlaspack/logger'; // @ts-expect-error TS7016 import split from 'split2'; import JSONParseStream from './JSONParseStream'; import promiseFromProcess from './promiseFromProcess'; import {exec, npmSpecifierFromModuleRequest} from './utils'; import pkg from '../package.json'; const PNPM_CMD = 'pnpm'; type LogLevel = 'error' | 'warn' | 'info' | 'debug'; type ErrorLog = { err: { message: string; code: string; stack: string; }; }; type PNPMLog = | { readonly name: 'pnpm:progress'; packageId: string; status: 'fetched' | 'found_in_store' | 'resolved'; } | { readonly name: 'pnpm:root'; added?: { id?: string; name: string; realName: string; version?: string; dependencyType?: 'prod' | 'dev' | 'optional'; latest?: string; linkedFrom?: string; }; removed?: { name: string; version?: string; dependencyType?: 'prod' | 'dev' | 'optional'; }; } | { readonly name: 'pnpm:importing'; from: string; method: string; to: string; } | { readonly name: 'pnpm:link'; target: string; link: string; } | { readonly name: 'pnpm:stats'; prefix: string; removed?: number; added?: number; }; type PNPMResults = { level: LogLevel; prefix?: string; message?: string; } & ErrorLog & PNPMLog; let hasPnpm: boolean | null | undefined; let pnpmVersion: number | null | undefined; export class Pnpm implements PackageInstaller { static async exists(): Promise { if (hasPnpm != null) { return hasPnpm; } try { hasPnpm = Boolean(await commandExists('pnpm')); } catch (err: any) { hasPnpm = false; } return hasPnpm; } async install({ modules, cwd, saveDev = true, }: InstallerOptions): Promise { if (pnpmVersion == null) { let version = await exec('pnpm --version'); // @ts-expect-error TS2345 pnpmVersion = parseInt(version.stdout, 10); } let args = ['add', '--reporter', 'ndjson']; if (saveDev) { args.push('-D'); } if (pnpmVersion >= 7) { if (fs.existsSync(path.join(cwd, 'pnpm-workspace.yaml'))) { // installs in workspace root (regardless of cwd) args.push('-w'); } } else { // ignores workspace root check args.push('-W'); } args = args.concat(modules.map(npmSpecifierFromModuleRequest)); let env: Record = {}; for (let key in process.env) { if (!key.startsWith('npm_') && key !== 'INIT_CWD' && key !== 'NODE_ENV') { env[key] = process.env[key]; } } let addedCount = 0, removedCount = 0; let installProcess = spawn(PNPM_CMD, args, { cwd, env, }); installProcess.stdout .pipe(split()) // @ts-expect-error TS2554 .pipe(new JSONParseStream()) // @ts-expect-error TS7006 .on('error', (e) => { logger.warn({ origin: '@atlaspack/package-manager', message: e.chunk, stack: e.stack, }); }) .on('data', (json: PNPMResults) => { if (json.level === 'error') { logger.error({ origin: '@atlaspack/package-manager', message: json.err.message, stack: json.err.stack, }); } else if (json.level === 'info' && typeof json.message === 'string') { logger.info({ origin: '@atlaspack/package-manager', message: prefix(json.message), }); } else if (json.name === 'pnpm:stats') { addedCount += json.added ?? 0; removedCount += json.removed ?? 0; } }); let stderr: Array = []; installProcess.stderr // @ts-expect-error TS7006 .on('data', (str) => { stderr.push(str.toString()); }) // @ts-expect-error TS7006 .on('error', (e) => { logger.warn({ origin: '@atlaspack/package-manager', message: e.message, }); }); try { await promiseFromProcess(installProcess); if (addedCount > 0 || removedCount > 0) { logger.log({ origin: '@atlaspack/package-manager', message: `Added ${addedCount} ${ removedCount > 0 ? `and removed ${removedCount} ` : '' }packages via pnpm`, }); } // Since we succeeded, stderr might have useful information not included // in the json written to stdout. It's also not necessary to log these as // errors as they often aren't. for (let message of stderr) { logger.log({ origin: '@atlaspack/package-manager', message, }); } } catch (e: any) { throw new Error('pnpm failed to install modules'); } } } function prefix(message: string): string { return 'pnpm: ' + message; } registerSerializableClass(`${pkg.version}:Pnpm`, Pnpm);