diff --git a/.changes/pnpm-support.md b/.changes/pnpm-support.md new file mode 100644 index 000000000..70e9ed886 --- /dev/null +++ b/.changes/pnpm-support.md @@ -0,0 +1,5 @@ +--- +"cli.js": patch +--- + +Adds `pnpm` support. diff --git a/tooling/cli.js/src/api/dependency-manager/managers/index.ts b/tooling/cli.js/src/api/dependency-manager/managers/index.ts new file mode 100644 index 000000000..318431a10 --- /dev/null +++ b/tooling/cli.js/src/api/dependency-manager/managers/index.ts @@ -0,0 +1,4 @@ +export * from './yarn-manager' +export * from './npm-manager' +export * from './pnpm-manager' +export * from './types' diff --git a/tooling/cli.js/src/api/dependency-manager/managers/npm-manager.ts b/tooling/cli.js/src/api/dependency-manager/managers/npm-manager.ts new file mode 100644 index 000000000..0b5de13e3 --- /dev/null +++ b/tooling/cli.js/src/api/dependency-manager/managers/npm-manager.ts @@ -0,0 +1,48 @@ +import { IManager } from './types' +import { sync as crossSpawnSync } from 'cross-spawn' +import { spawnSync } from '../../../helpers/spawn' +import { appDir } from '../../../helpers/app-paths' + +export class NpmManager implements IManager { + type = 'npm' + + installPackage(packageName: string): void { + spawnSync('npm', ['install', packageName], appDir) + } + + installDevPackage(packageName: string): void { + spawnSync('npm', ['install', packageName, '--save-dev'], appDir) + } + + updatePackage(packageName: string): void { + spawnSync('npm', ['install', `${packageName}@latest`], appDir) + } + + getPackageVersion(packageName: string): string | null { + const child = crossSpawnSync( + 'npm', + ['list', packageName, 'version', '--depth', '0'], + { + cwd: appDir + } + ) + + const output = String(child.output[1]) + // eslint-disable-next-line security/detect-non-literal-regexp + const matches = new RegExp(packageName + '@(\\S+)', 'g').exec(output) + + if (matches?.[1]) { + return matches[1] + } else { + return null + } + } + + getLatestVersion(packageName: string): string { + const child = crossSpawnSync('npm', ['show', packageName, 'version'], { + cwd: appDir + }) + + return String(child.output[1]).replace('\n', '') + } +} diff --git a/tooling/cli.js/src/api/dependency-manager/managers/pnpm-manager.ts b/tooling/cli.js/src/api/dependency-manager/managers/pnpm-manager.ts new file mode 100644 index 000000000..f8353ce07 --- /dev/null +++ b/tooling/cli.js/src/api/dependency-manager/managers/pnpm-manager.ts @@ -0,0 +1,48 @@ +import { IManager } from './types' +import { sync as crossSpawnSync } from 'cross-spawn' +import { spawnSync } from '../../../helpers/spawn' +import { appDir } from '../../../helpers/app-paths' + +export class PnpmManager implements IManager { + type = 'pnpm' + + installPackage(packageName: string): void { + spawnSync('pnpm', ['add', packageName], appDir) + } + + installDevPackage(packageName: string): void { + spawnSync('pnpm', ['add', packageName, '--save-dev'], appDir) + } + + updatePackage(packageName: string): void { + spawnSync('pnpm', ['add', `${packageName}@latest`], appDir) + } + + getPackageVersion(packageName: string): string | null { + const child = crossSpawnSync( + 'pnpm', + ['list', packageName, 'version', '--depth', '0'], + { + cwd: appDir + } + ) + + const output = String(child.output[1]) + // eslint-disable-next-line security/detect-non-literal-regexp + const matches = new RegExp(packageName + ' (\\S+)', 'g').exec(output) + + if (matches?.[1]) { + return matches[1] + } else { + return null + } + } + + getLatestVersion(packageName: string): string { + const child = crossSpawnSync('pnpm', ['info', packageName, 'version'], { + cwd: appDir + }) + + return String(child.output[1]).replace('\n', '') + } +} diff --git a/tooling/cli.js/src/api/dependency-manager/managers/types.ts b/tooling/cli.js/src/api/dependency-manager/managers/types.ts new file mode 100644 index 000000000..19289baf3 --- /dev/null +++ b/tooling/cli.js/src/api/dependency-manager/managers/types.ts @@ -0,0 +1,8 @@ +export interface IManager { + type: string + installPackage: (packageName: string) => void + installDevPackage: (packageName: string) => void + updatePackage: (packageName: string) => void + getPackageVersion: (packageName: string) => string | null + getLatestVersion: (packageName: string) => string +} diff --git a/tooling/cli.js/src/api/dependency-manager/managers/yarn-manager.ts b/tooling/cli.js/src/api/dependency-manager/managers/yarn-manager.ts new file mode 100644 index 000000000..e427fccb1 --- /dev/null +++ b/tooling/cli.js/src/api/dependency-manager/managers/yarn-manager.ts @@ -0,0 +1,48 @@ +import { IManager } from './types' +import { sync as crossSpawnSync } from 'cross-spawn' +import { spawnSync } from '../../../helpers/spawn' +import { appDir } from '../../../helpers/app-paths' + +export class YarnManager implements IManager { + type = 'yarn' + + installPackage(packageName: string): void { + spawnSync('yarn', ['add', packageName], appDir) + } + + installDevPackage(packageName: string): void { + spawnSync('yarn', ['add', packageName, '--dev'], appDir) + } + + updatePackage(packageName: string): void { + spawnSync('yarn', ['upgrade', packageName, '--latest'], appDir) + } + + getPackageVersion(packageName: string): string | null { + const child = crossSpawnSync( + 'yarn', + ['list', '--pattern', packageName, '--depth', '0'], + { cwd: appDir } + ) + + const output = String(child.output[1]) + // eslint-disable-next-line security/detect-non-literal-regexp + const matches = new RegExp(packageName + '@(\\S+)', 'g').exec(output) + if (matches?.[1]) { + return matches[1] + } else { + return null + } + } + + getLatestVersion(packageName: string): string { + const child = crossSpawnSync( + 'yarn', + ['info', packageName, 'versions', '--json'], + { cwd: appDir } + ) + const output = String(child.output[1]) + const packageJson = JSON.parse(output) as { data: string[] } + return packageJson.data[packageJson.data.length - 1] + } +} diff --git a/tooling/cli.js/src/api/dependency-manager/npm-packages.ts b/tooling/cli.js/src/api/dependency-manager/npm-packages.ts index 9e5cac94a..88e20d2a5 100644 --- a/tooling/cli.js/src/api/dependency-manager/npm-packages.ts +++ b/tooling/cli.js/src/api/dependency-manager/npm-packages.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -import { ManagementType, Result } from './types' +import { Answer, ManagementType, Result } from './types' import { getNpmLatestVersion, getNpmPackageVersion, @@ -10,7 +10,7 @@ import { installNpmDevPackage, updateNpmPackage, semverLt, - useYarn + getManager } from './util' import logger from '../../helpers/logger' import { resolve } from '../../helpers/app-paths' @@ -29,37 +29,42 @@ async function manageDependencies( const npmChild = crossSpawnSync('npm', ['--version']) const yarnChild = crossSpawnSync('yarn', ['--version']) + const pnpmChild = crossSpawnSync('pnpm', ['--version']) if ( (npmChild.status ?? npmChild.error) && - (yarnChild.status ?? yarnChild.error) + (yarnChild.status ?? yarnChild.error) && + (pnpmChild.status ?? pnpmChild.error) ) { throw new Error( - 'must have `npm` or `yarn` installed to manage dependenices' + 'must have installed one of the following package managers `npm`, `yarn`, `pnpm` to manage dependenices' ) } if (existsSync(resolve.app('package.json'))) { for (const dependency of dependencies) { const currentVersion = getNpmPackageVersion(dependency) + const packageManager = getManager().type.toUpperCase() + if (currentVersion === null) { log(`Installing ${dependency}...`) if ( managementType === ManagementType.Install || managementType === ManagementType.InstallDev ) { - const packageManager = useYarn() ? 'YARN' : 'NPM' - const inquired = (await inquirer.prompt([ + const prefix = + managementType === ManagementType.InstallDev + ? ' as dev-dependency' + : '' + + const inquired = await inquirer.prompt([ { type: 'confirm', name: 'answer', - message: `[${packageManager}]: "Do you want to install ${dependency} ${ - managementType === ManagementType.InstallDev - ? 'as dev-dependency' - : '' - }?"`, + message: `[${packageManager}]: "Do you want to install ${dependency}${prefix}?"`, default: false } - ])) as { answer: boolean } + ]) + if (inquired.answer) { if (managementType === ManagementType.Install) { installNpmPackage(dependency) @@ -71,15 +76,17 @@ async function manageDependencies( } } else if (managementType === ManagementType.Update) { const latestVersion = getNpmLatestVersion(dependency) + if (semverLt(currentVersion, latestVersion)) { - const inquired = (await inquirer.prompt([ + const inquired = await inquirer.prompt([ { type: 'confirm', name: 'answer', - message: `[NPM]: "${dependency}" latest version is ${latestVersion}. Do you want to update?`, + message: `[${packageManager}]: "${dependency}" latest version is ${latestVersion}. Do you want to update?`, default: false } - ])) as { answer: boolean } + ]) + if (inquired.answer) { log(`Updating ${dependency}...`) updateNpmPackage(dependency) diff --git a/tooling/cli.js/src/api/dependency-manager/types.ts b/tooling/cli.js/src/api/dependency-manager/types.ts index 15a525bae..07f6e8bb4 100644 --- a/tooling/cli.js/src/api/dependency-manager/types.ts +++ b/tooling/cli.js/src/api/dependency-manager/types.ts @@ -9,3 +9,7 @@ export enum ManagementType { } export type Result = Map + +export interface Answer { + answer: boolean +} diff --git a/tooling/cli.js/src/api/dependency-manager/util.ts b/tooling/cli.js/src/api/dependency-manager/util.ts index 77b5a3741..5500f07e1 100644 --- a/tooling/cli.js/src/api/dependency-manager/util.ts +++ b/tooling/cli.js/src/api/dependency-manager/util.ts @@ -2,16 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -import { spawnSync } from '../../helpers/spawn' import { sync as crossSpawnSync } from 'cross-spawn' -import { appDir, resolve as appResolve } from '../../helpers/app-paths' +import { resolve as appResolve } from '../../helpers/app-paths' import { existsSync } from 'fs' import semver from 'semver' +import { IManager, NpmManager, YarnManager, PnpmManager } from './managers' -const useYarn = (): boolean => - process.env.npm_execpath - ? process.env.npm_execpath.includes('yarn') - : existsSync(appResolve.app('yarn.lock')) +const getManager = (): IManager => { + if (existsSync(appResolve.app('yarn.lock'))) { + return new YarnManager() + } else if (existsSync(appResolve.app('pnpm-lock.yaml'))) { + return new PnpmManager() + } else { + return new NpmManager() + } +} function getCrateLatestVersion(crateName: string): string | null { const child = crossSpawnSync('cargo', ['search', crateName, '--limit', '1']) @@ -26,69 +31,23 @@ function getCrateLatestVersion(crateName: string): string | null { } function getNpmLatestVersion(packageName: string): string { - if (useYarn()) { - const child = crossSpawnSync( - 'yarn', - ['info', packageName, 'versions', '--json'], - { - cwd: appDir - } - ) - const output = String(child.output[1]) - const packageJson = JSON.parse(output) as { data: string[] } - return packageJson.data[packageJson.data.length - 1] - } else { - const child = crossSpawnSync('npm', ['show', packageName, 'version'], { - cwd: appDir - }) - return String(child.output[1]).replace('\n', '') - } + return getManager().getLatestVersion(packageName) } function getNpmPackageVersion(packageName: string): string | null { - const child = useYarn() - ? crossSpawnSync( - 'yarn', - ['list', '--pattern', packageName, '--depth', '0'], - { - cwd: appDir - } - ) - : crossSpawnSync('npm', ['list', packageName, 'version', '--depth', '0'], { - cwd: appDir - }) - const output = String(child.output[1]) - // eslint-disable-next-line security/detect-non-literal-regexp - const matches = new RegExp(packageName + '@(\\S+)', 'g').exec(output) - if (matches?.[1]) { - return matches[1] - } else { - return null - } + return getManager().getPackageVersion(packageName) } function installNpmPackage(packageName: string): void { - if (useYarn()) { - spawnSync('yarn', ['add', packageName], appDir) - } else { - spawnSync('npm', ['install', packageName], appDir) - } + return getManager().installPackage(packageName) } function installNpmDevPackage(packageName: string): void { - if (useYarn()) { - spawnSync('yarn', ['add', packageName, '--dev'], appDir) - } else { - spawnSync('npm', ['install', packageName, '--save-dev'], appDir) - } + return getManager().installDevPackage(packageName) } function updateNpmPackage(packageName: string): void { - if (useYarn()) { - spawnSync('yarn', ['upgrade', packageName, '--latest'], appDir) - } else { - spawnSync('npm', ['install', `${packageName}@latest`], appDir) - } + return getManager().updatePackage(packageName) } function padVersion(version: string): string { @@ -105,7 +64,7 @@ function semverLt(first: string, second: string): boolean { } export { - useYarn, + getManager, getCrateLatestVersion, getNpmLatestVersion, getNpmPackageVersion,