From ab3372d33c4e785e60ed5bf2195792f1e0924999 Mon Sep 17 00:00:00 2001
From: JYC <jycouet@gmail.com>
Date: Thu, 30 Nov 2017 06:43:56 +0100
Subject: [PATCH] feat: copy local packages (#1244)

If we have in a package.json links to some local lib file:../path/to/folder
Then the local lib package.json will be copied to the tmp folder to be able to generate the right yarn lock file. This is not working with tgz files, only folder reference.

Closes #1215
---
 docs/configuration.md                         |  8 +++
 lib/config/definitions.js                     |  7 ++
 lib/workers/branch/lock-files.js              | 64 +++++++++++++++++++
 .../__snapshots__/resolve.spec.js.snap        |  7 ++
 test/workers/branch/lock-files.spec.js        | 63 +++++++++++++++++-
 .../package/__snapshots__/index.spec.js.snap  |  1 +
 .../__snapshots__/branchify.spec.js.snap      |  5 ++
 7 files changed, 152 insertions(+), 3 deletions(-)

diff --git a/docs/configuration.md b/docs/configuration.md
index 27535462ef..db30434596 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -250,6 +250,14 @@ Obviously, you can't set repository or package file location with this method.
   <td>`RENOVATE_YARNRC`</td>
   <td>`--yarnrc`<td>
 </tr>
+<tr>
+  <td>`copyLocalLibs`</td>
+  <td>enable copy local libraries found in package.json like `"lib1: file:../path/to/folder"`, warning: feature may be deprecated in future.</td>
+  <td>boolean</td>
+  <td><pre>false</pre></td>
+  <td>`RENOVATE_COPY_LOCAL_LIBS`</td>
+  <td>`--copy-local-libs`<td>
+</tr>
 <tr>
   <td>`ignoreNpmrcFile`</td>
   <td>Whether to ignore any .npmrc file found in repository</td>
diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index 60fa204d50..b04802f417 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -153,6 +153,13 @@ const options = [
     stage: 'branch',
     type: 'string',
   },
+  {
+    name: 'copyLocalLibs',
+    description:
+      'enable copy local libraries found in package.json like `"lib1: file:../path/to/folder"`, warning: feature may be deprecated in future.',
+    type: 'boolean',
+    default: false,
+  },
   {
     name: 'ignoreNpmrcFile',
     description: 'Whether to ignore any .npmrc file found in repository',
diff --git a/lib/workers/branch/lock-files.js b/lib/workers/branch/lock-files.js
index 6828ff0cba..bfb1365a1c 100644
--- a/lib/workers/branch/lock-files.js
+++ b/lib/workers/branch/lock-files.js
@@ -123,6 +123,49 @@ async function writeExistingFiles(config) {
         upath.join(basedir, 'package.json'),
         JSON.stringify(massagedFile)
       );
+
+      if (config.copyLocalLibs) {
+        const toCopy = listLocalLibs(massagedFile.dependencies);
+        toCopy.push(...listLocalLibs(massagedFile.devDependencies));
+        if (toCopy.length !== 0) {
+          logger.debug(`listOfNeededLocalFiles files to copy: ${toCopy}`);
+          await Promise.all(
+            toCopy.map(async localPath => {
+              const resolvedLocalPath = upath.join(
+                path.resolve(basedir, localPath)
+              );
+              if (
+                !resolvedLocalPath.startsWith(upath.join(config.tmpDir.path))
+              ) {
+                logger.info(
+                  `local lib '${
+                    toCopy
+                  }' will not be copied because it's out of the repo.`
+                );
+              } else {
+                // at the root of local Lib we should find a package.json so that yarn/npm will use it to update *lock file
+                const resolvedRepoPath = upath.join(
+                  resolvedLocalPath.substring(config.tmpDir.path.length + 1),
+                  'package.json'
+                );
+                const fileContent = await platform.getFile(resolvedRepoPath);
+                if (fileContent !== null) {
+                  await fs.outputFile(
+                    upath.join(resolvedLocalPath, 'package.json'),
+                    fileContent
+                  );
+                } else {
+                  logger.info(
+                    `listOfNeededLocalFiles - file '${
+                      resolvedRepoPath
+                    }' not found.`
+                  );
+                }
+              }
+            })
+          );
+        }
+      }
     }
     if (packageFile.npmrc) {
       logger.debug(`Writing .npmrc to ${basedir}`);
@@ -161,6 +204,27 @@ async function writeExistingFiles(config) {
   }
 }
 
+function listLocalLibs(dependencies) {
+  logger.trace(`listLocalLibs (${dependencies})`);
+  const toCopy = [];
+  if (dependencies) {
+    for (const [libName, libVersion] of Object.entries(dependencies)) {
+      if (libVersion.startsWith('file:')) {
+        if (libVersion.endsWith('.tgz')) {
+          logger.info(
+            `Link to local lib "${libName}": "${
+              libVersion
+            }" is not supported. Please do it like: 'file:/path/to/folder'`
+          );
+        } else {
+          toCopy.push(libVersion.substring('file:'.length));
+        }
+      }
+    }
+  }
+  return toCopy;
+}
+
 async function writeUpdatedPackageFiles(config) {
   logger.trace({ config }, 'writeUpdatedPackageFiles');
   logger.debug('Writing any updated package files');
diff --git a/test/manager/__snapshots__/resolve.spec.js.snap b/test/manager/__snapshots__/resolve.spec.js.snap
index 95e312ac25..2502e7d1b4 100644
--- a/test/manager/__snapshots__/resolve.spec.js.snap
+++ b/test/manager/__snapshots__/resolve.spec.js.snap
@@ -10,6 +10,7 @@ Object {
   "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
   "branchPrefix": "renovate/",
   "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "copyLocalLibs": false,
   "dependencies": Object {
     "semanticCommitType": "fix",
   },
@@ -490,6 +491,7 @@ Object {
   "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
   "branchPrefix": "renovate/",
   "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "copyLocalLibs": false,
   "dependencies": Object {
     "semanticCommitType": "fix",
   },
@@ -976,6 +978,7 @@ Object {
   "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
   "branchPrefix": "renovate/",
   "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "copyLocalLibs": false,
   "dependencies": Object {
     "semanticCommitType": "fix",
   },
@@ -1670,6 +1673,7 @@ Object {
   "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
   "branchPrefix": "renovate/",
   "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "copyLocalLibs": false,
   "dependencies": Object {
     "semanticCommitType": "fix",
   },
@@ -2153,6 +2157,7 @@ Object {
   "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
   "branchPrefix": "renovate/",
   "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "copyLocalLibs": false,
   "dependencies": Object {
     "semanticCommitType": "fix",
   },
@@ -2628,6 +2633,7 @@ Object {
   "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
   "branchPrefix": "renovate/",
   "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "copyLocalLibs": false,
   "dependencies": Object {
     "semanticCommitType": "fix",
   },
@@ -3103,6 +3109,7 @@ Object {
   "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
   "branchPrefix": "renovate/",
   "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "copyLocalLibs": false,
   "dependencies": Object {
     "semanticCommitType": "fix",
   },
diff --git a/test/workers/branch/lock-files.spec.js b/test/workers/branch/lock-files.spec.js
index c48a453c7b..2dba3d0d95 100644
--- a/test/workers/branch/lock-files.spec.js
+++ b/test/workers/branch/lock-files.spec.js
@@ -1,6 +1,7 @@
 const fs = require('fs-extra');
 const lockFiles = require('../../../lib/workers/branch/lock-files');
 const defaultConfig = require('../../../lib/config/defaults').getConfig();
+const upath = require('upath');
 
 const npm = require('../../../lib/workers/branch/npm');
 const yarn = require('../../../lib/workers/branch/yarn');
@@ -208,15 +209,71 @@ describe('workers/branch/lock-files', () => {
       expect(fs.outputFile.mock.calls).toHaveLength(6);
       expect(fs.remove.mock.calls).toHaveLength(4);
     });
-    it('writes lock files', async () => {
+    it('writes package.json of local lib', async () => {
+      const renoPath = upath.join(__dirname, '../../../');
+      config.copyLocalLibs = true;
+      config.tmpDir = { path: renoPath };
       config.packageFiles = [
         {
-          packageFile: 'package.json',
-          content: { name: 'package 1' },
+          packageFile: 'client/package.json',
+          content: {
+            name: 'package 1',
+            dependencies: {
+              test: 'file:../test.tgz',
+              testFolder: 'file:../test',
+            },
+          },
+          yarnLock: 'some yarn lock',
+          packageLock: 'some package lock',
+        },
+      ];
+      platform.getFile.mockReturnValue('some lock file contents');
+      await writeExistingFiles(config);
+      expect(fs.outputFile.mock.calls).toHaveLength(4);
+      expect(fs.remove.mock.calls).toHaveLength(0);
+    });
+    it('Try to write package.json of local lib, but file not found', async () => {
+      const renoPath = upath.join(__dirname, '../../../');
+      config.copyLocalLibs = true;
+      config.tmpDir = { path: renoPath };
+      config.packageFiles = [
+        {
+          packageFile: 'client/package.json',
+          content: {
+            name: 'package 1',
+            dependencies: {
+              test: 'file:../test.tgz',
+              testFolder: 'file:../test',
+            },
+          },
+          yarnLock: 'some yarn lock',
+          packageLock: 'some package lock',
+        },
+      ];
+      platform.getFile.mockReturnValue(null);
+      await writeExistingFiles(config);
+      expect(fs.outputFile.mock.calls).toHaveLength(3);
+      expect(fs.remove.mock.calls).toHaveLength(0);
+    });
+    it('detect malicious intent (error config in package.json) local lib is not in the repo', async () => {
+      const renoPath = upath.join(__dirname, '../../../');
+      config.copyLocalLibs = true;
+      config.tmpDir = { path: renoPath };
+      config.packageFiles = [
+        {
+          packageFile: 'client/package.json',
+          content: {
+            name: 'package 1',
+            dependencies: {
+              test: 'file:../test.tgz',
+              testFolder: 'file:../../../../test',
+            },
+          },
           yarnLock: 'some yarn lock',
           packageLock: 'some package lock',
         },
       ];
+      platform.getFile.mockReturnValue(null);
       await writeExistingFiles(config);
       expect(fs.outputFile.mock.calls).toHaveLength(3);
       expect(fs.remove.mock.calls).toHaveLength(0);
diff --git a/test/workers/package/__snapshots__/index.spec.js.snap b/test/workers/package/__snapshots__/index.spec.js.snap
index f4505c9f4b..af1a7d707d 100644
--- a/test/workers/package/__snapshots__/index.spec.js.snap
+++ b/test/workers/package/__snapshots__/index.spec.js.snap
@@ -8,6 +8,7 @@ Object {
   "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
   "branchPrefix": "renovate/",
   "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "copyLocalLibs": false,
   "currentVersion": "1.0.0",
   "depName": "foo",
   "description": Array [],
diff --git a/test/workers/repository/updates/__snapshots__/branchify.spec.js.snap b/test/workers/repository/updates/__snapshots__/branchify.spec.js.snap
index e71bdcaa3b..078787f322 100644
--- a/test/workers/repository/updates/__snapshots__/branchify.spec.js.snap
+++ b/test/workers/repository/updates/__snapshots__/branchify.spec.js.snap
@@ -55,6 +55,7 @@ Object {
     },
   ],
   "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "copyLocalLibs": false,
   "dependencies": Object {
     "semanticCommitType": "fix",
   },
@@ -567,6 +568,7 @@ Object {
     },
   ],
   "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "copyLocalLibs": false,
   "dependencies": Object {
     "semanticCommitType": "fix",
   },
@@ -1085,6 +1087,7 @@ Object {
     },
   ],
   "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "copyLocalLibs": false,
   "dependencies": Object {
     "semanticCommitType": "fix",
   },
@@ -1587,6 +1590,7 @@ Object {
     },
   ],
   "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "copyLocalLibs": false,
   "dependencies": Object {
     "semanticCommitType": "fix",
   },
@@ -2092,6 +2096,7 @@ Object {
     },
   ],
   "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "copyLocalLibs": false,
   "dependencies": Object {
     "semanticCommitType": "fix",
   },
-- 
GitLab