mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-25 15:07:06 -04:00
feat(env): add remove command to pnpm env (#5263)
* feat(env): add uninstall command to pnpm env * chore(changeset): define minor bump * fix(env): fixes for pnpm env uninstall - Fix broken node path for the windows platform - Notify user when removing default Node.JS - Remove `await` on `fs.unlink` in `Promise.all` - Add support for Node.JS v14.13 and earlier * fix(env): fix failing tests on Windows platform * fix(env): update aliases and error catching - Add `rm, un, uninstall` aliases to the `remove` command - Update CLI help message - Ignore only `ENOENT` errors * refactor(env): cr updates - Use `remove` with `rm` alias only - Update CLI help message - Update tests to use `remove` command - Replace custom support for older Node.JS versions with `@zkochan/rimraf` * refactor(env): simplify catch block * fix: env rm * refactor: test Co-authored-by: Mark Omarov <inkfaust@gmail.com> Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
6
.changeset/long-trainers-share.md
Normal file
6
.changeset/long-trainers-share.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-env": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Enhance `pnpm env` with the `remove` command.
|
||||
@@ -37,8 +37,10 @@
|
||||
"@pnpm/logger": "^4.0.0",
|
||||
"@pnpm/node.fetcher": "workspace:*",
|
||||
"@pnpm/node.resolver": "workspace:*",
|
||||
"@pnpm/remove-bins": "workspace:*",
|
||||
"@pnpm/store-path": "workspace:*",
|
||||
"@zkochan/cmd-shim": "^5.3.1",
|
||||
"@zkochan/rimraf": "^2.1.2",
|
||||
"load-json-file": "^6.2.0",
|
||||
"render-help": "^1.0.2",
|
||||
"write-json-file": "^4.3.0"
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import { promises as fs, existsSync } from 'fs'
|
||||
import path from 'path'
|
||||
import { docsUrl } from '@pnpm/cli-utils'
|
||||
import PnpmError from '@pnpm/error'
|
||||
import { createFetchFromRegistry } from '@pnpm/fetch'
|
||||
import { resolveNodeVersion } from '@pnpm/node.resolver'
|
||||
import { globalInfo } from '@pnpm/logger'
|
||||
import { removeBin } from '@pnpm/remove-bins'
|
||||
import cmdShim from '@zkochan/cmd-shim'
|
||||
import rimraf from '@zkochan/rimraf'
|
||||
import renderHelp from 'render-help'
|
||||
import { getNodeDir, NvmNodeCommandOptions } from './node'
|
||||
import { getNodeDir, NvmNodeCommandOptions, getNodeVersionsBaseDir } from './node'
|
||||
import getNodeMirror from './getNodeMirror'
|
||||
import { parseNodeEditionSpecifier } from './parseNodeEditionSpecifier'
|
||||
|
||||
@@ -24,14 +27,27 @@ export const commandNames = ['env']
|
||||
|
||||
export function help () {
|
||||
return renderHelp({
|
||||
description: 'Install and use the specified version of Node.js. The npm CLI bundled with the given Node.js version gets installed as well.',
|
||||
description: 'Manage Node.js versions.',
|
||||
descriptionLists: [
|
||||
{
|
||||
title: 'Options',
|
||||
|
||||
title: 'Commands',
|
||||
list: [
|
||||
{
|
||||
description: 'Installs Node.js globally',
|
||||
description: 'Installs the specified version of Node.JS. The npm CLI bundled with the given Node.js version gets installed as well.',
|
||||
name: 'use',
|
||||
},
|
||||
{
|
||||
description: 'Removes the specified version of Node.JS.',
|
||||
name: 'remove',
|
||||
shortAlias: 'rm',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Options',
|
||||
list: [
|
||||
{
|
||||
description: 'Manages Node.js versions globally',
|
||||
name: '--global',
|
||||
shortAlias: '-g',
|
||||
},
|
||||
@@ -40,12 +56,17 @@ export function help () {
|
||||
],
|
||||
url: docsUrl('env'),
|
||||
usages: [
|
||||
'pnpm env use --global <version>',
|
||||
'pnpm env [command] [options] <version>',
|
||||
'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 remove --global argon',
|
||||
'pnpm env remove --global latest',
|
||||
'pnpm env remove --global rc/16',
|
||||
],
|
||||
})
|
||||
}
|
||||
@@ -98,6 +119,60 @@ export async function handler (opts: NvmNodeCommandOptions, params: string[]) {
|
||||
return `Node.js ${nodeVersion as string} is activated
|
||||
${dest} -> ${src}`
|
||||
}
|
||||
case 'remove':
|
||||
case 'rm':
|
||||
case 'uninstall':
|
||||
case 'un': {
|
||||
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 } = parseNodeEditionSpecifier(params[1])
|
||||
const nodeMirrorBaseUrl = getNodeMirror(opts.rawConfig, releaseChannel)
|
||||
const nodeVersion = await resolveNodeVersion(fetch, versionSpecifier, nodeMirrorBaseUrl)
|
||||
const nodeDir = getNodeVersionsBaseDir(opts.pnpmHomeDir)
|
||||
|
||||
if (!nodeVersion) {
|
||||
throw new PnpmError('COULD_NOT_RESOLVE_NODEJS', `Couldn't find Node.js version matching ${params[1]}`)
|
||||
}
|
||||
|
||||
const versionDir = path.resolve(nodeDir, nodeVersion)
|
||||
|
||||
if (!existsSync(versionDir)) {
|
||||
throw new PnpmError('ENV_NO_NODE_DIRECTORY', `Couldn't find Node.js directory in ${versionDir}`)
|
||||
}
|
||||
|
||||
const nodePath = path.resolve(opts.pnpmHomeDir, process.platform === 'win32' ? 'node.exe' : 'node')
|
||||
let nodeLink: string | undefined
|
||||
try {
|
||||
nodeLink = await fs.readlink(nodePath)
|
||||
} catch (err) {
|
||||
nodeLink = undefined
|
||||
}
|
||||
|
||||
if (nodeLink?.includes(versionDir)) {
|
||||
globalInfo(`Node.JS version ${nodeVersion} was detected as the default one, removing ...`)
|
||||
|
||||
const npmPath = path.resolve(opts.pnpmHomeDir, 'npm')
|
||||
const npxPath = path.resolve(opts.pnpmHomeDir, 'npx')
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
removeBin(nodePath),
|
||||
removeBin(npmPath),
|
||||
removeBin(npxPath),
|
||||
])
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
if (err.code !== 'ENOENT') throw err
|
||||
}
|
||||
}
|
||||
|
||||
await rimraf(versionDir)
|
||||
|
||||
return `Node.js ${nodeVersion} is removed
|
||||
${versionDir}`
|
||||
}
|
||||
default: {
|
||||
throw new PnpmError('ENV_UNKNOWN_SUBCOMMAND', 'This subcommand is not known')
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function getNodeBinDir (opts: NvmNodeCommandOptions) {
|
||||
return process.platform === 'win32' ? nodeDir : path.join(nodeDir, 'bin')
|
||||
}
|
||||
|
||||
function getNodeVersionsBaseDir (pnpmHomeDir: string) {
|
||||
export function getNodeVersionsBaseDir (pnpmHomeDir: string) {
|
||||
return path.join(pnpmHomeDir, 'nodejs')
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
import PnpmError from '@pnpm/error'
|
||||
import { tempDir } from '@pnpm/prepare'
|
||||
import { env } from '@pnpm/plugin-commands-env'
|
||||
import { env, node } from '@pnpm/plugin-commands-env'
|
||||
import * as execa from 'execa'
|
||||
import nock from 'nock'
|
||||
import PATH from 'path-name'
|
||||
@@ -133,3 +133,81 @@ test('it re-attempts failed downloads', async () => {
|
||||
nock.cleanAll()
|
||||
}
|
||||
})
|
||||
|
||||
describe('env remove', () => {
|
||||
test('fail if --global is missing', async () => {
|
||||
tempDir()
|
||||
|
||||
await expect(
|
||||
env.handler({
|
||||
bin: process.cwd(),
|
||||
global: false,
|
||||
pnpmHomeDir: process.cwd(),
|
||||
rawConfig: {},
|
||||
}, ['remove', 'lts'])
|
||||
).rejects.toEqual(new PnpmError('NOT_IMPLEMENTED_YET', '"pnpm env use <version>" can only be used with the "--global" option currently'))
|
||||
})
|
||||
|
||||
test('fail if can not resolve Node.js version', async () => {
|
||||
tempDir()
|
||||
|
||||
await expect(
|
||||
env.handler({
|
||||
bin: process.cwd(),
|
||||
global: true,
|
||||
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'))
|
||||
})
|
||||
|
||||
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(),
|
||||
global: true,
|
||||
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')}`))
|
||||
})
|
||||
|
||||
test('install and remove Node.js by exact version', async () => {
|
||||
tempDir()
|
||||
|
||||
const configDir = path.resolve('config')
|
||||
|
||||
await env.handler({
|
||||
bin: process.cwd(),
|
||||
configDir,
|
||||
global: true,
|
||||
pnpmHomeDir: process.cwd(),
|
||||
rawConfig: {},
|
||||
}, ['use', '16.4.0'])
|
||||
|
||||
const opts = {
|
||||
env: {
|
||||
[PATH]: process.cwd(),
|
||||
},
|
||||
extendEnv: false,
|
||||
}
|
||||
|
||||
{
|
||||
const { stdout } = execa.sync('node', ['-v'], opts)
|
||||
expect(stdout.toString()).toBe('v16.4.0')
|
||||
}
|
||||
|
||||
await env.handler({
|
||||
bin: process.cwd(),
|
||||
global: true,
|
||||
pnpmHomeDir: process.cwd(),
|
||||
rawConfig: {},
|
||||
}, ['rm', '16.4.0'])
|
||||
|
||||
expect(() => execa.sync('node', ['-v'], opts)).toThrowError()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -73,3 +73,9 @@ test('install and rc version of Node.js', async () => {
|
||||
const extension = process.platform === 'win32' ? 'zip' : 'tar.gz'
|
||||
expect(fetchMock.mock.calls[0][0]).toBe(`https://nodejs.org/download/rc/v18.0.0-rc.3/node-v18.0.0-rc.3-${platform}-x64.${extension}`)
|
||||
})
|
||||
|
||||
test('get node version base dir', async () => {
|
||||
expect(typeof node.getNodeVersionsBaseDir).toBe('function')
|
||||
const versionDir = node.getNodeVersionsBaseDir(process.cwd())
|
||||
expect(versionDir).toBe(path.resolve(process.cwd(), 'nodejs'))
|
||||
})
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
{
|
||||
"path": "../node.resolver"
|
||||
},
|
||||
{
|
||||
"path": "../remove-bins"
|
||||
},
|
||||
{
|
||||
"path": "../store-path"
|
||||
}
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -3078,12 +3078,18 @@ importers:
|
||||
'@pnpm/node.resolver':
|
||||
specifier: workspace:*
|
||||
version: link:../node.resolver
|
||||
'@pnpm/remove-bins':
|
||||
specifier: workspace:*
|
||||
version: link:../remove-bins
|
||||
'@pnpm/store-path':
|
||||
specifier: workspace:*
|
||||
version: link:../store-path
|
||||
'@zkochan/cmd-shim':
|
||||
specifier: ^5.3.1
|
||||
version: 5.3.1
|
||||
'@zkochan/rimraf':
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2
|
||||
load-json-file:
|
||||
specifier: ^6.2.0
|
||||
version: 6.2.0
|
||||
|
||||
Reference in New Issue
Block a user