UNPKG

9.73 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const errors_1 = require("@oclif/errors");
4const path = require("path");
5const util_1 = require("util");
6const command_1 = require("./command");
7const debug_1 = require("./debug");
8const ts_node_1 = require("./ts-node");
9const util_2 = require("./util");
10const ROOT_INDEX_CMD_ID = '';
11const _pjson = require('../package.json');
12const hasManifest = function (p) {
13 try {
14 require(p);
15 return true;
16 }
17 catch (_a) {
18 return false;
19 }
20};
21function topicsToArray(input, base) {
22 if (!input)
23 return [];
24 base = base ? `${base}:` : '';
25 if (Array.isArray(input)) {
26 return input.concat(util_2.flatMap(input, t => topicsToArray(t.subtopics, `${base}${t.name}`)));
27 }
28 return util_2.flatMap(Object.keys(input), k => {
29 input[k].name = k;
30 return [Object.assign(Object.assign({}, input[k]), { name: `${base}${k}` })].concat(topicsToArray(input[k].subtopics, `${base}${input[k].name}`));
31 });
32}
33// eslint-disable-next-line valid-jsdoc
34/**
35 * find package root
36 * for packages installed into node_modules this will go up directories until
37 * it finds a node_modules directory with the plugin installed into it
38 *
39 * This is needed because of the deduping npm does
40 */
41async function findRoot(name, root) {
42 // essentially just "cd .."
43 function* up(from) {
44 while (path.dirname(from) !== from) {
45 yield from;
46 from = path.dirname(from);
47 }
48 yield from;
49 }
50 for (const next of up(root)) {
51 let cur;
52 if (name) {
53 cur = path.join(next, 'node_modules', name, 'package.json');
54 // eslint-disable-next-line no-await-in-loop
55 if (await util_2.exists(cur))
56 return path.dirname(cur);
57 try {
58 // eslint-disable-next-line no-await-in-loop
59 const pkg = await util_2.loadJSON(path.join(next, 'package.json'));
60 if (pkg.name === name)
61 return next;
62 }
63 catch (_a) { }
64 }
65 else {
66 cur = path.join(next, 'package.json');
67 // eslint-disable-next-line no-await-in-loop
68 if (await util_2.exists(cur))
69 return path.dirname(cur);
70 }
71 }
72}
73class Plugin {
74 // eslint-disable-next-line no-useless-constructor
75 constructor(options) {
76 this.options = options;
77 // static loadedPlugins: {[name: string]: Plugin} = {}
78 this._base = `${_pjson.name}@${_pjson.version}`;
79 this.valid = false;
80 this.alreadyLoaded = false;
81 this.children = [];
82 // eslint-disable-next-line new-cap
83 this._debug = debug_1.default();
84 this.warned = false;
85 }
86 async load() {
87 this.type = this.options.type || 'core';
88 this.tag = this.options.tag;
89 const root = await findRoot(this.options.name, this.options.root);
90 if (!root)
91 throw new Error(`could not find package.json with ${util_1.inspect(this.options)}`);
92 this.root = root;
93 this._debug('reading %s plugin %s', this.type, root);
94 this.pjson = await util_2.loadJSON(path.join(root, 'package.json'));
95 this.name = this.pjson.name;
96 const pjsonPath = path.join(root, 'package.json');
97 if (!this.name)
98 throw new Error(`no name in ${pjsonPath}`);
99 const isProd = hasManifest(path.join(root, 'oclif.manifest.json'));
100 if (!isProd && !this.pjson.files)
101 this.warn(`files attribute must be specified in ${pjsonPath}`);
102 // eslint-disable-next-line new-cap
103 this._debug = debug_1.default(this.name);
104 this.version = this.pjson.version;
105 if (this.pjson.oclif) {
106 this.valid = true;
107 }
108 else {
109 this.pjson.oclif = this.pjson['cli-engine'] || {};
110 }
111 this.hooks = util_2.mapValues(this.pjson.oclif.hooks || {}, i => Array.isArray(i) ? i : [i]);
112 this.manifest = await this._manifest(Boolean(this.options.ignoreManifest), Boolean(this.options.errorOnManifestCreate));
113 this.commands = Object.entries(this.manifest.commands)
114 .map(([id, c]) => (Object.assign(Object.assign({}, c), { load: () => this.findCommand(id, { must: true }) })));
115 this.commands.sort((a, b) => {
116 if (a.id < b.id)
117 return -1;
118 if (a.id > b.id)
119 return 1;
120 return 0;
121 });
122 }
123 get topics() {
124 return topicsToArray(this.pjson.oclif.topics || {});
125 }
126 get commandsDir() {
127 return ts_node_1.tsPath(this.root, this.pjson.oclif.commands);
128 }
129 get commandIDs() {
130 if (!this.commandsDir)
131 return [];
132 let globby;
133 try {
134 const globbyPath = require.resolve('globby', { paths: [this.root, __dirname] });
135 globby = require(globbyPath);
136 }
137 catch (error) {
138 this.warn(error, 'not loading commands, globby not found');
139 return [];
140 }
141 this._debug(`loading IDs from ${this.commandsDir}`);
142 const patterns = [
143 '**/*.+(js|ts|tsx)',
144 '!**/*.+(d.ts|test.ts|test.js|spec.ts|spec.js)?(x)',
145 ];
146 const ids = globby.sync(patterns, { cwd: this.commandsDir })
147 .map(file => {
148 const p = path.parse(file);
149 const topics = p.dir.split('/');
150 const command = p.name !== 'index' && p.name;
151 // support src/commands/index as a "root" command
152 if (!command && this.type === 'core' && p.dir.length === 0 && p.name === 'index')
153 return ROOT_INDEX_CMD_ID;
154 return [...topics, command].filter(f => f).join(':');
155 });
156 this._debug('found commands', ids);
157 return ids;
158 }
159 findCommand(id, opts = {}) {
160 const fetch = () => {
161 if (!this.commandsDir)
162 return;
163 const search = (cmd) => {
164 if (typeof cmd.run === 'function')
165 return cmd;
166 if (cmd.default && cmd.default.run)
167 return cmd.default;
168 return Object.values(cmd).find((cmd) => typeof cmd.run === 'function');
169 };
170 const p = require.resolve(path.join(this.commandsDir, ...id.split(':')));
171 this._debug('require', p);
172 let m;
173 try {
174 m = require(p);
175 }
176 catch (error) {
177 if (!opts.must && error.code === 'MODULE_NOT_FOUND')
178 return;
179 throw error;
180 }
181 const cmd = search(m);
182 if (!cmd)
183 return;
184 cmd.id = id;
185 cmd.plugin = this;
186 return cmd;
187 };
188 const cmd = fetch();
189 if (!cmd && opts.must)
190 errors_1.error(`command ${id} not found`);
191 return cmd;
192 }
193 async _manifest(ignoreManifest, errorOnManifestCreate = false) {
194 const readManifest = async (dotfile = false) => {
195 try {
196 const p = path.join(this.root, `${dotfile ? '.' : ''}oclif.manifest.json`);
197 const manifest = await util_2.loadJSON(p);
198 if (!process.env.OCLIF_NEXT_VERSION && manifest.version.split('-')[0] !== this.version.split('-')[0]) {
199 process.emitWarning(`Mismatched version in ${this.name} plugin manifest. Expected: ${this.version} Received: ${manifest.version}\nThis usually means you have an oclif.manifest.json file that should be deleted in development. This file should be automatically generated when publishing.`);
200 }
201 else {
202 this._debug('using manifest from', p);
203 return manifest;
204 }
205 }
206 catch (error) {
207 if (error.code === 'ENOENT') {
208 if (!dotfile)
209 return readManifest(true);
210 }
211 else {
212 this.warn(error, 'readManifest');
213 }
214 }
215 };
216 if (!ignoreManifest) {
217 const manifest = await readManifest();
218 if (manifest)
219 return manifest;
220 }
221 return {
222 version: this.version,
223 // eslint-disable-next-line array-callback-return
224 commands: this.commandIDs.map(id => {
225 try {
226 return [id, command_1.Command.toCached(this.findCommand(id, { must: true }), this)];
227 }
228 catch (error) {
229 const scope = 'toCached';
230 if (Boolean(errorOnManifestCreate) === false)
231 this.warn(error, scope);
232 else
233 throw this.addErrorScope(error, scope);
234 }
235 })
236 .filter((f) => Boolean(f))
237 .reduce((commands, [id, c]) => {
238 commands[id] = c;
239 return commands;
240 }, {}),
241 };
242 }
243 warn(err, scope) {
244 if (this.warned)
245 return;
246 if (typeof err === 'string')
247 err = new Error(err);
248 process.emitWarning(this.addErrorScope(err, scope));
249 }
250 addErrorScope(err, scope) {
251 err.name = `${err.name} Plugin: ${this.name}`;
252 err.detail = util_2.compact([err.detail, `module: ${this._base}`, scope && `task: ${scope}`, `plugin: ${this.name}`, `root: ${this.root}`, 'See more details with DEBUG=*']).join('\n');
253 return err;
254 }
255}
256exports.Plugin = Plugin;