UNPKG

11.5 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const errors_1 = require("@anycli/errors");
4const fs = require("fs");
5const path = require("path");
6const util_1 = require("util");
7const command_1 = require("./command");
8const debug_1 = require("./debug");
9const ts_node_1 = require("./ts_node");
10const util_2 = require("./util");
11const debug = debug_1.default();
12const _pjson = require('../package.json');
13class Plugin {
14 constructor(opts) {
15 this._base = `${_pjson.name}@${_pjson.version}`;
16 this.plugins = [];
17 this.valid = false;
18 this.alreadyLoaded = false;
19 this.warned = false;
20 this.type = opts.type || 'core';
21 this.tag = opts.tag;
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 (Plugin.loadedPlugins[root]) {
26 Plugin.loadedPlugins[root].alreadyLoaded = true;
27 return Plugin.loadedPlugins[root];
28 }
29 Plugin.loadedPlugins[root] = this;
30 this.root = root;
31 debug('reading %s plugin %s', this.type, root);
32 this.pjson = util_2.loadJSONSync(path.join(root, 'package.json'));
33 this.name = this.pjson.name;
34 this.version = this.pjson.version;
35 if (this.pjson.anycli) {
36 this.valid = true;
37 }
38 else {
39 this.pjson.anycli = this.pjson['cli-engine'] || {};
40 }
41 this._topics = topicsToArray(this.pjson.anycli.topics || {});
42 this.hooks = util_2.mapValues(this.pjson.anycli.hooks || {}, i => Array.isArray(i) ? i : [i]);
43 this.manifest = this._manifest(!!opts.ignoreManifest);
44 this.loadPlugins(this.root, this.type, this.pjson.anycli.plugins || []);
45 this.addMissingTopics();
46 }
47 get commandsDir() {
48 return ts_node_1.tsPath(this.root, this.pjson.anycli.commands);
49 }
50 get topics() {
51 let topics = [...this._topics];
52 for (let plugin of this.plugins) {
53 for (let topic of plugin.topics) {
54 let existing = topics.find(t => t.name === topic.name);
55 if (existing) {
56 existing.description = topic.description || existing.description;
57 existing.hidden = existing.hidden || topic.hidden;
58 }
59 else
60 topics.push(topic);
61 }
62 topics = [...topics, ...plugin.topics];
63 }
64 return topics;
65 }
66 get commands() {
67 let commands = Object.entries(this.manifest.commands)
68 .map(([id, c]) => (Object.assign({}, c, { load: () => this._findCommand(id, { must: true }) })));
69 for (let plugin of this.plugins) {
70 commands = [...commands, ...plugin.commands];
71 }
72 return commands;
73 }
74 get commandIDs() {
75 let commands = Object.keys(this.manifest.commands);
76 for (let plugin of this.plugins) {
77 commands = [...commands, ...plugin.commandIDs];
78 }
79 return util_2.uniq(commands);
80 }
81 findCommand(id, opts = {}) {
82 let command = this.manifest.commands[id];
83 if (command)
84 return Object.assign({}, command, { load: () => this._findCommand(id, { must: true }) });
85 for (let plugin of this.plugins) {
86 let command = plugin.findCommand(id);
87 if (command)
88 return command;
89 }
90 if (opts.must)
91 errors_1.error(`command ${id} not found`);
92 }
93 findTopic(name, opts = {}) {
94 let topic = this.topics.find(t => t.name === name);
95 if (topic)
96 return topic;
97 for (let plugin of this.plugins) {
98 let topic = plugin.findTopic(name);
99 if (topic)
100 return topic;
101 }
102 if (opts.must)
103 throw new Error(`topic ${name} not found`);
104 }
105 async runHook(event, opts) {
106 const context = {
107 exit(code = 0) { errors_1.exit(code); },
108 log(message = '') {
109 message = typeof message === 'string' ? message : util_1.inspect(message);
110 process.stdout.write(message + '\n');
111 },
112 error(message, options = {}) {
113 errors_1.error(message, options);
114 },
115 warn(message) { errors_1.warn(message); },
116 };
117 const promises = (this.hooks[event] || [])
118 .map(async (hook) => {
119 try {
120 const p = ts_node_1.tsPath(this.root, hook);
121 debug('hook', event, p);
122 const search = (m) => {
123 if (typeof m === 'function')
124 return m;
125 if (m.default && typeof m.default === 'function')
126 return m.default;
127 return Object.values(m).find((m) => typeof m === 'function');
128 };
129 await search(require(p)).call(context, opts);
130 }
131 catch (err) {
132 if (err && err.anycli && err.anycli.exit !== undefined)
133 throw err;
134 this.warn(err, `runHook ${event}`);
135 }
136 });
137 promises.push(...this.plugins.map(p => p.runHook(event, opts)));
138 await Promise.all(promises);
139 }
140 get _commandIDs() {
141 if (!this.commandsDir)
142 return [];
143 let globby;
144 try {
145 globby = require('globby');
146 }
147 catch (_a) {
148 debug('not loading plugins, globby not found');
149 return [];
150 }
151 debug(`loading IDs from ${this.commandsDir}`);
152 const ids = globby.sync(['**/*.+(js|ts)', '!**/*.+(d.ts|test.ts|test.js)'], { cwd: this.commandsDir })
153 .map(file => {
154 const p = path.parse(file);
155 const topics = p.dir.split('/');
156 let command = p.name !== 'index' && p.name;
157 return [...topics, command].filter(f => f).join(':');
158 });
159 debug('found ids', ids);
160 return ids;
161 }
162 _findCommand(id, opts = {}) {
163 const fetch = () => {
164 if (!this.commandsDir)
165 return;
166 const search = (cmd) => {
167 if (typeof cmd.run === 'function')
168 return cmd;
169 if (cmd.default && cmd.default.run)
170 return cmd.default;
171 return Object.values(cmd).find((cmd) => typeof cmd.run === 'function');
172 };
173 const p = require.resolve(path.join(this.commandsDir, ...id.split(':')));
174 debug('require', p);
175 let m;
176 try {
177 m = require(p);
178 }
179 catch (err) {
180 if (!opts.must && err.code === 'MODULE_NOT_FOUND')
181 return;
182 throw err;
183 }
184 const cmd = search(m);
185 if (!cmd)
186 return;
187 cmd.id = id;
188 cmd.plugin = this;
189 return cmd;
190 };
191 const cmd = fetch();
192 if (!cmd && opts.must)
193 errors_1.error(`command ${id} not found`);
194 return cmd;
195 }
196 _manifest(ignoreManifest) {
197 const readManifest = () => {
198 try {
199 const p = path.join(this.root, '.anycli.manifest.json');
200 const manifest = util_2.loadJSONSync(p);
201 if (!process.env.ANYCLI_NEXT_VERSION && manifest.version !== this.version) {
202 process.emitWarning(`Mismatched version in ${this.name} plugin manifest. Expected: ${this.version} Received: ${manifest.version}`);
203 }
204 else {
205 debug('using manifest from', p);
206 return manifest;
207 }
208 }
209 catch (err) {
210 if (err.code !== 'ENOENT')
211 this.warn(err, 'readManifest');
212 }
213 };
214 if (!ignoreManifest) {
215 let manifest = readManifest();
216 if (manifest)
217 return manifest;
218 }
219 return {
220 version: this.version,
221 commands: this._commandIDs.map(id => {
222 try {
223 return [id, command_1.Command.toCached(this._findCommand(id, { must: true }))];
224 }
225 catch (err) {
226 this.warn(err, 'toCached');
227 }
228 })
229 .filter((f) => !!f)
230 .reduce((commands, [id, c]) => {
231 commands[id] = c;
232 return commands;
233 }, {})
234 };
235 }
236 loadPlugins(root, type, plugins) {
237 if (!plugins.length)
238 return;
239 if (!plugins || !plugins.length)
240 return;
241 debug('loading plugins', plugins);
242 for (let plugin of plugins || []) {
243 try {
244 let opts = { type, root };
245 if (typeof plugin === 'string') {
246 opts.name = plugin;
247 }
248 else {
249 opts.name = plugin.name || opts.name;
250 opts.tag = plugin.tag || opts.tag;
251 opts.root = plugin.root || opts.root;
252 }
253 this.plugins.push(new Plugin(opts));
254 }
255 catch (err) {
256 this.warn(err, 'loadPlugins');
257 }
258 }
259 return plugins;
260 }
261 warn(err, scope) {
262 if (this.warned)
263 return;
264 err.name = `${err.name} Plugin: ${this.name}`;
265 err.detail = util_2.compact([err.detail, `module: ${this._base}`, scope && `task: ${scope}`, `plugin: ${this.name}`, `root: ${this.root}`]).join('\n');
266 process.emitWarning(err);
267 }
268 addMissingTopics() {
269 for (let c of this.commands.filter(c => !c.hidden)) {
270 let parts = c.id.split(':');
271 while (parts.length) {
272 let name = parts.join(':');
273 if (name && !this._topics.find(t => t.name === name)) {
274 this._topics.push({ name });
275 }
276 parts.pop();
277 }
278 }
279 }
280}
281Plugin.loadedPlugins = {};
282exports.Plugin = Plugin;
283function topicsToArray(input, base) {
284 if (!input)
285 return [];
286 base = base ? `${base}:` : '';
287 if (Array.isArray(input)) {
288 return input.concat(util_2.flatMap(input, t => topicsToArray(t.subtopics, `${base}${t.name}`)));
289 }
290 return util_2.flatMap(Object.keys(input), k => {
291 return [Object.assign({}, input[k], { name: `${base}${k}` })].concat(topicsToArray(input[k].subtopics, `${base}${input[k].name}`));
292 });
293}
294/**
295 * find package root
296 * for packages installed into node_modules this will go up directories until
297 * it finds a node_modules directory with the plugin installed into it
298 *
299 * This is needed because of the deduping npm does
300 */
301function findRoot(name, root) {
302 // essentially just "cd .."
303 function* up(from) {
304 while (path.dirname(from) !== from) {
305 yield from;
306 from = path.dirname(from);
307 }
308 yield from;
309 }
310 for (let next of up(root)) {
311 let cur;
312 if (name) {
313 cur = path.join(next, 'node_modules', name, 'package.json');
314 }
315 else {
316 cur = path.join(next, 'package.json');
317 }
318 if (fs.existsSync(cur))
319 return path.dirname(cur);
320 }
321}