Skip to content

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

@vsumner

Description

@vsumner

Bug

When using pnpm fetch (or pnpm install) with enableGlobalVirtualStore: true and enableModulesDir: false, the Global Virtual Store's links/ directory is never populated. The CAS (files/ and index/) is written correctly, but packages are not materialized into links/.

Expected behavior

enableModulesDir: false should skip creation of project-level node_modules/ directories and symlinks, but should still populate the store's links/ directory when GVS is enabled. The links/ directory is part of the store, not the project's node_modules/.

Actual behavior

Both links/ materialization and node_modules/ creation are skipped.

Root cause

In pkg-manager/headless/src/index.ts, linkAllPkgs is inside the enableModulesDir !== false guard (line ~415):

} else if (opts.enableModulesDir !== false) {
    await Promise.all(depNodes.map(async (depNode) => fs.mkdir(depNode.modules, { recursive: true })))
    await Promise.all([
        linkAllModules(depNodes, { ... }),       // project-level symlinks — correctly guarded
        linkAllPkgs(opts.storeController, depNodes, { ... }),  // CAS → target import — should NOT be guarded for GVS
    ])
}

linkAllPkgs calls storeController.importPackage(depNode.dir, ...) for each package. When GVS is enabled, depNode.dir resolves to a path inside {storeDir}/v11/links/ (computed via iteratePkgsForVirtualStore). This is store population, not project node_modules/ creation.

Because linkAllPkgs is guarded by enableModulesDir !== false, setting enableModulesDir: false prevents store-level GVS materialization as a side effect.

Suggested fix

Hoist linkAllPkgs out of the enableModulesDir conditional when GVS is enabled:

// Always import packages from CAS into their target dirs when GVS is enabled.
// When GVS is enabled, depNode.dir is in {storeDir}/v11/links/, not node_modules/.
if (opts.enableGlobalVirtualStore || opts.enableModulesDir !== false) {
    await linkAllPkgs(opts.storeController, depNodes, { ... })
}

if (opts.enableModulesDir !== false) {
    await Promise.all(depNodes.map(async (depNode) => fs.mkdir(depNode.modules, { recursive: true })))
    if (opts.symlink !== false) {
        await linkAllModules(depNodes, { ... })
    }
}

The importPackage implementation (tryImportIndexedDir in fs/indexed-pkg-importer) already creates the target directory via makeEmptyDir(newDir, { recursive: true }), so the mkdir(depNode.modules) at line 416 is only needed for project-level node_modules/ structure, not for GVS.

Use case

Nix-based build systems use pnpm fetch --offline with enableGlobalVirtualStore: true and enableModulesDir: false to pre-build a CAS + GVS store without creating unnecessary project-level node_modules/. Pre-populating links/ enables the GVS zero-fetch fast path (lockfileToDepGraph.ts:259-263) at dev time. Currently the workaround is to omit enableModulesDir: false, which creates an unnecessary node_modules/ that must be discarded.

Version

pnpm 11.0.0-alpha.12 (verified against current main)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions