Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: replace deprecated dependencies with their replacements #5558

Merged
merged 5 commits into from
Nov 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -1652,6 +1652,31 @@ For example to apply a special label for Major updates:
}
```

### replacementName
JamieMagee marked this conversation as resolved.
Show resolved Hide resolved
JamieMagee marked this conversation as resolved.
Show resolved Hide resolved
JamieMagee marked this conversation as resolved.
Show resolved Hide resolved

Use this field to define the name of a replacement package.
Must be used with `replacementVersion` (see example below).
You can suggest a new community package rule by editing [the `replacements.ts` file on the Renovate repository](https://github.com/renovatebot/renovate/blob/main/lib/config/presets/internal/replacements.ts) and opening a pull request.

### replacementVersion

Use this field to define the name of a replacement package.
Must be used with `replacementVersion`.
For example to replace the npm package `jade` with version `2.0.0` of the package `pug`:
rarkins marked this conversation as resolved.
Show resolved Hide resolved

```json
{
"packageRules": [
{
"matchDatasources": ["npm"],
"matchPackageNames": ["jade"],
"replacementName": "pug",
"replacementVersion": "2.0.0"
}
]
}
```

## patch

Add to this object if you wish to define rules that apply only to patch updates.
Expand Down Expand Up @@ -2233,6 +2258,10 @@ In case there is a need to configure them manually, it can be done using this `r

The field supports multiple URLs however it is datasource-dependent on whether only the first is used or multiple.

## replacement

Add to this object if you wish to define rules that apply only to PRs that replace dependencies.

## respectLatest

Similar to `ignoreUnstable`, this option controls whether to update to versions that are greater than the version tagged as `latest` in the repository.
Expand Down
39 changes: 39 additions & 0 deletions lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,28 @@ const options: RenovateOptions[] = [
cli: false,
env: false,
},
{
name: 'replacementName',
description:
'The name of the new dependency that replaces the old deprecated dependency.',
type: 'string',
stage: 'package',
parent: 'packageRules',
mergeable: true,
cli: false,
env: false,
},
{
name: 'replacementVersion',
description:
'The version of the new dependency that replaces the old deprecated dependency.',
type: 'string',
stage: 'package',
parent: 'packageRules',
mergeable: true,
cli: false,
env: false,
},
{
name: 'matchUpdateTypes',
description:
Expand Down Expand Up @@ -1196,6 +1218,23 @@ const options: RenovateOptions[] = [
cli: false,
mergeable: true,
},
{
name: 'replacement',
description: 'Configuration to apply when replacing a dependency.',
stage: 'package',
type: 'object',
default: {
branchTopic: '{{{depNameSanitized}}}-replacement',
commitMessageAction: 'Replace',
commitMessageExtra:
'with {{newName}} {{#if isMajor}}v{{{newMajor}}}{{else}}{{#if isSingleVersion}}v{{{newVersion}}}{{else}}{{{newValue}}}{{/if}}{{/if}}',
prBodyNotes: [
'This is a special PR that replaces `{{{depNameSanitized}}}` with the community suggested minimal stable replacement version.',
],
},
cli: false,
mergeable: true,
},
// Semantic commit / Semantic release
{
name: 'semanticCommits',
Expand Down
1 change: 1 addition & 0 deletions lib/config/presets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export function parsePreset(input: string): ParsedPreset {
'packages',
'preview',
'regexManagers',
'replacements',
'schedule',
'workarounds',
];
Expand Down
2 changes: 2 additions & 0 deletions lib/config/presets/internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as npm from './npm';
import * as packagesPreset from './packages';
import * as previewPreset from './preview';
import * as regexManagersPreset from './regex-managers';
import * as replacements from './replacements';
import * as schedulePreset from './schedule';
import * as workaroundsPreset from './workarounds';

Expand All @@ -25,6 +26,7 @@ export const groups: Record<string, Record<string, Preset>> = {
packages: packagesPreset.presets,
preview: previewPreset.presets,
regexManagers: regexManagersPreset.presets,
replacements: replacements.presets,
schedule: schedulePreset.presets,
workarounds: workaroundsPreset.presets,
};
Expand Down
19 changes: 19 additions & 0 deletions lib/config/presets/internal/replacements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Preset } from '../types';
JamieMagee marked this conversation as resolved.
Show resolved Hide resolved

export const presets: Record<string, Preset> = {
all: {
description: 'All replacements',
extends: ['replacements:jade-to-pug'],
},
'jade-to-pug': {
description: 'Jade was renamed to Pug',
packageRules: [
{
matchDatasources: ['npm'],
matchPackageNames: ['jade'],
replacementName: 'pug',
replacementVersion: '2.0.0',
JamieMagee marked this conversation as resolved.
Show resolved Hide resolved
},
],
},
};
3 changes: 2 additions & 1 deletion lib/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,8 @@ export type UpdateType =
| 'lockFileMaintenance'
| 'lockfileUpdate'
| 'rollback'
| 'bump';
| 'bump'
| 'replacement';

export type MatchStringsStrategy = 'any' | 'recursive' | 'combination';

Expand Down
14 changes: 14 additions & 0 deletions lib/datasource/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,18 @@ describe('datasource/index', () => {
});
expect(res.sourceUrl).toBe('https://github.com/Jasig/cas');
});

