diff --git a/.changeset/rude-vans-build.md b/.changeset/rude-vans-build.md new file mode 100644 index 00000000000..496af602313 --- /dev/null +++ b/.changeset/rude-vans-build.md @@ -0,0 +1,8 @@ +--- +"@pnpm/outdated": minor +"@pnpm/plugin-commands-installation": minor +"@pnpm/plugin-commands-outdated": minor +"@pnpm/types": minor +--- + +Ignore packages listed in package.json > pnpm.updateConfig.ignoreDependencies fields on update/outdated command [#5358](https://github.com/pnpm/pnpm/issues/5358) diff --git a/fixtures/with-pnpm-update-ignore/package.json b/fixtures/with-pnpm-update-ignore/package.json new file mode 100644 index 00000000000..3e738ee45ae --- /dev/null +++ b/fixtures/with-pnpm-update-ignore/package.json @@ -0,0 +1,15 @@ +{ + "name": "with-pnpm-update-ignore", + "version": "1.0.0", + "dependencies": { + "is-positive": "1.0.0", + "is-negative": "1.0.0" + }, + "pnpm": { + "updateConfig": { + "ignoreDependencies": [ + "is-positive" + ] + } + } +} diff --git a/fixtures/with-pnpm-update-ignore/pnpm-lock.yaml b/fixtures/with-pnpm-update-ignore/pnpm-lock.yaml new file mode 100644 index 00000000000..2a57a8642d3 --- /dev/null +++ b/fixtures/with-pnpm-update-ignore/pnpm-lock.yaml @@ -0,0 +1,21 @@ +lockfileVersion: 5.4 + +specifiers: + is-negative: 1.0.0 + is-positive: 1.0.0 + +dependencies: + is-negative: 1.0.0 + is-positive: 1.0.0 + +packages: + + /is-negative/1.0.0: + resolution: {integrity: sha512-1aKMsFUc7vYQGzt//8zhkjRWPoYkajY/I5MJEvrc0pDoHXrW7n5ri8DYxhy3rR+Dk0QFl7GjHHsZU1sppQrWtw==} + engines: {node: '>=0.10.0'} + dev: false + + /is-positive/1.0.0: + resolution: {integrity: sha512-xxzPGZ4P2uN6rROUa5N9Z7zTX6ERuE0hs6GUOc/cKBLF2NqKc16UwqHMt3tFg4CO6EBTE5UecUasg+3jZx3Ckg==} + engines: {node: '>=0.10.0'} + dev: false diff --git a/packages/outdated/src/outdated.ts b/packages/outdated/src/outdated.ts index 6c545ee6690..870ecd59a14 100644 --- a/packages/outdated/src/outdated.ts +++ b/packages/outdated/src/outdated.ts @@ -34,6 +34,7 @@ export default async function outdated ( compatible?: boolean currentLockfile: Lockfile | null getLatestManifest: GetLatestManifestFunction + ignoreDependencies?: Set include?: IncludedDependencies lockfileDir: string manifest: ProjectManifest @@ -69,8 +70,10 @@ export default async function outdated ( pkgs.map(async (alias) => { const ref = opts.wantedLockfile!.importers[importerId][depType]![alias] - // ignoring linked packages. (For backward compatibility) - if (ref.startsWith('file:')) { + if ( + ref.startsWith('file:') || // ignoring linked packages. (For backward compatibility) + opts.ignoreDependencies?.has(alias) + ) { return } diff --git a/packages/outdated/src/outdatedDepsOfProjects.ts b/packages/outdated/src/outdatedDepsOfProjects.ts index dccf6fc2402..18186d3954d 100644 --- a/packages/outdated/src/outdatedDepsOfProjects.ts +++ b/packages/outdated/src/outdatedDepsOfProjects.ts @@ -18,6 +18,7 @@ export default async function outdatedDepsOfProjects ( args: string[], opts: Omit & { compatible?: boolean + ignoreDependencies?: Set include: IncludedDependencies } & Partial> ): Promise { @@ -44,6 +45,7 @@ export default async function outdatedDepsOfProjects ( compatible: opts.compatible, currentLockfile, getLatestManifest, + ignoreDependencies: opts.ignoreDependencies, include: opts.include, lockfileDir, manifest, diff --git a/packages/plugin-commands-installation/jest.config.js b/packages/plugin-commands-installation/jest.config.js index f697d831691..739e8317bd5 100644 --- a/packages/plugin-commands-installation/jest.config.js +++ b/packages/plugin-commands-installation/jest.config.js @@ -1 +1,7 @@ -module.exports = require('../../jest.config.js') +module.exports = { + ...require('../../jest.config.js'), + // This is a temporary workaround. + // Currently, multiple tests use the @pnpm.e2e/foo package and they change it's dist-tags. + // These tests are in separate files, so sometimes they will simultaneously set the dist tag and fail because they expect different versions to be tagged. + maxWorkers: 1, +} diff --git a/packages/plugin-commands-installation/src/installDeps.ts b/packages/plugin-commands-installation/src/installDeps.ts index 1939f1c4c3d..9242642b3bf 100644 --- a/packages/plugin-commands-installation/src/installDeps.ts +++ b/packages/plugin-commands-installation/src/installDeps.ts @@ -24,7 +24,7 @@ import getOptionsFromRootManifest from './getOptionsFromRootManifest' import getPinnedVersion from './getPinnedVersion' import getSaveType from './getSaveType' import getNodeExecPath from './nodeExecPath' -import recursive, { createMatcher, matchDependencies } from './recursive' +import recursive, { createMatcher, matchDependencies, makeIgnorePatterns, UpdateDepsMatcher } from './recursive' import updateToLatestSpecsFromManifest, { createLatestSpecs } from './updateToLatestSpecsFromManifest' import { createWorkspaceSpecs, updateToWorkspacePackagesFromManifest } from './updateWorkspaceDependencies' @@ -199,7 +199,18 @@ when running add/update with the --workspace option') } } - const updateMatch = opts.update && (params.length > 0) ? createMatcher(params) : null + let updateMatch: UpdateDepsMatcher | null + if (opts.update) { + if (params.length === 0) { + const ignoreDeps = manifest.pnpm?.updateConfig?.ignoreDependencies + if (ignoreDeps?.length) { + params = makeIgnorePatterns(ignoreDeps) + } + } + updateMatch = params.length ? createMatcher(params) : null + } else { + updateMatch = null + } if (updateMatch != null) { params = matchDependencies(updateMatch, manifest, includeDirect) if (params.length === 0 && opts.depth === 0) { diff --git a/packages/plugin-commands-installation/src/recursive.ts b/packages/plugin-commands-installation/src/recursive.ts index 496ed7445ea..51e0821a61a 100755 --- a/packages/plugin-commands-installation/src/recursive.ts +++ b/packages/plugin-commands-installation/src/recursive.ts @@ -173,8 +173,18 @@ export default async function recursive ( optionalDependencies: true, } - const updateMatch = cmdFullName === 'update' && (params.length > 0) ? createMatcher(params) : null - + let updateMatch: UpdateDepsMatcher | null + if (cmdFullName === 'update') { + if (params.length === 0) { + const ignoreDeps = manifestsByPath[opts.workspaceDir]?.manifest?.pnpm?.updateConfig?.ignoreDependencies + if (ignoreDeps?.length) { + params = makeIgnorePatterns(ignoreDeps) + } + } + updateMatch = params.length ? createMatcher(params) : null + } else { + updateMatch = null + } // For a workspace with shared lockfile if (opts.lockfileDir && ['add', 'install', 'remove', 'update', 'import'].includes(cmdFullName)) { let importers = await getImporters() @@ -499,7 +509,9 @@ export function matchDependencies ( return matchedDeps } -export function createMatcher (params: string[]) { +export type UpdateDepsMatcher = (input: string) => string | null + +export function createMatcher (params: string[]): UpdateDepsMatcher { const patterns: string[] = [] const specs: string[] = [] for (const param of params) { @@ -519,3 +531,7 @@ export function createMatcher (params: string[]) { return specs[index] } } + +export function makeIgnorePatterns (ignoredDependencies: string[]): string[] { + return ignoredDependencies.map(depName => `!${depName}`) +} diff --git a/packages/plugin-commands-installation/test/update/update.ts b/packages/plugin-commands-installation/test/update/update.ts index 14a05ab1234..5a533924d63 100644 --- a/packages/plugin-commands-installation/test/update/update.ts +++ b/packages/plugin-commands-installation/test/update/update.ts @@ -224,3 +224,92 @@ test('update should work normal when set empty string version', async () => { expect(lockfile.devDependencies['@pnpm.e2e/foo']).toEqual('2.0.0') expect(lockfile.devDependencies['@pnpm.e2e/peer-c']).toEqual('2.0.0') }) + +test('ignore packages in package.json > updateConfig.ignoreDependencies fields in update command', async () => { + await addDistTag({ package: '@pnpm.e2e/foo', version: '100.0.0', distTag: 'latest' }) + await addDistTag({ package: '@pnpm.e2e/bar', version: '100.0.0', distTag: 'latest' }) + + const project = prepare({ + dependencies: { + '@pnpm.e2e/foo': '100.0.0', + '@pnpm.e2e/bar': '100.0.0', + }, + pnpm: { + updateConfig: { + ignoreDependencies: [ + '@pnpm.e2e/foo', + ], + }, + }, + }) + + await install.handler({ + ...DEFAULT_OPTS, + dir: process.cwd(), + workspaceDir: process.cwd(), + }) + + const lockfile = await project.readLockfile() + + expect(lockfile.packages['/@pnpm.e2e/foo/100.0.0']).toBeTruthy() + expect(lockfile.packages['/@pnpm.e2e/bar/100.0.0']).toBeTruthy() + + await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' }) + await addDistTag({ package: '@pnpm.e2e/bar', version: '100.1.0', distTag: 'latest' }) + + await update.handler({ + ...DEFAULT_OPTS, + dir: process.cwd(), + workspaceDir: process.cwd(), + latest: true, + }) + + const lockfileUpdated = await project.readLockfile() + + expect(lockfileUpdated.packages['/@pnpm.e2e/foo/100.0.0']).toBeTruthy() + expect(lockfileUpdated.packages['/@pnpm.e2e/bar/100.1.0']).toBeTruthy() +}) + +test('not ignore packages if these are specified in parameter even if these are listed in package.json > pnpm.update.ignoreDependencies fields in update command', async () => { + await addDistTag({ package: '@pnpm.e2e/foo', version: '100.0.0', distTag: 'latest' }) + await addDistTag({ package: '@pnpm.e2e/bar', version: '100.0.0', distTag: 'latest' }) + + const project = prepare({ + dependencies: { + '@pnpm.e2e/foo': '100.0.0', + '@pnpm.e2e/bar': '100.0.0', + }, + pnpm: { + updateConfig: { + ignoreDependencies: [ + '@pnpm.e2e/foo', + ], + }, + }, + }) + + await install.handler({ + ...DEFAULT_OPTS, + dir: process.cwd(), + workspaceDir: process.cwd(), + }) + + const lockfile = await project.readLockfile() + + expect(lockfile.packages['/@pnpm.e2e/foo/100.0.0']).toBeTruthy() + expect(lockfile.packages['/@pnpm.e2e/bar/100.0.0']).toBeTruthy() + + await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' }) + await addDistTag({ package: '@pnpm.e2e/bar', version: '100.1.0', distTag: 'latest' }) + + await update.handler({ + ...DEFAULT_OPTS, + dir: process.cwd(), + workspaceDir: process.cwd(), + }, ['@pnpm.e2e/foo@latest', '@pnpm.e2e/bar@latest']) + + const lockfileUpdated = await project.readLockfile() + + expect(lockfileUpdated.packages['/@pnpm.e2e/foo/100.1.0']).toBeTruthy() + expect(lockfileUpdated.packages['/@pnpm.e2e/bar/100.1.0']).toBeTruthy() +}) diff --git a/packages/plugin-commands-outdated/src/outdated.ts b/packages/plugin-commands-outdated/src/outdated.ts index fc326fb725f..952f2d14d0b 100644 --- a/packages/plugin-commands-outdated/src/outdated.ts +++ b/packages/plugin-commands-outdated/src/outdated.ts @@ -167,15 +167,17 @@ export async function handler ( const pkgs = Object.values(opts.selectedProjectsGraph).map((wsPkg) => wsPkg.package) return outdatedRecursive(pkgs, params, { ...opts, include }) } + const manifest = await readProjectManifestOnly(opts.dir, opts) const packages = [ { dir: opts.dir, - manifest: await readProjectManifestOnly(opts.dir, opts), + manifest, }, ] const [outdatedPackages] = await outdatedDepsOfProjects(packages, params, { ...opts, fullMetadata: opts.long, + ignoreDependencies: new Set(manifest?.pnpm?.updateConfig?.ignoreDependencies ?? []), include, retry: { factor: opts.fetchRetryFactor, diff --git a/packages/plugin-commands-outdated/src/recursive.ts b/packages/plugin-commands-outdated/src/recursive.ts index 7eb9d86f5f6..a8e66bae78d 100644 --- a/packages/plugin-commands-outdated/src/recursive.ts +++ b/packages/plugin-commands-outdated/src/recursive.ts @@ -50,9 +50,11 @@ export default async ( opts: OutdatedCommandOptions & { include: IncludedDependencies } ) => { const outdatedMap = {} as Record + const rootManifest = pkgs.find(({ dir }) => dir === opts.lockfileDir ?? opts.dir) const outdatedPackagesByProject = await outdatedDepsOfProjects(pkgs, params, { ...opts, fullMetadata: opts.long, + ignoreDependencies: new Set(rootManifest?.manifest?.pnpm?.updateConfig?.ignoreDependencies ?? []), retry: { factor: opts.fetchRetryFactor, maxTimeout: opts.fetchRetryMaxtimeout, diff --git a/packages/plugin-commands-outdated/test/index.ts b/packages/plugin-commands-outdated/test/index.ts index 60ff64fa3ce..67ea802ae1c 100644 --- a/packages/plugin-commands-outdated/test/index.ts +++ b/packages/plugin-commands-outdated/test/index.ts @@ -15,6 +15,7 @@ const hasOutdatedDepsFixtureAndExternalLockfile = path.join(fixtures, 'has-outda const hasNotOutdatedDepsFixture = path.join(fixtures, 'has-not-outdated-deps') const hasMajorOutdatedDepsFixture = path.join(fixtures, 'has-major-outdated-deps') const hasNoLockfileFixture = path.join(fixtures, 'has-no-lockfile') +const withPnpmUpdateIgnore = path.join(fixtures, 'with-pnpm-update-ignore') const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}` @@ -317,3 +318,19 @@ test('pnpm outdated: print only compatible versions', async () => { └─────────────┴─────────┴────────┘ `) }) + +test('ignore packages in package.json > pnpm.updateConfig.ignoreDependencies in outdated command', async () => { + const { output, exitCode } = await outdated.handler({ + ...OUTDATED_OPTIONS, + dir: withPnpmUpdateIgnore, + }) + + expect(exitCode).toBe(1) + expect(stripAnsi(output)).toBe(`\ +┌─────────────┬─────────┬────────┐ +│ Package │ Current │ Latest │ +├─────────────┼─────────┼────────┤ +│ is-negative │ 1.0.0 │ 2.1.0 │ +└─────────────┴─────────┴────────┘ +`) +}) \ No newline at end of file diff --git a/packages/types/src/package.ts b/packages/types/src/package.ts index 016fd10f738..ddfb2b96dfd 100644 --- a/packages/types/src/package.ts +++ b/packages/types/src/package.ts @@ -131,6 +131,9 @@ export type ProjectManifest = BaseManifest & { allowedDeprecatedVersions?: AllowedDeprecatedVersions allowNonAppliedPatches?: boolean patchedDependencies?: Record + updateConfig?: { + ignoreDependencies?: string[] + } } private?: boolean resolutions?: Record