UNPKG

15.6 kBPlain TextView Raw
1import fs from 'fs';
2import path from 'path';
3import osenv from 'osenv';
4import _ from 'lodash';
5
6import Feflow from '..';
7import { execPlugin } from '../plugin/loadUniversalPlugin';
8import logger from '../logger';
9import { parseYaml, safeDump } from '../../shared/yaml';
10import {
11 UNIVERSAL_MODULES,
12 CACHE_FILE,
13 FEFLOW_ROOT
14} from '../../shared/constant';
15import { getPluginsList } from '../plugin/loadPlugins';
16
17const internalPlugins = {
18 devtool: '@feflow/feflow-plugin-devtool'
19};
20
21export enum COMMAND_TYPE {
22 NATIVE_TYPE = 'native',
23 PLUGIN_TYPE = 'plugin',
24 INTERNAL_PLUGIN_TYPE = 'devtool',
25 UNIVERSAL_PLUGIN_TYPE = 'universal',
26 UNKNOWN_TYPE = 'unknown'
27}
28
29export const LOAD_PLUGIN = 1 << 0;
30export const LOAD_DEVKIT = 1 << 1;
31export const LOAD_UNIVERSAL_PLUGIN = 1 << 2;
32export const LOAD_ALL = LOAD_PLUGIN | LOAD_DEVKIT | LOAD_UNIVERSAL_PLUGIN;
33
34type PluginItem = {
35 commands: Array<{
36 name: string;
37 path?: string;
38 version?: string;
39 }>;
40 path?: string;
41 type: COMMAND_TYPE;
42};
43
44type PluginInfo = {
45 [key: string]: PluginItem;
46};
47
48type PickMap = {
49 [COMMAND_TYPE.PLUGIN_TYPE]?: PluginInfo;
50 [COMMAND_TYPE.UNIVERSAL_PLUGIN_TYPE]?: PluginInfo;
51 [COMMAND_TYPE.NATIVE_TYPE]: PluginInfo;
52 [COMMAND_TYPE.INTERNAL_PLUGIN_TYPE]: PluginInfo;
53};
54
55type Cache = {
56 commandPickerMap: PickMap;
57 version: string;
58};
59
60class TargetPlugin {
61 path: string;
62 type: COMMAND_TYPE;
63 pkg?: string;
64 constructor(type: COMMAND_TYPE, path: string, pkg: string) {
65 this.type = type;
66 this.path = path;
67 this.pkg = pkg;
68 }
69}
70
71class NativePlugin extends TargetPlugin {}
72
73class TargetUniversalPlugin {
74 type: COMMAND_TYPE;
75 version: string;
76 pkg: string;
77 constructor(type: COMMAND_TYPE, version: string, pkg: string) {
78 this.type = type;
79 this.version = version;
80 this.pkg = pkg;
81 }
82}
83
84export class CommandPickConfig {
85 ctx: Feflow;
86 cache: Cache | undefined;
87 lastCommand = '';
88 lastVersion = '';
89 lastStore: Record<string, { pluginName: string }> = {};
90 subCommandMap: { [key: string]: string[] } = {};
91 subCommandMapWithVersion: {
92 commands: Array<{
93 [key: string]: { name: string; version: string };
94 }>;
95 } = { commands: [] };
96 root: string;
97 cacheFilePath: string;
98 cacheVersion = '1.0.0';
99
100 PICK_ORDER = [
101 COMMAND_TYPE.PLUGIN_TYPE,
102 COMMAND_TYPE.UNIVERSAL_PLUGIN_TYPE,
103 COMMAND_TYPE.NATIVE_TYPE,
104 COMMAND_TYPE.INTERNAL_PLUGIN_TYPE
105 ];
106
107 constructor(ctx: Feflow) {
108 this.ctx = ctx;
109 this.root = path.join(osenv.home(), FEFLOW_ROOT);
110 this.cacheFilePath = path.join(this.root, CACHE_FILE);
111 this.cache = this.getCache();
112 if (!this.cache) {
113 this.ctx.logger.debug('command picker is empty');
114 } else {
115 const isCacheExpried = this.cache.version !== this.cacheVersion;
116 if (isCacheExpried) {
117 this.ctx.logger.debug('fef cache is expried, clear invalid cache.');
118 this.cache = {
119 version: this.cacheVersion
120 } as Cache;
121 this.writeCache();
122 }
123 }
124 }
125
126 registSubCommand(
127 type: COMMAND_TYPE,
128 store: Record<string, any>,
129 pluginName: string = COMMAND_TYPE.NATIVE_TYPE,
130 version = 'latest'
131 ) {
132 const newCommands = _.difference(
133 Object.keys(store),
134 Object.keys(this.lastStore)
135 );
136
137 if (!!this.lastCommand) {
138 if (type == COMMAND_TYPE.PLUGIN_TYPE) {
139 // 命令相同的场景,插件提供方变化后,依然可以探测到是新命令
140 const commonCommands = Object.keys(store).filter(
141 item => !newCommands.includes(item)
142 );
143 for (const common of commonCommands) {
144 if (!this.lastStore[common]) continue;
145 if (store[common].pluginName !== this.lastStore[common].pluginName) {
146 newCommands.push(common);
147 }
148 }
149 this.subCommandMap[this.lastCommand] = newCommands;
150 } else {
151 if (!this.subCommandMapWithVersion[this.lastCommand]) {
152 this.subCommandMapWithVersion[this.lastCommand] = {
153 commands: [
154 {
155 name: newCommands[0],
156 version: this.lastVersion
157 }
158 ]
159 };
160 } else {
161 this.subCommandMapWithVersion[this.lastCommand].commands.push({
162 name: newCommands[0],
163 version: this.lastVersion
164 });
165 }
166 }
167 }
168
169 this.lastVersion = version;
170 this.lastCommand = pluginName;
171 this.lastStore = Object.assign({}, store);
172 }
173
174 initCacheFile() {
175 this.cache = {
176 commandPickerMap: this.getAllCommandPickerMap() as PickMap,
177 version: this.cacheVersion
178 };
179 this.writeCache();
180 }
181
182 writeCache(filePath = this.cacheFilePath) {
183 safeDump(this.cache as Cache, filePath);
184 }
185
186 updateCache(type: COMMAND_TYPE) {
187 if (!this.cache?.commandPickerMap) {
188 this.initCacheFile();
189 return;
190 }
191
192 if (type === COMMAND_TYPE.PLUGIN_TYPE) {
193 this.cache.commandPickerMap[type] = this.getPluginMap();
194 } else if (type === COMMAND_TYPE.UNIVERSAL_PLUGIN_TYPE) {
195 this.cache.commandPickerMap[type] = this.getUniversalMap();
196 }
197
198 this.writeCache();
199
200 this.lastStore = {};
201 this.lastCommand = '';
202 }
203
204 getAllCommandPickerMap(): Partial<PickMap> {
205 const commandPickerMap: Partial<PickMap> = {};
206 commandPickerMap[COMMAND_TYPE.NATIVE_TYPE] = this.getNativeMap();
207 commandPickerMap[COMMAND_TYPE.PLUGIN_TYPE] = this.getPluginMap();
208
209 commandPickerMap[
210 COMMAND_TYPE.INTERNAL_PLUGIN_TYPE
211 ] = this.getInternalPluginMap();
212
213 commandPickerMap[
214 COMMAND_TYPE.UNIVERSAL_PLUGIN_TYPE
215 ] = this.getUniversalMap();
216
217 return commandPickerMap;
218 }
219
220 getNativeMap(): PluginInfo {
221 const nativePath = path.join(__dirname, '../native');
222 const nativeMap: PluginInfo = {};
223 fs.readdirSync(nativePath)
224 .filter(file => {
225 return file.endsWith('.js');
226 })
227 .forEach(file => {
228 const command = file.split('.')[0];
229 // 通过缓存路径的方式并不是一个值得主张的方案,例如在我们使用webpack构建单文件时这个机制会成为束缚
230 // 缓存绝对路径更不提倡,当客户端node切换不同版本时,绝对路径将导致异常
231 // 此处将其变更为相对路径,暂时解决版本切换的问题
232 // 另外值得讨论的是,cache逻辑本身不应该阻塞正常业务流程,但目前cache带来的问题反而比主逻辑还多,这是很不健康的现象
233 nativeMap[command] = {
234 commands: [
235 {
236 name: command
237 }
238 ],
239 path: file,
240 type: COMMAND_TYPE.NATIVE_TYPE
241 };
242 });
243 return nativeMap;
244 }
245
246 getInternalPluginMap(): PluginInfo {
247 const devtool: PluginInfo = {};
248 for (const command of Object.keys(internalPlugins)) {
249 devtool[command] = {
250 path: internalPlugins[command],
251 type: COMMAND_TYPE.INTERNAL_PLUGIN_TYPE,
252 commands: [{ name: 'devtool' }]
253 };
254 }
255 return devtool;
256 }
257
258 getPluginMap(): PluginInfo {
259 const plugin: PluginInfo = {};
260 const [err, pluginList] = getPluginsList(this.ctx);
261 const home = path.join(osenv.home(), FEFLOW_ROOT);
262
263 if (!Object.keys(this.subCommandMap).length) {
264 return plugin;
265 }
266
267 if (!err) {
268 for (const pluginNpm of pluginList) {
269 const pluginPath = path.join(home, 'node_modules', pluginNpm);
270 // TODO
271 // read plugin command from the key which from its package.json
272 plugin[pluginNpm] = {
273 type: COMMAND_TYPE.PLUGIN_TYPE,
274 commands:
275 this.subCommandMap[pluginNpm] &&
276 this.subCommandMap[pluginNpm].map((cmd: string) => ({
277 name: cmd
278 })),
279 path: pluginPath
280 };
281 }
282 } else {
283 this.ctx.logger.debug('picker load plugin failed', err);
284 }
285 return plugin;
286 }
287
288 getUniversalMap(): PluginInfo {
289 const unversalPlugin: PluginInfo = {};
290 for (const pkg of Object.keys(this.subCommandMapWithVersion)) {
291 if (!pkg) continue;
292 const plugin = this.subCommandMapWithVersion[pkg];
293 unversalPlugin[pkg] = {
294 ...plugin,
295 type: COMMAND_TYPE.UNIVERSAL_PLUGIN_TYPE
296 };
297 }
298 return unversalPlugin;
299 }
300
301 getCache(): Cache | undefined {
302 const { cacheFilePath } = this;
303 let localCache: Cache | undefined;
304 try {
305 localCache = parseYaml(cacheFilePath) as Cache;
306 } catch (error) {
307 this.ctx.logger.debug('.feflowCache.yml parse err ', error);
308 }
309 return localCache;
310 }
311
312 removeCache(name: string) {
313 if (!this.cache) return;
314 const commandPickerMap = this.cache.commandPickerMap;
315 let targetPath = { type: '', plugin: '' };
316
317 for (const type of this.PICK_ORDER) {
318 const pluginsInType = commandPickerMap[type];
319 if (!pluginsInType) continue;
320 for (const plugin of Object.keys(pluginsInType as PluginInfo)) {
321 if (name === plugin) {
322 targetPath = {
323 type,
324 plugin
325 };
326 }
327 }
328
329 if (targetPath.type) {
330 break;
331 }
332 }
333 if (targetPath.type && targetPath.plugin) {
334 delete this.cache.commandPickerMap[targetPath.type][targetPath.plugin];
335 this.writeCache();
336 }
337 }
338
339 // 获取命令的缓存目录
340 getCommandPath(cmd: string): TargetPlugin | TargetUniversalPlugin {
341 let target:
342 | TargetPlugin
343 | TargetUniversalPlugin = new TargetUniversalPlugin(
344 COMMAND_TYPE.UNKNOWN_TYPE,
345 '',
346 ''
347 );
348
349 if (!this.cache?.commandPickerMap) return target;
350 const commandPickerMap = this.cache.commandPickerMap;
351
352 let cmdList: Array<TargetPlugin | TargetUniversalPlugin> = [];
353
354 for (const type of this.PICK_ORDER) {
355 const pluginsInType = commandPickerMap[type];
356 if (!pluginsInType) continue;
357 for (const plugin of Object.keys(pluginsInType as PluginInfo)) {
358 const { commands, path, type } = pluginsInType[plugin] as PluginItem;
359 commands?.forEach(({ name, path: cmdPath, version }) => {
360 if (cmd === name) {
361 if (type === COMMAND_TYPE.UNIVERSAL_PLUGIN_TYPE) {
362 target = new TargetUniversalPlugin(
363 type,
364 version as string,
365 plugin
366 );
367 } else if (type === COMMAND_TYPE.NATIVE_TYPE) {
368 target = new NativePlugin(
369 type,
370 (cmdPath || path) as string,
371 COMMAND_TYPE.NATIVE_TYPE
372 );
373 } else {
374 target = new TargetPlugin(
375 type,
376 (cmdPath || path) as string,
377 plugin
378 );
379 }
380 cmdList.push(_.cloneDeep(target));
381 }
382 });
383 }
384 }
385
386 const { args } = this.ctx;
387 if (cmdList.length >= 2) {
388 if (!args.pick) {
389 this.ctx.logger.debug(`
390 当前命令(${cmd})出现冲突, 如果如果想要执行其他插件,请使用--pick参数指明
391 例如: fef doctor --pick native 或者 fef doctor --pick @tencent/feflow-plugin-raft`);
392 } else {
393 cmdList = cmdList.filter(({ pkg }) => {
394 return pkg === args.pick;
395 });
396 }
397 }
398
399 return cmdList[0];
400 }
401}
402
403export default class CommandPicker {
404 root: string;
405 cmd: string;
406 ctx: any;
407 isHelp: boolean;
408 cacheController: CommandPickConfig;
409 SUPPORT_TYPE = [
410 COMMAND_TYPE.NATIVE_TYPE,
411 COMMAND_TYPE.PLUGIN_TYPE,
412 COMMAND_TYPE.UNIVERSAL_PLUGIN_TYPE,
413 COMMAND_TYPE.INTERNAL_PLUGIN_TYPE
414 ];
415
416 homeRunCmd = ['help', 'list'];
417
418 constructor(ctx: any, cmd = 'help') {
419 this.root = ctx.root;
420 this.ctx = ctx;
421 this.cmd = cmd;
422 this.isHelp = cmd === 'help' || ctx.args.h || ctx.args.help;
423 this.cacheController = new CommandPickConfig(ctx);
424 }
425
426 loadHelp() {
427 this.cmd = 'help';
428 this.pickCommand();
429 }
430
431 isAvailable() {
432 const tartgetCommand = this.cacheController.getCommandPath(this.cmd) || {};
433 const { type } = tartgetCommand;
434
435 if (type === COMMAND_TYPE.UNIVERSAL_PLUGIN_TYPE) {
436 const { version, pkg } = tartgetCommand as TargetUniversalPlugin;
437 const pkgPath = path.join(this.ctx.universalModules, `${pkg}@${version}`);
438 const pathExists = fs.existsSync(pkgPath);
439 return !this.isHelp && pathExists && !!version;
440 } else if (type === COMMAND_TYPE.PLUGIN_TYPE) {
441 const { path } = tartgetCommand as TargetPlugin;
442 const pathExists = fs.existsSync(path);
443 const isCachType = this.SUPPORT_TYPE.includes(type);
444 return !this.isHelp && !!pathExists && isCachType;
445 } else if (type === COMMAND_TYPE.NATIVE_TYPE) {
446 if (!this.homeRunCmd.includes(this.cmd)) {
447 return true;
448 }
449 }
450
451 return false;
452 }
453
454 checkCommand() {
455 const fn = this.ctx?.commander.get(this.cmd);
456 if (!fn) {
457 this.loadHelp();
458 }
459 }
460
461 getCommandSource(path: string): string {
462 const reg = /node_modules\/(.*)/;
463 const commandSource = (reg.exec(path) || [])[1];
464 return commandSource;
465 }
466
467 pickCommand() {
468 const tartgetCommand = this.cacheController.getCommandPath(this.cmd) || {};
469 const { type } = tartgetCommand;
470 const pluginLogger = logger({
471 debug: Boolean(this.ctx.args.debug),
472 silent: Boolean(this.ctx.args.silent),
473 name: tartgetCommand.pkg
474 });
475 this.ctx.logger.debug('pick command type: ', type);
476 if (!this.SUPPORT_TYPE.includes(type)) {
477 return this.ctx.logger.warn(
478 `this kind of command is not supported in command picker, ${type}`
479 );
480 }
481 if (type === COMMAND_TYPE.UNIVERSAL_PLUGIN_TYPE) {
482 const { version, pkg } = tartgetCommand as TargetUniversalPlugin;
483 execPlugin(
484 Object.assign({}, this.ctx, { logger: pluginLogger }),
485 pkg,
486 version
487 );
488 } else {
489 let commandPath = '';
490 if (tartgetCommand instanceof TargetPlugin) {
491 commandPath = tartgetCommand.path;
492 }
493 // native命令跟node版本挂钩,需要解析到具体node版本下路径
494 if (type === 'native') {
495 // 兼容原来的绝对路径形式
496 if (path.isAbsolute(commandPath)) {
497 commandPath = path.basename(commandPath);
498 }
499 commandPath = path.join(__dirname, '../native', commandPath);
500 }
501 const commandSource =
502 this.getCommandSource(commandPath) || COMMAND_TYPE.NATIVE_TYPE;
503 this.ctx.logger.debug('pick command path: ', commandPath);
504 this.ctx.logger.debug('pick command source: ', commandSource);
505
506 try {
507 this.ctx?.reporter?.setCommandSource(commandSource);
508 require(commandPath)(
509 Object.assign({}, this.ctx, { logger: pluginLogger })
510 );
511 } catch (error) {
512 this.ctx.fefError.printError(error, 'command load failed: %s');
513 }
514 }
515 }
516
517 getCmdInfo(): { path: string; type: COMMAND_TYPE } {
518 const tartgetCommand = this.cacheController.getCommandPath(this.cmd) || {};
519 const { type } = tartgetCommand;
520 const cmdInfo: { path: string; type: COMMAND_TYPE } = {
521 type,
522 path: ''
523 };
524
525 if (type === COMMAND_TYPE.PLUGIN_TYPE) {
526 cmdInfo.path = (tartgetCommand as TargetPlugin).path;
527 } else if (type === COMMAND_TYPE.UNIVERSAL_PLUGIN_TYPE) {
528 const { pkg, version } = tartgetCommand as TargetUniversalPlugin;
529 cmdInfo.path = path.join(
530 this.ctx.root,
531 UNIVERSAL_MODULES,
532 `${pkg}@${version}`
533 );
534 } else {
535 cmdInfo.path = this.ctx.root;
536 }
537
538 return cmdInfo;
539 }
540
541 getLoadOrder() {
542 return LOAD_ALL;
543 }
544}