diff --git a/.changeset/wise-meals-smile.md b/.changeset/wise-meals-smile.md new file mode 100644 index 0000000000..9f61a5ae71 --- /dev/null +++ b/.changeset/wise-meals-smile.md @@ -0,0 +1,6 @@ +--- +"@pnpm/plugin-commands-env": minor +"pnpm": minor +--- + +Allow `env rm` to remove multiple node versions at once, and introduce `env add` for installing node versions without setting as default [#7155](https://github.com/pnpm/pnpm/pull/7155). diff --git a/env/plugin-commands-env/src/downloadNodeVersion.ts b/env/plugin-commands-env/src/downloadNodeVersion.ts new file mode 100644 index 0000000000..373f34b2b8 --- /dev/null +++ b/env/plugin-commands-env/src/downloadNodeVersion.ts @@ -0,0 +1,30 @@ +import { resolveNodeVersion } from '@pnpm/node.resolver' +import { getNodeMirror } from './getNodeMirror' +import { getNodeDir, type NvmNodeCommandOptions } from './node' +import { parseEnvSpecifier } from './parseEnvSpecifier' +import { createFetchFromRegistry } from '@pnpm/fetch' +import { globalInfo } from '@pnpm/logger' + +export async function getNodeVersion (opts: NvmNodeCommandOptions, envSpecifier: string) { + const fetch = createFetchFromRegistry(opts) + const { releaseChannel, versionSpecifier } = parseEnvSpecifier(envSpecifier) + const nodeMirrorBaseUrl = getNodeMirror(opts.rawConfig, releaseChannel) + const nodeVersion = await resolveNodeVersion(fetch, versionSpecifier, nodeMirrorBaseUrl) + return { nodeVersion, nodeMirrorBaseUrl, releaseChannel, versionSpecifier } +} + +export async function downloadNodeVersion (opts: NvmNodeCommandOptions, envSpecifier: string) { + const fetch = createFetchFromRegistry(opts) + const { nodeVersion, nodeMirrorBaseUrl } = await getNodeVersion(opts, envSpecifier) + if (!nodeVersion) { + return null + } + const nodeDir = await getNodeDir(fetch, { + ...opts, + useNodeVersion: nodeVersion, + nodeMirrorBaseUrl, + }) + globalInfo(`Node.js ${nodeVersion as string} was installed + ${nodeDir}`) + return { nodeVersion, nodeDir, nodeMirrorBaseUrl } +} diff --git a/env/plugin-commands-env/src/env.ts b/env/plugin-commands-env/src/env.ts index f26ef91f27..6d5690671f 100644 --- a/env/plugin-commands-env/src/env.ts +++ b/env/plugin-commands-env/src/env.ts @@ -5,6 +5,7 @@ import { envRemove } from './envRemove' import { envUse } from './envUse' import { type NvmNodeCommandOptions } from './node' import { envList } from './envList' +import { envAdd } from './envAdd' export function rcOptionsTypes () { return {} @@ -27,11 +28,15 @@ export function help () { title: 'Commands', list: [ { - description: 'Installs the specified version of Node.js. The npm CLI bundled with the given Node.js version gets installed as well.', + description: 'Installs the specified version of Node.js. The npm CLI bundled with the given Node.js version gets installed as well. This sets this version of Node.js as the current version.', name: 'use', }, { - description: 'Removes the specified version of Node.js.', + description: 'Installs the specified version(s) of Node.js without activating them as the current version.', + name: 'add', + }, + { + description: 'Removes the specified version(s) of Node.js.', name: 'remove', shortAlias: 'rm', }, @@ -59,17 +64,18 @@ export function help () { ], url: docsUrl('env'), usages: [ - 'pnpm env [command] [options] ', + 'pnpm env [command] [options] [...]', 'pnpm env use --global 16', 'pnpm env use --global lts', 'pnpm env use --global argon', 'pnpm env use --global latest', 'pnpm env use --global rc/16', - 'pnpm env remove --global 16', - 'pnpm env remove --global lts', + 'pnpm env add --global 16', + 'pnpm env add --global 18 19 20.6.0', + 'pnpm env remove --global 16 lts', 'pnpm env remove --global argon', 'pnpm env remove --global latest', - 'pnpm env remove --global rc/16', + 'pnpm env remove --global rc/16 16 20.6.0', 'pnpm env list', 'pnpm env list --remote', 'pnpm env list --remote 16', @@ -93,6 +99,9 @@ export async function handler (opts: NvmNodeCommandOptions, params: string[]) { }) } switch (params[0]) { + case 'add': { + return envAdd(opts, params.slice(1)) + } case 'use': { return envUse(opts, params.slice(1)) } diff --git a/env/plugin-commands-env/src/envAdd.ts b/env/plugin-commands-env/src/envAdd.ts new file mode 100644 index 0000000000..87c01fb12a --- /dev/null +++ b/env/plugin-commands-env/src/envAdd.ts @@ -0,0 +1,21 @@ +/* eslint-disable no-await-in-loop */ +import { PnpmError } from '@pnpm/error' +import { type NvmNodeCommandOptions } from './node' +import { downloadNodeVersion } from './downloadNodeVersion' + +export async function envAdd (opts: NvmNodeCommandOptions, params: string[]) { + if (!opts.global) { + throw new PnpmError('NOT_IMPLEMENTED_YET', '"pnpm env use " can only be used with the "--global" option currently') + } + const failed: string[] = [] + for (const envSpecifier of params) { + const result = await downloadNodeVersion(opts, envSpecifier) + if (!result) { + failed.push(envSpecifier) + } + } + if (failed.length > 0) { + throw new PnpmError('COULD_NOT_RESOLVE_NODEJS', `Couldn't find Node.js version matching ${failed.join(', ')}`) + } + return 'All specified Node.js versions were installed' +} diff --git a/env/plugin-commands-env/src/envRemove.ts b/env/plugin-commands-env/src/envRemove.ts index 2d17dade89..e65edd5750 100644 --- a/env/plugin-commands-env/src/envRemove.ts +++ b/env/plugin-commands-env/src/envRemove.ts @@ -1,35 +1,42 @@ +/* eslint-disable no-await-in-loop */ import { existsSync } from 'fs' import path from 'path' import { PnpmError } from '@pnpm/error' -import { createFetchFromRegistry } from '@pnpm/fetch' -import { globalInfo } from '@pnpm/logger' -import { resolveNodeVersion } from '@pnpm/node.resolver' +import { logger, globalInfo } from '@pnpm/logger' import { removeBin } from '@pnpm/remove-bins' import rimraf from '@zkochan/rimraf' -import { parseEnvSpecifier } from './parseEnvSpecifier' import { getNodeExecPathAndTargetDir } from './utils' -import { getNodeMirror } from './getNodeMirror' import { getNodeVersionsBaseDir, type NvmNodeCommandOptions } from './node' +import { getNodeVersion } from './downloadNodeVersion' export async function envRemove (opts: NvmNodeCommandOptions, params: string[]) { if (!opts.global) { throw new PnpmError('NOT_IMPLEMENTED_YET', '"pnpm env use " can only be used with the "--global" option currently') } - const fetch = createFetchFromRegistry(opts) - const { releaseChannel, versionSpecifier } = parseEnvSpecifier(params[0]) - const nodeMirrorBaseUrl = getNodeMirror(opts.rawConfig, releaseChannel) - const nodeVersion = await resolveNodeVersion(fetch, versionSpecifier, nodeMirrorBaseUrl) + let failed = false + for (const version of params) { + const err = await removeNodeVersion(opts, version) + if (err) { + logger.error(err) + failed = true + } + } + return { exitCode: failed ? 1 : 0 } +} + +async function removeNodeVersion (opts: NvmNodeCommandOptions, version: string): Promise { + const { nodeVersion } = await getNodeVersion(opts, version) const nodeDir = getNodeVersionsBaseDir(opts.pnpmHomeDir) if (!nodeVersion) { - throw new PnpmError('COULD_NOT_RESOLVE_NODEJS', `Couldn't find Node.js version matching ${params[0]}`) + return new PnpmError('COULD_NOT_RESOLVE_NODEJS', `Couldn't find Node.js version matching ${version}`) } const versionDir = path.resolve(nodeDir, nodeVersion) if (!existsSync(versionDir)) { - throw new PnpmError('ENV_NO_NODE_DIRECTORY', `Couldn't find Node.js directory in ${versionDir}`) + return new PnpmError('ENV_NO_NODE_DIRECTORY', `Couldn't find Node.js directory in ${versionDir}`) } const { nodePath, nodeLink } = await getNodeExecPathAndTargetDir(opts.pnpmHomeDir) @@ -47,12 +54,13 @@ export async function envRemove (opts: NvmNodeCommandOptions, params: string[]) removeBin(npxPath), ]) } catch (err: any) { // eslint-disable-line - if (err.code !== 'ENOENT') throw err + if (err.code !== 'ENOENT') return err } } await rimraf(versionDir) - return `Node.js ${nodeVersion as string} is removed -${versionDir}` + globalInfo(`Node.js ${nodeVersion as string} was removed +${versionDir}`) + return undefined } diff --git a/env/plugin-commands-env/src/envUse.ts b/env/plugin-commands-env/src/envUse.ts index 16ed2cf469..db73917901 100644 --- a/env/plugin-commands-env/src/envUse.ts +++ b/env/plugin-commands-env/src/envUse.ts @@ -2,32 +2,22 @@ import { promises as fs } from 'fs' import gfs from 'graceful-fs' import path from 'path' import { PnpmError } from '@pnpm/error' -import { createFetchFromRegistry } from '@pnpm/fetch' -import { resolveNodeVersion } from '@pnpm/node.resolver' import cmdShim from '@zkochan/cmd-shim' import isWindows from 'is-windows' import symlinkDir from 'symlink-dir' -import { getNodeDir, type NvmNodeCommandOptions } from './node' -import { getNodeMirror } from './getNodeMirror' -import { parseEnvSpecifier } from './parseEnvSpecifier' +import { type NvmNodeCommandOptions } from './node' import { CURRENT_NODE_DIRNAME, getNodeExecPathInBinDir, getNodeExecPathInNodeDir } from './utils' +import { downloadNodeVersion } from './downloadNodeVersion' export async function envUse (opts: NvmNodeCommandOptions, params: string[]) { if (!opts.global) { throw new PnpmError('NOT_IMPLEMENTED_YET', '"pnpm env use " can only be used with the "--global" option currently') } - const fetch = createFetchFromRegistry(opts) - const { releaseChannel, versionSpecifier } = parseEnvSpecifier(params[0]) - const nodeMirrorBaseUrl = getNodeMirror(opts.rawConfig, releaseChannel) - const nodeVersion = await resolveNodeVersion(fetch, versionSpecifier, nodeMirrorBaseUrl) - if (!nodeVersion) { + const nodeInfo = await downloadNodeVersion(opts, params[0]) + if (!nodeInfo) { throw new PnpmError('COULD_NOT_RESOLVE_NODEJS', `Couldn't find Node.js version matching ${params[0]}`) } - const nodeDir = await getNodeDir(fetch, { - ...opts, - useNodeVersion: nodeVersion, - nodeMirrorBaseUrl, - }) + const { nodeDir, nodeVersion } = nodeInfo const src = getNodeExecPathInNodeDir(nodeDir) const dest = getNodeExecPathInBinDir(opts.bin) await symlinkDir(nodeDir, path.join(opts.pnpmHomeDir, CURRENT_NODE_DIRNAME)) diff --git a/env/plugin-commands-env/test/env.test.ts b/env/plugin-commands-env/test/env.test.ts index 7c7ef24723..721efc5323 100644 --- a/env/plugin-commands-env/test/env.test.ts +++ b/env/plugin-commands-env/test/env.test.ts @@ -2,7 +2,7 @@ import fs from 'fs' import path from 'path' import { PnpmError } from '@pnpm/error' import { tempDir } from '@pnpm/prepare' -import { env, node } from '@pnpm/plugin-commands-env' +import { env } from '@pnpm/plugin-commands-env' import * as execa from 'execa' import nock from 'nock' import PATH from 'path-name' @@ -136,7 +136,7 @@ test('it re-attempts failed downloads', async () => { } }) -describe('env remove', () => { +describe('env add/remove', () => { test('fail if --global is missing', async () => { tempDir() @@ -160,14 +160,12 @@ describe('env remove', () => { pnpmHomeDir: process.cwd(), rawConfig: {}, }, ['rm', 'non-existing-version']) - ).rejects.toEqual(new PnpmError('COULD_NOT_RESOLVE_NODEJS', 'Couldn\'t find Node.js version matching non-existing-version')) + ).resolves.toEqual({ exitCode: 1 }) }) test('fail if trying to remove version that is not installed', async () => { tempDir() - const nodeDir = node.getNodeVersionsBaseDir(process.cwd()) - await expect( env.handler({ bin: process.cwd(), @@ -175,7 +173,7 @@ describe('env remove', () => { pnpmHomeDir: process.cwd(), rawConfig: {}, }, ['remove', '16.4.0']) - ).rejects.toEqual(new PnpmError('ENV_NO_NODE_DIRECTORY', `Couldn't find Node.js directory in ${path.resolve(nodeDir, '16.4.0')}`)) + ).resolves.toEqual({ exitCode: 1 }) }) test('install and remove Node.js by exact version', async () => { @@ -212,6 +210,48 @@ describe('env remove', () => { expect(() => execa.sync('node', ['-v'], opts)).toThrowError() }) + + test('install and remove multiple Node.js versions in one command', async () => { + tempDir() + + const configDir = path.resolve('config') + + await env.handler({ + bin: process.cwd(), + configDir, + global: true, + pnpmHomeDir: process.cwd(), + rawConfig: {}, + }, ['add', '16.4.0', '18.18.0']) + + { + const version = await env.handler({ + bin: process.cwd(), + configDir, + pnpmHomeDir: process.cwd(), + rawConfig: {}, + }, ['list']) + + expect((version as string).trim().replaceAll(/\s/g, '')).toMatch(/16\.4\.0.*18\.18\.0/) + } + await env.handler({ + bin: process.cwd(), + global: true, + pnpmHomeDir: process.cwd(), + rawConfig: {}, + }, ['rm', '16.4.0', '18.18.0']) + + { + const version = await env.handler({ + bin: process.cwd(), + configDir, + pnpmHomeDir: process.cwd(), + rawConfig: {}, + }, ['list']) + + expect(version).toMatch('') + } + }) }) describe('env list', () => { @@ -262,7 +302,7 @@ describe('env list', () => { remote: true, }, ['list', '16']) - const versions = versionStr.split('\n') + const versions = (versionStr as string).split('\n') expect(versions.every(version => semver.satisfies(version, '16'))).toBeTruthy() }) })