diff --git a/.changeset/calm-laws-stare.md b/.changeset/calm-laws-stare.md new file mode 100644 index 0000000000..8cf89ee181 --- /dev/null +++ b/.changeset/calm-laws-stare.md @@ -0,0 +1,5 @@ +--- +"@pnpm/plugin-commands-env": minor +--- + +Install prerelease Node.js versions. diff --git a/packages/plugin-commands-env/src/env.ts b/packages/plugin-commands-env/src/env.ts index 8d1a823776..25df330b5f 100644 --- a/packages/plugin-commands-env/src/env.ts +++ b/packages/plugin-commands-env/src/env.ts @@ -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 " 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 } -} diff --git a/packages/plugin-commands-env/src/node.ts b/packages/plugin-commands-env/src/node.ts index 63eddc08e7..4372d75a03 100644 --- a/packages/plugin-commands-env/src/node.ts +++ b/packages/plugin-commands-env/src/node.ts @@ -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}`, } } diff --git a/packages/plugin-commands-env/src/resolveNodeVersion.ts b/packages/plugin-commands-env/src/resolveNodeVersion.ts new file mode 100644 index 0000000000..6d9051d135 --- /dev/null +++ b/packages/plugin-commands-env/src/resolveNodeVersion.ts @@ -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 } +} diff --git a/packages/plugin-commands-env/test/env.test.ts b/packages/plugin-commands-env/test/env.test.ts index 923ceba9cf..af02e58a40 100644 --- a/packages/plugin-commands-env/test/env.test.ts +++ b/packages/plugin-commands-env/test/env.test.ts @@ -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() diff --git a/packages/plugin-commands-env/test/resolveNodeVersion.test.ts b/packages/plugin-commands-env/test/resolveNodeVersion.test.ts new file mode 100644 index 0000000000..851bb35cbf --- /dev/null +++ b/packages/plugin-commands-env/test/resolveNodeVersion.test.ts @@ -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) +})