Skip to content

pnpm: enable enableGlobalVirtualStore to share deps across worktrees#50401

Draft
manzoorwanijk wants to merge 6 commits into
trunkfrom
update/pnpm-enable-global-virtual-store
Draft

pnpm: enable enableGlobalVirtualStore to share deps across worktrees#50401
manzoorwanijk wants to merge 6 commits into
trunkfrom
update/pnpm-enable-global-virtual-store

Conversation

@manzoorwanijk

@manzoorwanijk manzoorwanijk commented Jul 10, 2026

Copy link
Copy Markdown
Member

Another attempt for #48093

Proposed changes

Enables pnpm's global virtual store so separate checkouts — git worktrees in particular — share one set of installed packages instead of each building their own node_modules/.pnpm.

node_modules becomes symlinks into the pnpm store:

Before After
du -sh node_modules 2.5G 1.4M
pnpm install in a fresh worktree 74s 2s

(Install times measured in a fresh worktree of this repo against a warm store.)

Because packages now resolve to the store, outside the repo, three things that assumed otherwise needed fixing:

  • The monorepo container bind-mounted the repo at /workspace. The store symlinks are relative and encode the checkout's depth on the host, so at /workspace they all dangled. It now mounts the repo, the store, and the main .git/ at their host paths. The old /workspace/.pnpm-store vs /root/.pnpm-store split is gone: node_modules no longer contains hardlinked files, so the store doesn't need to share its mount.
  • jest's transformIgnorePatterns matched the first /node_modules/ in a path. CI installs pnpm under ~/setup-pnpm/node_modules/.bin/store/, so that matched the store's own path, and every package in it went untransformed — ESM-only deps failed with SyntaxError: Unexpected token 'export'. Now anchored to the last /node_modules/, the one before the package name, which is correct in either layout.
  • Storybook's vitest project: Vite won't serve files outside its allow list, so the store is now allowed alongside the workspace root.

Two smaller knock-ons: jetpack rsync had the rsh-proxy path hardcoded to /workspace, and the image's WORKDIR /workspace / baked safe.directory /workspace are now dead (the container sets safe.directory at runtime instead).

Notes on the docs' caveats:

  • The NODE_PATH/ESM caveat only affects hoisted deps. We set hoistPattern: [], so nothing is hoisted.
  • The docs say pnpm auto-disables this in CI. It does not when the setting is explicit in pnpm-workspace.yaml. CI is fine anyway — actions/setup-node caches the store, not node_modules.

⚠️ The first pnpm install after this merges purges and relinks node_modules. Interactively pnpm prompts; in a non-TTY it aborts with ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY, so scripted installs need --config.confirmModulesPurge=false. One-time, but worth flagging so it doesn't read as a broken install.

Related product discussion/links

Does this pull request change what data or activity we track or use?

No.

Testing instructions

Install and worktrees

  • Check out this branch and run pnpm install, confirming the node_modules removal prompt.
  • readlink node_modules/husky should point into your pnpm store, and node_modules/.pnpm/node_modules should not exist.
  • git worktree add --detach /tmp/wt-test HEAD && cd /tmp/wt-test && time pnpm install — should finish in seconds.
  • node -e "require.resolve('husky')" in both. Clean up with git worktree remove --force /tmp/wt-test.

Monorepo container

Most likely thing to regress, since node_modules is bind-mounted in. Needs an image with the pinned pnpm 11.5.2 — older ones ship pnpm 10, which ignores pnpm_config_store_dir (already a no-op before this PR). docker pull automattic/jetpack-monorepo:latest, or let the 24-hour image check refresh it.

  • tools/docker/bin/monorepo bash -c 'pwd; node -e "require.resolve(\"husky\")" && echo OK'pwd prints the repo's host path, resolution succeeds.
  • tools/docker/bin/monorepo git status — no "dubious ownership" error.
  • pnpm store path on the host and tools/docker/bin/monorepo pnpm store path should match.
  • Run tools/docker/bin/monorepo pnpm install, then pnpm install on the host: the second should be a fast no-op, no purge prompt, no lockfile churn.
  • Repeat from inside a worktree, which exercises the .git mounts.

Tests

  • pnpm exec jest in projects/js-packages/components and projects/js-packages/charts.
  • pnpm run storybook:test in projects/js-packages/storybook (build first — stale build-module artifacts cause unrelated failures).

Other

  • If you use jetpack rsync with RSYNC_PROXY_SOCKET, confirm a sync still works.
  • On a Linux host, confirm tools/docker/bin/monorepo git status succeeds. Docker Desktop on macOS maps bind-mount ownership to the container user, so git's "dubious ownership" check never fires there and the runtime safe.directory entry can't be exercised locally.

Symlink node_modules from the store-wide virtual store rather than a per-checkout
node_modules/.pnpm, so multiple checkouts (notably git worktrees) share one copy
and `pnpm install` in a fresh worktree is near-instant.

See https://pnpm.io/git-worktrees.

The docs' NODE_PATH/ESM caveat only applies to hoisted dependencies, and we set
`hoistPattern: []`, so nothing is hoisted and `node_modules/.pnpm/node_modules`
is still never created.

