Skip to content

feat(npm): add pnpm_package rule for pnpm workspace protocol resolution#2897

Open
kirkobyte wants to merge 15 commits into
aspect-build:mainfrom
kirkobyte:kirkh/silly-kowalevski-835eea
Open

feat(npm): add pnpm_package rule for pnpm workspace protocol resolution#2897
kirkobyte wants to merge 15 commits into
aspect-build:mainfrom
kirkobyte:kirkh/silly-kowalevski-835eea

Conversation

@kirkobyte

@kirkobyte kirkobyte commented Jun 28, 2026

Copy link
Copy Markdown

Summary

Adds pnpm_package — a Bazel macro for building publishable npm packages in pnpm workspaces. It wraps npm_package with a build-time transform that resolves pnpm workspace protocols (catalog:, workspace:, etc.) in package.json, mirroring what pnpm publish does before packing.

Parts of this can be performed solely through pnpm publish, however dynamic versions of packages (derived from Git, etc.) cannot.

What this enables

In a pnpm monorepo, workspace packages may use a package.json like this:

{
  "name": "@myorg/components",
  // Can either be the actual version, or a placeholder, with Bazel rules determining the
  // actual version e.g. by referencing version from git tags, like `semantic-release`
  "version": "0.0.0-development",

  // Points to source for in-IDE typechecking/navigation
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": "./src/index.ts"
  },

  // At publish time, pnpm promotes these fields to top-level,
  // so the published package points to compiled output
  "publishConfig": {
    "main": "./dist/index.js",
    "types": "./dist/index.d.ts",
    "exports": {
      ".": {
        "types": "./dist/index.d.ts",
        "import": "./dist/index.mjs",
        "require": "./dist/index.js"
      }
    }
  },

  "dependencies": {
    // Resolved from the "default" catalog in pnpm-workspace.yaml
    "react": "catalog:",
    // Resolved from the named "utils" catalog
    "lodash": "catalog:utils",
    // Resolved to the sibling package's current version (e.g. "^1.2.3")
    "@myorg/utils": "workspace:^"
  }
}

pnpm_package handles all of these transforms at build time:

  • publishConfig promotionexports, main, types, etc. are promoted to top-level fields
  • catalog: resolution — resolved from pnpm-workspace.yaml catalog definitions
  • workspace: resolution — resolved to the actual version of the sibling package
  • Version override"0.0.0-development" is replaced with the real version (from a git tag, stamp, or explicit string)
  • CleanupdevDependencies, publish lifecycle scripts, and pnpm-specific fields are stripped

IDE vs build entrypoints

The current making the editor happy advice requires adding paths to tsconfig.json for the IDE to resolve types for peer packages within the IDE, as the entrypoints will be pointing to compiled paths that do not exist on disk.

The provided example is fairly straightforward:

https://github.com/aspect-build/rules_ts/blob/74d54bda208695d7e8992520e560166875cfbce7/examples/simple/tsconfig.json#L8-L10

However if packages under @myorg/ are not in the same directory, or the exports from packages within @myorg differ from the folder structure, a more complex set of rules will be required. Making this more difficult to maintain in a distributed way.

If we instead follow PNPM's lead in using publishConfig, we can make this simpler. Separate values can be used for main/module/exports/types in the root package.json and publishConfig. This allows centrally declaring a separate set of values that should be used by the IDE vs in actual builds.

Auto-swapping protocols for versions (workspace:, catalog:, etc.)

Versions to use for catalog: and workpsace:dependnecies are generated automatically by npm_translate_lock. So a minimal pnpm_package target requires no extra setup to automatically find and swap in these versions for both local builds and publish events:

pnpm_package(
    name = "components",
    srcs = [":lib"],
)

Git-tag versioning

To allow packages to use git tags to version themselves, a rule could extract the git tag into a file label and pass that to pnpm_package's version parameter.

E.g. with a rule pnpm_git_tag_version that searches for git tags following <prefix>@<semver> we could set up a rule like this, with consuming packages correctly resolving the workspace: dependency to the actual version.

pnpm_git_tag_version(
    name = "version",
    prefix = "@myorg/components",
    fallback = "0.0.0-development",
)

pnpm_package(
    name = "components",
    srcs = [":lib"],
    version = ":version",
    publishable = True,
)

Automatic workspace version resolution via aspect

When workspace packages use placeholder versions like "0.0.0-development", the static workspace_versions.json from npm_translate_lock will contain the placeholder. The PnpmPackageInfo provider and _collect_workspace_versions_aspect solve this:

  • Each pnpm_package transform provides PnpmPackageInfo containing its transformed package.json (with the real resolved version)
  • An aspect on srcs traverses srcs/deps/src attrs transitively to auto-discover PnpmPackageInfo from workspace deps
  • Discovered versions override the base workspace_versions.json, so consumers resolving workspace:^ get the correct build-time version

For deps not reachable via the srcs graph, use explicit workspace_deps:

pnpm_package(
    name = "my_app",
    srcs = [":lib"],
    workspace_deps = [
        "//packages/components",
        "//packages/utils",
    ],
)

Publishing

With publishable = True, a .publish target is created that invokes pnpm publish:

bazel run //packages/components:components.publish
bazel run //packages/components:components.publish -- --tag=next --dry-run

