Skip to content

feat: add virtualStoreOnly option to skip post-import linking#10965

Merged
zkochan merged 3 commits intopnpm:mainfrom
vsumner:vsumner/virtual-store-only-v2
Mar 15, 2026
Merged

feat: add virtualStoreOnly option to skip post-import linking#10965
zkochan merged 3 commits intopnpm:mainfrom
vsumner:vsumner/virtual-store-only-v2

Conversation

@vsumner
Copy link
Copy Markdown
Contributor

@vsumner vsumner commented Mar 14, 2026

Summary

Adds a new virtualStoreOnly config option that populates the virtual store (standard or GVS) without creating importer symlinks, hoisting, bin links, or running lifecycle scripts.

  • Config: add virtual-store-only to types, Config interface, defaults
  • extendInstallOptions: validate against enableModulesDir=false, force ignoreScripts=true and empty hoist patterns when enabled
  • Headless: add skipPostImportLinking flag guarding 7 post-import steps
  • Core install: guard buildModules, bin linking, and lifecycle hooks
  • link.ts: skip hoisting and symlink creation
  • pnpm fetch: uses virtualStoreOnly internally
  • CLI: wire through rcOptionsTypes and installDeps Pick type

Closes #10840

Test plan

  • virtualStoreOnly populates standard virtual store without importer symlinks
  • virtualStoreOnly with enableModulesDir=false throws config error
  • virtualStoreOnly with GVS populates global virtual store without importer links
  • virtualStoreOnly with frozenLockfile populates GVS without importer symlinks
  • virtualStoreOnly with frozenLockfile populates standard virtual store without importer symlinks
  • virtualStoreOnly suppresses hoisting even with explicit hoistPattern
  • Type-checks pass for @pnpm/core, @pnpm/headless, @pnpm/plugin-commands-installation
  • Lint passes

Adds a new `virtualStoreOnly` config option that populates the virtual
store (standard or GVS) without creating importer symlinks, hoisting,
bin links, or running lifecycle scripts.

- Config: add virtual-store-only to types, Config interface, defaults
- extendInstallOptions: validate against enableModulesDir=false, force
  ignoreScripts=true and empty hoist patterns when enabled
- headless: add skipPostImportLinking flag guarding 7 post-import steps
- core install: guard buildModules, bin linking, and lifecycle hooks
- link.ts: skip hoisting and symlink creation
- fetch command: use virtualStoreOnly internally
- CLI: wire through rcOptionsTypes and installDeps Pick type

Closes pnpm#10840
@vsumner vsumner requested a review from zkochan as a code owner March 14, 2026 00:51
Copilot AI review requested due to automatic review settings March 14, 2026 00:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new virtualStoreOnly mode intended to populate the virtual store (standard or GVS) while skipping post-import behaviors like importer symlinks, hoisting, bin links, and lifecycle scripts—primarily to support pnpm fetch / store prepopulation workflows.

Changes:

  • Introduces virtual-store-only config/CLI option and threads it through config parsing and installation option types.
  • Implements “skip post-import linking” guards across core and headless install paths, and updates pnpm fetch to use this mode internally.
  • Adds core tests covering standard virtual store and GVS behavior under virtualStoreOnly (including a config-conflict case).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pkg-manager/plugin-commands-installation/src/installDeps.ts Adds virtualStoreOnly to install option pick set passed through CLI installation flows.
