feat: a command for installing Node.js (#3620)

The following command will install Node.js (inspired by [poetry](https://python-poetry.org/docs/managing-environments/)):

```
pnpm env use --global 16.5.0
```
This commit is contained in:
Zoltan Kochan
2021-07-30 14:21:19 +03:00
committed by GitHub
parent 320482c057
commit af8b5716e4
22 changed files with 233 additions and 69 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-env": patch
---
New command added: `pnpm env use --global <version>`. This command installs the specified Node.js version globally.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/config": patch
---
pnpm should always have write access to its home directory

View File

@@ -39,3 +39,21 @@ export function getStateDir (
}
return path.join(os.homedir(), '.pnpm-state')
}
export function getDataDir (
opts: {
env: NodeJS.ProcessEnv
platform: string
}
) {
if (opts.env.XDG_DATA_HOME) {
return path.join(opts.env.XDG_DATA_HOME, 'pnpm')
}
if (opts.platform !== 'win32' && opts.platform !== 'darwin') {
return path.join(os.homedir(), '.local/share/pnpm')
}
if (opts.env.LOCALAPPDATA) {
return path.join(opts.env.LOCALAPPDATA, 'pnpm')
}
return path.join(os.homedir(), '.pnpm')
}

View File

@@ -13,7 +13,7 @@ import realpathMissing from 'realpath-missing'
import whichcb from 'which'
import getScopeRegistries, { normalizeRegistry } from './getScopeRegistries'
import findBestGlobalPrefix from './findBestGlobalPrefix'
import { getCacheDir, getStateDir } from './dirs'
import { getCacheDir, getDataDir, getStateDir } from './dirs'
import {
Config,
ConfigWithDeprecatedSettings,
@@ -427,19 +427,7 @@ export default async (
pnpmConfig.noProxy = pnpmConfig['noproxy'] ?? getProcessEnv('no_proxy')
}
pnpmConfig.enablePnp = pnpmConfig['nodeLinker'] === 'pnp'
if (process['pkg'] != null) {
// If the pnpm CLI was bundled by vercel/pkg then we cannot use the js path for npm_execpath
// because in that case the js is in a virtual filesystem inside the executor.
// Instead, we use the path to the exe file.
pnpmConfig.pnpmExecPath = process.execPath
pnpmConfig.pnpmHomeDir = path.dirname(pnpmConfig.pnpmExecPath)
} else if (require.main != null) {
pnpmConfig.pnpmExecPath = require.main.filename
pnpmConfig.pnpmHomeDir = path.dirname(pnpmConfig.pnpmExecPath)
} else {
pnpmConfig.pnpmExecPath = process.cwd()
pnpmConfig.pnpmHomeDir = process.cwd()
}
pnpmConfig.pnpmHomeDir = getDataDir(process)
if (opts.checkUnknownSetting) {
const settingKeys = Object.keys({

View File

@@ -1,6 +1,6 @@
import os from 'os'
import path from 'path'
import { getCacheDir, getStateDir } from '../lib/dirs'
import { getCacheDir, getDataDir, getStateDir } from '../lib/dirs'
test('getCacheDir()', () => {
expect(getCacheDir({
@@ -55,3 +55,30 @@ test('getStateDir()', () => {
platform: 'win32',
})).toBe(path.join(os.homedir(), '.pnpm-state'))
})
test('getDataDir()', () => {
expect(getDataDir({
env: {
XDG_DATA_HOME: '/home/foo/data',
},
platform: 'linux',
})).toBe(path.join('/home/foo/data', 'pnpm'))
expect(getDataDir({
env: {},
platform: 'linux',
})).toBe(path.join(os.homedir(), '.local/share/pnpm'))
expect(getDataDir({
env: {},
platform: 'darwin',
})).toBe(path.join(os.homedir(), '.pnpm'))
expect(getDataDir({
env: {
LOCALAPPDATA: '/localappdata',
},
platform: 'win32',
})).toBe(path.join('/localappdata', 'pnpm'))
expect(getDataDir({
env: {},
platform: 'win32',
})).toBe(path.join(os.homedir(), '.pnpm'))
})

View File

@@ -1,4 +1,4 @@
# @pnpm/plugin-commands-nvm
# @pnpm/plugin-commands-env
## 0.2.8

View File

@@ -1,13 +1,13 @@
# @pnpm/plugin-commands-nvm
# @pnpm/plugin-commands-env
> pnpm commands for managing Node.js
[![npm version](https://img.shields.io/npm/v/@pnpm/plugin-commands-nvm.svg)](https://www.npmjs.com/package/@pnpm/plugin-commands-nvm)
[![npm version](https://img.shields.io/npm/v/@pnpm/plugin-commands-env.svg)](https://www.npmjs.com/package/@pnpm/plugin-commands-env)
## Installation
```sh
<pnpm|npm|yarn> add @pnpm/plugin-commands-nvm
<pnpm|npm|yarn> add @pnpm/plugin-commands-env
```
## License

View File

@@ -1,5 +1,5 @@
{
"name": "@pnpm/plugin-commands-nvm",
"name": "@pnpm/plugin-commands-env",
"version": "0.2.8",
"description": "pnpm commands for managing Node.js",
"main": "lib/index.js",
@@ -18,31 +18,37 @@
"prepublishOnly": "pnpm run compile",
"compile": "rimraf lib tsconfig.tsbuildinfo && tsc --build && pnpm run lint -- --fix"
},
"repository": "https://github.com/pnpm/pnpm/blob/master/packages/plugin-commands-nvm",
"repository": "https://github.com/pnpm/pnpm/blob/master/packages/plugin-commands-env",
"keywords": [
"pnpm",
"nvm"
"env"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"homepage": "https://github.com/pnpm/pnpm/blob/master/packages/plugin-commands-nvm#readme",
"homepage": "https://github.com/pnpm/pnpm/blob/master/packages/plugin-commands-env#readme",
"dependencies": {
"@pnpm/cli-utils": "workspace:0.6.13",
"@pnpm/config": "workspace:12.4.3",
"@pnpm/error": "workspace:2.0.0",
"@pnpm/fetch": "workspace:4.0.2",
"@pnpm/package-store": "workspace:12.0.12",
"@pnpm/store-path": "^5.0.0",
"@pnpm/tarball-fetcher": "workspace:9.3.4",
"@zkochan/cmd-shim": "^5.1.3",
"adm-zip": "^0.5.5",
"load-json-file": "^6.2.0",
"rename-overwrite": "^4.0.0",
"render-help": "^1.0.1",
"tempy": "^1.0.0",
"write-json-file": "^4.3.0"
},
"funding": "https://opencollective.com/pnpm",
"devDependencies": {
"@pnpm/prepare": "workspace:0.0.26",
"@types/adm-zip": "^0.4.34"
"@types/adm-zip": "^0.4.34",
"execa": "^5.0.0",
"path-name": "^1.0.0"
}
}

View File

@@ -0,0 +1,66 @@
import path from 'path'
import { docsUrl } from '@pnpm/cli-utils'
import PnpmError from '@pnpm/error'
import cmdShim from '@zkochan/cmd-shim'
import renderHelp from 'render-help'
import { getNodeDir, NvmNodeCommandOptions } from './node'
export function rcOptionsTypes () {
return {}
}
export function cliOptionsTypes () {
return {
global: Boolean,
}
}
export const commandNames = ['env']
export function help () {
return renderHelp({
description: 'Install and use the specified version of Node.js',
descriptionLists: [
{
title: 'Options',
list: [
{
description: 'Installs Node.js globally',
name: '--global',
shortAlias: '-g',
},
],
},
],
url: docsUrl('env'),
usages: [
'pnpm env use --global <version>',
],
})
}
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 nodeDir = await getNodeDir({
...opts,
useNodeVersion: params[1],
})
const src = path.join(nodeDir, process.platform === 'win32' ? 'node.exe' : 'node')
const dest = path.join(opts.bin, 'node')
await cmdShim(src, dest)
return `Node.js ${params[1]} is activated
${dest} -> ${src}`
}
default: {
throw new PnpmError('ENV_UNKNOWN_SUBCOMMAND', 'This subcommand is not known')
}
}
}

View File

@@ -0,0 +1,4 @@
import * as env from './env'
import * as node from './node'
export { env, node }

View File

@@ -12,6 +12,8 @@ import loadJsonFile from 'load-json-file'
import writeJsonFile from 'write-json-file'
export type NvmNodeCommandOptions = Pick<Config,
| 'bin'
| 'global'
| 'rawConfig'
| 'fetchRetries'
| 'fetchRetryFactor'
@@ -34,10 +36,9 @@ export type NvmNodeCommandOptions = Pick<Config,
>
export async function getNodeDir (opts: NvmNodeCommandOptions) {
const nodesDir = path.join(opts.pnpmHomeDir, 'nodes')
const nodesDir = path.join(opts.pnpmHomeDir, 'nodejs')
let wantedNodeVersion = opts.useNodeVersion ?? (await readNodeVersionsManifest(nodesDir))?.default
await fs.promises.mkdir(nodesDir, { recursive: true })
fs.writeFileSync(path.join(nodesDir, 'pnpm-workspace.yaml'), '', 'utf8')
if (wantedNodeVersion == null) {
const response = await fetch('https://registry.npmjs.org/node')
wantedNodeVersion = (await response.json())['dist-tags'].lts

View File

@@ -0,0 +1,27 @@
import fs from 'fs'
import path from 'path'
import { tempDir } from '@pnpm/prepare'
import { env } from '@pnpm/plugin-commands-env'
import execa from 'execa'
import PATH from 'path-name'
test('install node', async () => {
tempDir()
await env.handler({
bin: process.cwd(),
global: true,
pnpmHomeDir: process.cwd(),
rawConfig: {},
}, ['use', '16.4.0'])
const { stdout } = execa.sync('node', ['-v'], {
env: {
[PATH]: `${process.cwd()}${path.delimiter}${process.env[PATH] as string}`,
},
})
expect(stdout.toString()).toBe('v16.4.0')
const dirs = fs.readdirSync(path.resolve('nodejs'))
expect(dirs).toEqual(['16.4.0'])
})

View File

@@ -1,4 +1,4 @@
import { node } from '@pnpm/plugin-commands-nvm'
import { node } from '@pnpm/plugin-commands-env'
test('check API (placeholder test)', async () => {
expect(typeof node.getNodeDir).toBe('function')

View File

@@ -12,9 +12,15 @@
{
"path": "../../privatePackages/prepare"
},
{
"path": "../cli-utils"
},
{
"path": "../config"
},
{
"path": "../error"
},
{
"path": "../fetch"
},

View File

@@ -1,3 +0,0 @@
import * as node from './node'
export { node }

View File

@@ -43,7 +43,7 @@
"@pnpm/plugin-commands-import": "workspace:3.0.8",
"@pnpm/plugin-commands-installation": "workspace:6.0.8",
"@pnpm/plugin-commands-listing": "workspace:4.0.4",
"@pnpm/plugin-commands-nvm": "workspace:0.2.8",
"@pnpm/plugin-commands-env": "workspace:0.2.8",
"@pnpm/plugin-commands-outdated": "workspace:5.0.4",
"@pnpm/plugin-commands-publishing": "workspace:4.2.0",
"@pnpm/plugin-commands-rebuild": "workspace:5.0.4",

View File

@@ -1,6 +1,7 @@
import { CompletionFunc } from '@pnpm/command'
import { types as allTypes } from '@pnpm/config'
import { audit } from '@pnpm/plugin-commands-audit'
import { env } from '@pnpm/plugin-commands-env'
import { importCommand } from '@pnpm/plugin-commands-import'
import { add, fetch, install, link, prune, remove, unlink, update } from '@pnpm/plugin-commands-installation'
import { list, ll, why } from '@pnpm/plugin-commands-listing'
@@ -61,6 +62,7 @@ const commands: Array<{
add,
audit,
bin,
env,
exec,
fetch,
importCommand,

View File

@@ -11,7 +11,7 @@ import { filterPackages } from '@pnpm/filter-workspace-packages'
import findWorkspacePackages from '@pnpm/find-workspace-packages'
import logger from '@pnpm/logger'
import { ParsedCliArgs } from '@pnpm/parse-cli-args'
import { node } from '@pnpm/plugin-commands-nvm'
import { node } from '@pnpm/plugin-commands-env'
import chalk from 'chalk'
import checkForUpdates from './checkForUpdates'
import pnpmCmds, { rcOptionsTypes } from './cmd'

View File

@@ -72,6 +72,9 @@
{
"path": "../plugin-commands-audit"
},
{
"path": "../plugin-commands-env"
},
{
"path": "../plugin-commands-import"
},
@@ -81,9 +84,6 @@
{
"path": "../plugin-commands-listing"
},
{
"path": "../plugin-commands-nvm"
},
{
"path": "../plugin-commands-outdated"
},

80
pnpm-lock.yaml generated
View File

@@ -1758,6 +1758,49 @@ importers:
strip-ansi: 6.0.0
tempy: 1.0.1
packages/plugin-commands-env:
specifiers:
'@pnpm/cli-utils': workspace:0.6.13
'@pnpm/config': workspace:12.4.3
'@pnpm/error': workspace:2.0.0
'@pnpm/fetch': workspace:4.0.2
'@pnpm/package-store': workspace:12.0.12
'@pnpm/plugin-commands-env': 'link:'
'@pnpm/prepare': workspace:0.0.26
'@pnpm/store-path': ^5.0.0
'@pnpm/tarball-fetcher': workspace:9.3.4
'@types/adm-zip': ^0.4.34
'@zkochan/cmd-shim': ^5.1.3
adm-zip: ^0.5.5
execa: ^5.0.0
load-json-file: ^6.2.0
path-name: ^1.0.0
rename-overwrite: ^4.0.0
render-help: ^1.0.1
tempy: ^1.0.0
write-json-file: ^4.3.0
dependencies:
'@pnpm/cli-utils': link:../cli-utils
'@pnpm/config': link:../config
'@pnpm/error': link:../error
'@pnpm/fetch': link:../fetch
'@pnpm/package-store': link:../package-store
'@pnpm/store-path': 5.0.0
'@pnpm/tarball-fetcher': link:../tarball-fetcher
'@zkochan/cmd-shim': 5.1.3
adm-zip: 0.5.5
load-json-file: 6.2.0
rename-overwrite: 4.0.0
render-help: 1.0.2
tempy: 1.0.1
write-json-file: 4.3.0
devDependencies:
'@pnpm/plugin-commands-env': 'link:'
'@pnpm/prepare': link:../../privatePackages/prepare
'@types/adm-zip': 0.4.34
execa: 5.1.1
path-name: 1.0.0
packages/plugin-commands-import:
specifiers:
'@pnpm/assert-project': workspace:*
@@ -1957,37 +2000,6 @@ importers:
strip-ansi: 6.0.0
write-yaml-file: 4.2.0
packages/plugin-commands-nvm:
specifiers:
'@pnpm/config': workspace:12.4.3
'@pnpm/fetch': workspace:4.0.2
'@pnpm/package-store': workspace:12.0.12
'@pnpm/plugin-commands-nvm': 'link:'
'@pnpm/prepare': workspace:0.0.26
'@pnpm/store-path': ^5.0.0
'@pnpm/tarball-fetcher': workspace:9.3.4
'@types/adm-zip': ^0.4.34
adm-zip: ^0.5.5
load-json-file: ^6.2.0
rename-overwrite: ^4.0.0
tempy: ^1.0.0
write-json-file: ^4.3.0
dependencies:
'@pnpm/config': link:../config
'@pnpm/fetch': link:../fetch
'@pnpm/package-store': link:../package-store
'@pnpm/store-path': 5.0.0
'@pnpm/tarball-fetcher': link:../tarball-fetcher
adm-zip: 0.5.5
load-json-file: 6.2.0
rename-overwrite: 4.0.0
tempy: 1.0.1
write-json-file: 4.3.0
devDependencies:
'@pnpm/plugin-commands-nvm': 'link:'
'@pnpm/prepare': link:../../privatePackages/prepare
'@types/adm-zip': 0.4.34
packages/plugin-commands-outdated:
specifiers:
'@pnpm/cli-utils': workspace:0.6.13
@@ -2440,10 +2452,10 @@ importers:
'@pnpm/parse-cli-args': workspace:4.3.0
'@pnpm/pick-registry-for-package': workspace:2.0.4
'@pnpm/plugin-commands-audit': workspace:5.1.2
'@pnpm/plugin-commands-env': workspace:0.2.8
'@pnpm/plugin-commands-import': workspace:3.0.8
'@pnpm/plugin-commands-installation': workspace:6.0.8
'@pnpm/plugin-commands-listing': workspace:4.0.4
'@pnpm/plugin-commands-nvm': workspace:0.2.8
'@pnpm/plugin-commands-outdated': workspace:5.0.4
'@pnpm/plugin-commands-publishing': workspace:4.2.0
'@pnpm/plugin-commands-rebuild': workspace:5.0.4
@@ -2533,10 +2545,10 @@ importers:
'@pnpm/parse-cli-args': link:../parse-cli-args
'@pnpm/pick-registry-for-package': link:../pick-registry-for-package
'@pnpm/plugin-commands-audit': link:../plugin-commands-audit
'@pnpm/plugin-commands-env': link:../plugin-commands-env
'@pnpm/plugin-commands-import': link:../plugin-commands-import
'@pnpm/plugin-commands-installation': link:../plugin-commands-installation
'@pnpm/plugin-commands-listing': link:../plugin-commands-listing
'@pnpm/plugin-commands-nvm': link:../plugin-commands-nvm
'@pnpm/plugin-commands-outdated': link:../plugin-commands-outdated
'@pnpm/plugin-commands-publishing': link:../plugin-commands-publishing
'@pnpm/plugin-commands-rebuild': link:../plugin-commands-rebuild
@@ -4665,7 +4677,7 @@ packages:
/@types/adm-zip/0.4.34:
resolution: {integrity: sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==}
dependencies:
'@types/node': 16.4.1
'@types/node': 16.4.2
dev: true
/@types/archy/0.0.31: