UNPKG

10.4 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// essentially just "cd .."
34function* up(from) {
35 while (path.dirname(from) !== from) {
36 yield from;
37 from = path.dirname(from);
38 }
39 yield from;
40}
41async function findSourcesRoot(root) {
42 for (const next of up(root)) {
43 const cur = path.join(next, 'package.json');
44 // eslint-disable-next-line no-await-in-loop
45 if (await util_2.exists(cur))
46 return path.dirname(cur);
47 }
48}
49/**
50 * @returns string
51 * @param name string
52 * @param root string
53 * find package root
54 * for packages installed into node_modules this will go up directories until
55 * it finds a node_modules directory with the plugin installed into it
56 *
57 * This is needed because some oclif plugins do not declare the `main` field in their package.json
58 * https://github.com/oclif/config/pull/289#issuecomment-983904051
59 */
60async function findRootLegacy(name, root) {
61 for (const next of up(root)) {
62 let cur;
63 if (name) {
64 cur = path.join(next, 'node_modules', name, 'package.json');
65 // eslint-disable-next-line no-await-in-loop
66 if (await util_2.exists(cur))
67 return path.dirname(cur);
68 try {
69 // eslint-disable-next-line no-await-in-loop
70 const pkg = await util_2.loadJSON(path.join(next, 'package.json'));
71 if (pkg.name === name)
72 return next;
73 }
74 catch (_a) { }
75 }
76 else {
77 cur = path.join(next, 'package.json');
78 // eslint-disable-next-line no-await-in-loop
79 if (await util_2.exists(cur))
80 return path.dirname(cur);
81 }
82 }
83}
84async function findRoot(name, root) {
85 if (name) {
86 let pkgPath;
87 try {
88 pkgPath = util_2.resolvePackage(name, { paths: [root] });
89 }
90 catch (error) { }
91 return pkgPath ? findSourcesRoot(path.dirname(pkgPath)) : findRootLegacy(name, root);
92 }
93 return findSourcesRoot(root);
94}
95class Plugin {
96 // eslint-disable-next-line no-useless-constructor
97 constructor(options) {
98 this.options = options;
99 // static loadedPlugins: {[name: string]: Plugin} = {}
100 this._base = `${_pjson.name}@${_pjson.version}`;
101 this.valid = false;
102 this.alreadyLoaded = false;
103 this.children = [];
104 // eslint-disable-next-line new-cap
105 this._debug = debug_1.default();
106 this.warned = false;
107 }
108 async load() {
109 this.type = this.options.type || 'core';
110 this.tag = this.options.tag;
111 const root = await findRoot(this.options.name, this.options.root);
112 if (!root)
113 throw new Error(`could not find package.json with ${util_1.inspect(this.options)}`);
114 this.root = root;
115 this._debug('reading %s plugin %s', this.type, root);
116 this.pjson = await util_2.loadJSON(path.join(root, 'package.json'));
117 this.name = this.pjson.name;
118 const pjsonPath = path.join(root, 'package.json');
119 if (!this.name)
120 throw new Error(`no name in ${pjsonPath}`);
121 const isProd = hasManifest(path.join(root, 'oclif.manifest.json'));
122 if (!isProd && !this.pjson.files)
123 this.warn(`files attribute must be specified in ${pjsonPath}`);
124 // eslint-disable-next-line new-cap
125 this._debug = debug_1.default(this.name);
126 this.version = this.pjson.version;
127 if (this.pjson.oclif) {
128 this.valid = true;
129 }
130 else {
131 this.pjson.oclif = this.pjson['cli-engine'] || {};
132 }
133 this.hooks = util_2.mapValues(this.pjson.oclif.hooks || {}, i => Array.isArray(i) ? i : [i]);
134 this.manifest = await this._manifest(Boolean(this.options.ignoreManifest), Boolean(this.options.errorOnManifestCreate));
135 this.commands = Object.entries(this.manifest.commands)
136 .map(([id, c]) => (Object.assign(Object.assign({}, c), { load: () => this.findCommand(id, { must: true }) })));
137 this.commands.sort((a, b) => {
138 if (a.id < b.id)
139 return -1;
140 if (a.id > b.id)
141 return 1;
142 return 0;
143 });
144 }
145 get topics() {
146 return topicsToArray(this.pjson.oclif.topics || {});
147 }
148 get commandsDir() {
149 return ts_node_1.tsPath(this.root, this.pjson.oclif.commands);
150 }
151 get commandIDs() {
152 if (!this.commandsDir)
153 return [];
154 let globby;
155 try {
156 const globbyPath = require.resolve('globby', { paths: [this.root, __dirname] });
157 globby = require(globbyPath);
158 }
159 catch (error) {
160 this.warn(error, 'not loading commands, globby not found');
161 return [];
162 }
163 this._debug(`loading IDs from ${this.commandsDir}`);
164 const patterns = [
165 '**/*.+(js|ts|tsx)',
166 '!**/*.+(d.ts|test.ts|test.js|spec.ts|spec.js)?(x)',
167 ];
168 const ids = globby.sync(patterns, { cwd: this.commandsDir })
169 .map(file => {
170 const p = path.parse(file);
171 const topics = p.dir.split('/');
172 const command = p.name !== 'index' && p.name;
173 // support src/commands/index as a "root" command
174 if (!command && this.type === 'core' && p.dir.length === 0 && p.name === 'index')
175 return ROOT_INDEX_CMD_ID;
176 return [...topics, command].filter(f => f).join(':');
177 });
178 this._debug('found commands', ids);
179 return ids;
180 }
181 findCommand(id, opts = {}) {
182 const fetch = () => {
183 if (!this.commandsDir)
184 return;
185 const search = (cmd) => {
186 if (typeof cmd.run === 'function')
187 return cmd;
188 if (cmd.default && cmd.default.run)
189 return cmd.default;
190 return Object.values(cmd).find((cmd) => typeof cmd.run === 'function');
191 };
192 const p = require.resolve(path.join(this.commandsDir, ...id.split(':')));
193 this._debug('require', p);
194 let m;
195 try {
196 m = require(p);
197 }
198 catch (error) {
199 if (!opts.must && error.code === 'MODULE_NOT_FOUND')
200 return;
201 throw error;
202 }
203 const cmd = search(m);
204 if (!cmd)
205 return;
206 cmd.id = id;
207 cmd.plugin = this;
208 return cmd;
209 };
210 const cmd = fetch();
211 if (!cmd && opts.must)
212 errors_1.error(`command ${id} not found`);
213 return cmd;
214 }
215 async _manifest(ignoreManifest, errorOnManifestCreate = false) {
216 const readManifest = async (dotfile = false) => {
217 try {
218 const p = path.join(this.root, `${dotfile ? '.' : ''}oclif.manifest.json`);
219 const manifest = await util_2.loadJSON(p);
220 if (!process.env.OCLIF_NEXT_VERSION && manifest.version.split('-')[0] !== this.version.split('-')[0]) {
221 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.`);
222 }
223 else {
224 this._debug('using manifest from', p);
225 return manifest;
226 }
227 }
228 catch (error) {
229 if (error.code === 'ENOENT') {
230 if (!dotfile)
231 return readManifest(true);
232 }
233 else {
234 this.warn(error, 'readManifest');
235 }
236 }
237 };
238 if (!ignoreManifest) {
239 const manifest = await readManifest();
240 if (manifest)
241 return manifest;
242 }
243 return {
244 version: this.version,
245 // eslint-disable-next-line array-callback-return
246 commands: this.commandIDs.map(id => {
247 try {
248 return [id, command_1.Command.toCached(this.findCommand(id, { must: true }), this)];
249 }
250 catch (error) {
251 const scope = 'toCached';
252 if (Boolean(errorOnManifestCreate) === false)
253 this.warn(error, scope);
254 else
255 throw this.addErrorScope(error, scope);
256 }
257 })
258 .filter((f) => Boolean(f))
259 .reduce((commands, [id, c]) => {
260 commands[id] = c;
261 return commands;
262 }, {}),
263 };
264 }
265 warn(err, scope) {
266 if (this.warned)
267 return;
268 if (typeof err === 'string')
269 err = new Error(err);
270 process.emitWarning(this.addErrorScope(err, scope));
271 }
272 addErrorScope(err, scope) {
273 err.name = `${err.name} Plugin: ${this.name}`;
274 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');
275 return err;
276 }
277}
278exports.Plugin = Plugin;