From 23f8467d1e9486bbfd17ace6b9ec29568378e16c Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@keylocation.sg>
Date: Fri, 29 Sep 2017 08:08:52 +0200
Subject: [PATCH] feat: support non-root yarn workspaces (#852)

Previously, Renovate assumed that any yarn workspaces configuration would be in the root of the repository. Now, workspaces can be located within a subdirectory, e.g. `frontend/`. Note: Renovate still supports only one workspace per repository, please file an issue if you require more than one.

Closes #842
---
 lib/workers/branch/lock-files.js              | 14 ++++--
 lib/workers/repository/apis.js                | 44 ++++++++++---------
 test/workers/branch/lock-files.spec.js        |  3 +-
 .../__snapshots__/apis.spec.js.snap           |  9 ++++
 test/workers/repository/apis.spec.js          | 31 ++++++++++++-
 test/workers/repository/index.spec.js         |  1 +
 6 files changed, 76 insertions(+), 26 deletions(-)

diff --git a/lib/workers/branch/lock-files.js b/lib/workers/branch/lock-files.js
index fc01f65e84..c7f78385f6 100644
--- a/lib/workers/branch/lock-files.js
+++ b/lib/workers/branch/lock-files.js
@@ -69,9 +69,17 @@ function determineLockFileDirs(config) {
     }
   }
 
-  // If yarn workspaces are in use, then we need to generate yarn.lock from root dir
-  if (config.updatedPackageFiles.length && config.hasYarnWorkspaces) {
-    yarnLockFileDirs.push(path.dirname('package.json'));
+  // If yarn workspaces are in use, then we need to generate yarn.lock from the workspaces dir
+  if (config.updatedPackageFiles.length && config.workspaceDir) {
+    const updatedPackageFileNames = config.updatedPackageFiles.map(p => p.name);
+    for (const packageFile of config.packageFiles) {
+      if (
+        updatedPackageFileNames.includes(packageFile.packageFile) &&
+        packageFile.workspaceDir &&
+        !yarnLockFileDirs.includes(packageFile.workspaceDir)
+      )
+        yarnLockFileDirs.push(packageFile.workspaceDir);
+    }
   }
 
   return { yarnLockFileDirs, packageLockFileDirs };
diff --git a/lib/workers/repository/apis.js b/lib/workers/repository/apis.js
index b4498493b4..8a0645c39e 100644
--- a/lib/workers/repository/apis.js
+++ b/lib/workers/repository/apis.js
@@ -42,20 +42,30 @@ async function checkMonorepos(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);
-      }
+  let workspaces = [];
+  for (const packageFile of config.packageFiles) {
+    if (
+      packageFile.packageFile &&
+      packageFile.packageFile.endsWith('package.json') &&
+      packageFile.content.workspaces
+    ) {
+      config.workspaceDir = path.dirname(packageFile.packageFile);
+      logger.info(`workspaceDir=${config.workspaceDir}`);
+      ({ workspaces } = packageFile.content);
     }
-    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);
-        }
+  }
+  if (workspaces.length) {
+    logger.debug({ workspaces }, 'Found yarn workspaces');
+  }
+  for (const workspace of workspaces) {
+    const basePath = path.join(config.workspaceDir, workspace);
+    logger.info(`basePath=${basePath}`);
+    for (const packageFile of config.packageFiles) {
+      if (minimatch(path.dirname(packageFile.packageFile), basePath)) {
+        logger.info(`Matched ${packageFile.packageFile}`);
+        const depName = packageFile.content.name;
+        config.monorepoPackages.push(depName);
+        packageFile.workspaceDir = config.workspaceDir;
       }
     }
   }
