From e0f9266a50549d8a58e5e093e1ff0348b30e1034 Mon Sep 17 00:00:00 2001 From: Jeldrik Hanschke Date: Wed, 1 Nov 2023 17:15:17 +0100 Subject: [PATCH] feat(manager/npm): Optimize npm dedupe option (#25466) Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- docs/usage/configuration-options.md | 2 +- .../__snapshots__/npm.spec.ts.snap | 20 +------ .../manager/npm/post-update/npm.spec.ts | 56 +++++++++++++++++++ lib/modules/manager/npm/post-update/npm.ts | 19 ++++++- 4 files changed, 74 insertions(+), 23 deletions(-) diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 51a02f3db66b23..e1ee7b04372b42 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -2972,7 +2972,7 @@ Table with options: | `gomodTidyE` | Run `go mod tidy -e` after Go module updates. | | `gomodUpdateImportPaths` | Update source import paths on major module updates, using [mod](https://github.com/marwan-at-work/mod). | | `helmUpdateSubChartArchives` | Update subchart archives in the `/charts` folder. | -| `npmDedupe` | Run `npm dedupe` after `package-lock.json` updates. | +| `npmDedupe` | Run `npm install` with `--prefer-dedupe` for npm >= 7 or `npm dedupe` after `package-lock.json` update for npm <= 6. | | `pnpmDedupe` | Run `pnpm dedupe --config.ignore-scripts=true` after `pnpm-lock.yaml` updates. | | `yarnDedupeFewer` | Run `yarn-deduplicate --strategy fewer` after `yarn.lock` updates. | | `yarnDedupeHighest` | Run `yarn-deduplicate --strategy highest` (`yarn dedupe --strategy highest` for Yarn >=2.2.0) after `yarn.lock` updates. | diff --git a/lib/modules/manager/npm/post-update/__snapshots__/npm.spec.ts.snap b/lib/modules/manager/npm/post-update/__snapshots__/npm.spec.ts.snap index 83ccde3506e1b6..459c4f599bade8 100644 --- a/lib/modules/manager/npm/post-update/__snapshots__/npm.spec.ts.snap +++ b/lib/modules/manager/npm/post-update/__snapshots__/npm.spec.ts.snap @@ -3,25 +3,7 @@ exports[`modules/manager/npm/post-update/npm generates lock files 1`] = ` [ { - "cmd": "npm install --no-audit --ignore-scripts", - "options": { - "cwd": "some-dir", - "encoding": "utf-8", - "env": { - "HOME": "/home/user", - "HTTPS_PROXY": "https://example.com", - "HTTP_PROXY": "http://example.com", - "LANG": "en_US.UTF-8", - "LC_ALL": "en_US", - "NO_PROXY": "localhost", - "PATH": "/tmp/path", - }, - "maxBuffer": 10485760, - "timeout": 900000, - }, - }, - { - "cmd": "npm dedupe", + "cmd": "npm install --package-lock-only --no-audit --prefer-dedupe --ignore-scripts", "options": { "cwd": "some-dir", "encoding": "utf-8", diff --git a/lib/modules/manager/npm/post-update/npm.spec.ts b/lib/modules/manager/npm/post-update/npm.spec.ts index ffa0273a2a8490..7bff57fb2d12b0 100644 --- a/lib/modules/manager/npm/post-update/npm.spec.ts +++ b/lib/modules/manager/npm/post-update/npm.spec.ts @@ -162,6 +162,62 @@ describe('modules/manager/npm/post-update/npm', () => { expect(execSnapshots).toEqual([]); }); + it('deduplicates dependencies on installation with npm >= 7', async () => { + const execSnapshots = mockExecAll(); + // package.json + fs.readLocalFile.mockResolvedValueOnce('{}'); + fs.readLocalFile.mockResolvedValueOnce('package-lock-contents'); + const postUpdateOptions = ['npmDedupe']; + const updates = [ + { packageName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: false }, + ]; + const res = await npmHelper.generateLockFile( + 'some-dir', + {}, + 'package-lock.json', + { postUpdateOptions }, + updates + ); + expect(fs.readLocalFile).toHaveBeenCalledTimes(2); + expect(res.error).toBeFalse(); + expect(res.lockFile).toBe('package-lock-contents'); + expect(execSnapshots).toHaveLength(1); + expect(execSnapshots).toMatchObject([ + { + cmd: 'npm install --package-lock-only --no-audit --prefer-dedupe --ignore-scripts', + }, + ]); + }); + + it('deduplicates dependencies after installation with npm <= 6', async () => { + const execSnapshots = mockExecAll(); + // package.json + fs.readLocalFile.mockResolvedValueOnce('package-lock-contents'); + const postUpdateOptions = ['npmDedupe']; + const updates = [ + { packageName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: false }, + ]; + const res = await npmHelper.generateLockFile( + 'some-dir', + {}, + 'package-lock.json', + { postUpdateOptions, constraints: { npm: '^6.0.0' } }, + updates + ); + expect(fs.readLocalFile).toHaveBeenCalledTimes(1); + expect(res.error).toBeFalse(); + expect(res.lockFile).toBe('package-lock-contents'); + expect(execSnapshots).toHaveLength(2); + expect(execSnapshots).toMatchObject([ + { + cmd: 'npm install --no-audit --ignore-scripts', + }, + { + cmd: 'npm dedupe', + }, + ]); + }); + it('runs twice if remediating', async () => { const execSnapshots = mockExecAll(); fs.readLocalFile.mockResolvedValueOnce('package-lock-contents'); diff --git a/lib/modules/manager/npm/post-update/npm.ts b/lib/modules/manager/npm/post-update/npm.ts index f81921d8eb75f1..fd2b7b3f7b1b7d 100644 --- a/lib/modules/manager/npm/post-update/npm.ts +++ b/lib/modules/manager/npm/post-update/npm.ts @@ -1,5 +1,6 @@ // TODO: types (#22198) import is from '@sindresorhus/is'; +import semver from 'semver'; import upath from 'upath'; import { GlobalConfig } from '../../../../config/global'; import { @@ -49,10 +50,14 @@ export async function generateLockFile( config.constraints?.npm ?? getPackageManagerVersion('npm', await lazyPgkJson.getValue()), }; + const supportsPreferDedupeFlag = + !npmToolConstraint.constraint || + semver.intersects('>=7.0.0', npmToolConstraint.constraint); const commands: string[] = []; let cmdOptions = ''; if ( - postUpdateOptions?.includes('npmDedupe') === true || + (postUpdateOptions?.includes('npmDedupe') === true && + !supportsPreferDedupeFlag) || skipInstalls === false ) { logger.debug('Performing node_modules install'); @@ -62,6 +67,11 @@ export async function generateLockFile( cmdOptions += '--package-lock-only --no-audit'; } + if (postUpdateOptions?.includes('npmDedupe') && supportsPreferDedupeFlag) { + logger.debug('Deduplicate dependencies on installation'); + cmdOptions += ' --prefer-dedupe'; + } + if (!GlobalConfig.get('allowScripts') || config.ignoreScripts) { cmdOptions += ' --ignore-scripts'; } @@ -130,8 +140,11 @@ export async function generateLockFile( } // postUpdateOptions - if (config.postUpdateOptions?.includes('npmDedupe')) { - logger.debug('Performing npm dedupe'); + if ( + config.postUpdateOptions?.includes('npmDedupe') && + !supportsPreferDedupeFlag + ) { + logger.debug('Performing npm dedupe after installation'); commands.push('npm dedupe'); }