UNPKG

@anycli/config

Version:

base config object and standard interfaces for anycli components

242 lines (241 loc) 8.84 kB
"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); } }