Skip to content

Commit 47e4ca9

Browse files
committed
feat(npm): support pnpm publish for npm_package
1 parent 5a47903 commit 47e4ca9

7 files changed

Lines changed: 143 additions & 17 deletions

File tree

npm/private/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ js_library(
1111
visibility = ["//visibility:public"],
1212
)
1313

14+
js_library(
15+
name = "npm_publish_with_tool_mjs",
16+
srcs = ["npm_publish_with_tool.mjs"],
17+
visibility = ["//visibility:public"],
18+
)
19+
1420
bzl_library(
1521
name = "npm_package",
1622
srcs = ["npm_package.bzl"],

npm/private/npm_package.bzl

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,31 @@ def npm_package(
140140
include_runfiles = False,
141141
hardlink = "auto",
142142
publishable = False,
143+
publish_tool = "npm",
144+
pnpm_binary = "@pnpm//:pnpm",
143145
verbose = False,
144146
**kwargs):
145147
"""A macro that packages sources into a directory (a tree artifact) and provides an `NpmPackageInfo`.
146148
147149
This target can be used as the `src` attribute to `npm_link_package`.
148150
149151
With `publishable = True` the macro also produces a target `[name].publish`, that can be run to publish to an npm registry.
150-
Under the hood, this target runs `npm publish`. You can pass arguments to npm by escaping them from Bazel using a double-hyphen,
151-
for example: `bazel run //path/to:my_package.publish -- --tag=next`
152+
Under the hood, this target runs `npm publish` by default. You can pass arguments to npm by escaping them from Bazel using a double-hyphen,
153+
for example: `bazel run //path/to:my_package.publish -- --tag=next`.
154+
155+
To publish with pnpm instead, use `publish_tool = "pnpm"`:
156+
157+
```python
158+
npm_package(
159+
name = "pkg",
160+
srcs = [...],
161+
publishable = True,
162+
publish_tool = "pnpm",
163+
)
164+
```
165+
166+
pnpm publishing uses the configured `pnpm_binary` and can resolve pnpm workspace settings such as `catalog` entries from
167+
`pnpm-workspace.yaml`. Workspace protocol dependencies follow pnpm's own resolution requirements.
152168
153169
Files and directories can be arranged as needed in the output directory using
154170
the `root_paths`, `include_srcs_patterns`, `exclude_srcs_patterns` and `replace_prefixes` attributes.
@@ -210,7 +226,7 @@ def npm_package(
210226
211227
srcs: Files and/or directories or targets that provide `DirectoryPathInfo` to copy into the output directory.
212228
213-
args: Arguments that are passed down to `<name>.publish` target and `npm publish` command.
229+
args: Arguments that are passed down to `<name>.publish` target and the publish command.
214230
215231
data: Runtime / linktime npm dependencies of this npm package.
216232
@@ -409,11 +425,19 @@ def npm_package(
409425
410426
publishable: When True, enable generation of `{name}.publish` target
411427
428+
publish_tool: Package manager to use for the `{name}.publish` target. Allowed values are `"npm"` and `"pnpm"`.
429+
430+
pnpm_binary: Label of the pnpm binary used when `publish_tool = "pnpm"`. This is typically `@pnpm//:pnpm`
431+
from the `pnpm` extension in `@aspect_rules_js//npm:extensions.bzl`.
432+
412433
verbose: If true, prints out verbose logs to stdout
413434
414435
**kwargs: Additional attributes such as `tags` and `visibility`
415436
"""
416437

438+
if publish_tool not in ["npm", "pnpm"]:
439+
fail("publish_tool must be one of 'npm' or 'pnpm', got '{}'".format(publish_tool))
440+
417441
if include_sources or include_transitive_sources or include_types or include_transitive_types or include_runfiles:
418442
files_target = "{}_files".format(name)
419443
_npm_package_files(
@@ -433,20 +457,37 @@ def npm_package(
433457
srcs = srcs + [files_target]
434458

435459
if publishable:
436-
js_binary(
437-
name = "{}.publish".format(name),
438-
entry_point = Label("@aspect_rules_js//npm/private:npm_publish_mjs"),
439-
fixed_args = [
440-
"./$(rootpath :{})".format(name),
441-
],
442-
data = [name],
443-
# required to make npm to be available in PATH
444-
include_npm = True,
445-
args = args,
446-
tags = kwargs.get("tags", []) + ["manual"],
447-
testonly = kwargs.get("testonly", False),
448-
visibility = kwargs.get("visibility", None),
449-
)
460+
if publish_tool == "pnpm":
461+
js_binary(
462+
name = "{}.publish".format(name),
463+
entry_point = Label("@aspect_rules_js//npm/private:npm_publish_with_tool_mjs"),
464+
fixed_args = [
465+
"$(rootpath {})".format(pnpm_binary),
466+
"./$(rootpath :{})".format(name),
467+
],
468+
data = [name, pnpm_binary],
469+
# required to make npm available because pnpm may delegate registry operations to npm
470+
include_npm = True,
471+
args = args,
472+
tags = kwargs.get("tags", []) + ["manual"],
473+
testonly = kwargs.get("testonly", False),
474+
visibility = kwargs.get("visibility", None),
475+
)
476+
else:
477+
js_binary(
478+
name = "{}.publish".format(name),
479+
entry_point = Label("@aspect_rules_js//npm/private:npm_publish_mjs"),
480+
fixed_args = [
481+
"./$(rootpath :{})".format(name),
482+
],
483+
data = [name],
484+
# required to make npm to be available in PATH
485+
include_npm = True,
486+
args = args,
487+
tags = kwargs.get("tags", []) + ["manual"],
488+
testonly = kwargs.get("testonly", False),
489+
visibility = kwargs.get("visibility", None),
490+
)
450491

451492
_npm_package(
452493
name = name,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { spawnSync } from 'node:child_process'
2+
import path from 'node:path'
3+
4+
const [toolPath, packageDir, ...restArgs] = process.argv.slice(2)
5+
6+
if (!toolPath || !packageDir) {
7+
console.error(
8+
'Expected publish tool path and package directory arguments.',
9+
)
10+
process.exit(1)
11+
}
12+
13+
const publishCwd = process.env.BUILD_WORKSPACE_DIRECTORY || process.cwd()
14+
15+
const spawn = spawnSync(
16+
path.resolve(toolPath),
17+
['publish', path.resolve(packageDir), ...restArgs],
18+
{
19+
cwd: publishCwd,
20+
env: {
21+
...process.env,
22+
BAZEL_BINDIR: process.env.BAZEL_BINDIR || '.',
23+
},
24+
stdio: 'inherit',
25+
},
26+
)
27+
28+
process.exit(spawn.status ?? 1)

npm/private/test/npm_package_publish/BUILD.bazel

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,29 @@ npm_package(
2020
publishable = True,
2121
)
2222

23+
npm_package(
24+
name = "pkg_pnpm",
25+
srcs = [
26+
"pnpm_catalog/index.js",
27+
"pnpm_catalog/package.json",
28+
],
29+
publish_tool = "pnpm",
30+
publishable = True,
31+
root_paths = [package_name() + "/pnpm_catalog"],
32+
)
33+
2334
sh_test(
2435
name = "test",
2536
srcs = ["test.sh"],
2637
args = [
2738
"$(locations :pkg_a.publish)",
2839
"$(locations :pkg_b.publish)",
40+
"$(locations :pkg_pnpm.publish)",
2941
],
3042
data = [
3143
":pkg_a.publish",
3244
":pkg_b.publish",
45+
":pkg_pnpm.publish",
3346
],
3447
tags = [
3548
"no-remote-exec",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const value = 1
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "@mycorp/pkg-to-publish-pnpm",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"typescript": "catalog:"
6+
}
7+
}

npm/private/test/npm_package_publish/test.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
readonly PUBLISH_A="$1"
44
readonly PUBLISH_B="$2"
5+
readonly PUBLISH_PNPM="$3"
56

67
# assert that it prints package name from package.json to stderr,
78
# to ensure package directory is properly passed and npm can read it.
@@ -26,3 +27,32 @@ if [ $? != 0 ]; then
2627
echo "FAIL: expected 'npm error enoent Could not read package.json:' error, GOT: $(cat pub_b.log)"
2728
exit 1
2829
fi
30+
31+
readonly TMP_WORKSPACE="$(mktemp -d)"
32+
trap 'rm -rf "${TMP_WORKSPACE}"' EXIT
33+
34+
cat >"${TMP_WORKSPACE}/pnpm-workspace.yaml" <<'EOF'
35+
packages:
36+
- packages/*
37+
catalog:
38+
typescript: 5.9.3
39+
EOF
40+
41+
# Ensure pnpm publish can read workspace-level catalog settings when publishing
42+
# a generated package directory.
43+
BUILD_WORKSPACE_DIRECTORY="${TMP_WORKSPACE}" \
44+
"$PUBLISH_PNPM" --dry-run --json --no-git-checks >pub_pnpm.log 2>pub_pnpm.err
45+
46+
# shellcheck disable=SC2181
47+
if [ $? != 0 ]; then
48+
echo "FAIL: expected pnpm publish dry-run to pass, GOT stdout: $(cat pub_pnpm.log), stderr: $(cat pub_pnpm.err)"
49+
exit 1
50+
fi
51+
52+
cat pub_pnpm.log | grep '"name": "@mycorp/pkg-to-publish-pnpm"'
53+
54+
# shellcheck disable=SC2181
55+
if [ $? != 0 ]; then
56+
echo "FAIL: expected pnpm publish dry-run output to include package name, GOT: $(cat pub_pnpm.log)"
57+
exit 1
58+
fi

0 commit comments

Comments
 (0)