#!/usr/bin/env node /** * Module dependencies. */ // Interface heavily inspired by the yarn package manager: // https://yarnpkg.com/ require('./util'); const program = require('commander'); const inquirer = require('inquirer'); // Make things colorful! require('colors'); const emoji = require('node-emoji'); const fs = require('fs-extra'); const path = require('path'); const dir = path.resolve(__dirname, '..'); const spawn = require('cross-spawn'); const semver = require('semver'); const resolve = require('resolve'); const { plugins, iteratePlugins, 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 = iteratePlugins(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 --ignore-scripts ${fetchable.map( ({ name, version }) => `${name}@${version}`.cyan )}` ); if (!dryRun) { let args = [ 'add', '--ignore-scripts', ...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.`); } async function createSeedPlugin() { const pluginsDir = path.resolve(__dirname, '..', 'plugins'); function pluginNameExists(pluginName) { const pluginNames = fs.readdirSync(pluginsDir); return !!pluginNames.filter(pn => pn === pluginName).length; } let answers = await inquirer.prompt([ { type: 'input', name: 'pluginName', message: 'Plugin Name:', validate: input => { if (pluginNameExists(input)) { return 'Please, choose another name. That name already exists'; } if (input && input.length > 0) { return true; } return 'Plugin Name is required.'; }, }, { type: 'confirm', name: 'server', message: 'Is this plugin extending the server capabilities?', }, { type: 'confirm', name: 'client', message: 'Is this plugin extending the client capabilities?', }, { type: 'confirm', name: 'addPluginsJson', message: 'Should we add it to the plugins.json?', }, ]); //============================================================================== // Creating plugin seed //============================================================================== const seedPlugin = path.join(__dirname, 'templates/plugin'); const newPluginPath = path.join(pluginsDir, answers.pluginName); if (fs.existsSync(seedPlugin)) { if (answers.server && answers.client) { // This is a server-side and client-side plugin!, let's copy the template fs.copySync(seedPlugin, newPluginPath); } else { fs.copySync(seedPlugin, newPluginPath, { filter: p => { // Allowing plugin folder and files with no subfolders const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/gim; if ( rootRx.test(p) && (fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile()) ) { return true; } // If it's a client-side plugin, copying client folder if (answers.client) { return /client/.test(p); } // If it's a server-side plugin, copying server folder if (answers.server) { return /server/.test(p); } }, }); } // Let's add this to the plugins.json if (answers.addPluginsJson) { const pluginsJson = path.resolve(__dirname, '..', 'plugins.json'); fs .readJson(pluginsJson) .then(j => { // This is a client-side plugin, let's push this. if (answers.client) { j.client.push(answers.pluginName); const output = JSON.stringify(j, null, 2); fs.writeFileSync(pluginsJson, output); } // This is a server-side plugin, let's push this. if (answers.server) { j.server.push(answers.pluginName); const output = JSON.stringify(j, null, 2); fs.writeFileSync(pluginsJson, output); } }) .catch(err => { console.error(err); }); } console.log( `✨ Yay! Plugin created! Find your plugin: ${ answers.pluginName } in the ./plugins folder` ); } } //============================================================================== // Setting up the program command line arguments. //============================================================================== program .command('create') .description('creates a seed plugin') .action(createSeedPlugin); 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(); }