pkg-manager/plugin-commands-installation/src/install.ts Registers virtual-store-only as an rc/CLI option.
pkg-manager/plugin-commands-installation/src/fetch.ts Forces pnpm fetch to run installs with virtualStoreOnly: true.
pkg-manager/headless/src/index.ts Adds virtualStoreOnly option and guards multiple post-import linking/build/script steps.
pkg-manager/core/test/install/globalVirtualStore.ts Adds coverage for virtualStoreOnly behavior for both standard virtual store and GVS.
pkg-manager/core/src/install/link.ts Skips hoisting and root symlink creation when virtualStoreOnly is enabled.
pkg-manager/core/src/install/index.ts Skips builds, bin linking, and lifecycle hooks in virtualStoreOnly mode.
pkg-manager/core/src/install/extendInstallOptions.ts Adds option default, validation, and forces ignoreScripts + empty hoist patterns under virtualStoreOnly.
config/config/src/types.ts Adds virtual-store-only to config type definitions.
config/config/src/index.ts Sets default value for virtual-store-only.
config/config/src/configFileKey.ts Adds virtual-store-only to the excluded key list for global config file keys.
config/config/src/Config.ts Extends Config interface with virtualStoreOnly?: boolean.
.changeset/virtual-store-only.md Changeset documenting the new setting and that pnpm fetch uses it internally.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +417 to +422
if (!skipPostImportLinking) {
linkedToRoot = await symlinkDirectDependencies({
directDependenciesByImporterId: symlinkedDirectDependenciesByImporterId!,
dedupe: Boolean(opts.dedupeDirectDeps),
filteredLockfile,
lockfileDir,
Comment on lines 593 to 596
if (opts.enableModulesDir !== false && !skipPostImportLinking) {
const rootProjectDeps = !opts.dedupeDirectDeps ? {} : (directDependenciesByImporterId['.'] ?? {})
/** Skip linking and due to no project manifest */
if (!opts.ignorePackageManifest) {
Comment on lines 79 to 82
// virtualStoreOnly skips post-import linking (symlinks, bins, hoisting, scripts)
// even if ignorePackageManifest handling changes in the future.
virtualStoreOnly: true,
} as InstallOptions)
Comment on lines +318 to +321
if (extendedOpts.virtualStoreOnly && !extendedOpts.enableModulesDir) {
throw new PnpmError('CONFIG_CONFLICT_VIRTUAL_STORE_ONLY_WITH_NO_MODULES_DIR',
'Cannot use virtualStoreOnly when enableModulesDir is false')
}
Comment on lines +247 to +250
if (opts.virtualStoreOnly && opts.enableModulesDir === false) {
throw new PnpmError('CONFIG_CONFLICT_VIRTUAL_STORE_ONLY_WITH_NO_MODULES_DIR',
'Cannot use virtualStoreOnly when enableModulesDir is false')
}
'Cannot use virtualStoreOnly when enableModulesDir is false')
}
if (extendedOpts.virtualStoreOnly) {
extendedOpts.ignoreScripts = true
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am not confident the global virtual store will be correctly built on subsequent installation. If I am right, it probably should be fixed.

I would not mind to run the build when virtualStoreOnly is true. However, since you also disable hoisting, the build could fail in some cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@zkochan Good points — addressed in 55a4aaa:

  1. Removed ignoreScripts = true — builds now run with virtualStoreOnly. The inter-package symlinks (linkAllModules + linkAllPkgs) are not guarded by skipPostImportLinking, so they execute before buildModules and packages can resolve their dependencies during build.

  2. Hoisting concern — you're right that hoistPattern: [] means builds relying on hoisted packages could fail. But this is intentional: we record empty hoist patterns in .modules.yaml so a subsequent normal install knows hoisting must be redone from scratch. The alternative (hoisting during virtualStoreOnly) would defeat the purpose of the option. If a build needs hoisted deps, the user should run a normal install after the virtualStoreOnly pass.

  3. GVS on subsequent installwriteModulesManifest now always runs (even with virtualStoreOnly), so .modules.yaml correctly records the pending build state and empty hoist patterns. The metadata block was split so only bin linking is skipped, not state persistence.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hoisting concern

I am ok with this strictness but we will have to prominently document this in the description of the setting on the website.

- Remove ignoreScripts=true forcing (allow builds with virtualStoreOnly)
- Allow virtualStoreOnly + enableModulesDir=false when GVS is enabled
- Guard linkHoistedModules with skipPostImportLinking in hoisted branch
- Un-guard buildModules so lifecycle scripts can run with virtualStoreOnly
- Split metadata block so writeModulesManifest persists with virtualStoreOnly
- Add enableModulesDir=true to pnpm fetch to avoid config conflict
- Fix test bugs: dep version 100.0.0→100.1.0, globalVirtualStoreDir→virtualStoreDir
- Add test for virtualStoreOnly + enableModulesDir=false + GVS
- Relax headless import guard to allow GVS with enableModulesDir=false
@zkochan
Copy link
Copy Markdown
Member

zkochan commented Mar 15, 2026

Two tests are broken.

The tests hardcode dep-of-pkg-with-1-dep@100.1.0 in path assertions
but didn't call addDistTag to pin the latest version. Since test files
run concurrently, other tests can change the dist-tag to 100.0.0,
causing resolution to pick a different version and the path check to
fail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zkochan zkochan merged commit 09a999a into pnpm:main Mar 15, 2026
9 of 11 checks passed
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.

enableModulesDir: false prevents Global Virtual Store links/ from being populated

3 participants