mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-11 09:28:24 -04:00
feat: installing prerelease Node.js versions (#3892)
ref https://github.com/pnpm/pnpm/discussions/3857
This commit is contained in:
5
.changeset/calm-laws-stare.md
Normal file
5
.changeset/calm-laws-stare.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-env": minor
|
||||
---
|
||||
|
||||
Install prerelease Node.js versions.
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
60
packages/plugin-commands-env/src/resolveNodeVersion.ts
Normal file
60
packages/plugin-commands-env/src/resolveNodeVersion.ts
Normal 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 }
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
15
packages/plugin-commands-env/test/resolveNodeVersion.test.ts
Normal file
15
packages/plugin-commands-env/test/resolveNodeVersion.test.ts
Normal 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)
|
||||
})
|
||||
Reference in New Issue
Block a user