@anycli/config
Version:
base config object and standard interfaces for anycli components
242 lines (241 loc) • 8.84 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const path = require("path");
const util_1 = require("util");
const debug_1 = require("./debug");
const errors_1 = require("./errors");
const manifest_1 = require("./manifest");
const ts_node_1 = require("./ts_node");
const util_2 = require("./util");
const debug = debug_1.default();
const _pjson = require('../package.json');
class Plugin {
constructor(opts) {
this._base = `${_pjson.name}@${_pjson.version}`;
this.plugins = [];
this.alreadyLoaded = false;
this.ignoreManifest = !!opts.ignoreManifest;
this.type = opts.type || 'core';
this.tag = opts.tag;
const root = findRoot(opts.name, opts.root);
if (!root)
throw new Error(`could not find package.json with ${util_1.inspect(opts)}`);
if (Plugin.loadedPlugins[root]) {
Plugin.loadedPlugins[root].alreadyLoaded = true;
return Plugin.loadedPlugins[root];
}
Plugin.loadedPlugins[root] = this;
this.root = root;
debug('reading plugin %s', root);
this.pjson = util_2.loadJSONSync(path.join(root, 'package.json'));
this.name = this.pjson.name;
this.version = this.pjson.version;
if (!this.pjson.anycli) {
this.pjson.anycli = this.pjson['cli-engine'] || {};
}
this.valid = this.pjson.anycli.schema === 1;
this._topics = topicsToArray(this.pjson.anycli.topics || {});
this.hooks = util_2.mapValues(this.pjson.anycli.hooks || {}, i => Array.isArray(i) ? i : [i]);
this.manifest = this._manifest();
this.loadPlugins(this.root, this.pjson.anycli.plugins || []);
}
get commandsDir() {
return ts_node_1.tsPath(this.root, this.pjson.anycli.commands);
}
get topics() {
let topics = [...this._topics];
for (let plugin of this.plugins) {
topics = [...topics, ...plugin.topics];
}
return topics;
}
get commands() {
let commands = Object.entries(this.manifest.commands)
.map(([id, c]) => (Object.assign({}, c, { load: () => this._findCommand(id) })));
for (let plugin of this.plugins) {
commands = [...commands, ...plugin.commands];
}
return commands;
}
get commandIDs() {
let commands = Object.keys(this.manifest.commands);
for (let plugin of this.plugins) {
commands = [...commands, ...plugin.commandIDs];
}
return commands;
}
findCommand(id, opts = {}) {
let command = this.manifest.commands[id];
if (command)
return Object.assign({}, command, { load: () => this._findCommand(id) });
for (let plugin of this.plugins) {
let command = plugin.findCommand(id);
if (command)
return command;
}
if (opts.must)
throw new Error(`command ${id} not found`);
}
_findCommand(id) {
const search = (cmd) => {
if (typeof cmd.run === 'function')
return cmd;
if (cmd.default && cmd.default.run)
return cmd.default;
return Object.values(cmd).find((cmd) => typeof cmd.run === 'function');
};
const p = require.resolve(path.join(this.commandsDir, ...id.split(':')));
debug('require', p);
const cmd = search(require(p));
cmd.id = id;
cmd.plugin = this;
return cmd;
}
findTopic(name, opts = {}) {
let topic = this.topics.find(t => t.name === name);
if (topic)
return topic;
for (let plugin of this.plugins) {
let topic = plugin.findTopic(name);
if (topic)
return topic;
}
if (opts.must)
throw new Error(`topic ${name} not found`);
}
async runHook(event, opts) {
const context = {
exit(code) {
throw new errors_1.ExitError(code);
},
log(message) {
process.stdout.write((message || '') + '\n');
},
error(message, options = {}) {
throw new errors_1.CLIError(message, options);
},
};
const promises = (this.hooks[event] || [])
.map(async (hook) => {
try {
const p = ts_node_1.tsPath(this.root, hook);
debug('hook', event, p);
const search = (m) => {
if (typeof m === 'function')
return m;
if (m.default && typeof m.default === 'function')
return m.default;
return Object.values(m).find((m) => typeof m === 'function');
};
await search(require(p)).call(context, opts);
}
catch (err) {
if (err && err['cli-ux'] && err['cli-ux'])
throw err;
process.emitWarning(err);
}
});
promises.push(...this.plugins.map(p => p.runHook(event, opts)));
await Promise.all(promises);
}
// findCommand(id: string, opts?: {must: boolean}): ICommand | undefined
// findManifestCommand(id: string, opts: {must: true}): IManifestCommand
// findManifestCommand(id: string, opts?: {must: boolean}): IManifestCommand | undefined
// findTopic(id: string, opts: {must: true}): ITopic
// findTopic(id: string, opts?: {must: boolean}): ITopic | undefined
_manifest() {
const readManifest = () => {
try {
const p = path.join(this.root, '.anycli.manifest.json');
const manifest = util_2.loadJSONSync(p);
if (manifest.version !== this.version) {
process.emitWarning(`Mismatched version in ${this.name} plugin manifest. Expected: ${this.version} Received: ${manifest.version}`);
}
else {
debug('using manifest from', p);
return manifest;
}
}
catch (err) {
if (err.code !== 'ENOENT')
process.emitWarning(err);
}
};
if (!this.ignoreManifest) {
let manifest = readManifest();
if (manifest)
return manifest;
}
if (this.commandsDir)
return manifest_1.Manifest.build(this.version, this.commandsDir, id => this._findCommand(id));
return { version: this.version, commands: {} };
}
loadPlugins(root, plugins) {
if (!plugins.length)
return;
if (!plugins || !plugins.length)
return;
debug('loading plugins', plugins);
for (let plugin of plugins || []) {
try {
let opts = { type: this.type, root };
if (typeof plugin === 'string') {
opts.name = plugin;
}
else {
opts.name = plugin.name || opts.name;
opts.type = plugin.type || opts.type;
opts.tag = plugin.tag || opts.tag;
opts.root = plugin.root || opts.root;
}
this.plugins.push(new Plugin(opts));
}
catch (err) {
process.emitWarning(err);
}
}
return plugins;
}
}
Plugin.loadedPlugins = {};
exports.Plugin = Plugin;
function topicsToArray(input, base) {
if (!input)
return [];
base = base ? `${base}:` : '';
if (Array.isArray(input)) {
return input.concat(util_2.flatMap(input, t => topicsToArray(t.subtopics, `${base}${t.name}`)));
}
return util_2.flatMap(Object.keys(input), k => {
return [Object.assign({}, input[k], { name: `${base}${k}` })].concat(topicsToArray(input[k].subtopics, `${base}${input[k].name}`));
});
}
/**
* find package root
* for packages installed into node_modules this will go up directories until
* it finds a node_modules directory with the plugin installed into it
*
* This is needed because of the deduping npm does
*/
function findRoot(name, root) {
// essentially just "cd .."
function* up(from) {
while (path.dirname(from) !== from) {
yield from;
from = path.dirname(from);
}
yield from;
}
for (let next of up(root)) {
let cur;
if (name) {
cur = path.join(next, 'node_modules', name, 'package.json');
}
else {
cur = path.join(next, 'package.json');
}
if (fs.existsSync(cur))
return path.dirname(cur);
}
}