Files
pnpm/env/node.resolver/src/parseNodeSpecifier.ts
Zoltan Kochan 23eb4a6141 refactor(env): unify node version specifier parsing into parseNodeSpecifier in node.resolver (#10668)
* refactor(env): unify node version specifier parsing into parseNodeSpecifier in node.resolver

Move parseNodeSpecifier from @pnpm/plugin-commands-env to @pnpm/node.resolver and
replace the simpler parseEnvSpecifier with an enhanced version that supports all
Node.js version specifier formats: standalone release channels (nightly, rc, test,
v8-canary, release), well-known aliases (lts, latest), LTS codenames (argon, iron),
semver ranges (18, ^18), and channel/version combos (rc/18, nightly/latest).

* fix(env): address parseNodeSpecifier review feedback

- Remove overly strict release/X.Y.Z-only validation; release/latest,
  release/lts, and release/<range> are now accepted
- Validate unknown release channels (e.g. foo/18) with a clear error
  instead of letting them fall through to a confusing network failure
- Add test cases for release/latest, release/lts, and release/18
2026-02-22 14:34:02 +01:00

52 lines
2.2 KiB
TypeScript

import { PnpmError } from '@pnpm/error'
export interface NodeSpecifier {
releaseChannel: string
versionSpecifier: string
}
const RELEASE_CHANNELS = ['nightly', 'rc', 'test', 'v8-canary', 'release']
const isStableVersion = (version: string): boolean => /^\d+\.\d+\.\d+$/.test(version)
export function parseNodeSpecifier (specifier: string): NodeSpecifier {
// Handle "channel/version" format: "rc/18", "rc/18.0.0-rc.4", "release/22.0.0", "nightly/latest"
if (specifier.includes('/')) {
const [releaseChannel, versionSpecifier] = specifier.split('/', 2)
if (!RELEASE_CHANNELS.includes(releaseChannel)) {
throw new PnpmError('INVALID_NODE_RELEASE_CHANNEL', `"${releaseChannel}" is not a valid Node.js release channel`, {
hint: `Valid release channels are: ${RELEASE_CHANNELS.join(', ')}`,
})
}
return { releaseChannel, versionSpecifier }
}
// Exact prerelease version with a recognized release channel suffix.
// e.g. "22.0.0-rc.4", "22.0.0-nightly20250315d765e70802", "22.0.0-v8-canary2025..."
const prereleaseChannelMatch = specifier.match(/^\d+\.\d+\.\d+-(nightly|rc|test|v8-canary)/)
if (prereleaseChannelMatch != null) {
return { releaseChannel: prereleaseChannelMatch[1], versionSpecifier: specifier }
}
// Exact stable version: "22.0.0"
if (isStableVersion(specifier)) {
return { releaseChannel: 'release', versionSpecifier: specifier }
}
// Standalone release channel name means "latest from that channel".
// e.g. "nightly" → latest nightly, "rc" → latest rc, "release" → latest release
if (RELEASE_CHANNELS.includes(specifier)) {
return { releaseChannel: specifier, versionSpecifier: 'latest' }
}
// Well-known version aliases on the stable release channel
if (specifier === 'lts' || specifier === 'latest') {
return { releaseChannel: 'release', versionSpecifier: specifier }
}
// Semver ranges ("18", "^18", ">=18", "18.x") and LTS codenames ("argon", "iron", "hydrogen")
// are all passed through as versionSpecifier on the release channel.
// Any truly invalid input will fail at resolution time with NODEJS_VERSION_NOT_FOUND.
return { releaseChannel: 'release', versionSpecifier: specifier }
}