HarperFast / HarperFast/harper

npm 12 drops npm-shrinkwrap.json support, silently voiding Harper's dependency pinning and react-native prune

Open
#2,172 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
89
Forks
10
Avg merge
2d 6h
Merged PRs (30d)
200

Description

## Summary

npm 12 **removed `npm-shrinkwrap.json` support entirely**. Harper's published tarball still ships one, and npm 12 ignores it silently — no warning, no error. Two guarantees we deliberately built on it stop holding:

- **Version pinning** (#2042) — the reason it exists is that `@harperfast/rocksdb-js` 2.6.0/2.6.1 redefined a store option in a way that breaks every table open.
- **The react-native prune** (#1937) — ~140MB of unreachable code comes back.

npm/cli [#7622](https://github.com/npm/cli/issues/7622) (open since 2024, about OS-constrained optional deps under a shrinkwrapped dependency) was closed 2026-04-21 by [PR #9262](https://github.com/npm/cli/pull/9262):

> **BREAKING CHANGE:** `npm shrinkwrap` is removed, the `shrinkwrap` config alias is removed, and `npm-shrinkwrap.json` is no longer loaded or honored at the project root or from inside dependency tarballs. Rename project-root `npm-shrinkwrap.json` to `package-lock.json`; use `bundleDependencies` if you need to ship a locked dependency tree.

## Measured

`npm install harper@5.2.0`, production only, same host, two npm majors:

| | npm 11.16.0 (shrinkwrap honored) | npm 12.0.2 |
|---|---|---|
| packages | 520 | **740** |
| `@harperfast/rocksdb-js` | **2.5.0** (pinned) | **2.7.1** (fresh resolve of `^2.5.0`) |
| react-native tree | absent | **present** (plus `metro`) |
| platform prebuilds | 8 rocksdb + 7 lmdb | 1 + 1 |
| tree | 416M | **453M** |

The tarball still contains `npm-shrinkwrap.json` under npm 12 — it is simply not read.

## Which half actually matters

Pinning, not size. Under npm 12 a consumer resolves `^2.5.0` past the pinned 2.5.0 to 2.7.1.

To be clear about the risk level: that is **expected to be fine**, because `@harperfast/rocksdb-js` keeps proper semver — anything genuinely breaking would land as a major, outside `^2.5.0`. The concern here is the loss of *reproducibility* in general, not a specific known-bad version, and it applies to every dependency in the tree rather than the native bindings in particular. Recording it because #2042 was motivated by a real 2.6.x incident, so the pinning was bought with something; losing it silently is the part worth tracking.

Ironically npm 12 *fixes* the platform-prebuild bloat in #2164 (1 prebuild instead of 8+7, because a fresh resolve applies `os`/`cpu` filtering) — but the net tree is larger and both guarantees are gone.

## Blast radius today

No current Node release bundles npm 12:

| Node | bundled npm | shrinkwrap honored |
|---|---|---|
| 22.21.1 | 10.9.4 | yes |
| 24.13.0 | 11.6.2 | yes |
| 25.8.1 | 11.11.0 | yes |
| — | **12.0.2** (registry `latest`) | **no** |

So exposure right now is limited to anyone who explicitly runs `npm i -g npm@latest` / `npm@12`, or CI that installs latest npm. The Docker image is unaffected for now because it inherits the `node:24` image's npm 11.

This is a **dated fuse, not a current fire** — it becomes universal the moment Node bumps its bundled npm to 12. Worth deciding before that lands rather than after.

The silence is the part that worries me most: nothing warns that a shipped shrinkwrap is being ignored, so the first symptom would be a consumer resolving a dependency we thought was pinned.

## Options

- **`bundleDependencies`** — npm's stated replacement, and the only mechanism that still ships a locked *tree*. Materially changes the published artifact (deps vendored into the tarball, much larger download, and dedupe across the consumer's tree is lost). Needs costing.
- **Pin exact versions in `package.json`** — replaces range resolution with exact pins for the dependencies where drift actually hurts (native bindings first). Doesn't lock transitives, so it is partial, but it is cheap and works on every npm.
- **Do neither, and drop the shrinkwrap** — accept fresh resolution and rely on ranges. Restores the react-native tree (#1937) and forfeits pinning; probably unacceptable given #2042's motivation.
- **Engine-pin npm < 12** — buys time, doesn't solve it, and Node will make it moot.

Whatever we choose, `build-tools/prune-shrinkwrap-dev.mjs` and `prune-shrinkwrap-react-native.mjs` become no-ops for npm 12 consumers, and `DESIGN.md` / `dependencies.md` both document the shrinkwrap as authoritative — all three need updating.

## Verification notes

npm 12 exercised via `npx npm@12.0.2 install --omit=dev --ignore-scripts harper@5.2.0`. Bundled-npm versions read from local Node installs. The `--ignore-scripts` flag does not affect any of the above: `@harperfast/rocksdb-js` declares no `install`/`preinstall`/`postinstall` hook, and re-running with scripts enabled produced the same counts and sizes.

## References

- npm/cli#7622 — the bug that prompted the removal
- npm/cli#9262 — `feat!: drop npm-shrinkwrap.json support`
- #2042 — Docker install pinning fix (depends on the shrinkwrap)
- #1937 — react-native prune (depends on the shrinkwrap)
- #2043 — react-native subtree in the image
- #2164 — install footprint; its prebuild half is resolved by npm 12

---

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the npm 12 behavior with `npx npm@12.0.2 install --omit=dev --ignore-scripts harper@5.2.0`, then read `build-tools/prune-shrinkwrap-dev.mjs`, `build-tools/prune-shrinkwrap-react-native.mjs`, `DESIGN.md`, and `dependencies.md`. Done means choosing and implementing a supported dependency-pinning strategy, removing obsolete assumptions, and updating the documentation and verification evidence.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
build-system, release
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.