From 750455fd8e71aecd5626135db9f9db0db2209132 Mon Sep 17 00:00:00 2001 From: Simeon Visotskiy Date: Mon, 28 Jul 2025 19:13:48 +0300 Subject: [PATCH 01/12] Add replace_package module extension tag --- .../MODULE.bazel | 9 ++++--- npm/extensions.bzl | 26 +++++++++++++++---- npm/private/npm_translate_lock_generate.bzl | 2 +- npm/private/npm_translate_lock_helpers.bzl | 8 +++--- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/e2e/npm_translate_lock_replace_packages/MODULE.bazel b/e2e/npm_translate_lock_replace_packages/MODULE.bazel index 63186ce21..22d0b7f25 100644 --- a/e2e/npm_translate_lock_replace_packages/MODULE.bazel +++ b/e2e/npm_translate_lock_replace_packages/MODULE.bazel @@ -19,13 +19,16 @@ npm = use_extension( "npm", dev_dependency = True, ) + +npm.replace_package( + package = "chalk@5.3.0", + replacement = "@chalk_501//:pkg", +) + npm.npm_translate_lock( name = "npm", npmrc = "//:.npmrc", pnpm_lock = "//:pnpm-lock.yaml", - replace_packages = { - "chalk@5.3.0": "@chalk_501//:pkg", - }, verify_node_modules_ignored = "//:.bazelignore", ) diff --git a/npm/extensions.bzl b/npm/extensions.bzl index 0104e8288..9bf9f3ec7 100644 --- a/npm/extensions.bzl +++ b/npm/extensions.bzl @@ -30,15 +30,21 @@ def _npm_extension_impl(module_ctx): exclude_package_contents_config = _build_exclude_package_contents_config(module_ctx) for mod in module_ctx.modules: + replace_packages = {} + for attr in mod.tags.replace_package: + if attr.package in replace_packages: + fail("Each package can have only one replacement") + replace_packages[attr.package] = "@@{}//{}:{}".format(attr.replacement.repo_name, attr.replacement.package, attr.replacement.name) + for attr in mod.tags.npm_translate_lock: - _npm_translate_lock_bzlmod(attr, exclude_package_contents_config) + _npm_translate_lock_bzlmod(attr, exclude_package_contents_config, replace_packages) # We cannot read the pnpm_lock file before it has been bootstrapped. # See comment in e2e/update_pnpm_lock_with_import/test.sh. if attr.pnpm_lock: if hasattr(module_ctx, "watch"): module_ctx.watch(attr.pnpm_lock) - _npm_lock_imports_bzlmod(module_ctx, attr, exclude_package_contents_config) + _npm_lock_imports_bzlmod(module_ctx, attr, exclude_package_contents_config, replace_packages) for i in mod.tags.npm_import: _npm_import_bzlmod(i) @@ -71,7 +77,7 @@ def _build_exclude_package_contents_config(module_ctx): return exclusions -def _npm_translate_lock_bzlmod(attr, exclude_package_contents_config): +def _npm_translate_lock_bzlmod(attr, exclude_package_contents_config, replace_packages): npm_translate_lock_rule( name = attr.name, bins = attr.bins, @@ -94,7 +100,7 @@ def _npm_translate_lock_bzlmod(attr, exclude_package_contents_config): prod = attr.prod, public_hoist_packages = attr.public_hoist_packages, quiet = attr.quiet, - replace_packages = attr.replace_packages, + replace_packages = replace_packages, root_package = attr.root_package, update_pnpm_lock = attr.update_pnpm_lock, use_home_npmrc = attr.use_home_npmrc, @@ -105,7 +111,7 @@ def _npm_translate_lock_bzlmod(attr, exclude_package_contents_config): bzlmod = True, ) -def _npm_lock_imports_bzlmod(module_ctx, attr, exclude_package_contents_config): +def _npm_lock_imports_bzlmod(module_ctx, attr, exclude_package_contents_config, replace_packages): state = npm_translate_lock_state.new(attr.name, module_ctx, attr, True) importers, packages = translate_to_transitive_closure( @@ -148,6 +154,7 @@ WARNING: Cannot determine home directory in order to load home `.npmrc` file in imports = npm_translate_lock_helpers.get_npm_imports( importers = importers, packages = packages, + replace_packages = replace_packages, patched_dependencies = state.patched_dependencies(), only_built_dependencies = state.only_built_dependencies(), root_package = attr.pnpm_lock.package, @@ -248,6 +255,9 @@ def _npm_translate_lock_attrs(): # Args not supported or unnecessary in bzlmod attrs.pop("repositories_bzl_filename") attrs.pop("exclude_package_contents") # Use tag classes only for MODULE.bazel + + # Replaced with tag in bzlmod + attrs.pop("replace_packages") return attrs @@ -283,12 +293,18 @@ def _npm_exclude_package_contents_attrs(): ), } +_REPLACE_PACKAGE_ATTRS = { + "package": attr.string(), + "replacement": attr.label(), +} + npm = module_extension( implementation = _npm_extension_impl, tag_classes = { "npm_translate_lock": tag_class(attrs = _npm_translate_lock_attrs()), "npm_import": tag_class(attrs = _npm_import_attrs()), "npm_exclude_package_contents": tag_class(attrs = _npm_exclude_package_contents_attrs()), + "replace_package": tag_class(attrs = _REPLACE_PACKAGE_ATTRS), }, ) diff --git a/npm/private/npm_translate_lock_generate.bzl b/npm/private/npm_translate_lock_generate.bzl index cddf272b5..ad6ffc2d4 100644 --- a/npm/private/npm_translate_lock_generate.bzl +++ b/npm/private/npm_translate_lock_generate.bzl @@ -97,7 +97,7 @@ def generate_repository_files(rctx, pnpm_lock_label, importers, packages, patche # empty line after bzl docstring since buildifier expects this if this file is vendored in generated_by_prefix = "\"\"\"@generated by npm_translate_lock(name = \"{}\", pnpm_lock = \"{}\")\"\"\"\n".format(helpers.to_apparent_repo_name(rctx.name), str(pnpm_lock_label)) - npm_imports = helpers.get_npm_imports(importers, packages, patched_dependencies, only_built_dependencies, root_package, rctx.name, rctx.attr, rctx.attr.lifecycle_hooks, rctx.attr.lifecycle_hooks_execution_requirements, rctx.attr.lifecycle_hooks_use_default_shell_env, npm_registries, default_registry, npm_auth) + npm_imports = helpers.get_npm_imports(importers, packages, rctx.attr.replace_packages, patched_dependencies, only_built_dependencies, root_package, rctx.name, rctx.attr, rctx.attr.lifecycle_hooks, rctx.attr.lifecycle_hooks_execution_requirements, rctx.attr.lifecycle_hooks_use_default_shell_env, npm_registries, default_registry, npm_auth) link_packages = [helpers.link_package(root_package, import_path) for import_path in importers.keys()] diff --git a/npm/private/npm_translate_lock_helpers.bzl b/npm/private/npm_translate_lock_helpers.bzl index 28ea9c0fb..ad26740dc 100644 --- a/npm/private/npm_translate_lock_helpers.bzl +++ b/npm/private/npm_translate_lock_helpers.bzl @@ -253,7 +253,7 @@ def _select_npm_auth(url, npm_auth): return npm_auth_bearer, npm_auth_basic, npm_auth_username, npm_auth_password ################################################################################ -def _get_npm_imports(importers, packages, patched_dependencies, only_built_dependencies, root_package, rctx_name, attr, all_lifecycle_hooks, all_lifecycle_hooks_execution_requirements, all_lifecycle_hooks_use_default_shell_env, registries, default_registry, npm_auth, exclude_package_contents_config = None): +def _get_npm_imports(importers, packages, replace_packages, patched_dependencies, only_built_dependencies, root_package, rctx_name, attr, all_lifecycle_hooks, all_lifecycle_hooks_execution_requirements, all_lifecycle_hooks_use_default_shell_env, registries, default_registry, npm_auth, exclude_package_contents_config = None): "Converts packages from the lockfile to a struct of attributes for npm_import" if attr.prod and attr.dev: fail("prod and dev attributes cannot both be set to true") @@ -386,11 +386,11 @@ ERROR: can not apply both `pnpm.patchedDependencies` and `npm_translate_lock(pat exclude_package_contents = _gather_package_content_excludes(attr.exclude_package_contents, name, friendly_name, unfriendly_name) # gather replace packages - replace_packages, _ = _gather_values_from_matching_names(True, attr.replace_packages, name, friendly_name, unfriendly_name) - if len(replace_packages) > 1: + replace_package, _ = _gather_values_from_matching_names(True, replace_packages, name, friendly_name, unfriendly_name) + if len(replace_package) > 1: msg = "Multiple package replacements found for package {}".format(name) fail(msg) - replace_package = replace_packages[0] if replace_packages else None + replace_package = replace_package[0] if replace_package else None # gather custom postinstalls custom_postinstalls, _ = _gather_values_from_matching_names(True, attr.custom_postinstalls, name, friendly_name, unfriendly_name) From 738aba8e9a30ac56c01c0e5578c2c0f6bb5fe242 Mon Sep 17 00:00:00 2001 From: Simeon Visotskiy Date: Mon, 28 Jul 2025 19:34:05 +0300 Subject: [PATCH 02/12] Update documentation for replace_package tag and format code --- e2e/npm_translate_lock_replace_packages/MODULE.bazel | 2 -- 1 file changed, 2 deletions(-) diff --git a/e2e/npm_translate_lock_replace_packages/MODULE.bazel b/e2e/npm_translate_lock_replace_packages/MODULE.bazel index 22d0b7f25..8a0cf7ba1 100644 --- a/e2e/npm_translate_lock_replace_packages/MODULE.bazel +++ b/e2e/npm_translate_lock_replace_packages/MODULE.bazel @@ -19,12 +19,10 @@ npm = use_extension( "npm", dev_dependency = True, ) - npm.replace_package( package = "chalk@5.3.0", replacement = "@chalk_501//:pkg", ) - npm.npm_translate_lock( name = "npm", npmrc = "//:.npmrc", From e8c6da8713556f9255f271997964759145118490 Mon Sep 17 00:00:00 2001 From: Simeon Visotskiy Date: Mon, 28 Jul 2025 19:40:12 +0300 Subject: [PATCH 03/12] Format and docs --- docs/npm_translate_lock.md | 2 +- npm/extensions.bzl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/npm_translate_lock.md b/docs/npm_translate_lock.md index 217c73e98..711fd3f9c 100644 --- a/docs/npm_translate_lock.md +++ b/docs/npm_translate_lock.md @@ -123,7 +123,7 @@ For more about how to use npm_translate_lock, read [pnpm and rules_js](/docs/pnp | lifecycle_hooks_execution_requirements | Execution requirements applied to the preinstall, install and postinstall lifecycle hooks on npm packages.