it('applies replacements', async () => {
JamieMagee marked this conversation as resolved.
Show resolved Hide resolved
npmDatasource.getReleases.mockResolvedValue({
releases: [{ version: '1.0.0' }],
});
const res = await datasource.getPkgReleases({
datasource: datasourceNpm.id,
depName: 'abc',
replacementName: 'def',
replacementVersion: '2.0.0',
rarkins marked this conversation as resolved.
Show resolved Hide resolved
});
viceice marked this conversation as resolved.
Show resolved Hide resolved
expect(res.replacementName).toBe('def');
expect(res.replacementVersion).toBe('2.0.0');
});
});
13 changes: 13 additions & 0 deletions lib/datasource/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,18 @@ export function getDefaultVersioning(datasourceName: string): string {
return datasource?.defaultVersioning || 'semver';
}

function applyReplacements(
config: GetReleasesInternalConfig
): Pick<ReleaseResult, 'replacementName' | 'replacementVersion'> | undefined {
if (config.replacementName && config.replacementVersion) {
return {
replacementName: config.replacementName,
replacementVersion: config.replacementVersion,
};
}
return undefined;
}

async function fetchReleases(
config: GetReleasesInternalConfig
): Promise<ReleaseResult | null> {
Expand Down Expand Up @@ -250,6 +262,7 @@ async function fetchReleases(
return null;
}
addMetaData(dep, datasourceName, config.lookupName);
dep = { ...dep, ...applyReplacements(config) };
return dep;
}

Expand Down
4 changes: 4 additions & 0 deletions lib/datasource/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export interface GetPkgReleasesConfig extends ReleasesConfigBase {
versioning?: string;
extractVersion?: string;
constraints?: Record<string, string>;
replacementName?: string;
replacementVersion?: string;
}

export interface Release {
Expand Down Expand Up @@ -60,6 +62,8 @@ export interface ReleaseResult {
sourceUrl?: string;
sourceDirectory?: string;
registryUrl?: string;
replacementName?: string;
replacementVersion?: string;
}

export interface DatasourceApi {
Expand Down
43 changes: 43 additions & 0 deletions lib/manager/npm/update/dependency/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,5 +208,48 @@ describe('manager/npm/update/dependency/index', () => {
});
expect(testContent).toEqual(outputContent);
});

it('returns null if empty file', () => {
JamieMagee marked this conversation as resolved.
Show resolved Hide resolved
const upgrade = {
depType: 'dependencies',
depName: 'angular-touch-not',
newValue: '1.5.8',
};
const testContent = npmUpdater.updateDependency({
fileContent: null,
upgrade,
});
expect(testContent).toBeNull();
});

it('replaces package', () => {
JamieMagee marked this conversation as resolved.
Show resolved Hide resolved
const upgrade = {
depType: 'dependencies',
depName: 'config',
newName: 'abc',
newValue: '2.0.0',
};
const testContent = npmUpdater.updateDependency({
fileContent: input01Content,
upgrade,
});
expect(JSON.parse(testContent).dependencies.config).toBeUndefined();
expect(JSON.parse(testContent).dependencies.abc).toBe('2.0.0');
});

