Skip to content

Commit 27e6331

Browse files
authored
feat(env): install Node.js using a version range (pnpm#3629)
1 parent af8b571 commit 27e6331

File tree

5 files changed

+146
-3
lines changed

5 files changed

+146
-3
lines changed

.changeset/odd-drinks-care.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pnpm/plugin-commands-env": patch
3+
---
4+
5+
Allow to install a Node.js version using a semver range.

packages/plugin-commands-env/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
"load-json-file": "^6.2.0",
4242
"rename-overwrite": "^4.0.0",
4343
"render-help": "^1.0.1",
44+
"semver": "^7.3.4",
4445
"tempy": "^1.0.0",
46+
"version-selector-type": "^3.0.0",
4547
"write-json-file": "^4.3.0"
4648
},
4749
"funding": "https://opencollective.com/pnpm",

packages/plugin-commands-env/src/env.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import path from 'path'
22
import { docsUrl } from '@pnpm/cli-utils'
33
import PnpmError from '@pnpm/error'
4+
import fetch from '@pnpm/fetch'
45
import cmdShim from '@zkochan/cmd-shim'
56
import renderHelp from 'render-help'
7+
import semver from 'semver'
8+
import versionSelectorType from 'version-selector-type'
69
import { getNodeDir, NvmNodeCommandOptions } from './node'
710

