This is the most common error rules_js users encounter.
These problems generally stem from a runtime require
call of some library which was not declared as a dependency.
Fortunately, these problems are not unique to Bazel.
As described in our documentation,
rules_js should behave the same way pnpm
does with hoist=false
.
These problems are also reproducible under Yarn PnP because it also relies on correct dependencies.
The Node.js documentation describes the algorithm used: https://nodejs.org/api/modules.html#loading-from-node_modules-folders
Since the resolution starts from the callsite, the remedy depends on where the require
statement appears.
This is the case when you write a config.js
file and pass it to a tool.
This is the "ideal" way for JavaScript libraries to be configured, because it allows an easy "symmetry" where you
require
a library and declare your dependency on it in the same place.
In this case you should add the runtime dependency to your BUILD file where the config.js
is a source.
For example,
js_library(
name = "requires_foo",
srcs = ["config.js"], # contains "require('foo')"
data = [":node_modules/foo"], # satisfies that require
)
This case itself breaks down into three possible remedies, depending on whether you can move the require to your own code, the missing dependency can be considered a "bug", or the third-party package uses the "plugin pattern" to discover its plugins dynamically at runtime based on finding them based on a string you provided.
This is the most principled solution. In many cases, a library that accepts the name of a package as
a string will also accept it as an object, so you can refactor config: ['some-package']
to
config: [require('some-package')]
. You may need to change from json or yaml config to a JavaScript
config file to allow the require
syntax.
Once you've done this, it's handled like the "require appears in your code" case above.
For example, the
documentation for the postcss-loader for Webpack
suggests that you npm install --save-dev sugarss
and then pass the string "sugarss" to the options.postcssOptions.parser
property of the loader.
However this violates symmetry and would require workarounds listed below.
You can simply pass require("sugarss")
instead of the bare string, then include the sugarss
package in the data
(runtime dependencies) of your webpack.config.js
.
This is the case when a package has a require
statement in its runtime code for some package, but
it doesn't list that package in its package.json
, or lists it only as a devDependency
.
pnpm and Yarn PnP will hit the same bug. Conveniently, there's already a shared database used by both projects to list these, along with the missing dependency edge: https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-extensions/sources/index.ts
We should use this database under Bazel as well. Follow aspect-build#1215.
The recommended fix for both pnpm and rules_js is to use
pnpm.packageExtensions
in your package.json
to add the missing dependencies
or peerDependencies
.
Example,
Make sure you pnpm install after changing
package.json
, as rules_js only reads thepnpm-lock.yaml
file to gather dependency information. See Fetch third-party packages
Sometimes the package intentionally doesn't list dependencies, because it discovers them at runtime.
This is used for tools that locate their "plugins"; eslint
and prettier
are common typical examples.
The solution is based on pnpm's public-hoist-pattern.
Use the public_hoist_packages
attribute of npm_translate_lock
.
The documentation says the value provided to each element in the map is:
a list of Bazel packages in which to hoist the package to the top-level of the node_modules tree
To make plugins work, you should have the Bazel package containing the pnpm workspace root (the folder containing pnpm-lock.yaml
) in this list.
This ensures that the tool in the pnpm virtual store node_modules/.aspect_rules_js
will be able to locate the plugins.
If your lockfile is in the root of the Bazel workspace, this value should be an empty string: ""
.
If the lockfile is in some/subpkg/pnpm-lock.yaml
then "some/subpkg"
should appear in the list.
For example:
WORKSPACE
npm_translate_lock(
...
public_hoist_packages = {
"eslint-config-react-app": [""],
},
)
Note that public_hoist_packages
affects the layout of the node_modules
tree, but you still need
to depend on that hoisted package, e.g. with deps = [":node_modules/hoisted_pkg"]
. Continuing the example:
BUILD
eslint_bin.eslint_test(
...
data = [
...
"//:node_modules/eslint-config-react-app",
],
)
NB: We plan to add support for the
.npmrc
public-hoist-pattern
setting torules_js
in a future release. For now, you must emulate public-hoist-pattern inrules_js
using thepublic_hoist_packages
attribute shown above.