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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions