mirror of
https://github.com/wassname/talk.git
synced 2026-06-28 04:23:53 +08:00
407 lines
10 KiB
JavaScript
Executable File
407 lines
10 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 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({ dryRun, upgradeRemote }) {
|
|
console.log(`\n['1/2'] ${emoji.get('mag')} Reconciling plugins...`.yellow);
|
|
const { fetchable, upgradable } = reconcilePackages({ upgradeRemote });
|
|
|
|
console.log(`['2/2'] ${emoji.get('truck')} Fetching plugins...\n`.yellow);
|
|
|
|
if (fetchable.length > 0) {
|
|
console.log(
|
|
`$ yarn add --ignore-scripts --ignore-workspace-root-check ${fetchable
|
|
.map(({ name, version }) => `${name}@${version}`.cyan)
|
|
.join(' ')}`
|
|
);
|
|
|
|
if (!dryRun) {
|
|
let args = [
|
|
'add',
|
|
'--ignore-scripts',
|
|
'--ignore-workspace-root-check',
|
|
...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 occurred 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 occurred during install'
|
|
);
|
|
}
|
|
|
|
console.log(output.stdout.toString());
|
|
}
|
|
}
|
|
|
|
return { upgradable, fetchable };
|
|
}
|
|
|
|
// 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({ dryRun, upgradeRemote }) {
|
|
try {
|
|
let startTime = new Date();
|
|
|
|
// Locate any external plugins and install them.
|
|
const results = await reconcileRemotePlugins({
|
|
dryRun,
|
|
upgradeRemote,
|
|
});
|
|
|
|
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.`);
|
|
} catch (err) {
|
|
console.error(err);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
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');
|
|
|
|
let j;
|
|
try {
|
|
j = await fs.readJson(pluginsJson);
|
|
} catch (err) {
|
|
// Fallback to plugins.default.json if the plugins.json file does not
|
|
// exist.
|
|
const defaultPluginsJson = path.resolve(
|
|
__dirname,
|
|
'..',
|
|
'plugins.default.json'
|
|
);
|
|
|
|
try {
|
|
j = await fs.readJson(defaultPluginsJson);
|
|
} catch (err) {
|
|
// Fallback to an empty one if that also, doesn't exist.
|
|
j = { client: [], server: [] };
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
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 dependencies by downloading 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'
|
|
)
|
|
.action(reconcilePluginDeps);
|
|
|
|
program.parse(process.argv);
|
|
|
|
// If there is no command listed, output help.
|
|
if (!process.argv.slice(2).length) {
|
|
program.outputHelp();
|
|
}
|