Skip to content

fix(libnpmexec): honor min-release-age-exclude in npx#9768

Open
Sanjays2402 wants to merge 2 commits into
npm:latestfrom
Sanjays2402:fix/issue-9765
Open

fix(libnpmexec): honor min-release-age-exclude in npx#9768
Sanjays2402 wants to merge 2 commits into
npm:latestfrom
Sanjays2402:fix/issue-9765

Conversation

@Sanjays2402

Copy link
Copy Markdown

Description

npx calls pacote.manifest(spec, { ...flatOptions }) directly, which forwards flatOptions.before (set by min-release-age) to pacote. pacote has no concept of min-release-age-exclude, so even packages whose name matches an exclude pattern are rejected with ETARGET. arborist already handles this per-spec inside npm install by dropping before when the resolved package name matches; libnpmexec needs to do the same.

Change in workspaces/libnpmexec/lib/index.js: before calling pacote.manifest in getManifest, run the spec through a new helper (applyReleaseAgeExclude) that returns a shallow copy of flatOptions with before set to null when the spec's trusted registry name matches minReleaseAgeExclude. npm: aliases are keyed on the alias target, mirroring arborist's trustedSpecName. The pattern semantics match arborist exactly (nonegate, nocomment, noext) so an exemption list can never silently widen into match-all.

Repro before the fix, with ~/.npmrc:

min-release-age=3
min-release-age-exclude[]=@myscope/*
$ npx @myscope/some-package@1.2.3
npm error code ETARGET
npm error notarget No matching version found for @myscope/some-package@1.2.3 with a date before <cutoff>.

After the fix, the exempt package resolves normally; unrelated packages still respect the cutoff.

Existing Issue

Fixes #9765

Test Coverage

New unit tests in workspaces/libnpmexec/test/release-age-exclude.js cover the helper end to end: exact and glob patterns, hardened !/#/extglob semantics, npm: alias resolution, non-mutation of the input options, and the passthrough branches (no before, no match, empty/undefined options). File coverage is 100%. The existing libnpmexec suite continues to pass, since the wire-in is a passthrough whenever before is unset (the norm in existing tests).

Verified both directions on the affected test file: it fails on main (module missing) and passes with the patch applied.

AI Disclosure

This change was written with the assistance of Claude. I have reviewed the diff and the test and take responsibility for the code.

npx calls pacote.manifest directly with flatOptions.before set by
min-release-age, but pacote has no notion of min-release-age-exclude
so npx would fail with ETARGET even for packages that should be
exempt. arborist already does the equivalent per-spec drop inside
npm install; do the same in libnpmexec's getManifest by clearing
before when the resolved spec name matches an exclude pattern.

Fixes: npm#9765
@Sanjays2402
Sanjays2402 requested review from a team as code owners July 15, 2026 09:24
Comment thread workspaces/libnpmexec/lib/index.js Outdated

// when checking the local tree we look up manifests, cache those results by
// spec.raw so we don't have to fetch again when we check npxCache
const manifests = new Map()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding min-release-age-exclude support to npx. I found one cache-lifetime issue that should be addressed before merging.

manifests is module-scoped cache and keyed only by spec.raw, while applyReleaseAgeExclude() runs only on a cache miss. If the first libnpmexec call excludes a package, it can cache a post-cutoff manifest resolved with before: null. A later call for the same package without the exclusion then reuses that manifest and bypasses its release-age restriction.

I reproduced this with two sequential calls: both executed the newer version, although the second call should have selected the older, cutoff-eligible version.

Could we scope the manifest cache to each exec() invocation and pass it through all three missingFromTree()? That preserves caching between the local and npx-tree checks while preventing policy-specific results from leaking across calls.

const getManifest = async (spec, flatOptions, manifestCache) => {
  if (!manifestCache.has(spec.raw)) {
    ...
    manifestCache.set(spec.raw, manifest)
  }
  return manifestCache.get(spec.raw)
}

...

const missingFromTree = async ({
  spec,
  tree,
  flatOptions,
  manifestCache,
  isNpxTree,
  shallow,
}) => {
   // Existing tree-checking logic...
    const manifest = await getManifest(spec, flatOptions, manifestCache)
     return { manifest }
   } else {
    const manifest = await getManifest(spec, flatOptions, manifestCache)

     // Existing logic...
   }
 }

Please also add a two-call regression test covering this scenario.

The manifest lookup cache was module-scoped and keyed only by spec.raw,
while min-release-age-exclude is applied only on a cache miss. The first
exec() for a spec matching the exclude list resolved and cached a manifest
with the `before` cutoff dropped; a later exec() for the same spec without
the exclusion reused that cached manifest and bypassed its release-age
restriction.

Move the cache into a per-exec() Map and thread it through getManifest and
all three missingFromTree() call sites, so caching still avoids duplicate
fetches within a single exec() while policy-specific results no longer leak
across separate exec() invocations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] min-release-age-exclude is not honored by npx

2 participants