fix(npm): correct package_store_prefix_len for links in external-repo sub-packages (#2864)
When calculating the package store prefix truncation with external
modules (i.e. a repo name is included in the path), an extra slash after
the repo name was unaccounted for, resulting in incorrect symlinks (and
subsequently `ERR_MODULE_NOT_FOUND` error during builds).
Below is an example of the logic used to calculate the truncation
length:
```python
# ../[com_github_example_repo+]/[example_package]/[node_modules/.aspect_rules_js/]foo@1.2.3/node_modules/bar
# | repo name | | package name | | store prefix |
path = "../com_github_example_repo+/example_package/node_modules/.aspect_rules_js/foo@1.2.3/node_modules/bar"
truncate_length = len(repo_name) + len(package_name) + len(store_prefix) + 3 # = 72
truncated_path = path[truncate_length:] # = "s/foo@1.2.3/node_modules/bar"
```
The extra `s` in the beginning of `truncated_path` should not be there
and is solved by changing the `+ 3` to a `+ 4`.
---------
Co-authored-by: Fredrik Medley <fredrik@meroton.com>
Co-authored-by: Jason Bedard <jason+github@jbedard.ca>
diff --git a/e2e/bzlmod/BUILD.bazel b/e2e/bzlmod/BUILD.bazel
index 9cacb00..ba3ed46 100644
--- a/e2e/bzlmod/BUILD.bazel
+++ b/e2e/bzlmod/BUILD.bazel
@@ -134,6 +134,9 @@
# js_run_binary() running a binary in @other_repo
":run_other_module_binary_test",
+
+ # js_run_binary() running a binary linked in a sub-package of @other_repo
+ ":run_other_module_subpackage_binary_test",
],
)
@@ -148,3 +151,14 @@
silent_on_success = False,
tool = "@other_module//:lessc", # A js_binary() tool in a different repo
)
+
+js_run_binary(
+ name = "run_other_module_subpackage_binary_test",
+ srcs = ["my.less"],
+ outs = ["other-module-subpackage-my.css"],
+ args = [
+ "my.less",
+ "other-module-subpackage-my.css",
+ ],
+ tool = "@other_module//frontend:lessc", # A js_binary() tool linked in a sub-package of a different repo
+)
diff --git a/e2e/bzlmod/other_module/.bazelignore b/e2e/bzlmod/other_module/.bazelignore
index 3c3629e..aac7dfb 100644
--- a/e2e/bzlmod/other_module/.bazelignore
+++ b/e2e/bzlmod/other_module/.bazelignore
@@ -1 +1,2 @@
node_modules
+frontend/node_modules
diff --git a/e2e/bzlmod/other_module/MODULE.bazel b/e2e/bzlmod/other_module/MODULE.bazel
index 1e20ad9..c026237 100644
--- a/e2e/bzlmod/other_module/MODULE.bazel
+++ b/e2e/bzlmod/other_module/MODULE.bazel
@@ -9,7 +9,13 @@
pnpm_lock = "//:pnpm-lock.yaml",
verify_node_modules_ignored = "//:.bazelignore",
)
-use_repo(npm, "npm_other_module")
+npm.npm_translate_lock(
+ name = "npm_other_module_frontend",
+ npmrc = "//:.npmrc",
+ pnpm_lock = "//frontend:pnpm-lock.yaml",
+ verify_node_modules_ignored = "//:.bazelignore",
+)
+use_repo(npm, "npm_other_module", "npm_other_module_frontend")
# A non-root module may request a pnpm version for the default "pnpm" repo, but
# the version requested by the e2e test root module takes priority. The e2e test
diff --git a/e2e/bzlmod/other_module/frontend/BUILD.bazel b/e2e/bzlmod/other_module/frontend/BUILD.bazel
new file mode 100644
index 0000000..8c0af27
--- /dev/null
+++ b/e2e/bzlmod/other_module/frontend/BUILD.bazel
@@ -0,0 +1,11 @@
+load("@npm_other_module_frontend//:defs.bzl", "npm_link_all_packages")
+load("@npm_other_module_frontend//frontend:less/package_json.bzl", less_bin = "bin")
+
+# npm packages linked in a sub-package of an external module, see
+# https://github.com/aspect-build/rules_js/pull/2896
+npm_link_all_packages()
+
+less_bin.lessc_binary(
+ name = "lessc",
+ visibility = ["//visibility:public"],
+)
diff --git a/e2e/bzlmod/other_module/frontend/package.json b/e2e/bzlmod/other_module/frontend/package.json
new file mode 100644
index 0000000..11e1359
--- /dev/null
+++ b/e2e/bzlmod/other_module/frontend/package.json
@@ -0,0 +1,8 @@
+{
+ "pnpm": {
+ "onlyBuiltDependencies": []
+ },
+ "dependencies": {
+ "less": "4.1.3"
+ }
+}
diff --git a/e2e/bzlmod/other_module/frontend/pnpm-lock.yaml b/e2e/bzlmod/other_module/frontend/pnpm-lock.yaml
new file mode 100644
index 0000000..78c165c
--- /dev/null
+++ b/e2e/bzlmod/other_module/frontend/pnpm-lock.yaml
@@ -0,0 +1,160 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ less:
+ specifier: 4.1.3
+ version: 4.1.3
+
+packages:
+
+ copy-anything@2.0.6:
+ resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==}
+
+ errno@0.1.8:
+ resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
+ hasBin: true
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
+ image-size@0.5.5:
+ resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==}
+ engines: {node: '>=0.10.0'}
+ hasBin: true
+
+ is-what@3.14.1:
+ resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
+
+ less@4.1.3:
+ resolution: {integrity: sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ make-dir@2.1.0:
+ resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
+ engines: {node: '>=6'}
+
+ mime@1.6.0:
+ resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ needle@3.5.0:
+ resolution: {integrity: sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==}
+ engines: {node: '>= 4.4.x'}
+ hasBin: true
+
+ parse-node-version@1.0.1:
+ resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==}
+ engines: {node: '>= 0.10'}
+
+ pify@4.0.1:
+ resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
+ engines: {node: '>=6'}
+
+ prr@1.0.1:
+ resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ sax@1.6.1:
+ resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==}
+ engines: {node: '>=11.0.0'}
+
+ semver@5.7.2:
+ resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
+ hasBin: true
+
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+snapshots:
+
+ copy-anything@2.0.6:
+ dependencies:
+ is-what: 3.14.1
+
+ errno@0.1.8:
+ dependencies:
+ prr: 1.0.1
+ optional: true
+
+ graceful-fs@4.2.11:
+ optional: true
+
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+ optional: true
+
+ image-size@0.5.5:
+ optional: true
+
+ is-what@3.14.1: {}
+
+ less@4.1.3:
+ dependencies:
+ copy-anything: 2.0.6
+ parse-node-version: 1.0.1
+ tslib: 2.8.1
+ optionalDependencies:
+ errno: 0.1.8
+ graceful-fs: 4.2.11
+ image-size: 0.5.5
+ make-dir: 2.1.0
+ mime: 1.6.0
+ needle: 3.5.0
+ source-map: 0.6.1
+
+ make-dir@2.1.0:
+ dependencies:
+ pify: 4.0.1
+ semver: 5.7.2
+ optional: true
+
+ mime@1.6.0:
+ optional: true
+
+ needle@3.5.0:
+ dependencies:
+ iconv-lite: 0.6.3
+ sax: 1.6.1
+ optional: true
+
+ parse-node-version@1.0.1: {}
+
+ pify@4.0.1:
+ optional: true
+
+ prr@1.0.1:
+ optional: true
+
+ safer-buffer@2.1.2:
+ optional: true
+
+ sax@1.6.1:
+ optional: true
+
+ semver@5.7.2:
+ optional: true
+
+ source-map@0.6.1:
+ optional: true
+
+ tslib@2.8.1: {}
diff --git a/e2e/bzlmod/other_module/frontend/pnpm-workspace.yaml b/e2e/bzlmod/other_module/frontend/pnpm-workspace.yaml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/e2e/bzlmod/other_module/frontend/pnpm-workspace.yaml
diff --git a/npm/private/npm_package_store.bzl b/npm/private/npm_package_store.bzl
index c33ac08..dab5c13 100644
--- a/npm/private/npm_package_store.bzl
+++ b/npm/private/npm_package_store.bzl
@@ -12,8 +12,6 @@
_SUPPORTS_SYMLINK_TARGET_TYPE = bazel_features.rules.symlink_action_has_target_type
-_PACKAGE_STORE_PREFIX_LEN = len("node_modules/{}/".format(utils.package_store_root))
-
_EXTRACT_EXECUTION_REQUIREMENTS = {
"supports-path-mapping": "1",
}
@@ -191,11 +189,8 @@
if not version:
fail("No package version specified to link to. Package version must either be specified explicitly via 'version' attribute or come from the 'src' 'JsInfo|NpmPackageInfo', typically a 'js_library|npm_package' target")
- package_store_prefix_len = _PACKAGE_STORE_PREFIX_LEN
- if ctx.label.package:
- package_store_prefix_len += len(ctx.label.package)
- if ctx.label.repo_name:
- package_store_prefix_len += len(ctx.label.repo_name) + 3 # +3 for ../
+ package_store_prefix = utils.package_store_prefix(ctx.label.repo_name, ctx.label.package)
+ package_store_prefix_len = len(package_store_prefix)
package_key = "{}@{}".format(package, version)
package_store_name = utils.package_store_name(package_key)
@@ -225,14 +220,7 @@
npm_pkg_info = ctx.attr.src[NpmPackageInfo]
# output the package as a TreeArtifact to its package store location
- if ctx.label.repo_name and ctx.label.package:
- expected_short_path = "../{}/{}/{}".format(ctx.label.repo_name, ctx.label.package, package_store_directory_path)
- elif ctx.label.repo_name:
- expected_short_path = "../{}/{}".format(ctx.label.repo_name, package_store_directory_path)
- elif ctx.label.package:
- expected_short_path = "{}/{}".format(ctx.label.package, package_store_directory_path)
- else:
- expected_short_path = package_store_directory_path
+ expected_short_path = "{}{}/node_modules/{}".format(package_store_prefix, package_store_name, package)
src = npm_pkg_info.src
if src.short_path == expected_short_path:
diff --git a/npm/private/test/utils_tests.bzl b/npm/private/test/utils_tests.bzl
index aba47c4..e794213 100644
--- a/npm/private/test/utils_tests.bzl
+++ b/npm/private/test/utils_tests.bzl
@@ -219,6 +219,21 @@
)
return unittest.end(env)
+# buildifier: disable=function-docstring
+def test_package_store_prefix(ctx):
+ env = unittest.begin(ctx)
+
+ asserts.equals(env, "node_modules/.aspect_rules_js/", utils.package_store_prefix("", ""))
+ asserts.equals(env, "frontend/node_modules/.aspect_rules_js/", utils.package_store_prefix("", "frontend"))
+ asserts.equals(env, "../other_module+/node_modules/.aspect_rules_js/", utils.package_store_prefix("other_module+", ""))
+ asserts.equals(env, "../other_module+/frontend/node_modules/.aspect_rules_js/", utils.package_store_prefix("other_module+", "frontend"))
+
+ # the prefix is stripped from a dependency's package store short_path to form the relative symlink target
+ short_path = "../other_module+/frontend/node_modules/.aspect_rules_js/less@4.1.3/node_modules/less"
+ asserts.equals(env, "less@4.1.3/node_modules/less", short_path[len(utils.package_store_prefix("other_module+", "frontend")):])
+
+ return unittest.end(env)
+
t2_test = unittest.make(test_package_store_and_target_name)
t3_test = unittest.make(test_friendly_name)
t6_test = unittest.make(test_parse_package_name)
@@ -226,6 +241,7 @@
t8_test = unittest.make(test_npm_registry_url)
t9_test = unittest.make(test_package_store_name_link_versions)
t10_test = unittest.make(test_hex_to_base64)
+t11_test = unittest.make(test_package_store_prefix)
def utils_tests(name):
unittest.suite(
@@ -237,4 +253,5 @@
t8_test,
t9_test,
t10_test,
+ t11_test,
)
diff --git a/npm/private/utils.bzl b/npm/private/utils.bzl
index 461712c..0f0c4ba 100644
--- a/npm/private/utils.bzl
+++ b/npm/private/utils.bzl
@@ -382,6 +382,18 @@
return "".join(output)
+_PACKAGE_STORE_ROOT = ".aspect_rules_js"
+_PACKAGE_STORE_PREFIX = "node_modules/{}/".format(_PACKAGE_STORE_ROOT)
+
+def _package_store_prefix(repo_name, package):
+ """Path of the package store of a repository package, relative to the main repository."""
+ prefix = _PACKAGE_STORE_PREFIX
+ if package:
+ prefix = "{}/{}".format(package, prefix)
+ if repo_name:
+ prefix = "../{}/{}".format(repo_name, prefix)
+ return prefix
+
utils = struct(
sorted_map = _sorted_map,
friendly_name = _friendly_name,
@@ -389,9 +401,10 @@
importer_to_link = _importer_to_link,
package_repo_name = _package_repo_name,
package_store_name = _package_store_name,
+ package_store_prefix = _package_store_prefix,
make_directory_symlink = _make_directory_symlink,
# Symlinked node_modules structure package store path under node_modules
- package_store_root = ".aspect_rules_js",
+ package_store_root = _PACKAGE_STORE_ROOT,
# Suffix for npm_import links repository
links_repo_suffix = "__links",
# Output group name for the package directory of a linked npm package