From 4aa5cdc721f9248cd1d27537eaa9528c38769751 Mon Sep 17 00:00:00 2001
From: Oleg Krivtsov <olegkrivtsov@gmail.com>
Date: Thu, 23 Sep 2021 13:48:46 +0700
Subject: [PATCH] feat(datasource/npm): massage non compliant npm repo strings
 (#11776)

---
 lib/datasource/npm/get.spec.ts | 117 +++++++++++++++++++++++++++++++++
 lib/datasource/npm/get.ts      |  21 ++++++
 2 files changed, 138 insertions(+)

diff --git a/lib/datasource/npm/get.spec.ts b/lib/datasource/npm/get.spec.ts
index 9486d34075..a983e37223 100644
--- a/lib/datasource/npm/get.spec.ts
+++ b/lib/datasource/npm/get.spec.ts
@@ -230,4 +230,121 @@ describe('datasource/npm/get', () => {
 
     expect(httpMock.getTrace()).toMatchSnapshot();
   });
+
+  it('massages non-compliant repository urls', async () => {
+    setNpmrc('registry=https://test.org\n_authToken=XXX');
+
+    httpMock
+      .scope('https://test.org')
+      .get('/@neutrinojs%2Freact')
+      .reply(200, {
+        name: '@neutrinojs/react',
+        repository: {
+          type: 'git',
+          url: 'https://github.com/neutrinojs/neutrino/tree/master/packages/react',
+        },
+        versions: { '1.0.0': {} },
+        'dist-tags': { latest: '1.0.0' },
+      });
+
+    const dep = await getDependency('@neutrinojs/react');
+
+    expect(dep.sourceUrl).toBe('https://github.com/neutrinojs/neutrino');
+    expect(dep.sourceDirectory).toBe('packages/react');
+
+    expect(httpMock.getTrace()).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "headers": Object {
+            "accept": "application/json",
+            "accept-encoding": "gzip, deflate, br",
+            "authorization": "Bearer XXX",
+            "host": "test.org",
+            "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)",
+          },
+          "method": "GET",
+          "url": "https://test.org/@neutrinojs%2Freact",
+        },
+      ]
+    `);
+  });
+
+  it('warns about repo directory override', async () => {
+    setNpmrc('registry=https://test.org\n_authToken=XXX');
+
+    httpMock
+      .scope('https://test.org')
+      .get('/@neutrinojs%2Freact')
+      .reply(200, {
+        name: '@neutrinojs/react',
+        repository: {
+          type: 'git',
+          url: 'https://github.com/neutrinojs/neutrino/tree/master/packages/react',
+          directory: 'path/to/directory',
+        },
+        versions: { '1.0.0': {} },
+        'dist-tags': { latest: '1.0.0' },
+      });
+
+    const dep = await getDependency('@neutrinojs/react');
+
+    expect(dep.sourceUrl).toBe('https://github.com/neutrinojs/neutrino');
+    expect(dep.sourceDirectory).toBe('packages/react');
+
+    expect(httpMock.getTrace()).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "headers": Object {
+            "accept": "application/json",
+            "accept-encoding": "gzip, deflate, br",
+            "authorization": "Bearer XXX",
+            "host": "test.org",
+            "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)",
+          },
+          "method": "GET",
+          "url": "https://test.org/@neutrinojs%2Freact",
+        },
+      ]
+    `);
+  });
+
+  it('does not massage non-github non-compliant repository urls', async () => {
+    setNpmrc('registry=https://test.org\n_authToken=XXX');
+
+    httpMock
+      .scope('https://test.org')
+      .get('/@neutrinojs%2Freact')
+      .reply(200, {
+        name: '@neutrinojs/react',
+        repository: {
+          type: 'git',
+          url: 'https://bitbucket.org/neutrinojs/neutrino/tree/master/packages/react',
+        },
+        versions: { '1.0.0': {} },
+        'dist-tags': { latest: '1.0.0' },
+      });
+
+    const dep = await getDependency('@neutrinojs/react');
+
+    expect(dep.sourceUrl).toBe(
+      'https://bitbucket.org/neutrinojs/neutrino/tree/master/packages/react'
+    );
+    expect(dep.sourceDirectory).toBeUndefined();
+
+    expect(httpMock.getTrace()).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "headers": Object {
+            "accept": "application/json",
+            "accept-encoding": "gzip, deflate, br",
+            "authorization": "Bearer XXX",
+            "host": "test.org",
+            "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)",
+          },
+          "method": "GET",
+          "url": "https://test.org/@neutrinojs%2Freact",
+        },
+      ]
+    `);
+  });
 });
diff --git a/lib/datasource/npm/get.ts b/lib/datasource/npm/get.ts
index cfbb1764e4..98ee0b7388 100644
--- a/lib/datasource/npm/get.ts
+++ b/lib/datasource/npm/get.ts
@@ -80,6 +80,7 @@ export async function getDependency(
         sourceUrl = res.repository.url;
       }
     }
+
     // Simplify response before caching and returning
     const dep: NpmDependency = {
       name: res.name,
@@ -93,6 +94,26 @@ export async function getDependency(
     if (res.repository?.directory) {
       dep.sourceDirectory = res.repository.directory;
     }
+
+    // Massage the repository URL for non-compliant strings for github (see issue #4610)
+    // Remove the non-compliant segments of path, so the URL looks like "<scheme>://<domain>/<vendor>/<repo>"
+    // and add directory to the repository
+    const sourceUrlCopy = `${sourceUrl}`;
+    const sourceUrlSplit: string[] = sourceUrlCopy.split('/');
+
+    if (sourceUrlSplit.length > 7 && sourceUrlSplit[2] === 'github.com') {
+      if (dep.sourceDirectory) {
+        logger.debug(
+          { dependency: packageName },
+          `Ambiguity: dependency has the repository URL path and repository/directory set at once; have to override repository/directory`
+        );
+      }
+      dep.sourceUrl = sourceUrlSplit.slice(0, 5).join('/');
+      dep.sourceDirectory = sourceUrlSplit
+        .slice(7, sourceUrlSplit.length)
+        .join('/');
+    }
+
     if (latestVersion.deprecated) {
       dep.deprecationMessage = `On registry \`${registryUrl}\`, the "latest" version of dependency \`${packageName}\` has the following deprecation notice:\n\n\`${latestVersion.deprecated}\`\n\nMarking the latest version of an npm package as deprecated results in the entire package being considered deprecated, so contact the package author you think this is a mistake.`;
       dep.deprecationSource = id;
-- 
GitLab