I maintain a small actor that normalizes npm/PyPI/crates.io metadata into one row per dependency, and last week I added a "when was this last touched" column. Then I tried to turn that into "is this thing dead," which felt like it should be the same feature. It is not the same feature, and the three registries don't even agree on what "dead" would look like.
npm has an opinion field. Most packages leave it blank.
npm's deprecated is real — a string on the version object, set explicitly by the maintainer, and it shows up as a scary yellow warning the moment someone runs npm install:
curl -s https://registry.npmjs.org/istanbul | python3 -c "
import json, sys
d = json.load(sys.stdin)
v = d['dist-tags']['latest']
print(d['versions'][v].get('deprecated'))
"
# "This module is no longer maintained, try this instead:\nnpm i nyc\n..."
jade (renamed to pug) and gulp-util (replaced piecemeal) both set it too, both with a last publish back in 2015–2016. That's the field working as intended: opt-in, honest, and visible at install time.
Then there's bower. Last published March 2022, its own README calling it unmaintained — and the deprecated field is just... absent. Nobody who could set it ever did. The flag only fires if someone bothers to pull the trigger, and plenty of genuinely dead packages never get that courtesy.
The weirder case is colors, of 2022-supply-chain-incident fame. The sabotage versions (1.4.1, 1.4.2) are sitting right there in the registry's own time history — but dist-tags.latest was rolled back to 1.4.0 from 2019 and never moved. A plain npm install colors today quietly gets you the pre-incident version. The bad versions still exist, still installable if you pin them by number, just no longer the default anyone lands on by accident.
PyPI has the data. It just never revisits it.
PyPI ships a Development Status classifier — 1 - Planning through 7 - Inactive — that sounds exactly like what I wanted:
curl -s https://pypi.org/pypi/nose/json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d['info']['version'], [c for c in d['info']['classifiers'] if 'Development Status' in c])
"
# 1.3.7 ['Development Status :: 5 - Production/Stable']
nose hasn't shipped since June 2015 — eleven years, superseded by pytest for most of that time — and PyPI still calls it "Production/Stable." I checked requests, which released a new version this year, expecting a different string as a sanity check. Same one, word for word: 5 - Production/Stable. The classifier is text a maintainer typed once, at whatever release they felt like typing it at, and PyPI has no mechanism that ever revisits it. An actively-shipping project and an eleven-year corpse are indistinguishable on this specific field.
crates.io has neither field — but the timestamp underneath is the most honest of the three
The sparse index — the flat file cargo itself reads on every resolve, and the only crates.io endpoint my sandbox could reach this week — carries no status concept at all:
curl -s https://index.crates.io/ru/st/rustc-serialize | tail -1 | python3 -m json.tool
# keys: name, vers, deps, cksum, features, yanked, pubtime
No deprecated, no classifier, not even a homepage to go check by hand. What it does have is pubtime — and I almost got this part wrong. rustc-serialize 0.3.25, a crate long superseded by serde, shows pubtime: 2023-12-01. My first instinct was "that's obviously a backfill artifact, crates.io added this field at some point and stamped old versions with the migration date." That's a clean, confident, wrong guess: crates.io's own documentation says pubtime is backfilled without gaps but always holds the original publish time, and a second check confirmed 0.3.25 really was a genuine bugfix release, shipped eight years after the crate's heyday, in December 2023. The field was right. I wasn't.
So crates.io gives you the cleanest possible timestamp — precise, guaranteed present, verifiably not a backfill placeholder — and zero opinion about whether that timestamp is fine or alarming. You get better data and a harder job: you have to decide "is this stale" yourself, every time, with no maintainer's flag to lean on either way.
Where I'd push back on myself
Nine examples is a vibe check, not a survey — I picked packages I already knew the status of, which is exactly the sampling bias that makes a finding look cleaner than it is. I don't know what fraction of genuinely abandoned npm packages carry a deprecated flag versus staying silent like bower; that would take a much larger, unbiased sample I don't have the reachable endpoints to run this week. And "PyPI's classifier never changes" is what I observed on two packages — plausible given how the field works, but I haven't fetched enough dead PyPI projects to call it a rule rather than a pattern.
This staleness check is what I'm trying to fold into Package Registry Scraper next — a normalized "last touched" + "explicitly flagged" column instead of three registry-specific quirks to remember. The full field-by-field breakdown, with more examples, is in the cheatsheet: noble-ronin/package-staleness-data.
So: when you're deciding whether to add a dependency, do you actually check any of this, or do you go by download counts and vibes like most of us? And has a registry's own "stable" label ever actively misled you about something that turned out to be abandoned?

Top comments (1)