feat: add virtualStoreOnly option to skip post-import linking#10965
feat: add virtualStoreOnly option to skip post-import linking#10965
Conversation
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
There was a problem hiding this comment.
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-onlyconfig/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 fetchto 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.
| if (!skipPostImportLinking) { | ||
| linkedToRoot = await symlinkDirectDependencies({ | ||
| directDependenciesByImporterId: symlinkedDirectDependenciesByImporterId!, | ||
| dedupe: Boolean(opts.dedupeDirectDeps), | ||
| filteredLockfile, | ||
| lockfileDir, |
pkg-manager/headless/src/index.ts
Outdated
| if (opts.enableModulesDir !== false && !skipPostImportLinking) { | ||
| const rootProjectDeps = !opts.dedupeDirectDeps ? {} : (directDependenciesByImporterId['.'] ?? {}) | ||
| /** Skip linking and due to no project manifest */ | ||
| if (!opts.ignorePackageManifest) { |
| // virtualStoreOnly skips post-import linking (symlinks, bins, hoisting, scripts) | ||
| // even if ignorePackageManifest handling changes in the future. | ||
| virtualStoreOnly: true, | ||
| } as InstallOptions) |
| if (extendedOpts.virtualStoreOnly && !extendedOpts.enableModulesDir) { | ||
| throw new PnpmError('CONFIG_CONFLICT_VIRTUAL_STORE_ONLY_WITH_NO_MODULES_DIR', | ||
| 'Cannot use virtualStoreOnly when enableModulesDir is false') | ||
| } |
pkg-manager/headless/src/index.ts
Outdated
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@zkochan Good points — addressed in 55a4aaa:
-
Removed
ignoreScripts = true— builds now run withvirtualStoreOnly. The inter-package symlinks (linkAllModules+linkAllPkgs) are not guarded byskipPostImportLinking, so they execute beforebuildModulesand packages can resolve their dependencies during build. -
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.yamlso 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. -
GVS on subsequent install —
writeModulesManifestnow always runs (even with virtualStoreOnly), so.modules.yamlcorrectly records the pending build state and empty hoist patterns. The metadata block was split so only bin linking is skipped, not state persistence.
There was a problem hiding this comment.
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
|
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>
Summary
Adds a new
virtualStoreOnlyconfig option that populates the virtual store (standard or GVS) without creating importer symlinks, hoisting, bin links, or running lifecycle scripts.virtual-store-onlyto types, Config interface, defaultsextendInstallOptions: validate againstenableModulesDir=false, forceignoreScripts=trueand empty hoist patterns when enabledskipPostImportLinkingflag guarding 7 post-import stepsbuildModules, bin linking, and lifecycle hookslink.ts: skip hoisting and symlink creationpnpm fetch: usesvirtualStoreOnlyinternallyrcOptionsTypesandinstallDepsPick typeCloses #10840
Test plan
virtualStoreOnlypopulates standard virtual store without importer symlinksvirtualStoreOnlywithenableModulesDir=falsethrows config errorvirtualStoreOnlywith GVS populates global virtual store without importer linksvirtualStoreOnlywithfrozenLockfilepopulates GVS without importer symlinksvirtualStoreOnlywithfrozenLockfilepopulates standard virtual store without importer symlinksvirtualStoreOnlysuppresses hoisting even with explicithoistPattern@pnpm/core,@pnpm/headless,@pnpm/plugin-commands-installation