node_modules¶
Creates a hermetic node_modules directory in the Bazel sandbox holding exactly
the packages named and their transitive dependencies.
Most workspaces declare few of these by hand. ts_test builds its own from
deps (see ts_test), Gazelle writes the node_modules a
generated vite_bundler or next_dev_server needs, and a ts_compile target
needs none at all: it reaches npm declarations through depsets. Declare one by
hand for what Gazelle cannot infer — a program or tool that needs packages on
disk at runtime, and a ts_test whose tree is not the one its deps describe.
Usage¶
load("@rules_typescript//npm:defs.bzl", "node_modules")
node_modules(
name = "node_modules",
deps = ["@npm//:vite"],
)
ts_dev_server(
name = "dev",
entry_point = ":app",
node_modules = ":node_modules",
)
One tree can serve several targets in the same package, keeping one copy in the sandbox.
Attributes¶
| Attribute | Type | Default | Description |
|---|---|---|---|
deps |
label_list |
required | npm package targets from @npm to include in node_modules |
On a framework root Gazelle generates deps from the framework's own
requirements and recomputes it every run. A package no import implies —
sharp, which next/image loads at runtime — needs a # keep on its line to
survive. See
Attributes Gazelle owns.
The Layout¶
One npm name can resolve more than once inside a single closure, and pnpm records each resolution separately. The tree is flat where flat is unambiguous and keyed by resolution where it is not:
node_modules/
minimatch/ ← the primary resolution, as files
.pnpm/minimatch@9.0.9/node_modules/minimatch/ ← any other one, files once
glob/node_modules/minimatch ← relative link, → the store above
A resolution is name, version and peer set. pnpm resolves a package once per
distinct set of peers and keys the outcomes apart —
fdir@6.5.0(picomatch@4.0.3) beside fdir@6.5.0(picomatch@4.0.7) — because
they share a tarball and have different dependency edges. Two of those in one
closure get two store entries, distinguished by a peer component after the
version:
- Primary is the resolution the tree's own
depsdeclare. Where they declare none it is the highest version present, the same rule@npm//:<name>follows, and among peer variants of that version the one pnpm left un-suffixed, or the lowest-sorting peer set if every variant carries one. It keeps the top-level directory Node's walk-up finds. - Every other resolution gets its bytes exactly once under
.pnpm/<name>@<version>[_<peer set>]/node_modules/<name>, using pnpm's own encoding for a scoped name (.pnpm/@scope+name@1.2.3/node_modules/@scope/name). The peer component is a readable prefix plus a digest of the whole peer set: a nested peer set can run to hundreds of characters, and truncation alone would collide two resolutions into one directory. - A link is emitted only for an edge that disagrees with the primary, at
<dependent>/node_modules/<name>, pointing at the store copy with a relative target. Cost scales with the disagreeing edges. Links chain: a store copy's own disagreeing dep gets a link inside it.
The links are relative and internal to the tree, so they survive everywhere the
tree goes: as an input to another action, in a test's runfiles, and under
bazel run.
Two resolutions of one name in deps¶
Declaring two resolutions of a name directly on one node_modules target is an
error:
node_modules: @@//src/app:node_modules depends on two versions of 'minimatch' at once:
minimatch@10.2.4
minimatch@9.0.9
node_modules/minimatch is one directory and Node resolves the name to it, so a
tree cannot present both as the answer to `import "minimatch"`.
Did you mean to depend on one of them here and let the other arrive through the
package that needs it? A version reached transitively keeps its own version.
Otherwise split the two into separate node_modules targets.
Two peer resolutions of one version is the same error, one level narrower:
node_modules: @@//src/app:node_modules depends on two resolutions of
'fdir@6.5.0' at once, one per peer set:
peers picomatch_4_0_3_<digest>
peers picomatch_4_0_7_<digest>
The tarball is the same either way; what differs is what the package's own
dependencies resolve to, and node_modules/fdir/node_modules can hold one
answer.
Did you mean to depend on one of them here and let the other arrive through the
package that needs it? A resolution reached transitively keeps its own peers.
Otherwise split the two into separate node_modules targets.
The transitive arrival both messages point at is the case the layout above handles.
Trees ts_test generates¶
ts_test generates a per-target tree from its deps through this same builder,
so a test gets the same layout with nothing to declare. See
ts_test.