diff --git a/lib/modules/manager/npm/extract/index.spec.ts b/lib/modules/manager/npm/extract/index.spec.ts index 2f5e4d73342417..b6c18455b92048 100644 --- a/lib/modules/manager/npm/extract/index.spec.ts +++ b/lib/modules/manager/npm/extract/index.spec.ts @@ -669,6 +669,85 @@ describe('modules/manager/npm/extract/index', () => { ], }); }); + + it('extracts dependencies from overrides', async () => { + const content = `{ + "devDependencies": { + "@types/react": "18.0.5" + }, + "overrides": { + "node": "8.9.2", + "@types/react": "18.0.5", + "baz": { + "node": "8.9.2", + "bar": { + "foo": "1.0.0" + } + }, + "foo2": { + ".": "1.0.0", + "bar2": "1.0.0" + }, + "emptyObject":{} + } + }`; + const res = await npmExtract.extractPackageFile( + content, + 'package.json', + defaultConfig + ); + expect(res).toMatchObject({ + deps: [ + { + depType: 'devDependencies', + depName: '@types/react', + currentValue: '18.0.5', + datasource: 'npm', + prettyDepType: 'devDependency', + }, + { + depType: 'overrides', + depName: 'node', + currentValue: '8.9.2', + datasource: 'npm', + commitMessageTopic: 'Node.js', + prettyDepType: 'overrides', + }, + { + depType: 'overrides', + depName: '@types/react', + currentValue: '18.0.5', + datasource: 'npm', + prettyDepType: 'overrides', + }, + { + depName: 'node', + managerData: { parents: ['baz'] }, + commitMessageTopic: 'Node.js', + currentValue: '8.9.2', + datasource: 'npm', + }, + { + depName: 'foo', + managerData: { parents: ['baz', 'bar'] }, + currentValue: '1.0.0', + datasource: 'npm', + }, + { + depName: 'foo2', + managerData: { parents: ['foo2'] }, + currentValue: '1.0.0', + datasource: 'npm', + }, + { + depName: 'bar2', + managerData: { parents: ['foo2'] }, + currentValue: '1.0.0', + datasource: 'npm', + }, + ], + }); + }); }); describe('.postExtract()', () => { diff --git a/lib/modules/manager/npm/extract/index.ts b/lib/modules/manager/npm/extract/index.ts index 322d7cd01c249a..3d5ba8e4a6a5b5 100644 --- a/lib/modules/manager/npm/extract/index.ts +++ b/lib/modules/manager/npm/extract/index.ts @@ -173,6 +173,7 @@ export async function extractPackageFile( volta: 'volta', resolutions: 'resolutions', packageManager: 'packageManager', + overrides: 'overrides', }; const constraints: Record = {}; @@ -338,6 +339,47 @@ export async function extractPackageFile( return dep; } + /** + * Used when there is a json object as a value in overrides block. + * @param parents + * @param child + * @returns PackageDependency array + */ + function extractOverrideDepsRec( + parents: string[], + child: NpmManagerData + ): PackageDependency[] { + const deps: PackageDependency[] = []; + if (!child || is.emptyObject(child)) { + return deps; + } + for (const [overrideName, versionValue] of Object.entries(child)) { + if (is.string(versionValue)) { + // special handling for "." override depenency name + // "." means the constraint is applied to the parent dep + const currDepName = + overrideName === '.' ? parents[parents.length - 1] : overrideName; + const dep: PackageDependency = { + depName: currDepName, + depType: 'overrides', + managerData: { parents: parents.slice() }, // set parents for dependency + }; + setNodeCommitTopic(dep); + deps.push({ + ...dep, + ...extractDependency('overrides', currDepName, versionValue), + }); + } else { + // versionValue is an object, run recursively. + parents.push(overrideName); + const depsOfObject = extractOverrideDepsRec(parents, versionValue); + deps.push(...depsOfObject); + } + } + parents.pop(); + return deps; + } + for (const depType of Object.keys(depTypes) as (keyof typeof depTypes)[]) { let dependencies = packageJson[depType]; if (dependencies) { @@ -363,13 +405,14 @@ export async function extractPackageFile( if (depName !== key) { dep.managerData = { key }; } - dep = { ...dep, ...extractDependency(depType, depName, val) }; - if (depName === 'node') { - // This is a special case for Node.js to group it together with other managers - dep.commitMessageTopic = 'Node.js'; + if (depType === 'overrides' && !is.string(val)) { + deps.push(...extractOverrideDepsRec([depName], val)); + } else { + dep = { ...dep, ...extractDependency(depType, depName, val) }; + setNodeCommitTopic(dep); + dep.prettyDepType = depTypes[depType]; + deps.push(dep); } - dep.prettyDepType = depTypes[depType]; - deps.push(dep); } } catch (err) /* istanbul ignore next */ { logger.debug({ fileName, depType, err }, 'Error parsing package.json'); @@ -460,3 +503,10 @@ export async function extractAllPackageFiles( await postExtract(npmFiles, !!config.updateInternalDeps); return npmFiles; } + +function setNodeCommitTopic(dep: NpmManagerData): void { + // This is a special case for Node.js to group it together with other managers + if (dep.depName === 'node') { + dep.commitMessageTopic = 'Node.js'; + } +} diff --git a/lib/modules/manager/npm/extract/types.ts b/lib/modules/manager/npm/extract/types.ts index 2ba3a6fc89437c..0312418f4e54bd 100644 --- a/lib/modules/manager/npm/extract/types.ts +++ b/lib/modules/manager/npm/extract/types.ts @@ -13,7 +13,7 @@ export interface NpmPackage extends PackageJson { _id?: any; dependenciesMeta?: DependenciesMeta; packageManager?: string; - + overrides?: OverrideDependency; volta?: PackageJson.Dependency; } @@ -31,3 +31,7 @@ export interface LockFile { export interface PnpmWorkspaceFile { packages: string[]; } + +export type OverrideDependency = Record; + +export type RecursiveOverride = string | { [_: string]: RecursiveOverride }; diff --git a/lib/modules/manager/npm/types.ts b/lib/modules/manager/npm/types.ts index 30db6771e39984..ed7a7adbad7d53 100644 --- a/lib/modules/manager/npm/types.ts +++ b/lib/modules/manager/npm/types.ts @@ -67,12 +67,13 @@ export type NpmDepType = | 'dependencies' | 'devDependencies' | 'optionalDependencies' + | 'overrides' | 'peerDependencies' | 'resolutions'; export interface NpmManagerData extends Record { hasPackageManager?: boolean; - lernaJsonFile?: string; + parents?: string[]; yarnZeroInstall?: boolean; } diff --git a/lib/modules/manager/npm/update/dependency/index.spec.ts b/lib/modules/manager/npm/update/dependency/index.spec.ts index fd1223261fece0..e438a6bf15c7b3 100644 --- a/lib/modules/manager/npm/update/dependency/index.spec.ts +++ b/lib/modules/manager/npm/update/dependency/index.spec.ts @@ -297,5 +297,56 @@ describe('modules/manager/npm/update/dependency/index', () => { }); expect(testContent).toEqual(outputContent); }); + + it('handles override dependency', () => { + const upgrade = { + depType: 'overrides', + depName: 'typescript', + newValue: '0.60.0', + }; + const overrideDependencies = `{ + "overrides": { + "typescript": "0.0.5" + } + }`; + const expected = `{ + "overrides": { + "typescript": "0.60.0" + } + }`; + const testContent = npmUpdater.updateDependency({ + fileContent: overrideDependencies, + upgrade, + }); + expect(testContent).toEqual(expected); + }); + + it('handles override dependency object', () => { + const upgrade = { + depType: 'overrides', + depName: 'typescript', + newValue: '0.60.0', + managerData: { parents: ['awesome-typescript-loader'] }, + }; + const overrideDependencies = `{ + "overrides": { + "awesome-typescript-loader": { + "typescript": "3.0.0" + } + } + }`; + const expected = `{ + "overrides": { + "awesome-typescript-loader": { + "typescript": "0.60.0" + } + } + }`; + const testContent = npmUpdater.updateDependency({ + fileContent: overrideDependencies, + upgrade, + }); + expect(testContent).toEqual(expected); + }); }); }); diff --git a/lib/modules/manager/npm/update/dependency/index.ts b/lib/modules/manager/npm/update/dependency/index.ts index 5eb8d9329a0efe..c2231d7c560e15 100644 --- a/lib/modules/manager/npm/update/dependency/index.ts +++ b/lib/modules/manager/npm/update/dependency/index.ts @@ -1,10 +1,16 @@ +import is from '@sindresorhus/is'; import { dequal } from 'dequal'; import { logger } from '../../../../../logger'; import { escapeRegExp, regEx } from '../../../../../util/regex'; import { matchAt, replaceAt } from '../../../../../util/string'; -import type { UpdateDependencyConfig } from '../../../types'; -import type { DependenciesMeta, NpmPackage } from '../../extract/types'; -import type { NpmDepType } from '../../types'; +import type { UpdateDependencyConfig, Upgrade } from '../../../types'; +import type { + DependenciesMeta, + NpmPackage, + OverrideDependency, + RecursiveOverride, +} from '../../extract/types'; +import type { NpmDepType, NpmManagerData } from '../../types'; function renameObjKey( oldObj: DependenciesMeta, @@ -28,7 +34,8 @@ function replaceAsString( depType: NpmDepType | 'dependenciesMeta' | 'packageManager', depName: string, oldValue: string, - newValue: string + newValue: string, + parents?: string[] ): string { if (depType === 'packageManager') { parsedContents[depType] = newValue; @@ -46,6 +53,16 @@ function replaceAsString( newValue ); } + } else if (parents && depType === 'overrides') { + // there is an object as a value in overrides block + const { depObjectReference, overrideDepName } = overrideDepPosition( + parsedContents[depType]!, + parents, + depName + ); + if (depObjectReference) { + depObjectReference[overrideDepName] = newValue; + } } else { // The old value is the version of the dependency parsedContents[depType]![depName] = newValue; @@ -117,13 +134,28 @@ export function updateDependency({ logger.debug(`npm.updateDependency(): ${depType}.${depName} = ${newValue}`); try { const parsedContents: NpmPackage = JSON.parse(fileContent); + let overrideDepParents: string[] | undefined = undefined; // Save the old version let oldVersion: string | undefined; if (depType === 'packageManager') { oldVersion = parsedContents[depType]; newValue = `${depName}@${newValue}`; + } else if (isOverrideObject(upgrade)) { + overrideDepParents = managerData?.parents; + if (overrideDepParents) { + // old version when there is an object as a value in overrides block + const { depObjectReference, overrideDepName } = overrideDepPosition( + parsedContents['overrides']!, + overrideDepParents, + depName + ); + if (depObjectReference) { + oldVersion = depObjectReference[overrideDepName]!; + } + } } else { - oldVersion = parsedContents[depType as NpmDepType]![depName]; + // eslint-disable @typescript-eslint/no-unnecessary-type-assertion + oldVersion = parsedContents[depType as NpmDepType]![depName] as string; } if (oldVersion === newValue) { logger.trace('Version is already updated'); @@ -137,7 +169,8 @@ export function updateDependency({ depType as NpmDepType, depName, oldVersion!, - newValue! + newValue!, + overrideDepParents ); if (upgrade.newName) { newFileContent = replaceAsString( @@ -146,7 +179,8 @@ export function updateDependency({ depType as NpmDepType, depName, depName, - upgrade.newName + upgrade.newName, + overrideDepParents ); } /* eslint-enable @typescript-eslint/no-unnecessary-type-assertion */ @@ -223,3 +257,30 @@ export function updateDependency({ return null; } } +function overrideDepPosition( + overrideBlock: OverrideDependency, + parents: string[], + depName: string +): { + depObjectReference: Record; + overrideDepName: string; +} { + // get override dep position when its nested in an object + const lastParent = parents[parents.length - 1]; + let overrideDep: OverrideDependency = overrideBlock; + for (const parent of parents) { + if (overrideDep) { + overrideDep = overrideDep[parent]! as Record; + } + } + const overrideDepName = depName === lastParent ? '.' : depName; + const depObjectReference = overrideDep as Record; + return { depObjectReference, overrideDepName }; +} + +function isOverrideObject(upgrade: Upgrade): boolean { + return ( + is.array(upgrade.managerData?.parents, is.nonEmptyStringAndNotWhitespace) && + upgrade.depType === 'overrides' + ); +}