blob: 53a04c35ffde5dd97207801982c234d73eca9d13 [file] [view] [edit]
# Common troubleshooting tips
## Module not found errors
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](./pnpm.md#hoisting),
rules_js should behave the same way `pnpm` does with [`hoist=false`](https://pnpm.io/npmrc#hoist).
These problems are also reproducible under [Yarn PnP](https://yarnpkg.com/features/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.
### require appears in your code
This is the case when you write an `import` or `require` statement.
In this case you should add the runtime dependency to your BUILD file alongside your source file:
For example,
```starlark
js_library(
name = "requires_foo",
srcs = ["config.js"], # contains "require('foo')"
data = [":node_modules/foo"], # satisfies that require
)
```
and also, the `foo` module should be listed in your `package.json#dependencies` since pnpm is strict
about hoisting transitive dependencies to the root of `node_modules`.
This case also includes when you run some other tool, passing it a `config.js` file.
> This is the "ideal" way for JavaScript tools to be configured, because it allows an easy
> "symmetry" where you `require` a library and declare your dependency on it in the same place.
> When you pass a tool a `config.json` or other non-JavaScript file, and have string-typed references
> to npm packages, you'll fall into the next case: "require appears in third-party code".
### require appears in third-party code
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.
#### The `require` can move to first-party
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](https://webpack.js.org/loaders/postcss-loader/#sugarss)
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`.
#### It's a bug
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
> https://github.com/aspect-build/rules_js/issues/1215.
The recommended fix for both pnpm and rules_js is to use
[pnpm.packageExtensions](https://pnpm.io/package_json#pnpmpackageextensions)
in your `package.json` to add the missing `dependencies` or `peerDependencies`.
Example,
https://github.com/aspect-build/rules_js/blob/a8c192eed0e553acb7000beee00c60d60a32ed82/package.json#L12
> Make sure you pnpm install after changing `package.json`, as rules_js only reads the
> `pnpm-lock.yaml` file to gather dependency information.
> See [Fetch third-party packages](./README.md#fetch-third-party-packages-from-npm)
#### It's a plugin
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](https://pnpm.io/settings#publichoistpattern).
Use the [`public_hoist_packages` attribute of `npm_translate_lock`](https://registry.bazel.build/docs/aspect_rules_js#npm-extensions-bzl).
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 package 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:
`MODULE.bazel`
```starlark
npm.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`
```starlark
eslint_bin.eslint_test(
...
data = [
...
"//:node_modules/eslint-config-react-app",
],
)
```
> NB: We plan to add support for the `.npmrc` `public-hoist-pattern` setting to `rules_js` in a future release.
> For now, you must emulate public-hoist-pattern in `rules_js` using the `public_hoist_packages` attribute shown above.
## Ugly stack traces
Bazel's sandboxing and runfiles directory layouts can make stack traces and logs hard to read. This issue is common in many
languages when used within bazel, not only JavaScript.
One solution involving `Error.prepareStackTrace` was [suggested on bazelbuild slack](https://bazelbuild.slack.com/archives/CA31HN1T3/p1733518986229749?thread_ts=1733516180.969159&cid=CA31HN1T3) by [John Firebaugh](https://github.com/jfirebaugh). This overrides `Error.prepareStackTrace` to strip the bazel sandbox and runfiles paths from error stack traces. This also uses [`source-map-support`](https://www.npmjs.com/package/source-map-support) to also apply source maps to the stack traces.
See [examples/stack_traces](../examples/stack_traces) for a working example.
## Performance
For general bazel performance tips see the [Aspect bazelrc guide](https://docs.aspect.build/guides/bazelrc/#performance-options).
### Linking first-party packages
When linking first-party packages it is recommended to use `js_library` or another `JsInfo`-providing rule to represent the package instead of the `npm_package` rule (which provides `NpmPackageInfo`).
The use of `NpmPackageInfo` requires building the full package content in order to output a single directory artifact representing the package.
Using `JsInfo` allows rules_js to passthru the provider without collecting the package content until another action requests it. For example `JsInfo.types`, normally outputted by a slow `.d.ts` producing tool such as `tsc`, is most likely unnecessary when only executing or bundling JavaScript files from `JsInfo.sources`. If `JsInfo.types` is produced by different actions then `JsInfo.sources` then those actions may not be required at all.
### Parallelism (build, test)
A lot of tooling in the JS ecosystem uses parallelism to speed up builds. This is great, but as Bazel also parallels builds this can lead to a lot of contention for resources.
Some rulesets configure tools to take this into account such as the [rules_jest](https://github.com/aspect-build/rules_jest) default [run_in_band](https://github.com/aspect-build/rules_jest/blob/main/docs/jest_test.md#jest_test-run_in_band), while other tools (especially those without dedicated rulesets) may need to be configured manually.
For example, the [default WebPack configuration](https://webpack.js.org/configuration/optimization/#optimizationminimizer) uses Terser for optimization. `terser-webpack-plugin` defaults to [parallelizing its work across os.cpus().length - 1](https://www.npmjs.com/package/terser-webpack-plugin#parallel).
This can lead to builds performing slower due to IO throttling, or even failing if running in a virtualized environment where IO throughput is limited.
If you are experiencing slower than expected builds, you can try disabling or reducing parallelism for the tools you are using.
### Unnecessary npm package content
Npm packages sometimes include unnecessary files such as tests, test data etc. Large files or a large number of files
can effect performance and are sometimes worth explicitly excluding content.
In these cases you can add such packages and the respective files/folders you want to exclude.
```starlark
npm.npm_exclude_package_contents(
package = "resolve",
patterns = ["**/test/*"],
use_defaults = True,
)
```
These examples will remove the `test` folder from the "resolve" package as well use excluding the some default patterns
copied from [yarn autoclean](https://github.com/yarnpkg/yarn/blob/7cafa512a777048ce0b666080a24e80aae3d66a9/src/cli/commands/autoclean.js#L16).
The `package` can be dynamic, for example to opt-in to the default exclusions for all packages:
```starlark
npm.npm_exclude_package_contents(
package = "*",
use_defaults = True,
)
```
### Path mapping
Some build actions do essentially the same thing regardless of the compilation
mode (e.g. `opt` or `dbg`), but for technical reasons Bazel will not allow
cached results to be shared across different modes by default. See the [path
mapping guide](./path_mapping.md) for how you can address this problem.
#### Jest
See [rules_jest](https://github.com/aspect-build/rules_jest) for troubleshooting.
### Remote cache and npm package extraction
rules_js extracts each third-party npm package with one `NpmPackageExtract` action per
package, producing tree artifacts that can add up to a large number of files. Remote
caches generally store tree artifacts file-by-file, so caching these actions can involve
many network round trips, and in a cache-only setup (no remote execution) it may be
slower than simply re-extracting the tarballs locally; see
[#2715](https://github.com/aspect-build/rules_js/issues/2715) for an example.
If your builds spend significant time on `NpmPackageExtract` actions when using a remote
cache, you may want to experiment with excluding them from it:
```
# Don't upload or fetch npm package extraction results; re-extract locally instead.
common --modify_execution_info=NpmPackageExtract=+no-remote-cache
```
With remote execution the extracted outputs are needed in the remote CAS anyway, so
there you most likely want to keep these actions cached, especially when combined with
Build without the Bytes (`--remote_download_minimal`). There is no setting that is right
for everyone, which is why rules_js leaves this choice to you.
You can also reduce the size of the extracted packages themselves with
`npm.npm_exclude_package_contents`, which removes files you consider unnecessary (docs,
tests, etc.) at extraction time; see
[Unnecessary npm package content](#unnecessary-npm-package-content) above.
## Code coverage
Under `bazel coverage`, `js_test` produces its lcov report as the test program
exits, where the V8 coverage data and the instrumented sources are both available.
The `_lcov_merger` step only publishes that report.
### Coverage runs count against the test's timeout
Because the report is produced by the test itself, generating it consumes the
target's own `timeout`/`size` budget. Converting V8 coverage to lcov takes time
proportional to the number of instrumented files, so a test that sits near its
timeout under `bazel test` may need a larger `size` or `timeout` to pass under
`bazel coverage`.
For the same reason the report is produced by the target's own `node_toolchain`,
not by the one used for build actions. A test pinned to an old Node version runs
the coverage reporter under that version too.
### Code run from the program's own `exit` listeners is not covered
The reporter is registered by a `--require` preload, so it necessarily runs
before any `process.on('exit', ...)` listener the test program itself registers.
The V8 profile is snapshotted at that point, which means code executed from the
program's own `exit` listeners is reported as uncovered. A preload cannot
register last, so this is inherent to producing the report in-process.
### Coverage requires a runfiles tree
The reporter maps V8 coverage data back onto sources through the test's runfiles,
so a target built without a runfiles tree (`enable_runfiles = False`, which is the
default on Windows without `--enable_runfiles`) reports empty coverage and logs
```
ERROR: ...: coverage report generator '...' not found; code coverage requires a runfiles tree
```
The test itself still passes; enable runfiles to collect coverage for it.
### Empty coverage reports
An empty `coverage.dat` usually means nothing was instrumented — see
`--instrumentation_filter` and `--instrument_test_targets`.
If it is accompanied by
```
WARNING: no coverage report was generated in the test action, reporting empty coverage.
```
then either the target is `testonly = False`, which rules_js does not collect
coverage for, or it is a custom test rule that reuses only part of rules_js's
coverage support. In the latter case use `js_test`, or the test rule provided by the
ruleset for your test framework, rather than assembling one from rules_js internals.
### First-party code repackaged with `npm_package`
Coverage of first-party code imported by package name (`require("@my-scope/lib")`)
is supported when the package is linked from a `js_library` (a pnpm-workspace-style
first-party dependency): the `.aspect_rules_js` store entry points at the library's
own sources, so coverage attributes to them.
It is **not** supported when the code is repackaged with `npm_package` and then
linked by name. `npm_package` produces a distributable _copy_ in the store with no
link back to the sources, so the executed file cannot be mapped to them and the
target reports empty coverage. This is a deliberate limitation: for coverage of
first-party code under test, depend on it as a `js_library` (by relative path or by
name), not as a repackaged `npm_package`. See
[#2933](https://github.com/aspect-build/rules_js/issues/2933).
### Test frameworks that report their own coverage
A test program that writes lcov directly to `$COVERAGE_OUTPUT_FILE` (jest, nyc, ...)
owns that report, and rules_js will not overwrite it — but only while
post-processing runs alongside the test. Under
`--experimental_split_coverage_postprocessing` (required for remote execution, along
with `--experimental_fetch_all_coverage_outputs`) Bazel discards whatever the test
wrote there and publishes the report rules_js generated instead. Rulesets in that
situation, such as
[rules_jest](https://github.com/aspect-build/rules_jest), handle coverage themselves
rather than relying on the rules_js implementation.