From 9c806cb6e4bef3d68df85c785c5304f036fbdb9a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 3 Apr 2017 11:58:54 -0600 Subject: [PATCH] Added support for plugin upgrades --- bin/cli-plugins | 150 ++++++++++++++++++++++++++++++++---------------- package.json | 1 + plugins.js | 66 ++++++++++++--------- 3 files changed, 141 insertions(+), 76 deletions(-) diff --git a/bin/cli-plugins b/bin/cli-plugins index ecffd6f0b..94ce74904 100755 --- a/bin/cli-plugins +++ b/bin/cli-plugins @@ -17,8 +17,9 @@ const dir = process.cwd(); const fs = require('fs'); const path = require('path'); const spawn = require('cross-spawn'); +const semver = require('semver'); const resolve = require('resolve'); -const {plugins, isInternal} = require('../plugins'); +const {plugins, itteratePlugins, isInternal} = require('../plugins'); function existsInNodeModules(name) { try { @@ -30,20 +31,48 @@ function existsInNodeModules(name) { } } +function versionMatch(name, version) { + try { + let matched = false; + + resolve.sync(name, { + basedir: dir, + packageFilter: (pkg) => { + if (pkg && pkg.version && semver.satisfies(pkg.version, version)) { + matched = true; + } + + return pkg; + } + }); + + return matched; + } catch (e) { + return false; + } +} + const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc. -function reconcilePackages(log = console.log) { - const linkable = []; +function reconcilePackages({quiet = false, upgradeRemote = false}) { const fetchable = []; const local = []; + const upgradable = []; - log(); - log(' +local (l) packages in your project\n +external (e) referenced packages in node_modules but not in current project\n +missing (m) referenced packages but not found\n +symlinked (sl) symlinked external packages\n'); + if (!quiet) { + console.log(); + console.log(' +local (l) packages in your project'); + console.log(' +external (e) packages are external'); + console.log(' +outofdate (oe) packages are external but are out of date'); + console.log(' +missing (m) packages are not found'); + console.log(); + } + for (let i in plugins) { - let section = plugins[i]; + let section = itteratePlugins(plugins[i]); - for (let name in section) { - let version = section[name]; + for (let j in section) { + let {name, version} = section[j]; let namespaced = name.charAt(0) === '@'; let dep = name.split('/') @@ -56,36 +85,59 @@ function reconcilePackages(log = console.log) { } if (isInternal(dep)) { - let stat = fs.lstatSync(path.join(dir, 'plugins', name)); - if (stat.isSymbolicLink()) { - log(` sl ${name.cyan}`); - } else { - log(` l ${name.cyan}`); - local.push({name, version}); + if (!quiet) { + console.log(` l ${name}`); } + local.push({name, version}); continue; } if (!existsInNodeModules(dep)) { - log(` m ${name.cyan}`); + if (!quiet) { + console.log(` m ${name}`); + } fetchable.push({name, version}); + } else if (!versionMatch(dep, version)) { + + // A plugin was found, yet the current version does not match the + // current version installed. We should warn if upgradeRemote is + // not enabled that it is currently not supported. + if (!upgradeRemote) { + if (!quiet) { + console.warn(` oe ${name} (package upgrade may be required)`.bgRed); + } + + continue; + } + + console.log(` oe ${name} (package upgrade may be required)`); + + upgradable.push({name, version}); } else { - log(` e ${name.cyan}`); - linkable.push({name, version}); + if (!quiet) { + console.log(` e ${name}`); + } + + if (upgradeRemote) { + upgradable.push({name, version}); + } } } } - log(); - return {local, linkable, fetchable}; + if (!quiet) { + console.log(); + } + + return {local, fetchable, upgradable}; } -async function reconcileRemotePlugins({skipLocal, dryRun}) { - console.log(`\n[${skipLocal ? '1/3' : '2/4'}] ${emoji.get('mag')} Reconciling plugins...`.yellow); - const {linkable, fetchable} = reconcilePackages(); +async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) { + console.log(`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get('mag')} Reconciling plugins...`.yellow); + const {fetchable, upgradable} = reconcilePackages({upgradeRemote}); - console.log(`[${skipLocal ? '2/3' : '3/4'}] ${emoji.get('truck')} Fetching plugins...\n`.yellow); + console.log(`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`.yellow); if (fetchable.length > 0) { @@ -108,37 +160,36 @@ async function reconcileRemotePlugins({skipLocal, dryRun}) { console.log(output.stdout.toString()); } - - fetchable.forEach((plugin) => { - linkable.push(plugin); - }); } - // TODO fetch the plugins using yarn - - console.log(`\n[${skipLocal ? '3/3' : '4/4'}] ${emoji.get('link')} Linking plugins...\n`.yellow); - - for (let i in linkable) { - let {name} = linkable[i]; - - let src = path.join(dir, 'node_modules', name); - let dst = path.join(dir, 'plugins', name); - - console.log(`$ link ${src.cyan} -> ${dst.cyan}`); + if (upgradable.length > 0) { + console.log(`$ yarn upgrade ${upgradable.map(({name, version}) => `${name}@${version}`.cyan)}`); if (!dryRun) { - // Create the symlink for the plugin directory. - fs.symlinkSync(src, dst, 'dir'); + let args = [ + 'upgrade', + ...upgradable.map(({name, version}) => `${name}@${version}`) + ]; + + let output = spawn.sync('yarn', args, { + stdio: ['ignore', 'pipe', 'inherit'] + }); + + if (output.status) { + throw new Error('Could not install external plugins, errors occured during install'); + } + + console.log(output.stdout.toString()); } } - return {linkable, fetchable}; + return {upgradable, fetchable}; } async function reconcileLocalPlugins({skipRemote, dryRun}) { - console.log(`\n[${skipRemote ? '1/1' : '1/4'}] ${emoji.get('pick')} Installing local plugin dependencies...\n`.yellow); - const {local} = reconcilePackages(() => {}); + console.log(`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get('pick')} Installing local plugin dependencies...\n`.yellow); + const {local} = reconcilePackages({quiet: true}); for (let i in local) { let {name} = local[i]; @@ -171,7 +222,7 @@ async function reconcileLocalPlugins({skipRemote, dryRun}) { // This traverses the local plugins and installs any dependencies listed there, // this only is really needed for plugins that are installed via docker because // core plugins will have their dependencies already included in core. -async function reconcilePluginDeps({skipLocal, skipRemote, dryRun}) { +async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote}) { let startTime = new Date(); // We don't need to do anything if we skip everything.... @@ -188,7 +239,7 @@ async function reconcilePluginDeps({skipLocal, skipRemote, dryRun}) { if (!skipRemote) { let results = []; try { - results = await reconcileRemotePlugins({skipLocal, skipRemote, dryRun}); + results = await reconcileRemotePlugins({skipLocal, skipRemote, dryRun, upgradeRemote}); } catch (e) { throw e; } @@ -201,14 +252,14 @@ async function reconcilePluginDeps({skipLocal, skipRemote, dryRun}) { } let message; - if (results.linkable.length === 0 && results.fetchable.length === 0) { + if (results.upgradable.length === 0 && results.fetchable.length === 0) { message = 'Already up-to-date.'; - } else if (results.linkable.length === 0) { + } else if (results.upgradable.length === 0) { message = `Fetched ${results.fetchable.length} new plugins.`; } else if (results.fetchable.length === 0) { - message = `Linked ${results.linkable.length} new plugins.`; + message = `Upgraded ${results.upgradable.length} new plugins.`; } else { - message = `Fetched ${results.fetchable.length} new plugins, linked ${results.linkable.length} plugins.`; + message = `Fetched ${results.fetchable.length} new plugins, upgraded ${results.upgradable.length} plugins.`; } console.log(`\n${status} ${message}`); @@ -232,6 +283,7 @@ program program .command('reconcile') .description('reconciles local plugin dependencies and downloads external plugins') + .option('-u, --upgrade-remote', 'upgrades remote dependencies') .option('-d, --dry-run', 'does not actually change anything on the filesystem acts only as a simulation') .option('--skip-local', 'skips the local dependancy reconciliation') .option('--skip-remote', 'skips the remote plugin reconciliation') diff --git a/package.json b/package.json index 6c0affbba..f29351b3e 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "react-recaptcha": "^2.2.6", "redis": "^2.7.1", "resolve": "^1.3.2", + "semver": "^5.3.0", "simplemde": "^1.11.2", "uuid": "^2.0.3" }, diff --git a/plugins.js b/plugins.js index 6b506f5aa..4fae45d5a 100644 --- a/plugins.js +++ b/plugins.js @@ -1,5 +1,6 @@ const fs = require('fs'); const path = require('path'); +const resolve = require('resolve'); const debug = require('debug')('talk:plugins'); let plugins = {}; @@ -43,8 +44,40 @@ function pluginPath(name) { return path.join(__dirname, 'plugins', name); } - // The plugin is not available. - return undefined; + try { + return resolve.sync(name, {basedir: process.cwd()}); + } catch (e) { + return undefined; + } +} + +function itteratePlugins(plugins) { + return plugins.map((p) => { + let plugin = {}; + + // This checks to see if the structure for this entry is an object: + // + // {"people": "^1.2.0"} + // + // otherwise it's checked whether it matches the local version: + // + // "people" + // + if (typeof p === 'object') { + plugin.name = Object.keys(p).find((name) => name !== null); + plugin.version = p[plugin.name]; + } else if (typeof p === 'string') { + plugin.name = p; + plugin.version = `file:./plugins/${plugin.name}`; + } else { + throw new Error(`plugins.json is malformed, refer to PLUGINS.md for formatting, expected a string or an object for a plugin entry, found a ${typeof p}`); + } + + // Get the path for the plugin. + plugin.path = pluginPath(plugin.name); + + return plugin; + }); } /** @@ -52,31 +85,9 @@ function pluginPath(name) { */ class PluginSection { constructor(plugins) { - this.plugins = plugins.map((p) => { - let plugin = {}; - - // This checks to see if the structure for this entry is an object: - // - // {"people": "^1.2.0"} - // - // otherwise it's checked whether it matches the local version: - // - // "people" - // - if (typeof p === 'object') { - plugin.name = Object.keys(p).find((name) => name !== null); - plugin.version = p[plugin.name]; - } else if (typeof p === 'string') { - plugin.name = p; - } else { - throw new Error(`plugins.json is malformed, refer to PLUGINS.md for formatting, expected a string or an object for a plugin entry, found a ${typeof p}`); - } - - // Get the path for the plugin. - plugin.path = pluginPath(plugin.name); - + this.plugins = itteratePlugins(plugins).map((plugin) => { if (typeof plugin.path === 'undefined') { - throw new Error(`plugin '${plugin.name}' is not local or is not symlinked from your node_modules directory, plugin reconsiliation may be required`); + throw new Error(`plugin '${plugin.name}' is not local and is not resolvable, plugin reconsiliation may be required`); } try { @@ -151,5 +162,6 @@ module.exports = { plugins, PluginManager, isInternal, - pluginPath + pluginPath, + itteratePlugins };