Skip to content

Support a stale marker on npm and pnpm dependencies #255

Description

@fniessink

The problem

Every package.json dependency is checked for staleness, and no project can say "yes, this one is old, and we know". The staleness warning is the only check such a dependency gets, and it is the only warning in Update-time that a single reference can neither silence nor put on a threshold of its own.

The README's answer does not answer this: "A package.json dependency takes no marker, because npm and pnpm update it rather than Update-time rewriting its lines. Opt one out by declaring an upper bound or an exact version instead." An upper bound opts the dependency out of updates, which is a different question. A dependency deliberately kept at a version whose project has gone quiet keeps warning at every run, and the only ways to stop it are to raise --stale-after for the whole scan or to disable the check entirely.

Every other reference Update-time warns about takes an ignore[stale] or an ignore[stale<DAYS]. This one cannot, for want of somewhere to write it.

What the file will accept

package.json is strict JSON, so the marker cannot be a comment. Probed 2026-08-15 with the installed npm:

Written into package.json npm's answer
// update-time: ignore[stale] on its own line npm error code EJSONPARSE … package.json must be actual JSON, not just JavaScript
"//": "update-time: ignore[stale]" accepted; npm pkg get reads the rest of the file back
"update-time": { "left-pad": "ignore[stale]" } accepted; npm pkg get update-time returns it and npm install --package-lock-only succeeds

A field it is, then, and it has to name the dependency it applies to, since a package.json declares many.

Scope

  • Decide where a marker for a package.json dependency is written, and which dependency each one names.
  • Honour the stale scope in both its forms: ignore[stale] silences the warning, and ignore[stale<90] (or allow[stale>=90]) puts that dependency on a threshold of its own.
  • Report every other scope as redundant, with the reason each is: update, a bound, and cooldown steer an update npm and pnpm make rather than Update-time, and yanked and vulnerable name checks a range-declared dependency does not get. hash-drift is redundant too, since the integrity hashes live in the lock file.

The design constraint

Every marker Update-time reads today is found by position: on the reference's own line, or on the line above it. A field-based marker is found by name, so the reference has to be identified rather than located. Reading and validating the directive text is unaffected — that is the Marker domain's work and takes a string.

Locating the reference itself is already solved: package_json.dependency_locations() returns each declared dependency with the line of every entry declaring it, which is where the staleness warning is already reported. A name declared in more than one section carries a location per entry, so a marker naming that dependency has to say whether it covers all of them.

Out of scope

  • The Node engine version in the same file, which is a different reference with a different set of checks, covered on its own.
  • Reading package-lock.json or pnpm-lock.yaml. A marker changes what is reported, not what is resolved.
  • The update side of the language. Which versions npm and pnpm may move a dependency to is what the declared range says, and that stays the only lever.

Documentation

  • The per-type "Markers" answer under "npm and pnpm dependencies", which says the dependency takes none and points at the version-specifier workaround.
  • The per-type "Stale dependencies" answer, which would gain the threshold override.
  • "Where to put a marker", which says a dependency updated through uv, npm, or pnpm takes no marker.
  • A changelog entry under [Unreleased].

Open questions

  1. Is the field "update-time", or npm's "//" convention, or a sidecar file that would serve every format that cannot hold a comment?
  2. Does a marker naming a dependency cover every section declaring it — dependencies, devDependencies, and optionalDependencies — or does it have to name the section too?
  3. If the checks for exactly pinned dependencies are ever added, yanked and vulnerable stop being redundant here. Should the marker language be shaped for that now, or reported as redundant until then?
  4. Does npm preserve an unknown field across npm update and npm pkg set? It should, but the marker's home depends on it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions