diff --git a/.changeset/rare-spoons-play.md b/.changeset/rare-spoons-play.md new file mode 100644 index 0000000000..da76aba3bf --- /dev/null +++ b/.changeset/rare-spoons-play.md @@ -0,0 +1,6 @@ +--- +"@pnpm/headless": patch +"pnpm": patch +--- + +If an external tool or a user have removed a package from node_modules, pnpm should add it back on install. This was only an issue with `node-linker=hoisted`. diff --git a/pkg-manager/core/test/hoistedNodeLinker/install.ts b/pkg-manager/core/test/hoistedNodeLinker/install.ts index f8aeb0e98e..1f3054488b 100644 --- a/pkg-manager/core/test/hoistedNodeLinker/install.ts +++ b/pkg-manager/core/test/hoistedNodeLinker/install.ts @@ -3,6 +3,7 @@ import path from 'path' import { addDependenciesToPackage, install, mutateModules, mutateModulesInSingleProject } from '@pnpm/core' import { prepareEmpty } from '@pnpm/prepare' import { addDistTag } from '@pnpm/registry-mock' +import rimraf from '@zkochan/rimraf' import { sync as loadJsonFile } from 'load-json-file' import { sync as readYamlFile } from 'read-yaml-file' import symlinkDir from 'symlink-dir' @@ -11,13 +12,14 @@ import { testDefaults } from '../utils' test('installing with hoisted node-linker', async () => { prepareEmpty() - await install({ + const manifest = { dependencies: { send: '0.17.2', 'has-flag': '1.0.0', ms: '1.0.0', }, - }, await testDefaults({ + } + await install(manifest, await testDefaults({ nodeLinker: 'hoisted', })) @@ -27,6 +29,13 @@ test('installing with hoisted node-linker', async () => { expect(fs.existsSync('node_modules/send/node_modules/ms')).toBeTruthy() expect(readYamlFile<{ nodeLinker: string }>('node_modules/.modules.yaml').nodeLinker).toBe('hoisted') + + // If a package from node_modules is removed, it should be re-added. + await rimraf('node_modules/send') + await install(manifest, await testDefaults({ + nodeLinker: 'hoisted', + })) + expect(fs.realpathSync('node_modules/send')).toEqual(path.resolve('node_modules/send')) }) test('installing with hoisted node-linker and no lockfile', async () => { diff --git a/pkg-manager/headless/src/lockfileToHoistedDepGraph.ts b/pkg-manager/headless/src/lockfileToHoistedDepGraph.ts index 026c66e459..0d76eeb118 100644 --- a/pkg-manager/headless/src/lockfileToHoistedDepGraph.ts +++ b/pkg-manager/headless/src/lockfileToHoistedDepGraph.ts @@ -1,3 +1,4 @@ +import pathExists from 'path-exists' import path from 'path' import { Lockfile, @@ -181,7 +182,12 @@ async function fetchDeps ( const depLocation = path.relative(opts.lockfileDir, dir) const resolution = pkgSnapshotToResolution(depPath, pkgSnapshot, opts.registries) let fetchResponse!: ReturnType - const skipFetch = opts.currentHoistedLocations?.[depPath]?.includes(depLocation) + // We check for the existence of the package inside node_modules. + // It will only be missing if the user manually removed it. + // That shouldn't normally happen but Bit CLI does remove node_modules in component directories: + // https://github.com/teambit/bit/blob/5e1eed7cd122813ad5ea124df956ee89d661d770/scopes/dependencies/dependency-resolver/dependency-installer.ts#L169 + const skipFetch = opts.currentHoistedLocations?.[depPath]?.includes(depLocation) && + await pathExists(path.join(opts.lockfileDir, depLocation)) const pkgResolution = { id: packageId, resolution,