UNPKG

8.91 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const command_1 = require("@dxcli/command");
4const config_1 = require("@dxcli/config");
5const cli_ux_1 = require("cli-ux");
6const fs = require("fs-extra");
7const globby = require("globby");
8const _ = require("lodash");
9const path = require("path");
10const cache_1 = require("./cache");
11const typescript_1 = require("./typescript");
12const util_1 = require("./util");
13class Engine {
14 constructor() {
15 this._plugins = [];
16 this._commands = [];
17 this._topics = [];
18 this._hooks = {};
19 }
20 get plugins() { return this._plugins; }
21 get topics() { return this._topics; }
22 get commands() { return this._commands; }
23 get commandIDs() { return this.commands.map(c => c.id); }
24 get rootTopics() { return this.topics.filter(t => !t.name.includes(':')); }
25 get rootCommands() { return this.commands.filter(c => !c.id.includes(':')); }
26 async load(config, loadOptions = {}) {
27 this.config = Object.assign({}, config, { engine: this });
28 // set global config for plugins to use in any part of their loading
29 if (!global.dxcli)
30 global.dxcli = {};
31 if (!global.dxcli.config)
32 global.dxcli.config = this.config;
33 this.debug = require('debug')(['@dxcli/engine', this.config.name].join(':'));
34 const loadPlugin = async (opts) => {
35 this.debug('loading plugin', opts.name || opts.root);
36 const config = opts.config || await config_1.read(opts);
37 const pjson = config.pjson;
38 const name = pjson.name;
39 const version = pjson.version;
40 const type = opts.type || 'core';
41 const plugin = {
42 name,
43 version,
44 root: config.root,
45 tag: opts.tag,
46 type,
47 config,
48 hooks: config.hooks,
49 commands: [],
50 topics: [],
51 plugins: [],
52 };
53 if (config.pluginsModuleTS || config.hooksTS || config.commandsDirTS) {
54 typescript_1.registerTSNode(this.debug, config.root);
55 }
56 if (config.pluginsModule) {
57 let plugins;
58 let fetch = (d) => util_1.undefault(require(d))(config, loadPlugin);
59 if (config.pluginsModuleTS) {
60 try {
61 plugins = await fetch(config.pluginsModuleTS);
62 }
63 catch (err) {
64 cli_ux_1.default.warn(err);
65 }
66 }
67 if (!plugins)
68 plugins = await fetch(config.pluginsModule);
69 plugin.plugins = plugins;
70 }
71 if (_.isArray(pjson.dxcli.plugins)) {
72 const promises = pjson.dxcli.plugins.map(p => loadPlugin({ root: config.root, type, name: p }).catch(cli_ux_1.default.warn));
73 plugin.plugins = _(await Promise.all(promises)).compact().flatMap().value();
74 }
75 this.plugins.push(plugin);
76 return plugin;
77 };
78 await loadPlugin({ type: 'core', root: config.root });
79 await this.runHook('legacy', { engine: this });
80 // add hooks and topics
81 for (let p of this._plugins) {
82 for (let [hook, hooks] of Object.entries(p.hooks)) {
83 this._hooks[hook] = [...this._hooks[hook] || [], ...hooks];
84 }
85 this._topics.push(...p.topics);
86 }
87 this._commands.push(..._(await Promise.all(this.plugins.map(p => this.loadCommands(p, loadOptions)))).flatMap().value());
88 // add missing topics from commands
89 for (let c of this._commands) {
90 let name = c.id.split(':').slice(0, -1).join(':');
91 if (!this.topics.find(t => t.name === name)) {
92 this.topics.push({ name });
93 }
94 }
95 }
96 findCommand(id, must) {
97 const cmd = this.commands.find(c => c.id === id);
98 if (!cmd && must)
99 throw new Error(`command ${id} not found`);
100 return cmd;
101 }
102 findTopic(name, must) {
103 const topic = this.topics.find(t => t.name === name);
104 if (!topic && must)
105 throw new Error(`command ${name} not found`);
106 return topic;
107 }
108 async runHook(event, opts) {
109 this.debug('starting hook', event);
110 await Promise.all((this._hooks[event] || [])
111 .map(async (hook) => {
112 try {
113 this.debug('running hook', event, hook);
114 const m = await util_1.undefault(require(hook));
115 await m(Object.assign({}, opts || {}, { config: this.config }));
116 }
117 catch (err) {
118 if (err.code === 'EEXIT')
119 throw err;
120 cli_ux_1.default.warn(err, { context: { hook: event, module: hook } });
121 }
122 }));
123 this.debug('finished hook', event);
124 }
125 async loadCommands(plugin, loadOptions) {
126 function getCached(c) {
127 const opts = { plugin };
128 if (c.convertToCached)
129 return c.convertToCached(opts);
130 return command_1.convertToCached(c, opts);
131 }
132 const getLastUpdated = async () => {
133 try {
134 if (loadOptions.resetCache)
135 return new Date();
136 if (!await fs.pathExists(path.join(plugin.root, '.git')))
137 return;
138 let files = await globby([`${plugin.root}/+(src|lib)/**/*.+(js|ts)`, '!**/*.+(d.ts|test.ts|test.js)']);
139 let stats = await Promise.all(files.map(async (f) => {
140 const stat = await fs.stat(f);
141 return [f, stat];
142 }));
143 const max = _.maxBy(stats, '[1].mtime');
144 if (!max)
145 return new Date();
146 this.debug('most recently updated file: %s %o', max[0], max[1].mtime);
147 return max[1].mtime;
148 }
149 catch (err) {
150 cli_ux_1.default.warn(err);
151 return new Date();
152 }
153 };
154 const lastUpdated = await getLastUpdated();
155 const debug = require('debug')(['@dxcli/load', plugin.name].join(':'));
156 const cacheFile = path.join(plugin.config.cacheDir, 'commands', plugin.type, `${plugin.name}.json`);
157 let cacheKey = [plugin.config.version, plugin.version];
158 if (lastUpdated)
159 cacheKey.push(lastUpdated.toISOString());
160 const cache = new cache_1.default(cacheFile, cacheKey.join(':'), plugin.name);
161 async function fetchFromDir(dir) {
162 async function fetchCommandIDs() {
163 function idFromPath(file) {
164 const p = path.parse(file);
165 const topics = p.dir.split('/');
166 let command = p.name !== 'index' && p.name;
167 return _([...topics, command]).compact().join(':');
168 }
169 debug(`loading IDs from ${dir}`);
170 const files = await globby(['**/*.+(js|ts)', '!**/*.+(d.ts|test.ts|test.js)'], { cwd: dir });
171 let ids = files.map(idFromPath);
172 debug('commandIDs dir: %s ids: %s', dir, ids.join(' '));
173 return ids;
174 }
175 function findCommand(id) {
176 function commandPath(id) {
177 return require.resolve(path.join(dir, ...id.split(':')));
178 }
179 debug('fetching %s from %s', id, dir);
180 const p = commandPath(id);
181 let c = util_1.undefault(require(p));
182 c.id = id;
183 return c;
184 }
185 return (await cache.fetch('commands', async () => {
186 const commands = (await fetchCommandIDs())
187 .map(id => {
188 try {
189 const cmd = findCommand(id);
190 return getCached(cmd);
191 }
192 catch (err) {
193 cli_ux_1.default.warn(err);
194 }
195 });
196 return _.compact(commands);
197 }))
198 .map((cmd) => (Object.assign({}, cmd, { load: async () => findCommand(cmd.id) })));
199 }
200 let commands = [];
201 if (plugin.config.commandsDirTS) {
202 try {
203 commands.push(...await fetchFromDir(plugin.config.commandsDirTS));
204 }
205 catch (err) {
206 cli_ux_1.default.warn(err);
207 // debug(err)
208 }
209 }
210 if (plugin.config.commandsDir)
211 commands.push(...await fetchFromDir(plugin.config.commandsDir));
212 return commands;
213 }
214}
215exports.default = Engine;