UNPKG

10.6 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const command_1 = require("@anycli/command");
4const config_1 = require("@anycli/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).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 // await this.runHook('legacy', {engine: this})
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 // // add missing topics from commands
40 // for (let c of this._commands) {
41 // let name = c.id!.split(':').slice(0, -1).join(':')
42 // if (!this.topics.find(t => t.name === name)) {
43 // this.topics.push({name})
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 // if (!await fs.pathExists(path.join(plugin.root, '.git'))) return
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}
245exports.default = Engine;