diff --git a/.changeset/odd-drinks-care.md b/.changeset/odd-drinks-care.md new file mode 100644 index 0000000000..3e9d9f8640 --- /dev/null +++ b/.changeset/odd-drinks-care.md @@ -0,0 +1,5 @@ +--- +"@pnpm/plugin-commands-env": patch +--- + +Allow to install a Node.js version using a semver range. diff --git a/packages/plugin-commands-env/package.json b/packages/plugin-commands-env/package.json index b4e3aeb908..669fc71b84 100644 --- a/packages/plugin-commands-env/package.json +++ b/packages/plugin-commands-env/package.json @@ -41,7 +41,9 @@ "load-json-file": "^6.2.0", "rename-overwrite": "^4.0.0", "render-help": "^1.0.1", + "semver": "^7.3.4", "tempy": "^1.0.0", + "version-selector-type": "^3.0.0", "write-json-file": "^4.3.0" }, "funding": "https://opencollective.com/pnpm", diff --git a/packages/plugin-commands-env/src/env.ts b/packages/plugin-commands-env/src/env.ts index 5c3c39d830..c31a4673a9 100644 --- a/packages/plugin-commands-env/src/env.ts +++ b/packages/plugin-commands-env/src/env.ts @@ -1,8 +1,11 @@ import path from 'path' import { docsUrl } from '@pnpm/cli-utils' import PnpmError from '@pnpm/error' +import fetch from '@pnpm/fetch' import cmdShim from '@zkochan/cmd-shim' import renderHelp from 'render-help' +import semver from 'semver' +import versionSelectorType from 'version-selector-type' import { getNodeDir, NvmNodeCommandOptions } from './node' export function rcOptionsTypes () { @@ -36,6 +39,9 @@ export function help () { url: docsUrl('env'), usages: [ 'pnpm env use --global ', + 'pnpm env use --global 16', + 'pnpm env use --global lts', + 'pnpm env use --global argon', ], }) } @@ -49,14 +55,18 @@ export async function handler (opts: NvmNodeCommandOptions, params: string[]) { if (!opts.global) { throw new PnpmError('NOT_IMPLEMENTED_YET', '"pnpm env use " can only be used with the "--global" option currently') } + const nodeVersion = await resolveNodeVersion(params[1]) + if (!nodeVersion) { + throw new PnpmError('COULD_NOT_RESOLVE_NODEJS', `Couldn't find Node.js version matching ${params[1]}`) + } const nodeDir = await getNodeDir({ ...opts, - useNodeVersion: params[1], + useNodeVersion: nodeVersion, }) 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 + return `Node.js ${nodeVersion} is activated ${dest} -> ${src}` } default: { @@ -64,3 +74,35 @@ export async function handler (opts: NvmNodeCommandOptions, params: string[]) { } } } + +interface NodeVersion { + version: string + lts: false | string +} + +async function resolveNodeVersion (rawVersionSelector: string) { + const response = await fetch('https://nodejs.org/download/release/index.json') + const allVersions = (await response.json()) as NodeVersion[] + const { versions, versionSelector } = filterVersions(allVersions, rawVersionSelector) + const pickedVersion = semver.maxSatisfying(versions.map(({ version }) => version), versionSelector) + if (!pickedVersion) return null + return pickedVersion.substring(1) +} + +function filterVersions (versions: NodeVersion[], versionSelector: string) { + if (versionSelector === 'lts') { + return { + versions: versions.filter(({ lts }) => lts !== false), + versionSelector: '*', + } + } + const vst = versionSelectorType(versionSelector) + if (vst?.type === 'tag') { + const wantedLtsVersion = vst.normalized.toLowerCase() + return { + versions: versions.filter(({ lts }) => typeof lts === 'string' && lts.toLowerCase() === wantedLtsVersion), + versionSelector: '*', + } + } + return { versions, versionSelector } +} diff --git a/packages/plugin-commands-env/test/env.test.ts b/packages/plugin-commands-env/test/env.test.ts index ba89a08a0a..6fcad7b0a8 100644 --- a/packages/plugin-commands-env/test/env.test.ts +++ b/packages/plugin-commands-env/test/env.test.ts @@ -1,11 +1,12 @@ 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 execa from 'execa' import PATH from 'path-name' -test('install node', async () => { +test('install Node by exact version', async () => { tempDir() await env.handler({ @@ -25,3 +26,92 @@ test('install node', async () => { const dirs = fs.readdirSync(path.resolve('nodejs')) expect(dirs).toEqual(['16.4.0']) }) + +test('install Node by version range', async () => { + tempDir() + + await env.handler({ + bin: process.cwd(), + global: true, + pnpmHomeDir: process.cwd(), + rawConfig: {}, + }, ['use', '6']) + + const { stdout } = execa.sync('node', ['-v'], { + env: { + [PATH]: `${process.cwd()}${path.delimiter}${process.env[PATH] as string}`, + }, + }) + expect(stdout.toString()).toBe('v6.17.1') + + const dirs = fs.readdirSync(path.resolve('nodejs')) + expect(dirs).toEqual(['6.17.1']) +}) + +test('install the LTS version of Node', async () => { + tempDir() + + await env.handler({ + bin: process.cwd(), + global: true, + pnpmHomeDir: process.cwd(), + rawConfig: {}, + }, ['use', 'lts']) + + const { stdout: version } = execa.sync('node', ['-v'], { + env: { + [PATH]: `${process.cwd()}${path.delimiter}${process.env[PATH] as string}`, + }, + }) + expect(version).toBeTruthy() + + const dirs = fs.readdirSync(path.resolve('nodejs')) + expect(dirs).toEqual([version.substring(1)]) +}) + +test('install Node by its LTS name', async () => { + tempDir() + + await env.handler({ + bin: process.cwd(), + global: true, + pnpmHomeDir: process.cwd(), + rawConfig: {}, + }, ['use', 'argon']) + + const { stdout: version } = execa.sync('node', ['-v'], { + env: { + [PATH]: `${process.cwd()}${path.delimiter}${process.env[PATH] as string}`, + }, + }) + expect(version).toBe('v4.9.1') + + const dirs = fs.readdirSync(path.resolve('nodejs')) + expect(dirs).toEqual([version.substring(1)]) +}) + +test('fail if a non-existend Node.js version is tried to be installed', async () => { + tempDir() + + await expect( + env.handler({ + bin: process.cwd(), + global: true, + pnpmHomeDir: process.cwd(), + rawConfig: {}, + }, ['use', '6.999']) + ).rejects.toEqual(new PnpmError('COULD_NOT_RESOLVE_NODEJS', 'Couldn\'t find Node.js version matching 6.999')) +}) + +test('fail if a non-existend Node.js LTS is tried to be installed', async () => { + tempDir() + + await expect( + env.handler({ + bin: process.cwd(), + global: true, + pnpmHomeDir: process.cwd(), + rawConfig: {}, + }, ['use', 'boo']) + ).rejects.toEqual(new PnpmError('COULD_NOT_RESOLVE_NODEJS', 'Couldn\'t find Node.js version matching boo')) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1837b97dc2..fc3590e278 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1777,7 +1777,9 @@ importers: path-name: ^1.0.0 rename-overwrite: ^4.0.0 render-help: ^1.0.1 + semver: ^7.3.4 tempy: ^1.0.0 + version-selector-type: ^3.0.0 write-json-file: ^4.3.0 dependencies: '@pnpm/cli-utils': link:../cli-utils @@ -1792,7 +1794,9 @@ importers: load-json-file: 6.2.0 rename-overwrite: 4.0.0 render-help: 1.0.2 + semver: 7.3.5 tempy: 1.0.1 + version-selector-type: 3.0.0 write-json-file: 4.3.0 devDependencies: '@pnpm/plugin-commands-env': 'link:'