diff --git a/.changeset/forty-lies-work.md b/.changeset/forty-lies-work.md new file mode 100644 index 0000000000..21bb8e27fa --- /dev/null +++ b/.changeset/forty-lies-work.md @@ -0,0 +1,33 @@ +--- +"@pnpm/resolve-dependencies": patch +"supi": patch +--- + +The lockfile should be correctly updated when a direct dependency that has peer dependencies has a new version specifier in `package.json`. + +For instance, `jest@26` has `cascade@2` in its peer dependencies. So `pnpm install` will scope Jest to some version of cascade. This is how it will look like in `pnpm-lock.yaml`: + +```yaml +dependencies: + canvas: 2.6.0 + jest: 26.4.0_canvas@2.6.0 +``` + +If the version specifier of Jest gets changed in the `package.json` to `26.5.0`, the next time `pnpm install` is executed, the lockfile should be changed to this: + +```yaml +dependencies: + canvas: 2.6.0 + jest: 26.5.0_canvas@2.6.0 +``` + +Prior to this fix, after the update, Jest was not scoped with canvas, so the lockfile was incorrectly updated to the following: + +```yaml +dependencies: + canvas: 2.6.0 + jest: 26.5.0 +``` + +Related issue: [#2919](https://github.com/pnpm/pnpm/issues/2919). +Related PR: [#2920](https://github.com/pnpm/pnpm/pull/2920). diff --git a/packages/plugin-commands-installation/test/index.ts b/packages/plugin-commands-installation/test/index.ts index d378ee16ea..b5d7460408 100644 --- a/packages/plugin-commands-installation/test/index.ts +++ b/packages/plugin-commands-installation/test/index.ts @@ -3,6 +3,7 @@ import './add' import './addRecursive' import './linkRecursive' import './miscRecursive' +import './peerDependencies' import './prune' import './remove/completion' import './remove/remove' diff --git a/packages/plugin-commands-installation/test/peerDependencies.ts b/packages/plugin-commands-installation/test/peerDependencies.ts new file mode 100644 index 0000000000..98c3da7a0b --- /dev/null +++ b/packages/plugin-commands-installation/test/peerDependencies.ts @@ -0,0 +1,68 @@ +import { add, install } from '@pnpm/plugin-commands-installation' +import prepare from '@pnpm/prepare' +import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import test = require('tape') +import tempy = require('tempy') + +const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}` + +const DEFAULT_OPTIONS = { + argv: { + original: [], + }, + bail: false, + cliOptions: {}, + include: { + dependencies: true, + devDependencies: true, + optionalDependencies: true, + }, + lock: true, + pnpmfile: 'pnpmfile.js', + rawConfig: { registry: REGISTRY_URL }, + rawLocalConfig: { registry: REGISTRY_URL }, + registries: { + default: REGISTRY_URL, + }, + sort: true, + storeDir: tempy.directory(), + workspaceConcurrency: 1, +} + +test('root dependency that has a peer is correctly updated after its version changes', async (t) => { + const project = prepare(t, {}) + + await add.handler({ + ...DEFAULT_OPTIONS, + dir: process.cwd(), + linkWorkspacePackages: true, + }, ['ajv@4.10.4', 'ajv-keywords@1.5.0']) + + { + const lockfile = await project.readLockfile() + t.equal(lockfile.dependencies['ajv-keywords'], '1.5.0_ajv@4.10.4') + } + + await project.writePackageJson({ + dependencies: { + ajv: '4.10.4', + 'ajv-keywords': '1.5.1', + }, + }) + + await install.handler({ + ...DEFAULT_OPTIONS, + dir: process.cwd(), + linkWorkspacePackages: true, + rawLocalConfig: { + 'frozen-lockfile': false, + }, + }) + + { + const lockfile = await project.readLockfile() + t.equal(lockfile.dependencies['ajv-keywords'], '1.5.1_ajv@4.10.4') + } + + t.end() +}) diff --git a/packages/resolve-dependencies/src/resolveDependencies.ts b/packages/resolve-dependencies/src/resolveDependencies.ts index a60f500462..10c4592656 100644 --- a/packages/resolve-dependencies/src/resolveDependencies.ts +++ b/packages/resolve-dependencies/src/resolveDependencies.ts @@ -358,7 +358,7 @@ function getDepsToResolve ( // The only reason we resolve children in case the package depends on peers // is to get information about the existing dependencies, so that they can // be merged with the resolved peers. - const proceedAll = options.proceed + let proceedAll = options.proceed const allPeers = new Set() for (const wantedDependency of wantedDependencies) { let reference = wantedDependency.alias && resolvedDependencies[wantedDependency.alias] @@ -394,6 +394,15 @@ function getDepsToResolve ( allPeers.add(peerName) }) } + if (!infoFromLockfile && !proceedAll) { + // In this case we don't know if the package depends on peer dependencies, so we proceed all. + proceedAll = true + for (const extendedWantedDep of extendedWantedDeps) { + if (!extendedWantedDep.proceed) { + extendedWantedDep.proceed = true + } + } + } extendedWantedDeps.push({ infoFromLockfile, proceed, diff --git a/packages/supi/src/install/index.ts b/packages/supi/src/install/index.ts index f72a25b007..9d9351d016 100644 --- a/packages/supi/src/install/index.ts +++ b/packages/supi/src/install/index.ts @@ -850,7 +850,7 @@ async function toResolveImporter ( project: ImporterToUpdate ) { const allDeps = getWantedDependencies(project.manifest) - const { linkedAliases, nonLinkedDependencies } = await partitionLinkedPackages(allDeps, { + const { nonLinkedDependencies } = await partitionLinkedPackages(allDeps, { lockfileOnly: opts.lockfileOnly, modulesDir: project.modulesDir, projectDir: project.rootDir, @@ -890,8 +890,7 @@ async function toResolveImporter ( ...project, hasRemovedDependencies: Boolean(project.removePackages?.length), preferredVersions: opts.preferredVersions ?? (project.manifest && getPreferredVersionsFromPackage(project.manifest)) ?? {}, - wantedDependencies: wantedDependencies - .filter(({ alias, updateDepth }) => updateDepth >= 0 || !linkedAliases.has(alias)), + wantedDependencies, } }