@@ -315,14 +325,6 @@ async function resolvePackageFiles(inputConfig) {
         delete packageFile.yarnrc;
       }
       if (packageFile.content) {
-        // check for workspaces
-        if (
-          packageFile.packageFile === 'package.json' &&
-          packageFile.content.workspaces
-        ) {
-          logger.info('Found yarn workspaces configuration');
-          config.hasYarnWorkspaces = true;
-        }
         // hoist renovate config if exists
         if (packageFile.content.renovate) {
           config.hasPackageJsonRenovateConfig = true;
diff --git a/test/workers/branch/lock-files.spec.js b/test/workers/branch/lock-files.spec.js
index 60d479d1f0..4a7fad10ff 100644
--- a/test/workers/branch/lock-files.spec.js
+++ b/test/workers/branch/lock-files.spec.js
@@ -150,7 +150,7 @@ describe('workers/branch/lock-files', () => {
       expect(res).toMatchSnapshot();
     });
     it('returns root directory if using yarn workspaces', () => {
-      config.hasYarnWorkspaces = true;
+      config.workspaceDir = '.';
       config.upgrades = [{}];
       config.packageFiles = [
         {
@@ -159,6 +159,7 @@ describe('workers/branch/lock-files', () => {
         },
         {
           packageFile: 'backend/package.json',
+          workspaceDir: '.',
         },
       ];
       config.updatedPackageFiles = [
diff --git a/test/workers/repository/__snapshots__/apis.spec.js.snap b/test/workers/repository/__snapshots__/apis.spec.js.snap
index add163b03e..90daea4501 100644
--- a/test/workers/repository/__snapshots__/apis.spec.js.snap
+++ b/test/workers/repository/__snapshots__/apis.spec.js.snap
@@ -14,6 +14,13 @@ Array [
 ]
 `;
 
+exports[`workers/repository/apis checkMonorepos adds nested yarn workspaces 1`] = `
+Array [
+  "@a/b",
+  "@a/c",
+]
+`;
+
 exports[`workers/repository/apis checkMonorepos adds yarn workspaces 1`] = `
 Array [
   "@a/b",
@@ -21,6 +28,8 @@ Array [
 ]
 `;
 
+exports[`workers/repository/apis checkMonorepos skips if no lerna packages 1`] = `Array []`;
+
 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 6b0b2e0b4e..3db1f3c716 100644
--- a/test/workers/repository/apis.spec.js
+++ b/test/workers/repository/apis.spec.js
@@ -57,7 +57,6 @@ describe('workers/repository/apis', () => {
       };
     });
     it('adds yarn workspaces', async () => {
-      config.hasYarnWorkspaces = true;
       config.packageFiles = [
         {
           packageFile: 'package.json',
@@ -75,10 +74,29 @@ describe('workers/repository/apis', () => {
       const res = await apis.checkMonorepos(config);
       expect(res.monorepoPackages).toMatchSnapshot();
     });
+    it('adds nested yarn workspaces', async () => {
+      config.packageFiles = [
+        {
+          packageFile: 'frontend/package.json',
+          content: { workspaces: ['packages/*'] },
+        },
+        {
+          packageFile: 'frontend/packages/something/package.json',
+          content: { name: '@a/b' },
+        },
+        {
+          packageFile: 'frontend/packages/something-else/package.json',
+          content: { name: '@a/c' },
+        },
+      ];
+      const res = await apis.checkMonorepos(config);
+      expect(res.monorepoPackages).toMatchSnapshot();
+    });
     it('adds lerna packages', async () => {
       config.packageFiles = [
         {
           packageFile: 'package.json',
+          content: {},
         },
         {
           packageFile: 'packages/something/package.json',
@@ -93,6 +111,17 @@ describe('workers/repository/apis', () => {
       const res = await apis.checkMonorepos(config);
       expect(res.monorepoPackages).toMatchSnapshot();
     });
+    it('skips if no lerna packages', async () => {
+      config.packageFiles = [
+        {
+          packageFile: 'package.json',
+          content: {},
+        },
+      ];
+      config.api.getFileJson.mockReturnValue({});
+      const res = await apis.checkMonorepos(config);
+      expect(res.monorepoPackages).toMatchSnapshot();
+    });
   });
   describe('detectSemanticCommits', () => {
     it('disables semantic commits', async () => {
diff --git a/test/workers/repository/index.spec.js b/test/workers/repository/index.spec.js
index f6b87e2af8..f653f35bd7 100644
--- a/test/workers/repository/index.spec.js
+++ b/test/workers/repository/index.spec.js
@@ -47,6 +47,7 @@ describe('workers/repository', () => {
       apis.mergeRenovateJson = jest.fn(input => input);
       apis.detectPackageFiles = jest.fn();
       apis.resolvePackageFiles = jest.fn(input => input);
+      apis.checkMonorepos = jest.fn(input => input);
       onboarding.getOnboardingStatus = jest.fn();
       onboarding.ensurePr = jest.fn();
       upgrades.determineRepoUpgrades = jest.fn(() => []);
-- 
GitLab