diff --git a/.changeset/skip-gvs-warm-reinstall.md b/.changeset/skip-gvs-warm-reinstall.md new file mode 100644 index 0000000000..47bf01527c --- /dev/null +++ b/.changeset/skip-gvs-warm-reinstall.md @@ -0,0 +1,6 @@ +--- +"@pnpm/installing.deps-restorer": patch +"pnpm": patch +--- + +Skip redundant internal linking during GVS warm reinstall when no packages were added. Also filter direct dependency directories by `hasBin` before reading manifests to avoid unnecessary package.json reads. diff --git a/installing/deps-restorer/src/index.ts b/installing/deps-restorer/src/index.ts index 596cbb2036..832789a290 100644 --- a/installing/deps-restorer/src/index.ts +++ b/installing/deps-restorer/src/index.ts @@ -387,6 +387,7 @@ export async function headlessInstall (opts: HeadlessOptions): Promise fetching).length + const skipGvsInternalLinking = opts.enableGlobalVirtualStore === true && added === 0 statsLogger.debug({ added, prefix: lockfileDir, @@ -429,28 +430,30 @@ export async function headlessInstall (opts: HeadlessOptions): Promise fs.mkdir(depNode.modules, { recursive: true }))) - } - await Promise.all([ - opts.symlink === false || opts.enableModulesDir === false - ? Promise.resolve() - : linkAllModules(depNodes, { - optional: opts.include.optionalDependencies, + if (!skipGvsInternalLinking) { + if (opts.enableModulesDir !== false) { + await Promise.all(depNodes.map(async (depNode) => fs.mkdir(depNode.modules, { recursive: true }))) + } + await Promise.all([ + opts.symlink === false || opts.enableModulesDir === false + ? Promise.resolve() + : linkAllModules(depNodes, { + optional: opts.include.optionalDependencies, + }), + linkAllPkgs(opts.storeController, depNodes, { + allowBuild, + force: opts.force, + disableRelinkLocalDirDeps: opts.disableRelinkLocalDirDeps, + depGraph: graph, + depsStateCache, + enableGlobalVirtualStore: opts.enableGlobalVirtualStore, + ignoreScripts: opts.ignoreScripts, + lockfileDir: opts.lockfileDir, + sideEffectsCacheRead: opts.sideEffectsCacheRead, + storeDir: opts.storeDir, }), - linkAllPkgs(opts.storeController, depNodes, { - allowBuild, - force: opts.force, - disableRelinkLocalDirDeps: opts.disableRelinkLocalDirDeps, - depGraph: graph, - depsStateCache, - enableGlobalVirtualStore: opts.enableGlobalVirtualStore, - ignoreScripts: opts.ignoreScripts, - lockfileDir: opts.lockfileDir, - sideEffectsCacheRead: opts.sideEffectsCacheRead, - storeDir: opts.storeDir, - }), - ]) + ]) + } stageLogger.debug({ prefix: lockfileDir, @@ -493,7 +496,7 @@ export async function headlessInstall (opts: HeadlessOptions): Promise graph[dir] == null || graph[dir].hasBin) await linkBinsOfPackages( ( await Promise.all( diff --git a/pnpm/test/install/globalVirtualStore.ts b/pnpm/test/install/globalVirtualStore.ts index 0d67e3a684..6a561838cc 100644 --- a/pnpm/test/install/globalVirtualStore.ts +++ b/pnpm/test/install/globalVirtualStore.ts @@ -75,3 +75,34 @@ test('approve-builds updates GVS symlinks and runs builds at correct hash direct const workspaceManifest = readYamlFileSync(path.resolve('pnpm-workspace.yaml')) // eslint-disable-line expect(workspaceManifest.allowBuilds?.['@pnpm.e2e/pre-and-postinstall-scripts-example']).toBe(true) }) + +test('warm GVS reinstall skips internal linking', async () => { + prepare({ + dependencies: { + '@pnpm.e2e/hello-world-js-bin': '*', + '@pnpm.e2e/pkg-with-1-dep': '100.0.0', + }, + }) + const storeDir = path.resolve('store') + writeYamlFileSync(path.resolve('pnpm-workspace.yaml'), { + enableGlobalVirtualStore: true, + storeDir, + privateHoistPattern: '*', + }) + + // First install — warms GVS store + await execPnpm(['install']) + + // Delete node_modules + fs.rmSync(path.resolve('node_modules'), { recursive: true }) + + // Reinstall with frozen lockfile — should skip internal GVS linking + await execPnpm(['install', '--frozen-lockfile']) + + // Verify structure is correct after warm reinstall + expect(fs.existsSync(path.resolve('node_modules/@pnpm.e2e/hello-world-js-bin/package.json'))).toBeTruthy() + expect(fs.existsSync(path.resolve('node_modules/@pnpm.e2e/pkg-with-1-dep/package.json'))).toBeTruthy() + expect(fs.existsSync(path.resolve('node_modules/.pnpm/node_modules/@pnpm.e2e/dep-of-pkg-with-1-dep'))).toBeTruthy() + expect(fs.existsSync(path.resolve('node_modules/.bin/hello-world-js-bin'))).toBeTruthy() + expect(fs.existsSync(path.resolve('node_modules/.pnpm/lock.yaml'))).toBeTruthy() +})