A little Node.js dependency installer for module end-users.
dep aims to match npm's interface for the features it covers, while keeping the internals small: it ships with zero runtime dependencies.
- Install the dependencies defined in a local package.json.
- Lock the dependencies installed in a local node_modules.
- Run an arbitrary command from the scripts in a local package.json.
- Workspaces in a monorepo, installed and locked together.
- Design principles
- Installation
- Usage
- Registry auth
- Commands
- Workspaces
- npm compatibility
- Why dep has no cache
- Stability
- Contributing
- License
A thin client. dep resolves a dependency tree, downloads it, verifies it, and links it — nothing else. Less code and less state mean fewer failure modes.
npm-compatible where it counts. Same package.json, same package-lock.json
(v3), same node_modules layout. Try dep on an existing project today and
switch back tomorrow; nothing to migrate either way.
Zero runtime dependencies. Everything is built on the Node.js standard library, so the installer can't be compromised through its own dependency tree — and the whole codebase is small enough to audit in an afternoon.
Caching belongs to infrastructure. dep keeps no local package cache: a caching registry proxy (Verdaccio, Nexus, Artifactory) serves the whole team with standard HTTP semantics, instead of a mutable store duplicated on every machine. See Why dep has no cache for the rationale and a two-minute proxy setup.
Built for end-users, not library authors. dep is for people who build
things with node_modules, not people who publish packages. It installs,
locks, and runs — publishing, versioning, and auditing belong to other tools.
These principles decide what's in and what's out — see the npm compatibility table for the honest consequences.
dep requires Node.js >=20.19.0.
$ npm install --global depTo uninstall:
$ npm uninstall --global dep$ dep -h
A little Node.js dependency installer
Usage: dep <command> [options]
Commands:
install, i Install dependencies defined in package.json
lock, l Lock dependencies installed in node_modules
run, r Run an arbitrary command from scripts in package.json
Options:
--save Save to dependencies (--save=dev for devDependencies)
--save-dev Save to devDependencies
--only=prod|dev Install only prod or dev dependencies
-w, --workspace <name> Add the package(s) to the named workspace(s)
-h, --help Show help
-v, --version Show version informationdep reads npm's standard registry credentials from .npmrc (a project-local
.npmrc overrides ~/.npmrc) — no login command needed:
$ echo 'registry=https://npm-proxy.internal.example.com/' >> ~/.npmrc
$ echo '//npm-proxy.internal.example.com/:_authToken=${NPM_TOKEN}' >> ~/.npmrcDetails — header formats, env expansion, token scoping, and why there is no login or scoped-registry support
//host/:_authToken=<token> sends Authorization: Bearer to that registry
(the legacy //host/:_auth=<base64> sends Basic). ${NPM_TOKEN} is expanded
from the environment at run time, so the token itself never has to be written
to disk — set the variable in your shell or CI secrets. Credentials are
matched by host (and path prefix) and are never sent to any other host, no
matter where a lockfile's resolved URLs point.
Scoped registries (@scope:registry=...) are intentionally unsupported: dep
talks to one registry, and routing scopes to different upstreams is the
proxy's job — Verdaccio, Nexus, and Artifactory all route @company/* to an
internal registry through their uplink config, so the client never needs to.
It's the same reasoning as Why dep has no cache:
push cross-cutting concerns to the layer that owns them.
dep install [pkg][@spec] [--save[=dev|prod]] [--only=dev|prod]
Install all the dependencies defined in a local package.json.
Install a specific package, just like npm install:
$ dep install webpackInstall a package and save it to your package.json. --save (or --save=prod)
writes to dependencies; --save=dev or --save-dev writes to
devDependencies. When no version is given, the resolved version is saved with
the ^ prefix.
$ dep install webpack --save
$ dep install webpack --save-devInstall only dependencies or only devDependencies:
$ dep install --only=proddep lock — write an npm-compatible package-lock.json
Resolve the dependencies defined in a local package.json and write a
package-lock.json that follows the spec of npm's package-lock.json
(lockfileVersion: 3). The lockfile records each package's resolved version,
resolved URL, and integrity, so npm can read it as well.
$ dep lockdep run [script] [-- args] — run or list package.json scripts
Look up <script> in the scripts field of the local package.json and execute
its value:
$ dep run testPass additional arguments after --:
$ dep run build -- dist/bundle.jsWithout a script name, dep run lists every script defined in the local
package.json:
$ dep run
Available scripts via `dep run`
dep run build:
webpack src/index.js
dep run test:
node --test "test/*.js"dep supports monorepos through the npm-style workspaces field in the root
package.json (an array of globs, or { "packages": [...] }).
{
"name": "monorepo",
"private": true,
"workspaces": ["packages/*"]
}Workspace commands — hoisted root install, -w targeting, workspace-aware lock
Running install at the root resolves and hoists every workspace's
dependencies into the root node_modules, and symlinks each workspace
package into node_modules (with its bins) so cross-workspace imports resolve.
Add a package to a specific workspace. The workspace is matched by its name or
by its path relative to the root, and the package is saved to that workspace's
package.json. Use --save-dev to save it to devDependencies, and repeat -w
to target several workspaces.
$ dep install lodash -w @scope/a
$ dep install tap -w packages/b --save-devdep lock records workspaces in package-lock.json the npm way: a source entry
at each workspace's path plus a link entry under node_modules. Pass
-w <workspace> to narrow the lockfile to the given workspace(s).
$ dep lock
$ dep lock -w @scope/adep deliberately implements a focused subset of npm. The table below is the honest state of each feature — what works, what works partially, and what the design principles place out of scope.
Feature-by-feature table — 15 supported, 1 partial, 3 by design
| Feature | Status | Notes |
|---|---|---|
dependencies |
✅ Supported | Resolved (deterministic, hoisted) and installed. |
devDependencies |
✅ Supported | Root package only. Filter with --only, save with --save-dev. |
optionalDependencies |
✅ Supported | Followed (including transitively); skipped without failing the install when they can't be resolved, fail to build, or don't match the current os/cpu. The lockfile keeps every platform's optionals, marked optional. |
peerDependencies |
✅ Supported | Non-optional peers are auto-installed and hoisted (npm v7+ style); peers marked optional in peerDependenciesMeta are skipped. Recorded in package-lock.json. |
workspaces |
✅ Supported | Globs (and { "packages": [...] }). Hoisted install + symlinks, npm-style lockfile entries, and -w, --workspace. |
package-lock.json (v3) |
✅ Supported | dep lock emits an npm-compatible lockfileVersion: 3, and a plain dep install reproduces it (skipping registry resolution). A stale lock, installing a specific package, -w, or --only falls back to a fresh resolve. |
bin |
✅ Supported | Symlinks on POSIX; .cmd/.ps1/sh shims on Windows. |
| Lifecycle scripts | ✅ Supported | Dependencies run preinstall/install/postinstall. The local project and each workspace run the full npm install sequence: preinstall → (deps) → install → postinstall → prepublish → prepare. |
engines / os / cpu |
✅ Supported | On install, a required dep whose os/cpu doesn't match fails (optional ones are skipped); engines.node mismatches warn, or fail under engine-strict. The lockfile stays cross-platform. |
| Integrity verification | ✅ Supported | Registry tarballs are hashed and checked against integrity (SRI sha512) or the legacy shasum before extraction; a mismatch fails the install. |
bundledDependencies |
✅ Supported | A package's bundled deps ship inside its tarball, so they aren't re-fetched or hoisted out. |
| Dependency sources | ✅ Supported | Registry ranges/tags, git URLs (git+https, with #commit/#semver:), remote tarball URLs, and local file/directory paths. |
overrides |
✅ Supported | Global ({ "foo": "1.2.3" }), parent-scoped nesting ({ "parent": { "child": "1" } }), and $-references. Version-qualified targets ("foo@2") are ignored. |
Aliased specifiers (pkg@npm:other) |
✅ Supported | Installs the target package under the alias name; the lockfile records the real name. (Registry targets only.) |
| Private / authed registries | ✅ Supported | Registry-level token auth via standard .npmrc: //host/:_authToken=<token> (Bearer) or the legacy //host/:_auth (Basic), with ${NPM_TOKEN}-style env expansion — see Registry auth. No login command, no scoped registries. |
.npmrc config |
🟡 Partial | Reads registry, save-prefix, engine-strict, and the auth keys above; a project-local .npmrc overrides ~/.npmrc. No scoped registries or most other config keys. |
| Local package cache | ➖ By design | Caching belongs to infrastructure: point dep at a caching proxy (Verdaccio, Nexus, Artifactory) via registry in ~/.npmrc. See Why dep has no cache. |
npm-shrinkwrap.json |
➖ By design | One lock format, npm's current one: dep reads and writes package-lock.json (v3) exclusively and ignores any npm-shrinkwrap.json. |
| Commands beyond install/lock/run | ➖ By design | Follows from the end-user scope: no publish, version, audit, fund, outdated, exec/npx, and no ci, update, uninstall, dedupe, etc. |
✅ Supported · 🟡 Partial · ➖ By design — a consequence of the principles
Every Node.js package manager ships a cache. npm has ~/.npm and the cacache
content-addressable store. yarn has its cache directory and, in Berry, a whole
zip-based offline mirror. pnpm's global store is the centerpiece of its design.
The assumption is so universal that it reads like a requirement: a package
manager is a downloader plus a cache.
dep has no cache. Not "no cache yet" — no cache on purpose. Every install
downloads every tarball from the registry, verifies it against the lockfile,
and extracts it into node_modules. That's the whole lifecycle.
This sounds like a limitation. This section makes the case that it's a feature, and that the cache you actually want belongs somewhere else entirely.
Read the full rationale — what client caches cost, the trade-offs, and a two-minute proxy recipe
npm cache clean --force is a meme for a reason. It's the
"turn it off and on again" of the Node ecosystem — the first reply under every
EINTEGRITY error, every sha512 integrity checksum failed... but expected,
every mysteriously half-extracted package. The command even makes you say
--force, npm's own acknowledgment that you shouldn't need to do this, and
yet everyone eventually does.
That's not an npm-specific failure. It's what a client-side cache is: a long-lived, mutable, concurrently-written database sitting on thousands of machines, maintained by a tool whose primary job is something else. The failure modes are structural:
- Corruption. An interrupted download, a full disk, or a crashed process leaves a truncated tarball behind. The next install trusts the cache and fails with an integrity error that looks like a registry problem — the wrongness surfaces far from its cause.
- Concurrency. Two package-manager processes writing the same store need locking, and locking across processes on three operating systems is where bugs live. Every mainstream package manager has shipped, and fixed, races in its cache layer.
- Permissions. One
sudo npm installand parts of your cache are owned by root. Everything works until it doesn't, days later, in a different project. - Complexity in the client. cacache alone — the thing that makes npm's cache safe(ish) — is a nontrivial content-addressable store with SRI indexing, garbage collection, and its own dependency tree. That machinery has to be shipped, versioned, and debugged inside every client install.
- Duplication. The same
reacttarball sits in the cache of every laptop on your team and gets rebuilt into every CI runner. In CI it's often worse than useless: uploading and restoring hundreds of megabytes throughactions/cache, with hand-rolled cache keys, regularly costs more time than the downloads it was supposed to save.
None of this means caches are bad. It means a per-machine cache inside the client is an expensive place to put one.
Here's the thing about npm packages: a published version is immutable.
lodash@4.17.21 will never change. This is the single easiest caching
problem in computing — cache forever, never invalidate — and we've had
boring, battle-tested software for exactly that problem for decades: HTTP
proxies.
A caching registry proxy — Verdaccio, Nexus, Artifactory — gives you everything the client-side cache promised, in a better place:
- Shared, not duplicated. The first person to install a package pays the registry round-trip; everyone else on the team, and every CI job, gets it from the local network. A per-machine cache can never do this.
- Standard semantics. It's HTTP. You can observe it, size it, and reason about it with tools you already know, instead of spelunking through a package manager's store format.
- Offline resilience where it matters. When the public registry has an outage, your CI keeps working as long as the proxy has the tarballs. That's the offline scenario that actually costs money — not the laptop on a plane.
- One cache, owned by the layer that owns caching. Your proxy doesn't also resolve semver or run lifecycle scripts. Separation of concerns cuts both ways.
And here's the part that makes this less radical than it sounds: most companies already run one. If your team uses Nexus or Artifactory — for private packages, for compliance, for audit trails — then every client-side cache in your fleet is a second cache layered on top of the one you already operate. For that environment, dep's model isn't a workaround; it's the shape the system already had.
The obvious objection: "you didn't remove the complexity, you just moved it." Yes — deliberately. Moved, not duplicated: one cache per team instead of one per machine, run by software whose entire job is caching, instead of being a side quest inside an installer. "Move the hard part to the layer that owns it" is the whole argument.
Deleting the cache isn't just deleting code. It changes what the client can be.
Zero runtime dependencies. dep is built entirely on the Node.js standard library. No cacache, no tar chain, no store implementation — and therefore no way to compromise the installer through its own dependency tree. In a year when npm supply-chain attacks are a recurring news item, the installer being small enough to audit in an afternoon is not a vanity metric.
No state to corrupt. dep's writable state is node_modules and
package-lock.json. Both are visible, versionable, and disposable.
rm -rf node_modules && dep install is a complete reset — there is no
hidden store that can stay poisoned. There is no dep equivalent of
npm cache clean --force, because there is nothing to clean.
Verification on every install. Every tarball is hashed off the wire and
checked against the lockfile's integrity before extraction, every time.
There is no "trusted because it was cached" path, so there is no cache-
poisoning path either.
Predictability. Every install does the same thing. No warm-versus-cold behavioral differences, no "works after clearing the cache" class of bug report. When something fails, the failure is about the network, the registry, or the lockfile — things you can see.
This design has real costs, and they belong here in the README rather than in a footnote.
Cold installs on a laptop without a proxy re-download everything. If you create scratch projects all day on a hotel wifi connection, dep will feel slower than a warm pnpm, full stop. dep is fast per install — it benchmarks ahead of npm, yarn, and pnpm on cold installs with the network in the picture — but it re-pays the network cost that a local cache would amortize.
There is no offline mode out of the box. No network, no proxy, no install. pnpm can install a known project on a plane; dep cannot.
pnpm's store also deduplicates disk. Content-addressable storage plus hard links means twenty projects share one copy of each package on disk. That's a genuine benefit dep does not have and will not grow — disk-level dedup is a store feature, and dep doesn't have a store.
Most solo developers don't run a registry proxy. True. If you're solo, on a metered connection, installing frequently, and unwilling to run one container — npm or pnpm will serve you better, and that's fine. dep is opinionated about where caching belongs, and if that layer doesn't exist in your setup, you're outside the opinion. The recipe below is for everyone who's willing to spend two minutes changing that.
Verdaccio proxies the public registry and caches every tarball it serves. One file:
# docker-compose.yml
services:
verdaccio:
image: verdaccio/verdaccio:6
ports:
- "4873:4873"
volumes:
- verdaccio-storage:/verdaccio/storage
volumes:
verdaccio-storage:$ docker compose up -dThen point your client at it. dep reads the standard registry key from
~/.npmrc, so this one line configures dep — and npm, yarn, and pnpm —
at the same time:
$ echo 'registry=http://localhost:4873/' >> ~/.npmrcIf your proxy requires a token (Nexus and Artifactory usually do), add npm's
standard auth line next to it — dep expands ${NPM_TOKEN} from the
environment:
$ echo '//npm-proxy.internal.example.com/:_authToken=${NPM_TOKEN}' >> ~/.npmrcThat's the entire migration. The first install populates the proxy; every
install after that is served from your own disk (or your team's server, if
you run it on the network instead of localhost) — with standard HTTP
behavior you can inspect at http://localhost:4873.
In CI, the same idea pays off most when the proxy persists across jobs: run Verdaccio (or use your existing Nexus/Artifactory) as a long-lived service on your network or alongside your self-hosted runners, and set the registry line in the job:
steps:
- run: |
echo 'registry=https://npm-proxy.internal.example.com/' >> ~/.npmrc
echo '//npm-proxy.internal.example.com/:_authToken=${NPM_TOKEN}' >> ~/.npmrc
- run: npx dep install --only=prod
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}Every job then installs at LAN speed with zero cache upload/download/restore steps and zero cache keys to bust. (A per-job ephemeral Verdaccio container starts empty each run, so it only helps jobs that install more than once — prefer the persistent proxy.)
dep resolves a dependency tree, downloads it, verifies it, and links it. That's the whole job, so that's the whole program.
Caching is real work, but it's cross-cutting work — it serves every client on the network equally, it has its own operational lifecycle, and it's been solved well by dedicated software for longer than npm has existed. Pulling it into every client multiplies the state, the code, and the failure modes by the number of machines you own. Pushing it to the layer that owns it leaves the client small enough to read, to audit, and to trust.
dep has no cache for the same reason your text editor has no filesystem: someone else already does that, better, one layer down.
Stability is a core value, and a small feature set makes keeping the green
badges easier. dep resolves the dependency tree deterministically, with bounded
concurrency to avoid exhausting sockets and file handles: downloads/extractions
run 16-wide and metadata resolution 64-wide by default (tunable via the
DEP_CONCURRENCY and DEP_RESOLVE_CONCURRENCY environment variables; an
explicit DEP_CONCURRENCY throttles both).
See CONTRIBUTING.md for more info.
