UNPKG

8.7 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 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 // add hooks and topics
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 // add missing topics from commands
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 // debug(err)
203 }
204 }
205 if (plugin.config.commandsDir)
206 commands.push(...await fetchFromDir(plugin.config.commandsDir));
207 return commands;
208 }
209}
210exports.default = Engine;