fix(env): use-node-version should work with prerelease versions (#4903)

This commit is contained in:
Zoltan Kochan
2022-06-21 01:55:12 +03:00
committed by GitHub
parent c2836c9d95
commit d1df380abe
8 changed files with 126 additions and 71 deletions

View File

@@ -0,0 +1,11 @@
---
"@pnpm/plugin-commands-env": patch
"pnpm": patch
---
The `use-node-version` setting should work with prerelease Node.js versions. For instance:
```
use-node-version=18.0.0-rc.3
```

View File

@@ -6,7 +6,9 @@ import { createFetchFromRegistry } from '@pnpm/fetch'
import cmdShim from '@zkochan/cmd-shim'
import renderHelp from 'render-help'
import { getNodeDir, NvmNodeCommandOptions } from './node'
import getNodeMirror from './getNodeMirror'
import resolveNodeVersion from './resolveNodeVersion'
import { parseNodeVersionSelector } from './parseNodeVersionSelector'
export function rcOptionsTypes () {
return {}
@@ -58,14 +60,16 @@ export async function handler (opts: NvmNodeCommandOptions, params: string[]) {
throw new PnpmError('NOT_IMPLEMENTED_YET', '"pnpm env use <version>" can only be used with the "--global" option currently')
}
const fetch = createFetchFromRegistry(opts)
const { version: nodeVersion, releaseDir } = await resolveNodeVersion(fetch, params[1], opts.rawConfig)
const { releaseDir, version } = parseNodeVersionSelector(params[1])
const nodeMirrorBaseUrl = getNodeMirror(opts.rawConfig, releaseDir)
const nodeVersion = await resolveNodeVersion(fetch, version, nodeMirrorBaseUrl)
if (!nodeVersion) {
throw new PnpmError('COULD_NOT_RESOLVE_NODEJS', `Couldn't find Node.js version matching ${params[1]}`)
}
const nodeDir = await getNodeDir(fetch, {
...opts,
useNodeVersion: nodeVersion,
releaseDir,
nodeMirrorBaseUrl,
})
const src = path.join(nodeDir, process.platform === 'win32' ? 'node.exe' : 'bin/node')
const dest = path.join(opts.bin, process.platform === 'win32' ? 'node.exe' : 'node')

View File

@@ -13,6 +13,7 @@ import loadJsonFile from 'load-json-file'
import writeJsonFile from 'write-json-file'
import normalizeArch from './normalizeArch'
import getNodeMirror from './getNodeMirror'
import { parseNodeVersionSelector } from './parseNodeVersionSelector'
export type NvmNodeCommandOptions = Pick<Config,
| 'bin'
@@ -39,14 +40,8 @@ export type NvmNodeCommandOptions = Pick<Config,
export async function getNodeBinDir (opts: NvmNodeCommandOptions) {
const fetch = createFetchFromRegistry(opts)
const nodeDir = await getNodeDir(fetch, opts)
return process.platform === 'win32' ? nodeDir : path.join(nodeDir, 'bin')
}
export async function getNodeDir (fetch: FetchFromRegistry, opts: NvmNodeCommandOptions & { releaseDir?: string }) {
const nodesDir = path.join(opts.pnpmHomeDir, 'nodejs')
const nodesDir = getNodeVersionsBaseDir(opts.pnpmHomeDir)
let wantedNodeVersion = opts.useNodeVersion ?? (await readNodeVersionsManifest(nodesDir))?.default
await fs.promises.mkdir(nodesDir, { recursive: true })
if (wantedNodeVersion == null) {
const response = await fetch('https://registry.npmjs.org/node')
wantedNodeVersion = (await response.json() as any)['dist-tags'].lts // eslint-disable-line
@@ -57,16 +52,32 @@ export async function getNodeDir (fetch: FetchFromRegistry, opts: NvmNodeCommand
default: wantedNodeVersion,
})
}
const versionDir = path.join(nodesDir, wantedNodeVersion)
const { version, releaseDir } = parseNodeVersionSelector(wantedNodeVersion)
const nodeMirrorBaseUrl = getNodeMirror(opts.rawConfig, releaseDir)
const nodeDir = await getNodeDir(fetch, {
...opts,
useNodeVersion: version,
nodeMirrorBaseUrl,
})
return process.platform === 'win32' ? nodeDir : path.join(nodeDir, 'bin')
}
function getNodeVersionsBaseDir (pnpmHomeDir: string) {
return path.join(pnpmHomeDir, 'nodejs')
}
export async function getNodeDir (fetch: FetchFromRegistry, opts: NvmNodeCommandOptions & { useNodeVersion: string, nodeMirrorBaseUrl: string }) {
const nodesDir = getNodeVersionsBaseDir(opts.pnpmHomeDir)
await fs.promises.mkdir(nodesDir, { recursive: true })
const versionDir = path.join(nodesDir, opts.useNodeVersion)
if (!fs.existsSync(versionDir)) {
await installNode(fetch, wantedNodeVersion, versionDir, opts)
await installNode(fetch, opts.useNodeVersion, versionDir, opts)
}
return versionDir
}
async function installNode (fetch: FetchFromRegistry, wantedNodeVersion: string, versionDir: string, opts: NvmNodeCommandOptions & { releaseDir?: string }) {
const nodeMirror = getNodeMirror(opts.rawConfig, opts.releaseDir ?? 'release')
const { tarball, pkgName } = getNodeJSTarball(wantedNodeVersion, nodeMirror)
async function installNode (fetch: FetchFromRegistry, wantedNodeVersion: string, versionDir: string, opts: NvmNodeCommandOptions & { nodeMirrorBaseUrl: string }) {
const { tarball, pkgName } = getNodeJSTarball(wantedNodeVersion, opts.nodeMirrorBaseUrl)
if (tarball.endsWith('.zip')) {
await downloadAndUnpackZip(fetch, tarball, versionDir, pkgName)
return

View File

@@ -0,0 +1,14 @@
export 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 }
}

View File

@@ -1,47 +1,22 @@
import { Config } from '@pnpm/config'
import { FetchFromRegistry } from '@pnpm/fetch'
import semver from 'semver'
import versionSelectorType from 'version-selector-type'
import getNodeMirror from './getNodeMirror'
interface NodeVersion {
version: string
lts: false | string
}
export default async function resolveNodeVersion (fetch: FetchFromRegistry, rawVersionSelector: string, rawConfig: Config['rawConfig']) {
const { releaseDir, version } = parseNodeVersionSelector(rawVersionSelector)
const nodeMirrorBaseUrl = getNodeMirror(rawConfig, releaseDir)
export default async function resolveNodeVersion (fetch: FetchFromRegistry, version: string, nodeMirrorBaseUrl: string): Promise<string | null> {
const response = await fetch(`${nodeMirrorBaseUrl}index.json`)
const allVersions = (await response.json()) as NodeVersion[]
if (version === 'latest') {
return {
version: allVersions[0].version.substring(1),
releaseDir,
}
return allVersions[0].version.substring(1)
}
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 }
if (!pickedVersion) return null
return pickedVersion.substring(1)
}
function filterVersions (versions: NodeVersion[], versionSelector: string) {

View File

@@ -5,6 +5,28 @@ import { Readable } from 'stream'
import { node } from '@pnpm/plugin-commands-env'
import { tempDir } from '@pnpm/prepare'
const fetchMock = jest.fn(async (url: string) => {
if (url.endsWith('.zip')) {
// The Windows code path for pnpm's node bootstrapping expects a subdir
// within the .zip file.
const pkgName = path.basename(url, '.zip')
const zip = new AdmZip()
zip.addFile(`${pkgName}/dummy-file`, Buffer.from('test'))
return new Response(Readable.from(zip.toBuffer()))
}
return new Response(Readable.from(Buffer.alloc(0)))
})
jest.mock('@pnpm/fetch', () => ({
createFetchFromRegistry: () => fetchMock,
}))
beforeEach(() => {
fetchMock.mockClear()
})
test('check API (placeholder test)', async () => {
expect(typeof node.getNodeDir).toBe('function')
})
@@ -25,23 +47,29 @@ test('install Node uses node-mirror:release option', async () => {
useNodeVersion: '16.4.0',
}
const fetchMock = jest.fn(async (url: string) => {
if (url.endsWith('.zip')) {
// The Windows code path for pnpm's node bootstrapping expects a subdir
// within the .zip file.
const pkgName = path.basename(url, '.zip')
const zip = new AdmZip()
zip.addFile(`${pkgName}/dummy-file`, Buffer.from('test'))
return new Response(Readable.from(zip.toBuffer()))
}
return new Response(Readable.from(Buffer.alloc(0)))
})
await node.getNodeDir(fetchMock, opts)
await node.getNodeBinDir(opts)
for (const call of fetchMock.mock.calls) {
expect(call[0]).toMatch(nodeMirrorRelease)
}
})
test('install and rc version of Node.js', async () => {
tempDir()
const configDir = path.resolve('config')
const opts: node.NvmNodeCommandOptions = {
bin: process.cwd(),
configDir,
global: true,
pnpmHomeDir: process.cwd(),
rawConfig: {},
useNodeVersion: 'rc/18.0.0-rc.3',
}
await node.getNodeBinDir(opts)
const platform = process.platform === 'win32' ? 'win' : process.platform
const extension = process.platform === 'win32' ? 'zip' : 'tar.gz'
expect(fetchMock.mock.calls[0][0]).toBe(`https://nodejs.org/download/rc/v18.0.0-rc.3/node-v18.0.0-rc.3-${platform}-x64.${extension}`)
})

View File

@@ -0,0 +1,15 @@
import { parseNodeVersionSelector } from '../lib/parseNodeVersionSelector'
test.each([
['6', '6', 'release'],
['16.0.0-rc.0', '16.0.0-rc.0', 'rc'],
['rc/10', '10', 'rc'],
['nightly', 'latest', 'nightly'],
['lts', 'lts', 'release'],
['argon', 'argon', 'release'],
['latest', 'latest', 'release'],
])('Node.js version selector is parsed', (spec, version, releaseDir) => {
const node = parseNodeVersionSelector(spec)
expect(node.version).toMatch(version)
expect(node.releaseDir).toBe(releaseDir)
})

View File

@@ -3,18 +3,15 @@ import resolveNodeVersion from '../lib/resolveNodeVersion'
const fetch = createFetchFromRegistry({})
const rawConfig = {}
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(fetch, spec, rawConfig)
expect(node.version).toMatch(version)
expect(node.releaseDir).toBe(releaseDir)
['https://nodejs.org/download/release/', '6', '6.17.1'],
['https://nodejs.org/download/rc/', '16.0.0-rc.0', '16.0.0-rc.0'],
['https://nodejs.org/download/rc/', '10', '10.23.0-rc.0'],
['https://nodejs.org/download/nightly/', 'latest', /.+/],
['https://nodejs.org/download/release/', 'lts', /.+/],
['https://nodejs.org/download/release/', 'argon', '4.9.1'],
['https://nodejs.org/download/release/', 'latest', /.+/],
])('Node.js %s is resolved', async (nodeMirrorBaseUrl, spec, expectedVersion) => {
const version = await resolveNodeVersion(fetch, spec, nodeMirrorBaseUrl)
expect(version).toMatch(expectedVersion)
})