mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-27 19:41:44 -04:00
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:
33
engine/runtime/node-resolver/src/getNodeArtifactAddress.ts
Normal file
33
engine/runtime/node-resolver/src/getNodeArtifactAddress.ts
Normal 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',
|
||||
}
|
||||
}
|
||||
12
engine/runtime/node-resolver/src/getNodeMirror.ts
Normal file
12
engine/runtime/node-resolver/src/getNodeMirror.ts
Normal 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}/`
|
||||
}
|
||||
223
engine/runtime/node-resolver/src/index.ts
Normal file
223
engine/runtime/node-resolver/src/index.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
15
engine/runtime/node-resolver/src/normalizeArch.ts
Normal file
15
engine/runtime/node-resolver/src/normalizeArch.ts
Normal 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
|
||||
}
|
||||
51
engine/runtime/node-resolver/src/parseNodeSpecifier.ts
Normal file
51
engine/runtime/node-resolver/src/parseNodeSpecifier.ts
Normal 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user