Skip to content

Add replace_package module extension tag #2289

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/npm_translate_lock.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 14 additions & 3 deletions e2e/npm_translate_lock_replace_packages/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ npm = use_extension(
"npm",
dev_dependency = True,
)
npm.npm_replace_package(
package = "[email protected]",
replacement = "@chalk_501//:pkg",
)
npm.npm_translate_lock(
name = "npm",
npmrc = "//:.npmrc",
pnpm_lock = "//:pnpm-lock.yaml",
replace_packages = {
"[email protected]": "@chalk_501//:pkg",
},
verify_node_modules_ignored = "//:.bazelignore",
)

Expand All @@ -39,4 +40,14 @@ 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 = "[email protected]",
replacement = "@lodash_replacement_module//:lodash_replacement",
)
use_repo(npm, "npm")
11 changes: 11 additions & 0 deletions e2e/npm_translate_lock_replace_packages/lodash_module/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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"],
)
11 changes: 11 additions & 0 deletions e2e/npm_translate_lock_replace_packages/lodash_module/MODULE.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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 = "../../..",
)
Original file line number Diff line number Diff line change
@@ -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"
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "lodash",
"version": "4.17.20",
"type": "module",
"main": "lodash.js"
}
42 changes: 35 additions & 7 deletions e2e/npm_translate_lock_replace_packages/main.js
Original file line number Diff line number Diff line change
@@ -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`))
3 changes: 2 additions & 1 deletion e2e/npm_translate_lock_replace_packages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"private": true,
"type": "module",
"dependencies": {
"chalk": "5.3.0"
"chalk": "5.3.0",
"lodash": "4.17.21"
},
"pnpm": {
"onlyBuiltDependencies": []
Expand Down
8 changes: 8 additions & 0 deletions e2e/npm_translate_lock_replace_packages/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 52 additions & 6 deletions npm/extensions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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.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)

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)
Expand Down Expand Up @@ -71,7 +77,18 @@ 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):
# 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,
Expand All @@ -94,7 +111,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,
Expand All @@ -105,7 +122,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(
Expand Down Expand Up @@ -148,6 +165,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,
Expand Down Expand Up @@ -247,8 +265,10 @@ 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")
return attrs

def _npm_import_attrs():
Expand Down Expand Up @@ -283,12 +303,38 @@ def _npm_exclude_package_contents_attrs():
),
}

_REPLACE_PACKAGE_ATTRS = {
"package": attr.string(
doc = "The package name and version to replace (e.g., '[email protected]')",
mandatory = True,
),
"replacement": attr.label(
doc = "The target to use as replacement for this package",
mandatory = True,
),
}

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()),
"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 = "[email protected]",
replacement = "@chalk_501//:pkg",
)
```""",
),
},
)

Expand Down
13 changes: 12 additions & 1 deletion npm/private/npm_translate_lock.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,18 @@ 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:

```starlark
npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm")
npm.npm_replace_package(
package = "[email protected]",
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
Expand Down
Loading
Loading