feat: new plugin for managing Node.js versions (#3459)

This commit is contained in:
Zoltan Kochan
2021-05-20 02:10:25 +03:00
committed by GitHub
parent ecd00529a4
commit 84ec82e05c
21 changed files with 298 additions and 3 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/config": minor
"pnpm": minor
---
New setting added: `use-node-version`. When set, pnpm will install the specified version of Node.js and use it for running any lifecycle scripts.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-nvm": minor
---
Project created.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/config": minor
---
New settings are returned: pnpmExecPath and pnpmHomeDir.

View File

@@ -33,6 +33,8 @@ export interface Config {
saveWorkspaceProtocol?: boolean
scriptShell?: string
stream?: boolean
pnpmExecPath: string
pnpmHomeDir: string
production?: boolean
fetchRetries?: number
fetchRetryFactor?: number
@@ -66,6 +68,7 @@ export interface Config {
ignoreCurrentPrefs?: boolean
recursive?: boolean
enablePrePostScripts?: boolean
useNodeVersion?: string
useStderr?: boolean
// proxy

View File

@@ -94,6 +94,7 @@ export const types = Object.assign({
stream: Boolean,
'strict-peer-dependencies': Boolean,
'use-beta-cli': Boolean,
'use-node-version': String,
'use-running-store-server': Boolean,
'use-store-server': Boolean,
'use-stderr': Boolean,
@@ -409,6 +410,19 @@ 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()
}
if (opts.checkUnknownSetting) {
const settingKeys = Object.keys({

View File

@@ -0,0 +1,15 @@
# @pnpm/plugin-commands-nvm
> 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)
## Installation
```sh
<pnpm|npm|yarn> add @pnpm/plugin-commands-nvm
```
## License
MIT

View File

@@ -0,0 +1,3 @@
const config = require('../../jest.config.js')
module.exports = config

View File

@@ -0,0 +1,44 @@
{
"name": "@pnpm/plugin-commands-nvm",
"version": "0.0.0",
"description": "pnpm commands for managing Node.js",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"files": [
"lib",
"!*.map"
],
"engines": {
"node": ">=12.17"
},
"scripts": {
"lint": "eslint -c ../../eslint.json src/**/*.ts test/**/*.ts",
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"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",
"keywords": [
"pnpm",
"nvm"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"homepage": "https://github.com/pnpm/pnpm/blob/master/packages/plugin-commands-nvm#readme",
"dependencies": {
"@pnpm/cli-utils": "workspace:0.6.4",
"@pnpm/fetch": "workspace:3.1.0",
"execa": "^5.0.0",
"load-json-file": "^6.2.0",
"path-name": "^1.0.0",
"render-help": "^1.0.1",
"write-json-file": "^4.3.0"
},
"funding": "https://opencollective.com/pnpm",
"devDependencies": {
"@pnpm/prepare": "workspace:0.0.23"
}
}

View File

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

View File

@@ -0,0 +1,97 @@
import fs from 'fs'
import path from 'path'
import { docsUrl } from '@pnpm/cli-utils'
import fetch from '@pnpm/fetch'
import execa from 'execa'
import PATH from 'path-name'
import renderHelp from 'render-help'
import loadJsonFile from 'load-json-file'
import writeJsonFile from 'write-json-file'
export const rcOptionsTypes = () => ({})
export const cliOptionsTypes = () => ({})
export const shorthands = {}
export const commandNames = ['node']
export function help () {
return renderHelp({
description: 'Run Node.js',
descriptionLists: [],
url: docsUrl('node'),
usages: ['pnpm node'],
})
}
export async function handler (
opts: {
argv: {
original: string[]
}
useNodeVersion?: string
pnpmHomeDir: string
}
) {
const nodeDir = await getNodeDir(opts.pnpmHomeDir, opts.useNodeVersion)
const { exitCode } = await execa('node', opts.argv.original.slice(1), {
env: {
[PATH]: `${path.join(nodeDir, 'node_modules/.bin')}${path.delimiter}${process.env[PATH]!}`,
},
stdout: 'inherit',
stdin: 'inherit',
})
return { exitCode }
}
export async function getNodeDir (pnpmHomeDir: string, nodeVersion?: string) {
const nodesDir = path.join(pnpmHomeDir, 'nodes')
let wantedNodeVersion = nodeVersion ?? (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
if (wantedNodeVersion == null) {
throw new Error('Could not resolve LTS version of Node.js')
}
await writeJsonFile(path.join(nodesDir, 'versions.json'), {
default: wantedNodeVersion,
})
}
const versionDir = path.join(nodesDir, wantedNodeVersion)
if (!fs.existsSync(versionDir)) {
await installNode(wantedNodeVersion, versionDir)
}
return versionDir
}
async function installNode (wantedNodeVersion: string, versionDir: string) {
await fs.promises.mkdir(versionDir, { recursive: true })
await writeJsonFile(path.join(versionDir, 'package.json'), {})
const { exitCode } = await execa('pnpm', ['add', `${getNodePkgName()}@${wantedNodeVersion}`], {
cwd: versionDir,
stdout: 'inherit',
})
if (exitCode !== 0) {
throw new Error(`Couldn't install Node.js ${wantedNodeVersion}`)
}
}
function getNodePkgName () {
const platform = process.platform === 'win32' ? 'win' : process.platform
const arch = platform === 'win' && process.arch === 'ia32' ? 'x86' : process.arch
return `node-${platform}-${arch}`
}
async function readNodeVersionsManifest (nodesDir: string): Promise<{ default?: string }> {
try {
return await loadJsonFile<{ default?: string }>(path.join(nodesDir, 'versions.json'))
} catch (err) {
if (err.code === 'ENOENT') {
return {}
}
throw err
}
}

View File

@@ -0,0 +1,28 @@
import fs from 'fs'
import { node } from '@pnpm/plugin-commands-nvm'
import { tempDir } from '@pnpm/prepare'
test('run specific version of Node.js', async () => {
tempDir()
const { exitCode } = await node.handler({
argv: {
original: ['node', '-e', 'require("fs").writeFileSync("version",process.version, "utf8")'],
},
useNodeVersion: '14.0.0',
pnpmHomeDir: process.cwd(),
})
expect(exitCode).toBe(0)
expect(fs.readFileSync('version', 'utf8')).toBe('v14.0.0')
})
test('run LTS version of Node.js by default', async () => {
tempDir()
const { exitCode } = await node.handler({
argv: {
original: ['node', '-e', 'require("fs").writeFileSync("version",process.version, "utf8")'],
},
pnpmHomeDir: process.cwd(),
})
expect(exitCode).toBe(0)
expect(fs.readFileSync('version', 'utf8')).toMatch(/^v[0-9]+\.[0-9]+\.[0-9]+$/)
})

View File

@@ -0,0 +1,16 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../typings/**/*.d.ts"
],
"references": [
{
"path": "../fetch"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../typings/**/*.d.ts"
]
}

1
packages/pnpm/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
bin/nodes

View File

@@ -42,6 +42,7 @@
"@pnpm/plugin-commands-import": "workspace:2.0.11",
"@pnpm/plugin-commands-installation": "workspace:4.1.7",
"@pnpm/plugin-commands-listing": "workspace:3.0.7",
"@pnpm/plugin-commands-nvm": "workspace:0.0.0",
"@pnpm/plugin-commands-outdated": "workspace:4.1.4",
"@pnpm/plugin-commands-publishing": "workspace:3.1.5",
"@pnpm/plugin-commands-rebuild": "workspace:4.0.3",
@@ -160,7 +161,7 @@
"test": "pnpm run compile && pnpm run _test",
"prepublishOnly": "pnpm compile && npm cache clear --force && publish-packed --prune --npm-client yarn --dest dist",
"postpublish": "publish-packed",
"compile": "rimraf lib tsconfig.tsbuildinfo && tsc --build && pnpm run lint -- --fix && rimraf dist && pnpm run bundle && shx cp -r node-gyp-bin dist/node-gyp-bin && shx cp -r node_modules/@pnpm/tabtab/lib/scripts dist/scripts && shx cp -r node_modules/ps-list/vendor dist/vendor && pkg ./dist/pnpm.cjs --out-path=../artifacts/win-x64 --targets=node14-win-x64 && pkg ./dist/pnpm.cjs --out-path=../artifacts/linux-x64 --targets=node14-linux-x64 && pkg ./dist/pnpm.cjs --out-path=../artifacts/macos-x64 --targets=node14-macos-x64"
"compile": "rimraf lib tsconfig.tsbuildinfo && tsc --build && pnpm run lint -- --fix && rimraf dist bin/nodes && pnpm run bundle && shx cp -r node-gyp-bin dist/node-gyp-bin && shx cp -r node_modules/@pnpm/tabtab/lib/scripts dist/scripts && shx cp -r node_modules/ps-list/vendor dist/vendor && pkg ./dist/pnpm.cjs --out-path=../artifacts/win-x64 --targets=node14-win-x64 && pkg ./dist/pnpm.cjs --out-path=../artifacts/linux-x64 --targets=node14-linux-x64 && pkg ./dist/pnpm.cjs --out-path=../artifacts/macos-x64 --targets=node14-macos-x64"
},
"publishconfig": {
"tag": "next"

View File

@@ -4,6 +4,7 @@ import { audit } from '@pnpm/plugin-commands-audit'
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'
import { node } from '@pnpm/plugin-commands-nvm'
import { outdated } from '@pnpm/plugin-commands-outdated'
import { pack, publish } from '@pnpm/plugin-commands-publishing'
import { rebuild } from '@pnpm/plugin-commands-rebuild'
@@ -68,6 +69,7 @@ const commands: Array<{
link,
list,
ll,
node,
outdated,
pack,
prune,

View File

@@ -11,6 +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 chalk from 'chalk'
import checkForUpdates from './checkForUpdates'
import pnpmCmds, { rcOptionsTypes } from './cmd'
@@ -61,7 +62,7 @@ export default async function run (inputArgv: string[]) {
process.exit(1)
}
if (unknownOptions.size > 0) {
if (unknownOptions.size > 0 && cmd !== 'node') {
const unknownOptionsArray = Array.from(unknownOptions.keys())
if (unknownOptionsArray.every((option) => DEPRECATED_OPTIONS.has(option))) {
let deprecationMsg = `${chalk.bgYellow.black('\u2009WARN\u2009')}`
@@ -225,6 +226,10 @@ export default async function run (inputArgv: string[]) {
})
try {
if (config.useNodeVersion != null) {
const nodePath = path.join(await node.getNodeDir(config.pnpmHomeDir, config.useNodeVersion), 'node_modules/.bin')
config.extraBinPaths.push(nodePath)
}
let result = pnpmCmds[cmd ?? 'help'](
// TypeScript doesn't currently infer that the type of config
// is `Omit<typeof config, 'reporter'>` after the `delete config.reporter` statement

View File

@@ -170,3 +170,14 @@ test('the bundled CLI prints the correct version, when executed from stdin', asy
const { version } = await loadJsonFile<{ version: string }>(path.join(__dirname, '../package.json'))
expect((await nodeProcess).stdout).toBe(version)
})
test('use the specified Node.js version for running scripts', async () => {
prepare({
scripts: {
test: "node -e \"require('fs').writeFileSync('version',process.version,'utf8')\"",
},
})
await fs.writeFile('.npmrc', 'use-node-version=14.0.0', 'utf8')
await execPnpm(['run', 'test'])
expect(await fs.readFile('version', 'utf8')).toBe('v14.0.0')
})

View File

@@ -81,6 +81,9 @@
{
"path": "../plugin-commands-listing"
},
{
"path": "../plugin-commands-nvm"
},
{
"path": "../plugin-commands-outdated"
},

25
pnpm-lock.yaml generated
View File

@@ -1925,6 +1925,29 @@ importers:
strip-ansi: 6.0.0
write-yaml-file: 4.2.0
packages/plugin-commands-nvm:
specifiers:
'@pnpm/cli-utils': workspace:0.6.4
'@pnpm/fetch': workspace:3.1.0
'@pnpm/plugin-commands-nvm': 'link:'
'@pnpm/prepare': workspace:0.0.23
execa: ^5.0.0
load-json-file: ^6.2.0
path-name: ^1.0.0
render-help: ^1.0.1
write-json-file: ^4.3.0
dependencies:
'@pnpm/cli-utils': link:../cli-utils
'@pnpm/fetch': link:../fetch
execa: 5.0.0
load-json-file: 6.2.0
path-name: 1.0.0
render-help: 1.0.2
write-json-file: 4.3.0
devDependencies:
'@pnpm/plugin-commands-nvm': 'link:'
'@pnpm/prepare': link:../../privatePackages/prepare
packages/plugin-commands-outdated:
specifiers:
'@pnpm/cli-utils': workspace:0.6.4
@@ -2354,6 +2377,7 @@ importers:
'@pnpm/plugin-commands-import': workspace:2.0.11
'@pnpm/plugin-commands-installation': workspace:4.1.7
'@pnpm/plugin-commands-listing': workspace:3.0.7
'@pnpm/plugin-commands-nvm': workspace:0.0.0
'@pnpm/plugin-commands-outdated': workspace:4.1.4
'@pnpm/plugin-commands-publishing': workspace:3.1.5
'@pnpm/plugin-commands-rebuild': workspace:4.0.3
@@ -2446,6 +2470,7 @@ importers:
'@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

View File

@@ -162,7 +162,7 @@ async function updateManifest (workspaceDir: string, manifest: ProjectManifest,
type: 'git',
url: 'git+https://github.com/pnpm/pnpm.git',
}
scripts.compile += ' && rimraf dist && pnpm run bundle \
scripts.compile += ' && rimraf dist bin/nodes && pnpm run bundle \
&& shx cp -r node-gyp-bin dist/node-gyp-bin \
&& shx cp -r node_modules/@pnpm/tabtab/lib/scripts dist/scripts \
&& shx cp -r node_modules/ps-list/vendor dist/vendor \