Files
talk/bin/cli-plugins
T
2018-01-11 15:24:46 -07:00

419 lines
11 KiB
JavaScript
Executable File

#!/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 dir = process.cwd();
const fs = require('fs-extra');
const path = require('path');
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}/igm;
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();
}