Files
pnpm/packages/plugin-commands-env/src/env.ts
Mark Omarov ba270a9671 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>
2022-08-30 01:53:13 +03:00

181 lines
6.1 KiB
TypeScript

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, getNodeVersionsBaseDir } from './node'
import getNodeMirror from './getNodeMirror'
import { parseNodeEditionSpecifier } from './parseNodeEditionSpecifier'
export function rcOptionsTypes () {
return {}
}
export function cliOptionsTypes () {
return {
global: Boolean,
}
}
export const commandNames = ['env']
export function help () {
return renderHelp({
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: 'Manages Node.js versions globally',
name: '--global',
shortAlias: '-g',
},
],
},
],
url: docsUrl('env'),
usages: [
'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',
],
})
}
export async function handler (opts: NvmNodeCommandOptions, params: string[]) {
if (params.length === 0) {
throw new PnpmError('ENV_NO_SUBCOMMAND', 'Please specify the subcommand')
}
switch (params[0]) {
case 'use': {
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)
if (!nodeVersion) {
throw new PnpmError('COULD_NOT_RESOLVE_NODEJS', `Couldn't find Node.js version matching ${params[1]}`)
}
const nodeDir = await getNodeDir(fetch, {
...opts,
useNodeVersion: nodeVersion,
nodeMirrorBaseUrl,
})
const src = path.join(nodeDir, process.platform === 'win32' ? 'node.exe' : 'bin/node')
const dest = path.join(opts.bin, process.platform === 'win32' ? 'node.exe' : 'node')
try {
await fs.unlink(dest)
} catch (err) {}
await fs.symlink(src, dest, 'file')
try {
let npmDir = nodeDir
if (process.platform !== 'win32') {
npmDir = path.join(npmDir, 'lib')
}
npmDir = path.join(npmDir, 'node_modules/npm')
if (opts.configDir) {
// We want the global npm settings to persist when Node.js or/and npm is changed to a different version,
// so we tell npm to read the global config from centralized place that is outside of npm's directory.
await fs.writeFile(path.join(npmDir, 'npmrc'), `globalconfig=${path.join(opts.configDir, 'npmrc')}`, 'utf-8')
}
const npmBinDir = path.join(npmDir, 'bin')
const cmdShimOpts = { createPwshFile: false }
await cmdShim(path.join(npmBinDir, 'npm-cli.js'), path.join(opts.bin, 'npm'), cmdShimOpts)
await cmdShim(path.join(npmBinDir, 'npx-cli.js'), path.join(opts.bin, 'npx'), cmdShimOpts)
} catch (err: any) { // eslint-disable-line
// ignore
}
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')
}
}
}