From f182708232b5aa623b77f0f3dc7d5e2e002b6567 Mon Sep 17 00:00:00 2001
From: Sergei Zharinov <zharinov@users.noreply.github.com>
Date: Wed, 5 Feb 2025 05:45:05 -0300
Subject: [PATCH] feat(npm): Support for new option `replacementApproach`
 (#34018)

Co-authored-by: Rhys Arkins <rhys@arkins.net>
---
 docs/usage/configuration-options.md           | 10 +++++
 lib/config/options/index.ts                   | 10 +++++
 .../npm/update/dependency/index.spec.ts       | 18 +++++++++
 .../manager/npm/update/dependency/index.ts    | 37 +++++++++++++------
 lib/modules/manager/types.ts                  |  1 +
 5 files changed, 64 insertions(+), 12 deletions(-)

diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index 042dc89b93..987fdfa029 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -3849,6 +3849,16 @@ The field supports multiple URLs but it is datasource-dependent on whether only
 
 Add to this object if you wish to define rules that apply only to PRs that replace dependencies.
 
+## replacementApproach
+
+For `npm` manager when `replacementApproach=alias` then instead of replacing `"foo": "1.2.3"` with `"@my/foo": "1.2.4"` we would instead replace it with `"foo": "npm:@my/foo@1.2.4"`.
+
+```json
+{
+  "replacementApproach": "alias"
+}
+```
+
 ## respectLatest
 
 Similar to `ignoreUnstable`, this option controls whether to update to versions that are greater than the version tagged as `latest` in the repository.
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index c5ad124c28..6f20b33e45 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -1465,6 +1465,16 @@ const options: RenovateOptions[] = [
     cli: false,
     env: false,
   },
+  {
+    name: 'replacementApproach',
+    description:
+      'Select whether to perform a direct replacement or alias replacement.',
+    type: 'string',
+    stage: 'branch',
+    allowedValues: ['replace', 'alias'],
+    supportedManagers: ['npm'],
+    default: 'replace',
+  },
   {
     name: 'matchConfidence',
     description:
diff --git a/lib/modules/manager/npm/update/dependency/index.spec.ts b/lib/modules/manager/npm/update/dependency/index.spec.ts
index e02760aadc..18d871756e 100644
--- a/lib/modules/manager/npm/update/dependency/index.spec.ts
+++ b/lib/modules/manager/npm/update/dependency/index.spec.ts
@@ -1,5 +1,6 @@
 import * as npmUpdater from '../..';
 import { Fixtures } from '../../../../../../test/fixtures';
+import { type Upgrade } from '../../../types';
 
 const readFixture = (x: string): string => Fixtures.get(x, '../..');
 
@@ -254,6 +255,23 @@ describe('modules/manager/npm/update/dependency/index', () => {
       expect(JSON.parse(testContent!).dependencies.abc).toBe('2.0.0');
     });
 
+    it('supports alias-based replacement', () => {
+      const upgrade: Upgrade = {
+        depType: 'dependencies',
+        depName: 'config',
+        newName: 'abc',
+        replacementApproach: 'alias',
+        newValue: '2.0.0',
+      };
+      const testContent = npmUpdater.updateDependency({
+        fileContent: input01Content,
+        upgrade,
+      });
+      expect(JSON.parse(testContent!).dependencies.config).toBe(
+        'npm:abc@2.0.0',
+      );
+    });
+
     it('replaces glob package resolutions', () => {
       const upgrade = {
         depType: 'dependencies',
diff --git a/lib/modules/manager/npm/update/dependency/index.ts b/lib/modules/manager/npm/update/dependency/index.ts
index 0d4bb218f0..c86ed856b2 100644
--- a/lib/modules/manager/npm/update/dependency/index.ts
+++ b/lib/modules/manager/npm/update/dependency/index.ts
@@ -161,25 +161,38 @@ export function updateDependency({
     }
 
     // TODO #22198
-    let newFileContent = replaceAsString(
-      parsedContents,
-      fileContent,
-      depType as NpmDepType,
-      depName,
-      oldVersion!,
-      newValue!,
-      overrideDepParents,
-    );
-    if (upgrade.newName) {
+    let newFileContent: string;
+    if (upgrade.newName && upgrade.replacementApproach === 'alias') {
       newFileContent = replaceAsString(
         parsedContents,
-        newFileContent,
+        fileContent,
         depType as NpmDepType,
         depName,
+        oldVersion!,
+        `npm:${upgrade.newName}@${newValue}`,
+        overrideDepParents,
+      );
+    } else {
+      newFileContent = replaceAsString(
+        parsedContents,
+        fileContent,
+        depType as NpmDepType,
         depName,
-        upgrade.newName,
+        oldVersion!,
+        newValue!,
         overrideDepParents,
       );
+      if (upgrade.newName) {
+        newFileContent = replaceAsString(
+          parsedContents,
+          newFileContent,
+          depType as NpmDepType,
+          depName,
+          depName,
+          upgrade.newName,
+          overrideDepParents,
+        );
+      }
     }
     // istanbul ignore if
     if (!newFileContent) {
diff --git a/lib/modules/manager/types.ts b/lib/modules/manager/types.ts
index 28ede22bda..3ce881369a 100644
--- a/lib/modules/manager/types.ts
+++ b/lib/modules/manager/types.ts
@@ -192,6 +192,7 @@ export interface Upgrade<T = Record<string, any>> extends PackageDependency<T> {
   registryUrls?: string[] | null;
   currentVersion?: string;
   replaceString?: string;
+  replacementApproach?: 'replace' | 'alias';
 }
 
 export interface ArtifactNotice {
-- 
GitLab