Note the first install after this lands purges and relinks node_modules.
With `enableGlobalVirtualStore`, node_modules is a tree of relative symlinks into
the pnpm store's `links/` dir. Those links encode the checkout's depth on the
host, so bind-mounting the repo at /workspace left every one of them dangling
inside the container and nothing resolved.

Mount the repo, and the pnpm store, at the same absolute paths the host uses, so
a single set of symlinks is valid on both sides. The store no longer needs to
share a mount with node_modules to preserve hardlinks, since under the global
virtual store node_modules contains no hardlinked files at all -- the hardlinks
live entirely inside the store.

Knock-on changes:

- The main .git/ is mounted at its host path too, so a worktree's `.git` file
  resolves natively.
- Mark the repo as a git safe.directory at runtime, replacing the image's baked
  `/workspace` entry without needing a rebuild.
- `jetpack rsync` derived the rsh-proxy script's path from the /workspace mount
  point; resolve it relative to the CLI instead.
The container no longer mounts the repo at /workspace, so `WORKDIR /workspace`
just left you in an empty directory and the baked `safe.directory /workspace`
covered a path that no longer exists.

`bin/monorepo` always passes `-w`, and it now marks the repo as a safe.directory
at runtime via GIT_CONFIG_*, which git honours as protected ("command" scope)
configuration.
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the update/pnpm-enable-global-virtual-store branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack update/pnpm-enable-global-virtual-store
bin/jetpack-downloader test jetpack-mu-wpcom-plugin update/pnpm-enable-global-virtual-store

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions github-actions Bot added [Tools] Development CLI The tools/cli to assist during JP development. Docker labels Jul 10, 2026
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Jetpack plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

@jp-launch-control

jp-launch-control Bot commented Jul 10, 2026

Copy link
Copy Markdown

Code Coverage Summary

No summary data is available for parent commit c8d63e7, so cannot calculate coverage changes. 😴

If that commit is a feature branch rather than a trunk commit, this is expected. Otherwise, this should be updated once coverage for c8d63e7 is available.

Full summary · PHP report · JS report

`enableGlobalVirtualStore` resolves packages into the pnpm store, outside the
monorepo. Two bits of test tooling assumed otherwise.

jest's `transformIgnorePatterns` matched the *first* `/node_modules/` in a path.
That used to be the one preceding the package name, but a store path can contain
its own -- CI installs pnpm under `~/setup-pnpm/node_modules/.bin/store/` -- so
the next segment was `.bin/`, which is in no unignore list, and every package in
the store went untransformed. ESM-only deps then blew up with
`SyntaxError: Unexpected token 'export'`. Anchor to the last `/node_modules/`
instead, which is the one before the package name in either layout.

Vite refuses to serve files outside its allow list, which defaults to the
workspace root. The Storybook vitest project now also allows the pnpm store, so
`@storybook/addon-vitest`'s setup file can be imported.
@github-actions github-actions Bot added [JS Package] Charts [JS Package] Storybook [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Tests] Includes Tests RNA labels Jul 10, 2026
`PnpmDeterministicModuleIdsPlugin` strips pnpm's paths out of module identifiers
so that module IDs don't change when a package is updated. It only recognised
the `node_modules/.pnpm/<pkg>@<ver>/node_modules/` layout.

Under `enableGlobalVirtualStore` packages live in the store instead, as
`<store>/links/<@scope|@>/<name>/<ver>/<hash>/node_modules/<path>`. That went
unrecognised, so the store's absolute path -- which includes `$HOME`, and
differs between a laptop and CI -- ended up hashed into every module ID.

Loaders are the reason this bites even a bundle of purely local sources:
`require.resolve( 'thread-loader' )` bakes an absolute realpath into each
module's identifier.

With this, module IDs are identical under both layouts, so the
i18n-check-webpack-plugin snapshots pass unchanged.
- The monorepo container no longer hard-requires pnpm on the host. Without it
  there is no host store to share, and no host node_modules pointing into one,
  so fall back to the container's own store as before.

- Mark the repo as a git safe.directory via the container's global config rather
  than `GIT_CONFIG_*`. The env vars are a single numbered list: ours discarded a
  caller's entries, and a caller's would have discarded ours.

- Anchor the global-virtual-store path pattern to the store's `v<N>` version
  directory, so a project path that merely contains `links/<a>/<b>/<c>/<hex>/
  node_modules/` is not rewritten.

@anomiex anomiex left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Most of the code changes here seem fine, except for a lot of AI over-verbose comments. I still oppose the config change.

* store-version directory keeps this from matching a project path that merely contains `links/`.
*/
const PNPM_GLOBAL_STORE_PATH_REGEXP =
/(?<=^|[|!])[^|!]*?\/v[0-9]+\/links\/[^/]+\/[^/]+\/[^/]+\/[0-9a-f]{32,}\/node_modules\/([^|!]+)/g;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should the [^/] in here be [^/!|]?

I suppose the same goes for the one above too.

You might also do like \/links\/@[^/]*\/ since, according to the comment above, that component always begins with an @.

