A published npm package (a library consumed by other projects) does not commit a lockfile, while the internal web application that depends on it does commit one. Why does this convention differ between libraries and applications?
answer
- consumer's lockfile wins, library's is invisible to installers
- app = end of line, needs one frozen answer
- library range = public compatibility contract
- narrow pins in a library break consumer dedup
basics
~20 sAn app is the final thing that actually runs, so it needs one fixed, tested set of dependency versions locked down. A library gets installed inside many different apps' own dependency trees, so locking its own versions would fight with -- and often duplicate -- whatever versions the consuming app already resolved.
solid answer
~40 sApplications are deployment endpoints: they run as-is, so they need a lockfile to freeze the exact, tested dependency graph that goes to production. Libraries are inputs to someone else's resolution: when installed, their dependencies get merged into the consuming project's own dependency tree, and the consuming application's lockfile is what actually governs versions at runtime. A library's own lockfile is irrelevant to how it's consumed -- package managers ignore a dependency's lockfile during installation -- so committing one for consumer benefit gives false confidence and can encourage overly-narrow ranges that hurt deduplication for consumers. Libraries may still use a lockfile locally for reproducible CI/test runs, but what actually matters for a library's compatibility contract is well-chosen semver ranges in its own manifest.
go deeper
Knows that apps commit lockfiles and libraries generally don't, as a convention to follow, without necessarily explaining the resolution mechanics behind it.
Can explain that a consumer's package manager resolves the library's ranges within its own tree and that the library's own lockfile isn't consulted.
Reasons about the deduplication and conflict consequences of narrow library ranges/pins for consumers, and can diagnose duplicate-instance bugs caused by getting this convention backwards.
Sets publishing policy for an org's internal package registry (peer-dependency range width, lockfile exclusion from published tarballs, CI matrix testing against a support range) balancing consumer compatibility against maintenance cost.
## An application is the end of the line The key distinction is between what 'installing' a package means for an application versus for a library, and who actually controls dependency resolution at runtime. When an application is deployed, its own lockfile is the one and only source of truth for every version in its dependency tree, direct and transitive -- the application's build/deploy pipeline runs a strict install against that lockfile and that's what ships to production. There is no 'someone else' downstream consuming the application's dependency tree; the application is the end of the line. That makes committing its lockfile unambiguously correct: it's the artifact that guarantees the exact bytes tested in CI are the exact bytes running in production. ## A library is an input to somebody else's resolution A library is different because it is itself a dependency of other projects, and when a consumer installs that library, the consumer's package manager resolves the library's own dependencies (as declared in the library's manifest, via semver ranges) within the consumer's overall tree -- the library's own lockfile, even if one exists in its source repo, plays no role in that resolution at all. Package managers don't read or honor a dependency's lockfile when installing it as a sub-dependency; they only look at its manifest to see what ranges it declares, then resolve those ranges against everything else already in the consumer's tree, ideally deduplicating shared dependencies onto single shared copies. So a library's lockfile is only ever useful for the library's own local development and CI -- reproducible test runs of the library itself -- and has zero effect on what versions actually ship inside a consuming application. ## What that means for how a library specifies versions This has direct practical consequences for how libraries should specify dependencies. Because consumers merge the library's stated ranges into their own tree, a library that specifies its dependencies with unnecessarily narrow or exact-pinned versions actively hurts consumers: - **It prevents deduplication** -- an exact pin can't be satisfied by whatever version the rest of the consumer's tree already resolved to, forcing a duplicate nested install. - **It can create outright unresolvable conflicts for peer dependencies** (e.g. a UI library hard-pinning one major version of a framework makes it uninstallable in a consumer already on a newer major). The convention, then, is: | | Ranges in the manifest | The lockfile | |---|---|---| | **Libraries** | declare deliberately permissive, well-considered semver ranges (this is their public compatibility contract with the ecosystem) | generally treat their lockfile as a dev-only artifact | | **Applications** | declare ranges too | but additionally commit a lockfile because they need a single frozen answer for their specific deployment, not a public compatibility contract for others to build on | ## The mistake to avoid The trade-off engineers sometimes get wrong: committing a library's lockfile isn't harmful for the library's own repo hygiene (reproducible local dev/CI for the library's maintainers is genuinely useful), the mistake is assuming it does anything for consumers or treating its presence as equivalent to an application's lockfile guarantee. Some ecosystems reinforce this at the tooling level -- publishing configuration typically excludes the lockfile from what's packed into the published tarball, so even if you commit it to source control for your own CI, it typically never even reaches consumers' install step in the first place. ## When the convention is reversed A concrete production failure mode from getting this backwards: a team publishes an internal library with an accidentally over-narrow exact-pinned dependency (say, a specific patch of a validation library), and every consuming application ends up with a duplicated, slightly different copy of that validation library nested inside the library's own installed folder, sometimes producing subtle bugs when the app's own top-level copy and the library's nested copy disagree on behavior for the same input (a classic case is two copies of a schema-validation or state-management library failing instanceof checks against each other because they're literally different module instances). The fix is almost always loosening the library's declared range and letting the consuming applications' own lockfiles govern the final resolved versions.
- If a library's own lockfile has zero effect on a consumer's install, why would the library maintainers keep one in their repo at all?It still gives the library's own CI and local development reproducible builds and tests -- the maintainers want deterministic results when running the library's own test suite, independent of what version ranges its own dependencies float within. It's purely for the library's internal development loop, not a promise to consumers.
- What specifically prevents a consumer's package manager from reading and honoring a dependency's committed lockfile during install?Package managers resolve the entire tree from manifests holistically so they can dedupe shared dependencies across all dependents; if they instead honored each nested package's own lockfile independently, you'd lose the ability to dedupe and could get conflicting frozen sub-trees that don't compose, so the tooling simply never reads nested lockfiles at all.
- How should a library maintainer decide how wide to make a semver range for a peer dependency like a UI framework?As wide as the library is genuinely tested and compatible with, since narrower ranges needlessly exclude consumers on adjacent versions and can force duplicate installs or hard conflicts; the range should reflect real compatibility testing, such as a CI matrix against multiple major versions of the peer, not just whatever version the maintainer happened to develop against.
An application's lockfile is like a chef's final, exact printed menu for tonight's service -- fixed, tested, going out the door as-is. A library's manifest is more like a recipe card handed to other kitchens: it should say 'any ripe tomato' (a range) so each kitchen can use whatever tomatoes are already in their own fridge, rather than demanding one specific tomato that forces them to buy a duplicate.
saying these in an interview costs you the question
- Thinks a library's committed lockfile constrains what versions a consuming app installs
- Recommends exact-pinning a library's dependencies without mentioning the deduplication/consumer cost
- Can't explain why package managers don't read nested/sub-dependency lockfiles
- Conflates 'reproducible builds for the library's own CI' with 'reproducible builds for consumers'