Skip to content

Commit 9e65f63

Browse files
committed
fix: support pnpm v10+ configuration in pnpm-workspace.yaml
1 parent ea0bea5 commit 9e65f63

File tree

9 files changed

+156
-100
lines changed

9 files changed

+156
-100
lines changed

.aspect/rules/external_repository_action_cache/npm_translate_lock_LTE4Nzc1MDcwNjU=

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,6 @@ npm/private/test/package.json=-475044943
3232
npm/private/test/vendored/is-odd/package.json=1041695223
3333
npm/private/test/vendored/lodash-4.17.21.tgz=-1206623349
3434
npm/private/test/vendored/semver-max/package.json=578664053
35-
package.json=1085031407
36-
pnpm-lock.yaml=1538679820
37-
pnpm-workspace.yaml=-2039776064
35+
package.json=-1957263621
36+
pnpm-lock.yaml=1438020331
37+
pnpm-workspace.yaml=-1336981113

MODULE.bazel

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,13 @@ use_repo(
6464
####### Dev dependencies ########
6565

6666
# Dev-only pnpm used for local testing
67-
pnpm9 = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm", dev_dependency = True)
68-
pnpm9.pnpm(
69-
name = "pnpm9",
70-
pnpm_version = "9.15.9",
71-
pnpm_version_integrity = "sha512-aARhQYk8ZvrQHAeSMRKOmvuJ74fiaR1p5NQO7iKJiClf1GghgbrlW1hBjDolO95lpQXsfF+UA+zlzDzTfc8lMQ==",
67+
pnpm10 = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm", dev_dependency = True)
68+
pnpm10.pnpm(
69+
name = "pnpm10",
70+
pnpm_version = "10.22.0",
71+
pnpm_version_integrity = "sha512-vwSe/plbKPUn/StBrgR0zikYb37cs79UUIe9Yfu+uyv3U2LRMH/aCcLSiOHkmXh6wS1Py2F6l0cYpgUfLu50HA==",
7272
)
73-
use_repo(pnpm9, "pnpm9")
73+
use_repo(pnpm10, "pnpm10")
7474

7575
bazel_dep(name = "bazelrc-preset.bzl", version = "1.3.0", dev_dependency = True)
7676
bazel_dep(name = "aspect_rules_lint", version = "1.1.0", dev_dependency = True)
@@ -277,7 +277,7 @@ npm.npm_translate_lock(
277277
"[email protected]": ["examples/npm_deps"],
278278
},
279279
update_pnpm_lock = True,
280-
use_pnpm = "@pnpm9//:package/bin/pnpm.cjs",
280+
use_pnpm = "@pnpm10//:package/bin/pnpm.cjs",
281281
verify_node_modules_ignored = "//:.bazelignore",
282282
verify_patches = "//examples/npm_deps/patches:patches",
283283
)

npm/private/npm_translate_lock_helpers.bzl

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -611,23 +611,27 @@ def _verify_lifecycle_hooks_specified(_, state):
611611
return
612612

613613
if state.only_built_dependencies() == None:
614-
fail("""\
615-
ERROR: pnpm.onlyBuiltDependencies required in pnpm workspace root package.json when using pnpm v9 or later
614+
msg = """\
615+
ERROR: pnpm 'onlyBuiltDependencies' configuration required when using pnpm v9 or later.
616616
617-
The root package.json must be alongside the pnpm-lock.yaml file and contain a pnpm.onlyBuiltDependencies and
618-
will be automatically detected even if not explicitly added to data attribute.
617+
Packages that rules_js should generate lifecycle hook actions for must be declared in
618+
'onlyBuiltDependencies'.
619619
620-
As of pnpm v9, the lockfile no longer specifies if packages have lifecycle hooks.
620+
As of pnpm v10 'onlyBuiltDependencies' should be configured in the pnpm-workspace.yaml file
621+
alongside other pnpm configuration. See https://pnpm.io/10.x/settings#onlybuiltdependencies
622+
for more information.
621623
622-
Packages that rules_js should generate lifecycle hook actions for must now be declared in
623-
pnpm.onlyBuiltDependencies in the pnpm workspace root package.json. See
624-
https://pnpm.io/package_json#pnpmonlybuiltdependencies for more information.
624+
In pnpm v9 and earlier 'onlyBuiltDependencies' is configured in the root package.json
625+
pnpm.onlyBuiltDependencies. The root package.json must be alongside the pnpm-lock.yaml file
626+
and will be automatically detected even if not explicitly added to data attribute.
627+
See https://pnpm.io/9.x/package_json#pnpmonlybuiltdependencies for more information.
625628
626629
Prior to pnpm v9, rules_js keyed off of the requiresBuild attribute in the pnpm lock
627630
file to determine if a lifecycle hook action should be generated for an npm package.
628631
See [pnpm #7707](https://github.com/pnpm/pnpm/issues/7707) for the reasons that pnpm
629632
removed the requiresBuild attribute from the lockfile in v9.
630-
""")
633+
"""
634+
fail(msg)
631635

632636
################################################################################
633637
def _verify_patches(rctx, state):

npm/private/npm_translate_lock_state.bzl

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ WARNING: `update_pnpm_lock` attribute in `npm_translate_lock(name = "{rctx_name}
5656
_init_patched_dependencies_labels(priv, rctx, attr, label_store)
5757

5858
# May depend on lockfile state
59-
_init_root_package(priv, rctx, attr, label_store)
59+
_init_root_package(priv, attr, label_store)
60+
_init_workspace(priv, rctx, attr, label_store, is_windows)
6061

6162
if _should_update_pnpm_lock(priv):
6263
_init_importer_labels(priv, label_store)
@@ -191,7 +192,7 @@ def _init_external_repository_action_cache(priv, attr):
191192
priv["external_repository_action_cache"] = attr.external_repository_action_cache if attr.external_repository_action_cache else utils.default_external_repository_action_cache()
192193

193194
################################################################################
194-
def _init_root_package(priv, rctx, attr, label_store):
195+
def _init_root_package(priv, attr, label_store):
195196
pnpm_lock_label = label_store.label("pnpm_lock")
196197

197198
# use the directory of the pnpm_lock file as the root_package unless overridden by the root_package attribute
@@ -208,19 +209,51 @@ def _init_root_package(priv, rctx, attr, label_store):
208209
fail("root_package cannot be overridden if there are pnpm workspace packages specified")
209210
priv["root_package"] = attr.root_package
210211

211-
# Load a declared root package.json
212+
def _init_workspace(priv, rctx, attr, label_store, is_windows):
213+
root_package_json = {}
214+
215+
# Load pnpm settings from root package.json for pnpm <= v9.
212216
if label_store.has("package_json_root"):
217+
# Load a declared root package.json
213218
root_package_json_path = label_store.path("package_json_root")
214-
priv["root_package_json"] = json.decode(rctx.read(root_package_json_path))
219+
root_package_json = json.decode(rctx.read(root_package_json_path))
215220
elif "." in priv["importers"].keys():
216221
# Load an undeclared package.json derived from the root importer in the lockfile
217222
label_store.add_sibling("lock", "package_json_root", PACKAGE_JSON_FILENAME)
218223
root_package_json_path = label_store.path("package_json_root")
219-
priv["root_package_json"] = json.decode(rctx.read(root_package_json_path))
220-
else:
221-
# if there is no root importer that means there is no root package.json to read; pnpm allows
222-
# you to just have a pnpm-workspaces.yaml at the root and no package.json at that location
223-
priv["root_package_json"] = {}
224+
root_package_json = json.decode(rctx.read(root_package_json_path))
225+
226+
priv["pnpm_settings"] = root_package_json.get("pnpm", {})
227+
228+
# Read settings from pnpm-workspace.yaml for pnpm v10+ (NOTE: pnpm 9-10+ has lockfile version 9).
229+
# Support scenario where pnpm-lock.yaml was never parsed and "lock_version" is not set.
230+
if priv.get("lock_version", 0) >= 9.0 and label_store.has("pnpm_workspace"):
231+
pnpm_workspace_path = label_store.path("pnpm_workspace")
232+
if is_windows or utils.exists(rctx, pnpm_workspace_path):
233+
pnpm_workspace_json, workspace_parse_err = _yaml_to_json(rctx, attr, str(pnpm_workspace_path), is_windows)
234+
235+
if workspace_parse_err == None:
236+
pnpm_workspace_settings, workspace_parse_err = pnpm.parse_pnpm_workspace_json(pnpm_workspace_json)
237+
238+
if pnpm_workspace_settings:
239+
priv["pnpm_settings"] = priv["pnpm_settings"] | pnpm_workspace_settings
240+
241+
if workspace_parse_err != None:
242+
should_update = _should_update_pnpm_lock(priv)
243+
msg = """
244+
{type}: pnpm-workspace.yaml parse error {error}`.
245+
""".format(type = "WARNING" if should_update else "ERROR", error = workspace_parse_err)
246+
247+
if should_update:
248+
# buildifier: disable=print
249+
print(msg)
250+
else:
251+
fail(msg)
252+
253+
# Default to empty settings if none found such as no pnpm-workspace.yaml or no package.json alongside the lockfile
254+
# TODO(3.0): pnpm-workspace should always be available
255+
if priv["pnpm_settings"] == None:
256+
priv["pnpm_settings"] = {}
224257

225258
################################################################################
226259
def _init_npmrc(priv, rctx, attr, label_store, is_windows):
@@ -473,24 +506,33 @@ WARNING: Cannot determine home directory in order to load home `.npmrc` file in
473506
_load_npmrc(priv, rctx, home_npmrc_path)
474507

475508
################################################################################
476-
def _load_lockfile(priv, rctx, attr, pnpm_lock_path, is_windows):
477-
importers = {}
478-
packages = {}
479-
patched_dependencies = {}
480-
lock_version = None
481-
lock_parse_err = None
482-
509+
def _yaml_to_json(rctx, attr, yaml_path, is_windows):
483510
host_yq = Label("@{}_{}//:yq{}".format(attr.yq_toolchain_prefix, repo_utils.platform(rctx), ".exe" if is_windows else ""))
484511
yq_args = [
485512
str(rctx.path(host_yq)),
486-
str(pnpm_lock_path),
513+
str(yaml_path),
487514
"-o=json",
488515
]
489516
result = rctx.execute(yq_args)
490517
if result.return_code:
491-
lock_parse_err = "failed to parse pnpm lock file with yq. '{}' exited with {}: \nSTDOUT:\n{}\nSTDERR:\n{}".format(" ".join(yq_args), result.return_code, result.stdout, result.stderr)
492-
else:
493-
importers, packages, patched_dependencies, lock_version, lock_parse_err = pnpm.parse_pnpm_lock_json(result.stdout if result.stdout != "null" else None) # NB: yq will return the string "null" if the yaml file is empty
518+
return None, "failed to parse {} with yq. '{}' exited with {}: \nSTDOUT:\n{}\nSTDERR:\n{}".format(" ".join(yq_args), yaml_path, result.return_code, result.stdout, result.stderr)
519+
520+
# NB: yq will return the string "null" if the yaml file is empty
521+
if result.stdout != "null":
522+
return result.stdout, None
523+
524+
return None, None
525+
526+
def _load_lockfile(priv, rctx, attr, pnpm_lock_path, is_windows):
527+
importers = {}
528+
packages = {}
529+
patched_dependencies = {}
530+
lock_version = None
531+
lock_parse_err = None
532+
533+
lockfile_content, lock_parse_err = _yaml_to_json(rctx, attr, str(pnpm_lock_path), is_windows)
534+
if lock_parse_err == None:
535+
importers, packages, patched_dependencies, lock_version, lock_parse_err = pnpm.parse_pnpm_lock_json(lockfile_content)
494536

495537
priv["lock_version"] = lock_version
496538
priv["importers"] = importers
@@ -538,7 +580,7 @@ def _patched_dependencies(priv):
538580
return priv["patched_dependencies"]
539581

540582
def _only_built_dependencies(priv):
541-
return _root_package_json(priv).get("pnpm", {}).get("onlyBuiltDependencies", None)
583+
return _pnpm_settings(priv).get("onlyBuiltDependencies", None)
542584

543585
def _num_patches(priv):
544586
return priv["num_patches"]
@@ -552,8 +594,8 @@ def _npm_auth(priv):
552594
def _root_package(priv):
553595
return priv["root_package"]
554596

555-
def _root_package_json(priv):
556-
return priv["root_package_json"]
597+
def _pnpm_settings(priv):
598+
return priv["pnpm_settings"]
557599

558600
################################################################################
559601
def _new(rctx_name, rctx, attr, bzlmod):
@@ -576,7 +618,7 @@ def _new(rctx_name, rctx, attr, bzlmod):
576618
"npm_registries": {},
577619
"packages": {},
578620
"root_package": attr.root_package,
579-
"root_package_json": {},
621+
"pnpm_settings": {},
580622
"patched_dependencies": {},
581623
"should_update_pnpm_lock": should_update_pnpm_lock,
582624
}

npm/private/pnpm.bzl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,9 +641,31 @@ def _assert_lockfile_version(version, testonly = False):
641641
fail(msg)
642642
return msg
643643

644+
def _parse_pnpm_workspace_json(content):
645+
"""Parse the content of a pnpm-workspace.yaml file.
646+
647+
Args:
648+
content: pnpm-workspace.yaml file content as json
649+
650+
Returns:
651+
A tuple of (packages list, error string)
652+
"""
653+
if not content:
654+
return {}, None
655+
656+
parsed = json.decode(content)
657+
if not parsed:
658+
return {}, None
659+
660+
if not types.is_dict(parsed):
661+
return None, "pnpm-workspace should be a starlark dict"
662+
663+
return parsed, None
664+
644665
pnpm = struct(
645666
assert_lockfile_version = _assert_lockfile_version,
646667
parse_pnpm_lock_json = _parse_pnpm_lock_json,
668+
parse_pnpm_workspace_json = _parse_pnpm_workspace_json,
647669
)
648670

649671
# Exported only to be tested

npm/private/test/snapshots/npm_defs.bzl

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,57 +9,5 @@
99
"inline-fixtures": "1.1.0",
1010
"jsonpath-plus": "7.2.0",
1111
"typescript": "^5.8.0"
12-
},
13-
"pnpm": {
14-
"onlyBuiltDependencies": [
15-
"@aspect-test/c",
16-
"@figma/nodegit",
17-
"@kubernetes/client-node",
18-
"bufferutil",
19-
"esbuild",
20-
"fsevents",
21-
"segfault-handler",
22-
"puppeteer",
23-
"protoc-gen-grpc-ts"
24-
],
25-
"packageExtensions": {
26-
"mocha": {
27-
"peerDependencies": {
28-
"mocha-multi-reporters": "*"
29-
}
30-
},
31-
"mocha-multi-reporters": {
32-
"peerDependencies": {
33-
"mocha-junit-reporter": "*"
34-
}
35-
},
36-
"segfault-handler": {
37-
"dependencies": {
38-
"node-gyp": "*"
39-
}
40-
},
41-
"unix-dgram": {
42-
"dependencies": {
43-
"node-gyp": "*"
44-
}
45-
},
46-
"@kubernetes/client-node": {
47-
"//1": "these deps are from devDependencies of @kubernetes/client-node and needed for 'prepare' lifecycle hook",
48-
"//2": "https://github.com/kubernetes-client/javascript/blob/cb821e92b766f6ffba6ad8cf5e7ff6ba77c3a1c9/package.json#L28",
49-
"//3": "A very specific @types/node version is required for the tsc compilation within @kubernetes/client-node",
50-
"dependencies": {
51-
"@types/node": "22.7.4",
52-
"typescript": "~5.6.2"
53-
}
54-
}
55-
},
56-
"overrides": {
57-
"jsonify": "https://github.com/aspect-build/test-packages/releases/download/0.0.0/@foo-jsonify-0.0.0.tgz",
58-
"semver-max": "file:./npm/private/test/vendored/semver-max",
59-
"is-odd": "file:./npm/private/test/vendored/is-odd"
60-
},
61-
"patchedDependencies": {
62-
"[email protected]": "examples/npm_deps/patches/[email protected]"
63-
}
6412
}
6513
}

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)