Comment on lines +19 to +22
* With `enableGlobalVirtualStore`, packages live in the pnpm store instead of `node_modules/.pnpm`,
* as `<store>/v<N>/links/<@scope|@>/<name>/<version>/<hash>/node_modules/<path>`. The store is
* outside the monorepo, so its absolute path would otherwise leak into the identifier. The `v<N>`
* store-version directory keeps this from matching a project path that merely contains `links/`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI over-verbosity.

Suggested change
* With `enableGlobalVirtualStore`, packages live in the pnpm store instead of `node_modules/.pnpm`,
* as `<store>/v<N>/links/<@scope|@>/<name>/<version>/<hash>/node_modules/<path>`. The store is
* outside the monorepo, so its absolute path would otherwise leak into the identifier. The `v<N>`
* store-version directory keeps this from matching a project path that merely contains `links/`.
* With `enableGlobalVirtualStore`, packages live at
* `<store>/v<N>/links/<@scope|@>/<name>/<version>/<hash>/node_modules/<path>`.

Comment thread tools/docker/bin/monorepo
Comment on lines +140 to +145
# `enableGlobalVirtualStore` makes node_modules a tree of relative symlinks into the store's
# `links/` dir, so the store has to sit at the same absolute path inside the container as it does
# on the host. Together with mounting the repo at its host path (below), that keeps one set of
# symlinks valid on both sides.
# Without pnpm on the host there's no host store to share, and no host node_modules pointing into
# one. Let the container fall back to its own store, as it did before.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This one seems overly verbose too.

Comment thread tools/docker/bin/monorepo
# symlinks valid on both sides.
# Without pnpm on the host there's no host store to share, and no host node_modules pointing into
# one. Let the container fall back to its own store, as it did before.
PNPM_STORE_PATH="$( cd "$MONOREPO_ROOT" && command -v pnpm >/dev/null 2>&1 && pnpm store path 2>/dev/null )"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since this script is using bash, we can simplify this.

Suggested change
PNPM_STORE_PATH="$( cd "$MONOREPO_ROOT" && command -v pnpm >/dev/null 2>&1 && pnpm store path 2>/dev/null )"
PNPM_STORE_PATH="$( cd "$MONOREPO_ROOT" && command -v pnpm &>/dev/null && pnpm store path 2>/dev/null )"

Comment thread tools/docker/bin/monorepo
if [[ "$IS_WORKTREE" = "1" ]]; then
echo "[pnpm] store dir (host): $DOCKER_DATA_DIR/.pnpm-store"
fi
echo "[pnpm] store path (host and container): ${PNPM_STORE_PATH:-<container default>}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remember the variable may be unset. This might print like

[pnpm] store path (host and container): :-<container default>

That may be confusing.

Comment thread tools/docker/bin/monorepo
Comment on lines +168 to +170
# Mark the repo as a safe directory for git, which otherwise refuses to operate on a checkout owned
# by another user. `$DOCKER_DATA_DIR` is the container's HOME, so this lands in git's global config;
# passing it via `GIT_CONFIG_*` instead would clobber, and be clobbered by, a caller's own entries.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
# Mark the repo as a safe directory for git, which otherwise refuses to operate on a checkout owned
# by another user. `$DOCKER_DATA_DIR` is the container's HOME, so this lands in git's global config;
# passing it via `GIT_CONFIG_*` instead would clobber, and be clobbered by, a caller's own entries.
# Mark the repo as a safe directory for git, which otherwise refuses to operate on a checkout owned
# by another user. `$DOCKER_DATA_DIR` is the container's HOME, so this lands in git's global config.

// - @gravatar-com: for the lifted Gravatar component's hovercard styles
// - marked: esm-only
// - uuid: v14 went esm-only, so it needs transforming
// `(?!.*/node_modules/)` anchors to the last `node_modules`, the one before the package name. The pnpm store's own path may contain one too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// `(?!.*/node_modules/)` anchors to the last `node_modules`, the one before the package name. The pnpm store's own path may contain one too.
// `(?!.*/node_modules/)` handles pnpm's store with nested `node_modules` dirs.

Comment thread pnpm-workspace.yaml
Comment on lines +27 to +31
# Symlink node_modules from a store-wide virtual store instead of node_modules/.pnpm, so
# checkouts (notably git worktrees) share one copy. https://pnpm.io/git-worktrees
# The docs' NODE_PATH/ESM caveat only affects hoisted deps, and we hoist nothing.
enableGlobalVirtualStore: true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I still think that if people want this, they probably want it for everything and should set it in their global config. Meanwhile, not setting it here will avoid breaking people's workflows that make use of node_modules/.pnpm/ having everything, as discussed in #48093.

Suggested change
# Symlink node_modules from a store-wide virtual store instead of node_modules/.pnpm, so
# checkouts (notably git worktrees) share one copy. https://pnpm.io/git-worktrees
# The docs' NODE_PATH/ESM caveat only affects hoisted deps, and we hoist nothing.
enableGlobalVirtualStore: true

@anomiex

anomiex commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Other references to .pnpm that you may want to update to handle either:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants