mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-01 12:41:16 -04:00
feat(outdated): include node, deno, and bun runtimes (#11739)
`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.
This commit is contained in:
21
.changeset/outdated-runtimes.md
Normal file
21
.changeset/outdated-runtimes.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
"@pnpm/deps.inspection.outdated": minor
|
||||
"@pnpm/installing.client": minor
|
||||
"@pnpm/resolving.default-resolver": minor
|
||||
"@pnpm/resolving.resolver-base": minor
|
||||
"@pnpm/resolving.npm-resolver": minor
|
||||
"@pnpm/resolving.git-resolver": minor
|
||||
"@pnpm/resolving.tarball-resolver": minor
|
||||
"@pnpm/resolving.local-resolver": minor
|
||||
"@pnpm/engine.runtime.node-resolver": minor
|
||||
"@pnpm/engine.runtime.bun-resolver": minor
|
||||
"@pnpm/engine.runtime.deno-resolver": minor
|
||||
"@pnpm/pkg-manifest.utils": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
`pnpm outdated` and `pnpm update --interactive` now report Node.js, Deno, and Bun runtimes installed as project dependencies (`runtime:` specifiers). Previously these were silently skipped because the npm specifier parser did not understand the `runtime:` protocol, so runtime versions never appeared in the outdated table or the interactive update picker.
|
||||
|
||||
Internally, the outdated check is now resolver-driven: `@pnpm/resolving.resolver-base` defines a `ResolveLatestFunction` shape (with `LatestQuery` input — `{ wantedDependency, compatible? }` — and `LatestInfo` result — `{ latestManifest? }`), and every 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 dispatcher, exposed through `@pnpm/installing.client` as `createResolver(...).resolveLatest`.
|
||||
|
||||
Each resolver decides whether it owns the dep and what "latest" means for its protocol; the outdated command derives `current` / `wanted` display values from the lockfile snapshot (`pkgSnapshot.version` for semver protocols, raw ref for URL-shaped ones) and uses raw ref equality for the "lockfile changed" check, so protocol knowledge stays inside each resolver instead of the command.
|
||||
1
deps/inspection/outdated/package.json
vendored
1
deps/inspection/outdated/package.json
vendored
@@ -36,7 +36,6 @@
|
||||
"@pnpm/catalogs.types": "workspace:*",
|
||||
"@pnpm/config.matcher": "workspace:*",
|
||||
"@pnpm/config.parse-overrides": "workspace:*",
|
||||
"@pnpm/config.pick-registry-for-package": "workspace:*",
|
||||
"@pnpm/config.version-policy": "workspace:*",
|
||||
"@pnpm/constants": "workspace:*",
|
||||
"@pnpm/deps.path": "workspace:*",
|
||||
|
||||
122
deps/inspection/outdated/src/outdated.ts
vendored
122
deps/inspection/outdated/src/outdated.ts
vendored
@@ -7,33 +7,30 @@ import {
|
||||
import type { Catalogs } from '@pnpm/catalogs.types'
|
||||
import { createMatcher } from '@pnpm/config.matcher'
|
||||
import { parseOverrides } from '@pnpm/config.parse-overrides'
|
||||
import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package'
|
||||
import { LOCKFILE_VERSION, WANTED_LOCKFILE } from '@pnpm/constants'
|
||||
import { LOCKFILE_VERSION } from '@pnpm/constants'
|
||||
import * as dp from '@pnpm/deps.path'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { createReadPackageHook } from '@pnpm/hooks.read-package-hook'
|
||||
import type { ResolveLatestDispatcher } from '@pnpm/installing.client'
|
||||
import {
|
||||
getLockfileImporterId,
|
||||
type LockfileObject,
|
||||
type ProjectSnapshot,
|
||||
} from '@pnpm/lockfile.fs'
|
||||
import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils'
|
||||
import { getAllDependenciesFromManifest } from '@pnpm/pkg-manifest.utils'
|
||||
import { parseBareSpecifier } from '@pnpm/resolving.npm-resolver'
|
||||
import {
|
||||
DEPENDENCIES_FIELDS,
|
||||
type DependenciesField,
|
||||
type DepPath,
|
||||
type IncludedDependencies,
|
||||
type PackageManifest,
|
||||
type PackageVersionPolicy,
|
||||
type ProjectManifest,
|
||||
type Registries,
|
||||
} from '@pnpm/types'
|
||||
import semver from 'semver'
|
||||
|
||||
export * from './createManifestGetter.js'
|
||||
|
||||
export type GetLatestManifestFunction = (packageName: string, rangeOrTag: string) => Promise<PackageManifest | null>
|
||||
|
||||
export interface OutdatedPackage {
|
||||
alias: string
|
||||
belongsTo: DependenciesField
|
||||
@@ -49,7 +46,7 @@ export async function outdated (
|
||||
catalogs?: Catalogs
|
||||
compatible?: boolean
|
||||
currentLockfile: LockfileObject | null
|
||||
getLatestManifest: GetLatestManifestFunction
|
||||
resolveLatest: ResolveLatestDispatcher
|
||||
ignoreDependencies?: string[]
|
||||
include?: IncludedDependencies
|
||||
lockfileDir: string
|
||||
@@ -58,7 +55,8 @@ export async function outdated (
|
||||
minimumReleaseAge?: number
|
||||
minimumReleaseAgeExclude?: string[]
|
||||
prefix: string
|
||||
registries: Registries
|
||||
publishedBy?: Date
|
||||
publishedByExclude?: PackageVersionPolicy
|
||||
wantedLockfile: LockfileObject | null
|
||||
}
|
||||
): Promise<OutdatedPackage[]> {
|
||||
@@ -89,6 +87,14 @@ export async function outdated (
|
||||
|
||||
const ignoreDependenciesMatcher = opts.ignoreDependencies?.length ? createMatcher(opts.ignoreDependencies) : undefined
|
||||
|
||||
const resolveOpts = {
|
||||
lockfileDir: opts.lockfileDir,
|
||||
preferredVersions: {},
|
||||
projectDir: opts.prefix,
|
||||
publishedBy: opts.publishedBy,
|
||||
publishedByExclude: opts.publishedByExclude,
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
DEPENDENCIES_FIELDS.map(async (depType) => {
|
||||
if (
|
||||
@@ -107,41 +113,37 @@ export async function outdated (
|
||||
await Promise.all(
|
||||
pkgs.map(async (alias) => {
|
||||
if (!allDeps[alias]) return
|
||||
const ref = opts.wantedLockfile!.importers[importerId][depType]![alias]
|
||||
|
||||
if (
|
||||
ref.startsWith('file:') || // ignoring linked packages. (For backward compatibility)
|
||||
ignoreDependenciesMatcher?.(alias)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const relativeDepPath = dp.refToRelative(ref, alias)
|
||||
|
||||
// ignoring linked packages
|
||||
if (relativeDepPath === null) return
|
||||
|
||||
const pkgSnapshot = opts.wantedLockfile!.packages?.[relativeDepPath]
|
||||
|
||||
if (pkgSnapshot == null) {
|
||||
throw new Error(`Invalid ${WANTED_LOCKFILE} file. ${relativeDepPath} not found in packages field`)
|
||||
}
|
||||
const wantedRef = opts.wantedLockfile!.importers[importerId][depType]![alias]
|
||||
if (ignoreDependenciesMatcher?.(alias)) return
|
||||
|
||||
const currentRef = (currentLockfile.importers[importerId] as ProjectSnapshot)?.[depType]?.[alias]
|
||||
const currentRelative = currentRef && dp.refToRelative(currentRef, alias)
|
||||
const current = (currentRelative && dp.parse(currentRelative).version) ?? currentRef
|
||||
const wanted = dp.parse(relativeDepPath).version ?? ref
|
||||
const { name: packageName } = nameVerFromPkgSnapshot(relativeDepPath, pkgSnapshot)
|
||||
const name = dp.parse(relativeDepPath).name ?? packageName
|
||||
const wantedRelative = dp.refToRelative(wantedRef, alias)
|
||||
const currentRelative = currentRef ? dp.refToRelative(currentRef, alias) : null
|
||||
const wantedSnapshot = wantedRelative != null ? opts.wantedLockfile!.packages?.[wantedRelative] : undefined
|
||||
const currentSnapshot = currentRelative != null ? currentLockfile.packages?.[currentRelative] : undefined
|
||||
// Aliased npm deps lock under their real name (e.g. `positive: is-positive@3.1.0`);
|
||||
// pull the name off the depPath so the report shows the real package.
|
||||
const packageName = (wantedRelative != null ? dp.parse(wantedRelative).name : undefined) ?? alias
|
||||
|
||||
const bareSpecifier = _replaceCatalogProtocolIfNecessary({ alias, bareSpecifier: allDeps[alias] })
|
||||
// If the npm resolve parser cannot parse the spec of the dependency,
|
||||
// it means that the package is not from a npm-compatible registry.
|
||||
// In that case, we can't check whether the package is up-to-date
|
||||
if (
|
||||
parseBareSpecifier(bareSpecifier, alias, 'latest', pickRegistryForPackage(opts.registries, name)) == null
|
||||
) {
|
||||
if (current !== wanted) {
|
||||
|
||||
const info = await opts.resolveLatest(
|
||||
{ wantedDependency: { alias, bareSpecifier }, compatible: opts.compatible },
|
||||
resolveOpts
|
||||
)
|
||||
if (info == null) return // resolver doesn't claim this dep — skip silently
|
||||
|
||||
const wanted = displayVersion(wantedRef, wantedRelative, wantedSnapshot?.version)
|
||||
const current = currentRef ? displayVersion(currentRef, currentRelative, currentSnapshot?.version) : undefined
|
||||
const { latestManifest } = info
|
||||
|
||||
// Compare the parsed `wanted` / `current` rather than raw refs.
|
||||
// For npm-style deps that means peer-graph-only changes (same
|
||||
// semver, different `(peer-hash)`) don't surface as fake
|
||||
// "outdated" entries; for URL/git refs the display values *are*
|
||||
// the refs, so a commit/path change still fires correctly.
|
||||
if (latestManifest == null) {
|
||||
if (wanted !== current) {
|
||||
outdated.push({
|
||||
alias,
|
||||
belongsTo: depType,
|
||||
@@ -154,14 +156,6 @@ export async function outdated (
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const latestManifest = await opts.getLatestManifest(
|
||||
name,
|
||||
opts.compatible ? (bareSpecifier ?? 'latest') : 'latest'
|
||||
)
|
||||
|
||||
if (latestManifest == null) return
|
||||
|
||||
if (!current) {
|
||||
outdated.push({
|
||||
alias,
|
||||
@@ -170,12 +164,10 @@ export async function outdated (
|
||||
packageName,
|
||||
wanted,
|
||||
workspace: opts.manifest.name,
|
||||
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (current !== wanted || semver.lt(current, latestManifest.version) || latestManifest.deprecated) {
|
||||
if (wanted !== current || isLowerVersion(wanted, latestManifest.version) || latestManifest.deprecated) {
|
||||
outdated.push({
|
||||
alias,
|
||||
belongsTo: depType,
|
||||
@@ -184,7 +176,6 @@ export async function outdated (
|
||||
packageName,
|
||||
wanted,
|
||||
workspace: opts.manifest.name,
|
||||
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -205,6 +196,33 @@ function isEmpty (obj: object): boolean {
|
||||
return Object.keys(obj).length === 0
|
||||
}
|
||||
|
||||
// Pick a clean display string for a lockfile ref.
|
||||
//
|
||||
// - If the dep-path parses to a semver, that's the value (handles
|
||||
// `pkg@1.0.0(peer-hash)` and aliased `positive: is-positive@3.1.0`).
|
||||
// - If the dep-path's non-semver version contains a `/`, it's a
|
||||
// URL/git-shape (`https://`, `git+ssh://`, scheme-less `github.com/…/sha`,
|
||||
// `link:../foo`, etc.) — return the raw ref so a commit/path change is
|
||||
// visible to the user.
|
||||
// - Otherwise prefer `snapshot.version` (clean semver for `runtime:`-style
|
||||
// refs); fall back to the raw ref when the snapshot didn't record one.
|
||||
function displayVersion (ref: string, relativeDepPath: DepPath | null, snapshotVersion: string | undefined): string {
|
||||
if (relativeDepPath != null) {
|
||||
const parsed = dp.parse(relativeDepPath)
|
||||
if (parsed.version != null) return parsed.version
|
||||
if (parsed.nonSemverVersion?.includes('/')) return ref
|
||||
}
|
||||
return snapshotVersion ?? ref
|
||||
}
|
||||
|
||||
// semver.lt throws on non-semver strings (e.g. URL refs from git/tarball).
|
||||
// Treat those as "not lower" so a ref change still gets surfaced via the
|
||||
// `wantedRef !== currentRef` check above.
|
||||
function isLowerVersion (current: string, latest: string): boolean {
|
||||
if (!semver.valid(current) || !semver.valid(latest)) return false
|
||||
return semver.lt(current, latest)
|
||||
}
|
||||
|
||||
function replaceCatalogProtocolIfNecessary (catalogs: Catalogs, wantedDependency: WantedDependency) {
|
||||
return matchCatalogResolveResult(resolveFromCatalog(catalogs, wantedDependency), {
|
||||
unused: () => wantedDependency.bareSpecifier,
|
||||
|
||||
@@ -2,6 +2,8 @@ import path from 'node:path'
|
||||
|
||||
import type { Catalogs } from '@pnpm/catalogs.types'
|
||||
import { createMatcher } from '@pnpm/config.matcher'
|
||||
import { getPublishedByPolicy } from '@pnpm/config.version-policy'
|
||||
import { type ClientOptions, createResolver } from '@pnpm/installing.client'
|
||||
import {
|
||||
readCurrentLockfile,
|
||||
readWantedLockfile,
|
||||
@@ -10,23 +12,33 @@ import type {
|
||||
IncludedDependencies,
|
||||
ProjectManifest,
|
||||
ProjectRootDir,
|
||||
RegistryConfig,
|
||||
} from '@pnpm/types'
|
||||
import { unnest } from 'ramda'
|
||||
|
||||
import { createManifestGetter, type ManifestGetterOptions } from './createManifestGetter.js'
|
||||
import { outdated, type OutdatedPackage } from './outdated.js'
|
||||
|
||||
export type OutdatedDepsOfProjectsOptions = Omit<ClientOptions, 'configByUri' | 'minimumReleaseAgeExclude' | 'storeIndex'>
|
||||
& {
|
||||
dir: string
|
||||
lockfileDir?: string
|
||||
configByUri: Record<string, RegistryConfig>
|
||||
fullMetadata?: boolean
|
||||
minimumReleaseAge?: number
|
||||
minimumReleaseAgeExclude?: string[]
|
||||
minimumReleaseAgeIgnoreMissingTime?: boolean
|
||||
minimumReleaseAgeStrict?: boolean
|
||||
}
|
||||
|
||||
export async function outdatedDepsOfProjects (
|
||||
pkgs: Array<{ rootDir: ProjectRootDir, manifest: ProjectManifest }>,
|
||||
args: string[],
|
||||
opts: Omit<ManifestGetterOptions, 'fullMetadata' | 'lockfileDir'> & {
|
||||
opts: OutdatedDepsOfProjectsOptions & {
|
||||
catalogs?: Catalogs
|
||||
compatible?: boolean
|
||||
ignoreDependencies?: string[]
|
||||
include: IncludedDependencies
|
||||
minimumReleaseAge?: number
|
||||
minimumReleaseAgeExclude?: string[]
|
||||
} & Partial<Pick<ManifestGetterOptions, 'fullMetadata' | 'lockfileDir'>>
|
||||
}
|
||||
): Promise<OutdatedPackage[][]> {
|
||||
if (!opts.lockfileDir) {
|
||||
return unnest(await Promise.all(
|
||||
@@ -39,20 +51,23 @@ export async function outdatedDepsOfProjects (
|
||||
const internalPnpmDir = path.join(path.join(lockfileDir, 'node_modules/.pnpm'))
|
||||
const currentLockfile = await readCurrentLockfile(internalPnpmDir, { ignoreIncompatible: false })
|
||||
const wantedLockfile = await readWantedLockfile(lockfileDir, { ignoreIncompatible: false }) ?? currentLockfile
|
||||
const getLatestManifest = createManifestGetter({
|
||||
const { publishedBy, publishedByExclude } = getPublishedByPolicy(opts)
|
||||
|
||||
const { resolveLatest } = createResolver({
|
||||
...opts,
|
||||
configByUri: opts.configByUri,
|
||||
filterMetadata: false,
|
||||
fullMetadata: opts.fullMetadata === true || Boolean(opts.minimumReleaseAge),
|
||||
lockfileDir,
|
||||
minimumReleaseAge: opts.minimumReleaseAge,
|
||||
minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude,
|
||||
ignoreMissingTimeField: opts.minimumReleaseAgeIgnoreMissingTime,
|
||||
})
|
||||
|
||||
return Promise.all(pkgs.map(async ({ rootDir, manifest }): Promise<OutdatedPackage[]> => {
|
||||
const match = (args.length > 0) && createMatcher(args) || undefined
|
||||
return outdated({
|
||||
catalogs: opts.catalogs,
|
||||
compatible: opts.compatible,
|
||||
currentLockfile,
|
||||
getLatestManifest,
|
||||
resolveLatest,
|
||||
ignoreDependencies: opts.ignoreDependencies,
|
||||
include: opts.include,
|
||||
lockfileDir,
|
||||
@@ -61,7 +76,8 @@ export async function outdatedDepsOfProjects (
|
||||
minimumReleaseAge: opts.minimumReleaseAge,
|
||||
minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude,
|
||||
prefix: rootDir,
|
||||
registries: opts.registries,
|
||||
publishedBy,
|
||||
publishedByExclude,
|
||||
wantedLockfile,
|
||||
})
|
||||
}))
|
||||
|
||||
270
deps/inspection/outdated/test/outdated.spec.ts
vendored
270
deps/inspection/outdated/test/outdated.spec.ts
vendored
@@ -1,10 +1,44 @@
|
||||
import { expect, test } from '@jest/globals'
|
||||
import { LOCKFILE_VERSION } from '@pnpm/constants'
|
||||
import type { DepPath, ProjectId } from '@pnpm/types'
|
||||
import type { ResolveLatestDispatcher } from '@pnpm/installing.client'
|
||||
import type { DepPath, PackageManifest, ProjectId } from '@pnpm/types'
|
||||
|
||||
import { outdated } from '../lib/outdated.js'
|
||||
|
||||
async function getLatestManifest (packageName: string) {
|
||||
type ManifestGetter = (packageName: string) => Promise<PackageManifest | null>
|
||||
|
||||
// Test stand-in for the real dispatcher: route by protocol shape, look up
|
||||
// "latest" through the per-test mock. Resolvers for non-latest protocols
|
||||
// (git, tarball) return `{}` to claim the dep with no latest available;
|
||||
// local refs return undefined so the dispatcher falls through and skips.
|
||||
function makeResolveLatest (getLatest: ManifestGetter): ResolveLatestDispatcher {
|
||||
return async (query) => {
|
||||
const { alias, bareSpecifier } = query.wantedDependency
|
||||
if (bareSpecifier?.startsWith('file:') || bareSpecifier?.startsWith('link:') || bareSpecifier?.startsWith('workspace:')) {
|
||||
return undefined
|
||||
}
|
||||
if (bareSpecifier?.startsWith('runtime:') && (alias === 'node' || alias === 'bun' || alias === 'deno')) {
|
||||
const latestManifest = await getLatest(alias)
|
||||
return { latestManifest: latestManifest ?? undefined }
|
||||
}
|
||||
if (
|
||||
bareSpecifier?.startsWith('http://') || bareSpecifier?.startsWith('https://') ||
|
||||
bareSpecifier?.startsWith('github:') || bareSpecifier?.startsWith('git+') || bareSpecifier?.startsWith('git:')
|
||||
) {
|
||||
return {}
|
||||
}
|
||||
let pkgName = alias ?? ''
|
||||
if (bareSpecifier?.startsWith('npm:')) {
|
||||
const inner = bareSpecifier.slice(4)
|
||||
const atIdx = inner.lastIndexOf('@')
|
||||
pkgName = (atIdx > 0 ? inner.slice(0, atIdx) : inner) || pkgName
|
||||
}
|
||||
const latestManifest = await getLatest(pkgName)
|
||||
return { latestManifest: latestManifest ?? undefined }
|
||||
}
|
||||
}
|
||||
|
||||
async function getLatestManifest (packageName: string): Promise<PackageManifest | null> {
|
||||
return ({
|
||||
'deprecated-pkg': {
|
||||
deprecated: 'This package is deprecated',
|
||||
@@ -23,9 +57,11 @@ async function getLatestManifest (packageName: string) {
|
||||
name: 'pkg-with-1-dep',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})[packageName] ?? null
|
||||
} as Record<string, PackageManifest>)[packageName] ?? null
|
||||
}
|
||||
|
||||
const resolveLatest = makeResolveLatest(getLatestManifest)
|
||||
|
||||
test('outdated()', async () => {
|
||||
const outdatedPkgs = await outdated({
|
||||
currentLockfile: {
|
||||
@@ -70,7 +106,7 @@ test('outdated()', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
getLatestManifest,
|
||||
resolveLatest,
|
||||
lockfileDir: 'project',
|
||||
manifest: {
|
||||
name: 'wanted-shrinkwrap',
|
||||
@@ -136,9 +172,6 @@ test('outdated()', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
registries: {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
},
|
||||
})
|
||||
expect(outdatedPkgs).toStrictEqual([
|
||||
{
|
||||
@@ -209,7 +242,7 @@ test('outdated() should return deprecated package even if its current version is
|
||||
}
|
||||
const outdatedPkgs = await outdated({
|
||||
currentLockfile: lockfile,
|
||||
getLatestManifest,
|
||||
resolveLatest,
|
||||
lockfileDir: 'project',
|
||||
manifest: {
|
||||
name: 'wanted-shrinkwrap',
|
||||
@@ -221,9 +254,6 @@ test('outdated() should return deprecated package even if its current version is
|
||||
},
|
||||
prefix: 'project',
|
||||
wantedLockfile: lockfile,
|
||||
registries: {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
},
|
||||
})
|
||||
expect(outdatedPkgs).toStrictEqual([
|
||||
{
|
||||
@@ -286,7 +316,7 @@ test('outdated() with minimumReleaseAge', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
getLatestManifest: getLatestManifestForMinimumAge,
|
||||
resolveLatest: makeResolveLatest(getLatestManifestForMinimumAge),
|
||||
lockfileDir: 'project',
|
||||
manifest: {
|
||||
name: 'with-min-age',
|
||||
@@ -324,9 +354,6 @@ test('outdated() with minimumReleaseAge', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
registries: {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
},
|
||||
minimumReleaseAge: 10080,
|
||||
})
|
||||
|
||||
@@ -402,7 +429,7 @@ test('outdated() with minimumReleaseAgeExclude', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
getLatestManifest: getLatestManifestWithExclude,
|
||||
resolveLatest: makeResolveLatest(getLatestManifestWithExclude),
|
||||
lockfileDir: 'project',
|
||||
manifest: {
|
||||
name: 'with-exclude',
|
||||
@@ -440,9 +467,6 @@ test('outdated() with minimumReleaseAgeExclude', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
registries: {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
},
|
||||
minimumReleaseAge: 10080,
|
||||
minimumReleaseAgeExclude: ['is-negative'],
|
||||
})
|
||||
@@ -515,7 +539,7 @@ test('using a matcher', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
getLatestManifest,
|
||||
resolveLatest,
|
||||
lockfileDir: 'wanted-shrinkwrap',
|
||||
manifest: {
|
||||
name: 'wanted-shrinkwrap',
|
||||
@@ -575,9 +599,6 @@ test('using a matcher', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
registries: {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
},
|
||||
})
|
||||
expect(outdatedPkgs).toStrictEqual([
|
||||
{
|
||||
@@ -617,7 +638,7 @@ test('outdated() aliased dependency', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
getLatestManifest,
|
||||
resolveLatest,
|
||||
lockfileDir: 'project',
|
||||
manifest: {
|
||||
name: 'wanted-shrinkwrap',
|
||||
@@ -648,9 +669,6 @@ test('outdated() aliased dependency', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
registries: {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
},
|
||||
})
|
||||
expect(outdatedPkgs).toStrictEqual([
|
||||
{
|
||||
@@ -705,7 +723,7 @@ test('a dependency is not outdated if it is newer than the latest version', asyn
|
||||
}
|
||||
const outdatedPkgs = await outdated({
|
||||
currentLockfile: lockfile,
|
||||
getLatestManifest: async (packageName) => {
|
||||
resolveLatest: makeResolveLatest( async (packageName) => {
|
||||
switch (packageName) {
|
||||
case 'foo':
|
||||
return {
|
||||
@@ -724,7 +742,7 @@ test('a dependency is not outdated if it is newer than the latest version', asyn
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
}),
|
||||
lockfileDir: 'project',
|
||||
manifest: {
|
||||
name: 'pkg',
|
||||
@@ -738,9 +756,6 @@ test('a dependency is not outdated if it is newer than the latest version', asyn
|
||||
},
|
||||
prefix: 'project',
|
||||
wantedLockfile: lockfile,
|
||||
registries: {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
},
|
||||
})
|
||||
expect(outdatedPkgs).toStrictEqual([])
|
||||
})
|
||||
@@ -748,9 +763,9 @@ test('a dependency is not outdated if it is newer than the latest version', asyn
|
||||
test('outdated() should [] when there is no dependency', async () => {
|
||||
const outdatedPkgs = await outdated({
|
||||
currentLockfile: null,
|
||||
getLatestManifest: async () => {
|
||||
resolveLatest: makeResolveLatest( async () => {
|
||||
return null
|
||||
},
|
||||
}),
|
||||
lockfileDir: 'project',
|
||||
manifest: {
|
||||
name: 'pkg',
|
||||
@@ -758,9 +773,6 @@ test('outdated() should [] when there is no dependency', async () => {
|
||||
},
|
||||
prefix: 'project',
|
||||
wantedLockfile: null,
|
||||
registries: {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
},
|
||||
})
|
||||
expect(outdatedPkgs).toStrictEqual([])
|
||||
})
|
||||
@@ -810,7 +822,7 @@ test('should ignore dependencies as expected', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
getLatestManifest,
|
||||
resolveLatest,
|
||||
lockfileDir: 'project',
|
||||
manifest: {
|
||||
name: 'wanted-shrinkwrap',
|
||||
@@ -876,9 +888,6 @@ test('should ignore dependencies as expected', async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
registries: {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
},
|
||||
ignoreDependencies: [
|
||||
'from-*',
|
||||
'is-negative',
|
||||
@@ -899,3 +908,182 @@ test('should ignore dependencies as expected', async () => {
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('outdated() lists outdated runtimes (node, deno, bun)', async () => {
|
||||
const runtimeLatestManifest = async (packageName: string) => {
|
||||
return ({
|
||||
node: { name: 'node', version: '23.0.0' },
|
||||
deno: { name: 'deno', version: '2.5.0' },
|
||||
bun: { name: 'bun', version: '1.1.42' },
|
||||
} as Record<string, PackageManifest>)[packageName] ?? null
|
||||
}
|
||||
|
||||
const lockfile = {
|
||||
importers: {
|
||||
['.' as ProjectId]: {
|
||||
dependencies: {
|
||||
node: 'runtime:22.0.0',
|
||||
deno: 'runtime:2.4.2',
|
||||
},
|
||||
devDependencies: {
|
||||
bun: 'runtime:1.1.40',
|
||||
},
|
||||
specifiers: {
|
||||
node: 'runtime:^22.0.0',
|
||||
deno: 'runtime:2.4.2',
|
||||
bun: 'runtime:^1.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
lockfileVersion: LOCKFILE_VERSION,
|
||||
packages: {
|
||||
['node@runtime:22.0.0' as DepPath]: {
|
||||
version: '22.0.0',
|
||||
resolution: { type: 'variations', variants: [] } as any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
},
|
||||
['deno@runtime:2.4.2' as DepPath]: {
|
||||
version: '2.4.2',
|
||||
resolution: { type: 'variations', variants: [] } as any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
},
|
||||
['bun@runtime:1.1.40' as DepPath]: {
|
||||
version: '1.1.40',
|
||||
resolution: { type: 'variations', variants: [] } as any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const outdatedPkgs = await outdated({
|
||||
currentLockfile: lockfile,
|
||||
resolveLatest: makeResolveLatest(runtimeLatestManifest),
|
||||
lockfileDir: 'project',
|
||||
manifest: {
|
||||
name: 'has-runtimes',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
node: 'runtime:^22.0.0',
|
||||
deno: 'runtime:2.4.2',
|
||||
},
|
||||
devDependencies: {
|
||||
bun: 'runtime:^1.1.0',
|
||||
},
|
||||
},
|
||||
prefix: 'project',
|
||||
wantedLockfile: lockfile,
|
||||
})
|
||||
|
||||
expect(outdatedPkgs).toStrictEqual([
|
||||
{
|
||||
alias: 'bun',
|
||||
belongsTo: 'devDependencies',
|
||||
current: '1.1.40',
|
||||
latestManifest: { name: 'bun', version: '1.1.42' },
|
||||
packageName: 'bun',
|
||||
wanted: '1.1.40',
|
||||
workspace: 'has-runtimes',
|
||||
},
|
||||
{
|
||||
alias: 'deno',
|
||||
belongsTo: 'dependencies',
|
||||
current: '2.4.2',
|
||||
latestManifest: { name: 'deno', version: '2.5.0' },
|
||||
packageName: 'deno',
|
||||
wanted: '2.4.2',
|
||||
workspace: 'has-runtimes',
|
||||
},
|
||||
{
|
||||
alias: 'node',
|
||||
belongsTo: 'dependencies',
|
||||
current: '22.0.0',
|
||||
latestManifest: { name: 'node', version: '23.0.0' },
|
||||
packageName: 'node',
|
||||
wanted: '22.0.0',
|
||||
workspace: 'has-runtimes',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('outdated() runtime in --compatible mode resolves within the declared range', async () => {
|
||||
const getLatestForCompat = async (packageName: string): Promise<PackageManifest | null> => {
|
||||
expect(packageName).toBe('node')
|
||||
return { name: 'node', version: '22.5.0' }
|
||||
}
|
||||
|
||||
const lockfile = {
|
||||
importers: {
|
||||
['.' as ProjectId]: {
|
||||
dependencies: { node: 'runtime:22.0.0' },
|
||||
specifiers: { node: 'runtime:^22.0.0' },
|
||||
},
|
||||
},
|
||||
lockfileVersion: LOCKFILE_VERSION,
|
||||
packages: {
|
||||
['node@runtime:22.0.0' as DepPath]: {
|
||||
version: '22.0.0',
|
||||
resolution: { type: 'variations', variants: [] } as any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const outdatedPkgs = await outdated({
|
||||
compatible: true,
|
||||
currentLockfile: lockfile,
|
||||
resolveLatest: makeResolveLatest(getLatestForCompat),
|
||||
lockfileDir: 'project',
|
||||
manifest: {
|
||||
name: 'with-runtime-range',
|
||||
version: '1.0.0',
|
||||
dependencies: { node: 'runtime:^22.0.0' },
|
||||
},
|
||||
prefix: 'project',
|
||||
wantedLockfile: lockfile,
|
||||
})
|
||||
|
||||
expect(outdatedPkgs).toStrictEqual([
|
||||
{
|
||||
alias: 'node',
|
||||
belongsTo: 'dependencies',
|
||||
current: '22.0.0',
|
||||
latestManifest: { name: 'node', version: '22.5.0' },
|
||||
packageName: 'node',
|
||||
wanted: '22.0.0',
|
||||
workspace: 'with-runtime-range',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('outdated() does not list runtime that is already up to date', async () => {
|
||||
const getLatestManifestUpToDate = async (packageName: string) => {
|
||||
return packageName === 'node' ? { name: 'node', version: '22.0.0' } : null
|
||||
}
|
||||
|
||||
const lockfile = {
|
||||
importers: {
|
||||
['.' as ProjectId]: {
|
||||
dependencies: { node: 'runtime:22.0.0' },
|
||||
specifiers: { node: 'runtime:22.0.0' },
|
||||
},
|
||||
},
|
||||
lockfileVersion: LOCKFILE_VERSION,
|
||||
packages: {
|
||||
['node@runtime:22.0.0' as DepPath]: {
|
||||
version: '22.0.0',
|
||||
resolution: { type: 'variations', variants: [] } as any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const outdatedPkgs = await outdated({
|
||||
currentLockfile: lockfile,
|
||||
resolveLatest: makeResolveLatest(getLatestManifestUpToDate),
|
||||
lockfileDir: 'project',
|
||||
manifest: {
|
||||
name: 'up-to-date',
|
||||
version: '1.0.0',
|
||||
dependencies: { node: 'runtime:22.0.0' },
|
||||
},
|
||||
prefix: 'project',
|
||||
wantedLockfile: lockfile,
|
||||
})
|
||||
|
||||
expect(outdatedPkgs).toStrictEqual([])
|
||||
})
|
||||
|
||||
3
deps/inspection/outdated/tsconfig.json
vendored
3
deps/inspection/outdated/tsconfig.json
vendored
@@ -21,9 +21,6 @@
|
||||
{
|
||||
"path": "../../../config/parse-overrides"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/pick-registry-for-package"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/version-policy"
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { fetchShasumsFile } from '@pnpm/crypto.shasums-file'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import type { FetchFromRegistry } from '@pnpm/fetching.types'
|
||||
import type { NpmResolver } from '@pnpm/resolving.npm-resolver'
|
||||
import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, type NpmResolver } from '@pnpm/resolving.npm-resolver'
|
||||
import type {
|
||||
BinaryResolution,
|
||||
LatestInfo,
|
||||
LatestQuery,
|
||||
PlatformAssetResolution,
|
||||
PlatformAssetTarget,
|
||||
ResolveOptions,
|
||||
@@ -65,6 +67,30 @@ export async function resolveBunRuntime (
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveLatestBunRuntime (
|
||||
ctx: { resolveFromNpm: NpmResolver },
|
||||
query: LatestQuery,
|
||||
opts: ResolveOptions
|
||||
): Promise<LatestInfo | undefined> {
|
||||
const manifestSpec = query.wantedDependency.bareSpecifier
|
||||
if (query.wantedDependency.alias !== 'bun' || !manifestSpec?.startsWith('runtime:')) return undefined
|
||||
const versionSpec = query.compatible ? manifestSpec.substring('runtime:'.length) : 'latest'
|
||||
try {
|
||||
const npmResolution = await ctx.resolveFromNpm(
|
||||
{ alias: 'bun', 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: 'bun', version: npmResolution.manifest.version } }
|
||||
} catch (err) {
|
||||
if (opts.publishedBy && (err as { code?: string }).code === 'ERR_PNPM_NO_MATCHING_VERSION') {
|
||||
return {}
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function readBunAssets (fetch: FetchFromRegistry, version: string): Promise<PlatformAssetResolution[]> {
|
||||
const integritiesFileUrl = `https://github.com/oven-sh/bun/releases/download/bun-v${version}/SHASUMS256.txt`
|
||||
const shasumsFileItems = await fetchShasumsFile(fetch, integritiesFileUrl)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import type { FetchFromRegistry } from '@pnpm/fetching.types'
|
||||
import type { NpmResolver } from '@pnpm/resolving.npm-resolver'
|
||||
import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, type NpmResolver } from '@pnpm/resolving.npm-resolver'
|
||||
import type {
|
||||
BinaryResolution,
|
||||
LatestInfo,
|
||||
LatestQuery,
|
||||
PlatformAssetResolution,
|
||||
PlatformAssetTarget,
|
||||
ResolveOptions,
|
||||
@@ -96,6 +98,30 @@ export async function resolveDenoRuntime (
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -3,6 +3,8 @@ import { PnpmError } from '@pnpm/error'
|
||||
import type { FetchFromRegistry } from '@pnpm/fetching.types'
|
||||
import type {
|
||||
BinaryResolution,
|
||||
LatestInfo,
|
||||
LatestQuery,
|
||||
PlatformAssetResolution,
|
||||
PlatformAssetTarget,
|
||||
ResolveOptions,
|
||||
@@ -79,6 +81,21 @@ export async function resolveNodeRuntime (
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveLatestNodeRuntime (
|
||||
ctx: { fetchFromRegistry: FetchFromRegistry, nodeDownloadMirrors?: Record<string, string> },
|
||||
query: LatestQuery,
|
||||
_opts: ResolveOptions
|
||||
): Promise<LatestInfo | undefined> {
|
||||
const manifestSpec = query.wantedDependency.bareSpecifier
|
||||
if (query.wantedDependency.alias !== 'node' || !manifestSpec?.startsWith('runtime:')) return undefined
|
||||
const versionSpec = query.compatible ? manifestSpec.substring('runtime:'.length) : 'latest'
|
||||
const { releaseChannel, versionSpecifier } = parseNodeSpecifier(versionSpec)
|
||||
const nodeMirrorBaseUrl = getNodeMirror(ctx.nodeDownloadMirrors, releaseChannel)
|
||||
const version = await resolveNodeVersion(ctx.fetchFromRegistry, versionSpecifier, nodeMirrorBaseUrl)
|
||||
if (!version) return {}
|
||||
return { latestManifest: { name: 'node', version } }
|
||||
}
|
||||
|
||||
async function readNodeAssets (fetch: FetchFromRegistry, nodeMirrorBaseUrl: string, version: string): Promise<PlatformAssetResolution[]> {
|
||||
const assets = await readNodeAssetsFromMirror(fetch, { nodeMirrorBaseUrl, version, muslOnly: false })
|
||||
|
||||
|
||||
@@ -14,14 +14,15 @@ import {
|
||||
createResolver as _createResolver,
|
||||
type ResolutionVerifierFactoryOptions,
|
||||
type ResolveFunction,
|
||||
type ResolveLatestDispatcher,
|
||||
type ResolverFactoryOptions,
|
||||
} from '@pnpm/resolving.default-resolver'
|
||||
import { MINIMUM_RELEASE_AGE_VIOLATION_CODE } from '@pnpm/resolving.npm-resolver'
|
||||
import type { ResolutionPolicyViolation, ResolutionVerifier } from '@pnpm/resolving.resolver-base'
|
||||
import type { LatestInfo, LatestQuery, ResolutionPolicyViolation, ResolutionVerifier } from '@pnpm/resolving.resolver-base'
|
||||
import type { StoreIndex } from '@pnpm/store.index'
|
||||
import type { RegistryConfig } from '@pnpm/types'
|
||||
|
||||
export type { ResolutionVerifier, ResolveFunction }
|
||||
export type { LatestInfo, LatestQuery, ResolutionVerifier, ResolveFunction, ResolveLatestDispatcher }
|
||||
|
||||
export type ClientOptions = {
|
||||
configByUri: Record<string, RegistryConfig>
|
||||
@@ -77,7 +78,7 @@ export function createClient (opts: ClientOptions): Client {
|
||||
}
|
||||
}
|
||||
|
||||
export function createResolver (opts: Omit<ClientOptions, 'storeIndex'>): { resolve: ResolveFunction, clearCache: () => void } {
|
||||
export function createResolver (opts: Omit<ClientOptions, 'storeIndex'>): { resolve: ResolveFunction, resolveLatest: ResolveLatestDispatcher, clearCache: () => void } {
|
||||
const fetchFromRegistry = createFetchFromRegistry(opts)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri, opts.registries?.default)
|
||||
|
||||
|
||||
@@ -5,7 +5,13 @@ import type {
|
||||
ProjectManifest,
|
||||
} from '@pnpm/types'
|
||||
|
||||
const RUNTIME_NAMES = ['node', 'deno', 'bun'] as const
|
||||
export const RUNTIME_NAMES = ['node', 'deno', 'bun'] as const
|
||||
|
||||
export type RuntimeName = typeof RUNTIME_NAMES[number]
|
||||
|
||||
export function isRuntimeAlias (alias: string): alias is RuntimeName {
|
||||
return (RUNTIME_NAMES as readonly string[]).includes(alias)
|
||||
}
|
||||
|
||||
export function convertEnginesRuntimeToDependencies (
|
||||
manifest: ProjectManifest,
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -3545,9 +3545,6 @@ importers:
|
||||
'@pnpm/config.parse-overrides':
|
||||
specifier: workspace:*
|
||||
version: link:../../../config/parse-overrides
|
||||
'@pnpm/config.pick-registry-for-package':
|
||||
specifier: workspace:*
|
||||
version: link:../../../config/pick-registry-for-package
|
||||
'@pnpm/config.version-policy':
|
||||
specifier: workspace:*
|
||||
version: link:../../../config/version-policy
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { type BunRuntimeResolveResult, resolveBunRuntime } from '@pnpm/engine.runtime.bun-resolver'
|
||||
import { type DenoRuntimeResolveResult, resolveDenoRuntime } from '@pnpm/engine.runtime.deno-resolver'
|
||||
import { type NodeRuntimeResolveResult, resolveNodeRuntime } from '@pnpm/engine.runtime.node-resolver'
|
||||
import { type BunRuntimeResolveResult, resolveBunRuntime, resolveLatestBunRuntime } from '@pnpm/engine.runtime.bun-resolver'
|
||||
import { type DenoRuntimeResolveResult, resolveDenoRuntime, resolveLatestDenoRuntime } from '@pnpm/engine.runtime.deno-resolver'
|
||||
import { type NodeRuntimeResolveResult, resolveLatestNodeRuntime, resolveNodeRuntime } from '@pnpm/engine.runtime.node-resolver'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import type { FetchFromRegistry, GetAuthHeader } from '@pnpm/fetching.types'
|
||||
import { checkCustomResolverCanResolve, type CustomResolver } from '@pnpm/hooks.types'
|
||||
import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header'
|
||||
import { createGitResolver, type GitResolveResult } from '@pnpm/resolving.git-resolver'
|
||||
import { type LocalResolveResult, resolveFromLocalPath, resolveFromLocalScheme } from '@pnpm/resolving.local-resolver'
|
||||
import { createGitResolver, type GitResolveResult, resolveLatestFromGit } from '@pnpm/resolving.git-resolver'
|
||||
import { type LocalResolveResult, resolveFromLocalPath, resolveFromLocalScheme, resolveLatestFromLocal } from '@pnpm/resolving.local-resolver'
|
||||
import {
|
||||
createNpmResolutionVerifier,
|
||||
type CreateNpmResolutionVerifierOptions,
|
||||
@@ -21,13 +21,15 @@ import {
|
||||
type WorkspaceResolveResult,
|
||||
} from '@pnpm/resolving.npm-resolver'
|
||||
import type {
|
||||
LatestInfo,
|
||||
LatestQuery,
|
||||
ResolutionVerifier,
|
||||
ResolveFunction,
|
||||
ResolveOptions,
|
||||
ResolveResult,
|
||||
WantedDependency,
|
||||
} from '@pnpm/resolving.resolver-base'
|
||||
import { resolveFromTarball, type TarballResolveResult } from '@pnpm/resolving.tarball-resolver'
|
||||
import { resolveFromTarball, resolveLatestFromTarball, type TarballResolveResult } from '@pnpm/resolving.tarball-resolver'
|
||||
import type { RegistryConfig } from '@pnpm/types'
|
||||
|
||||
export type {
|
||||
@@ -90,6 +92,8 @@ async function resolveFromCustomResolvers (
|
||||
return null
|
||||
}
|
||||
|
||||
export type ResolveLatestDispatcher = (query: LatestQuery, opts: ResolveOptions) => Promise<LatestInfo | undefined>
|
||||
|
||||
export function createResolver (
|
||||
fetchFromRegistry: FetchFromRegistry,
|
||||
getAuthHeader: GetAuthHeader,
|
||||
@@ -97,8 +101,16 @@ export function createResolver (
|
||||
nodeDownloadMirrors?: Record<string, string>
|
||||
customResolvers?: CustomResolver[]
|
||||
}
|
||||
): { resolve: DefaultResolver, clearCache: () => void } {
|
||||
const { resolveFromNpm, resolveFromJsr, resolveFromNamedRegistry, clearCache } = createNpmResolver(fetchFromRegistry, getAuthHeader, pnpmOpts)
|
||||
): { resolve: DefaultResolver, resolveLatest: ResolveLatestDispatcher, clearCache: () => void } {
|
||||
const {
|
||||
resolveFromNpm,
|
||||
resolveFromJsr,
|
||||
resolveFromNamedRegistry,
|
||||
resolveLatestFromNpm,
|
||||
resolveLatestFromJsr,
|
||||
resolveLatestFromNamedRegistry,
|
||||
clearCache,
|
||||
} = createNpmResolver(fetchFromRegistry, getAuthHeader, pnpmOpts)
|
||||
const resolveFromGit = createGitResolver(pnpmOpts)
|
||||
const localCtx = { preserveAbsolutePaths: pnpmOpts.preserveAbsolutePaths }
|
||||
const _resolveFromLocalScheme = resolveFromLocalScheme.bind(null, localCtx)
|
||||
@@ -106,6 +118,9 @@ export function createResolver (
|
||||
const _resolveNodeRuntime = resolveNodeRuntime.bind(null, { fetchFromRegistry, offline: pnpmOpts.offline, nodeDownloadMirrors: pnpmOpts.nodeDownloadMirrors })
|
||||
const _resolveDenoRuntime = resolveDenoRuntime.bind(null, { fetchFromRegistry, offline: pnpmOpts.offline, resolveFromNpm })
|
||||
const _resolveBunRuntime = resolveBunRuntime.bind(null, { fetchFromRegistry, offline: pnpmOpts.offline, resolveFromNpm })
|
||||
const _resolveLatestNodeRuntime = resolveLatestNodeRuntime.bind(null, { fetchFromRegistry, nodeDownloadMirrors: pnpmOpts.nodeDownloadMirrors })
|
||||
const _resolveLatestDenoRuntime = resolveLatestDenoRuntime.bind(null, { resolveFromNpm })
|
||||
const _resolveLatestBunRuntime = resolveLatestBunRuntime.bind(null, { resolveFromNpm })
|
||||
const _resolveFromCustomResolvers = pnpmOpts.customResolvers
|
||||
? resolveFromCustomResolvers.bind(null, pnpmOpts.customResolvers)
|
||||
: null
|
||||
@@ -141,6 +156,18 @@ export function createResolver (
|
||||
}
|
||||
return resolution
|
||||
},
|
||||
resolveLatest: async (query, opts) => {
|
||||
const info = (await resolveLatestFromNpm(query, opts)) ??
|
||||
(await resolveLatestFromJsr(query, opts)) ??
|
||||
(await resolveLatestFromGit(query)) ??
|
||||
(await resolveLatestFromTarball(query)) ??
|
||||
(await resolveLatestFromLocal(query)) ??
|
||||
(await _resolveLatestNodeRuntime(query, opts)) ??
|
||||
(await _resolveLatestDenoRuntime(query, opts)) ??
|
||||
(await _resolveLatestBunRuntime(query, opts)) ??
|
||||
(await resolveLatestFromNamedRegistry(query, opts))
|
||||
return info
|
||||
},
|
||||
clearCache,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import type { DispatcherOptions } from '@pnpm/network.fetch'
|
||||
import type { GitResolution, PkgResolutionId, ResolveOptions, ResolveResult, TarballResolution } from '@pnpm/resolving.resolver-base'
|
||||
import type { GitResolution, LatestInfo, LatestQuery, PkgResolutionId, ResolveOptions, ResolveResult, TarballResolution } from '@pnpm/resolving.resolver-base'
|
||||
import { gracefulGit as git } from 'graceful-git'
|
||||
import semver from 'semver'
|
||||
|
||||
@@ -101,6 +101,18 @@ export function createGitResolver (
|
||||
}
|
||||
}
|
||||
|
||||
// Git deps have no concept of "latest" — we'd need to query the host's tag list
|
||||
// to know about newer commits, which isn't a uniform thing across protocols.
|
||||
// Claim the dep so the dispatcher stops; the caller still surfaces a
|
||||
// ref-mismatch report if the lockfile shifted to a different commit.
|
||||
export async function resolveLatestFromGit (query: LatestQuery): Promise<LatestInfo | undefined> {
|
||||
const bareSpecifier = query.wantedDependency.bareSpecifier
|
||||
if (!bareSpecifier) return undefined
|
||||
const parsedSpecFunc = parseBareSpecifier(bareSpecifier, {})
|
||||
if (parsedSpecFunc == null) return undefined
|
||||
return {}
|
||||
}
|
||||
|
||||
function resolveVTags (vTags: string[], range: string): string | null {
|
||||
return semver.maxSatisfying(vTags, range, true)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from 'node:path'
|
||||
import { getTarballIntegrity } from '@pnpm/crypto.hash'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import type { DirectoryResolution, Resolution, ResolveResult, TarballResolution } from '@pnpm/resolving.resolver-base'
|
||||
import type { DirectoryResolution, LatestInfo, LatestQuery, Resolution, ResolveResult, TarballResolution } from '@pnpm/resolving.resolver-base'
|
||||
import type { DependencyManifest, PkgResolutionId } from '@pnpm/types'
|
||||
import { readProjectManifestOnly } from '@pnpm/workspace.project-manifest-reader'
|
||||
|
||||
@@ -64,6 +64,18 @@ export async function resolveFromLocalPath (
|
||||
return resolveSpec(spec, opts)
|
||||
}
|
||||
|
||||
// link:/file:/workspace: dependencies don't have a "latest" — claim them so
|
||||
// the dispatcher stops here. Returning undefined would let downstream
|
||||
// resolvers try; in particular, a user-configured named-registry alias
|
||||
// called `link`, `file`, or `workspace` could otherwise hijack these.
|
||||
export async function resolveLatestFromLocal (query: LatestQuery): Promise<LatestInfo | undefined> {
|
||||
const spec = query.wantedDependency.bareSpecifier
|
||||
if (spec?.startsWith('link:') || spec?.startsWith('file:') || spec?.startsWith('workspace:')) {
|
||||
return {}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function resolveSpec (
|
||||
spec: LocalPackageSpec | null,
|
||||
opts: LocalResolverOptions
|
||||
|
||||
@@ -10,10 +10,13 @@ import type {
|
||||
import type { PackageInRegistry, PackageMeta } from '@pnpm/resolving.registry.types'
|
||||
import type {
|
||||
DirectoryResolution,
|
||||
LatestInfo,
|
||||
LatestQuery,
|
||||
PkgResolutionId,
|
||||
PreferredVersions,
|
||||
Resolution,
|
||||
ResolutionPolicyViolation,
|
||||
ResolveOptions,
|
||||
ResolveResult,
|
||||
TarballResolution,
|
||||
WantedDependency,
|
||||
@@ -181,11 +184,24 @@ export type NpmResolver = (
|
||||
opts: ResolveFromNpmOptions
|
||||
) => Promise<NpmResolveResult | JsrResolveResult | NamedRegistryResolveResult | WorkspaceResolveResult | null>
|
||||
|
||||
export type ResolveLatestFromNpmStyle = (
|
||||
query: LatestQuery,
|
||||
opts: ResolveOptions
|
||||
) => Promise<LatestInfo | undefined>
|
||||
|
||||
export function createNpmResolver (
|
||||
fetchFromRegistry: FetchFromRegistry,
|
||||
getAuthHeader: GetAuthHeader,
|
||||
opts: ResolverFactoryOptions
|
||||
): { resolveFromNpm: NpmResolver, resolveFromJsr: NpmResolver, resolveFromNamedRegistry: NpmResolver, clearCache: () => void } {
|
||||
): {
|
||||
resolveFromNpm: NpmResolver
|
||||
resolveFromJsr: NpmResolver
|
||||
resolveFromNamedRegistry: NpmResolver
|
||||
resolveLatestFromNpm: ResolveLatestFromNpmStyle
|
||||
resolveLatestFromJsr: ResolveLatestFromNpmStyle
|
||||
resolveLatestFromNamedRegistry: ResolveLatestFromNpmStyle
|
||||
clearCache: () => void
|
||||
} {
|
||||
if (typeof opts.cacheDir !== 'string') {
|
||||
throw new TypeError('`opts.cacheDir` is required and needs to be a string')
|
||||
}
|
||||
@@ -250,10 +266,19 @@ export function createNpmResolver (
|
||||
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
|
||||
peekManifestFromStore,
|
||||
}
|
||||
const boundResolveFromNpm = resolveNpm.bind(null, ctx)
|
||||
const boundResolveFromJsr = resolveJsr.bind(null, ctx)
|
||||
const boundResolveFromNamedRegistry = resolveFromNamedRegistry.bind(null, ctx)
|
||||
const defaultRegistry = opts.registries.default
|
||||
return {
|
||||
resolveFromNpm: resolveNpm.bind(null, ctx),
|
||||
resolveFromJsr: resolveJsr.bind(null, ctx),
|
||||
resolveFromNamedRegistry: resolveFromNamedRegistry.bind(null, ctx),
|
||||
resolveFromNpm: boundResolveFromNpm,
|
||||
resolveFromJsr: boundResolveFromJsr,
|
||||
resolveFromNamedRegistry: boundResolveFromNamedRegistry,
|
||||
resolveLatestFromNpm: createResolveLatest(boundResolveFromNpm,
|
||||
(query) => isNpmSpec(query, defaultRegistry)),
|
||||
resolveLatestFromJsr: createResolveLatest(boundResolveFromJsr, isJsrSpec),
|
||||
resolveLatestFromNamedRegistry: createResolveLatest(boundResolveFromNamedRegistry,
|
||||
(query) => isNamedRegistrySpec(query, ctx.namedRegistryNames)),
|
||||
clearCache: () => {
|
||||
if ('clear' in metaCache && typeof metaCache.clear === 'function') {
|
||||
metaCache.clear()
|
||||
@@ -263,6 +288,70 @@ export function createNpmResolver (
|
||||
}
|
||||
}
|
||||
|
||||
function isNpmSpec (query: LatestQuery, defaultRegistry: string): boolean {
|
||||
const { alias, bareSpecifier } = query.wantedDependency
|
||||
if (!bareSpecifier) return alias != null
|
||||
return parseBareSpecifier(bareSpecifier, alias, 'latest', defaultRegistry) != null
|
||||
}
|
||||
|
||||
function isJsrSpec (query: LatestQuery): boolean {
|
||||
if (!query.wantedDependency.bareSpecifier?.startsWith('jsr:')) return false
|
||||
return parseJsrSpecifierToRegistryPackageSpec(
|
||||
query.wantedDependency.bareSpecifier,
|
||||
query.wantedDependency.alias,
|
||||
'latest'
|
||||
) != null
|
||||
}
|
||||
|
||||
function isNamedRegistrySpec (
|
||||
query: LatestQuery,
|
||||
knownRegistryNames: ReadonlySet<string>
|
||||
): boolean {
|
||||
if (!query.wantedDependency.bareSpecifier) return false
|
||||
try {
|
||||
return parseNamedRegistrySpecifierToRegistryPackageSpec(
|
||||
query.wantedDependency.bareSpecifier,
|
||||
knownRegistryNames,
|
||||
query.wantedDependency.alias,
|
||||
'latest'
|
||||
) != null
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function createResolveLatest (
|
||||
resolve: NpmResolver,
|
||||
matches: (query: LatestQuery) => boolean
|
||||
) {
|
||||
return async (query: LatestQuery, opts: ResolveOptions): Promise<LatestInfo | undefined> => {
|
||||
if (!matches(query)) return undefined
|
||||
// Always pass the manifest's bareSpecifier so protocol-prefixed specs
|
||||
// (`jsr:@scope/pkg@^1.0.0`, `gh:owner/repo@^1.0.0`) still match their
|
||||
// resolver. In --compatible mode that range drives the pick; otherwise
|
||||
// `update: 'latest'` tells the resolver to ignore the range and take
|
||||
// the absolute newest.
|
||||
const bareSpecifier = query.wantedDependency.bareSpecifier ?? 'latest'
|
||||
const resolveOpts = query.compatible ? opts : { ...opts, update: 'latest' as const }
|
||||
try {
|
||||
const result = await resolve(
|
||||
{ alias: query.wantedDependency.alias, bareSpecifier },
|
||||
resolveOpts
|
||||
)
|
||||
// Policy-blocked: handled but no latest to surface.
|
||||
if (result?.policyViolation?.code === MINIMUM_RELEASE_AGE_VIOLATION_CODE) {
|
||||
return {}
|
||||
}
|
||||
return { latestManifest: result?.manifest }
|
||||
} catch (err) {
|
||||
if (opts.publishedBy && (err as { code?: string }).code === 'ERR_PNPM_NO_MATCHING_VERSION') {
|
||||
return {}
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResolveFromNpmContext {
|
||||
pickPackage: (spec: RegistryPackageSpec, opts: PickPackageOptions) => ReturnType<typeof pickPackage>
|
||||
getAuthHeaderValueByURI: (registry: string) => string | undefined
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
DependencyManifest,
|
||||
PackageManifest,
|
||||
PackageVersionPolicy,
|
||||
PinnedVersion,
|
||||
PkgResolutionId,
|
||||
@@ -312,3 +313,34 @@ export type WantedDependency = {
|
||||
})
|
||||
|
||||
export type ResolveFunction = (wantedDependency: WantedDependency & { optional?: boolean }, opts: ResolveOptions) => Promise<ResolveResult>
|
||||
|
||||
/**
|
||||
* Input to a resolver's `resolveLatest` function. The resolver decides
|
||||
* whether it owns this dep purely from `wantedDependency` (its alias and
|
||||
* manifest specifier) — the lockfile-resolved ref is the caller's
|
||||
* concern, not the resolver's.
|
||||
*/
|
||||
export interface LatestQuery {
|
||||
wantedDependency: WantedDependency
|
||||
compatible?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a resolver's `resolveLatest` call.
|
||||
*
|
||||
* - `undefined` means "this resolver does not handle this dep — try
|
||||
* the next one".
|
||||
* - An object (even without a `latestManifest`) means "I claim this
|
||||
* dep, but I can't tell you what's latest" (e.g. policy blocked,
|
||||
* network unavailable, no concept of latest for this protocol).
|
||||
* The caller still surfaces a ref-mismatch report if the lockfile
|
||||
* shifted.
|
||||
*/
|
||||
export interface LatestInfo {
|
||||
latestManifest?: PackageManifest
|
||||
}
|
||||
|
||||
export type ResolveLatestFunction = (
|
||||
query: LatestQuery,
|
||||
opts: ResolveOptions
|
||||
) => Promise<LatestInfo | undefined>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FetchFromRegistry } from '@pnpm/fetching.types'
|
||||
import type { PkgResolutionId, ResolveResult, TarballResolution } from '@pnpm/resolving.resolver-base'
|
||||
import type { LatestInfo, LatestQuery, PkgResolutionId, ResolveResult, TarballResolution } from '@pnpm/resolving.resolver-base'
|
||||
|
||||
export interface TarballResolveResult extends ResolveResult {
|
||||
normalizedBareSpecifier: string
|
||||
@@ -36,3 +36,12 @@ export async function resolveFromTarball (
|
||||
resolvedVia: 'url',
|
||||
}
|
||||
}
|
||||
|
||||
// URL tarballs lock to the exact URL — no concept of "latest". Claim the dep
|
||||
// so the dispatcher stops; the caller still surfaces a ref-mismatch report
|
||||
// if the lockfile points at a different URL than before.
|
||||
export async function resolveLatestFromTarball (query: LatestQuery): Promise<LatestInfo | undefined> {
|
||||
const bareSpecifier = query.wantedDependency.bareSpecifier
|
||||
if (!bareSpecifier?.startsWith('http:') && !bareSpecifier?.startsWith('https:')) return undefined
|
||||
return {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user