From 8fa94141a124a29aed1a5ee973ba5c4490607acf Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@keylocation.sg>
Date: Tue, 12 Sep 2017 07:49:56 +0200
Subject: [PATCH] feat: use package names for ignoring when lerna or workspaces
 (#787)

* add minimatch

* feat: use package names for ignoring when lerna or workspaces

Renovate will now:
- Find all package.json files matching lerna or yarn workspaces glob pattern
- Retrieve package names from within those package.json files
- Implicitly ignore (not renvoate) any of those names

Closes #781
---
 lib/workers/dep-type/index.js                 | 11 +--
 lib/workers/repository/apis.js                | 71 +++++++++-----
 lib/workers/repository/index.js               |  2 +
 package.json                                  |  1 +
 test/workers/dep-type/index.spec.js           |  2 +-
 .../__snapshots__/apis.spec.js.snap           | 28 ++----
 test/workers/repository/apis.spec.js          | 97 ++++++++++---------
 yarn.lock                                     |  2 +-
 8 files changed, 114 insertions(+), 100 deletions(-)

diff --git a/lib/workers/dep-type/index.js b/lib/workers/dep-type/index.js
index a089b82215..d19d0f600f 100644
--- a/lib/workers/dep-type/index.js
+++ b/lib/workers/dep-type/index.js
@@ -26,14 +26,11 @@ async function renovateDepType(packageContent, config) {
   logger.debug(`currentDeps length is ${currentDeps.length}`);
   logger.debug({ currentDeps }, `currentDeps`);
   // Filter out ignored dependencies
-  let filteredDeps = currentDeps.filter(
-    dependency => config.ignoreDeps.indexOf(dependency.depName) === -1
+  const filteredDeps = currentDeps.filter(
+    dependency =>
+      config.ignoreDeps.indexOf(dependency.depName) === -1 &&
+      config.monorepoPackages.indexOf(dependency.depName) === -1
   );
-  if (config.lernaPackages) {
-    filteredDeps = filteredDeps.filter(
-      dependency => config.lernaPackages.indexOf(dependency.depName) === -1
-    );
-  }
   logger.debug(`filteredDeps length is ${filteredDeps.length}`);
   logger.debug({ filteredDeps }, `filteredDeps`);
   // Obtain full config for each dependency
diff --git a/lib/workers/repository/apis.js b/lib/workers/repository/apis.js
index 4c915d7ff3..f18eddbdd6 100644
--- a/lib/workers/repository/apis.js
+++ b/lib/workers/repository/apis.js
@@ -1,3 +1,4 @@
+const minimatch = require('minimatch');
 const conventionalCommitsDetector = require('conventional-commits-detector');
 const path = require('path');
 const jsonValidator = require('json-dup-key-validator');
@@ -12,10 +13,10 @@ const gitlabApi = require('../../api/gitlab');
 
 module.exports = {
   detectSemanticCommits,
+  checkMonorepos,
   getNpmrc,
   initApis,
   mergeRenovateJson,
-  checkForLerna,
   detectPackageFiles,
   resolvePackageFiles,
   migrateAndValidate,
@@ -35,6 +36,50 @@ async function detectSemanticCommits(config) {
   return true;
 }
 
+async function checkMonorepos(input) {
+  const config = { ...input };
+  const { logger } = config;
+  config.monorepoPackages = [];
+  // yarn workspaces
+  if (config.hasYarnWorkspaces) {
+    let workspaces = [];
+    for (const packageFile of config.packageFiles) {
+      if (packageFile.packageFile === 'package.json') {
+        workspaces = packageFile.content.workspaces;
+      }
+    }
+    logger.debug({ workspaces }, 'workspaces');
+    for (const workspace of workspaces) {
+      for (const packageFile of config.packageFiles) {
+        if (minimatch(path.dirname(packageFile.packageFile), workspace)) {
+          const depName = packageFile.content.name;
+          config.monorepoPackages.push(depName);
+        }
+      }
+    }
+  }
+  // lerna
+  const lernaJson = await config.api.getFileJson('lerna.json');
+  if (!lernaJson) {
+    return config;
+  }
+  config.logger.debug({ lernaJson }, 'Found lerna config');
+  if (!lernaJson.packages) {
+    return config;
+  }
+  for (const packageGlob of lernaJson.packages) {
+    for (const packageFile of config.packageFiles) {
+      if (minimatch(path.dirname(packageFile.packageFile), packageGlob)) {
+        const depName = packageFile.content.name;
+        if (!config.monorepoPackages.includes(depName)) {
+          config.monorepoPackages.push(depName);
+        }
+      }
+    }
+  }
+  return config;
+}
+
 // Check for .npmrc in repository and pass it to npm api if found
 async function getNpmrc(config) {
   let npmrc;
@@ -49,27 +94,6 @@ async function getNpmrc(config) {
   return { ...config, npmrc };
 }
 
-async function checkForLerna(config) {
-  const lernaJson = await config.api.getFileJson('lerna.json');
-  if (!lernaJson) {
-    return {};
-  }
-  config.logger.debug({ lernaJson }, 'Found lerna config');
-  try {
-    const packagesPath = lernaJson.packages
-      ? lernaJson.packages[0].slice(0, -2)
-      : 'packages';
-    const lernaPackages = await config.api.getSubDirectories(packagesPath);
-    if (lernaPackages.length === 0) {
-      return {};
-    }
-    return { lernaPackages };
-  } catch (err) {
-    config.logger.info('could not find any lerna subdirectories');
-    return {};
-  }
-}
-
 async function initApis(inputConfig, token) {
   function getPlatformApi(platform) {
     if (platform === 'github') {
@@ -88,10 +112,7 @@ async function initApis(inputConfig, token) {
     config.endpoint,
     config.logger
   );
-  // Check for presence of .npmrc in repository
   Object.assign(config, platformConfig);
-  const lernaConfig = await module.exports.checkForLerna(config);
-  Object.assign(config, lernaConfig);
   if (config.semanticCommits === null) {
     config.semanticCommits = await module.exports.detectSemanticCommits(config);
   }
diff --git a/lib/workers/repository/index.js b/lib/workers/repository/index.js
index 709a6ba7bd..053f6588e0 100644
--- a/lib/workers/repository/index.js
+++ b/lib/workers/repository/index.js
@@ -79,6 +79,7 @@ async function renovateRepository(repoConfig, token) {
       }
       logger.debug('Resolving package files and content');
       config = await apis.resolvePackageFiles(config);
+      config = await apis.checkMonorepos(config);
       logger.trace({ config }, 'post-packageFiles config');
       // TODO: why is this fix needed?!
       config.logger = logger;
@@ -108,6 +109,7 @@ async function renovateRepository(repoConfig, token) {
           }
         }
         config = await apis.resolvePackageFiles(config);
+        config = await apis.checkMonorepos(config);
         config = await presets.resolveConfigPresets(config);
         config.logger = logger;
         logger.trace({ config }, 'onboarding config');
diff --git a/package.json b/package.json
index 507c881fed..cb6afb01cb 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
     "jsonwebtoken": "7.4.3",
     "later": "1.2.0",
     "lodash": "4.17.4",
+    "minimatch": "3.0.4",
     "moment": "2.18.1",
     "moment-timezone": "0.5.13",
     "registry-auth-token": "3.3.1",
diff --git a/test/workers/dep-type/index.spec.js b/test/workers/dep-type/index.spec.js
index d97e55e328..65860a0271 100644
--- a/test/workers/dep-type/index.spec.js
+++ b/test/workers/dep-type/index.spec.js
@@ -15,7 +15,7 @@ describe('lib/workers/dep-type/index', () => {
     beforeEach(() => {
       config = {
         ignoreDeps: ['a', 'b'],
-        lernaPackages: ['e'],
+        monorepoPackages: ['e'],
       };
     });
     it('returns empty if config is disabled', async () => {
diff --git a/test/workers/repository/__snapshots__/apis.spec.js.snap b/test/workers/repository/__snapshots__/apis.spec.js.snap
index 7af498776c..13465b626c 100644
--- a/test/workers/repository/__snapshots__/apis.spec.js.snap
+++ b/test/workers/repository/__snapshots__/apis.spec.js.snap
@@ -7,28 +7,20 @@ Object {
 }
 `;
 
-exports[`workers/repository/apis checkForLerna(config) ignores zero length lerna 1`] = `Object {}`;
-
-exports[`workers/repository/apis checkForLerna(config) implies lerna package path 1`] = `
-Object {
-  "lernaPackages": Array [
-    "a",
-    "b",
-  ],
-}
+exports[`workers/repository/apis checkMonorepos adds lerna packages 1`] = `
+Array [
+  "@a/b",
+  "@a/c",
+]
 `;
 
-exports[`workers/repository/apis checkForLerna(config) returns lerna package names 1`] = `
-Object {
-  "lernaPackages": Array [
-    "a",
-    "b",
-  ],
-}
+exports[`workers/repository/apis checkMonorepos adds yarn workspaces 1`] = `
+Array [
+  "@a/b",
+  "@a/c",
+]
 `;
 
-exports[`workers/repository/apis checkForLerna(config) swallows lerna 404 1`] = `Object {}`;
-
 exports[`workers/repository/apis detectPackageFiles(config) adds package files to object 1`] = `
 Array [
   "package.json",
diff --git a/test/workers/repository/apis.spec.js b/test/workers/repository/apis.spec.js
index 24da3950d6..bada3eef81 100644
--- a/test/workers/repository/apis.spec.js
+++ b/test/workers/repository/apis.spec.js
@@ -45,75 +45,76 @@ describe('workers/repository/apis', () => {
       expect(await apis.getNpmrc(config)).toMatchObject(config);
     });
   });
-  describe('detectSemanticCommits', () => {
-    it('disables semantic commits', async () => {
-      const config = {
+  describe('checkMonorepos', () => {
+    let config;
+    beforeEach(() => {
+      config = {
+        ...defaultConfig,
         api: {
-          getCommitMessages: jest.fn(() => []),
+          getFileJson: jest.fn(),
         },
         logger,
       };
-      const res = await apis.detectSemanticCommits(config);
-      expect(res).toEqual(false);
     });
-    it('enables semantic commits', async () => {
-      const config = {
-        api: {
-          getCommitMessages: jest.fn(() => []),
+    it('adds yarn workspaces', async () => {
+      config.hasYarnWorkspaces = true;
+      config.packageFiles = [
+        {
+          packageFile: 'package.json',
+          content: { workspaces: ['packages/*'] },
         },
-        logger,
-      };
-      config.api.getCommitMessages.mockReturnValueOnce(['fix: something']);
-      const res = await apis.detectSemanticCommits(config);
-      expect(res).toEqual(true);
-    });
-  });
-  describe('checkForLerna(config)', () => {
-    it('swallows lerna 404', async () => {
-      const config = {
-        api: {
-          getFileJson: jest.fn(() => ({})),
-          getSubDirectories: jest.fn(() => {
-            throw new Error('some-error');
-          }),
+        {
+          packageFile: 'packages/something/package.json',
+          content: { name: '@a/b' },
         },
-        logger,
-      };
-      const res = await apis.checkForLerna(config);
-      expect(res).toMatchSnapshot();
+        {
+          packageFile: 'packages/something-else/package.json',
+          content: { name: '@a/c' },
+        },
+      ];
+      const res = await apis.checkMonorepos(config);
+      expect(res.monorepoPackages).toMatchSnapshot();
     });
-    it('ignores zero length lerna', async () => {
-      const config = {
-        api: {
-          getFileJson: jest.fn(() => ({ packages: ['packages/*'] })),
-          getSubDirectories: jest.fn(() => []),
+    it('adds lerna packages', async () => {
+      config.packageFiles = [
+        {
+          packageFile: 'package.json',
         },
-        logger,
-      };
-      const res = await apis.checkForLerna(config);
-      expect(res).toMatchSnapshot();
+        {
+          packageFile: 'packages/something/package.json',
+          content: { name: '@a/b' },
+        },
+        {
+          packageFile: 'packages/something-else/package.json',
+          content: { name: '@a/c' },
+        },
+      ];
+      config.api.getFileJson.mockReturnValue({ packages: ['packages/*'] });
+      const res = await apis.checkMonorepos(config);
+      expect(res.monorepoPackages).toMatchSnapshot();
     });
-    it('implies lerna package path', async () => {
+  });
+  describe('detectSemanticCommits', () => {
+    it('disables semantic commits', async () => {
       const config = {
         api: {
-          getFileJson: jest.fn(() => ({})),
-          getSubDirectories: jest.fn(() => ['a', 'b']),
+          getCommitMessages: jest.fn(() => []),
         },
         logger,
       };
-      const res = await apis.checkForLerna(config);
-      expect(res).toMatchSnapshot();
+      const res = await apis.detectSemanticCommits(config);
+      expect(res).toEqual(false);
     });
-    it('returns lerna package names', async () => {
+    it('enables semantic commits', async () => {
       const config = {
         api: {
-          getFileJson: jest.fn(() => ({ packages: ['packages/*'] })),
-          getSubDirectories: jest.fn(() => ['a', 'b']),
+          getCommitMessages: jest.fn(() => []),
         },
         logger,
       };
-      const res = await apis.checkForLerna(config);
-      expect(res).toMatchSnapshot();
+      config.api.getCommitMessages.mockReturnValueOnce(['fix: something']);
+      const res = await apis.detectSemanticCommits(config);
+      expect(res).toEqual(true);
     });
   });
   describe('initApis(config)', () => {
diff --git a/yarn.lock b/yarn.lock
index e8ae67d2c4..036741cee2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2958,7 +2958,7 @@ mimic-response@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e"
 
-"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   dependencies:
-- 
GitLab