refactor: rename packages and consolidate runtime resolvers (#10999)

* refactor: rename workspace.sort-packages and workspace.pkgs-graph

- workspace.sort-packages -> workspace.projects-sorter
- workspace.pkgs-graph -> workspace.projects-graph

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: rename packages/ to core/ and pkg-manifest.read-package-json to reader

- Rename packages/ directory to core/ for clarity
- Rename pkg-manifest/read-package-json to pkg-manifest/reader (@pnpm/pkg-manifest.reader)
- Update all tsconfig, package.json, and lockfile references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: consolidate runtime resolvers under engine/runtime domain

- Remove unused @pnpm/engine.runtime.node.fetcher package
- Rename engine/runtime/node.resolver to node-resolver (dash convention)
- Move resolving/bun-resolver to engine/runtime/bun-resolver
- Move resolving/deno-resolver to engine/runtime/deno-resolver
- Update all package names, tsconfig paths, and lockfile references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update lockfile after removing node.fetcher

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: sort tsconfig references and package.json deps alphabetically

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: auto-fix import sorting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update __typings__ paths in tsconfig.lint.json for moved resolvers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove deno-resolver from deps of bun-resolver

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Zoltan Kochan
2026-03-18 00:19:58 +01:00
committed by GitHub
parent 4a36b9a110
commit dba4153767
350 changed files with 1621 additions and 3205 deletions

View File

@@ -0,0 +1,809 @@
# @pnpm/node.resolver
## 1001.0.5
### Patch Changes
- Updated dependencies [7c1382f]
- Updated dependencies [7c1382f]
- Updated dependencies [dee39ec]
- @pnpm/types@1000.9.0
- @pnpm/resolver-base@1005.1.0
- @pnpm/config@1004.4.2
## 1001.0.4
### Patch Changes
- Updated dependencies [9865167]
- @pnpm/config@1004.4.1
## 1001.0.3
### Patch Changes
- Updated dependencies [fb4da0c]
- @pnpm/config@1004.4.0
- @pnpm/crypto.shasums-file@1001.0.2
## 1001.0.2
### Patch Changes
- Updated dependencies [6365bc4]
- @pnpm/constants@1001.3.1
- @pnpm/config@1004.3.1
- @pnpm/error@1000.0.5
- @pnpm/crypto.shasums-file@1001.0.1
## 1001.0.1
### Patch Changes
- Updated dependencies [38e2599]
- Updated dependencies [e792927]
- @pnpm/config@1004.3.0
- @pnpm/types@1000.8.0
- @pnpm/resolver-base@1005.0.1
## 1001.0.0
### Major Changes
- d1edf73: Removed node fetcher. The binary fetcher should be used for downloading node assets.
- f91922c: Changed how the integrity of the node.js artifact is stored in the lockfile.
### Patch Changes
- Updated dependencies [d1edf73]
- Updated dependencies [86b33e9]
- Updated dependencies [86b33e9]
- Updated dependencies [d1edf73]
- Updated dependencies [f91922c]
- @pnpm/constants@1001.3.0
- @pnpm/resolver-base@1005.0.0
- @pnpm/crypto.shasums-file@1001.0.0
- @pnpm/config@1004.2.1
- @pnpm/error@1000.0.4
## 1000.1.0
### Minor Changes
- 1a07b8f: Added support for resolving and downloading the Node.js runtime specified in the [devEngines](https://github.com/openjs-foundation/package-metadata-interoperability-collab-space/issues/15) field of `package.json`.
Usage example:
```json
{
"devEngines": {
"runtime": {
"name": "node",
"version": "^24.4.0",
"onFail": "download"
}
}
}
```
When running `pnpm install`, pnpm will resolve Node.js to the latest version that satisfies the specified range and install it as a dependency of the project. As a result, when running scripts, the locally installed Node.js version will be used.
Unlike the existing options, `useNodeVersion` and `executionEnv.nodeVersion`, this new field supports version ranges, which are locked to exact versions during installation. The resolved version is stored in the pnpm lockfile, along with an integrity checksum for future validation of the Node.js content's validity.
Related PR: [#9755](https://github.com/pnpm/pnpm/pull/9755).
### Patch Changes
- Updated dependencies [1a07b8f]
- Updated dependencies [1ba2e15]
- Updated dependencies [1a07b8f]
- Updated dependencies [6f7ac0f]
- Updated dependencies [1a07b8f]
- Updated dependencies [1a07b8f]
- @pnpm/types@1000.7.0
- @pnpm/fetching-types@1000.2.0
- @pnpm/crypto.shasums-file@1000.0.0
- @pnpm/config@1004.2.0
- @pnpm/resolver-base@1004.1.0
- @pnpm/constants@1001.2.0
- @pnpm/error@1000.0.3
- @pnpm/crypto.hash@1000.2.0
## 1000.0.20
### Patch Changes
- @pnpm/node.fetcher@1000.0.20
## 1000.0.19
### Patch Changes
- @pnpm/node.fetcher@1000.0.19
## 1000.0.18
### Patch Changes
- @pnpm/node.fetcher@1000.0.18
## 1000.0.17
### Patch Changes
- @pnpm/node.fetcher@1000.0.17
## 1000.0.16
### Patch Changes
- @pnpm/node.fetcher@1000.0.16
## 1000.0.15
### Patch Changes
- @pnpm/node.fetcher@1000.0.15
## 1000.0.14
### Patch Changes
- @pnpm/node.fetcher@1000.0.14
## 1000.0.13
### Patch Changes
- @pnpm/node.fetcher@1000.0.13
## 1000.0.12
### Patch Changes
- @pnpm/node.fetcher@1000.0.12
## 1000.0.11
### Patch Changes
- @pnpm/node.fetcher@1000.0.11
## 1000.0.10
### Patch Changes
- @pnpm/node.fetcher@1000.0.10
## 1000.0.9
### Patch Changes
- @pnpm/node.fetcher@1000.0.9
## 1000.0.8
### Patch Changes
- @pnpm/node.fetcher@1000.0.8
## 1000.0.7
### Patch Changes
- @pnpm/node.fetcher@1000.0.7
## 1000.0.6
### Patch Changes
- @pnpm/node.fetcher@1000.0.6
## 1000.0.5
### Patch Changes
- @pnpm/node.fetcher@1000.0.5
## 1000.0.4
### Patch Changes
- @pnpm/node.fetcher@1000.0.4
## 1000.0.3
### Patch Changes
- @pnpm/node.fetcher@1000.0.3
## 1000.0.2
### Patch Changes
- @pnpm/node.fetcher@1000.0.2
## 1000.0.1
### Patch Changes
- Updated dependencies [b0f3c71]
- @pnpm/fetching-types@1000.1.0
- @pnpm/node.fetcher@1000.0.1
## 3.0.17
### Patch Changes
- @pnpm/node.fetcher@4.0.17
## 3.0.16
### Patch Changes
- @pnpm/node.fetcher@4.0.16
## 3.0.15
### Patch Changes
- @pnpm/node.fetcher@4.0.15
## 3.0.14
### Patch Changes
- @pnpm/node.fetcher@4.0.14
## 3.0.13
### Patch Changes
- @pnpm/node.fetcher@4.0.13
## 3.0.12
### Patch Changes
- @pnpm/node.fetcher@4.0.12
## 3.0.11
### Patch Changes
- @pnpm/node.fetcher@4.0.11
## 3.0.10
### Patch Changes
- @pnpm/node.fetcher@4.0.10
## 3.0.9
### Patch Changes
- @pnpm/node.fetcher@4.0.9
## 3.0.8
### Patch Changes
- Updated dependencies [afe520d]
- @pnpm/node.fetcher@4.0.8
## 3.0.7
### Patch Changes
- @pnpm/node.fetcher@4.0.7
## 3.0.6
### Patch Changes
- @pnpm/node.fetcher@4.0.6
## 3.0.5
### Patch Changes
- @pnpm/node.fetcher@4.0.5
## 3.0.4
### Patch Changes
- @pnpm/node.fetcher@4.0.4
## 3.0.3
### Patch Changes
- @pnpm/node.fetcher@4.0.3
## 3.0.2
### Patch Changes
- @pnpm/node.fetcher@4.0.2
## 3.0.1
### Patch Changes
- @pnpm/node.fetcher@4.0.1
## 3.0.0
### Major Changes
- 43cdd87: Node.js v16 support dropped. Use at least Node.js v18.12.
### Patch Changes
- Updated dependencies [43cdd87]
- Updated dependencies [730929e]
- @pnpm/fetching-types@6.0.0
- @pnpm/node.fetcher@4.0.0
## 2.0.40
### Patch Changes
- @pnpm/node.fetcher@3.0.39
## 2.0.39
### Patch Changes
- @pnpm/node.fetcher@3.0.38
## 2.0.38
### Patch Changes
- @pnpm/node.fetcher@3.0.37
## 2.0.37
### Patch Changes
- Updated dependencies [33313d2fd]
- @pnpm/node.fetcher@3.0.36
## 2.0.36
### Patch Changes
- @pnpm/node.fetcher@3.0.35
## 2.0.35
### Patch Changes
- @pnpm/node.fetcher@3.0.34
## 2.0.34
### Patch Changes
- @pnpm/node.fetcher@3.0.33
## 2.0.33
### Patch Changes
- @pnpm/node.fetcher@3.0.32
## 2.0.32
### Patch Changes
- @pnpm/node.fetcher@3.0.31
## 2.0.31
### Patch Changes
- @pnpm/node.fetcher@3.0.30
## 2.0.30
### Patch Changes
- @pnpm/node.fetcher@3.0.29
## 2.0.29
### Patch Changes
- @pnpm/node.fetcher@3.0.28
## 2.0.28
### Patch Changes
- @pnpm/node.fetcher@3.0.27
## 2.0.27
### Patch Changes
- @pnpm/node.fetcher@3.0.26
## 2.0.26
### Patch Changes
- @pnpm/node.fetcher@3.0.25
## 2.0.25
### Patch Changes
- @pnpm/node.fetcher@3.0.24
## 2.0.24
### Patch Changes
- @pnpm/node.fetcher@3.0.23
## 2.0.23
### Patch Changes
- @pnpm/node.fetcher@3.0.22
## 2.0.22
### Patch Changes
- @pnpm/node.fetcher@3.0.21
## 2.0.21
### Patch Changes
- @pnpm/node.fetcher@3.0.20
## 2.0.20
### Patch Changes
- Updated dependencies [9caa33d53]
- @pnpm/node.fetcher@4.0.0
## 2.0.19
### Patch Changes
- @pnpm/node.fetcher@3.0.18
## 2.0.18
### Patch Changes
- @pnpm/node.fetcher@3.0.17
## 2.0.17
### Patch Changes
- @pnpm/node.fetcher@3.0.16
## 2.0.16
### Patch Changes
- @pnpm/node.fetcher@3.0.15
## 2.0.15
### Patch Changes
- Updated dependencies [66423df83]
- @pnpm/node.fetcher@3.0.14
## 2.0.14
### Patch Changes
- @pnpm/node.fetcher@3.0.13
## 2.0.13
### Patch Changes
- @pnpm/node.fetcher@3.0.12
## 2.0.12
### Patch Changes
- @pnpm/node.fetcher@3.0.11
## 2.0.11
### Patch Changes
- @pnpm/node.fetcher@3.0.10
## 2.0.10
### Patch Changes
- @pnpm/node.fetcher@3.0.9
## 2.0.9
### Patch Changes
- @pnpm/node.fetcher@3.0.8
## 2.0.8
### Patch Changes
- @pnpm/node.fetcher@3.0.7
## 2.0.7
### Patch Changes
- @pnpm/node.fetcher@3.0.6
## 2.0.6
### Patch Changes
- @pnpm/node.fetcher@3.0.5
## 2.0.5
### Patch Changes
- @pnpm/node.fetcher@3.0.4
## 2.0.4
### Patch Changes
- @pnpm/node.fetcher@3.0.3
## 2.0.3
### Patch Changes
- @pnpm/node.fetcher@3.0.2
## 2.0.2
### Patch Changes
- Updated dependencies [8228c2cb1]
- @pnpm/node.fetcher@3.0.1
## 2.0.1
### Patch Changes
- c0760128d: bump semver to 7.4.0
## 2.0.0
### Major Changes
- eceaa8b8b: Node.js 14 support dropped.
### Patch Changes
- Updated dependencies [eceaa8b8b]
- @pnpm/fetching-types@5.0.0
- @pnpm/node.fetcher@3.0.0
## 1.1.11
### Patch Changes
- @pnpm/node.fetcher@2.0.14
## 1.1.10
### Patch Changes
- @pnpm/node.fetcher@2.0.13
## 1.1.9
### Patch Changes
- @pnpm/node.fetcher@2.0.12
## 1.1.8
### Patch Changes
- @pnpm/node.fetcher@2.0.11
## 1.1.7
### Patch Changes
- @pnpm/node.fetcher@2.0.10
## 1.1.6
### Patch Changes
- @pnpm/node.fetcher@2.0.9
## 1.1.5
### Patch Changes
- @pnpm/node.fetcher@2.0.8
## 1.1.4
### Patch Changes
- Updated dependencies [ec97a3105]
- @pnpm/node.fetcher@2.0.7
## 1.1.3
### Patch Changes
- @pnpm/node.fetcher@2.0.6
## 1.1.2
### Patch Changes
- @pnpm/node.fetcher@2.0.5
## 1.1.1
### Patch Changes
- @pnpm/node.fetcher@2.0.4
## 1.1.0
### Minor Changes
- f60d6c46f: Export a new function: resolveNodeVersions.
## 1.0.19
### Patch Changes
- @pnpm/node.fetcher@2.0.3
## 1.0.18
### Patch Changes
- Updated dependencies [804de211e]
- @pnpm/fetching-types@4.0.0
- @pnpm/node.fetcher@2.0.2
## 1.0.17
### Patch Changes
- @pnpm/node.fetcher@2.0.1
## 1.0.16
### Patch Changes
- Updated dependencies [f884689e0]
- @pnpm/node.fetcher@2.0.0
## 1.0.15
### Patch Changes
- @pnpm/node.fetcher@1.0.15
## 1.0.14
### Patch Changes
- @pnpm/node.fetcher@1.0.14
## 1.0.13
### Patch Changes
- @pnpm/node.fetcher@1.0.13
## 1.0.12
### Patch Changes
- @pnpm/node.fetcher@1.0.12
## 1.0.11
### Patch Changes
- Updated dependencies [1c7b439bb]
- @pnpm/node.fetcher@1.0.11
## 1.0.10
### Patch Changes
- @pnpm/node.fetcher@1.0.10
## 1.0.9
### Patch Changes
- @pnpm/node.fetcher@1.0.9
## 1.0.8
### Patch Changes
- Updated dependencies [32915f0e4]
- Updated dependencies [7a17f99ab]
- @pnpm/node.fetcher@1.0.8
## 1.0.7
### Patch Changes
- @pnpm/node.fetcher@1.0.7
## 1.0.6
### Patch Changes
- @pnpm/node.fetcher@1.0.6
## 1.0.5
### Patch Changes
- @pnpm/node.fetcher@1.0.5
## 1.0.4
### Patch Changes
- Updated dependencies [2105735a0]
- @pnpm/node.fetcher@1.0.4
## 1.0.3
### Patch Changes
- @pnpm/node.fetcher@1.0.3
## 1.0.2
### Patch Changes
- @pnpm/node.fetcher@1.0.2
## 1.0.1
### Patch Changes
- @pnpm/node.fetcher@1.0.1
## 1.0.0
### Major Changes
- badbab154: Initial release.
### Patch Changes
- Updated dependencies [228dcc3c9]
- @pnpm/node.fetcher@1.0.0

View File

@@ -0,0 +1,15 @@
# @pnpm/engine.runtime.node-resolver
> Resolves a Node.js version specifier to an exact Node.js version
[![npm version](https://img.shields.io/npm/v/@pnpm/engine.runtime.node-resolver.svg)](https://www.npmjs.com/package/@pnpm/engine.runtime.node-resolver)
## Installation
```sh
pnpm add @pnpm/engine.runtime.node-resolver
```
## License
MIT

View File

@@ -0,0 +1,57 @@
{
"name": "@pnpm/engine.runtime.node-resolver",
"version": "1001.0.5",
"description": "Resolves a Node.js version specifier to an exact Node.js version",
"keywords": [
"pnpm",
"pnpm11",
"env",
"node.js"
],
"license": "MIT",
"funding": "https://opencollective.com/pnpm",
"repository": "https://github.com/pnpm/pnpm/tree/main/engine/runtime/node-resolver",
"homepage": "https://github.com/pnpm/pnpm/tree/main/engine/runtime/node-resolver#readme",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": "./lib/index.js"
},
"files": [
"lib",
"!*.map"
],
"scripts": {
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
"_test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest",
"test": "pnpm run compile && pnpm run _test",
"prepublishOnly": "pnpm run compile",
"compile": "tsgo --build && pnpm run lint --fix"
},
"dependencies": {
"@pnpm/config.reader": "workspace:*",
"@pnpm/constants": "workspace:*",
"@pnpm/crypto.shasums-file": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/fetching.types": "workspace:*",
"@pnpm/resolving.resolver-base": "workspace:*",
"@pnpm/types": "workspace:*",
"semver": "catalog:",
"version-selector-type": "catalog:"
},
"devDependencies": {
"@pnpm/engine.runtime.node-resolver": "workspace:*",
"@pnpm/network.fetch": "workspace:*",
"@types/semver": "catalog:"
},
"engines": {
"node": ">=22.13"
},
"jest": {
"preset": "@pnpm/jest-config"
}
}

View File

@@ -0,0 +1,33 @@
import { getNormalizedArch } from './normalizeArch.js'
export interface NodeArtifactAddress {
basename: string
extname: string
dirname: string
}
export interface GetNodeArtifactAddressOptions {
version: string
baseUrl: string
platform: string
arch: string
libc?: string
}
export function getNodeArtifactAddress ({
version,
baseUrl,
platform,
arch,
libc,
}: GetNodeArtifactAddressOptions): NodeArtifactAddress {
const isWindowsPlatform = platform === 'win32'
const normalizedPlatform = isWindowsPlatform ? 'win' : platform
const normalizedArch = getNormalizedArch(platform, arch, version)
const archSuffix = libc === 'musl' ? '-musl' : ''
return {
dirname: `${baseUrl}v${version}`,
basename: `node-v${version}-${normalizedPlatform}-${normalizedArch}${archSuffix}`,
extname: isWindowsPlatform ? '.zip' : '.tar.gz',
}
}

View File

@@ -0,0 +1,12 @@
import type { Config } from '@pnpm/config.reader'
export function getNodeMirror (rawConfig: Config['rawConfig'], releaseChannel: string): string {
// This is a dynamic lookup since the 'use-node-version' option is allowed to be '<releaseChannel>/<version>'
const configKey = `node-mirror:${releaseChannel}`
const nodeMirror = rawConfig[configKey] ?? `https://nodejs.org/download/${releaseChannel}/`
return normalizeNodeMirror(nodeMirror)
}
function normalizeNodeMirror (nodeMirror: string): string {
return nodeMirror.endsWith('/') ? nodeMirror : `${nodeMirror}/`
}

View File

@@ -0,0 +1,223 @@
import { getNodeBinsForCurrentOS } from '@pnpm/constants'
import { fetchShasumsFile } from '@pnpm/crypto.shasums-file'
import { PnpmError } from '@pnpm/error'
import type { FetchFromRegistry } from '@pnpm/fetching.types'
import type {
BinaryResolution,
PlatformAssetResolution,
PlatformAssetTarget,
ResolveOptions,
ResolveResult,
VariationsResolution,
WantedDependency,
} from '@pnpm/resolving.resolver-base'
import type { PkgResolutionId } from '@pnpm/types'
import semver from 'semver'
import versionSelectorType from 'version-selector-type'
import { getNodeArtifactAddress } from './getNodeArtifactAddress.js'
import { getNodeMirror } from './getNodeMirror.js'
import { parseNodeSpecifier } from './parseNodeSpecifier.js'
export { getNodeArtifactAddress, getNodeMirror, parseNodeSpecifier }
export const DEFAULT_NODE_MIRROR_BASE_URL = 'https://nodejs.org/download/release/'
export const UNOFFICIAL_NODE_MIRROR_BASE_URL = 'https://unofficial-builds.nodejs.org/download/release/'
export interface NodeRuntimeResolveResult extends ResolveResult {
resolution: VariationsResolution
resolvedVia: 'nodejs.org'
}
export async function resolveNodeRuntime (
ctx: {
fetchFromRegistry: FetchFromRegistry
rawConfig: Record<string, string>
offline?: boolean
},
wantedDependency: WantedDependency,
opts?: Partial<ResolveOptions>
): Promise<NodeRuntimeResolveResult | null> {
if (wantedDependency.alias !== 'node' || !wantedDependency.bareSpecifier?.startsWith('runtime:')) return null
if (opts?.currentPkg && !opts.update) {
return {
id: opts.currentPkg.id,
resolution: opts.currentPkg.resolution as VariationsResolution,
resolvedVia: 'nodejs.org',
}
}
if (ctx.offline) throw new PnpmError('NO_OFFLINE_NODEJS_RESOLUTION', 'Offline Node.js resolution is not supported')
const versionSpec = wantedDependency.bareSpecifier.substring('runtime:'.length)
const { releaseChannel, versionSpecifier } = parseNodeSpecifier(versionSpec)
const nodeMirrorBaseUrl = getNodeMirror(ctx.rawConfig, releaseChannel)
const version = await resolveNodeVersion(ctx.fetchFromRegistry, versionSpecifier, nodeMirrorBaseUrl)
if (!version) {
throw new PnpmError('NODEJS_VERSION_NOT_FOUND', `Could not find a Node.js version that satisfies ${versionSpec}`)
}
const variants = await readNodeAssets(ctx.fetchFromRegistry, nodeMirrorBaseUrl, version)
const range = version === versionSpec ? version : `^${version}`
return {
id: `node@runtime:${version}` as PkgResolutionId,
normalizedBareSpecifier: `runtime:${range}`,
resolvedVia: 'nodejs.org',
manifest: {
name: 'node',
version,
bin: getNodeBinsForCurrentOS(),
},
resolution: {
type: 'variations',
variants,
},
}
}
async function readNodeAssets (fetch: FetchFromRegistry, nodeMirrorBaseUrl: string, version: string): Promise<PlatformAssetResolution[]> {
const assets = await readNodeAssetsFromMirror(fetch, { nodeMirrorBaseUrl, version, muslOnly: false })
// When using the default mirror, also fetch musl variants from unofficial-builds.nodejs.org,
// since musl builds are not available on the official mirror.
if (nodeMirrorBaseUrl === DEFAULT_NODE_MIRROR_BASE_URL) {
try {
const muslAssets = await readNodeAssetsFromMirror(fetch, { nodeMirrorBaseUrl: UNOFFICIAL_NODE_MIRROR_BASE_URL, version, muslOnly: true })
assets.push(...muslAssets)
} catch {
// Musl variants may not be available for all Node.js versions (e.g. very old ones)
}
}
return assets
}
async function readNodeAssetsFromMirror (
fetch: FetchFromRegistry,
opts: {
nodeMirrorBaseUrl: string
version: string
muslOnly: boolean
}
): Promise<PlatformAssetResolution[]> {
const { nodeMirrorBaseUrl, version, muslOnly } = opts
const integritiesFileUrl = `${nodeMirrorBaseUrl}v${version}/SHASUMS256.txt`
const shasumsFileItems = await fetchShasumsFile(fetch, integritiesFileUrl)
const escaped = version.replace(/\\/g, '\\\\').replace(/\./g, '\\.')
// The second capture group uses [^.-]+ to stop at a dash, so that the optional
// third group can capture the '-musl' suffix separately (e.g. 'x64' + '-musl').
const pattern = new RegExp(`^node-v${escaped}-([^-.]+)-([^.-]+)(-musl)?\\.(?:tar\\.gz|zip)$`)
const assets: PlatformAssetResolution[] = []
for (const { integrity, fileName } of shasumsFileItems) {
const match = pattern.exec(fileName)
if (!match) continue
let [, platform, arch, muslSuffix] = match
if (platform === 'win') {
platform = 'win32'
}
const isMusl = muslSuffix != null
if (muslOnly && !isMusl) continue
const libc = isMusl ? 'musl' : undefined
const address = getNodeArtifactAddress({
version,
baseUrl: nodeMirrorBaseUrl,
platform,
arch,
libc,
})
const url = `${address.dirname}/${address.basename}${address.extname}`
const resolution: BinaryResolution = {
type: 'binary',
archive: address.extname === '.zip' ? 'zip' : 'tarball',
bin: getNodeBinsForCurrentOS(platform),
integrity,
url,
}
if (resolution.archive === 'zip') {
resolution.prefix = address.basename
}
const target: PlatformAssetTarget = {
os: platform,
cpu: arch,
...(libc != null && { libc }),
}
assets.push({
targets: [target],
resolution,
})
}
return assets
}
interface NodeVersion {
version: string
lts: false | string
}
const SEMVER_OPTS = {
includePrerelease: true,
loose: true,
}
export async function resolveNodeVersion (
fetch: FetchFromRegistry,
versionSpec: string,
nodeMirrorBaseUrl?: string
): Promise<string | null> {
const allVersions = await fetchAllVersions(fetch, nodeMirrorBaseUrl)
if (versionSpec === 'latest') {
return allVersions[0].version
}
const { versions, versionRange } = filterVersions(allVersions, versionSpec)
return semver.maxSatisfying(versions, versionRange, SEMVER_OPTS) ?? null
}
export async function resolveNodeVersions (
fetch: FetchFromRegistry,
versionSpec?: string,
nodeMirrorBaseUrl?: string
): Promise<string[]> {
const allVersions = await fetchAllVersions(fetch, nodeMirrorBaseUrl)
if (!versionSpec) {
return allVersions.map(({ version }) => version)
}
if (versionSpec === 'latest') {
return [allVersions[0].version]
}
const { versions, versionRange } = filterVersions(allVersions, versionSpec)
return versions.filter(version => semver.satisfies(version, versionRange, SEMVER_OPTS))
}
async function fetchAllVersions (fetch: FetchFromRegistry, nodeMirrorBaseUrl?: string): Promise<NodeVersion[]> {
const response = await fetch(`${nodeMirrorBaseUrl ?? 'https://nodejs.org/download/release/'}index.json`)
return ((await response.json()) as NodeVersion[]).map(({ version, lts }) => ({
version: version.substring(1),
lts,
}))
}
function filterVersions (versions: NodeVersion[], versionSelector: string): { versions: string[], versionRange: string } {
if (versionSelector === 'lts') {
return {
versions: versions
.filter(({ lts }) => lts !== false)
.map(({ version }) => version),
versionRange: '*',
}
}
const vst = versionSelectorType(versionSelector)
if (vst?.type === 'tag') {
const wantedLtsVersion = vst.normalized.toLowerCase()
return {
versions: versions
.filter(({ lts }) => typeof lts === 'string' && lts.toLowerCase() === wantedLtsVersion)
.map(({ version }) => version),
versionRange: '*',
}
}
return {
versions: versions.map(({ version }) => version),
versionRange: versionSelector,
}
}

View File

@@ -0,0 +1,15 @@
export function getNormalizedArch (platform: string, arch: string, nodeVersion?: string): string {
if (nodeVersion) {
const nodeMajorVersion = +nodeVersion.split('.')[0]
if ((platform === 'darwin' && arch === 'arm64' && (nodeMajorVersion < 16))) {
return 'x64'
}
}
if (platform === 'win32' && arch === 'ia32') {
return 'x86'
}
if (arch === 'arm') {
return 'armv7l'
}
return arch
}

View File

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

View File

@@ -0,0 +1,80 @@
import { getNodeArtifactAddress } from '../lib/getNodeArtifactAddress.js'
test.each([
[
'16.0.0',
'https://nodejs.org/download/release/',
'win32',
'ia32',
{
basename: 'node-v16.0.0-win-x86',
dirname: 'https://nodejs.org/download/release/v16.0.0',
extname: '.zip',
},
],
[
'16.0.0',
'https://nodejs.org/download/release/',
'linux',
'arm',
{
basename: 'node-v16.0.0-linux-armv7l',
dirname: 'https://nodejs.org/download/release/v16.0.0',
extname: '.tar.gz',
},
],
[
'16.0.0',
'https://nodejs.org/download/release/',
'linux',
'x64',
{
basename: 'node-v16.0.0-linux-x64',
dirname: 'https://nodejs.org/download/release/v16.0.0',
extname: '.tar.gz',
},
],
[
'15.14.0',
'https://nodejs.org/download/release/',
'darwin',
'arm64',
{
basename: 'node-v15.14.0-darwin-x64',
dirname: 'https://nodejs.org/download/release/v15.14.0',
extname: '.tar.gz',
},
],
[
'16.0.0',
'https://nodejs.org/download/release/',
'darwin',
'arm64',
{
basename: 'node-v16.0.0-darwin-arm64',
dirname: 'https://nodejs.org/download/release/v16.0.0',
extname: '.tar.gz',
},
],
])('getNodeArtifactAddress', (version, nodeMirrorBaseUrl, platform, arch, tarball) => {
expect(getNodeArtifactAddress({
version,
baseUrl: nodeMirrorBaseUrl,
platform,
arch,
})).toStrictEqual(tarball)
})
test('getNodeArtifactAddress with libc=musl appends -musl suffix to arch', () => {
expect(getNodeArtifactAddress({
version: '22.0.0',
baseUrl: 'https://unofficial-builds.nodejs.org/download/release/',
platform: 'linux',
arch: 'x64',
libc: 'musl',
})).toStrictEqual({
basename: 'node-v22.0.0-linux-x64-musl',
dirname: 'https://unofficial-builds.nodejs.org/download/release/v22.0.0',
extname: '.tar.gz',
})
})

View File

@@ -0,0 +1,23 @@
import { getNodeMirror } from '../lib/getNodeMirror.js'
test.each([
['release', { 'node-mirror:release': 'http://test.mirror.localhost/release' }, 'http://test.mirror.localhost/release/'],
['nightly', { 'node-mirror:nightly': 'http://test.mirror.localhost/nightly' }, 'http://test.mirror.localhost/nightly/'],
['rc', { 'node-mirror:rc': 'http://test.mirror.localhost/rc' }, 'http://test.mirror.localhost/rc/'],
['test', { 'node-mirror:test': 'http://test.mirror.localhost/test' }, 'http://test.mirror.localhost/test/'],
['v8-canary', { 'node-mirror:v8-canary': 'http://test.mirror.localhost/v8-canary' }, 'http://test.mirror.localhost/v8-canary/'],
])('getNodeMirror(%s, %s)', (releaseDir, rawConfig, expected) => {
expect(getNodeMirror(rawConfig, releaseDir)).toBe(expected)
})
test('getNodeMirror uses defaults', () => {
const rawConfig = {}
expect(getNodeMirror(rawConfig, 'release')).toBe('https://nodejs.org/download/release/')
})
test('getNodeMirror returns base url with trailing /', () => {
const rawConfig = {
'node-mirror:release': 'http://test.mirror.localhost',
}
expect(getNodeMirror(rawConfig, 'release')).toBe('http://test.mirror.localhost/')
})

View File

@@ -0,0 +1,17 @@
import { getNormalizedArch } from '../lib/normalizeArch.js'
test.each([
['win32', 'ia32', 'x86'],
['linux', 'arm', 'armv7l'], // Raspberry Pi 4
['linux', 'x64', 'x64'],
])('getNormalizedArch(%s, %s)', (platform, arch, normalizedArch) => {
expect(getNormalizedArch(platform, arch)).toBe(normalizedArch)
})
// macos apple silicon
test.each([
['darwin', 'arm64', '14.20.0', 'x64'],
['darwin', 'arm64', '16.17.0', 'arm64'],
])('getNormalizedArch(%s, %s)', (platform, arch, nodeVersion, normalizedArch) => {
expect(getNormalizedArch(platform, arch, nodeVersion)).toBe(normalizedArch)
})

View File

@@ -0,0 +1,46 @@
import { parseNodeSpecifier } from '../lib/parseNodeSpecifier.js'
test.each([
// Semver ranges → release channel
['6', '6', 'release'],
['16.0', '16.0', 'release'],
// Exact prerelease with rc channel
['16.0.0-rc.0', '16.0.0-rc.0', 'rc'],
// Channel/range combo (major only)
['rc/10', '10', 'rc'],
// Standalone channel name → latest from that channel
['nightly', 'latest', 'nightly'],
['rc', 'latest', 'rc'],
['test', 'latest', 'test'],
['v8-canary', 'latest', 'v8-canary'],
['release', 'latest', 'release'],
// Well-known aliases
['lts', 'lts', 'release'],
['latest', 'latest', 'release'],
// LTS codenames
['argon', 'argon', 'release'],
['iron', 'iron', 'release'],
// Exact stable version
['22.0.0', '22.0.0', 'release'],
// Stable release with explicit channel prefix, aliases, and semver ranges
['release/22.0.0', '22.0.0', 'release'],
['release/latest', 'latest', 'release'],
['release/lts', 'lts', 'release'],
['release/18', '18', 'release'],
// Channel/version combos
['rc/18', '18', 'rc'],
['rc/18.0.0-rc.4', '18.0.0-rc.4', 'rc'],
['nightly/latest', 'latest', 'nightly'],
// Exact nightly version
['24.0.0-nightly20250315d765e70802', '24.0.0-nightly20250315d765e70802', 'nightly'],
// Exact v8-canary version
['22.0.0-v8-canary20250101abc', '22.0.0-v8-canary20250101abc', 'v8-canary'],
])('Node.js version specifier is parsed: %s', (specifier, expectedVersionSpecifier, expectedReleaseChannel) => {
const result = parseNodeSpecifier(specifier)
expect(result.versionSpecifier).toBe(expectedVersionSpecifier)
expect(result.releaseChannel).toBe(expectedReleaseChannel)
})
test('throws for unknown release channel', () => {
expect(() => parseNodeSpecifier('foo/18')).toThrow('"foo" is not a valid Node.js release channel')
})

View File

@@ -0,0 +1,18 @@
import { resolveNodeVersion } from '@pnpm/engine.runtime.node-resolver'
import { createFetchFromRegistry } from '@pnpm/network.fetch'
const fetch = createFetchFromRegistry({})
test.each([
['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', /.+/],
[undefined, 'latest', /.+/],
])('Node.js %s is resolved', async (nodeMirrorBaseUrl, spec, expectedVersion) => {
const version = await resolveNodeVersion(fetch, spec, nodeMirrorBaseUrl)
expect(version).toMatch(expectedVersion)
})

View File

@@ -0,0 +1,20 @@
import { resolveNodeVersions } from '@pnpm/engine.runtime.node-resolver'
import { createFetchFromRegistry } from '@pnpm/network.fetch'
const fetch = createFetchFromRegistry({})
test('resolve specified version list', async () => {
const versions = await resolveNodeVersions(fetch, '16')
expect(versions.length).toBeGreaterThan(1)
expect(versions.every(version => version.match(/^16.+/))).toBeTruthy()
})
test('resolve latest version', async () => {
const versions = await resolveNodeVersions(fetch, 'latest')
expect(versions).toHaveLength(1)
})
test('resolve all versions', async () => {
const versions = await resolveNodeVersions(fetch)
expect(versions.length).toBeGreaterThan(1)
})

View File

@@ -0,0 +1,18 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "../node_modules/.test.lib",
"rootDir": "..",
"isolatedModules": true
},
"include": [
"**/*.ts",
"../../../../__typings__/**/*.d.ts"
],
"references": [
{
"path": ".."
}
]
}

View File

@@ -0,0 +1,37 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../../__typings__/**/*.d.ts"
],
"references": [
{
"path": "../../../config/reader"
},
{
"path": "../../../core/constants"
},
{
"path": "../../../core/error"
},
{
"path": "../../../core/types"
},
{
"path": "../../../crypto/shasums-file"
},
{
"path": "../../../fetching/types"
},
{
"path": "../../../network/fetch"
},
{
"path": "../../../resolving/resolver-base"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../../__typings__/**/*.d.ts"
]
}