UNPKG

7.67 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const cli_ux_1 = require("cli-ux");
4const fs = require("fs-extra");
5const loadJSON = require("load-json-file");
6const _ = require("lodash");
7const path = require("path");
8const readPkg = require("read-pkg");
9const util_1 = require("util");
10const Config = require(".");
11const ts_node_1 = require("./ts_node");
12const util_2 = require("./util");
13const debug = require('debug')('@anycli/config');
14const _pjson = require('../package.json');
15const loadedPlugins = {};
16class Plugin {
17 constructor(opts) {
18 this._base = `${_pjson.name}@${_pjson.version}`;
19 this.plugins = [];
20 this.ignoreManifest = !!opts.ignoreManifest;
21 this.type = opts.type || 'core';
22 const root = findRoot(opts.name, opts.root);
23 if (!root)
24 throw new Error(`could not find package.json with ${util_1.inspect(opts)}`);
25 if (loadedPlugins[root])
26 return loadedPlugins[root];
27 loadedPlugins[root] = this;
28 this.root = root;
29 debug('reading plugin %s', root);
30 this.pjson = readPkg.sync(path.join(root, 'package.json'));
31 this.name = this.pjson.name;
32 this.version = this.pjson.version;
33 if (!this.pjson.anycli) {
34 this.pjson.anycli = this.pjson['cli-engine'] || {};
35 }
36 this.valid = this.pjson.anycli.schema === 1;
37 this._topics = topicsToArray(this.pjson.anycli.topics || {});
38 this.hooks = _.mapValues(this.pjson.anycli.hooks || {}, _.castArray);
39 this.manifest = this._manifest();
40 this.loadPlugins(...this.pjson.anycli.plugins || []);
41 }
42 get commandsDir() {
43 return ts_node_1.tsPath(this.root, this.pjson.anycli.commands);
44 }
45 get topics() {
46 let topics = [...this._topics];
47 for (let plugin of this.plugins) {
48 topics = [...topics, ...plugin.topics];
49 }
50 return topics;
51 }
52 get commands() {
53 let commands = Object.entries(this.manifest.commands)
54 .map(([id, c]) => (Object.assign({}, c, { load: () => this._findCommand(id) })));
55 for (let plugin of this.plugins) {
56 commands = [...commands, ...plugin.commands];
57 }
58 return commands;
59 }
60 get commandIDs() {
61 let commands = Object.keys(this.manifest.commands);
62 for (let plugin of this.plugins) {
63 commands = [...commands, ...plugin.commandIDs];
64 }
65 return commands;
66 }
67 findCommand(id, opts = {}) {
68 let command = this.manifest.commands[id];
69 if (command)
70 return Object.assign({}, command, { load: () => this._findCommand(id) });
71 for (let plugin of this.plugins) {
72 let command = plugin.findCommand(id);
73 if (command)
74 return command;
75 }
76 if (opts.must)
77 throw new Error(`command ${id} not found`);
78 }
79 _findCommand(id) {
80 const search = (cmd) => {
81 if (_.isFunction(cmd.run))
82 return cmd;
83 return Object.values(cmd).find((cmd) => _.isFunction(cmd.run));
84 };
85 const p = require.resolve(path.join(this.commandsDir, ...id.split(':')));
86 debug('require', p);
87 const cmd = search(require(p));
88 cmd.id = id;
89 return cmd;
90 }
91 findTopic(name, opts = {}) {
92 let topic = this.topics.find(t => t.name === name);
93 if (topic)
94 return topic;
95 for (let plugin of this.plugins) {
96 let topic = plugin.findTopic(name);
97 if (topic)
98 return topic;
99 }
100 if (opts.must)
101 throw new Error(`topic ${name} not found`);
102 }
103 async runHook(event, opts) {
104 const promises = (this.hooks[event] || [])
105 .map(async (hook) => {
106 try {
107 const p = ts_node_1.tsPath(this.root, hook);
108 debug('hook', event, p);
109 await util_2.undefault(require(p))(opts);
110 }
111 catch (err) {
112 if (err.code === 'EEXIT')
113 throw err;
114 cli_ux_1.default.warn(err);
115 }
116 });
117 promises.push(...this.plugins.map(p => p.runHook(event, opts)));
118 await Promise.all(promises);
119 }
120 // findCommand(id: string, opts?: {must: boolean}): ICommand | undefined
121 // findManifestCommand(id: string, opts: {must: true}): IManifestCommand
122 // findManifestCommand(id: string, opts?: {must: boolean}): IManifestCommand | undefined
123 // findTopic(id: string, opts: {must: true}): ITopic
124 // findTopic(id: string, opts?: {must: boolean}): ITopic | undefined
125 _manifest() {
126 const readManifest = () => {
127 try {
128 const p = path.join(this.root, '.anycli.manifest.json');
129 const manifest = loadJSON.sync(p);
130 if (manifest.version !== this.version) {
131 cli_ux_1.default.warn(`Mismatched version in ${this.name} plugin manifest. Expected: ${this.version} Received: ${manifest.version}`);
132 }
133 else {
134 debug('using manifest from', p);
135 return manifest;
136 }
137 }
138 catch (err) {
139 if (err.code !== 'ENOENT')
140 cli_ux_1.default.warn(err);
141 }
142 };
143 if (!this.ignoreManifest) {
144 let manifest = readManifest();
145 if (manifest)
146 return manifest;
147 }
148 if (this.commandsDir)
149 return Config.Manifest.build(this.version, this.commandsDir, id => this._findCommand(id));
150 return { version: this.version, commands: {} };
151 }
152 loadPlugins(...plugins) {
153 if (!plugins.length)
154 return;
155 if (!plugins || !plugins.length)
156 return;
157 debug('loading plugins', plugins);
158 for (let plugin of plugins || []) {
159 try {
160 let opts = { type: this.type, root: this.root };
161 if (typeof plugin === 'string')
162 opts.name = plugin;
163 else
164 opts = Object.assign({}, opts, plugin);
165 this.plugins.push(new Plugin(opts));
166 }
167 catch (err) {
168 cli_ux_1.default.warn(err);
169 }
170 }
171 return plugins;
172 }
173}
174exports.Plugin = Plugin;
175function topicsToArray(input, base) {
176 if (!input)
177 return [];
178 base = base ? `${base}:` : '';
179 if (Array.isArray(input)) {
180 return input.concat(_.flatMap(input, t => topicsToArray(t.subtopics, `${base}${t.name}`)));
181 }
182 return _.flatMap(Object.keys(input), k => {
183 return [Object.assign({}, input[k], { name: `${base}${k}` })].concat(topicsToArray(input[k].subtopics, `${base}${input[k].name}`));
184 });
185}
186/**
187 * find package root
188 * for packages installed into node_modules this will go up directories until
189 * it finds a node_modules directory with the plugin installed into it
190 *
191 * This is needed because of the deduping npm does
192 */
193function findRoot(name, root) {
194 // essentially just "cd .."
195 function* up(from) {
196 while (path.dirname(from) !== from) {
197 yield from;
198 from = path.dirname(from);
199 }
200 yield from;
201 }
202 for (let next of up(root)) {
203 let cur;
204 if (name) {
205 cur = path.join(next, 'node_modules', name, 'package.json');
206 }
207 else {
208 cur = path.join(next, 'package.json');
209 }
210 if (fs.pathExistsSync(cur))
211 return path.dirname(cur);
212 }
213}