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