feat: clear error on invalid node version (#6916)

related to #6909
This commit is contained in:
Khải
2023-08-10 05:38:06 +07:00
committed by GitHub
parent 96e165c7ff
commit 66423df837
10 changed files with 109 additions and 14 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/node.fetcher": patch
"pnpm": patch
---
Emit a clear error message when users attempt to specify an undownloadable node version [#6916](https://github.com/pnpm/pnpm/pull/6916).

View File

@@ -6,7 +6,7 @@ import { PnpmError } from '@pnpm/error'
import semver from 'semver'
import { getNodeMirror } from './getNodeMirror'
import { getNodeVersionsBaseDir, type NvmNodeCommandOptions } from './node'
import { parseNodeEditionSpecifier } from './parseNodeEditionSpecifier'
import { parseEnvSpecifier } from './parseEnvSpecifier'
import { getNodeExecPathAndTargetDir, getNodeExecPathInNodeDir } from './utils'
export async function envList (opts: NvmNodeCommandOptions, params: string[]) {
@@ -43,7 +43,7 @@ async function listLocalVersions (opts: NvmNodeCommandOptions) {
async function listRemoteVersions (opts: NvmNodeCommandOptions, versionSpec?: string) {
const fetch = createFetchFromRegistry(opts)
const { releaseChannel, versionSpecifier } = parseNodeEditionSpecifier(versionSpec ?? '')
const { releaseChannel, versionSpecifier } = parseEnvSpecifier(versionSpec ?? '')
const nodeMirrorBaseUrl = getNodeMirror(opts.rawConfig, releaseChannel)
const nodeVersionList = await resolveNodeVersions(fetch, versionSpecifier, nodeMirrorBaseUrl)
return nodeVersionList

View File

@@ -6,7 +6,7 @@ import { globalInfo } from '@pnpm/logger'
import { resolveNodeVersion } from '@pnpm/node.resolver'
import { removeBin } from '@pnpm/remove-bins'
import rimraf from '@zkochan/rimraf'
import { parseNodeEditionSpecifier } from './parseNodeEditionSpecifier'
import { parseEnvSpecifier } from './parseEnvSpecifier'
import { getNodeExecPathAndTargetDir } from './utils'
import { getNodeMirror } from './getNodeMirror'
import { getNodeVersionsBaseDir, type NvmNodeCommandOptions } from './node'
@@ -17,7 +17,7 @@ export async function envRemove (opts: NvmNodeCommandOptions, params: string[])
}
const fetch = createFetchFromRegistry(opts)
const { releaseChannel, versionSpecifier } = parseNodeEditionSpecifier(params[0])
const { releaseChannel, versionSpecifier } = parseEnvSpecifier(params[0])
const nodeMirrorBaseUrl = getNodeMirror(opts.rawConfig, releaseChannel)
const nodeVersion = await resolveNodeVersion(fetch, versionSpecifier, nodeMirrorBaseUrl)
const nodeDir = getNodeVersionsBaseDir(opts.pnpmHomeDir)

View File

@@ -9,7 +9,7 @@ import isWindows from 'is-windows'
import symlinkDir from 'symlink-dir'
import { getNodeDir, type NvmNodeCommandOptions } from './node'
import { getNodeMirror } from './getNodeMirror'
import { parseNodeEditionSpecifier } from './parseNodeEditionSpecifier'
import { parseEnvSpecifier } from './parseEnvSpecifier'
import { CURRENT_NODE_DIRNAME, getNodeExecPathInBinDir, getNodeExecPathInNodeDir } from './utils'
export async function envUse (opts: NvmNodeCommandOptions, params: string[]) {
@@ -17,7 +17,7 @@ export async function envUse (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 { releaseChannel, versionSpecifier } = parseNodeEditionSpecifier(params[0])
const { releaseChannel, versionSpecifier } = parseEnvSpecifier(params[0])
const nodeMirrorBaseUrl = getNodeMirror(opts.rawConfig, releaseChannel)
const nodeVersion = await resolveNodeVersion(fetch, versionSpecifier, nodeMirrorBaseUrl)
if (!nodeVersion) {

View File

@@ -8,7 +8,7 @@ import { getStorePath } from '@pnpm/store-path'
import loadJsonFile from 'load-json-file'
import writeJsonFile from 'write-json-file'
import { getNodeMirror } from './getNodeMirror'
import { parseNodeEditionSpecifier } from './parseNodeEditionSpecifier'
import { parseNodeSpecifier } from './parseNodeSpecifier'
export type NvmNodeCommandOptions = Pick<Config,
| 'bin'
@@ -49,11 +49,11 @@ export async function getNodeBinDir (opts: NvmNodeCommandOptions) {
default: wantedNodeVersion,
})
}
const { versionSpecifier, releaseChannel } = parseNodeEditionSpecifier(wantedNodeVersion)
const { useNodeVersion, releaseChannel } = parseNodeSpecifier(wantedNodeVersion)
const nodeMirrorBaseUrl = getNodeMirror(opts.rawConfig, releaseChannel)
const nodeDir = await getNodeDir(fetch, {
...opts,
useNodeVersion: versionSpecifier,
useNodeVersion,
nodeMirrorBaseUrl,
})
return process.platform === 'win32' ? nodeDir : path.join(nodeDir, 'bin')

View File

@@ -1,9 +1,9 @@
export interface NodeEditionSpecifier {
export interface EnvSpecifier {
releaseChannel: string
versionSpecifier: string
}
export function parseNodeEditionSpecifier (specifier: string): NodeEditionSpecifier {
export function parseEnvSpecifier (specifier: string): EnvSpecifier {
if (specifier.includes('/')) {
const [releaseChannel, versionSpecifier] = specifier.split('/')
return { releaseChannel, versionSpecifier }

View File

@@ -0,0 +1,44 @@
import { PnpmError } from '@pnpm/error'
export interface NodeSpecifier {
releaseChannel: string
useNodeVersion: string
}
const isStableVersion = (version: string) => /^[0-9]+\.[0-9]+\.[0-9]+$/.test(version)
const STABLE_RELEASE_ERROR_HINT = 'The correct syntax for stable release is strictly X.Y.Z or release/X.Y.Z'
export function parseNodeSpecifier (specifier: string): NodeSpecifier {
if (specifier.includes('/')) {
const [releaseChannel, useNodeVersion] = specifier.split('/')
if (releaseChannel === 'release') {
if (!isStableVersion(useNodeVersion)) {
throw new PnpmError('INVALID_NODE_VERSION', `"${specifier}" is not a valid node version`, {
hint: STABLE_RELEASE_ERROR_HINT,
})
}
} else if (!useNodeVersion.includes(releaseChannel)) {
throw new PnpmError('MISMATCHED_RELEASE_CHANNEL', `The node version (${useNodeVersion}) must contain the release channel (${releaseChannel})`)
}
return { releaseChannel, useNodeVersion }
}
const prereleaseMatch = specifier.match(/^[0-9]+\.[0-9]+\.[0-9]+-(nightly|rc|test|v8-canary)(\..+)$/)
if (prereleaseMatch != null) {
return { releaseChannel: prereleaseMatch[1], useNodeVersion: specifier }
}
if (isStableVersion(specifier)) {
return { releaseChannel: 'release', useNodeVersion: specifier }
}
let hint: string | undefined
if (['nightly', 'rc', 'test', 'v8-canary'].includes(specifier)) {
hint = `The correct syntax for ${specifier} release is strictly X.Y.Z-${specifier}.W`
} else if (/^[0-9]+\.[0-9]+$/.test(specifier) || /^[0-9]+$/.test(specifier) || ['release', 'stable', 'latest'].includes(specifier)) {
hint = STABLE_RELEASE_ERROR_HINT
}
throw new PnpmError('INVALID_NODE_VERSION', `"${specifier}" is not a valid node version`, { hint })
}

View File

@@ -54,7 +54,7 @@ test('install Node uses node-mirror:release option', async () => {
}
})
test('install and rc version of Node.js', async () => {
test('install an rc version of Node.js', async () => {
tempDir()
const configDir = path.resolve('config')

View File

@@ -1,4 +1,4 @@
import { parseNodeEditionSpecifier } from '../lib/parseNodeEditionSpecifier'
import { parseEnvSpecifier } from '../lib/parseEnvSpecifier'
test.each([
['6', '6', 'release'],
@@ -9,7 +9,7 @@ test.each([
['argon', 'argon', 'release'],
['latest', 'latest', 'release'],
])('Node.js version selector is parsed', (editionSpecifier, versionSpecifier, releaseChannel) => {
const node = parseNodeEditionSpecifier(editionSpecifier)
const node = parseEnvSpecifier(editionSpecifier)
expect(node.versionSpecifier).toMatch(versionSpecifier)
expect(node.releaseChannel).toBe(releaseChannel)
})

View File

@@ -0,0 +1,45 @@
import { parseNodeSpecifier } from '../lib/parseNodeSpecifier'
test.each([
['rc/16.0.0-rc.0', '16.0.0-rc.0', 'rc'],
['16.0.0-rc.0', '16.0.0-rc.0', 'rc'],
['release/16.0.0', '16.0.0', 'release'],
['16.0.0', '16.0.0', 'release'],
])('Node.js version selector is parsed', (editionSpecifier, useNodeVersion, releaseChannel) => {
const node = parseNodeSpecifier(editionSpecifier)
expect(node.useNodeVersion).toBe(useNodeVersion)
expect(node.releaseChannel).toBe(releaseChannel)
})
test.each([
['rc/10', '10', 'rc'],
['rc/10.0', '10.0', 'rc'],
['rc/10.0.0', '10.0.0', 'rc'],
['rc/10.0.0.test.0', '10.0.0.test.0', 'rc'],
])('invalid Node.js specifier', (editionSpecifier, useNodeVersion, releaseChannel) => {
expect(() => parseNodeSpecifier(editionSpecifier)).toThrow(`The node version (${useNodeVersion}) must contain the release channel (${releaseChannel})`)
})
test.each([
['nightly'],
['rc'],
['test'],
['v8-canary'],
])('invalid Node.js specifier', async (specifier) => {
const promise = Promise.resolve().then(() => parseNodeSpecifier(specifier))
await expect(promise).rejects.toThrow(`"${specifier}" is not a valid node version`)
await expect(promise).rejects.toHaveProperty('hint', `The correct syntax for ${specifier} release is strictly X.Y.Z-${specifier}.W`)
})
test.each([
['release'],
['stable'],
['latest'],
['release/16.0.0.release.0'],
['16'],
['16.0'],
])('invalid Node.js specifier', async (specifier) => {
const promise = Promise.resolve().then(() => parseNodeSpecifier(specifier))
await expect(promise).rejects.toThrow(`"${specifier}" is not a valid node version`)
await expect(promise).rejects.toHaveProperty('hint', 'The correct syntax for stable release is strictly X.Y.Z or release/X.Y.Z')
})