Skip to content

Commit

Permalink
feat(env): add remove command to pnpm env (#5263)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people committed Aug 29, 2022
1 parent e848f53 commit ba270a9
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .changeset/long-trainers-share.md
@@ -0,0 +1,6 @@
---
"@pnpm/plugin-commands-env": minor
"pnpm": minor
---

Enhance `pnpm env` with the `remove` command.
2 changes: 2 additions & 0 deletions packages/plugin-commands-env/package.json
Expand Up @@ -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"
Expand Down
87 changes: 81 additions & 6 deletions packages/plugin-commands-env/src/env.ts
@@ -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'

Expand All @@ -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: 'Commands',
list: [
{
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: 'Installs Node.js globally',
description: 'Manages Node.js versions globally',
name: '--global',
shortAlias: '-g',
},
Expand All @@ -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',
],
})
}
Expand Down Expand Up @@ -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')
}
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-commands-env/src/node.ts
Expand Up @@ -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')
}

Expand Down
80 changes: 79 additions & 1 deletion packages/plugin-commands-env/test/env.test.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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()
})
})
6 changes: 6 additions & 0 deletions packages/plugin-commands-env/test/node.test.ts
Expand Up @@ -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'))
})
3 changes: 3 additions & 0 deletions packages/plugin-commands-env/tsconfig.json
Expand Up @@ -30,6 +30,9 @@
{
"path": "../node.resolver"
},
{
"path": "../remove-bins"
},
{
"path": "../store-path"
}
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit ba270a9

Please sign in to comment.