@webpro/pnpm-exclude-newer
v1.0.0
Published
Resolve a pnpm lockfile whose entire dependency tree (direct + transitive) excludes versions published after a cutoff — a transitive minimumReleaseAge / uv-style --exclude-newer for pnpm.
Maintainers
Readme
pnpm-exclude-newer
NOTE: This script is 100% generated. Use at your own risk.
Bring a pnpm project up to the latest versions that are old enough to trust: every dependency —
direct and transitive — capped to what was published before a cutoff. Think uv's
--exclude-newer, or a transitive minimumReleaseAge, for pnpm.
By default it rewrites each direct dep range in package.json to the latest mature version
(keeping its ^/~ operator) and resolves a fully age-capped lockfile. Pass --no-bump to leave
package.json alone and only resolve the lockfile within the existing ranges.
pnpm dlx @webpro/pnpm-exclude-newer # cutoff from pnpm-workspace.yaml's minimumReleaseAge (else 1 day)
pnpm dlx @webpro/pnpm-exclude-newer --age 4320 # 3 days
pnpm dlx @webpro/pnpm-exclude-newer --exclude-newer 2026-05-30
pnpm dlx @webpro/pnpm-exclude-newer --no-bump # lockfile only, don't touch package.jsonWhy this exists
pnpm's minimumReleaseAge is verify-only: the
resolver picks the latest in-range version and then rejects it if it's too fresh
(ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION / ERR_PNPM_NO_MATURE_MATCHING_VERSION) — it does not
fall back to the latest mature version. resolutionMode: time-based is meant to do age-aware
resolution but is broken when combined with minimumReleaseAge. So out of the box you can't get a
lockfile that's mature all the way down, and the escape hatches are all exclusion-based.
Related pnpm issues: #10257 (time-based ignored), #11068 (transitive deps error out), #11203 (no intermediate fallback), #10488 (excludes don't cascade).
How it compares
Cooldown / min-release-age features that work at the manifest or PR layer only gate direct dependencies — your package manager still resolves the transitive tree to the freshest versions at install time:
| Tool | Operates on | Direct | Transitive |
| -------------------------------------------------- | ----------------------- | :------------------: | :---------: |
| npm-check-updates --cooldown | rewrites package.json | ✅ | ❌ |
| Renovate minimumReleaseAge / Dependabot cooldown | opens PRs | ✅ | ❌ |
| pnpm minimumReleaseAge | resolve + verify | ⚠️ latest-tag only | ❌ (errors) |
| pnpm-exclude-newer | registry resolution | ✅ | ✅ |
Verified: ncu --cooldown 3 on a project with a single direct dependency correctly cools that
dependency, yet its lockfile still contained a transitive dependency published 2 days earlier.
pnpm-exclude-newer on the same project left zero entries younger than the cutoff. Only
resolution-level filtering reaches transitive dependencies.
How it works
- Stands up a throwaway local registry mirror that hides every version published on/after the
cutoff (and repoints
dist-tags.latestto the newest mature version). - Unless
--no-bump, rewrites each direct dep range in yourpackage.json(s) to that latest mature version — preserving the^/~operator. This both updates stale ranges and lowers any whose floor is too fresh to have a mature match (e.g.^1.69.0→^1.68.0), which would otherwise error. Non-registry specs (workspace:,catalog:,file:, git, URL,npm:aliases,*, complex ranges) are left untouched. - Copies the manifests into a clean temp tree (your
node_moduleswould otherwise leak fresh peer versions) and runspnpm install --lockfile-onlyagainst the mirror — so pnpm's normal resolver produces a transitively age-capped tree. - Copies the lockfile back and runs a real
pnpm install, letting pnpm's own gate verify it.
Real integrity hashes and tarballs come straight from the upstream registry, so the lockfile stays
portable. If resolution fails, any package.json bumps from this run are reverted.
Options
| flag | meaning |
| ------------------------ | --------------------------------------------------------------------------------------------- |
| --exclude-newer <date> | hide versions published on/after <date> (e.g. 2026-05-30) |
| --age <minutes> | cutoff = now − minutes (default: minimumReleaseAge from pnpm-workspace.yaml, else 1440) |
| --no-bump | don't rewrite package.json; only resolve the lockfile within the existing ranges |
| --no-install | stop after writing the lockfile (skip the verifying install) |
| -h, --help | usage |
If a resolution fails, it means an already-mature package depends on a still-too-fresh version — wait for it to age, or raise the cutoff for that run.
Requirements & limitations
- Node ≥ 18 (global
fetch) andpnpmonPATH. - pnpm version: resolves with whatever
pnpmis onPATH(under corepack, the repo'spackageManagerpin — recommended for fidelity). A mismatch can change resolution; e.g. pnpm 11 ignores thepnpmfield inpackage.json, so on an older repo that keepsoverrides/patchedDependenciesthere, run it under the matching pnpm. - Registries: only the default registry is mirrored (read with its
_authTokenfromnpm config/.npmrc). Per-scope registries (@scope:registry=) bypass the mirror — those scopes aren't age-filtered (and private ones may fail). - Config fidelity: the isolated install copies
package.json(s),pnpm-workspace.yaml,.pnpmfile.cjs/pnpmfile.cjs, andpatches/. Resolution-affecting.npmrcsettings (e.g.auto-install-peers,node-linker) are not applied — keep them inpnpm-workspace.yaml. shared-workspace-lockfile=false(per-package lockfiles) isn't supported.- Non-registry deps (git, tarball URL,
file:,link:,workspace:) have no published-version concept and aren't age-filtered — they resolve as usual. - If an
overrides/ catalog /patchedDependenciesentry pins an exact version newer than the cutoff, resolution fails (the mirror hides it) — age the pin or raise the cutoff. minimumReleaseAgeExcludeis not honored: everything is aged, no exceptions.- Versions the registry has no publish
timefor are treated as too-new (excluded).
License
MIT
