aspect-build/rules_js

repository·main·Indexed 18 days ago

https://github.com/aspect-build/rules_js

A high-performance Bazel ruleset for JavaScript that leverages pnpm's layout logic for fast, correct, and lazy dependency management. It supports npm workspaces, Yarn lockfiles via npm_translate_lock, and provides specialized rules like js_image_layer for OCI images and contrib/nextjs for Next.js applications. Designed as a modern replacement for legacy rules_nodejs, it ensures seamless Node.js module resolution and optimizes build-and-push cycles by splitting application code and dependencies into distinct layers.

Tokens
12K
Snippets
35
Records
58
Agent score
63%

What's inside rules_js

  1. Overview of rules_js

    main

    rules_js is a high-performance Bazel integration for JavaScript based on the pnpm package manager. It is designed to be:

    • Lazy: Only fetches and installs npm packages required for the specific build or test targets requested.
    • Correct: Provides seamless Node.js module resolution, resolving common issues like TypeScript rootDirs path mapping.
    • Fast: Uses Bazel's sandbox to treat npm packages as directories rather than individual files, significantly improving performance.
    • Workspace-aware: Supports npm workspaces for managing nested packages in monorepos.
  2. Use NextJs Bazel Utils for Next.js applications

    main
    The rules_js/contrib/nextjs package provides a set of Bazel rules specifically designed to build and serve Next.js applications within a Bazel workspace. This allows you to integrate Next.js projects into your Bazel build graph, leveraging Bazel's caching and hermeticity for Next.js builds and deployments.
  3. Use npm_translate_lock to support Yarn lockfiles

    main

    The npm_translate_lock rule allows you to use Yarn lockfiles within a Bazel workspace by translating them into a format compatible with rules_js. When using this rule, you can configure the following attributes:

    • package_json: Specifies the package.json file.
    • yarn_lock: Specifies the yarn.lock file.
    • npmrc: Specifies the .npmrc configuration file.

    This enables rules_js to resolve dependencies from a Yarn-based environment.

    # Example attribute usage (conceptual)
    npm_translate_lock(
        name = "my_translated_lockfile",
        package_json = "package.json",
        yarn_lock = "yarn.lock",
        npmrc = ".npmrc",
    )
  4. Compare rules_js with rules_nodejs

    main

    If you are migrating from the unmaintained build_bazel_rules_nodejs, rules_js is the recommended replacement. While they share the rules_nodejs core toolchain and providers, the higher layers differ significantly:

    LayerLegacy (rules_nodejs)Modern (rules_js)
    Custom rulesnpm:@bazel/typescript, etc.aspect_rules_ts, etc.
    Package manager & Basic rulesbuild_bazel_rules_nodejsaspect_rules_js
    Toolchain & core providersrules_nodejsrules_nodejs
  5. Produce different packages using npm_package(root_paths)

    main

    You can use the npm_package(root_paths) rule to generate multiple distinct npm packages from the same source by specifying different root_paths. This is useful when you want to create different distribution versions or subsets of a package based on specific directory structures.

    # Conceptual usage of npm_package with different root_paths
    
    npm_package(
        name = "pkg_c1",
        root_paths = ["path/to/subset_1"],
        # ... other attributes
    )
    
    npm_package(
        name = "pkg_c2",
        root_paths = ["path/to/subset_2"],
        # ... other attributes
    )
  6. How rules_js manages dependencies and execution

    main

    Dependency Management

    Unlike legacy rules that call npm install or yarn install on a full package.json, rules_js uses Bazel's downloader to fetch only the specific packages needed for requested targets. It then mirrors the pnpm lock file into Starlark code to create a node_modules tree using Bazel repository rules.

    Program Execution

    rules_js follows the npm idiom (sources and outputs together in a common folder) rather than the traditional Bazel idiom (sources and outputs in separate trees).

    To ensure compatibility with tools that expect a standard filesystem layout, rules_js always runs JS tools with the working directory set to Bazel's output tree (bazel-out). A pnpm-style layout tool creates a node_modules under bazel-out so that all resolutions work naturally.

    Note for Rule Authors: Because the working directory is in the output tree, rule authors must re-path inputs and outputs. If you are writing custom rules that invoke a js_binary, use the js_binary_lib.run_binary_action helper (found in js/libs.bzl) instead of ctx.actions.run to ensure the BAZEL_BINDIR environment variable and pathing are handled correctly.

  7. Manage output directory structures in monorepos

    main

    Bazel requires that outputs for a package (a directory with a BUILD file) are written under that package's corresponding output folder. If your monorepo requires multiple packages to be output under a single dist/ folder, you have two architectural choices:

    1. Single Bazel Package: Place one BUILD file at the workspace root. This allows you to output to any path under my-workspace/dist/, but the BUILD file will become large and complex.
    2. Distributed dist folders: Move the dist/ folder inside each individual library (e.g., packages/lib1/dist/). This is more Bazel-idiomatic and allows for smaller, localized BUILD files, but may require updating tsconfig.json paths or other configuration files.

    Tip: If you need to keep legacy configuration files (like tsconfig.json) pointing to old paths during migration, use the jq rule to create a modified copy in bazel-bin instead of using copy_to_bin.

  8. How pnpm and rules_js work together

    main

    rules_js is designed to model npm package dependency handling by closely mimicking pnpm.

    Instead of Bazel performing its own resolution, it relies on a non-Bazel tool (typically pnpm) to resolve version constraints and determine the node_modules structure. This information is encoded in a pnpm-lock.yaml file. Bazel then uses this lockfile to create npm_import rules, allowing Bazel's downloader to fetch packages individually with integrity hashes for supply-chain security. This approach is highly performant because Bazel only fetches the specific packages required for the targets being analyzed.

  9. Optimize performance by linking first-party packages

    main

    When representing first-party packages in Bazel, prefer using js_library (or any rule providing JsInfo) over npm_package (which provides NpmPackageInfo).

    JsInfo allows rules_js to pass through the provider without collecting the full package content until an action actually requests it. In contrast, NpmPackageInfo requires building the full package content to output a single directory artifact, which is slower.

  10. Understand pnpm lockfile edge cases in rules_js

    main

    The rules_js repository includes a collection of specialized test cases located in the cases/ directory. These cases use unique pnpm-lock.yaml filenames to simulate complex dependency scenarios that are difficult to reproduce using standard package.json and pnpm workspaces. These edge cases are used to ensure npm_translate_lock and other rules handle non-standard lockfile structures correctly.

    Key edge cases include:

    • Transitive npm: dependency aliases: Using npm: to alias a transitive dependency to allow multiple versions of a single package (e.g., isaacs-cliui-v*).
    • Package overrides with different packages: Overriding a package with a completely different package (e.g., override-with-alias-url-v9).
    • Direct peer dependencies within importers: Scenarios where a direct dependency has a peer dependency requirement within the importers section, including inside npm: dependencies (e.g., docusaurus-direct-peer-v*).
    • Versionless patches: Handling pnpm v9.7+ behavior where patches are allowed without a version specifier (e.g., versionless-patch-v9).

    To see how these test cases are executed, refer to cases/BUILD.

  11. Link workspace packages using npm_link_all_packages

    main

    In a Bazel workspace using rules_js, you can define dependencies between local packages using the workspace:* protocol in your package.json files. When you use npm_link_all_packages, Bazel automatically resolves these workspace references and links the packages together, allowing them to be treated as dependencies within the Bazel build graph.

    {
      "dependencies": {
        "@mycorp/pkg-a": "workspace:*"
      }
    }