blob: 09643ceabd4f8cafecccde9feaeba28c6a63dd75 [file] [log] [blame] [view]
---
title: Getting Started
---
Stuck?
- See the [Frequently asked questions](./faq.md)
- Ask in `#javascript` on <http://slack.bazel.build>
- Check for [known issues](https://github.com/aspect-build/rules_js/issues)
- Sign up for commercial support, provided as a Slack Connect channel by <https://aspect.build/services>.
## Installation
Follow instructions in the release you wish to use:
<https://github.com/aspect-build/rules_js/releases>.
## Usage
### Bazel basics
Bazel's `BUILD` or `BUILD.bazel` files are used to declare the dependency graph of your code.
They describe the source files and their dependencies, and declare entry points for programs or tests.
However, they don't say _how to build_ the code, that's the job of Bazel rules.
Because `BUILD` files typically declare a finer-grained dependency graph than `package.json` files, Bazel can be smarter about what to fetch or invalidate for a given build.
For example, Bazel might only need to fetch a single npm package for a simple build,
where you might experience other tools installing the entire `package.json` file.
Authoring BUILD files by hand is a chore, so we recommend using the
[`configure`](https://docs.aspect.build/cli/commands/aspect_configure) command from
[Aspect CLI](https://aspect.build/cli) to automate 80% of this work.
Other recommendations:
- Put [common flags](https://blog.aspect.build/bazelrc-flags) in your `.bazelrc` file.
- Use [Renovate](https://docs.renovatebot.com/) to keep your Bazel dependencies up-to-date.
### Node.js
rules_js depends on rules_nodejs version 6.1.0 or greater.
Installation is included in the `MODULE` or `WORKSPACE` snippet you pasted from the Installation instructions above.
**API docs:**
- Choosing the version of Node.js:
<https://bazel-contrib.github.io/rules_nodejs/install.html>
- Rules API: <https://bazel-contrib.github.io/rules_nodejs/Core.html>
- The Node.js toolchain: <https://bazel-contrib.github.io/rules_nodejs/Toolchains.html>
### Fetch third-party packages from npm
rules_js accesses npm packages using [pnpm].
pnpm's ["symlinked node_modules structure"](https://pnpm.io/symlinked-node-modules-structure) of packages aligns with Bazel's "external repositories",
and the pnpm "linker" which creates the `node_modules` tree has semantics we can reproduce with Bazel actions.
If your code works with pnpm, then you should expect it works under Bazel as well.
This means that if your issue can be reproduced outside of Bazel, using a reproduction with only pnpm,
then we ask that you fix the issue there, and will close such issues filed on rules_js.
Supported pnpm versions are 7-9 (lockfile versions 5.4-6.1, 9).
The typical usage is to import an entire `pnpm-lock.yaml` file.
Create such a file if you don't have one. You could install pnpm on your machine, or use `npx` to run it.
We recommend this command, which creates a lockfile with minimal installation needed,
using the identical version of pnpm that Bazel is configured with:
```shell
$ bazel run -- @pnpm//:pnpm --dir $PWD install --lockfile-only
```
Next, you'll typically use `npm_translate_lock` to translate the lock file to Starlark, which Bazel extensions understand.
The `MODULE` or `WORKSPACE` snippet you pasted above already contains this code.
Technically, we run a port of pnpm rather than pnpm itself. Here are some design details:
1. You don't need to install pnpm on your machine to build and test with Bazel.
1. We re-use pnpm's resolver, by consuming the `pnpm-lock.yaml` file it produces.
1. We use Bazel's downloader API to fetch package tarballs and extract them to external repositories.
To modify the URLs Bazel uses to download packages (for example, to fetch from Artifactory), read
<https://blog.aspect.build/configuring-bazels-downloader>.
1. We re-use the [`@pnpm/lifecycle`](https://www.npmjs.com/package/@pnpm/lifecycle) package to perform postinstall steps.
(These run as cacheable Bazel actions.)
1. Finally, you link the `node_modules` tree by adding a `npm_link_package` or `npm_link_all_packages` in your `BUILD` file,
which populates a tree under `bazel-bin/[path/to/package]/node_modules`.
After importing the lockfile, you should be able to fetch the resulting repository.
Assuming your `npm_translate_lock` was named `npm`, you can run:
```shell
$ bazel fetch @npm//...
```
#### Migration from npm or yarn
During migration from npm or yarn you can still use a `package-lock.json` or `yarn.lock` as the source of truth by
setting the `npm_package_lock`/`yarn_lock` attributes of `npm_translate_lock`. When the `pnpm-lock.yaml` is out of date
rules_js will run `pnpm import` to generate the `pnpm-lock.yaml` file.
See the notes about these attributes in the [migration guide](https://docs.aspect.build/guides/rules_js_migration).
### Using pnpm workspaces
Here's an example `pnpm-workspace.yaml` file which will find all projects under `apps` or `packages` at any depth, and anything directly under `tools`, based on the existence of a `package.json` file.
Make sure to place this file at the root of the repository.
```yaml
packages:
- 'apps/**'
- 'packages/**'
- 'tools/*'
```
### Link the node_modules
Next, we'll need to "link" these npm packages into a `node_modules` tree.
If you use [pnpm workspaces], the `node_modules` tree contains first-party packages from your
monorepo as well as third-party packages from npm.
> Bazel doesn't use the `node_modules` installed in your source tree.
> You do not need to run `pnpm install` before running Bazel commands.
> Changes you make to files under `node_modules` in your source tree are not reflected in Bazel results.
Typically, you'll just link all npm packages into the Bazel package containing the `package.json` file.
If you use [pnpm workspaces], you will do this for each npm package in your monorepo.
In `BUILD.bazel`:
```starlark
load("@npm//:defs.bzl", "npm_link_all_packages")
npm_link_all_packages()
```
You can see this working by running `bazel build ...`, then look in the `bazel-bin` folder.
You'll see something like this:
```bash
# the package store
bazel-bin/node_modules/.aspect_rules_js
# symlink into the package store
bazel-bin/node_modules/some_pkg
# If you used pnpm workspaces:
bazel-bin/packages/some_pkg/node_modules/some_dep
```
**API docs:**
- [npm_import](./npm_import.md): Import all packages from the pnpm-lock.yaml file, or import individual packages.
- [npm_link_package](./npm_link_package.md): Link npm package(s) into the `bazel-bin/[path/to/package]/node_modules` tree so that the Node.js runtime can resolve them.
### JavaScript
rules_js provides some primitives to work with JS files.
However, since JavaScript is an interpreted language, simple use cases don't require performing build steps like compilation.
The Node.js module resolution algorithm requires that all files (sources, generated code, and dependencies) be co-located in a common filesystem tree, which is the working directory for the
Node.js interpreter.
As described earlier, the dependencies were linked into `bazel-bin/[path/to/package]/node_modules`,
and Bazel places generated files in `bazel-bin/[path/to/package]`. This leaves source files to be
copied to this location.
> Copying sources to the bazel-bin folder is surprising if you come from a Bazel background, as other
> Bazel rulesets accomodate tooling by teaching it to mix a source folder and an output folder.
> This is not possible with Node.js, without breaking compatibility of many tools.
Our custom rules will take care of copying their sources to the `bazel-bin` output folder automatically.
However this only works when those sources are under the same `BUILD` file as the target that does
the copying. If you have a source file in another `BUILD` file, you'll need to explicitly copy that
with a rule like [`copy_to_bin`](https://docs.aspect.build/bazel-contrib/bazel-lib/v1.0.0/docs/copy_to_bin-docgen.html#copy_to_bin).
**API docs:**
- [js_library](./js_library.md): Declare a logical grouping of JS files and their dependencies.
- [js_binary](./js_binary.md): Declare a Node.js executable program.
- [js_run_binary](./js_run_binary.md): Run a Node.js executable program as the "tool" in a Bazel action that produces outputs, similar to `genrule`.
### Using binaries published to npm
rules_js automatically mirrors the `bin` field from the `package.json` file of your npm dependencies
to a Starlark API you can load from in your BUILD file or macro.
For example, if you depend on the `typescript` npm package in your root `package.json`, the `tsc` bin entry can be accessed in a `BUILD`:
```starlark=
load("@npm//:typescript/package_json.bzl", typescript_bin = "bin")
typescript_bin.tsc(
name = "compile",
srcs = [
"fs.cts",
"tsconfig.json",
"//:node_modules/@types/node",
],
outs = ["fs.cjs"],
chdir = package_name(),
args = ["-p", "tsconfig.json"],
)
```
If you depend on the `typescript` npm package from a nested `package.json` such as `myapp/package.json`, the bin entry would be loaded from the nested package:
```starlark=
load("@npm//myapp:typescript/package_json.bzl", typescript_bin = "bin")
```
Each bin exposes three rules, one for each Bazel command ("verb"): build, test and run - each aligning with the corresponding [js_run_binary](./js_run_binary.md), [js_test](#js_test) and [js_binary](./js_binary.md) rule APIs.
For example:
| Rule | Underlying Rule | Invoked with | To |
| ------------ | --------------- | ------------- | --------------- |
| `foo` | `js_run_binary` | `bazel build` | produce outputs |
| `foo_binary` | `js_binary` | `bazel run` | side-effects |
| `foo_test` | `js_test` | `bazel test` | assert exit `0` |
> Note: this doesn't cause an eager fetch!
> Bazel doesn't download the typescript package when loading this file, so you can safely write this
> even in a BUILD.bazel file that includes unrelated rules.
To inspect what's in the `@npm` workspace, start with a `bazel query` like the following:
```shell
$ bazel query @npm//... --output=location | grep bzl_library
/shared/cache/bazel/user_base/581b2ac03dd093577e8a6ba6b6509be5/external/npm/BUILD.bazel:5095:12: bzl_library rule @npm//:typescript_bzl_library
/shared/cache/bazel/user_base/581b2ac03dd093577e8a6ba6b6509be5/external/npm/examples/macro/BUILD.bazel:4:12: bzl_library rule @npm//examples/macro:mocha_bzl_library
```
This shows locations on disk where the npm packages can be loaded.
> [!NOTE]
> These queries only work when `generate_bzl_library_targets = True` is passed to `npm_translate_lock`.
> If you get no results, check the settings in your `MODULE.bazel` or `WORKSPACE` file and try again.
To see the definition of one of these targets, you can run another `bazel query`:
```shell
$ bazel query --output=build @npm//:typescript_bzl_library
# /shared/cache/bazel/user_base/581b2ac03dd093577e8a6ba6b6509be5/external/npm/BUILD.bazel:5095:12
bzl_library(
name = "typescript_bzl_library",
visibility = ["//visibility:public"],
srcs = ["@npm//:typescript/package_json.bzl"],
deps = ["@npm__typescript__5.8.2//:typescript_bzl_library"],
)
```
This shows us that the label `@npm//:typescript/package_json.bzl` can be used to load the "bin" symbol. You can also follow the location on disk to find that file.
### Macros
[Bazel macros] are a critical part of making your BUILD files more maintainable.
Make sure to follow the [Style Guide](https://bazel.build/rules/bzl-style#macros) when writing a macro,
since some anti-patterns can make your BUILD files difficult to change in the future.
Like Custom Rules, Macros require you to use the Starlark language, but writing a macro is much easier
since it merely composes existing rules together, rather than writing any from scratch.
We believe that most use cases can be accomplished with macros, and discourage you learning how to write
custom rules unless you're really interested in investing time becoming a Bazel expert.
You can think of Macros as a way to create your own Build System, by piping the existing tools together
(like a unix pipeline that composes command-line utilities by piping their stdout/stdin).
As an example, we could write a wrapper for the `typescript_bin.tsc` rule above.
In `tsc.bzl` we could write:
```starlark
load("@npm//:typescript/package_json.bzl", typescript_bin = "bin")
def tsc(name, args = ["-p", "tsconfig.json"], **kwargs):
typescript_bin.tsc(
name = name,
args = args,
# Always run tsc with the working directory in the project folder
chdir = native.package_name(),
**kwargs
)
```
so that the users `BUILD` file can omit some of the syntax and default settings:
```starlark
load(":tsc.bzl", "tsc")
tsc(
name = "two",
srcs = [
"tsconfig.json",
"two.ts",
"//:node_modules/@types/node",
"//examples/js_library/one",
],
outs = [
"two.js",
],
)
```
### Custom rules
If macros are not sufficient to express your Bazel logic, you can use a custom rule instead.
Aspect has written a number of these based on rules_js, such as:
- [rules_ts](https://github.com/aspect-build/rules_ts) - Bazel rules for the `tsc` compiler from <http://typescriptlang.org>
- [rules_swc](https://github.com/aspect-build/rules_swc) - Bazel rules for the swc toolchain <https://swc.rs/>
- [rules_jest](https://github.com/aspect-build/rules_jest) - Bazel rules to run tests using https://jestjs.io
- [rules_esbuild](https://github.com/aspect-build/rules_esbuild) - Bazel rules for <https://esbuild.github.io/> JS bundler
- [rules_webpack](https://github.com/aspect-build/rules_webpack) - Bazel rules for webpack bundler <https://webpack.js.org/>
- [rules_terser](https://github.com/aspect-build/rules_terser) - Bazel rules for <https://terser.org/> - a JavaScript minifier
- [rules_rollup](https://github.com/aspect-build/rules_rollup) - Bazel rules for <https://rollupjs.org/> - a JavaScript bundler
- [rules_deno](https://github.com/aspect-build/rules_deno) - Bazel rules for Deno http://deno.land
- [rules_lint](https://github.com/aspect-build/rules_lint) includes [eslint support](https://github.com/aspect-build/rules_lint/blob/main/docs/eslint.md).
You can also write your own custom rule, though this is an advanced topic and not covered in this documentation.
### Documenting your macros and custom rules
You can use [stardoc] to produce API documentation from Starlark code.
We recommend producing Markdown output, and checking those `.md` files into your source repository.
This makes it easy to browse them at the same revision as the sources.
You'll need to create `bzl_library` targets for your Starlark files.
This is a good practice as it lets users of your code generate their own documentation as well.
In addition, Aspect's bazel-lib provides some helpers that make it easy to run stardoc and check that it's always up-to-date.
Continuing our example, where we wrote a macro in `tsc.bzl`, we'd write this to document it, in `BUILD`:
```starlark
load("@aspect_bazel_lib//lib:docs.bzl", "stardoc_with_diff_test", "update_docs")
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
bzl_library(
name = "tsc",
srcs = ["tsc.bzl"],
deps = [
# this is a bzl_library target, exposing the package_json.bzl file we depend on
"@npm//:typescript",
],
)
stardoc_with_diff_test(
name = "tsc-docs",
bzl_library_target = ":tsc",
)
update_docs(name = "docs")
```
This setup appears in [examples/macro](https://github.com/aspect-build/rules_js/blob/main/examples/macro/BUILD.bazel).
### Create first-party npm packages
You can declare an npm package from sources in your repository.
The package can be exported for usage outside the repository, to a registry like npm or Artifactory.
Or, you can use it locally within a monorepo using [pnpm workspaces].
> Note: we don't yet document how to publish. For now, build the `npm_package` target with `bazel build`, then
> `cd` into the `bazel-out` folder where the package was created, and run `npm pack` or `npm publish`.
**API docs:**
- [npm_package](./npm_package.md)
[pnpm]: https://pnpm.io/
[pnpm workspaces]: https://pnpm.io/workspaces
[bazel macros]: https://bazel.build/rules/macros
[gazelle]: https://github.com/bazelbuild/bazel-gazelle
[stardoc]: https://github.com/bazelbuild/stardoc
### CI/CD setup
We recommend using [Aspect Workflows](https://docs.aspect.build/workflows) which provides a pool of warm Bazel CI runners
for your existing CI system. This provides the promised Bazel speed and cost savings, with very little work required from your
developer infrastructure team!
### Debugging
Add [Debug options](https://docs.aspect.build/guides/bazelrc#debug-options) and [Options for JavaScript](https://docs.aspect.build/guides/bazelrc#options-for-javascript) to your project’s .bazelrc file to add the `--config=debug` settings for debugging Node.js programs.
In this repository, for example, we can debug the `//examples/js_binary:test_test` `js_test` target with,
```
$ bazel run //examples/js_binary:test_test --config=debug
Starting local Bazel server and connecting to it...
INFO: Analyzed target //examples/js_binary:test_test (65 packages loaded, 1023 targets configured).
INFO: Found 1 target...
Target //examples/js_binary:test_test up-to-date:
bazel-bin/examples/js_binary/test_test.sh
INFO: Elapsed time: 6.774s, Critical Path: 0.08s
INFO: 6 processes: 4 internal, 2 local.
INFO: Build completed successfully, 6 total actions
INFO: Build completed successfully, 6 total actions
exec ${PAGER:-/usr/bin/less} "$0" || exit 1
Executing tests from //examples/js_binary:test_test
-----------------------------------------------------------------------------
Debugger listening on ws://127.0.0.1:9229/76b4bb42-7d4e-41f6-a7fe-92b57db356ad
For help, see: https://nodejs.org/en/docs/inspector
```
#### Debugging with Chrome DevTools
At this point you can connect to this Node.js debugging session with a debugging tool.
To use Chrome, open a new tab and enter the URL `chrome://inspect/`. You should see the
session listed there and you can connect to it and debug in Chrome DevTools.
See [Debugging Node.js with Chrome DevTools](https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27)
to understand the basics of using the DevTools with Node.
#### Debugging with Visual Studio Code
In this repository, we have added a VSCode the `.vscode/launch.json` configuration file
so you can launch into a debugging session directly from the
[Run & Debug](https://code.visualstudio.com/docs/editor/debugging) window.
## Troubleshooting
See [troubleshooting.md](troubleshooting.md).