mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-25 15:07:06 -04:00
feat(env): allow installing and removing multiple NodeJS versions at once (#7155)
--------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
6
.changeset/wise-meals-smile.md
Normal file
6
.changeset/wise-meals-smile.md
Normal file
@@ -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).
|
||||
30
env/plugin-commands-env/src/downloadNodeVersion.ts
vendored
Normal file
30
env/plugin-commands-env/src/downloadNodeVersion.ts
vendored
Normal file
@@ -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 }
|
||||
}
|
||||
21
env/plugin-commands-env/src/env.ts
vendored
21
env/plugin-commands-env/src/env.ts
vendored
@@ -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] <version>',
|
||||
'pnpm env [command] [options] <version> [<additional-versions>...]',
|
||||
'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))
|
||||
}
|
||||
|
||||
21
env/plugin-commands-env/src/envAdd.ts
vendored
Normal file
21
env/plugin-commands-env/src/envAdd.ts
vendored
Normal file
@@ -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 <version>" 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'
|
||||
}
|
||||
36
env/plugin-commands-env/src/envRemove.ts
vendored
36
env/plugin-commands-env/src/envRemove.ts
vendored
@@ -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 <version>" 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<Error | undefined> {
|
||||
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
|
||||
}
|
||||
|
||||
20
env/plugin-commands-env/src/envUse.ts
vendored
20
env/plugin-commands-env/src/envUse.ts
vendored
@@ -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 <version>" 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))
|
||||
|
||||
54
env/plugin-commands-env/test/env.test.ts
vendored
54
env/plugin-commands-env/test/env.test.ts
vendored
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user