Changes

  • pnpm_package macro wrapping npm_package with pnpm_package_json_transform
  • pnpm_package_json_transform rule + Node.js script for protocol resolution, publishConfig promotion, cleanup
  • PnpmPackageInfo provider + _collect_workspace_versions_aspect for automatic workspace version discovery
  • npm_translate_lock now generates catalogs.json and workspace_versions.json in the @npm repo
  • pnpm_git_tag_version example rule for extracting versions from git tags
  • _pnpm_publish rule for publishable targets
  • 7 tests covering catalogs, versioning, version files, workspace deps, aspect auto-discovery, and publishing

Test plan

  • test_extract_catalogs — verifies catalog extraction from pnpm-workspace.yaml
  • test_pkg — basic catalog + workspace protocol resolution
  • test_pkg_versioned — version string override
  • test_pkg_version_file — version from file (git tag simulation)
  • test_workspace_deps — explicit workspace_deps overrides placeholder version ("0.0.0-development" → "3.2.1")
  • test_aspect_auto_discover — aspect auto-discovers PnpmPackageInfo through dep graph
  • test_publish — publishable target creates working launcher script

🤖 Generated with Claude Code

kirkobyte and others added 4 commits June 28, 2026 20:45
Adds a new `pnpm_package` macro that wraps `npm_package` with build-time
resolution of pnpm workspace protocols (catalog:, workspace:*, etc.) in
package.json, mirroring what `pnpm publish` does before packing. Uses
bundled @pnpm/* packages with auto-detection of pnpm 10/11 at runtime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests catalog extraction, catalog protocol resolution (default and named),
version override, and publishable target builds. Fixes action conflict when
multiple pnpm_package targets share a Bazel package by scoping the transform
output. Updates checked-in bundles to match Bazel-built rollup output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… deps

The new @pnpm/* dependencies added by pnpm_publish_tools are now
reflected in the generated @npm// workspace defs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…aged pnpm

The .publish target uses a Starlark rule that resolves both pnpm from
@pnpm//:pnpm and npm from the Node.js runtime toolchain, ensuring
pnpm publish works in sandboxed environments where pnpm internally
shells out to npm.

Also removes the unused pnpm_publish.mjs wrapper script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36116c1bf3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread npm/private/pnpm_package_json_transform.bzl
Comment thread npm/private/pnpm_package.bzl Outdated
Comment thread npm/private/pnpm_extract_catalogs.bzl Outdated
kirkobyte and others added 11 commits June 28, 2026 21:34
pnpm publish and npm pack both omit devDependencies from the published
package. The transform now does the same.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite the catalog extraction and package.json transform as simple
Node.js scripts with no npm dependencies, eliminating the
pnpm_publish_tools workspace package, its rollup bundling pipeline,
and all @pnpm/* transitive dependencies from the lockfile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Handle all pnpm publish package.json transformations:
- Strip publish lifecycle scripts (prepublishOnly, prepare, etc.)
- Remove pnpm-specific fields (pnpm, packageManager)
- Promote publishConfig fields to top-level (full whitelist)
- Clean up publishConfig if empty after promotion
- Normalize bin (string → object keyed by unscoped package name)
- Normalize repository (string → {type, url} object)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The @pnpm/* dependencies are no longer pulled in, so the lockfile and
generated snapshots match main again.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the hand-rolled YAML parser and js_binary with a single yq
expression. yq is already a dependency of the repo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…s_binary

Remove manual npm toolchain resolution and PATH setup from _pnpm_publish.
The @pnpm//:pnpm js_binary already supports include_npm which handles npm
availability via its own launcher. Also forward version to upstream
npm_package so NpmPackageInfo.version stays aligned with the transformed
package.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The pnpm_package macro now auto-constructs the :node_modules label from
the calling package and passes it to the transform action. The transform
script reads package.json files from the node_modules tree to resolve
workspace:*/^/~ protocols to concrete versions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of reading workspace package versions from node_modules on disk,
extract them from NpmPackageStoreInfo carried by JsInfo on srcs and deps
targets. This avoids pulling in all of node_modules and makes workspace
protocol resolution automatic from the dependency graph.

Also adds a deps attribute so workspace deps not included in srcs can
still have their versions resolved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…translate_lock, add PnpmPackageInfo aspect for automatic version resolution

- npm_translate_lock now generates both catalogs.json (from pnpm-workspace.yaml
  catalog/catalogs sections) and workspace_versions.json (from workspace
  package.json files) in the @npm repo, eliminating the need for separate
  pnpm_extract_catalogs targets.

- pnpm_package defaults to @npm//:catalogs.json and @npm//:workspace_versions.json
  so no explicit configuration is needed for basic usage.

- Added PnpmPackageInfo provider and _collect_workspace_versions_aspect that
  traverses srcs/deps/src attrs to auto-discover workspace package versions.
  This ensures consumers resolving workspace: protocols get the correct
  build-time version even when source package.json uses a placeholder like
  "0.0.0-development".

- Added workspace_deps parameter for explicit version override when the
  aspect can't reach a dep through the srcs graph.

- Added pnpm_git_tag_version example rule for extracting versions from git tags.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix shell injection in pnpm_git_tag_version: pass prefix/fallback/output
  as positional arguments instead of interpolating into the shell command
- Fix stderr/stdout redirect race in test_publish.sh (>file 2>&1)
- Rename resolveVersion parameter to avoid shadowing outer `version` variable
- Deduplicate aspect-collected deps against explicit workspace_deps
- Remove unused loads (js_library, pnpm_package_json_transform) from test BUILD

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant