feat: installing prerelease Node.js versions (#3892)

ref https://github.com/pnpm/pnpm/discussions/3857
This commit is contained in:
Zoltan Kochan
2021-10-19 11:39:14 +03:00
committed by GitHub
parent 72b78186a5
commit 37905fcf7c
6 changed files with 89 additions and 125 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-env": minor
---
Install prerelease Node.js versions.

View File

@@ -2,12 +2,10 @@ import { promises as fs } from 'fs'
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'
import resolveNodeVersion from './resolveNodeVersion'
export function rcOptionsTypes () {
return {}
@@ -44,6 +42,7 @@ export function help () {
'pnpm env use --global lts',
'pnpm env use --global argon',
'pnpm env use --global latest',
'pnpm env use --global rc/16',
],
})
}
@@ -57,13 +56,14 @@ export async function handler (opts: NvmNodeCommandOptions, params: string[]) {
if (!opts.global) {
throw new PnpmError('NOT_IMPLEMENTED_YET', '"pnpm env use <version>" can only be used with the "--global" option currently')
}
const nodeVersion = await resolveNodeVersion(params[1])
const { version: nodeVersion, releaseDir } = 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: nodeVersion,
releaseDir,
})
const src = path.join(nodeDir, process.platform === 'win32' ? 'node.exe' : 'bin/node')
const dest = path.join(opts.bin, process.platform === 'win32' ? 'node.exe' : 'node')
@@ -97,38 +97,3 @@ 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[]
if (rawVersionSelector === 'latest') {
return allVersions[0].version.substring(1)
}
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 }
}

View File

@@ -40,7 +40,7 @@ export async function getNodeBinDir (opts: NvmNodeCommandOptions) {
return process.platform === 'win32' ? nodeDir : path.join(nodeDir, 'bin')
}
export async function getNodeDir (opts: NvmNodeCommandOptions) {
export async function getNodeDir (opts: NvmNodeCommandOptions & { releaseDir?: string }) {
const nodesDir = path.join(opts.pnpmHomeDir, 'nodejs')
let wantedNodeVersion = opts.useNodeVersion ?? (await readNodeVersionsManifest(nodesDir))?.default
await fs.promises.mkdir(nodesDir, { recursive: true })
@@ -61,9 +61,9 @@ export async function getNodeDir (opts: NvmNodeCommandOptions) {
return versionDir
}
async function installNode (wantedNodeVersion: string, versionDir: string, opts: NvmNodeCommandOptions) {
async function installNode (wantedNodeVersion: string, versionDir: string, opts: NvmNodeCommandOptions & { releaseDir?: string }) {
await fs.promises.mkdir(versionDir, { recursive: true })
const { tarball, pkgName } = getNodeJSTarball(wantedNodeVersion)
const { tarball, pkgName } = getNodeJSTarball(wantedNodeVersion, opts.releaseDir ?? 'release')
const fetchFromRegistry = createFetchFromRegistry(opts)
if (tarball.endsWith('.zip')) {
await downloadAndUnpackZip(fetchFromRegistry, tarball, versionDir, pkgName)
@@ -113,14 +113,14 @@ async function downloadAndUnpackZip (
await fs.promises.unlink(tmp)
}
function getNodeJSTarball (nodeVersion: string) {
function getNodeJSTarball (nodeVersion: string, releaseDir: string) {
const platform = process.platform === 'win32' ? 'win' : process.platform
const arch = platform === 'win' && process.arch === 'ia32' ? 'x86' : process.arch
const extension = platform === 'win' ? 'zip' : 'tar.gz'
const pkgName = `node-v${nodeVersion}-${platform}-${arch}`
return {
pkgName,
tarball: `https://nodejs.org/download/release/v${nodeVersion}/${pkgName}.${extension}`,
tarball: `https://nodejs.org/download/${releaseDir}/v${nodeVersion}/${pkgName}.${extension}`,
}
}

View File

@@ -0,0 +1,60 @@
import fetch from '@pnpm/fetch'
import semver from 'semver'
import versionSelectorType from 'version-selector-type'
interface NodeVersion {
version: string
lts: false | string
}
export default async function resolveNodeVersion (rawVersionSelector: string) {
const { releaseDir, version } = parseNodeVersionSelector(rawVersionSelector)
const response = await fetch(`https://nodejs.org/download/${releaseDir}/index.json`)
const allVersions = (await response.json()) as NodeVersion[]
if (version === 'latest') {
return {
version: allVersions[0].version.substring(1),
releaseDir,
}
}
const { versions, versionSelector } = filterVersions(allVersions, version)
const pickedVersion = semver.maxSatisfying(versions.map(({ version }) => version), versionSelector, { includePrerelease: true, loose: true })
if (!pickedVersion) return { version: null, releaseDir }
return {
version: pickedVersion.substring(1),
releaseDir,
}
}
function parseNodeVersionSelector (rawVersionSelector: string) {
if (rawVersionSelector.includes('/')) {
const [releaseDir, version] = rawVersionSelector.split('/')
return { releaseDir, version }
}
const prereleaseMatch = rawVersionSelector.match(/-(nightly|rc|test|v8-canary)/)
if (prereleaseMatch != null) {
return { releaseDir: prereleaseMatch[1], version: rawVersionSelector }
}
if (['nightly', 'rc', 'test', 'release', 'v8-canary'].includes(rawVersionSelector)) {
return { releaseDir: rawVersionSelector, version: 'latest' }
}
return { releaseDir: 'release', version: rawVersionSelector }
}
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 }
}

View File

@@ -49,87 +49,6 @@ test('install Node (and npm, npx) by exact version of Node.js', async () => {
}
})
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('install the latest Node.js', async () => {
tempDir()
await env.handler({
bin: process.cwd(),
global: true,
pnpmHomeDir: process.cwd(),
rawConfig: {},
}, ['use', 'latest'])
const { stdout } = execa.sync('node', ['-v'], {
env: {
[PATH]: `${process.cwd()}${path.delimiter}${process.env[PATH] as string}`,
},
})
expect(stdout.toString()).toMatch(/^v/)
})
test('fail if a non-existend Node.js version is tried to be installed', async () => {
tempDir()

View File

@@ -0,0 +1,15 @@
import resolveNodeVersion from '@pnpm/plugin-commands-env/lib/resolveNodeVersion'
test.each([
['6', '6.17.1', 'release'],
['16.0.0-rc.0', '16.0.0-rc.0', 'rc'],
['rc/10', '10.23.0-rc.0', 'rc'],
['nightly', /.+/, 'nightly'],
['lts', /.+/, 'release'],
['argon', '4.9.1', 'release'],
['latest', /.+/, 'release'],
])('Node.js %s is resolved', async (spec, version, releaseDir) => {
const node = await resolveNodeVersion(spec)
expect(node.version).toMatch(version)
expect(node.releaseDir).toBe(releaseDir)
})