Skip to content

Commit

Permalink
feat(npm): add detection for overrides block (#15351)
Browse files Browse the repository at this point in the history
Co-authored-by: Rhys Arkins <rhys@arkins.net>
  • Loading branch information
hasanwhitesource and rarkins committed May 17, 2022
1 parent 714182e commit 4ef5aa2
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 15 deletions.
79 changes: 79 additions & 0 deletions lib/modules/manager/npm/extract/index.spec.ts
Expand Up @@ -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()', () => {
Expand Down
62 changes: 56 additions & 6 deletions lib/modules/manager/npm/extract/index.ts
Expand Up @@ -173,6 +173,7 @@ export async function extractPackageFile(
volta: 'volta',
resolutions: 'resolutions',
packageManager: 'packageManager',
overrides: 'overrides',
};

const constraints: Record<string, any> = {};
Expand Down Expand Up @@ -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<NpmManagerData> = {
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) {
Expand All @@ -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');
Expand Down Expand Up @@ -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';
}
}
6 changes: 5 additions & 1 deletion lib/modules/manager/npm/extract/types.ts
Expand Up @@ -13,7 +13,7 @@ export interface NpmPackage extends PackageJson {
_id?: any;
dependenciesMeta?: DependenciesMeta;
packageManager?: string;

overrides?: OverrideDependency;
volta?: PackageJson.Dependency;
}

Expand All @@ -31,3 +31,7 @@ export interface LockFile {
export interface PnpmWorkspaceFile {
packages: string[];
}

export type OverrideDependency = Record<string, RecursiveOverride>;

export type RecursiveOverride = string | { [_: string]: RecursiveOverride };
3 changes: 2 additions & 1 deletion lib/modules/manager/npm/types.ts
Expand Up @@ -67,12 +67,13 @@ export type NpmDepType =
| 'dependencies'
| 'devDependencies'
| 'optionalDependencies'
| 'overrides'
| 'peerDependencies'
| 'resolutions';

export interface NpmManagerData extends Record<string, any> {
hasPackageManager?: boolean;

lernaJsonFile?: string;
parents?: string[];
yarnZeroInstall?: boolean;
}
51 changes: 51 additions & 0 deletions lib/modules/manager/npm/update/dependency/index.spec.ts
Expand Up @@ -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);
});
});
});
75 changes: 68 additions & 7 deletions 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,
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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');
Expand All @@ -137,7 +169,8 @@ export function updateDependency({
depType as NpmDepType,
depName,
oldVersion!,
newValue!
newValue!,
overrideDepParents
);
if (upgrade.newName) {
newFileContent = replaceAsString(
Expand All @@ -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 */
Expand Down Expand Up @@ -223,3 +257,30 @@ export function updateDependency({
return null;
}
}
function overrideDepPosition(
overrideBlock: OverrideDependency,
parents: string[],
depName: string
): {
depObjectReference: Record<string, string>;
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<string, RecursiveOverride>;
}
}
const overrideDepName = depName === lastParent ? '.' : depName;
const depObjectReference = overrideDep as Record<string, string>;
return { depObjectReference, overrideDepName };
}

function isOverrideObject(upgrade: Upgrade<NpmManagerData>): boolean {
return (
is.array(upgrade.managerData?.parents, is.nonEmptyStringAndNotWhitespace) &&
upgrade.depType === 'overrides'
);
}

0 comments on commit 4ef5aa2

Please sign in to comment.