mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-31 12:10:49 -04:00
`pnpm outdated` and `pnpm update --interactive` previously skipped runtime dependencies (`node`/`deno`/`bun` installed via the `runtime:` protocol). Both commands go through `outdatedDepsOfProjects` → `outdated()`, and the inner loop bailed out for anything `parseBareSpecifier` couldn't parse — which is everything `runtime:`-shaped. A runtime was only ever reported if the current install differed from the wanted lockfile entry, so the latest available version was never surfaced. The same gap silently affected `jsr:` and named-registry deps too. Commits, smallest fix first → progressively cleaner architecture: 1. **`feat(outdated)`** — minimal fix: special-case runtime deps in `outdated.ts` so they appear in the table and the interactive update picker. 2. **`refactor(outdated)`** — per-resolver dispatch. Each protocol resolver gets its own "what's the latest?" function; `@pnpm/resolving.default-resolver` composes them. 3. **`refactor(outdated)`** — rename to `resolveLatest` (the function returns info regardless of whether the dep is outdated; "outdated" described a state, not an action). 4. **`refactor(outdated)`** — let the local-resolver own the `link:`/`file:` skip, drop the matching short-circuit in `outdated.ts`. 5. **`refactor(outdated)`** — slim `LatestQuery` / `LatestInfo` to the bare essentials; move `pickRegistryForPackage` into the npm-resolver where it belongs; derive `current`/`wanted` display from `pkgSnapshot.version` in `outdated.ts`. 6. **`chore(outdated)`** — drop stale tsconfig project reference left behind by #5. 7. **`refactor(outdated)`** — drop `wantedRef` from the query; resolvers detect protocol from `bareSpecifier` alone. ## Final architecture `@pnpm/resolving.resolver-base` defines a single tiny protocol: ```ts interface LatestQuery { wantedDependency: WantedDependency compatible?: boolean } interface LatestInfo { latestManifest?: PackageManifest } type ResolveLatestFunction = (query: LatestQuery, opts: ResolveOptions) => Promise<LatestInfo | undefined> ``` - `undefined` from a resolver means "I don't claim this dep — try the next one." - `{}` means "I claim it, but I can't tell you what's latest" (policy-blocked, network unavailable, or a protocol with no concept of latest — git/tarball). - `{ latestManifest }` is the happy path. Each protocol resolver (npm/jsr/named-registry, git, tarball, local, node/bun/deno runtimes) exports its own `resolveLatest*` function alongside its `resolve*`. `@pnpm/resolving.default-resolver` composes them into a single first-match dispatcher, surfaced through `@pnpm/installing.client` as `createResolver(...).resolveLatest`. `outdated.ts` is protocol-agnostic: dispatches, then derives `current`/`wanted` display from `pkgSnapshot.version` (falling back to the raw ref for URL-shaped refs where the URL is the only diff signal between commits), uses raw `wantedRef !== currentRef` for the lockfile-shifted check, and pulls `packageName` from `dp.parse(relativeDepPath).name` so aliased deps still report under the real package name. Per-resolver responsibilities: - **npm-resolver** (`resolveLatestFromNpm` / `resolveLatestFromJsr` / `resolveLatestFromNamedRegistry`): match their respective spec shapes, call the matching `resolveFromX` with `'latest'` (or the original spec under `--compatible`), handle `MINIMUM_RELEASE_AGE_VIOLATION` and `ERR_PNPM_NO_MATCHING_VERSION` so policy-blocked deps don't surface as available updates. Picks the per-package registry internally via its ctx. - **node/bun/deno runtime resolvers**: claim deps via `bareSpecifier.startsWith('runtime:')` + alias match, query their release sources for the latest version (only the version — no asset-hash fetches), return `{ latestManifest }`. - **git / tarball resolvers**: claim deps via spec shape, return `{}` (no concept of "latest"); the caller still surfaces a ref-mismatch report if the lockfile shifted to a different commit/URL. - **local-resolver**: returns `undefined` so `link:`/`file:`/`workspace:` deps fall through and get silently skipped.
154 lines
5.5 KiB
TypeScript
154 lines
5.5 KiB
TypeScript
import { PnpmError } from '@pnpm/error'
|
|
import type { FetchFromRegistry } from '@pnpm/fetching.types'
|
|
import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, type NpmResolver } from '@pnpm/resolving.npm-resolver'
|
|
import type {
|
|
BinaryResolution,
|
|
LatestInfo,
|
|
LatestQuery,
|
|
PlatformAssetResolution,
|
|
PlatformAssetTarget,
|
|
ResolveOptions,
|
|
ResolveResult,
|
|
VariationsResolution,
|
|
WantedDependency,
|
|
} from '@pnpm/resolving.resolver-base'
|
|
import type { PkgResolutionId } from '@pnpm/types'
|
|
import { lexCompare } from '@pnpm/util.lex-comparator'
|
|
|
|
const ASSET_REGEX = /^deno-(?<cpu>aarch64|x86_64)-(?<os>apple-darwin|unknown-linux-gnu|pc-windows-msvc)\.zip\.sha256sum$/
|
|
const OS_MAP = {
|
|
'apple-darwin': 'darwin',
|
|
'unknown-linux-gnu': 'linux',
|
|
'pc-windows-msvc': 'win32',
|
|
} as const
|
|
const CPU_MAP = {
|
|
aarch64: 'arm64',
|
|
x86_64: 'x64',
|
|
} as const
|
|
|
|
export interface DenoRuntimeResolveResult extends ResolveResult {
|
|
resolution: VariationsResolution
|
|
resolvedVia: 'github.com/denoland/deno'
|
|
}
|
|
|
|
export async function resolveDenoRuntime (
|
|
ctx: {
|
|
fetchFromRegistry: FetchFromRegistry
|
|
offline?: boolean
|
|
resolveFromNpm: NpmResolver
|
|
},
|
|
wantedDependency: WantedDependency,
|
|
opts?: Partial<ResolveOptions>
|
|
): Promise<DenoRuntimeResolveResult | null> {
|
|
if (wantedDependency.alias !== 'deno' || !wantedDependency.bareSpecifier?.startsWith('runtime:')) return null
|
|
|
|
if (opts?.currentPkg && !opts.update) {
|
|
return {
|
|
id: opts.currentPkg.id,
|
|
resolution: opts.currentPkg.resolution as VariationsResolution,
|
|
resolvedVia: 'github.com/denoland/deno',
|
|
}
|
|
}
|
|
|
|
const versionSpec = wantedDependency.bareSpecifier.substring('runtime:'.length)
|
|
// We use the npm registry for version resolution as it is easier than using the GitHub API for releases,
|
|
// which uses pagination (e.g. https://api.github.com/repos/denoland/deno/releases?per_page=100).
|
|
const npmResolution = await ctx.resolveFromNpm({ ...wantedDependency, bareSpecifier: versionSpec }, {})
|
|
if (npmResolution == null) {
|
|
throw new PnpmError('DENO_RESOLUTION_FAILURE', `Could not resolve Deno version specified as ${versionSpec}`)
|
|
}
|
|
const version = npmResolution.manifest.version
|
|
const res = await ctx.fetchFromRegistry(`https://api.github.com/repos/denoland/deno/releases/tags/v${version}`)
|
|
const data = (await res.json()) as { assets: Array<{ name: string, browser_download_url: string }> }
|
|
const assets: PlatformAssetResolution[] = []
|
|
if (data.assets == null) {
|
|
throw new PnpmError('DENO_MISSING_ASSETS', `No assets found for Deno v${version}`)
|
|
}
|
|
await Promise.all(data.assets.map(async (asset) => {
|
|
const targets = parseAssetName(asset.name)
|
|
if (!targets) return
|
|
const sha256 = await fetchSha256(ctx.fetchFromRegistry, asset.browser_download_url)
|
|
const base64 = Buffer.from(sha256, 'hex').toString('base64')
|
|
assets.push({
|
|
targets,
|
|
resolution: {
|
|
type: 'binary',
|
|
url: asset.browser_download_url.replace(/\.sha256sum$/, ''),
|
|
integrity: `sha256-${base64}`,
|
|
bin: getDenoBinLocationForCurrentOS(targets[0].os),
|
|
archive: 'zip',
|
|
},
|
|
})
|
|
}))
|
|
assets.sort((asset1, asset2) => lexCompare((asset1.resolution as BinaryResolution).url, (asset2.resolution as BinaryResolution).url))
|
|
|
|
return {
|
|
id: `deno@runtime:${version}` as PkgResolutionId,
|
|
normalizedBareSpecifier: `runtime:${versionSpec}`,
|
|
resolvedVia: 'github.com/denoland/deno',
|
|
manifest: {
|
|
name: 'deno',
|
|
version,
|
|
bin: getDenoBinLocationForCurrentOS(),
|
|
},
|
|
resolution: {
|
|
type: 'variations',
|
|
variants: assets,
|
|
},
|
|
}
|
|
}
|
|
|
|
export async function resolveLatestDenoRuntime (
|
|
ctx: { resolveFromNpm: NpmResolver },
|
|
query: LatestQuery,
|
|
opts: ResolveOptions
|
|
): Promise<LatestInfo | undefined> {
|
|
const manifestSpec = query.wantedDependency.bareSpecifier
|
|
if (query.wantedDependency.alias !== 'deno' || !manifestSpec?.startsWith('runtime:')) return undefined
|
|
const versionSpec = query.compatible ? manifestSpec.substring('runtime:'.length) : 'latest'
|
|
try {
|
|
const npmResolution = await ctx.resolveFromNpm(
|
|
{ alias: 'deno', bareSpecifier: versionSpec },
|
|
query.compatible ? opts : { ...opts, update: 'latest' }
|
|
)
|
|
if (npmResolution?.policyViolation?.code === MINIMUM_RELEASE_AGE_VIOLATION_CODE) return {}
|
|
if (!npmResolution?.manifest) return {}
|
|
return { latestManifest: { name: 'deno', version: npmResolution.manifest.version } }
|
|
} catch (err) {
|
|
if (opts.publishedBy && (err as { code?: string }).code === 'ERR_PNPM_NO_MATCHING_VERSION') {
|
|
return {}
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
|
|
function parseAssetName (name: string): PlatformAssetTarget[] | null {
|
|
const m = ASSET_REGEX.exec(name)
|
|
if (!m?.groups) return null
|
|
const os = OS_MAP[m.groups.os as keyof typeof OS_MAP]
|
|
const cpu = CPU_MAP[m.groups.cpu as keyof typeof CPU_MAP]
|
|
const targets = [{ os, cpu }]
|
|
if (os === 'win32' && cpu === 'x64') {
|
|
// The Windows x64 binaries of Deno are compatible with arm64 architecture.
|
|
targets.push({ os: 'win32', cpu: 'arm64' })
|
|
}
|
|
return targets
|
|
}
|
|
|
|
function getDenoBinLocationForCurrentOS (platform: string = process.platform): string {
|
|
return platform === 'win32' ? 'deno.exe' : 'deno'
|
|
}
|
|
|
|
async function fetchSha256 (fetch: FetchFromRegistry, url: string): Promise<string> {
|
|
const response = await fetch(url)
|
|
if (!response.ok) {
|
|
throw new PnpmError('DENO_GITHUB_FAILURE', `Failed to GET sha256 at ${url}`)
|
|
}
|
|
const txt = await response.text()
|
|
const m = txt.match(/([a-f0-9]{64})/i)
|
|
if (!m) {
|
|
throw new PnpmError('DENO_PARSE_HASH', `No SHA256 in ${url}`)
|
|
}
|
|
return m[1].toLowerCase()
|
|
}
|