From d765b34c33f246d319e88fc17fadba27c3480cf4 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@keylocation.sg>
Date: Tue, 12 Sep 2017 09:33:41 +0200
Subject: [PATCH] feat: renovate meteor package.js dependencies (#786)

This feature adds support for renovating Meteor's `package.js` files. Meteor config is disabled by default so must be manually enabled to work. If enabled, Renovate uses GitHub's search API to look for any files named `package.js` that include the text `Npm.depends`. If so then the file is parsed using Regex to extract its dependencies and check them for updates.

Closes #785
---
 docs/configuration.md                         |  27 +++
 lib/api/github.js                             |   8 +-
 lib/config/definitions.js                     |   9 +
 lib/workers/branch/lock-files.js              |  13 +-
 lib/workers/branch/package-files.js           |  26 ++-
 lib/workers/branch/package-js.js              |  21 ++
 lib/workers/dep-type/index.js                 |  40 ++--
 lib/workers/package-file/index.js             |  23 ++
 lib/workers/pr/index.js                       |  20 +-
 lib/workers/repository/apis.js                | 204 ++++++++++--------
 lib/workers/repository/upgrades.js            |  14 +-
 test/_fixtures/meteor/package-1.js            |  26 +++
 test/_fixtures/meteor/package-2.js            |  26 +++
 test/api/__snapshots__/github.spec.js.snap    |   2 +-
 test/api/github.spec.js                       |   2 +-
 .../__snapshots__/package-js.spec.js.snap     |  61 ++++++
 test/workers/branch/package-files.spec.js     |  10 +-
 test/workers/branch/package-js.spec.js        |  49 +++++
 test/workers/dep-type/index.spec.js           |  12 ++
 test/workers/package-file/index.spec.js       |  25 +++
 .../__snapshots__/apis.spec.js.snap           |  10 +
 test/workers/repository/apis.spec.js          |  28 ++-
 test/workers/repository/upgrades.spec.js      |   5 +-
 23 files changed, 521 insertions(+), 140 deletions(-)
 create mode 100644 lib/workers/branch/package-js.js
 create mode 100644 test/_fixtures/meteor/package-1.js
 create mode 100644 test/_fixtures/meteor/package-2.js
 create mode 100644 test/workers/branch/__snapshots__/package-js.spec.js.snap
 create mode 100644 test/workers/branch/package-js.spec.js

diff --git a/docs/configuration.md b/docs/configuration.md
index 1b00948f7c..7493a9020b 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -74,6 +74,8 @@ $ node renovate --help
     --log-file <string>                  Log file path
     --log-file-level <string>            Log file log level
     --onboarding [boolean]               Require a Configuration PR first
+    --private-key <string>               Server-side private key
+    --encrypted <json>                   A configuration object containing configuration encrypted with project key
     --timezone <string>                  [IANA Time Zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
     --onboarding [boolean]               Require a Configuration PR first
     --platform <string>                  Platform type of repository
@@ -109,6 +111,7 @@ $ node renovate --help
     --labels <list>                      Labels to add to Pull Request
     --assignees <list>                   Assignees for Pull Request
     --reviewers <list>                   Requested reviewers for Pull Requests (GitHub only)
+    --meteor <json>                      Configuration object for meteor package.js renovation
     -h, --help                           output usage information
   Examples:
 
@@ -205,6 +208,22 @@ Obviously, you can't set repository or package file location with this method.
   <td>`RENOVATE_ONBOARDING`</td>
   <td>`--onboarding`<td>
 </tr>
+<tr>
+  <td>`privateKey`</td>
+  <td>Server-side private key</td>
+  <td>string</td>
+  <td><pre>null</pre></td>
+  <td>`RENOVATE_PRIVATE_KEY`</td>
+  <td>`--private-key`<td>
+</tr>
+<tr>
+  <td>`encrypted`</td>
+  <td>A configuration object containing configuration encrypted with project key</td>
+  <td>json</td>
+  <td><pre>null</pre></td>
+  <td>`RENOVATE_ENCRYPTED`</td>
+  <td>`--encrypted`<td>
+</tr>
 <tr>
   <td>`timezone`</td>
   <td>[IANA Time Zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)</td>
@@ -685,3 +704,11 @@ Obviously, you can't set repository or package file location with this method.
   <td>`RENOVATE_REVIEWERS`</td>
   <td>`--reviewers`<td>
 </tr>
+<tr>
+  <td>`meteor`</td>
+  <td>Configuration object for meteor package.js renovation</td>
+  <td>json</td>
+  <td><pre>{"enabled": true}</pre></td>
+  <td>`RENOVATE_METEOR`</td>
+  <td>`--meteor`<td>
+</tr>
diff --git a/lib/api/github.js b/lib/api/github.js
index 92cd2ce1a6..95f9626b6e 100644
--- a/lib/api/github.js
+++ b/lib/api/github.js
@@ -227,9 +227,13 @@ async function setBaseBranch(branchName) {
 // Search
 
 // Returns an array of file paths in current repo matching the fileName
-async function findFilePaths(fileName) {
+async function findFilePaths(fileName, content) {
   let results = [];
-  let url = `search/code?q=repo:${config.repoName}+filename:${fileName}&per_page=100`;
+  let url = `search/code?q=`;
+  if (content) {
+    url += `${content}+`;
+  }
+  url += `repo:${config.repoName}+filename:${fileName}&per_page=100`;
   do {
     const res = await ghGotRetry(url);
     const exactMatches = res.body.items.filter(item => item.name === fileName);
diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index 4f7193ef5b..bd0b8a4689 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -540,6 +540,15 @@ const options = [
     description: 'Requested reviewers for Pull Requests (GitHub only)',
     type: 'list',
   },
+  // meteor
+  {
+    name: 'meteor',
+    description: 'Configuration object for meteor package.js renovation',
+    stage: 'repository',
+    type: 'json',
+    default: { enabled: false },
+    mergeable: true,
+  },
 ];
 
 function getOptions() {
diff --git a/lib/workers/branch/lock-files.js b/lib/workers/branch/lock-files.js
index b8a703a3e7..df205ed36a 100644
--- a/lib/workers/branch/lock-files.js
+++ b/lib/workers/branch/lock-files.js
@@ -96,11 +96,13 @@ async function writeExistingFiles(config) {
       config.tmpDir.name,
       path.dirname(packageFile.packageFile)
     );
-    logger.debug(`Writing package.json to ${basedir}`);
-    await fs.outputFile(
-      path.join(basedir, 'package.json'),
-      JSON.stringify(packageFile.content)
-    );
+    if (packageFile.packageFile.endsWith('package.json')) {
+      logger.debug(`Writing package.json to ${basedir}`);
+      await fs.outputFile(
+        path.join(basedir, 'package.json'),
+        JSON.stringify(packageFile.content)
+      );
+    }
     if (packageFile.npmrc) {
       logger.debug(`Writing .npmrc to ${basedir}`);
       await fs.outputFile(path.join(basedir, '.npmrc'), packageFile.npmrc);
@@ -112,7 +114,6 @@ async function writeExistingFiles(config) {
         packageFile.yarnrc.replace('--install.pure-lockfile true', '')
       );
     }
-    logger.debug('Removing any previous lock files');
     await fs.remove(path.join(basedir, 'yarn.lock'));
     await fs.remove(path.join(basedir, 'package-lock.json'));
   }
diff --git a/lib/workers/branch/package-files.js b/lib/workers/branch/package-files.js
index 8567f248a9..cde5433b43 100644
--- a/lib/workers/branch/package-files.js
+++ b/lib/workers/branch/package-files.js
@@ -1,4 +1,5 @@
 const packageJsonHelper = require('./package-json');
+const packageJsHelper = require('./package-js');
 
 module.exports = {
   getUpdatedPackageFiles,
@@ -15,13 +16,24 @@ async function getUpdatedPackageFiles(config) {
           upgrade.packageFile,
           config.parentBranch
         ));
-      const newContent = packageJsonHelper.setNewValue(
-        existingContent,
-        upgrade.depType,
-        upgrade.depName,
-        upgrade.newVersion,
-        config.logger
-      );
+      let newContent;
+      if (upgrade.packageFile.endsWith('package.json')) {
+        newContent = packageJsonHelper.setNewValue(
+          existingContent,
+          upgrade.depType,
+          upgrade.depName,
+          upgrade.newVersion,
+          config.logger
+        );
+      } else {
+        newContent = packageJsHelper.setNewValue(
+          existingContent,
+          upgrade.depName,
+          upgrade.currentVersion,
+          upgrade.newVersion,
+          config.logger
+        );
+      }
       if (newContent !== existingContent) {
         config.logger.debug('Updating packageFile content');
         updatedPackageFiles[upgrade.packageFile] = newContent;
diff --git a/lib/workers/branch/package-js.js b/lib/workers/branch/package-js.js
new file mode 100644
index 0000000000..f6986ec45d
--- /dev/null
+++ b/lib/workers/branch/package-js.js
@@ -0,0 +1,21 @@
+module.exports = {
+  setNewValue,
+};
+
+function setNewValue(
+  currentFileContent,
+  depName,
+  currentVersion,
+  newVersion,
+  logger
+) {
+  logger.debug(`setNewValue: ${depName} = ${newVersion}`);
+  const regexReplace = new RegExp(
+    `('|")(${depName})('|"):(\\s+)('|")${currentVersion}('|")`
+  );
+  const newFileContent = currentFileContent.replace(
+    regexReplace,
+    `$1$2$3:$4$5${newVersion}$6`
+  );
+  return newFileContent;
+}
diff --git a/lib/workers/dep-type/index.js b/lib/workers/dep-type/index.js
index d19d0f600f..a7ff97aef2 100644
--- a/lib/workers/dep-type/index.js
+++ b/lib/workers/dep-type/index.js
@@ -15,28 +15,36 @@ async function renovateDepType(packageContent, config) {
     logger.debug('depType is disabled');
     return [];
   }
-  // Extract all dependencies from the package.json
-  const currentDeps = await packageJson.extractDependencies(
-    packageContent,
-    config.depType
-  );
-  if (currentDeps.length === 0) {
-    return [];
+  let deps;
+  if (config.packageFile.endsWith('package.json')) {
+    // Extract all dependencies from the package.json
+    deps = await packageJson.extractDependencies(
+      packageContent,
+      config.depType
+    );
+    logger.debug(`currentDeps length is ${deps.length}`);
+    logger.debug({ deps }, `currentDeps`);
+  } else if (config.packageFile.endsWith('package.js')) {
+    deps = packageContent
+      .match(/Npm\.depends\({([\s\S]*?)}\);/)[1]
+      .replace(/(\s|\\n|\\t|'|")/g, '')
+      .split(',')
+      .map(dep => dep.split(/:(.*)/))
+      .map(arr => ({
+        depType: 'npmDepends',
+        depName: arr[0],
+        currentVersion: arr[1],
+      }));
   }
-  logger.debug(`currentDeps length is ${currentDeps.length}`);
-  logger.debug({ currentDeps }, `currentDeps`);
-  // Filter out ignored dependencies
-  const filteredDeps = currentDeps.filter(
+  deps = deps.filter(
     dependency =>
       config.ignoreDeps.indexOf(dependency.depName) === -1 &&
       config.monorepoPackages.indexOf(dependency.depName) === -1
   );
-  logger.debug(`filteredDeps length is ${filteredDeps.length}`);
-  logger.debug({ filteredDeps }, `filteredDeps`);
+  logger.debug(`filtered deps length is ${deps.length}`);
+  logger.debug({ deps }, `filtered deps`);
   // Obtain full config for each dependency
-  const depConfigs = filteredDeps.map(dep =>
-    module.exports.getDepConfig(config, dep)
-  );
+  const depConfigs = deps.map(dep => module.exports.getDepConfig(config, dep));
   logger.trace({ config: depConfigs }, `depConfigs`);
   // renovateDepType can return more than one upgrade each
   const pkgWorkers = depConfigs.map(depConfig =>
diff --git a/lib/workers/package-file/index.js b/lib/workers/package-file/index.js
index 2eeecaaf13..50fe84629b 100644
--- a/lib/workers/package-file/index.js
+++ b/lib/workers/package-file/index.js
@@ -1,11 +1,13 @@
 const configParser = require('../../config');
 const depTypeWorker = require('../dep-type');
+const packageWorker = require('../package');
 const npmApi = require('../../api/npm');
 
 let logger = require('../../logger');
 
 module.exports = {
   renovatePackageFile,
+  renovateMeteorPackageFile,
 };
 
 async function renovatePackageFile(packageFileConfig) {
@@ -69,3 +71,24 @@ async function renovatePackageFile(packageFileConfig) {
   logger.info('Finished processing package file');
   return upgrades;
 }
+
+async function renovateMeteorPackageFile(packageFileConfig) {
+  const config = { ...packageFileConfig };
+  let upgrades = [];
+  logger = config.logger;
+  logger.info(`Processing meteor package file`);
+
+  // Check if config is disabled
+  if (config.enabled === false) {
+    logger.info('packageFile is disabled');
+    return upgrades;
+  }
+  const content = await packageFileConfig.api.getFileContent(
+    packageFileConfig.packageFile
+  );
+  upgrades = upgrades.concat(
+    await depTypeWorker.renovateDepType(content, packageFileConfig)
+  );
+  logger.info('Finished processing package file');
+  return upgrades;
+}
diff --git a/lib/workers/pr/index.js b/lib/workers/pr/index.js
index 2a09224292..5ab329ea62 100644
--- a/lib/workers/pr/index.js
+++ b/lib/workers/pr/index.js
@@ -127,14 +127,18 @@ async function ensurePr(prConfig) {
   }
 
   const prTitle = handlebars.compile(config.prTitle)(config);
-  let prBodyMarkdown = handlebars.compile(config.prBody)(config);
-  const atUserRe = /[^`]@([a-z]+\/[a-z]+)/g;
-  prBodyMarkdown = prBodyMarkdown.replace(atUserRe, '@&#8203;$1');
-  let prBody = converter.makeHtml(prBodyMarkdown);
-  // Public GitHub repos need links prevented - see #489
-  prBody = prBody.replace(issueRe, '$1#&#8203;$2$3');
-  const backTickRe = /&#x60;([^/]*?)&#x60;/g;
-  prBody = prBody.replace(backTickRe, '<code>$1</code>');
+  let prBody;
+  do {
+    let prBodyMarkdown = handlebars.compile(config.prBody)(config);
+    const atUserRe = /[^`]@([a-z]+\/[a-z]+)/g;
+    prBodyMarkdown = prBodyMarkdown.replace(atUserRe, '@&#8203;$1');
+    prBody = converter.makeHtml(prBodyMarkdown);
+    // Public GitHub repos need links prevented - see #489
+    prBody = prBody.replace(issueRe, '$1#&#8203;$2$3');
+    const backTickRe = /&#x60;([^/]*?)&#x60;/g;
+    prBody = prBody.replace(backTickRe, '<code>$1</code>');
+    config.upgrades.pop();
+  } while (prBody.length > 250000);
 
   try {
     // Check if existing PR exists
diff --git a/lib/workers/repository/apis.js b/lib/workers/repository/apis.js
index f18eddbdd6..90756b09ff 100644
--- a/lib/workers/repository/apis.js
+++ b/lib/workers/repository/apis.js
@@ -272,6 +272,14 @@ async function detectPackageFiles(input) {
       config.packageFiles = ['package.json'];
     }
   }
+  if (config.meteor.enabled) {
+    const meteorPackageFiles = await config.api.findFilePaths(
+      'package.js',
+      'Npm.depends'
+    );
+    logger.info(`Found ${meteorPackageFiles.length} meteor package files`);
+    config.packageFiles = config.packageFiles.concat(meteorPackageFiles);
+  }
   return config;
 }
 
@@ -282,106 +290,118 @@ async function resolvePackageFiles(inputConfig) {
   for (let packageFile of config.packageFiles) {
     packageFile =
       typeof packageFile === 'string' ? { packageFile } : packageFile;
-    config.logger.debug(`Resolving packageFile ${JSON.stringify(packageFile)}`);
-    packageFile.content = await config.api.getFileJson(
-      packageFile.packageFile,
-      config.baseBranch
-    );
-    packageFile.npmrc = await config.api.getFileContent(
-      path.join(path.dirname(packageFile.packageFile), '.npmrc'),
-      config.baseBranch
-    );
-    if (!packageFile.npmrc) {
-      delete packageFile.npmrc;
-    }
-    packageFile.yarnrc = await config.api.getFileContent(
-      path.join(path.dirname(packageFile.packageFile), '.yarnrc'),
-      config.baseBranch
-    );
-    if (!packageFile.yarnrc) {
-      delete packageFile.yarnrc;
-    }
-    if (packageFile.content) {
-      // check for workspaces
-      if (
-        packageFile.packageFile === 'package.json' &&
-        packageFile.content.workspaces
-      ) {
-        config.logger.info('Found yarn workspaces configuration');
-        config.hasYarnWorkspaces = true;
-      }
-      // hoist renovate config if exists
-      if (packageFile.content.renovate) {
-        config.hasPackageJsonRenovateConfig = true;
-        config.logger.debug(
-          {
-            packageFile: packageFile.packageFile,
-            config: packageFile.content.renovate,
-          },
-          `Found package.json renovate config`
-        );
-        const migratedConfig = migrateAndValidate(
-          config,
-          packageFile.content.renovate
-        );
-        config.logger.debug(
-          { config: migratedConfig },
-          'package.json migrated config'
-        );
-        const resolvedConfig = await presets.resolveConfigPresets(
-          migratedConfig,
-          config.logger
-        );
-        config.logger.debug(
-          { config: resolvedConfig },
-          'package.json resolved config'
-        );
-        Object.assign(packageFile, resolvedConfig);
-        delete packageFile.content.renovate;
-      } else {
-        config.logger.debug(
-          { packageFile: packageFile.packageFile },
-          `No renovate config`
-        );
+    if (packageFile.packageFile.endsWith('package.json')) {
+      config.logger.debug(
+        `Resolving packageFile ${JSON.stringify(packageFile)}`
+      );
+      packageFile.content = await config.api.getFileJson(
+        packageFile.packageFile,
+        config.baseBranch
+      );
+      packageFile.npmrc = await config.api.getFileContent(
+        path.join(path.dirname(packageFile.packageFile), '.npmrc'),
+        config.baseBranch
+      );
+      if (!packageFile.npmrc) {
+        delete packageFile.npmrc;
       }
-      // Detect if lock files are used
-      const yarnLockFileName = path.join(
-        path.dirname(packageFile.packageFile),
-        'yarn.lock'
+      packageFile.yarnrc = await config.api.getFileContent(
+        path.join(path.dirname(packageFile.packageFile), '.yarnrc'),
+        config.baseBranch
       );
-      if (
-        await config.api.getFileContent(yarnLockFileName, config.baseBranch)
-      ) {
-        config.logger.debug(
-          { packageFile: packageFile.packageFile },
-          'Found yarn.lock'
+      if (!packageFile.yarnrc) {
+        delete packageFile.yarnrc;
+      }
+      if (packageFile.content) {
+        // check for workspaces
+        if (
+          packageFile.packageFile === 'package.json' &&
+          packageFile.content.workspaces
+        ) {
+          config.logger.info('Found yarn workspaces configuration');
+          config.hasYarnWorkspaces = true;
+        }
+        // hoist renovate config if exists
+        if (packageFile.content.renovate) {
+          config.hasPackageJsonRenovateConfig = true;
+          config.logger.debug(
+            {
+              packageFile: packageFile.packageFile,
+              config: packageFile.content.renovate,
+            },
+            `Found package.json renovate config`
+          );
+          const migratedConfig = migrateAndValidate(
+            config,
+            packageFile.content.renovate
+          );
+          config.logger.debug(
+            { config: migratedConfig },
+            'package.json migrated config'
+          );
+          const resolvedConfig = await presets.resolveConfigPresets(
+            migratedConfig,
+            config.logger
+          );
+          config.logger.debug(
+            { config: resolvedConfig },
+            'package.json resolved config'
+          );
+          Object.assign(packageFile, resolvedConfig);
+          delete packageFile.content.renovate;
+        } else {
+          config.logger.debug(
+            { packageFile: packageFile.packageFile },
+            `No renovate config`
+          );
+        }
+        // Detect if lock files are used
+        const yarnLockFileName = path.join(
+          path.dirname(packageFile.packageFile),
+          'yarn.lock'
+        );
+        if (
+          await config.api.getFileContent(yarnLockFileName, config.baseBranch)
+        ) {
+          config.logger.debug(
+            { packageFile: packageFile.packageFile },
+            'Found yarn.lock'
+          );
+          packageFile.hasYarnLock = true;
+        } else {
+          packageFile.hasYarnLock = false;
+        }
+        const packageLockFileName = path.join(
+          path.dirname(packageFile.packageFile),
+          'package-lock.json'
         );
-        packageFile.hasYarnLock = true;
+        if (
+          await config.api.getFileContent(
+            packageLockFileName,
+            config.baseBranch
+          )
+        ) {
+          config.logger.debug(
+            { packageFile: packageFile.packageFile },
+            'Found package-lock.json'
+          );
+          packageFile.hasPackageLock = true;
+        } else {
+          packageFile.hasPackageLock = false;
+        }
       } else {
-        packageFile.hasYarnLock = false;
-      }
-      const packageLockFileName = path.join(
-        path.dirname(packageFile.packageFile),
-        'package-lock.json'
-      );
-      if (
-        await config.api.getFileContent(packageLockFileName, config.baseBranch)
-      ) {
-        config.logger.debug(
+        config.logger.warn(
           { packageFile: packageFile.packageFile },
-          'Found package-lock.json'
+          'package file not found'
         );
-        packageFile.hasPackageLock = true;
-      } else {
-        packageFile.hasPackageLock = false;
+        continue; // eslint-disable-line
       }
-      packageFiles.push(packageFile);
-    } else {
-      config.logger.warn(
-        { packageFile: packageFile.packageFile },
-        'package file not found'
-      );
+    } else if (packageFile.packageFile.endsWith('package.js')) {
+      // meteor
+      packageFile = configParser.mergeChildConfig(config.meteor, packageFile);
     }
+
+    packageFiles.push(packageFile);
   }
   config.packageFiles = packageFiles;
   return config;
diff --git a/lib/workers/repository/upgrades.js b/lib/workers/repository/upgrades.js
index 64d6a0a7be..aa7c58dc90 100644
--- a/lib/workers/repository/upgrades.js
+++ b/lib/workers/repository/upgrades.js
@@ -24,9 +24,17 @@ async function determineRepoUpgrades(config) {
       config,
       index
     );
-    upgrades = upgrades.concat(
-      await packageFileWorker.renovatePackageFile(packageFileConfig)
-    );
+    if (packageFileConfig.packageFile.endsWith('package.json')) {
+      logger.info('Renovating package.json dependencies');
+      upgrades = upgrades.concat(
+        await packageFileWorker.renovatePackageFile(packageFileConfig)
+      );
+    } else if (packageFileConfig.packageFile.endsWith('package.js')) {
+      logger.info('Renovating package.js (meteor) dependencies');
+      upgrades = upgrades.concat(
+        await packageFileWorker.renovateMeteorPackageFile(packageFileConfig)
+      );
+    }
   }
   return upgrades;
 }
diff --git a/test/_fixtures/meteor/package-1.js b/test/_fixtures/meteor/package-1.js
new file mode 100644
index 0000000000..97066f419b
--- /dev/null
+++ b/test/_fixtures/meteor/package-1.js
@@ -0,0 +1,26 @@
+Package.describe({
+	'name': 'steffo:meteor-accounts-saml',
+	'summary': 'SAML Login (SP) for Meteor. Works with OpenAM, OpenIDP and provides Single Logout.',
+	'version': '0.0.1',
+	'git': 'https://github.com/steffow/meteor-accounts-saml.git'
+});
+
+Package.on_use(function(api) {
+	api.use('rocketchat:lib', 'server');
+	api.use('ecmascript');
+	api.use(['routepolicy', 'webapp', 'underscore', 'service-configuration'], 'server');
+	api.use(['http', 'accounts-base'], ['client', 'server']);
+
+	api.add_files(['saml_server.js', 'saml_utils.js'], 'server');
+	api.add_files(['saml_rocketchat.js'], 'server');
+	api.add_files('saml_client.js', 'client');
+});
+
+Npm.depends({
+	'xml2js': '0.2.0',
+	'xml-crypto': '0.6.0',
+	'xmldom': '0.1.19',
+	'connect': '2.7.10',
+	'xmlbuilder': '2.6.4',
+	'querystring': '0.2.0'
+});
diff --git a/test/_fixtures/meteor/package-2.js b/test/_fixtures/meteor/package-2.js
new file mode 100644
index 0000000000..182fa7c9a0
--- /dev/null
+++ b/test/_fixtures/meteor/package-2.js
@@ -0,0 +1,26 @@
+Package.describe({
+    "name": "steffo:meteor-accounts-saml",
+    "summary": "SAML Login (SP) for Meteor. Works with OpenAM, OpenIDP and provides Single Logout.",
+    "version": "0.0.1",
+    "git": "https://github.com/steffow/meteor-accounts-saml.git"
+});
+
+Package.on_use(function(api) {
+    api.use("rocketchat:lib", "server");
+    api.use("ecmascript");
+    api.use(["routepolicy", "webapp", "underscore", "service-configuration"], "server");
+    api.use(["http", "accounts-base"], ["client", "server"]);
+
+    api.add_files(["saml_server.js", "saml_utils.js"], "server");
+    api.add_files(["saml_rocketchat.js"], "server");
+    api.add_files("saml_client.js", "client");
+});
+
+Npm.depends({
+    "xml2js": "0.2.0",
+    "xml-crypto": "0.6.0",
+    "xmldom": "0.1.19",
+    "connect": "2.7.10",
+    "xmlbuilder": "2.6.4",
+    "querystring": "0.2.0"
+});
diff --git a/test/api/__snapshots__/github.spec.js.snap b/test/api/__snapshots__/github.spec.js.snap
index 20f10ceb34..81e25b43c5 100644
--- a/test/api/__snapshots__/github.spec.js.snap
+++ b/test/api/__snapshots__/github.spec.js.snap
@@ -542,7 +542,7 @@ Array [
     },
   ],
   Array [
-    "search/code?q=repo:some/repo+filename:package.json&per_page=100",
+    "search/code?q=some-content+repo:some/repo+filename:package.json&per_page=100",
     undefined,
   ],
 ]
diff --git a/test/api/github.spec.js b/test/api/github.spec.js
index d70a024eea..726e584d91 100644
--- a/test/api/github.spec.js
+++ b/test/api/github.spec.js
@@ -612,7 +612,7 @@ describe('api/github', () => {
           ],
         },
       }));
-      const files = await github.findFilePaths('package.json');
+      const files = await github.findFilePaths('package.json', 'some-content');
       expect(ghGot.mock.calls).toMatchSnapshot();
       expect(files).toMatchSnapshot();
     });
diff --git a/test/workers/branch/__snapshots__/package-js.spec.js.snap b/test/workers/branch/__snapshots__/package-js.spec.js.snap
new file mode 100644
index 0000000000..cb7db04c12
--- /dev/null
+++ b/test/workers/branch/__snapshots__/package-js.spec.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/branch/package-js .setNewValue(currentFileContent, depName, currentVersion, newVersion, logger) handles alternative quotes and white space 1`] = `
+"Package.describe({
+    \\"name\\": \\"steffo:meteor-accounts-saml\\",
+    \\"summary\\": \\"SAML Login (SP) for Meteor. Works with OpenAM, OpenIDP and provides Single Logout.\\",
+    \\"version\\": \\"0.0.1\\",
+    \\"git\\": \\"https://github.com/steffow/meteor-accounts-saml.git\\"
+});
+
+Package.on_use(function(api) {
+    api.use(\\"rocketchat:lib\\", \\"server\\");
+    api.use(\\"ecmascript\\");
+    api.use([\\"routepolicy\\", \\"webapp\\", \\"underscore\\", \\"service-configuration\\"], \\"server\\");
+    api.use([\\"http\\", \\"accounts-base\\"], [\\"client\\", \\"server\\"]);
+
+    api.add_files([\\"saml_server.js\\", \\"saml_utils.js\\"], \\"server\\");
+    api.add_files([\\"saml_rocketchat.js\\"], \\"server\\");
+    api.add_files(\\"saml_client.js\\", \\"client\\");
+});
+
+Npm.depends({
+    \\"xml2js\\": \\"0.2.0\\",
+    \\"xml-crypto\\": \\"0.6.0\\",
+    \\"xmldom\\": \\"0.22.1\\",
+    \\"connect\\": \\"2.7.10\\",
+    \\"xmlbuilder\\": \\"2.6.4\\",
+    \\"querystring\\": \\"0.2.0\\"
+});
+"
+`;
+
+exports[`workers/branch/package-js .setNewValue(currentFileContent, depName, currentVersion, newVersion, logger) replaces a dependency value 1`] = `
+"Package.describe({
+	'name': 'steffo:meteor-accounts-saml',
+	'summary': 'SAML Login (SP) for Meteor. Works with OpenAM, OpenIDP and provides Single Logout.',
+	'version': '0.0.1',
+	'git': 'https://github.com/steffow/meteor-accounts-saml.git'
+});
+
+Package.on_use(function(api) {
+	api.use('rocketchat:lib', 'server');
+	api.use('ecmascript');
+	api.use(['routepolicy', 'webapp', 'underscore', 'service-configuration'], 'server');
+	api.use(['http', 'accounts-base'], ['client', 'server']);
+
+	api.add_files(['saml_server.js', 'saml_utils.js'], 'server');
+	api.add_files(['saml_rocketchat.js'], 'server');
+	api.add_files('saml_client.js', 'client');
+});
+
+Npm.depends({
+	'xml2js': '0.2.0',
+	'xml-crypto': '0.6.0',
+	'xmldom': '0.22.1',
+	'connect': '2.7.10',
+	'xmlbuilder': '2.6.4',
+	'querystring': '0.2.0'
+});
+"
+`;
diff --git a/test/workers/branch/package-files.spec.js b/test/workers/branch/package-files.spec.js
index 520145377a..60488b8aeb 100644
--- a/test/workers/branch/package-files.spec.js
+++ b/test/workers/branch/package-files.spec.js
@@ -1,4 +1,5 @@
 const packageJsonHelper = require('../../../lib/workers/branch/package-json');
+const packageJsHelper = require('../../../lib/workers/branch/package-js');
 const {
   getUpdatedPackageFiles,
 } = require('../../../lib/workers/branch/package-files');
@@ -15,6 +16,7 @@ describe('workers/branch/package-files', () => {
         logger,
       };
       packageJsonHelper.setNewValue = jest.fn();
+      packageJsHelper.setNewValue = jest.fn();
     });
     it('returns empty if lock file maintenance', async () => {
       config.upgrades = [{ type: 'lockFileMaintenance' }];
@@ -22,11 +24,17 @@ describe('workers/branch/package-files', () => {
       expect(res).toHaveLength(0);
     });
     it('returns updated files', async () => {
-      config.upgrades = [{}, {}];
+      config.upgrades = [
+        { packageFile: 'package.json' },
+        { packageFile: 'backend/package.json' },
+        { packageFile: 'packages/foo/package.js' },
+      ];
       config.api.getFileContent.mockReturnValueOnce('old content 1');
       config.api.getFileContent.mockReturnValueOnce('old content 2');
+      config.api.getFileContent.mockReturnValueOnce('old content 3');
       packageJsonHelper.setNewValue.mockReturnValueOnce('old content 1');
       packageJsonHelper.setNewValue.mockReturnValueOnce('new content 2');
+      packageJsHelper.setNewValue.mockReturnValueOnce('old content 3');
       const res = await getUpdatedPackageFiles(config);
       expect(res).toHaveLength(1);
     });
diff --git a/test/workers/branch/package-js.spec.js b/test/workers/branch/package-js.spec.js
new file mode 100644
index 0000000000..3dc3ddf421
--- /dev/null
+++ b/test/workers/branch/package-js.spec.js
@@ -0,0 +1,49 @@
+const fs = require('fs');
+const path = require('path');
+const packageJs = require('../../../lib/workers/branch/package-js');
+const logger = require('../../_fixtures/logger');
+
+function readFixture(fixture) {
+  return fs.readFileSync(
+    path.resolve(__dirname, `../../_fixtures/meteor/${fixture}`),
+    'utf8'
+  );
+}
+
+const input01Content = readFixture('package-1.js');
+const input02Content = readFixture('package-2.js');
+
+describe('workers/branch/package-js', () => {
+  describe('.setNewValue(currentFileContent, depName, currentVersion, newVersion, logger)', () => {
+    it('replaces a dependency value', () => {
+      const testContent = packageJs.setNewValue(
+        input01Content,
+        'xmldom',
+        '0.1.19',
+        '0.22.1',
+        logger
+      );
+      expect(testContent).toMatchSnapshot();
+    });
+    it('handles alternative quotes and white space', () => {
+      const testContent = packageJs.setNewValue(
+        input02Content,
+        'xmldom',
+        '0.1.19',
+        '0.22.1',
+        logger
+      );
+      expect(testContent).toMatchSnapshot();
+    });
+    it('handles the case where the desired version is already supported', () => {
+      const testContent = packageJs.setNewValue(
+        input01Content,
+        'query-string',
+        '0.2.0',
+        '0.2.0',
+        logger
+      );
+      testContent.should.equal(input01Content);
+    });
+  });
+});
diff --git a/test/workers/dep-type/index.spec.js b/test/workers/dep-type/index.spec.js
index 65860a0271..025e19c862 100644
--- a/test/workers/dep-type/index.spec.js
+++ b/test/workers/dep-type/index.spec.js
@@ -1,3 +1,5 @@
+const path = require('path');
+const fs = require('fs');
 const packageJson = require('../../../lib/workers/dep-type/package-json');
 const pkgWorker = require('../../../lib/workers/package/index');
 const depTypeWorker = require('../../../lib/workers/dep-type/index');
@@ -14,6 +16,7 @@ describe('lib/workers/dep-type/index', () => {
     let config;
     beforeEach(() => {
       config = {
+        packageFile: 'package.json',
         ignoreDeps: ['a', 'b'],
         monorepoPackages: ['e'],
       };
@@ -46,6 +49,15 @@ describe('lib/workers/dep-type/index', () => {
       const res = await depTypeWorker.renovateDepType({}, config);
       expect(res).toHaveLength(2);
     });
+    it('returns upgrades for meteor', async () => {
+      config.packageFile = 'package.js';
+      const content = fs.readFileSync(
+        path.resolve('test/_fixtures/meteor/package-1.js'),
+        'utf8'
+      );
+      const res = await depTypeWorker.renovateDepType(content, config);
+      expect(res).toHaveLength(6);
+    });
   });
   describe('getDepConfig(depTypeConfig, dep)', () => {
     const depTypeConfig = {
diff --git a/test/workers/package-file/index.spec.js b/test/workers/package-file/index.spec.js
index 453f1cd484..23de2716c0 100644
--- a/test/workers/package-file/index.spec.js
+++ b/test/workers/package-file/index.spec.js
@@ -40,4 +40,29 @@ describe('packageFileWorker', () => {
       expect(res).toHaveLength(1);
     });
   });
+  describe('renovateMeteorPackageFile(config)', () => {
+    let config;
+    beforeEach(() => {
+      config = {
+        ...defaultConfig,
+        api: {
+          getFileContent: jest.fn(),
+        },
+        packageFile: 'package.js',
+        repoIsOnboarded: true,
+        logger,
+      };
+      depTypeWorker.renovateDepType.mockReturnValue([]);
+    });
+    it('returns empty if disabled', async () => {
+      config.enabled = false;
+      const res = await packageFileWorker.renovateMeteorPackageFile(config);
+      expect(res).toEqual([]);
+    });
+    it('returns upgrades', async () => {
+      depTypeWorker.renovateDepType.mockReturnValueOnce([{}, {}]);
+      const res = await packageFileWorker.renovateMeteorPackageFile(config);
+      expect(res).toHaveLength(2);
+    });
+  });
 });
diff --git a/test/workers/repository/__snapshots__/apis.spec.js.snap b/test/workers/repository/__snapshots__/apis.spec.js.snap
index 13465b626c..d21d5fc421 100644
--- a/test/workers/repository/__snapshots__/apis.spec.js.snap
+++ b/test/workers/repository/__snapshots__/apis.spec.js.snap
@@ -34,6 +34,13 @@ Array [
 ]
 `;
 
+exports[`workers/repository/apis detectPackageFiles(config) finds meteor package files 1`] = `
+Array [
+  "package.json",
+  "modules/something/package.js",
+]
+`;
+
 exports[`workers/repository/apis detectPackageFiles(config) ignores node modules 1`] = `
 Array [
   "package.json",
@@ -100,5 +107,8 @@ Array [
     "hasYarnLock": false,
     "packageFile": "a/package.json",
   },
+  Object {
+    "packageFile": "module/package.js",
+  },
 ]
 `;
diff --git a/test/workers/repository/apis.spec.js b/test/workers/repository/apis.spec.js
index bada3eef81..b8a04c5699 100644
--- a/test/workers/repository/apis.spec.js
+++ b/test/workers/repository/apis.spec.js
@@ -222,6 +222,9 @@ describe('workers/repository/apis', () => {
             'backend/package.json',
           ]),
         },
+        meteor: {
+          enabled: false,
+        },
         logger,
         warnings: [],
       };
@@ -229,8 +232,28 @@ describe('workers/repository/apis', () => {
       expect(res).toMatchObject(config);
       expect(res.packageFiles).toMatchSnapshot();
     });
+    it('finds meteor package files', async () => {
+      const config = {
+        api: {
+          findFilePaths: jest.fn(),
+        },
+        meteor: {
+          enabled: true,
+        },
+        logger,
+        warnings: [],
+      };
+      config.api.findFilePaths.mockReturnValueOnce(['package.json']);
+      config.api.findFilePaths.mockReturnValueOnce([
+        'modules/something/package.js',
+      ]);
+      const res = await apis.detectPackageFiles(config);
+      expect(res).toMatchObject(config);
+      expect(res.packageFiles).toMatchSnapshot();
+    });
     it('ignores node modules', async () => {
       const config = {
+        ...defaultConfig,
         ignorePaths: ['node_modules/'],
         api: {
           findFilePaths: jest.fn(() => [
@@ -248,6 +271,7 @@ describe('workers/repository/apis', () => {
     });
     it('defaults to package.json if found', async () => {
       const config = {
+        ...defaultConfig,
         api: {
           findFilePaths: jest.fn(() => []),
           getFileJson: jest.fn(() => ({})),
@@ -260,6 +284,7 @@ describe('workers/repository/apis', () => {
     });
     it('returns empty if package.json not found', async () => {
       const config = {
+        ...defaultConfig,
         api: {
           findFilePaths: jest.fn(() => []),
           getFileJson: jest.fn(() => null),
@@ -287,6 +312,7 @@ describe('workers/repository/apis', () => {
       expect(res.packageFiles).toEqual([]);
     });
     it('includes files with content', async () => {
+      config.packageFiles.push('module/package.js');
       config.api.getFileJson.mockReturnValueOnce({
         renovate: {},
         workspaces: [],
@@ -299,7 +325,7 @@ describe('workers/repository/apis', () => {
       config.api.getFileContent.mockReturnValueOnce(null);
       config.api.getFileContent.mockReturnValueOnce(null);
       const res = await apis.resolvePackageFiles(config);
-      expect(res.packageFiles).toHaveLength(2);
+      expect(res.packageFiles).toHaveLength(3);
       expect(res.packageFiles).toMatchSnapshot();
     });
   });
diff --git a/test/workers/repository/upgrades.spec.js b/test/workers/repository/upgrades.spec.js
index 1563b1eb8a..0adbcc7553 100644
--- a/test/workers/repository/upgrades.spec.js
+++ b/test/workers/repository/upgrades.spec.js
@@ -35,13 +35,14 @@ describe('workers/repository/upgrades', () => {
           packageFile: 'backend/package.json',
         },
         {
-          packageFile: 'frontend/package.json',
+          packageFile: 'frontend/package.js',
         },
       ];
       packageFileWorker.renovatePackageFile.mockReturnValueOnce(['a']);
       packageFileWorker.renovatePackageFile.mockReturnValueOnce(['b', 'c']);
+      packageFileWorker.renovateMeteorPackageFile.mockReturnValueOnce(['d']);
       const res = await upgrades.determineRepoUpgrades(config);
-      expect(res.length).toBe(3);
+      expect(res).toHaveLength(4);
     });
   });
   describe('generateConfig(branchUpgrades)', () => {
-- 
GitLab