Skip to content

Commit de550f2

Browse files
committed
Support for nvm aliases 🔖
includies lts/aliases, stable, personal ones, indirections and cycle protection :hammer_and_wrench: This is a rework of https://github.com/AdrieanKhisbe/normalize-node-version/commits/support-nvm-aliases--original prototype when #1 was still in building 🏗️
1 parent 91889c3 commit de550f2

File tree

22 files changed

+148
-7
lines changed

22 files changed

+148
-7
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ await normalizeNodeVersion('<7') // '6.17.1'
2525
await normalizeNodeVersion('*') // Latest Node version, e.g. '12.8.0'
2626
await normalizeNodeVersion('_') // Node version used by current process
2727
await normalizeNodeVersion('.') // Node version from a '.nvmrc', '.node-version' or '.naverc' file in the current directory or any parent directory
28+
await normalizeNodeVersion('default') // Nvm default version, if nvm is available
2829
await normalizeNodeVersion('not_a_version') // Error: Invalid Node version
2930

3031
// All available options
@@ -85,6 +86,14 @@ _Default_: `process.cwd()`
8586
When using the [`.` alias](#supported-aliases), start looking for a Node.js
8687
version file from this directory.
8788

89+
#### nvmDir
90+
91+
_Type_: `string`\
92+
_Default_: `process.env.NVM_DIR`
93+
94+
Override to specify what path to consider for Nvm directory. Defaults to content
95+
of `NVM_DIR` environment variable that nvm makes point to the `.nvm` directory
96+
8897
# See also
8998

9099
- [`nve`](https://github.com/ehmicky/nve): Run a specific Node.js version (CLI)

src/alias.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,69 @@
1+
import { promises as fs } from 'fs'
2+
3+
import { validRange, rsort } from 'semver'
4+
5+
const NODE_LATEST_ALIASES = new Set(['stable', 'node', 'latest'])
6+
const NODE_LTS_CODE_NAMES = new Set([
7+
'argon',
8+
'boron',
9+
'carbon',
10+
'dubnium',
11+
'erbium',
12+
'fermium',
13+
'gallium',
14+
'hydrogen',
15+
'iron',
16+
])
17+
18+
const resolveNvmLatestNode = async ({ nvmDir }) => {
19+
const nodeVersions = await fs.readdir(`${nvmDir}/versions/node`)
20+
return rsort(nodeVersions)[0]
21+
}
22+
23+
const getAliasedVersion = async ({ nvmDir, alias }) => {
24+
const aliasWithImplicitLts = NODE_LTS_CODE_NAMES.has(alias)
25+
? `lts/${alias}`
26+
: alias
27+
const aliasPath = `${nvmDir}/alias/${aliasWithImplicitLts}`
28+
29+
await fs.stat(aliasPath).catch((error) => {
30+
if (error.code === 'ENOENT')
31+
throw new Error(`alias ${alias} does not exist`)
32+
throw error
33+
})
34+
const content = await fs.readFile(aliasPath, 'utf-8')
35+
return content.trim()
36+
}
37+
38+
const resolveNvmAlias = async (
39+
alias,
40+
{ nvmDir, previousAliases = [] } = {},
41+
) => {
42+
if (NODE_LATEST_ALIASES.has(alias)) {
43+
return resolveNvmLatestNode({ nvmDir })
44+
}
45+
46+
if (previousAliases.includes(alias)) {
47+
throw new Error(
48+
`Nvm alias cycle detected ${previousAliases.join(' -> ')} -> ${alias}`,
49+
)
50+
}
51+
52+
const resolvedVersion = await getAliasedVersion({ nvmDir, alias })
53+
if (validRange(resolvedVersion)) return resolvedVersion
54+
55+
return resolveNvmAlias(resolvedVersion, {
56+
nvmDir,
57+
previousAliases: [...previousAliases, alias],
58+
})
59+
}
60+
161
// nvm allows several aliases like `lts/*`
2-
export const replaceAliases = function (versionRange) {
62+
export const replaceAliases = function (versionRange, { nvmDir } = {}) {
63+
if (nvmDir) {
64+
return resolveNvmAlias(versionRange, { nvmDir })
65+
}
66+
367
const aliasedVersionRange = ALIASES[versionRange]
468

569
if (aliasedVersionRange === undefined) {

src/aliases.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { version as processVersion } from 'process'
22

3+
import { validRange } from 'semver'
4+
5+
import { replaceAliases } from './alias.js'
36
import { getProjectVersion } from './project.js'
47

58
// `versionRange` can be one of the following aliases:
@@ -8,11 +11,15 @@ import { getProjectVersion } from './project.js'
811
export const resolveAlias = function (versionRange, opts) {
912
const getVersion = ALIASES[versionRange]
1013

11-
if (getVersion === undefined) {
12-
return versionRange
14+
if (getVersion !== undefined) {
15+
return getVersion(opts)
16+
}
17+
18+
if (!validRange(versionRange) && opts.nvmDir) {
19+
return replaceAliases(versionRange, opts)
1320
}
1421

15-
return getVersion(opts)
22+
return versionRange
1623
}
1724

1825
const getCurrentVersion = function () {

src/main.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { getOpts } from './options.js'
88

99
// Retrieve the Node version matching a specific `versionRange`
1010
const normalizeNodeVersion = async function (versionRange, opts) {
11-
const { cwd, ...optsA } = getOpts(opts)
12-
const versionRangeA = await resolveAlias(versionRange, { cwd })
11+
const { cwd, nvmDir, ...optsA } = getOpts(opts)
12+
const versionRangeA = await resolveAlias(versionRange, { cwd, nvmDir })
1313
const versions = await getVersions(versionRangeA, optsA)
1414

1515
const version = maxSatisfying(versions, versionRangeA)

src/options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { cwd as getCwd } from 'process'
1+
import { cwd as getCwd, env as processEnv } from 'process'
22

33
import { validate } from 'jest-validate'
44

@@ -12,6 +12,7 @@ export const getOpts = function (opts = {}) {
1212
const DEFAULT_OPTS = () => ({
1313
cache: true,
1414
cwd: getCwd(),
15+
nvmDir: processEnv.NVM_DIR,
1516
})
1617

1718
const EXAMPLE_OPTS = () => ({

test/aliases.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,16 @@ const VERSIONS = {
1414
nvmrc: '4.2.6',
1515
nodeVersion: '4.3.2',
1616
current: versions.node,
17+
NVM: {
18+
carbon: '8.17.0',
19+
node: '12.16.2',
20+
personal: '12.12.0',
21+
default: '10.10.0',
22+
},
1723
}
1824

25+
const NVM_OPTS = { nvmDir: `${FIXTURES_DIR}/nvm-dir` }
26+
1927
each(
2028
[
2129
{ versionRange: '.', fixture: 'naverc', result: VERSIONS.nave },
@@ -28,6 +36,20 @@ each(
2836
{ versionRange: '.', fixture: 'mixed', result: VERSIONS.nodeVersion },
2937
{ versionRange: '.', opts: { cwd: '/' }, result: VERSIONS.current },
3038
{ versionRange: '_', result: VERSIONS.current },
39+
{ versionRange: 'node', opts: NVM_OPTS, result: VERSIONS.NVM.node },
40+
{ versionRange: 'stable', opts: NVM_OPTS, result: VERSIONS.NVM.node },
41+
{ versionRange: 'latest', opts: NVM_OPTS, result: VERSIONS.NVM.node },
42+
{ versionRange: 'default', opts: NVM_OPTS, result: VERSIONS.NVM.default },
43+
{
44+
versionRange: 'personal-alias',
45+
opts: NVM_OPTS,
46+
result: VERSIONS.NVM.personal,
47+
},
48+
{ versionRange: 'lts/carbon', opts: NVM_OPTS, result: VERSIONS.NVM.carbon },
49+
// implicit lts prefix for lts node codenames
50+
{ versionRange: 'carbon', opts: NVM_OPTS, result: VERSIONS.NVM.carbon },
51+
// recursive resolving
52+
{ versionRange: 'carbon14', opts: NVM_OPTS, result: VERSIONS.NVM.carbon },
3153
],
3254
({ title }, { versionRange, opts, fixture, result }) => {
3355
test(`Resolve aliases | ${title}`, async (t) => {
@@ -39,6 +61,28 @@ each(
3961
},
4062
)
4163

64+
test('Resolve nvm aliases ignored if no NVM_DIR configured', async (t) => {
65+
await t.throwsAsync(
66+
() =>
67+
normalizeNodeVersion('default', {
68+
nvmDir: '',
69+
}),
70+
{ message: 'Invalid Node version: default' },
71+
)
72+
})
73+
74+
test('Resolved nvm alias fails and do not loop infinitely if alias cycle is present', async (t) => {
75+
const AVA_TIMEOUT_CYCLE = 2000
76+
t.timeout(AVA_TIMEOUT_CYCLE)
77+
await t.throwsAsync(() => normalizeNodeVersion('cycle', NVM_OPTS), {
78+
message: 'Nvm alias cycle detected cycle -> ping -> pong -> ping',
79+
})
80+
})
81+
82+
test('Resolve nvm aliases fails if nvm alias does not exists', async (t) => {
83+
await t.throwsAsync(() => normalizeNodeVersion('awol', NVM_OPTS))
84+
})
85+
4286
test.serial('Option cwd defaults to the current directory', async (t) => {
4387
const currentCwd = getCwd()
4488
chdir(`${FIXTURES_DIR}/nvmrc`)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lts/carbon

test/fixtures/nvm-dir/alias/cycle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ping
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v10.10

test/fixtures/nvm-dir/alias/lts/*

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lts/erbium

0 commit comments

Comments
 (0)