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,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 }
}