811
export function rcOptionsTypes () {
@@ -36,6 +39,9 @@ export function help () {
3639
url: docsUrl('env'),
3740
usages: [
3841
'pnpm env use --global <version>',
42+
'pnpm env use --global 16',
43+
'pnpm env use --global lts',
44+
'pnpm env use --global argon',
3945
],
4046
})
4147
}
@@ -49,18 +55,54 @@ export async function handler (opts: NvmNodeCommandOptions, params: string[]) {
4955
if (!opts.global) {
5056
throw new PnpmError('NOT_IMPLEMENTED_YET', '"pnpm env use <version>" can only be used with the "--global" option currently')
5157
}
58+
const nodeVersion = await resolveNodeVersion(params[1])
59+
if (!nodeVersion) {
60+
throw new PnpmError('COULD_NOT_RESOLVE_NODEJS', `Couldn't find Node.js version matching ${params[1]}`)
61+
}
5262
const nodeDir = await getNodeDir({
5363
...opts,
54-
useNodeVersion: params[1],
64+
useNodeVersion: nodeVersion,
5565
})
5666
const src = path.join(nodeDir, process.platform === 'win32' ? 'node.exe' : 'node')
5767
const dest = path.join(opts.bin, 'node')
5868
await cmdShim(src, dest)
59-
return `Node.js ${params[1]} is activated
69+
return `Node.js ${nodeVersion} is activated
6070
${dest} -> ${src}`
6171
}
6272
default: {
6373
throw new PnpmError('ENV_UNKNOWN_SUBCOMMAND', 'This subcommand is not known')
6474
}
6575
}
6676
}
77+
78+
interface NodeVersion {
79+
version: string
80+
lts: false | string
81+
}
82+
83+
async function resolveNodeVersion (rawVersionSelector: string) {
84+
const response = await fetch('https://nodejs.org/download/release/index.json')
85+
const allVersions = (await response.json()) as NodeVersion[]
86+
const { versions, versionSelector } = filterVersions(allVersions, rawVersionSelector)
87+
const pickedVersion = semver.maxSatisfying(versions.map(({ version }) => version), versionSelector)
88+
if (!pickedVersion) return null
89+
return pickedVersion.substring(1)
90+
}
91+
92+
function filterVersions (versions: NodeVersion[], versionSelector: string) {
93+
if (versionSelector === 'lts') {
94+
return {
95+
versions: versions.filter(({ lts }) => lts !== false),
96+
versionSelector: '*',
97+
}
98+
}
99+
const vst = versionSelectorType(versionSelector)
100+
if (vst?.type === 'tag') {
101+
const wantedLtsVersion = vst.normalized.toLowerCase()
102+
return {
103+
versions: versions.filter(({ lts }) => typeof lts === 'string' && lts.toLowerCase() === wantedLtsVersion),
104+
versionSelector: '*',
105+
}
106+
}
107+
return { versions, versionSelector }
108+
}

packages/plugin-commands-env/test/env.test.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import fs from 'fs'
22
import path from 'path'
3+
import PnpmError from '@pnpm/error'
34
import { tempDir } from '@pnpm/prepare'
45
import { env } from '@pnpm/plugin-commands-env'
56
import execa from 'execa'
67
import PATH from 'path-name'
78

8-
test('install node', async () => {
9+
test('install Node by exact version', async () => {
910
tempDir()
1011

1112
await env.handler({
@@ -25,3 +26,92 @@ test('install node', async () => {
2526
const dirs = fs.readdirSync(path.resolve('nodejs'))
2627
expect(dirs).toEqual(['16.4.0'])
2728
})
29+
30+
test('install Node by version range', async () => {
31+
tempDir()
32+
33+
await env.handler({
34+
bin: process.cwd(),
35+
global: true,
36+
pnpmHomeDir: process.cwd(),
37+
rawConfig: {},
38+
}, ['use', '6'])
39+
40+
const { stdout } = execa.sync('node', ['-v'], {
41+
env: {
42+
[PATH]: `${process.cwd()}${path.delimiter}${process.env[PATH] as string}`,
43+
},
44+
})
45+
expect(stdout.toString()).toBe('v6.17.1')
46+
47+
const dirs = fs.readdirSync(path.resolve('nodejs'))
48+
expect(dirs).toEqual(['6.17.1'])
49+
})
50+
51+
test('install the LTS version of Node', async () => {
52+
tempDir()
53+
54+
await env.handler({
55+
bin: process.cwd(),
56+
global: true,
57+
pnpmHomeDir: process.cwd(),
58+
rawConfig: {},
59+
}, ['use', 'lts'])
60+
61+
const { stdout: version } = execa.sync('node', ['-v'], {
62+
env: {
63+
[PATH]: `${process.cwd()}${path.delimiter}${process.env[PATH] as string}`,
64+
},
65+
})
66+
expect(version).toBeTruthy()
67+
68+
const dirs = fs.readdirSync(path.resolve('nodejs'))
69+
expect(dirs).toEqual([version.substring(1)])
70+
})
71+
72+
test('install Node by its LTS name', async () => {
73+
tempDir()
74+
75+
await env.handler({
76+
bin: process.cwd(),
77+
global: true,
78+
pnpmHomeDir: process.cwd(),
79+
rawConfig: {},
80+
}, ['use', 'argon'])
81+
82+
const { stdout: version } = execa.sync('node', ['-v'], {
83+
env: {
84+
[PATH]: `${process.cwd()}${path.delimiter}${process.env[PATH] as string}`,
85+
},
86+
})
87+
expect(version).toBe('v4.9.1')
88+
89+
const dirs = fs.readdirSync(path.resolve('nodejs'))
90+
expect(dirs).toEqual([version.substring(1)])
91+
})
92+
93+
test('fail if a non-existend Node.js version is tried to be installed', async () => {
94+
tempDir()
95+
96+
await expect(
97+
env.handler({
98+
bin: process.cwd(),
99+
global: true,
100+
pnpmHomeDir: process.cwd(),
101+
rawConfig: {},
102+
}, ['use', '6.999'])
103+
).rejects.toEqual(new PnpmError('COULD_NOT_RESOLVE_NODEJS', 'Couldn\'t find Node.js version matching 6.999'))
104+
})
105+
106+
test('fail if a non-existend Node.js LTS is tried to be installed', async () => {
107+
tempDir()
108+
109+
await expect(
110+
env.handler({
111+
bin: process.cwd(),
112+
global: true,
113+
pnpmHomeDir: process.cwd(),
114+
rawConfig: {},
115+
}, ['use', 'boo'])
116+
).rejects.toEqual(new PnpmError('COULD_NOT_RESOLVE_NODEJS', 'Couldn\'t find Node.js version matching boo'))
117+
})

pnpm-lock.yaml

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)