Skip to content

Stop publishing npm-shrinkwrap.json: ignored by npm 12 (Node 27), and it has broken consumer npm ci twice #2942

Description

@Ethan-Arrowood

Proposal

Stop publishing npm-shrinkwrap.json in the harper package, and move application reproducibility to locked artifacts (see Options). This is the decision #2172 asks for: its "drop the shrinkwrap" option.

Why

Scope

Why harper ships a shrinkwrap at all

harper is both a library and an application. For applications (CLIs, servers) that publish to the registry, the long-standing npm guidance has been to ship an npm-shrinkwrap.json. package-lock.json is never published, so a shrinkwrap is the only way for an installer to get the exact dependency tree the package was tested with. That is what #1622 turned on. The #1780 and #2941 breakages came from the library side of harper: installing it as a dependency and running npm ci.

What npm 12 does instead

npm 12 treats harper like any library. It reads the ranges in harper's package.json, resolves every transitive dependency fresh from the registry at install time, and hoists and dedupes the result into the consumer's tree. Specifically:

How much reproducibility is lost depends on how harper is installed:

  • Installed as a dependency: reproducible per consumer. Their own package-lock.json pins whatever was resolved on the first install, though that is not what harper tested.
  • Installed as an application (npm i -g harper, npx harper, or an image built without a lockfile): not reproducible at all. Every install resolves fresh. This is exactly the case the shrinkwrap existed for.

So removing the shrinkwrap is only half a proposal. The other half is what replaces it for application installs.

npm's recommended replacement: bundleDependencies

npm/cli#9262, the change that dropped shrinkwrap support, names the replacement:

use bundleDependencies if you need to ship a locked dependency tree

bundleDependencies copies the named packages, and their dependency subtrees, from the packing machine's node_modules into the published tarball. The installer then uses those files as they are, with no resolution. That behaves the same on every npm version and for both library and application installs.

For harper, it can't be applied wholesale (bundleDependencies: true):

  • Native prebuilds are chosen per platform at install time. A bundle captures only the packing host's. On a linux-arm64 pack of 5.3.0, 15 native or platform packages would ship for that platform only: @harperfast/rocksdb-js-*, @lmdb/lmdb-*, @harperfast/hnsw-*, @harperfast/fulltext-*, @cbor-extract/*, @msgpackr-extract/*, argon2, bufferutil, utf-8-validate, systeminformation, and their loaders. Every other platform would break.
  • These packages would have to stay regular, exact-pinned dependencies. Only the pure-JS set would be bundled.
  • The bundle list has to be closed: no native package can be reachable through a bundled one, otherwise the host's binary gets bundled anyway. The build would need a check that the packed tarball contains no os/cpu/gypfile package.

Measured cost, npm 12 production install of harper@5.3.0 (600 packages, 389 MiB unpacked):

gzipped
Bundle every dependency 78 MiB
…of which the react-native tree 31 MiB
Bundle after the alasql fix (#2941 step 2) ~47 MiB

The published tarball is 48 MiB unpacked today.

Costs that apply to library consumers specifically:

  • Bundled packages are not deduped against the consumer's tree.
  • Consumers can't npm update or overrides a bundled package, so a CVE in a bundled transitive dependency needs a harper release to fix.
  • Every harper install downloads the full bundle, including for consumers who already have those packages.

Options

  1. Drop the shrinkwrap, and lock the application artifacts separately (recommended).
  2. Replace it with a partial bundleDependencies.
    • Bundle the pure-JS closure, keep native packages as exact-pinned deps, and enforce "no native package in the tarball" at build time.
    • This is the only option that gives a locked tree to npm i -g on every npm version.
    • Cost: roughly 47 MiB gzipped per install once alasql is fixed, no dedupe, and harper releases gate transitive CVE fixes for all consumers.
  3. Keep the shrinkwrap. Not viable. It stops doing anything from Node 27, and until then it is a recurring source of consumer npm ci failures.

I'd go with option 1. The failures we've actually hit came from harper being installed as a library, and bundling makes that case worse (no dedupe, CVE fixes need a harper release) to protect application installs, which already have a better home: a locked image.

Trade-off (option 1)

npm 10 and 11 consumers go back to resolving transitive deps fresh within declared ranges, as npm 12 consumers already do. The react-native tree also comes back for them until alasql is fixed. Both are consistent across npm versions, and neither breaks npm ci.

Measurement: npx npm@12 install --omit=dev --ignore-scripts harper@5.3.0 in node:24 on linux/arm64. The bundle size is node_modules (excluding harper) as a gzipped tar. Native packages were identified by os, cpu, gypfile or an install script in their package.json.

Related: #2172, #2941, #1937, #2179, #2042.

sent with Claude Opus 5.5

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Fields

Priority

P2

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions