1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const command_1 = require("@anycli/command");
|
4 | const config_1 = require("@anycli/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).uniq().value(); }
|
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) {
|
27 | this.config = config;
|
28 | this.config.engine = this;
|
29 | this.debug = require('debug')(['@anycli/engine', this.config.name].join(':'));
|
30 | this.rootPlugin = await this.loadPlugin({ type: 'core', config, loadDevPlugins: true, useCache: true });
|
31 |
|
32 | const getAllPluginProps = (plugin) => {
|
33 | this.plugins.push(plugin);
|
34 | for (let [hook, hooks] of Object.entries(plugin.hooks)) {
|
35 | this._hooks[hook] = [...this._hooks[hook] || [], ...hooks];
|
36 | }
|
37 | this.topics.push(...plugin.topics);
|
38 | this.commands.push(...plugin.manifest.commands);
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | for (let p of plugin.plugins || []) {
|
47 | getAllPluginProps(p);
|
48 | }
|
49 | };
|
50 | getAllPluginProps(this.rootPlugin);
|
51 | }
|
52 | findCommand(id, must) {
|
53 | const cmd = this.commands.find(c => c.id === id);
|
54 | if (!cmd && must)
|
55 | throw new Error(`command ${id} not found`);
|
56 | return cmd;
|
57 | }
|
58 | findTopic(name, must) {
|
59 | const topic = this.topics.find(t => t.name === name);
|
60 | if (!topic && must)
|
61 | throw new Error(`command ${name} not found`);
|
62 | return topic;
|
63 | }
|
64 | async runHook(event, opts) {
|
65 | this.debug('starting hook', event);
|
66 | await Promise.all((this._hooks[event] || [])
|
67 | .map(async (hook) => {
|
68 | try {
|
69 | this.debug('running hook', event, hook);
|
70 | const m = await util_1.undefault(require(hook));
|
71 | await m(Object.assign({}, opts || {}, { config: this.config }));
|
72 | }
|
73 | catch (err) {
|
74 | if (err.code === 'EEXIT')
|
75 | throw err;
|
76 | cli_ux_1.default.warn(err, { context: { hook: event, module: hook } });
|
77 | }
|
78 | }));
|
79 | this.debug('finished hook', event);
|
80 | }
|
81 | async loadPlugin(opts) {
|
82 | const config = opts.config || await config_1.read(opts);
|
83 | this.debug('loading plugin', config.name);
|
84 | const pjson = config.pjson;
|
85 | const name = pjson.name;
|
86 | const version = pjson.version;
|
87 | const type = opts.type;
|
88 | if (config.pluginsModuleTS || config.hooksTS || config.commandsDirTS) {
|
89 | typescript_1.registerTSNode(this.debug, config.root);
|
90 | }
|
91 | let plugins = [];
|
92 | if (config.pluginsModule) {
|
93 | try {
|
94 | let roots;
|
95 | let fetch = (d) => util_1.undefault(require(d))(this.config);
|
96 | if (config.pluginsModuleTS) {
|
97 | try {
|
98 | roots = await fetch(config.pluginsModuleTS);
|
99 | }
|
100 | catch (err) {
|
101 | cli_ux_1.default.warn(err);
|
102 | }
|
103 | }
|
104 | if (!roots)
|
105 | roots = await fetch(config.pluginsModule);
|
106 | const promises = roots.map((r) => this.loadPlugin(Object.assign({}, r, { useCache: true })).catch(cli_ux_1.default.warn));
|
107 | plugins.push(...await Promise.all(promises));
|
108 | }
|
109 | catch (err) {
|
110 | cli_ux_1.default.warn(err);
|
111 | }
|
112 | }
|
113 | else if (_.isArray(pjson.anycli.plugins)) {
|
114 | const promises = pjson.anycli.plugins.map(p => this.loadPlugin({ root: config.root, type, name: p, useCache: opts.useCache }).catch(cli_ux_1.default.warn));
|
115 | plugins.push(..._(await Promise.all(promises)).compact().flatMap().value());
|
116 | }
|
117 | if (opts.loadDevPlugins && _.isArray(config.pjson.anycli.devPlugins)) {
|
118 | const devPlugins = config.pjson.anycli.devPlugins;
|
119 | this.debug('loading dev plugins', devPlugins);
|
120 | const promises = devPlugins.map(p => this.loadPlugin({ root: config.root, type: 'dev', name: p, useCache: opts.useCache }).catch(cli_ux_1.default.warn));
|
121 | plugins.push(..._(await Promise.all(promises)).compact().flatMap().value());
|
122 | }
|
123 | return {
|
124 | name,
|
125 | version,
|
126 | root: config.root,
|
127 | tag: opts.tag,
|
128 | type,
|
129 | config,
|
130 | hooks: config.hooksTS || config.hooks,
|
131 | topics: [],
|
132 | plugins,
|
133 | manifest: await this.getPluginManifest(config, opts),
|
134 | };
|
135 | }
|
136 | async getPluginManifest(config, opts) {
|
137 | const debug = require('debug')(['@anycli/load', config.name].join(':'));
|
138 | function findCommand(dir, id) {
|
139 | function commandPath(id) {
|
140 | return require.resolve(path.join(dir, ...id.split(':')));
|
141 | }
|
142 | debug('fetching %s from %s', id, dir);
|
143 | const p = commandPath(id);
|
144 | let c = util_1.undefault(require(p));
|
145 | c.id = id;
|
146 | return c;
|
147 | }
|
148 | const rehydrate = (dir, commands) => {
|
149 | return commands.map((cmd) => (Object.assign({}, cmd, { load: async () => findCommand(dir, cmd.id) })));
|
150 | };
|
151 | const fetchFromDir = async () => {
|
152 | const dir = config.commandsDirTS || config.commandsDir;
|
153 | if (!dir)
|
154 | return [];
|
155 | const fetch = async () => {
|
156 | function getCached(c) {
|
157 | const opts = { pluginName: config.name };
|
158 | if (c.convertToCached)
|
159 | return c.convertToCached(opts);
|
160 | return command_1.convertToCached(c, opts);
|
161 | }
|
162 | const fetchCommandIDs = async () => {
|
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 | const commands = (await fetchCommandIDs())
|
176 | .map(id => {
|
177 | try {
|
178 | const cmd = findCommand(dir, id);
|
179 | return getCached(cmd);
|
180 | }
|
181 | catch (err) {
|
182 | cli_ux_1.default.warn(err);
|
183 | }
|
184 | });
|
185 | return _.compact(commands);
|
186 | };
|
187 | let commands;
|
188 | if (opts.useCache) {
|
189 | const getLastUpdated = async () => {
|
190 | try {
|
191 |
|
192 | let files = await globby([`${config.root}/+(src|lib)/**/*.+(js|ts)`, '!**/*.+(d.ts|test.ts|test.js)']);
|
193 | let stats = await Promise.all(files.map(async (f) => {
|
194 | const stat = await fs.stat(f);
|
195 | return [f, stat];
|
196 | }));
|
197 | const max = _.maxBy(stats, '[1].mtime');
|
198 | if (!max)
|
199 | return new Date();
|
200 | this.debug('most recently updated file: %s %o', max[0], max[1].mtime);
|
201 | return max[1].mtime;
|
202 | }
|
203 | catch (err) {
|
204 | cli_ux_1.default.warn(err);
|
205 | return new Date();
|
206 | }
|
207 | };
|
208 | const cacheFile = path.join(this.config.cacheDir, 'commands', opts.type, `${config.name}.json`);
|
209 | let cacheKey = [this.config.version, config.version];
|
210 | const lastUpdated = await getLastUpdated();
|
211 | if (lastUpdated)
|
212 | cacheKey.push(lastUpdated.toISOString());
|
213 | const cache = new cache_1.default(cacheFile, cacheKey.join(':'), config.name);
|
214 | commands = await cache.fetch('commands', fetch);
|
215 | }
|
216 | else {
|
217 | commands = await fetch();
|
218 | }
|
219 | return rehydrate(dir, commands);
|
220 | };
|
221 | const loadFromManifest = async () => {
|
222 | try {
|
223 | const manifest = await fs.readJSON(path.join(config.root, '.anycli.manifest.json'));
|
224 | if (manifest.version !== config.version) {
|
225 | const err = new Error(`Mismatched version in plugin manifest. Expected: ${config.version} Received: ${manifest.version}`);
|
226 | err.code = 'EMISMATCH';
|
227 | throw err;
|
228 | }
|
229 | return rehydrate(config.commandsDir, manifest.commands);
|
230 | }
|
231 | catch (err) {
|
232 | switch (err.code) {
|
233 | case 'ENOENT': return;
|
234 | case 'EMISMATCH': return debug(err);
|
235 | default: cli_ux_1.default.warn(err);
|
236 | }
|
237 | }
|
238 | };
|
239 | return {
|
240 | version: config.version,
|
241 | commands: (await loadFromManifest()) || (await fetchFromDir()),
|
242 | };
|
243 | }
|
244 | }
|
245 | exports.default = Engine;
|