1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const command_1 = require("@dxcli/command");
|
4 | const config_1 = require("@dxcli/config");
|
5 | const cli_ux_1 = require("cli-ux");
|
6 | const fs = require("fs-extra");
|
7 | const globby = require("globby");
|
8 | const _ = require("lodash");
|
9 | const path = require("path");
|
10 | const cache_1 = require("./cache");
|
11 | const typescript_1 = require("./typescript");
|
12 | const util_1 = require("./util");
|
13 | class 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 |
|
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 |
|
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 |
|
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 |
|
208 | }
|
209 | }
|
210 | if (plugin.config.commandsDir)
|
211 | commands.push(...await fetchFromDir(plugin.config.commandsDir));
|
212 | return commands;
|
213 | }
|
214 | }
|
215 | exports.default = Engine;
|