Skip to content

Commit bcf4c5d

Browse files
committed
fix: support pnpm v10+ configuration in pnpm-workspace.yaml
1 parent 54a4cea commit bcf4c5d

File tree

11 files changed

+152
-101
lines changed

11 files changed

+152
-101
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=-1933829342
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=-155745742
37-
pnpm-workspace.yaml=-2039776064
35+
package.json=-1957263621
36+
pnpm-lock.yaml=-1250423305
37+
pnpm-workspace.yaml=-1336981113

MODULE.bazel

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ use_repo(
7070
####### Dev dependencies ########
7171

7272
# Dev-only pnpm used for local testing
73-
pnpm9 = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm", dev_dependency = True)
74-
pnpm9.pnpm(
75-
name = "pnpm9",
76-
pnpm_version = "9.15.9",
77-
pnpm_version_integrity = "sha512-aARhQYk8ZvrQHAeSMRKOmvuJ74fiaR1p5NQO7iKJiClf1GghgbrlW1hBjDolO95lpQXsfF+UA+zlzDzTfc8lMQ==",
73+
pnpm10 = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm", dev_dependency = True)
74+
pnpm10.pnpm(
75+
name = "pnpm10",
76+
pnpm_version = "10.22.0",
77+
pnpm_version_integrity = "sha512-vwSe/plbKPUn/StBrgR0zikYb37cs79UUIe9Yfu+uyv3U2LRMH/aCcLSiOHkmXh6wS1Py2F6l0cYpgUfLu50HA==",
7878
)
79-
use_repo(pnpm9, "pnpm9")
79+
use_repo(pnpm10, "pnpm10")
8080

8181
bazel_dep(name = "bazelrc-preset.bzl", version = "1.3.0", dev_dependency = True)
8282
bazel_dep(name = "aspect_rules_lint", version = "1.1.0", dev_dependency = True)
@@ -273,7 +273,7 @@ npm.npm_translate_lock(
273273
"[email protected]": ["examples/npm_deps"],
274274
},
275275
update_pnpm_lock = True,
276-
use_pnpm = "@pnpm9//:package/bin/pnpm.cjs",
276+
use_pnpm = "@pnpm10//:package/bin/pnpm.cjs",
277277
verify_node_modules_ignored = "//:.bazelignore",
278278
verify_patches = "//examples/npm_deps/patches:patches",
279279
)

npm/private/npm_translate_lock_helpers.bzl

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -573,23 +573,27 @@ def _normalize_bazelignore(lines):
573573
################################################################################
574574
def _verify_lifecycle_hooks_specified(_, state):
575575
if state.only_built_dependencies() == None:
576-
fail("""\
577-
ERROR: pnpm.onlyBuiltDependencies required in pnpm workspace root package.json when using pnpm v9 or later
576+
msg = """\
577+
ERROR: pnpm 'onlyBuiltDependencies' configuration required when using pnpm v9 or later.
578578
579-
The root package.json must be alongside the pnpm-lock.yaml file and contain a pnpm.onlyBuiltDependencies and
580-
will be automatically detected even if not explicitly added to data attribute.
579+
Packages that rules_js should generate lifecycle hook actions for must be declared in
580+
'onlyBuiltDependencies'.
581581
582-
As of pnpm v9, the lockfile no longer specifies if packages have lifecycle hooks.
582+
As of pnpm v10 'onlyBuiltDependencies' should be configured in the pnpm-workspace.yaml file
583+
alongside other pnpm configuration. See https://pnpm.io/10.x/settings#onlybuiltdependencies
584+
for more information.
583585
584-
Packages that rules_js should generate lifecycle hook actions for must now be declared in
585-
pnpm.onlyBuiltDependencies in the pnpm workspace root package.json. See
586-
https://pnpm.io/package_json#pnpmonlybuiltdependencies for more information.
586+
In pnpm v9 and earlier 'onlyBuiltDependencies' is configured in the root package.json
587+
pnpm.onlyBuiltDependencies. The root package.json must be alongside the pnpm-lock.yaml file
588+
and will be automatically detected even if not explicitly added to data attribute.
589+
See https://pnpm.io/9.x/package_json#pnpmonlybuiltdependencies for more information.
587590
588591
Prior to pnpm v9, rules_js keyed off of the requiresBuild attribute in the pnpm lock
589592
file to determine if a lifecycle hook action should be generated for an npm package.
590593
See [pnpm #7707](https://github.com/pnpm/pnpm/issues/7707) for the reasons that pnpm
591594
removed the requiresBuild attribute from the lockfile in v9.
592-
""")
595+
"""
596+
fail(msg)
593597

594598
################################################################################
595599
def _verify_patches(rctx, attr, state):

npm/private/npm_translate_lock_state.bzl

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ WARNING: `update_pnpm_lock` attribute in `npm_translate_lock(name = "{rctx_name}
5151
_init_patched_dependencies_labels(priv, rctx, attr, label_store)
5252

5353
# May depend on lockfile state
54-
_init_root_package(priv, rctx, attr, label_store)
54+
_init_root_package(priv, attr, label_store)
55+
_init_workspace(priv, rctx, label_store, is_windows)
5556

5657
if _should_update_pnpm_lock(priv):
5758
_init_importer_labels(priv, label_store)
@@ -167,7 +168,7 @@ def _init_external_repository_action_cache(priv, attr):
167168
priv["external_repository_action_cache"] = attr.external_repository_action_cache if attr.external_repository_action_cache else utils.default_external_repository_action_cache()
168169

169170
################################################################################
170-
def _init_root_package(priv, rctx, attr, label_store):
171+
def _init_root_package(priv, attr, label_store):
171172
pnpm_lock_label = label_store.label("pnpm_lock")
172173

173174
# use the directory of the pnpm_lock file as the root_package unless overridden by the root_package attribute
@@ -184,19 +185,46 @@ def _init_root_package(priv, rctx, attr, label_store):
184185
fail("root_package cannot be overridden if there are pnpm workspace packages specified")
185186
priv["root_package"] = attr.root_package
186187

187-
# Load a declared root package.json
188+
def _init_workspace(priv, rctx, label_store, is_windows):
189+
root_package_json = {}
190+
191+
# Load pnpm settings from root package.json for pnpm <= v9.
188192
if label_store.has("package_json_root"):
193+
# Load a declared root package.json
189194
root_package_json_path = label_store.path("package_json_root")
190-
priv["root_package_json"] = json.decode(rctx.read(root_package_json_path))
195+
root_package_json = json.decode(rctx.read(root_package_json_path))
191196
elif "." in priv["importers"].keys():
192197
# Load an undeclared package.json derived from the root importer in the lockfile
193198
label_store.add_sibling("lock", "package_json_root", PACKAGE_JSON_FILENAME)
194199
root_package_json_path = label_store.path("package_json_root")
195-
priv["root_package_json"] = json.decode(rctx.read(root_package_json_path))
196-
else:
197-
# if there is no root importer that means there is no root package.json to read; pnpm allows
198-
# you to just have a pnpm-workspaces.yaml at the root and no package.json at that location
199-
priv["root_package_json"] = {}
200+
root_package_json = json.decode(rctx.read(root_package_json_path))
201+
202+
priv["pnpm_settings"] = root_package_json.get("pnpm", {})
203+
204+
# Read settings from pnpm-workspace.yaml for pnpm v10+ (NOTE: pnpm 9-10+ has lockfile version 9).
205+
# Support scenario where pnpm-lock.yaml was never parsed and "lock_version" is not set.
206+
if label_store.has("pnpm_workspace"):
207+
pnpm_workspace_path = label_store.path("pnpm_workspace")
208+
if is_windows or utils.exists(rctx, pnpm_workspace_path):
209+
pnpm_workspace_json, workspace_parse_err = _yaml_to_json(rctx, str(pnpm_workspace_path), is_windows)
210+
211+
if workspace_parse_err == None:
212+
pnpm_workspace_settings, workspace_parse_err = pnpm.parse_pnpm_workspace_json(pnpm_workspace_json)
213+
214+
if pnpm_workspace_settings:
215+
priv["pnpm_settings"] = priv["pnpm_settings"] | pnpm_workspace_settings
216+
217+
if workspace_parse_err != None:
218+
should_update = _should_update_pnpm_lock(priv)
219+
msg = """
220+
{type}: pnpm-workspace.yaml parse error {error}`.
221+
""".format(type = "WARNING" if should_update else "ERROR", error = workspace_parse_err)
222+
223+
if should_update:
224+
# buildifier: disable=print
225+
print(msg)
226+
else:
227+
fail(msg)
200228

201229
################################################################################
202230
def _init_npmrc(priv, rctx, attr, label_store):
@@ -444,24 +472,33 @@ WARNING: Cannot determine home directory in order to load home `.npmrc` file in
444472
_load_npmrc(priv, rctx, home_npmrc_path)
445473

446474
################################################################################
447-
def _load_lockfile(priv, rctx, attr, pnpm_lock_path, is_windows):
448-
importers = {}
449-
packages = {}
450-
patched_dependencies = {}
451-
lock_parse_err = None
452-
475+
def _yaml_to_json(rctx, yaml_path, is_windows):
453476
host_yq = Label("@yq_{}//:yq{}".format(repo_utils.platform(rctx), ".exe" if is_windows else ""))
454477
yq_args = [
455478
str(rctx.path(host_yq)),
456-
str(pnpm_lock_path),
479+
str(yaml_path),
457480
"-o=json",
458481
]
459482
result = rctx.execute(yq_args)
460483
if result.return_code:
461-
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)
462-
else:
484+
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)
485+
486+
# NB: yq will return the string "null" if the yaml file is empty
487+
if result.stdout != "null":
488+
return result.stdout, None
489+
490+
return None, None
491+
492+
def _load_lockfile(priv, rctx, attr, pnpm_lock_path, is_windows):
493+
importers = {}
494+
packages = {}
495+
patched_dependencies = {}
496+
lock_parse_err = None
497+
498+
lockfile_content, lock_parse_err = _yaml_to_json(rctx, str(pnpm_lock_path), is_windows)
499+
if lock_parse_err == None:
463500
importers, packages, patched_dependencies, lock_parse_err = pnpm.parse_pnpm_lock_json(
464-
result.stdout if result.stdout != "null" else None, # NB: yq will return the string "null" if the yaml file is empty
501+
lockfile_content,
465502
attr.no_dev,
466503
attr.no_optional,
467504
)
@@ -507,7 +544,7 @@ def _patched_dependencies(priv):
507544
return priv["patched_dependencies"]
508545

509546
def _only_built_dependencies(priv):
510-
return _root_package_json(priv).get("pnpm", {}).get("onlyBuiltDependencies", None)
547+
return _pnpm_settings(priv).get("onlyBuiltDependencies", None)
511548

512549
def _num_patches(priv):
513550
return priv["num_patches"]
@@ -521,8 +558,8 @@ def _npm_auth(priv):
521558
def _root_package(priv):
522559
return priv["root_package"]
523560

524-
def _root_package_json(priv):
525-
return priv["root_package_json"]
561+
def _pnpm_settings(priv):
562+
return priv["pnpm_settings"]
526563

527564
################################################################################
528565
def _new(rctx_name, rctx, attr):
@@ -543,7 +580,7 @@ def _new(rctx_name, rctx, attr):
543580
"npm_registries": {},
544581
"packages": {},
545582
"root_package": attr.root_package,
546-
"root_package_json": {},
583+
"pnpm_settings": {},
547584
"patched_dependencies": {},
548585
"should_update_pnpm_lock": should_update_pnpm_lock,
549586
}

npm/private/pnpm.bzl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,29 @@ def _assert_lockfile_version(version, testonly = False):
322322
fail(msg)
323323
return msg
324324

325+
def _parse_pnpm_workspace_json(content):
326+
"""Parse the content of a pnpm-workspace.yaml file.
327+
328+
Args:
329+
content: pnpm-workspace.yaml file content as json
330+
331+
Returns:
332+
A tuple of (packages list, error string)
333+
"""
334+
if not content:
335+
return {}, None
336+
337+
parsed = json.decode(content)
338+
if not parsed:
339+
return {}, None
340+
341+
if not types.is_dict(parsed):
342+
return None, "pnpm-workspace should be a starlark dict"
343+
344+
return parsed, None
345+
325346
pnpm = struct(
326347
assert_lockfile_version = _assert_lockfile_version,
327348
parse_pnpm_lock_json = _parse_pnpm_lock_json,
349+
parse_pnpm_workspace_json = _parse_pnpm_workspace_json,
328350
)

npm/private/test/snapshots/npm_defs-no_dev.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.

npm/private/test/snapshots/npm_defs-no_optional.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.

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)