renovate/lib/modules/manager/npm/update/dependency/index.ts

298 lines
8.9 KiB
TypeScript

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, Upgrade } from '../../../types';
import type {
DependenciesMeta,
NpmPackage,
OverrideDependency,
RecursiveOverride,
} from '../../extract/types';
import type { NpmDepType, NpmManagerData } from '../../types';
function renameObjKey(
oldObj: DependenciesMeta,
oldKey: string,
newKey: string,
): DependenciesMeta {
const keys = Object.keys(oldObj);
return keys.reduce((acc, key) => {
if (key === oldKey) {
acc[newKey] = oldObj[oldKey];
} else {
acc[key] = oldObj[key];
}
return acc;
}, {} as DependenciesMeta);
}
function replaceAsString(
parsedContents: NpmPackage,
fileContent: string,
depType:
| NpmDepType
| 'dependenciesMeta'
| 'packageManager'
| 'pnpm.overrides',
depName: string,
oldValue: string,
newValue: string,
parents?: string[],
): string {
if (depType === 'packageManager') {
parsedContents[depType] = newValue;
} else if (depType === 'pnpm.overrides') {
parsedContents.pnpm!.overrides![depName] = newValue;
} else if (depName === oldValue) {
// The old value is the name of the dependency itself
delete Object.assign(parsedContents[depType]!, {
[newValue]: parsedContents[depType]![oldValue],
})[oldValue];
} else if (depType === 'dependenciesMeta') {
if (oldValue !== newValue) {
parsedContents.dependenciesMeta = renameObjKey(
// TODO #22198
parsedContents.dependenciesMeta!,
oldValue,
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;
}
// Look for the old version number
const searchString = `"${oldValue}"`;
let newString = `"${newValue}"`;
const escapedDepName = escapeRegExp(depName);
const patchRe = regEx(`^(patch:${escapedDepName}@(npm:)?).*#`);
const match = patchRe.exec(oldValue);
if (match && depType === 'resolutions') {
const patch = oldValue.replace(match[0], `${match[1]}${newValue}#`);
parsedContents[depType]![depName] = patch;
newString = `"${patch}"`;
}
// Skip ahead to depType section
let searchIndex = fileContent.indexOf(`"${depType}"`) + depType.length;
logger.trace(`Starting search at index ${searchIndex}`);
// Iterate through the rest of the file
for (; searchIndex < fileContent.length; searchIndex += 1) {
// First check if we have a hit for the old version
if (matchAt(fileContent, searchIndex, searchString)) {
logger.trace(`Found match at index ${searchIndex}`);
// Now test if the result matches
const testContent = replaceAt(
fileContent,
searchIndex,
searchString,
newString,
);
// Compare the parsed JSON structure of old and new
if (dequal(parsedContents, JSON.parse(testContent))) {
return testContent;
}
}
}
// istanbul ignore next
throw new Error();
}
export function updateDependency({
fileContent,
upgrade,
}: UpdateDependencyConfig): string | null {
const { depType, managerData } = upgrade;
const depName: string = managerData?.key || upgrade.depName;
let { newValue } = upgrade;
if (upgrade.currentRawValue) {
if (upgrade.currentDigest) {
logger.debug('Updating package.json git digest');
newValue = upgrade.currentRawValue.replace(
upgrade.currentDigest,
// TODO #22198
upgrade.newDigest!.substring(0, upgrade.currentDigest.length),
);
} else {
logger.debug('Updating package.json git version tag');
newValue = upgrade.currentRawValue.replace(
upgrade.currentValue,
upgrade.newValue,
);
}
}
if (upgrade.npmPackageAlias) {
newValue = `npm:${upgrade.packageName}@${newValue}`;
}
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 if (depType === 'pnpm.overrides') {
oldVersion = parsedContents.pnpm?.overrides?.[depName];
} else {
oldVersion = parsedContents[depType as NpmDepType]![depName] as string;
}
if (oldVersion === newValue) {
logger.trace('Version is already updated');
return fileContent;
}
// TODO #22198
let newFileContent = replaceAsString(
parsedContents,
fileContent,
depType as NpmDepType,
depName,
oldVersion!,
newValue!,
overrideDepParents,
);
if (upgrade.newName) {
newFileContent = replaceAsString(
parsedContents,
newFileContent,
depType as NpmDepType,
depName,
depName,
upgrade.newName,
overrideDepParents,
);
}
// istanbul ignore if
if (!newFileContent) {
logger.debug(
{ fileContent, parsedContents, depType, depName, newValue },
'Warning: updateDependency error',
);
return fileContent;
}
if (parsedContents?.resolutions) {
let depKey: string | undefined;
if (parsedContents.resolutions[depName]) {
depKey = depName;
} else if (parsedContents.resolutions[`**/${depName}`]) {
depKey = `**/${depName}`;
}
if (depKey) {
// istanbul ignore if
if (parsedContents.resolutions[depKey] !== oldVersion) {
logger.debug(
{
depName,
depKey,
oldVersion,
resolutionsVersion: parsedContents.resolutions[depKey],
},
'Upgraded dependency exists in yarn resolutions but is different version',
);
}
newFileContent = replaceAsString(
parsedContents,
newFileContent,
'resolutions',
depKey,
// TODO #22198
parsedContents.resolutions[depKey]!,
// TODO #22198
newValue!,
);
if (upgrade.newName) {
if (depKey === `**/${depName}`) {
// handles the case where a replacement is in a resolution
upgrade.newName = `**/${upgrade.newName}`;
}
newFileContent = replaceAsString(
parsedContents,
newFileContent,
'resolutions',
depKey,
depKey,
upgrade.newName,
);
}
}
}
if (parsedContents?.dependenciesMeta) {
for (const [depKey] of Object.entries(parsedContents.dependenciesMeta)) {
if (depKey.startsWith(depName + '@')) {
newFileContent = replaceAsString(
parsedContents,
newFileContent,
'dependenciesMeta',
depName,
depKey,
// TODO: types (#22198)
`${depName}@${newValue}`,
);
}
}
}
return newFileContent;
} catch (err) {
logger.debug({ err }, 'updateDependency error');
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'
);
}