Documentation Index
Fetch the complete documentation index at: https://docs.aspect.build/llms.txt
Use this file to discover all available pages before exploring further.
rules_js models npm package dependency handling on pnpm,
closely mimicking pnpm’s behavior.
When a non-Bazel tool—typically pnpm—performs dependency resolution and solves version constraints, it also determines the node_modules tree structure for runtime. This information is encoded in a lockfile that is checked into the source repository.
rules_js defines npm_import targets for each external package based on the pnpm lockfile, which allows Bazel’s downloader to fetch packages individually.
The lockfile also includes the integrity hash that the package manager calculates, so Bazel can guarantee supply-chain security.
Bazel only fetches packages the requested targets require. You can safely use a large pnpm-lock.yaml file without unnecessary package fetches. In benchmarks with 800+ importers and ~15,000 npm packages, processing completes in about 3 seconds when Bazel detects an input change.
While you can use the npm_import
rule to bring individual packages into Bazel, most users import their entire lockfile using the npm_translate_lock rule, which is described below. For more information, see the npm_translate_lock API documentation.
Rules overview
The primary rules and targets for fetching and linking npm package dependencies are:npm.translate_lock(): Generates targets representing packages from a pnpm lockfile.npm_link_all_packages(): Defines anode_modulestree and the associatednode_modules/{package}targets. This rule is required in the following BUILD files:- Each package in the pnpm workspace that has npm packages linked into a
node_modulesfolder that Bazel uses - The package at the root of the pnpm workspace where the lockfile resides
- Each package in the pnpm workspace that has npm packages linked into a
:node_modules/{package}: Represents each package dependency from apackage.jsonwithin the pnpm project, as generated bynpm_link_all_packages().
js_library() rules can depend on npm packages using the :node_modules/{package} targets that each npm_link_all_packages() generates. The :node_modules/{package} targets follow Node.js resolution rules: a package can depend on node_modules from its own BUILD file and any BUILD file above it.
Ignoring node_modules
You must configure Bazel to ignore pnpmnode_modules directories.
Bazel 8+: Add to REPO.bazel:
verify_node_modules_ignored attribute, which checks that .bazelignore includes all node_modules folders:
npm.npm_translate_lock more than once if your repository contains more than one pnpm lockfile.
Hoisting
When hoisting is disabled,rules_js produces a
node_modules tree that is bug-for-bug compatible with the one pnpm produces.
To match this behavior outside of Bazel, add hoist=false to your .npmrc:
hoist=false prevents pnpm from creating a hidden
node_modules/.pnpm/node_modules folder. Without this folder, packages cannot
depend on undeclared “phantom” dependencies.
With hoisting disabled, most import/require failures in third-party npm packages
are reproducible with pnpm outside of Bazel, whether they occur during
type-checking or at runtime.
rules_js does not support pnpm “phantom” hoisting,
which allows packages to depend on undeclared dependencies. All packages must declare
their dependencies to support lazy fetching and linking of npm dependencies.
For help, see Troubleshooting.
Creating and updating the pnpm-lock.yaml file
update_pnpm_lock
If you’re migrating from another package manager, use theupdate_pnpm_lock
attribute of npm_translate_lock to have Bazel manage the pnpm-lock.yaml file.
You can also use this mode if you want package.json changes to automatically
update the lockfile.
update_pnpm_lock also requires the data attribute, which must include
pnpm-workspace.yaml and all package.json files in the pnpm workspace. The
update fails if data is missing any files that pnpm install --lockfile-only
or pnpm import needs.
Tip: To list all local package.json files that pnpm requires, run:
pnpm recursive ls --depth -1 --porcelain
When pnpm-lock.yaml is out of date, npm_translate_lock automatically runs
one of the following:
pnpm import, if thenpm_package_lockoryarn_lockattribute is specifiedpnpm install --lockfile-only, if neither attribute is specified
pnpm-lock.yaml, use one of the following methods:
- Install pnpm and run
pnpm install --lockfile-onlyorpnpm import - Use the Bazel-managed pnpm by running
bazel run -- @pnpm//:pnpm --dir $PWD install --lockfile-onlyorbazel run -- @pnpm//:pnpm --dir $PWD import
update_pnpm_lock is true, setting the ASPECT_RULES_JS_FROZEN_PNPM_LOCK
environment variable causes the build to fail if pnpm-lock.yaml is out of date.
Set this variable on CI to enforce lockfile hygiene.
Setting ASPECT_RULES_JS_DISABLE_UPDATE_PNPM_LOCK disables update_pnpm_lock
even if it is set to true. This is useful in CI environments where multiple jobs
run Bazel but only one job should verify that pnpm-lock.yaml is up to date.
npm_translate_lock_<hash>
npm_translate_lock creates a
.aspect/rules/external_repository_action_cache/npm_translate_lock_<hash> file
to determine when pnpm-lock.yaml needs updating. This file tracks the state of
package and lock files that affect pnpm-lock.yaml generation. Check this file
into source control alongside pnpm-lock.yaml.
npm_translate_lock_<hash> can cause merge conflicts in workspaces with frequent
lockfile or package.json changes. Because npm_translate_lock generates and
updates this file, manual merge conflict resolution is never necessary.
To minimize disruption to developer workflows, configure git to ignore merge conflicts using .gitattributes and a custom merge driver. For more information, see the Aspect blog post, Easier merges on lockfiles.
First, mark the npm_translate_lock_<hash> file to use a custom merge driver.
Replace <hash> with the hash generated in your workspace. This example uses a
driver named ours:
ours custom merge driver in your git configuration to always
accept local changes:
Working with packages
Patching with pnpm.patchedDependencies
rules_js automatically applies patches listed in
pnpm.patchedDependencies.
Include these patches in the data attribute of npm_translate_lock:
rules_js may behave slightly differently from standard pnpm patching. rules_js uses the bazel-lib patch util instead of pnpm’s internal patching mechanism. For example, a malformed patch file may partially apply when using pnpm outside of Bazel, but fail entirely under rules_js.
Patching with the patches attribute
Use pnpm.patchedDependencies for patching when possible. If you’re importing a yarn or npm lockfile and pnpm.patchedDependencies is not available in your package.json, use the patches and patch_args attributes of npm_translate_lock instead. These attributes behave similarly to the same-named attributes of http_archive.
Paths in patch files must be relative to the root of the package. If you omit the version from the package name, the patch applies to every version of that npm package. patch_args defaults to -p0, but git-generated patches typically require -p1.
If multiple entries in patches match, the patches are additive—rules_js appends more specific matches to previous ones. If multiple entries in patch_args match, the most specific match takes precedence. rules_js applies patches after any patches in pnpm.patchedDependencies.
The following example applies patches to @foo/bar and fum@0.0.1:
Lifecycle hooks
npm packages support lifecycle scripts such aspostinstall, referred to in this document as lifecycle hooks.
The onlyBuiltDependencies setting in pnpm-workspace.yaml defines an explicit list of packages that may run lifecycle hooks.
When a package has lifecycle hooks, the lifecycle_* attributes control which hooks run and how they run. For example, to restrict lifecycle hooks across all packages to only run postinstall:
Becauselifecycle_hooks = { "*": ["postinstall"] }innpm_translate_lock.
rules_js models hook execution as build actions rather than repository rules, the remote cache can store and share results between developers. By default, these actions run outside of Bazel’s action sandbox to avoid the overhead of setting up and tearing down sandboxes.
Bazel also supports other execution_requirements for actions beyond sandboxing. Control these using the lifecycle_hooks_execution_requirements attribute of npm_translate_lock.
If a hook fails to run under rules_js and you don’t need it, use the lifecycle_hooks_exclude attribute of npm_translate_lock to disable it for that package. This is equivalent to setting lifecycle_hooks to an empty list for that package.
Set environment variables for hook build actions using the lifecycle_hooks_envs attribute of npm_translate_lock. If hooks depend on environment variables from use_default_shell_env, enable this for hook build actions using the lifecycle_hooks_use_default_shell_env attribute of npm_translate_lock.
If multiple entries match, attribute behavior depends on the attribute type: some are additive, where rules_js appends more specific matches to previous ones, while others use specificity, where the most specific match takes precedence.
| attribute | behavior |
|---|---|
| lifecycle_hooks | specificity |
| lifecycle_hooks_envs | additive |
| lifecycle_hooks_execution_requirements | specificity |
@kubernetes/client-noderuns only thepreparehook.fum@0.0.1runs onlypostinstall. All other packages run the default hooks.@foo/barhooks run with Bazel’s sandbox enabled, with the following effective environment:GLOBAL_KEY1=value1GLOBAL_KEY2=PREBULT_BINARY=http://downloadurl
fum@0.0.1has remote execution disabled. Like all packages except@foo/bar, the action sandbox is disabled for performance.

