UNPKG

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