The execution requirements can be defined per package by package name or globally using "*".

Execution requirements are not additive. The most specific match wins.

Read more: [lifecycles](/docs/pnpm.md#lifecycles) | `{}` | | lifecycle_hooks_no_sandbox | If True, a "no-sandbox" execution requirement is added to all lifecycle hooks unless overridden by `lifecycle_hooks_execution_requirements`.

Equivalent to adding `"*": ["no-sandbox"]` to `lifecycle_hooks_execution_requirements`.

This defaults to True to limit the overhead of sandbox creation and copying the output TreeArtifacts out of the sandbox.

Read more: [lifecycles](/docs/pnpm.md#lifecycles) | `True` | | lifecycle_hooks_use_default_shell_env | The `use_default_shell_env` attribute of the lifecycle hooks actions on npm packages.

See [use_default_shell_env](https://bazel.build/rules/lib/builtins/actions#run.use_default_shell_env)

Note: [--incompatible_merge_fixed_and_default_shell_env](https://bazel.build/reference/command-line-reference#flag--incompatible_merge_fixed_and_default_shell_env) is often required and not enabled by default in Bazel < 7.0.0.

This defaults to False reduce the negative effects of `use_default_shell_env`. Requires bazel-lib >= 2.4.2.

Read more: [lifecycles](/docs/pnpm.md#lifecycles) | `{}` | -| replace_packages | A dict of package names to npm_package targets to link instead of the sources specified in the pnpm lock file for the corresponding packages.

The injected npm_package targets may optionally contribute transitive npm package dependencies on top of the transitive dependencies specified in the pnpm lock file for their respective packages, however, these transitive dependencies must not collide with pnpm lock specified transitive dependencies.

Any patches specified for the packages will be not applied to the injected npm_package targets. They will be applied, however, to the fetches sources for their respecitve packages so they can still be useful for patching the fetched `package.json` files, which are used to determine the generated bin entries for packages.

NB: lifecycle hooks and custom_postinstall scripts, if implicitly or explicitly enabled, will be run on the injected npm_package targets. These may be disabled explicitly using the `lifecycle_hooks` attribute. | `{}` | +| replace_packages | A dict of package names to npm_package targets to link instead of the sources specified in the pnpm lock file for the corresponding packages.

**Note for bzlmod users:** Use the `replace_package` tag class instead:

npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm")
npm.replace_package(
    package = "chalk@5.3.0",
    replacement = "@chalk_501//:pkg",
)
npm.npm_translate_lock(name = "npm", pnpm_lock = "//:pnpm-lock.yaml")


The injected npm_package targets may optionally contribute transitive npm package dependencies on top of the transitive dependencies specified in the pnpm lock file for their respective packages, however, these transitive dependencies must not collide with pnpm lock specified transitive dependencies.

Any patches specified for the packages will be not applied to the injected npm_package targets. They will be applied, however, to the fetches sources for their respecitve packages so they can still be useful for patching the fetched `package.json` files, which are used to determine the generated bin entries for packages.

NB: lifecycle hooks and custom_postinstall scripts, if implicitly or explicitly enabled, will be run on the injected npm_package targets. These may be disabled explicitly using the `lifecycle_hooks` attribute. | `{}` | | bins | Binary files to create in `node_modules/.bin` for packages in this lock file.

For a given package, this is typically derived from the "bin" attribute in the package.json file of that package.

For example:

bins = {
    "@foo/bar": {
        "foo": "./foo.js",
        "bar": "./bar.js"
    },
}


Dicts of bins not additive. The most specific match wins.

In the future, this field may be automatically populated from information in the pnpm lock file. That feature is currently blocked on https://github.com/pnpm/pnpm/issues/5131.

Note: Bzlmod users must use an alternative syntax due to module extensions not supporting dict-of-dict attributes:

bins = {
    "@foo/bar": [
        "foo=./foo.js",
        "bar=./bar.js"
    ],
}
| `{}` | | verify_node_modules_ignored | node_modules folders in the source tree should be ignored by Bazel.

This points to a `.bazelignore` file to verify that all nested node_modules directories pnpm will create are listed.

See https://github.com/bazelbuild/bazel/issues/8106 | `None` | | verify_patches | Label to a patch list file.

Use this in together with the `list_patches` macro to guarantee that all patches in a patch folder are included in the `patches` attribute.

For example:

verify_patches = "//patches:patches.list",


In your patches folder add a BUILD.bazel file containing.
load("@aspect_rules_js//npm:repositories.bzl", "list_patches")

list_patches(
    name = "patches",
    out = "patches.list",
)


Once you have created this file, you need to create an empty `patches.list` file before generating the first list. You can do this by running
touch patches/patches.list


Finally, write the patches file at least once to make sure all patches are listed. This can be done by running `bazel run //patches:patches_update`.

See the `list_patches` documentation for further info. NOTE: if you would like to customize the patches directory location, you can set a flag in the `.npmrc`. Here is an example of what this might look like
# Set the directory for pnpm when patching
# https://github.com/pnpm/pnpm/issues/6508#issuecomment-1537242124
patches-dir=bazel/js/patches
If you do this, you will have to update the `verify_patches` path to be this path instead of `//patches` like above. | `None` | diff --git a/npm/extensions.bzl b/npm/extensions.bzl index 9bf9f3ec7..59bc588f1 100644 --- a/npm/extensions.bzl +++ b/npm/extensions.bzl @@ -255,7 +255,7 @@ def _npm_translate_lock_attrs(): # Args not supported or unnecessary in bzlmod attrs.pop("repositories_bzl_filename") attrs.pop("exclude_package_contents") # Use tag classes only for MODULE.bazel - + # Replaced with tag in bzlmod attrs.pop("replace_packages") From b5a60881d42f42bdaf5ac3f35449c5bbcc7084e1 Mon Sep 17 00:00:00 2001 From: Simeon Visotskiy Date: Mon, 28 Jul 2025 19:54:36 +0300 Subject: [PATCH 04/12] docs --- docs/npm_translate_lock.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/npm_translate_lock.md b/docs/npm_translate_lock.md index 711fd3f9c..217c73e98 100644 --- a/docs/npm_translate_lock.md +++ b/docs/npm_translate_lock.md @@ -123,7 +123,7 @@ For more about how to use npm_translate_lock, read [pnpm and rules_js](/docs/pnp | lifecycle_hooks_execution_requirements | Execution requirements applied to the preinstall, install and postinstall lifecycle hooks on npm packages.

The execution requirements can be defined per package by package name or globally using "*".

Execution requirements are not additive. The most specific match wins.

Read more: [lifecycles](/docs/pnpm.md#lifecycles) | `{}` | | lifecycle_hooks_no_sandbox | If True, a "no-sandbox" execution requirement is added to all lifecycle hooks unless overridden by `lifecycle_hooks_execution_requirements`.

Equivalent to adding `"*": ["no-sandbox"]` to `lifecycle_hooks_execution_requirements`.

This defaults to True to limit the overhead of sandbox creation and copying the output TreeArtifacts out of the sandbox.

Read more: [lifecycles](/docs/pnpm.md#lifecycles) | `True` | | lifecycle_hooks_use_default_shell_env | The `use_default_shell_env` attribute of the lifecycle hooks actions on npm packages.

See [use_default_shell_env](https://bazel.build/rules/lib/builtins/actions#run.use_default_shell_env)

Note: [--incompatible_merge_fixed_and_default_shell_env](https://bazel.build/reference/command-line-reference#flag--incompatible_merge_fixed_and_default_shell_env) is often required and not enabled by default in Bazel < 7.0.0.

This defaults to False reduce the negative effects of `use_default_shell_env`. Requires bazel-lib >= 2.4.2.

Read more: [lifecycles](/docs/pnpm.md#lifecycles) | `{}` | -| replace_packages | A dict of package names to npm_package targets to link instead of the sources specified in the pnpm lock file for the corresponding packages.

**Note for bzlmod users:** Use the `replace_package` tag class instead:

npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm")
npm.replace_package(
    package = "chalk@5.3.0",
    replacement = "@chalk_501//:pkg",
)
npm.npm_translate_lock(name = "npm", pnpm_lock = "//:pnpm-lock.yaml")


The injected npm_package targets may optionally contribute transitive npm package dependencies on top of the transitive dependencies specified in the pnpm lock file for their respective packages, however, these transitive dependencies must not collide with pnpm lock specified transitive dependencies.

Any patches specified for the packages will be not applied to the injected npm_package targets. They will be applied, however, to the fetches sources for their respecitve packages so they can still be useful for patching the fetched `package.json` files, which are used to determine the generated bin entries for packages.

NB: lifecycle hooks and custom_postinstall scripts, if implicitly or explicitly enabled, will be run on the injected npm_package targets. These may be disabled explicitly using the `lifecycle_hooks` attribute. | `{}` | +| replace_packages | A dict of package names to npm_package targets to link instead of the sources specified in the pnpm lock file for the corresponding packages.

The injected npm_package targets may optionally contribute transitive npm package dependencies on top of the transitive dependencies specified in the pnpm lock file for their respective packages, however, these transitive dependencies must not collide with pnpm lock specified transitive dependencies.

Any patches specified for the packages will be not applied to the injected npm_package targets. They will be applied, however, to the fetches sources for their respecitve packages so they can still be useful for patching the fetched `package.json` files, which are used to determine the generated bin entries for packages.

NB: lifecycle hooks and custom_postinstall scripts, if implicitly or explicitly enabled, will be run on the injected npm_package targets. These may be disabled explicitly using the `lifecycle_hooks` attribute. | `{}` | | bins | Binary files to create in `node_modules/.bin` for packages in this lock file.

For a given package, this is typically derived from the "bin" attribute in the package.json file of that package.

For example:

bins = {
    "@foo/bar": {
        "foo": "./foo.js",
        "bar": "./bar.js"
    },
}


Dicts of bins not additive. The most specific match wins.

In the future, this field may be automatically populated from information in the pnpm lock file. That feature is currently blocked on https://github.com/pnpm/pnpm/issues/5131.

Note: Bzlmod users must use an alternative syntax due to module extensions not supporting dict-of-dict attributes:

bins = {
    "@foo/bar": [
        "foo=./foo.js",
        "bar=./bar.js"
    ],
}
| `{}` | | verify_node_modules_ignored | node_modules folders in the source tree should be ignored by Bazel.

This points to a `.bazelignore` file to verify that all nested node_modules directories pnpm will create are listed.

See https://github.com/bazelbuild/bazel/issues/8106 | `None` | | verify_patches | Label to a patch list file.

Use this in together with the `list_patches` macro to guarantee that all patches in a patch folder are included in the `patches` attribute.

For example:

verify_patches = "//patches:patches.list",


In your patches folder add a BUILD.bazel file containing.
load("@aspect_rules_js//npm:repositories.bzl", "list_patches")

list_patches(
    name = "patches",
    out = "patches.list",
)


Once you have created this file, you need to create an empty `patches.list` file before generating the first list. You can do this by running
touch patches/patches.list


Finally, write the patches file at least once to make sure all patches are listed. This can be done by running `bazel run //patches:patches_update`.

See the `list_patches` documentation for further info. NOTE: if you would like to customize the patches directory location, you can set a flag in the `.npmrc`. Here is an example of what this might look like
# Set the directory for pnpm when patching
# https://github.com/pnpm/pnpm/issues/6508#issuecomment-1537242124
patches-dir=bazel/js/patches
If you do this, you will have to update the `verify_patches` path to be this path instead of `//patches` like above. | `None` | From 685b02a5b80d2658853321697df6bde25cd909d3 Mon Sep 17 00:00:00 2001 From: Simeon Visotskiy Date: Tue, 29 Jul 2025 16:45:55 +0300 Subject: [PATCH 05/12] PR comments --- docs/npm_translate_lock.md | 2 +- .../MODULE.bazel | 13 +++++- .../lodash_module/BUILD.bazel | 11 +++++ .../lodash_module/MODULE.bazel | 13 ++++++ .../lodash_module/lodash.js | 7 +++ .../lodash_module/package.json | 6 +++ .../main.js | 42 ++++++++++++++--- .../package.json | 3 +- .../pnpm-lock.yaml | 8 ++++ .../snapshots/bzlmod/npm_defs.bzl | 5 ++ npm/extensions.bzl | 46 ++++++++++++++++--- npm/private/npm_translate_lock.bzl | 11 +++++ 12 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 e2e/npm_translate_lock_replace_packages/lodash_module/BUILD.bazel create mode 100644 e2e/npm_translate_lock_replace_packages/lodash_module/MODULE.bazel create mode 100644 e2e/npm_translate_lock_replace_packages/lodash_module/lodash.js create mode 100644 e2e/npm_translate_lock_replace_packages/lodash_module/package.json diff --git a/docs/npm_translate_lock.md b/docs/npm_translate_lock.md index 217c73e98..f222a9fdc 100644 --- a/docs/npm_translate_lock.md +++ b/docs/npm_translate_lock.md @@ -123,7 +123,7 @@ For more about how to use npm_translate_lock, read [pnpm and rules_js](/docs/pnp | lifecycle_hooks_execution_requirements | Execution requirements applied to the preinstall, install and postinstall lifecycle hooks on npm packages.

The execution requirements can be defined per package by package name or globally using "*".

Execution requirements are not additive. The most specific match wins.

Read more: [lifecycles](/docs/pnpm.md#lifecycles) | `{}` | | lifecycle_hooks_no_sandbox | If True, a "no-sandbox" execution requirement is added to all lifecycle hooks unless overridden by `lifecycle_hooks_execution_requirements`.

Equivalent to adding `"*": ["no-sandbox"]` to `lifecycle_hooks_execution_requirements`.

This defaults to True to limit the overhead of sandbox creation and copying the output TreeArtifacts out of the sandbox.

Read more: [lifecycles](/docs/pnpm.md#lifecycles) | `True` | | lifecycle_hooks_use_default_shell_env | The `use_default_shell_env` attribute of the lifecycle hooks actions on npm packages.

See [use_default_shell_env](https://bazel.build/rules/lib/builtins/actions#run.use_default_shell_env)

Note: [--incompatible_merge_fixed_and_default_shell_env](https://bazel.build/reference/command-line-reference#flag--incompatible_merge_fixed_and_default_shell_env) is often required and not enabled by default in Bazel < 7.0.0.

This defaults to False reduce the negative effects of `use_default_shell_env`. Requires bazel-lib >= 2.4.2.

Read more: [lifecycles](/docs/pnpm.md#lifecycles) | `{}` | -| replace_packages | A dict of package names to npm_package targets to link instead of the sources specified in the pnpm lock file for the corresponding packages.

The injected npm_package targets may optionally contribute transitive npm package dependencies on top of the transitive dependencies specified in the pnpm lock file for their respective packages, however, these transitive dependencies must not collide with pnpm lock specified transitive dependencies.

Any patches specified for the packages will be not applied to the injected npm_package targets. They will be applied, however, to the fetches sources for their respecitve packages so they can still be useful for patching the fetched `package.json` files, which are used to determine the generated bin entries for packages.

NB: lifecycle hooks and custom_postinstall scripts, if implicitly or explicitly enabled, will be run on the injected npm_package targets. These may be disabled explicitly using the `lifecycle_hooks` attribute. | `{}` | +| replace_packages | A dict of package names to npm_package targets to link instead of the sources specified in the pnpm lock file for the corresponding packages.

**Note for bzlmod users:** Use the `npm_replace_package` tag class instead:

npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm")
npm.npm_replace_package(
    package = "chalk@5.3.0",
    replacement = "@chalk_501//:pkg",
)
npm.npm_translate_lock(name = "npm", pnpm_lock = "//:pnpm-lock.yaml")


The injected npm_package targets may optionally contribute transitive npm package dependencies on top of the transitive dependencies specified in the pnpm lock file for their respective packages, however, these transitive dependencies must not collide with pnpm lock specified transitive dependencies.

Any patches specified for the packages will be not applied to the injected npm_package targets. They will be applied, however, to the fetches sources for their respecitve packages so they can still be useful for patching the fetched `package.json` files, which are used to determine the generated bin entries for packages.

NB: lifecycle hooks and custom_postinstall scripts, if implicitly or explicitly enabled, will be run on the injected npm_package targets. These may be disabled explicitly using the `lifecycle_hooks` attribute. | `{}` | | bins | Binary files to create in `node_modules/.bin` for packages in this lock file.

For a given package, this is typically derived from the "bin" attribute in the package.json file of that package.

For example:

bins = {
    "@foo/bar": {
        "foo": "./foo.js",
        "bar": "./bar.js"
    },
}


Dicts of bins not additive. The most specific match wins.

In the future, this field may be automatically populated from information in the pnpm lock file. That feature is currently blocked on https://github.com/pnpm/pnpm/issues/5131.

Note: Bzlmod users must use an alternative syntax due to module extensions not supporting dict-of-dict attributes:

bins = {
    "@foo/bar": [
        "foo=./foo.js",
        "bar=./bar.js"
    ],
}
| `{}` | | verify_node_modules_ignored | node_modules folders in the source tree should be ignored by Bazel.

This points to a `.bazelignore` file to verify that all nested node_modules directories pnpm will create are listed.

See https://github.com/bazelbuild/bazel/issues/8106 | `None` | | verify_patches | Label to a patch list file.

Use this in together with the `list_patches` macro to guarantee that all patches in a patch folder are included in the `patches` attribute.

For example:

verify_patches = "//patches:patches.list",


In your patches folder add a BUILD.bazel file containing.
load("@aspect_rules_js//npm:repositories.bzl", "list_patches")

list_patches(
    name = "patches",
    out = "patches.list",
)


Once you have created this file, you need to create an empty `patches.list` file before generating the first list. You can do this by running
touch patches/patches.list


Finally, write the patches file at least once to make sure all patches are listed. This can be done by running `bazel run //patches:patches_update`.

See the `list_patches` documentation for further info. NOTE: if you would like to customize the patches directory location, you can set a flag in the `.npmrc`. Here is an example of what this might look like
# Set the directory for pnpm when patching
# https://github.com/pnpm/pnpm/issues/6508#issuecomment-1537242124
patches-dir=bazel/js/patches
If you do this, you will have to update the `verify_patches` path to be this path instead of `//patches` like above. | `None` | diff --git a/e2e/npm_translate_lock_replace_packages/MODULE.bazel b/e2e/npm_translate_lock_replace_packages/MODULE.bazel index 8a0cf7ba1..5e388ecc5 100644 --- a/e2e/npm_translate_lock_replace_packages/MODULE.bazel +++ b/e2e/npm_translate_lock_replace_packages/MODULE.bazel @@ -19,7 +19,7 @@ npm = use_extension( "npm", dev_dependency = True, ) -npm.replace_package( +npm.npm_replace_package( package = "chalk@5.3.0", replacement = "@chalk_501//:pkg", ) @@ -40,4 +40,15 @@ http_archive( url = "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", ) +bazel_dep(name = "lodash_replacement_module", version = "0.0.0") +local_path_override( + module_name = "lodash_replacement_module", + path = "lodash_module", +) + +npm.npm_replace_package( + package = "lodash@4.17.21", + replacement = "@lodash_replacement_module//:lodash_replacement", +) + use_repo(npm, "npm") diff --git a/e2e/npm_translate_lock_replace_packages/lodash_module/BUILD.bazel b/e2e/npm_translate_lock_replace_packages/lodash_module/BUILD.bazel new file mode 100644 index 000000000..414463199 --- /dev/null +++ b/e2e/npm_translate_lock_replace_packages/lodash_module/BUILD.bazel @@ -0,0 +1,11 @@ +load("@aspect_rules_js//npm:defs.bzl", "npm_package") + +# This module provides a lodash replacement package +npm_package( + name = "lodash_replacement", + srcs = [ + "lodash.js", + "package.json", + ], + visibility = ["//visibility:public"], +) \ No newline at end of file diff --git a/e2e/npm_translate_lock_replace_packages/lodash_module/MODULE.bazel b/e2e/npm_translate_lock_replace_packages/lodash_module/MODULE.bazel new file mode 100644 index 000000000..959ad7f36 --- /dev/null +++ b/e2e/npm_translate_lock_replace_packages/lodash_module/MODULE.bazel @@ -0,0 +1,13 @@ +module( + name = "lodash_replacement_module", + version = "0.0.0", + compatibility_level = 1, +) + +bazel_dep(name = "aspect_rules_js", version = "0.0.0") + +local_path_override( + module_name = "aspect_rules_js", + path = "../../..", +) + diff --git a/e2e/npm_translate_lock_replace_packages/lodash_module/lodash.js b/e2e/npm_translate_lock_replace_packages/lodash_module/lodash.js new file mode 100644 index 000000000..d460f9670 --- /dev/null +++ b/e2e/npm_translate_lock_replace_packages/lodash_module/lodash.js @@ -0,0 +1,7 @@ +// Simple lodash replacement - version 4.17.20 equivalent +export default { + uniq: function(array) { + return [...new Set(array)]; + }, + version: "4.17.20" +}; \ No newline at end of file diff --git a/e2e/npm_translate_lock_replace_packages/lodash_module/package.json b/e2e/npm_translate_lock_replace_packages/lodash_module/package.json new file mode 100644 index 000000000..583885f5a --- /dev/null +++ b/e2e/npm_translate_lock_replace_packages/lodash_module/package.json @@ -0,0 +1,6 @@ +{ + "name": "lodash", + "version": "4.17.20", + "type": "module", + "main": "lodash.js" +} \ No newline at end of file diff --git a/e2e/npm_translate_lock_replace_packages/main.js b/e2e/npm_translate_lock_replace_packages/main.js index aaf81a778..482f25042 100644 --- a/e2e/npm_translate_lock_replace_packages/main.js +++ b/e2e/npm_translate_lock_replace_packages/main.js @@ -1,21 +1,49 @@ import chalk from 'chalk' +import lodash from 'lodash' import { readFileSync } from 'fs' -const packageJsonDep = JSON.parse(readFileSync('./package.json', 'utf-8')) +// Test chalk replacement +const chalkPackageJsonDep = JSON.parse(readFileSync('./package.json', 'utf-8')) .dependencies['chalk'] -if (packageJsonDep !== '5.3.0') { +if (chalkPackageJsonDep !== '5.3.0') { throw new Error( - `Expected chalk version 5.3.0 declared in package.json, but got ${pkgDep}` + `Expected chalk version 5.3.0 declared in package.json, but got ${chalkPackageJsonDep}` ) } -const actualDep = JSON.parse( +const chalkActualDep = JSON.parse( readFileSync('./node_modules/chalk/package.json', 'utf-8') ).version -if (actualDep !== '5.0.1') { +if (chalkActualDep !== '5.0.1') { throw new Error( - `Expected chalk to be replaced with version 5.0.1, but got ${actualDep}` + `Expected chalk to be replaced with version 5.0.1, but got ${chalkActualDep}` ) } -console.log(chalk.blue(`Hello world! The meaning of life is... 42`)) +// Test lodash replacement +const lodashPackageJsonDep = JSON.parse(readFileSync('./package.json', 'utf-8')) + .dependencies['lodash'] +if (lodashPackageJsonDep !== '4.17.21') { + throw new Error( + `Expected lodash version 4.17.21 declared in package.json, but got ${lodashPackageJsonDep}` + ) +} + +const lodashActualDep = JSON.parse( + readFileSync('./node_modules/lodash/package.json', 'utf-8') +).version +if (lodashActualDep !== '4.17.20') { + throw new Error( + `Expected lodash to be replaced with version 4.17.20, but got ${lodashActualDep}` + ) +} + +// Test that both packages work functionally +const testArray = [1, 2, 2, 3, 3, 3] +const uniqueArray = lodash.uniq(testArray) +if (uniqueArray.length !== 3) { + throw new Error(`Expected lodash.uniq to work, but got array length ${uniqueArray.length}`) +} + +console.log(chalk.blue(`Hello world! Multiple package replacements work! 🎉`)) +console.log(chalk.green(`Chalk ${chalkActualDep} and Lodash ${lodashActualDep} both replaced successfully`)) diff --git a/e2e/npm_translate_lock_replace_packages/package.json b/e2e/npm_translate_lock_replace_packages/package.json index 1d87a0686..47547fe41 100644 --- a/e2e/npm_translate_lock_replace_packages/package.json +++ b/e2e/npm_translate_lock_replace_packages/package.json @@ -2,7 +2,8 @@ "private": true, "type": "module", "dependencies": { - "chalk": "5.3.0" + "chalk": "5.3.0", + "lodash": "4.17.21" }, "pnpm": { "onlyBuiltDependencies": [] diff --git a/e2e/npm_translate_lock_replace_packages/pnpm-lock.yaml b/e2e/npm_translate_lock_replace_packages/pnpm-lock.yaml index 41f2569d3..272e4b71d 100644 --- a/e2e/npm_translate_lock_replace_packages/pnpm-lock.yaml +++ b/e2e/npm_translate_lock_replace_packages/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: chalk: specifier: 5.3.0 version: 5.3.0 + lodash: + specifier: 4.17.21 + version: 4.17.21 packages: @@ -18,6 +21,11 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + snapshots: chalk@5.3.0: {} + + lodash@4.17.21: {} diff --git a/e2e/npm_translate_lock_replace_packages/snapshots/bzlmod/npm_defs.bzl b/e2e/npm_translate_lock_replace_packages/snapshots/bzlmod/npm_defs.bzl index e717e571b..bc9391d0e 100644 --- a/e2e/npm_translate_lock_replace_packages/snapshots/bzlmod/npm_defs.bzl +++ b/e2e/npm_translate_lock_replace_packages/snapshots/bzlmod/npm_defs.bzl @@ -1,6 +1,7 @@ """@generated by npm_translate_lock(name = "npm", pnpm_lock = "@@//:pnpm-lock.yaml")""" load("@@aspect_rules_js~~npm~npm__chalk__5.3.0__links//:defs.bzl", link_0 = "npm_link_imported_package_store", store_0 = "npm_imported_package_store") +load("@@aspect_rules_js~~npm~npm__lodash__4.17.21__links//:defs.bzl", link_1 = "npm_link_imported_package_store", store_1 = "npm_imported_package_store") # buildifier: disable=bzl-visibility load("@aspect_rules_js//js:defs.bzl", _js_library = "js_library") @@ -29,10 +30,13 @@ def npm_link_all_packages(name = "node_modules", imported_links = []): if is_root: store_0(name) + store_1(name) if link: if bazel_package == "": link_0("{}/chalk".format(name), link_root_name = name, link_alias = "chalk") link_targets.append(":{}/chalk".format(name)) + link_1("{}/lodash".format(name), link_root_name = name, link_alias = "lodash") + link_targets.append(":{}/lodash".format(name)) for scope, scoped_targets in scope_targets.items(): _js_library( @@ -59,4 +63,5 @@ def npm_link_targets(name = "node_modules", package = None): if link: if bazel_package == "": link_targets.append(":{}/chalk".format(name)) + link_targets.append(":{}/lodash".format(name)) return link_targets diff --git a/npm/extensions.bzl b/npm/extensions.bzl index 59bc588f1..dfa7fac8c 100644 --- a/npm/extensions.bzl +++ b/npm/extensions.bzl @@ -31,7 +31,7 @@ def _npm_extension_impl(module_ctx): for mod in module_ctx.modules: replace_packages = {} - for attr in mod.tags.replace_package: + for attr in mod.tags.npm_replace_package: if attr.package in replace_packages: fail("Each package can have only one replacement") replace_packages[attr.package] = "@@{}//{}:{}".format(attr.replacement.repo_name, attr.replacement.package, attr.replacement.name) @@ -78,6 +78,17 @@ def _build_exclude_package_contents_config(module_ctx): return exclusions def _npm_translate_lock_bzlmod(attr, exclude_package_contents_config, replace_packages): + # TODO(3.0): remove this warning when replace_packages attribute is removed + if attr.replace_packages: + # buildifier: disable=print + print("WARNING: replace_packages attribute is deprecated in bzlmod. Use npm.npm_replace_package() tag instead. This attribute will be removed in rules_js 3.0.") + + # Merge replace_packages attribute with replace_package tags + for package, replacement in attr.replace_packages.items(): + if package in replace_packages: + fail("Package replacement conflict: {} specified in both replace_packages attribute and replace_package tag".format(package)) + replace_packages[package] = replacement + npm_translate_lock_rule( name = attr.name, bins = attr.bins, @@ -256,9 +267,8 @@ def _npm_translate_lock_attrs(): attrs.pop("repositories_bzl_filename") attrs.pop("exclude_package_contents") # Use tag classes only for MODULE.bazel - # Replaced with tag in bzlmod - attrs.pop("replace_packages") - + # TODO(3.0): remove replace_packages attribute in favor of npm_replace_package tag + # attrs.pop("replace_packages") return attrs def _npm_import_attrs(): @@ -294,8 +304,14 @@ def _npm_exclude_package_contents_attrs(): } _REPLACE_PACKAGE_ATTRS = { - "package": attr.string(), - "replacement": attr.label(), + "package": attr.string( + doc = "The package name and version to replace (e.g., 'chalk@5.3.0')", + mandatory = True, + ), + "replacement": attr.label( + doc = "The target to use as replacement for this package", + mandatory = True, + ), } npm = module_extension( @@ -304,7 +320,23 @@ npm = module_extension( "npm_translate_lock": tag_class(attrs = _npm_translate_lock_attrs()), "npm_import": tag_class(attrs = _npm_import_attrs()), "npm_exclude_package_contents": tag_class(attrs = _npm_exclude_package_contents_attrs()), - "replace_package": tag_class(attrs = _REPLACE_PACKAGE_ATTRS), + "npm_replace_package": tag_class( + attrs = _REPLACE_PACKAGE_ATTRS, + doc = """Replace a package with a custom target. + +This allows you to replace packages declared in package.json with custom implementations. +Multiple npm_replace_package tags can be used to replace different packages. + +Example: +```starlark +npm.npm_replace_package( + package = "chalk@5.3.0", + replacement = "@chalk_501//:pkg", +) +``` + +This is the bzlmod equivalent of the replace_packages attribute in WORKSPACE mode.""", + ), }, ) diff --git a/npm/private/npm_translate_lock.bzl b/npm/private/npm_translate_lock.bzl index 92d89af46..2718a8206 100644 --- a/npm/private/npm_translate_lock.bzl +++ b/npm/private/npm_translate_lock.bzl @@ -447,6 +447,17 @@ def npm_translate_lock( replace_packages: A dict of package names to npm_package targets to link instead of the sources specified in the pnpm lock file for the corresponding packages. + **Note for bzlmod users:** Use the `npm_replace_package` tag class instead: + + ```starlark + npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm") + npm.npm_replace_package( + package = "chalk@5.3.0", + replacement = "@chalk_501//:pkg", + ) + npm.npm_translate_lock(name = "npm", pnpm_lock = "//:pnpm-lock.yaml") + ``` + The injected npm_package targets may optionally contribute transitive npm package dependencies on top of the transitive dependencies specified in the pnpm lock file for their respective packages, however, these transitive dependencies must not collide with pnpm lock specified transitive dependencies. From 120d95a625a070d9450eede027a2e16ecfda2ccd Mon Sep 17 00:00:00 2001 From: Mihail Vratchanski Date: Tue, 5 Aug 2025 20:48:29 +0300 Subject: [PATCH 06/12] format --- e2e/npm_translate_lock_replace_packages/MODULE.bazel | 1 - .../lodash_module/BUILD.bazel | 2 +- .../lodash_module/MODULE.bazel | 4 +--- npm/extensions.bzl | 4 ++-- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/e2e/npm_translate_lock_replace_packages/MODULE.bazel b/e2e/npm_translate_lock_replace_packages/MODULE.bazel index 5e388ecc5..dfc64acba 100644 --- a/e2e/npm_translate_lock_replace_packages/MODULE.bazel +++ b/e2e/npm_translate_lock_replace_packages/MODULE.bazel @@ -50,5 +50,4 @@ npm.npm_replace_package( package = "lodash@4.17.21", replacement = "@lodash_replacement_module//:lodash_replacement", ) - use_repo(npm, "npm") diff --git a/e2e/npm_translate_lock_replace_packages/lodash_module/BUILD.bazel b/e2e/npm_translate_lock_replace_packages/lodash_module/BUILD.bazel index 414463199..249499a7c 100644 --- a/e2e/npm_translate_lock_replace_packages/lodash_module/BUILD.bazel +++ b/e2e/npm_translate_lock_replace_packages/lodash_module/BUILD.bazel @@ -8,4 +8,4 @@ npm_package( "package.json", ], visibility = ["//visibility:public"], -) \ No newline at end of file +) diff --git a/e2e/npm_translate_lock_replace_packages/lodash_module/MODULE.bazel b/e2e/npm_translate_lock_replace_packages/lodash_module/MODULE.bazel index 959ad7f36..03b767bcb 100644 --- a/e2e/npm_translate_lock_replace_packages/lodash_module/MODULE.bazel +++ b/e2e/npm_translate_lock_replace_packages/lodash_module/MODULE.bazel @@ -1,13 +1,11 @@ module( - name = "lodash_replacement_module", + name = "lodash_replacement_module", version = "0.0.0", compatibility_level = 1, ) bazel_dep(name = "aspect_rules_js", version = "0.0.0") - local_path_override( module_name = "aspect_rules_js", path = "../../..", ) - diff --git a/npm/extensions.bzl b/npm/extensions.bzl index dfa7fac8c..4770c9541 100644 --- a/npm/extensions.bzl +++ b/npm/extensions.bzl @@ -82,13 +82,13 @@ def _npm_translate_lock_bzlmod(attr, exclude_package_contents_config, replace_pa if attr.replace_packages: # buildifier: disable=print print("WARNING: replace_packages attribute is deprecated in bzlmod. Use npm.npm_replace_package() tag instead. This attribute will be removed in rules_js 3.0.") - + # Merge replace_packages attribute with replace_package tags for package, replacement in attr.replace_packages.items(): if package in replace_packages: fail("Package replacement conflict: {} specified in both replace_packages attribute and replace_package tag".format(package)) replace_packages[package] = replacement - + npm_translate_lock_rule( name = attr.name, bins = attr.bins, From 07af84df9cee0dca46eaefa978231d8e4dda370a Mon Sep 17 00:00:00 2001 From: Mihail Vratchanski Date: Wed, 6 Aug 2025 19:29:25 +0300 Subject: [PATCH 07/12] PR comments --- npm/extensions.bzl | 6 ++---- npm/private/npm_translate_lock.bzl | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/npm/extensions.bzl b/npm/extensions.bzl index 4770c9541..fc4d45673 100644 --- a/npm/extensions.bzl +++ b/npm/extensions.bzl @@ -265,7 +265,7 @@ def _npm_translate_lock_attrs(): # Args not supported or unnecessary in bzlmod attrs.pop("repositories_bzl_filename") - attrs.pop("exclude_package_contents") # Use tag classes only for MODULE.bazel + attrs.pop("exclude_package_contents") # Use npm_exclude_package_contents tag instead # TODO(3.0): remove replace_packages attribute in favor of npm_replace_package tag # attrs.pop("replace_packages") @@ -333,9 +333,7 @@ npm.npm_replace_package( package = "chalk@5.3.0", replacement = "@chalk_501//:pkg", ) -``` - -This is the bzlmod equivalent of the replace_packages attribute in WORKSPACE mode.""", +```""", ), }, ) diff --git a/npm/private/npm_translate_lock.bzl b/npm/private/npm_translate_lock.bzl index 2718a8206..02cd942d3 100644 --- a/npm/private/npm_translate_lock.bzl +++ b/npm/private/npm_translate_lock.bzl @@ -445,7 +445,7 @@ def npm_translate_lock( Read more: [lifecycles](/docs/pnpm.md#lifecycles) - replace_packages: A dict of package names to npm_package targets to link instead of the sources specified in the pnpm lock file for the corresponding packages. + replace_packages: [DEPRECATED - Use npm.npm_replace_package() tag in MODULE.bazel instead] A dict of package names to npm_package targets to link instead of the sources specified in the pnpm lock file for the corresponding packages. **Note for bzlmod users:** Use the `npm_replace_package` tag class instead: From def32c275680f668d5f75eeadcf85b625a2a822a Mon Sep 17 00:00:00 2001 From: Mihail Vratchanski Date: Wed, 6 Aug 2025 19:32:15 +0300 Subject: [PATCH 08/12] Update docs --- docs/npm_translate_lock.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/npm_translate_lock.md b/docs/npm_translate_lock.md index f222a9fdc..2e299ea9a 100644 --- a/docs/npm_translate_lock.md +++ b/docs/npm_translate_lock.md @@ -123,7 +123,7 @@ For more about how to use npm_translate_lock, read [pnpm and rules_js](/docs/pnp | lifecycle_hooks_execution_requirements | Execution requirements applied to the preinstall, install and postinstall lifecycle hooks on npm packages.

The execution requirements can be defined per package by package name or globally using "*".

Execution requirements are not additive. The most specific match wins.

Read more: [lifecycles](/docs/pnpm.md#lifecycles) | `{}` | | lifecycle_hooks_no_sandbox | If True, a "no-sandbox" execution requirement is added to all lifecycle hooks unless overridden by `lifecycle_hooks_execution_requirements`.

Equivalent to adding `"*": ["no-sandbox"]` to `lifecycle_hooks_execution_requirements`.

This defaults to True to limit the overhead of sandbox creation and copying the output TreeArtifacts out of the sandbox.

Read more: [lifecycles](/docs/pnpm.md#lifecycles) | `True` | | lifecycle_hooks_use_default_shell_env | The `use_default_shell_env` attribute of the lifecycle hooks actions on npm packages.

See [use_default_shell_env](https://bazel.build/rules/lib/builtins/actions#run.use_default_shell_env)

Note: [--incompatible_merge_fixed_and_default_shell_env](https://bazel.build/reference/command-line-reference#flag--incompatible_merge_fixed_and_default_shell_env) is often required and not enabled by default in Bazel < 7.0.0.

This defaults to False reduce the negative effects of `use_default_shell_env`. Requires bazel-lib >= 2.4.2.

Read more: [lifecycles](/docs/pnpm.md#lifecycles) | `{}` | -| replace_packages | A dict of package names to npm_package targets to link instead of the sources specified in the pnpm lock file for the corresponding packages.

**Note for bzlmod users:** Use the `npm_replace_package` tag class instead:

npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm")
npm.npm_replace_package(
    package = "chalk@5.3.0",
    replacement = "@chalk_501//:pkg",
)
npm.npm_translate_lock(name = "npm", pnpm_lock = "//:pnpm-lock.yaml")


The injected npm_package targets may optionally contribute transitive npm package dependencies on top of the transitive dependencies specified in the pnpm lock file for their respective packages, however, these transitive dependencies must not collide with pnpm lock specified transitive dependencies.

Any patches specified for the packages will be not applied to the injected npm_package targets. They will be applied, however, to the fetches sources for their respecitve packages so they can still be useful for patching the fetched `package.json` files, which are used to determine the generated bin entries for packages.

NB: lifecycle hooks and custom_postinstall scripts, if implicitly or explicitly enabled, will be run on the injected npm_package targets. These may be disabled explicitly using the `lifecycle_hooks` attribute. | `{}` | +| replace_packages | [DEPRECATED - Use npm.npm_replace_package() tag in MODULE.bazel instead] A dict of package names to npm_package targets to link instead of the sources specified in the pnpm lock file for the corresponding packages.

**Note for bzlmod users:** Use the `npm_replace_package` tag class instead:

npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm")
npm.npm_replace_package(
    package = "chalk@5.3.0",
    replacement = "@chalk_501//:pkg",
)
npm.npm_translate_lock(name = "npm", pnpm_lock = "//:pnpm-lock.yaml")


The injected npm_package targets may optionally contribute transitive npm package dependencies on top of the transitive dependencies specified in the pnpm lock file for their respective packages, however, these transitive dependencies must not collide with pnpm lock specified transitive dependencies.

Any patches specified for the packages will be not applied to the injected npm_package targets. They will be applied, however, to the fetches sources for their respecitve packages so they can still be useful for patching the fetched `package.json` files, which are used to determine the generated bin entries for packages.

NB: lifecycle hooks and custom_postinstall scripts, if implicitly or explicitly enabled, will be run on the injected npm_package targets. These may be disabled explicitly using the `lifecycle_hooks` attribute. | `{}` | | bins | Binary files to create in `node_modules/.bin` for packages in this lock file.

For a given package, this is typically derived from the "bin" attribute in the package.json file of that package.

For example:

bins = {
    "@foo/bar": {
        "foo": "./foo.js",
        "bar": "./bar.js"
    },
}


Dicts of bins not additive. The most specific match wins.

In the future, this field may be automatically populated from information in the pnpm lock file. That feature is currently blocked on https://github.com/pnpm/pnpm/issues/5131.

Note: Bzlmod users must use an alternative syntax due to module extensions not supporting dict-of-dict attributes:

bins = {
    "@foo/bar": [
        "foo=./foo.js",
        "bar=./bar.js"
    ],
}
| `{}` | | verify_node_modules_ignored | node_modules folders in the source tree should be ignored by Bazel.

This points to a `.bazelignore` file to verify that all nested node_modules directories pnpm will create are listed.

See https://github.com/bazelbuild/bazel/issues/8106 | `None` | | verify_patches | Label to a patch list file.

Use this in together with the `list_patches` macro to guarantee that all patches in a patch folder are included in the `patches` attribute.

For example:

verify_patches = "//patches:patches.list",


In your patches folder add a BUILD.bazel file containing.
load("@aspect_rules_js//npm:repositories.bzl", "list_patches")

list_patches(
    name = "patches",
    out = "patches.list",
)


Once you have created this file, you need to create an empty `patches.list` file before generating the first list. You can do this by running
touch patches/patches.list


Finally, write the patches file at least once to make sure all patches are listed. This can be done by running `bazel run //patches:patches_update`.

See the `list_patches` documentation for further info. NOTE: if you would like to customize the patches directory location, you can set a flag in the `.npmrc`. Here is an example of what this might look like
# Set the directory for pnpm when patching
# https://github.com/pnpm/pnpm/issues/6508#issuecomment-1537242124
patches-dir=bazel/js/patches
If you do this, you will have to update the `verify_patches` path to be this path instead of `//patches` like above. | `None` | From 3a1e32566c74fb06b1360af6f2411a66ac751648 Mon Sep 17 00:00:00 2001 From: Mihail Vratchanski Date: Mon, 11 Aug 2025 20:00:19 +0300 Subject: [PATCH 09/12] PR comments --- npm/extensions.bzl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/npm/extensions.bzl b/npm/extensions.bzl index fc4d45673..4210dc455 100644 --- a/npm/extensions.bzl +++ b/npm/extensions.bzl @@ -29,13 +29,16 @@ def _npm_extension_impl(module_ctx): # Collect all exclude_package_contents tags and build exclusion dictionary exclude_package_contents_config = _build_exclude_package_contents_config(module_ctx) + # Collect all package replacements across all modules + replace_packages = {} for mod in module_ctx.modules: - replace_packages = {} for attr in mod.tags.npm_replace_package: if attr.package in replace_packages: - fail("Each package can have only one replacement") + fail("Package '{}' already has a replacement defined in another module".format(attr.package)) replace_packages[attr.package] = "@@{}//{}:{}".format(attr.replacement.repo_name, attr.replacement.package, attr.replacement.name) + # Process npm_translate_lock and npm_import tags + for mod in module_ctx.modules: for attr in mod.tags.npm_translate_lock: _npm_translate_lock_bzlmod(attr, exclude_package_contents_config, replace_packages) From e5535bbe1b1098ff7a9ffa73a5479461a935e312 Mon Sep 17 00:00:00 2001 From: Mihail Vratchanski Date: Mon, 11 Aug 2025 21:20:16 +0300 Subject: [PATCH 10/12] Add test for submodule depending on another submodule --- .../.bazelignore | 1 + .../MODULE.bazel | 38 +++++++++---------- .../utils_module/.bazelignore | 1 + .../utils_module/.npmrc | 4 ++ .../utils_module/BUILD.bazel | 21 ++++++++++ .../utils_module/MODULE.bazel | 37 ++++++++++++++++++ .../utils_module/package.json | 12 ++++++ .../utils_module/pnpm-lock.yaml | 22 +++++++++++ .../utils_module/pnpm-workspace.yaml | 2 + .../utils_module/test.mjs | 7 ++++ .../utils_module/utils.mjs | 14 +++++++ 11 files changed, 140 insertions(+), 19 deletions(-) create mode 100644 e2e/npm_translate_lock_replace_packages/utils_module/.bazelignore create mode 100644 e2e/npm_translate_lock_replace_packages/utils_module/.npmrc create mode 100644 e2e/npm_translate_lock_replace_packages/utils_module/BUILD.bazel create mode 100644 e2e/npm_translate_lock_replace_packages/utils_module/MODULE.bazel create mode 100644 e2e/npm_translate_lock_replace_packages/utils_module/package.json create mode 100644 e2e/npm_translate_lock_replace_packages/utils_module/pnpm-lock.yaml create mode 100644 e2e/npm_translate_lock_replace_packages/utils_module/pnpm-workspace.yaml create mode 100644 e2e/npm_translate_lock_replace_packages/utils_module/test.mjs create mode 100644 e2e/npm_translate_lock_replace_packages/utils_module/utils.mjs diff --git a/e2e/npm_translate_lock_replace_packages/.bazelignore b/e2e/npm_translate_lock_replace_packages/.bazelignore index 3c3629e64..b578137e9 100644 --- a/e2e/npm_translate_lock_replace_packages/.bazelignore +++ b/e2e/npm_translate_lock_replace_packages/.bazelignore @@ -1 +1,2 @@ node_modules +utils_module diff --git a/e2e/npm_translate_lock_replace_packages/MODULE.bazel b/e2e/npm_translate_lock_replace_packages/MODULE.bazel index dfc64acba..75590ed62 100644 --- a/e2e/npm_translate_lock_replace_packages/MODULE.bazel +++ b/e2e/npm_translate_lock_replace_packages/MODULE.bazel @@ -14,20 +14,9 @@ local_path_override( path = "../..", ) -npm = use_extension( - "@aspect_rules_js//npm:extensions.bzl", - "npm", - dev_dependency = True, -) -npm.npm_replace_package( - package = "chalk@5.3.0", - replacement = "@chalk_501//:pkg", -) -npm.npm_translate_lock( - name = "npm", - npmrc = "//:.npmrc", - pnpm_lock = "//:pnpm-lock.yaml", - verify_node_modules_ignored = "//:.bazelignore", +local_path_override( + module_name = "lodash_replacement_module", + path = "lodash_module", ) http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") @@ -40,14 +29,25 @@ http_archive( url = "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", ) -bazel_dep(name = "lodash_replacement_module", version = "0.0.0") +bazel_dep(name = "utils_replacement_module", version = "0.0.0") local_path_override( - module_name = "lodash_replacement_module", - path = "lodash_module", + module_name = "utils_replacement_module", + path = "utils_module", ) +npm = use_extension( + "@aspect_rules_js//npm:extensions.bzl", + "npm", + dev_dependency = True, +) +npm.npm_translate_lock( + name = "npm", + npmrc = "//:.npmrc", + pnpm_lock = "//:pnpm-lock.yaml", + verify_node_modules_ignored = "//:.bazelignore", +) npm.npm_replace_package( - package = "lodash@4.17.21", - replacement = "@lodash_replacement_module//:lodash_replacement", + package = "chalk@5.3.0", + replacement = "@chalk_501//:pkg", ) use_repo(npm, "npm") diff --git a/e2e/npm_translate_lock_replace_packages/utils_module/.bazelignore b/e2e/npm_translate_lock_replace_packages/utils_module/.bazelignore new file mode 100644 index 000000000..3c3629e64 --- /dev/null +++ b/e2e/npm_translate_lock_replace_packages/utils_module/.bazelignore @@ -0,0 +1 @@ +node_modules diff --git a/e2e/npm_translate_lock_replace_packages/utils_module/.npmrc b/e2e/npm_translate_lock_replace_packages/utils_module/.npmrc new file mode 100644 index 000000000..5de6efae1 --- /dev/null +++ b/e2e/npm_translate_lock_replace_packages/utils_module/.npmrc @@ -0,0 +1,4 @@ +# Disabling pnpm [hoisting](https://pnpm.io/npmrc#hoist) by setting `hoist=false` is recommended on +# projects using rules_js so that pnpm outside of Bazel lays out a node_modules tree similar to what +# rules_js lays out under Bazel (without a hidden node_modules/.pnpm/node_modules) +hoist=false diff --git a/e2e/npm_translate_lock_replace_packages/utils_module/BUILD.bazel b/e2e/npm_translate_lock_replace_packages/utils_module/BUILD.bazel new file mode 100644 index 000000000..ad087949b --- /dev/null +++ b/e2e/npm_translate_lock_replace_packages/utils_module/BUILD.bazel @@ -0,0 +1,21 @@ +load("@aspect_rules_js//js:defs.bzl", "js_library", "js_test") +load("@npm_utils//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages(name = "node_modules") + +js_library( + name = "lib", + srcs = [ + "utils.mjs", + ], + visibility = ["//visibility:public"], + deps = [":node_modules/lodash"], +) + +js_test( + name = "lib_test", + data = [ + ":lib", + ], + entry_point = "test.mjs", +) diff --git a/e2e/npm_translate_lock_replace_packages/utils_module/MODULE.bazel b/e2e/npm_translate_lock_replace_packages/utils_module/MODULE.bazel new file mode 100644 index 000000000..412ca5b7a --- /dev/null +++ b/e2e/npm_translate_lock_replace_packages/utils_module/MODULE.bazel @@ -0,0 +1,37 @@ +module( + name = "utils_replacement_module", + version = "0.0.0", + compatibility_level = 1, +) + +bazel_dep(name = "lodash_replacement_module", version = "0.0.0") +local_path_override( + module_name = "lodash_replacement_module", + path = "../lodash_module", +) + +bazel_dep(name = "platforms", version = "0.0.5") +bazel_dep(name = "aspect_rules_js", version = "0.0.0") +bazel_dep(name = "aspect_bazel_lib", version = "2.7.7") +bazel_dep(name = "bazel_skylib", version = "1.5.0") + +local_path_override( + module_name = "aspect_rules_js", + path = "../../..", +) + +npm_utils = use_extension( + "@aspect_rules_js//npm:extensions.bzl", + "npm", +) +npm_utils.npm_translate_lock( + name = "npm_utils", + npmrc = "//:.npmrc", + pnpm_lock = "//:pnpm-lock.yaml", + verify_node_modules_ignored = "//:.bazelignore", +) +npm_utils.npm_replace_package( + package = "lodash@4.17.21", + replacement = "@lodash_replacement_module//:lodash_replacement", +) +use_repo(npm_utils, "npm_utils") diff --git a/e2e/npm_translate_lock_replace_packages/utils_module/package.json b/e2e/npm_translate_lock_replace_packages/utils_module/package.json new file mode 100644 index 000000000..fa6cea204 --- /dev/null +++ b/e2e/npm_translate_lock_replace_packages/utils_module/package.json @@ -0,0 +1,12 @@ +{ + "name": "my-utils", + "version": "1.0.0", + "type": "module", + "main": "utils.mjs", + "dependencies": { + "lodash": "4.17.21" + }, + "pnpm": { + "onlyBuiltDependencies": [] + } +} \ No newline at end of file diff --git a/e2e/npm_translate_lock_replace_packages/utils_module/pnpm-lock.yaml b/e2e/npm_translate_lock_replace_packages/utils_module/pnpm-lock.yaml new file mode 100644 index 000000000..7bd0eccc0 --- /dev/null +++ b/e2e/npm_translate_lock_replace_packages/utils_module/pnpm-lock.yaml @@ -0,0 +1,22 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + lodash: + specifier: 4.17.21 + version: 4.17.21 + +packages: + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + +snapshots: + + lodash@4.17.21: {} diff --git a/e2e/npm_translate_lock_replace_packages/utils_module/pnpm-workspace.yaml b/e2e/npm_translate_lock_replace_packages/utils_module/pnpm-workspace.yaml new file mode 100644 index 000000000..2cce0eb74 --- /dev/null +++ b/e2e/npm_translate_lock_replace_packages/utils_module/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - '.' diff --git a/e2e/npm_translate_lock_replace_packages/utils_module/test.mjs b/e2e/npm_translate_lock_replace_packages/utils_module/test.mjs new file mode 100644 index 000000000..a6c4b838e --- /dev/null +++ b/e2e/npm_translate_lock_replace_packages/utils_module/test.mjs @@ -0,0 +1,7 @@ +import lodash from 'lodash' + +// In package.json, lodash is declared as a dependency with version 4.17.21 +// but it should be replaced with version 4.17.20 in the final build. +if (lodash.version !== '4.17.20') { + throw new Error(`Expected lodash version 4.17.20, but got ${lodash.version}`); +} \ No newline at end of file diff --git a/e2e/npm_translate_lock_replace_packages/utils_module/utils.mjs b/e2e/npm_translate_lock_replace_packages/utils_module/utils.mjs new file mode 100644 index 000000000..60827cfe5 --- /dev/null +++ b/e2e/npm_translate_lock_replace_packages/utils_module/utils.mjs @@ -0,0 +1,14 @@ +// Utils module that depends on lodash +import lodash from 'lodash'; + +export function removeDuplicates(array) { + return lodash.uniq(array); +} + +export function getUtilsVersion() { + return "1.0.0"; +} + +export function getLodashVersion() { + return lodash.version; +} \ No newline at end of file From 2c6341b52efb00af15fe7450343d37ebee3dea64 Mon Sep 17 00:00:00 2001 From: Mihail Vratchanski Date: Wed, 13 Aug 2025 19:43:27 +0300 Subject: [PATCH 11/12] connect test from submodule to top module --- .../BUILD.bazel | 9 ++++++++ .../main.js | 2 -- .../utils_module/BUILD.bazel | 18 +++++++++------ .../utils_module/test.mjs | 22 ++++++++++++++----- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/e2e/npm_translate_lock_replace_packages/BUILD.bazel b/e2e/npm_translate_lock_replace_packages/BUILD.bazel index 16e39531a..b9795f431 100644 --- a/e2e/npm_translate_lock_replace_packages/BUILD.bazel +++ b/e2e/npm_translate_lock_replace_packages/BUILD.bazel @@ -15,6 +15,15 @@ js_test( entry_point = "main.js", ) +js_test( + name = "utils_test", + data = [ + "@utils_replacement_module//:test_lib", + ], + entry_point = "@utils_replacement_module//:test.mjs", + no_copy_to_bin = ["@utils_replacement_module//:test.mjs"], +) + npm_package( name = "npm-pkg", srcs = [ diff --git a/e2e/npm_translate_lock_replace_packages/main.js b/e2e/npm_translate_lock_replace_packages/main.js index 482f25042..b06911a56 100644 --- a/e2e/npm_translate_lock_replace_packages/main.js +++ b/e2e/npm_translate_lock_replace_packages/main.js @@ -45,5 +45,3 @@ if (uniqueArray.length !== 3) { throw new Error(`Expected lodash.uniq to work, but got array length ${uniqueArray.length}`) } -console.log(chalk.blue(`Hello world! Multiple package replacements work! 🎉`)) -console.log(chalk.green(`Chalk ${chalkActualDep} and Lodash ${lodashActualDep} both replaced successfully`)) diff --git a/e2e/npm_translate_lock_replace_packages/utils_module/BUILD.bazel b/e2e/npm_translate_lock_replace_packages/utils_module/BUILD.bazel index ad087949b..3c811b30d 100644 --- a/e2e/npm_translate_lock_replace_packages/utils_module/BUILD.bazel +++ b/e2e/npm_translate_lock_replace_packages/utils_module/BUILD.bazel @@ -1,8 +1,12 @@ -load("@aspect_rules_js//js:defs.bzl", "js_library", "js_test") +load("@aspect_rules_js//js:defs.bzl", "js_library") load("@npm_utils//:defs.bzl", "npm_link_all_packages") npm_link_all_packages(name = "node_modules") +exports_files([ + "test.mjs", +], visibility = ["//visibility:public"]) + js_library( name = "lib", srcs = [ @@ -12,10 +16,10 @@ js_library( deps = [":node_modules/lodash"], ) -js_test( - name = "lib_test", - data = [ - ":lib", - ], - entry_point = "test.mjs", +js_library( + name = "test_lib", + srcs = ["test.mjs"], + testonly = True, + visibility = ["//visibility:public"], + deps = [":lib"], ) diff --git a/e2e/npm_translate_lock_replace_packages/utils_module/test.mjs b/e2e/npm_translate_lock_replace_packages/utils_module/test.mjs index a6c4b838e..76ce10216 100644 --- a/e2e/npm_translate_lock_replace_packages/utils_module/test.mjs +++ b/e2e/npm_translate_lock_replace_packages/utils_module/test.mjs @@ -1,7 +1,19 @@ -import lodash from 'lodash' +import { getLodashVersion, removeDuplicates } from './utils.mjs' -// In package.json, lodash is declared as a dependency with version 4.17.21 -// but it should be replaced with version 4.17.20 in the final build. -if (lodash.version !== '4.17.20') { - throw new Error(`Expected lodash version 4.17.20, but got ${lodash.version}`); +// Test that lodash is replaced with version 4.17.20 +const version = getLodashVersion(); +if (version !== '4.17.20') { + throw new Error(`[utils_module] Expected lodash version 4.17.20, but got ${version}`); +} + +const testArray = [1, 2, 2, 3, 3, 3]; +const uniqueArray = removeDuplicates(testArray); +if (uniqueArray.length !== 3) { + throw new Error(`[utils_module] Expected removeDuplicates to return 3 unique items, but got ${uniqueArray.length}`); +} + +console.log('[utils_module] All tests passed ✓'); + +export function testUtilsModule() { + return true; } \ No newline at end of file From f1bc621eb751e134fa8f8213ee8c2220d9429a83 Mon Sep 17 00:00:00 2001 From: Mihail Vratchanski Date: Wed, 13 Aug 2025 20:11:34 +0300 Subject: [PATCH 12/12] format --- .../utils_module/BUILD.bazel | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/e2e/npm_translate_lock_replace_packages/utils_module/BUILD.bazel b/e2e/npm_translate_lock_replace_packages/utils_module/BUILD.bazel index 3c811b30d..8ae7a845f 100644 --- a/e2e/npm_translate_lock_replace_packages/utils_module/BUILD.bazel +++ b/e2e/npm_translate_lock_replace_packages/utils_module/BUILD.bazel @@ -3,9 +3,12 @@ load("@npm_utils//:defs.bzl", "npm_link_all_packages") npm_link_all_packages(name = "node_modules") -exports_files([ - "test.mjs", -], visibility = ["//visibility:public"]) +exports_files( + [ + "test.mjs", + ], + visibility = ["//visibility:public"], +) js_library( name = "lib", @@ -18,8 +21,8 @@ js_library( js_library( name = "test_lib", - srcs = ["test.mjs"], testonly = True, + srcs = ["test.mjs"], visibility = ["//visibility:public"], deps = [":lib"], )