it('replaces glob package resolutions', () => {
const upgrade = {
depType: 'dependencies',
depName: 'config',
newName: 'abc',
newValue: '2.0.0',
};
const testContent = npmUpdater.updateDependency({
fileContent: input01GlobContent,
upgrade,
});
expect(JSON.parse(testContent).resolutions.config).toBeUndefined();
expect(JSON.parse(testContent).resolutions['**/abc']).toBe('2.0.0');
});
});
});
35 changes: 32 additions & 3 deletions lib/manager/npm/update/dependency/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,22 @@ function replaceAsString(
fileContent: string,
depType: string,
depName: string,
oldVersion: string,
oldValue: string,
newValue: string
): string | null {
// Update the file = this is what we want
if (depType === 'packageManager') {
parsedContents[depType] = 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 {
// The old value is the version of the dependency
parsedContents[depType][depName] = newValue;
}
// Look for the old version number
const searchString = `"${oldVersion}"`;
const searchString = `"${oldValue}"`;
const newString = `"${newValue}"`;
// Skip ahead to depType section
let searchIndex = fileContent.indexOf(`"${depType}"`) + depType.length;
Expand Down Expand Up @@ -94,6 +99,16 @@ export function updateDependency({
oldVersion,
newValue
);
if (upgrade.newName) {
newFileContent = replaceAsString(
parsedContents,
newFileContent,
depType,
depName,
depName,
upgrade.newName
);
}
// istanbul ignore if
if (!newFileContent) {
logger.debug(
Expand Down Expand Up @@ -130,6 +145,20 @@ export function updateDependency({
parsedContents.resolutions[depKey],
newValue
);
if (upgrade.newName) {
if (depKey === `**/${depName}`) {
// handles the case where a replacement is in a resolution
upgrade.newName = `**/${upgrade.newName}`;
viceice marked this conversation as resolved.
Show resolved Hide resolved
}
newFileContent = replaceAsString(
parsedContents,
newFileContent,
'resolutions',
depKey,
depKey,
upgrade.newName
);
}
JamieMagee marked this conversation as resolved.
Show resolved Hide resolved
}
}
return newFileContent;
Expand Down
3 changes: 3 additions & 0 deletions lib/manager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,11 @@ export interface LookupUpdate {
isPin?: boolean;
isRange?: boolean;
isRollback?: boolean;
isReplacement?: boolean;
newDigest?: string;
newMajor?: number;
newMinor?: number;
newName?: string;
newValue: string;
semanticCommitType?: string;
pendingChecks?: boolean;
Expand Down Expand Up @@ -177,6 +179,7 @@ export interface Upgrade<T = Record<string, any>>
newDigest?: string;
newFrom?: string;
newMajor?: number;
newName?: string;
newValue?: string;
packageFile?: string;
rangeStrategy?: RangeStrategy;
Expand Down
1 change: 1 addition & 0 deletions lib/util/merge-confidence/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const updateTypeConfidenceMapping: Record<UpdateType, MergeConfidence> = {
lockFileMaintenance: 'neutral',
lockfileUpdate: 'neutral',
rollback: 'neutral',
replacement: 'neutral',
major: null,
minor: null,
patch: null,
Expand Down
5 changes: 4 additions & 1 deletion lib/util/template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const allowedFields = {
isPatch: 'true if the upgrade is a patch upgrade',
isPin: 'true if the upgrade is pinning dependencies',
isRollback: 'true if the upgrade is a rollback PR',
isReplacement: 'true if the upgrade is a replacement',
isRange: 'true if the new value is a range',
isSingleVersion:
'true if the upgrade is to a single version rather than a range',
Expand All @@ -72,6 +73,8 @@ export const allowedFields = {
'The major version of the new version. e.g. "3" if the new version if "3.1.0"',
newMinor:
'The minor version of the new version. e.g. "1" if the new version if "3.1.0"',
newName:
'The name of the new dependency that replaces the current deprecated dependency',
newValue:
'The new value in the upgrade. Can be a range or version e.g. "^3.0.0" or "3.1.0"',
newVersion: 'The new version in the upgrade, e.g. "3.1.0"',
Expand All @@ -91,7 +94,7 @@ export const allowedFields = {
semanticPrefix: 'The fully generated semantic prefix for commit messages',
sourceRepoSlug: 'The slugified pathname of the sourceUrl, if present',
sourceUrl: 'The source URL for the package',
updateType: 'One of digest, pin, rollback, patch, minor, major',
updateType: 'One of digest, pin, rollback, patch, minor, major, replacement',
upgrades: 'An array of upgrade objects in the branch',
url: 'The url of the release notes',
version: 'The version number of the changelog',
Expand Down
Loading