#!/usr/bin/env node /** * Module dependencies. */ // Interface heavily inspired by the yarn package manager: // https://yarnpkg.com/ const program = require('./commander'); // Make things colorful! require('colors'); const emoji = require('node-emoji'); 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, itteratePlugins, isInternal} = require('../plugins'); function existsInNodeModules(name) { try { resolve.sync(name, {basedir: dir}); return true; } catch (e) { return false; } } 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({quiet = false, upgradeRemote = false}) { const fetchable = []; const local = []; const upgradable = []; 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 = itteratePlugins(plugins[i]); for (let j in section) { let {name, version} = section[j]; let namespaced = name.charAt(0) === '@'; let dep = name.split('/') .slice(0, namespaced ? 2 : 1) .join('/'); // Ignore relative modules, which aren't installed by NPM if (!dep.match(EXTERNAL) && !namespaced) { return; } if (isInternal(dep)) { if (!quiet) { console.log(` l ${name}`); } local.push({name, version}); continue; } if (!existsInNodeModules(dep)) { 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 { if (!quiet) { console.log(` e ${name}`); } if (upgradeRemote) { upgradable.push({name, version}); } } } } if (!quiet) { console.log(); } return {local, fetchable, upgradable}; } 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/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`.yellow); if (fetchable.length > 0) { console.log(`$ yarn add ${fetchable.map(({name, version}) => `${name}@${version}`.cyan)}`); if (!dryRun) { let args = [ 'add', ...fetchable.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()); } } if (upgradable.length > 0) { console.log(`$ yarn upgrade ${upgradable.map(({name, version}) => `${name}@${version}`.cyan)}`); if (!dryRun) { 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 {upgradable, fetchable}; } async function reconcileLocalPlugins({skipRemote, dryRun}) { 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]; if (!fs.existsSync(path.join(dir, 'plugins', name, 'package.json'))) { continue; } let wd = path.join(dir, 'plugins', name); console.log(`$ cd ${wd.cyan} && yarn`); if (!dryRun) { let args = []; let output = spawn.sync('yarn', args, { stdio: ['ignore', 'pipe', 'inherit'], cwd: wd }); if (output.status) { throw new Error('Could not install local plugin dependencies, errors occured during install'); } console.log(output.stdout.toString()); } } } // 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, upgradeRemote}) { let startTime = new Date(); // We don't need to do anything if we skip everything.... if (skipLocal && skipRemote) { return; } // Traverse local plugins and install dependencies if enabled. if (!skipLocal) { await reconcileLocalPlugins({skipRemote, dryRun}); } // Locate any external plugins and install them. if (!skipRemote) { let results = []; try { results = await reconcileRemotePlugins({skipLocal, skipRemote, dryRun, upgradeRemote}); } catch (e) { throw e; } let status; if (dryRun) { status = '[dry-run] success'.green; } else { status = 'success'.green; } let message; if (results.upgradable.length === 0 && results.fetchable.length === 0) { message = 'Already up-to-date.'; } else if (results.upgradable.length === 0) { message = `Fetched ${results.fetchable.length} new plugins.`; } else if (results.fetchable.length === 0) { message = `Upgraded ${results.upgradable.length} new plugins.`; } else { message = `Fetched ${results.fetchable.length} new plugins, upgraded ${results.upgradable.length} plugins.`; } console.log(`\n${status} ${message}`); } let endTime = new Date(); let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2); console.log(`✨ Done in ${totalTime}s.`); } //============================================================================== // Setting up the program command line arguments. //============================================================================== program .command('list') .description('') .action(reconcilePackages); 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') .action(reconcilePluginDeps); program.parse(process.argv); // If there is no command listed, output help. if (!process.argv.slice(2).length) { program.outputHelp(); }