Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(env): add remove command to pnpm env #5263

Merged
merged 9 commits into from Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.