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:
Eric Kwoka
2023-10-05 04:13:46 +04:00
committed by GitHub
parent ee6e0734e9
commit 2e6915727d
7 changed files with 146 additions and 42 deletions

View 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).

View 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 }
}

View File

@@ -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
View 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'
}

View File

@@ -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
}

View File

@@ -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))

View File

@@ -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()
})
})