* refactor: rename rebuildSelectedPkgs/rebuildProjects to buildSelectedPkgs/buildProjects
The "rebuild" prefix is redundant now that these functions live in
@pnpm/building.after-install.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: rename Rebuild option types to Build (RebuildOptions → BuildOptions, etc.)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: rename plugin-commands-rebuild and exec.build-commands to building domain
- @pnpm/plugin-commands-rebuild → @pnpm/building.build-commands
- @pnpm/exec.build-commands → @pnpm/building.policy-commands
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move build-modules and pkg-requires-build to building domain
- @pnpm/build-modules → @pnpm/building.during-install
- @pnpm/exec.pkg-requires-build → @pnpm/building.pkg-requires-build
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: alphabetically sort imports after package renames
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add changeset
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract rebuild implementation into @pnpm/building.after-install
Move the rebuild implementation (rebuildProjects, rebuildSelectedPkgs)
from @pnpm/plugin-commands-rebuild into a new @pnpm/building.after-install
package. This breaks the circular dependency chain:
config/deps-installer → core → plugin-commands-rebuild → cli-utils → config/deps-installer
The CLI command layer stays in plugin-commands-rebuild, while the core
rebuild logic now lives in building/after-install with no dependency
on cli-utils.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add README for @pnpm/building.after-install
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: rename @pnpm/builder.policy to @pnpm/building.policy
Move builder/policy to building/policy, consolidating all build-related
packages under the building/ domain. Update all consumers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add changeset
* refactor: remove old implementation files from plugin-commands-rebuild
These files were copied to @pnpm/building.after-install but not removed
from the original location.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: remove unused deps from plugin-commands-rebuild
Dependencies that were only needed by the implementation (now in
@pnpm/building.after-install) are removed. Deps used only in tests
are moved to devDependencies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: simplify patchedDependencies lockfile format to map selectors to hashes
Remove the `path` field from patchedDependencies in the lockfile, changing the
format from `Record<string, { path: string, hash: string }>` to
`Record<string, string>` (selector → hash). The path was never consumed from
the lockfile — patch file paths come from user config, not the lockfile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: migrate old patchedDependencies format when reading lockfile
When reading a lockfile with the old `{ path, hash }` format for
patchedDependencies, extract just the hash string. This ensures
backwards compatibility with existing lockfiles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: carry patchFilePath through patch groups for runtime patch application
The previous commit removed `path` from the lockfile format but also
accidentally dropped it from the runtime PatchInfo type. This broke
patch application since `applyPatchToDir` needs the file path.
- Add optional `patchFilePath` to `PatchInfo` for runtime use
- Build patch groups with resolved file paths in install
- Fix `build-modules` to use `patchFilePath` instead of `file.path`
- Fix `calcPatchHashes` call site in `checkDepsStatus` (extra arg)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update remaining references to old PatchFile type
- Update getPatchInfo tests to use { hash, key } instead of { file, key }
- Fix createDeployFiles to handle patchedDependencies as hash strings
- Fix configurationalDependencies test assertion
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: throw when patch exists but patchFilePath is missing
Also guard against undefined patchedDependencies entry when
ignorePackageManifest is true.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: don't join lockfileDir with already-absolute patch file paths
opts.patchedDependencies values are already absolute paths, so
path.join(opts.lockfileDir, absolutePath) created invalid doubled
paths like /project/home/runner/work/pnpm/...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use path.resolve for patch file paths and address Copilot review
- Use path.resolve instead of path.join to correctly handle both
relative and absolute patch file paths
- Use PnpmError instead of plain Error for missing patch file path
- Only copy patchedDependencies to deploy output when manifest
provides the patch file paths
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: pass rootProjectManifest in deploy patchedDependencies test
The test was missing rootProjectManifest, so createDeployFiles could
not find the manifest's patchedDependencies to propagate to the
deploy output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(calc-dep-state): use allowBuilds to compute engine-agnostic GVS hashes
Use the allowBuilds config to determine which packages need ENGINE_NAME
in their GVS hash. Packages that are not allowed to build (and don't
transitively depend on packages that are) now get engine-agnostic hashes,
so they survive Node.js upgrades and architecture changes without
re-import.
Closes#10837
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(modules-yaml): persist allowBuilds and re-link GVS on change
Store the allowBuilds config in modules.yaml so that when it changes
between installs, the headless installer detects the difference and
re-processes all packages with updated GVS hashes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: deduplicate computeBuiltDepPaths into iterateHashedGraphNodes
Move builtDepPaths computation inside iterateHashedGraphNodes, which now
accepts an AllowBuild function instead of a precomputed Set. This
eliminates duplicate logic in iteratePkgsForVirtualStore and
resolve-dependencies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(gvs): recover from failed or interrupted builds using .pnpm-needs-build marker
When a GVS package needs building, a .pnpm-needs-build marker file is added
to the filesMap before import. The import pipeline treats it as a regular file,
so it's atomically included in the staged directory and renamed with the
package. On the next install, GVS fast paths detect the marker and force a
re-fetch/re-import/re-build.
On build success, the marker is removed. On build failure, the entire hash
directory is removed so the next install starts fresh.
The marker is only checked for packages that are allowed to build (via
allowBuild), minimizing filesystem operations. It is also skipped when cached
side effects will be applied, since the package is already built.
Closes#10837
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove .pnpm-needs-build marker before uploading side effects
Move the marker removal before the side effects upload so the marker
file is not included in the side effects diff. Add a test assertion
that verifies the marker does not appear in the cached side effects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(calc-dep-state): return undefined from computeBuiltDepPaths when allowBuild is not configured
Previously, computeBuiltDepPaths returned an empty Set when allowBuild
was undefined, causing all GVS hashes to become engine-agnostic even
without allowBuilds configured. Now the function is only called when
allowBuild is provided, and iterateHashedGraphNodes avoids materializing
the iterator when it's not needed.
Also restore upfront filtering in extendGraph so non-GVS installs only
hash runtime dep paths, and only pass allowBuild when GVS is on.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
Replace individual `.mpk` (MessagePack) files under `$STORE/index/` with a single SQLite database at `$STORE/index.db` using Node.js 22's built-in `node:sqlite` module. This reduces filesystem syscall overhead and improves space efficiency for small metadata entries.
Closes#10826
## Design
### New package: `@pnpm/store.index`
A new `StoreIndex` class wraps a SQLite database with a simple key-value API (`get`, `set`, `delete`, `has`, `entries`). Data is serialized with msgpackr and stored as BLOBs. The table uses `WITHOUT ROWID` for compact storage.
Key design decisions:
- **WAL mode** enables concurrent reads from workers while the main process writes.
- **`busy_timeout=5000`** plus a retry loop with `Atomics.wait`-based `sleepSync` handles `SQLITE_BUSY` errors from concurrent access.
- **Performance PRAGMAs**: `synchronous=NORMAL`, `mmap_size=512MB`, `cache_size=32MB`, `temp_store=MEMORY`, `wal_autocheckpoint=10000`.
- **Write batching**: `queueWrites()` batches pre-packed entries from tarball extraction and flushes them in a single transaction on `process.nextTick`. `setRawMany()` writes immediate batches (e.g. from `addFilesFromDir`).
- **Lifecycle**: `close()` auto-flushes pending writes, runs `PRAGMA optimize`, and closes the DB. A `process.on('exit')` handler ensures cleanup even on unexpected exits.
- **`VACUUM` after `deleteMany`** (used by `pnpm store prune`) to reclaim disk space.
### Key format
Keys are `integrity\tpkgId` (tab-separated). Git-hosted packages use `pkgId\tbuilt` or `pkgId\tnot-built`.
### Shared StoreIndex instance
A single `StoreIndex` instance is threaded through the entire install lifecycle — from `createNewStoreController` through the fetcher chain, package requester, license scanner, SBOM collector, and dependencies hierarchy. This replaces the previous pattern of each component creating its own file-based index access.
### Worker architecture
Index writes are performed in the main process, not in worker threads. Workers send pre-packed `{ key, buffer }` pairs back to the main process via `postMessage`, where they are batched and flushed to SQLite. This avoids SQLite write contention between threads.
### SQLite ExperimentalWarning suppression
`node:sqlite` emits an `ExperimentalWarning` on first load. This is suppressed via a `process.emitWarning` override injected through esbuild's `banner` option, which runs on line 1 of both `dist/pnpm.mjs` and `dist/worker.js` — before any module that loads `node:sqlite`.
### No migration from `.mpk` files
Old `.mpk` index files are not migrated. Packages missing from the new SQLite index are re-fetched on demand (the same behavior as a fresh store).
## Changed packages
121 files changed across these areas:
- **`store/index/`** — New `@pnpm/store.index` package
- **`worker/`** — Write batching moved from worker module into `StoreIndex` class; workers send pre-packed buffers to main process
- **`store/package-store/`** — StoreIndex creation and lifecycle management
- **`store/cafs/`** — Removed `getFilePathInCafs` index-file utilities (no longer needed)
- **`store/pkg-finder/`** — Reads from StoreIndex instead of `.mpk` files
- **`store/plugin-commands-store/`** — `store status` uses StoreIndex
- **`store/plugin-commands-store-inspecting/`** — `cat-index` and `find-hash` use StoreIndex
- **`fetching/tarball-fetcher/`** — Threads StoreIndex through fetchers; git-hosted fetcher flushes before reading
- **`fetching/git-fetcher/`, `binary-fetcher/`, `pick-fetcher/`** — Accept StoreIndex parameter
- **`pkg-manager/`** — `client`, `core`, `headless`, `package-requester` thread StoreIndex
- **`reviewing/`** — `license-scanner`, `sbom`, `dependencies-hierarchy` accept StoreIndex
- **`cache/api/`** — Cache view uses StoreIndex
- **`pnpm/bundle.ts`** — esbuild banner for ExperimentalWarning suppression
## Test plan
- [x] `pnpm --filter @pnpm/store.index test` — Unit tests for StoreIndex CRUD and batching
- [x] `pnpm --filter @pnpm/package-store test` — Store controller lifecycle
- [x] `pnpm --filter @pnpm/package-requester test` — Package requester reads from SQLite index
- [x] `pnpm --filter @pnpm/tarball-fetcher test` — Tarball and git-hosted fetcher writes
- [x] `pnpm --filter @pnpm/headless test` — Headless install
- [x] `pnpm --filter @pnpm/core test` — Core install, side effects, patching
- [x] `pnpm --filter @pnpm/plugin-commands-rebuild test` — Rebuild reads from index
- [x] `pnpm --filter @pnpm/license-scanner test` — License scanning
- [x] e2e tests pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Replace all sinon.spy/match/withArgs usage with jest.fn, toHaveBeenCalledWith,
mockClear, and mock.calls filtering. Remove sinon and @types/sinon from all
package.json files and the workspace catalog.
* feat: use global virtual store for global packages and dlx
* fix(config): remove unnecessary virtualStoreDir override for global installs
When the global virtual store is disabled, the default `node_modules/.pnpm`
path works fine — no need to explicitly override it to `.pnpm`.
**TLDR:** Global packages in pnpm v10 are annoying and slow because they all are installed to a single global package. Instead, we will now use a system that is similar to the one used by "pnpm dlx" (aka "pnpx").
Each globally installed package (or group of packages installed together) now gets its own isolated installation directory with its own `package.json`, `node_modules`, and lockfile. This prevents global packages from interfering with each other through peer dependency conflicts or version resolution shifts.
## Changes
- Add `@pnpm/global-packages` shared utilities package for scanning, hashing, and managing isolated global installs
- `pnpm add -g` creates isolated installs in `{pnpmHomeDir}/global/v11/{hash}/`
- `pnpm remove -g` removes the entire installation group containing the package
- `pnpm update -g` re-installs into new isolated directories and swaps symlinks
- `pnpm list -g` scans isolated directories to show installed global packages
- `pnpm outdated -g` checks each isolated installation for outdated dependencies
- `pnpm store prune` cleans up orphaned global installation directories
## Breaking changes
- `pnpm install -g` (no args) is no longer supported — use `pnpm add -g <pkg>`
- `pnpm link <pkg-name>` no longer resolves packages from the global store — only relative or absolute paths are accepted
- `pnpm link --global` is removed — use `pnpm add -g .` to register a local package's bins globally
* fix(dlx): print help message on calling pnpm dlx without arguments
Running `pnpm dlx` with no arguments would crash Node.js with a
TypeError as it attempted to call `.indexOf()` on an undefined variable.
This commit adds a guard clause and displays the help message instead
and exits gracefully.
Fixes#10633
* refactor: dlx
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Allow approving all pending build dependencies at once without
interactive selection, useful for CI/CD pipelines and project
bootstrapping scenarios where interactive prompts are not feasible.
close#10136
* test: use `import type` in more places
Several tests are failing because a module isn't being mocked. This is
due to the mocked module being imported before the mock being set up.
Switching to `import type` should elide the import fully.
* build: replace ts-jest with simple transformer
* chore: remove `ts-jest`
* chore: remove babel dependencies from root project
* ci: use Node.js 22.13.0 (instead of 22.12.0)
Node.js 22.13.0 introduces the `stripTypeScriptTypes` function
* fix: copilot feedback
* fix(run): fail when no packages have script in filtered recursive run
Previously, `pnpm run -r <script>` and `pnpm run --filter <filter> <script>`
would silently succeed with exit code 0 when no packages had the specified
script, as long as a filter was used. This was inconsistent with the
documentation which states "If none of the packages have the command, the
command fails."
This change makes the command fail with ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT in
all cases where no packages have the script, regardless of whether a filter
is used. The `--if-present` flag can be used to suppress this error.
close#6844