mixcli 3.0.1 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/cli.ts +253 -0
- package/src/command.ts +451 -0
- package/src/finder.ts +142 -0
- package/src/index.ts +4 -0
- package/src/option.ts +102 -0
- package/src/oslocate.js +148 -0
- package/src/prompt.ts +180 -0
- package/src/utils.ts +128 -0
package/src/command.ts
ADDED
@@ -0,0 +1,451 @@
|
|
1
|
+
import { Command, Option } from "commander";
|
2
|
+
import prompts, { PromptObject } from "prompts";
|
3
|
+
import { MixOption, type MixedOptionParams } from "./option";
|
4
|
+
import { addBuiltInOptions, isEnablePrompts, outputDebug } from "./utils";
|
5
|
+
import path from "node:path";
|
6
|
+
import fs from "node:fs";
|
7
|
+
import type { AsyncFunction } from "flex-tools";
|
8
|
+
|
9
|
+
export type ICommandHookListener = ({
|
10
|
+
args,
|
11
|
+
options,
|
12
|
+
command,
|
13
|
+
}: {
|
14
|
+
args: any[];
|
15
|
+
options: Record<string, any>;
|
16
|
+
command: MixCommand;
|
17
|
+
}) => void | Promise<void>;
|
18
|
+
|
19
|
+
export type BeforeCommandHookListener = ({
|
20
|
+
args,
|
21
|
+
options,
|
22
|
+
command,
|
23
|
+
}: {
|
24
|
+
args: any[];
|
25
|
+
options: Record<string, any>;
|
26
|
+
command: MixCommand;
|
27
|
+
}) => void | Promise<void>;
|
28
|
+
export type AfterCommandHookListener = ({
|
29
|
+
value,
|
30
|
+
args,
|
31
|
+
options,
|
32
|
+
command,
|
33
|
+
}: {
|
34
|
+
value: any;
|
35
|
+
args: any[];
|
36
|
+
options: Record<string, any>;
|
37
|
+
command: MixCommand;
|
38
|
+
}) => void | Promise<void>;
|
39
|
+
|
40
|
+
export interface ActionOptions {
|
41
|
+
id: string;
|
42
|
+
at: "replace" | "before" | "after" | "preappend" | "append" | number;
|
43
|
+
// 函数签名类型,即采用原始的commander的action函数签名,还是mixcli的action函数签名
|
44
|
+
enhance: boolean;
|
45
|
+
}
|
46
|
+
|
47
|
+
export interface ActionRegistry extends Omit<ActionOptions, "at"> {
|
48
|
+
fn: Function;
|
49
|
+
}
|
50
|
+
|
51
|
+
// 原始的Action动作函数
|
52
|
+
export type OriginalAction = (...args: any[]) => void | Promise<void>;
|
53
|
+
// 增强的Action函数签名
|
54
|
+
export type EnhanceAction = ({
|
55
|
+
args,
|
56
|
+
options,
|
57
|
+
value,
|
58
|
+
command,
|
59
|
+
}: {
|
60
|
+
args: any[];
|
61
|
+
options: Record<string, any>;
|
62
|
+
value: any;
|
63
|
+
command: MixCommand;
|
64
|
+
}) => void | Promise<any>;
|
65
|
+
|
66
|
+
// 执行action的返回结果
|
67
|
+
export const BREAK = Symbol("BREAK_ACTION"); // 中止后续的action执行
|
68
|
+
|
69
|
+
export class MixCommand extends Command {
|
70
|
+
private _beforeHooks: [BeforeCommandHookListener, boolean][] = [];
|
71
|
+
private _afterHooks: [AfterCommandHookListener, boolean][] = [];
|
72
|
+
private _customPrompts: PromptObject[] = [];
|
73
|
+
private _optionValues: Record<string, any> = {}; // 命令行输入的选项值
|
74
|
+
private _actions: ActionRegistry[] = []; // 允许多个action
|
75
|
+
private _enable_prompts: boolean = true; // 是否启用交互提示
|
76
|
+
constructor(name?: string) {
|
77
|
+
super(name);
|
78
|
+
const self = this;
|
79
|
+
if (!this.isRoot) addBuiltInOptions(this);
|
80
|
+
this.hook("preAction", async function (this: any) {
|
81
|
+
self._optionValues = self.getOptionValues(this.hookedCommand);
|
82
|
+
try {
|
83
|
+
// @ts-ignore
|
84
|
+
await self.preActionHook.apply(self, arguments);
|
85
|
+
} catch {}
|
86
|
+
});
|
87
|
+
}
|
88
|
+
/**
|
89
|
+
* 是否是根命令
|
90
|
+
*/
|
91
|
+
get isRoot() {
|
92
|
+
return !!!this.parent;
|
93
|
+
}
|
94
|
+
get actions() {
|
95
|
+
return this._actions;
|
96
|
+
}
|
97
|
+
get beforeHooks() {
|
98
|
+
return this._beforeHooks;
|
99
|
+
}
|
100
|
+
get afterHooks() {
|
101
|
+
return this._afterHooks;
|
102
|
+
}
|
103
|
+
get fullname() {
|
104
|
+
let names = [this.name()];
|
105
|
+
let parent = this.parent;
|
106
|
+
while (parent) {
|
107
|
+
if (parent.name() !== "root") {
|
108
|
+
names.unshift(parent.name());
|
109
|
+
}
|
110
|
+
parent = parent.parent;
|
111
|
+
}
|
112
|
+
return names.join(".");
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* 返回根命令
|
117
|
+
*/
|
118
|
+
root() {
|
119
|
+
let root: MixCommand | null | undefined = this;
|
120
|
+
while (root && root.parent != null) {
|
121
|
+
root = root.parent as unknown as MixCommand;
|
122
|
+
}
|
123
|
+
return root;
|
124
|
+
}
|
125
|
+
action(fn: EnhanceAction, options: ActionOptions): this;
|
126
|
+
action(fn: OriginalAction): this;
|
127
|
+
action(fn: OriginalAction): this {
|
128
|
+
const actionFunc = arguments[0];
|
129
|
+
if (arguments.length == 1 && typeof actionFunc == "function") {
|
130
|
+
// 原始方式
|
131
|
+
this._actions.push({
|
132
|
+
id: Math.random().toString(36).substring(2),
|
133
|
+
enhance: false,
|
134
|
+
fn: actionFunc,
|
135
|
+
});
|
136
|
+
} else if (
|
137
|
+
arguments.length == 2 &&
|
138
|
+
typeof actionFunc == "function" &&
|
139
|
+
typeof arguments[1] == "object"
|
140
|
+
) {
|
141
|
+
// 增强模式
|
142
|
+
const actionFn = arguments[0];
|
143
|
+
const actionOpts: ActionOptions = Object.assign({ at: "append" }, arguments[1]);
|
144
|
+
if (actionOpts.at == "replace") this._actions = [];
|
145
|
+
const actionItem = {
|
146
|
+
id: actionOpts.id || Math.random().toString(36).substring(2),
|
147
|
+
enhance: actionOpts.enhance == undefined ? true : actionOpts.enhance,
|
148
|
+
fn: actionFn,
|
149
|
+
} as const;
|
150
|
+
if (typeof actionOpts.at == "number") {
|
151
|
+
this._actions.splice(Number(actionOpts.at), 0, actionItem);
|
152
|
+
} else if (["append", "before"].includes(actionOpts.at)) {
|
153
|
+
this._actions.push(actionItem);
|
154
|
+
} else if (["preappend", "after"].includes(actionOpts.at)) {
|
155
|
+
this._actions.splice(0, 0, actionItem);
|
156
|
+
} else {
|
157
|
+
this._actions.push(actionItem);
|
158
|
+
}
|
159
|
+
} else {
|
160
|
+
console.log("[mixcli] action params error");
|
161
|
+
}
|
162
|
+
return super.action(this.getWrapperedAction());
|
163
|
+
}
|
164
|
+
|
165
|
+
/**
|
166
|
+
* 读取命令配置值,包括父命令提供的配置选项
|
167
|
+
* @param command
|
168
|
+
*/
|
169
|
+
private getOptionValues(command: Command) {
|
170
|
+
let opts = {};
|
171
|
+
let parent: Command | null = command;
|
172
|
+
while (parent) {
|
173
|
+
Object.assign(opts, (parent as MixCommand)._optionValues);
|
174
|
+
parent = parent.parent;
|
175
|
+
}
|
176
|
+
return opts;
|
177
|
+
}
|
178
|
+
/**
|
179
|
+
* 本函数在运行时子类进行action生成该命令的action
|
180
|
+
*/
|
181
|
+
private getWrapperedAction() {
|
182
|
+
return this.wrapperWorkDirsAction(this.wrapperChainActions());
|
183
|
+
}
|
184
|
+
|
185
|
+
/**
|
186
|
+
* 向上查找所有祖先命令
|
187
|
+
*/
|
188
|
+
private getAncestorCommands(): MixCommand[] {
|
189
|
+
let cmds: MixCommand[] = [];
|
190
|
+
let cmd: MixCommand | null = this;
|
191
|
+
while (cmd) {
|
192
|
+
cmd = cmd.parent as MixCommand;
|
193
|
+
if (cmd) {
|
194
|
+
cmds.push(cmd);
|
195
|
+
}
|
196
|
+
}
|
197
|
+
return cmds;
|
198
|
+
}
|
199
|
+
/***
|
200
|
+
* 将所有actions包装成一个链式调用的函数
|
201
|
+
*/
|
202
|
+
private wrapperChainActions() {
|
203
|
+
const self = this;
|
204
|
+
return async function (this: any) {
|
205
|
+
const args = Array.from(arguments); // 原始输入的参数
|
206
|
+
let preValue: any; // 保存上一个action的返回值
|
207
|
+
//解析参数, 0-1个参数为options,最后一个参数为command
|
208
|
+
let actionOpts: Record<string, any> = {},
|
209
|
+
actionArgs: any[] = [],
|
210
|
+
cmd: any;
|
211
|
+
if (args.length >= 2) {
|
212
|
+
cmd = args[args.length - 1]; // 最后一个command
|
213
|
+
actionOpts = args[args.length - 2];
|
214
|
+
actionArgs = args.slice(0, args.length - 2);
|
215
|
+
}
|
216
|
+
await self.executeBeforeHooks({ args: actionArgs, options: actionOpts, command: cmd });
|
217
|
+
try {
|
218
|
+
for (let action of self._actions) {
|
219
|
+
try {
|
220
|
+
if (action.enhance) {
|
221
|
+
// 增强模式
|
222
|
+
outputDebug("执行<{}>: args={}, options={}", () => [
|
223
|
+
self.name(),
|
224
|
+
actionArgs,
|
225
|
+
actionOpts,
|
226
|
+
]);
|
227
|
+
preValue = await action.fn.call(this, {
|
228
|
+
command: cmd,
|
229
|
+
value: preValue,
|
230
|
+
args: actionArgs,
|
231
|
+
options: actionOpts,
|
232
|
+
});
|
233
|
+
} else {
|
234
|
+
// 原始模式
|
235
|
+
preValue = await action.fn.apply(this, args);
|
236
|
+
}
|
237
|
+
if (preValue === BREAK) break;
|
238
|
+
} catch (e) {
|
239
|
+
outputDebug("命令{}的Action({})执行出错:{}", [self.name, action.id, e]);
|
240
|
+
throw e;
|
241
|
+
}
|
242
|
+
}
|
243
|
+
} finally {
|
244
|
+
await self.executeAfterHooks({
|
245
|
+
value: preValue,
|
246
|
+
args: actionArgs,
|
247
|
+
options: actionOpts,
|
248
|
+
command: cmd,
|
249
|
+
});
|
250
|
+
}
|
251
|
+
};
|
252
|
+
}
|
253
|
+
/**
|
254
|
+
* 当传入--work-dirs时用来处理工作目录
|
255
|
+
*/
|
256
|
+
private wrapperWorkDirsAction(fn: AsyncFunction) {
|
257
|
+
const self = this;
|
258
|
+
return async function (this: any) {
|
259
|
+
let workDirs = self._optionValues.workDirs;
|
260
|
+
// 未指定工作目录参数
|
261
|
+
if (!workDirs) {
|
262
|
+
return await fn.apply(this, Array.from(arguments));
|
263
|
+
}
|
264
|
+
if (!Array.isArray(workDirs)) workDirs = workDirs.split(",");
|
265
|
+
workDirs = workDirs.reduce((dirs: any[], dir: string) => {
|
266
|
+
if (typeof dir == "string") dirs.push(...dir.split(","));
|
267
|
+
return dirs;
|
268
|
+
}, []);
|
269
|
+
for (let workDir of workDirs) {
|
270
|
+
const cwd = process.cwd();
|
271
|
+
try {
|
272
|
+
if (!path.isAbsolute(workDir)) workDir = path.join(cwd, workDir);
|
273
|
+
if (fs.existsSync(workDir) && fs.statSync(workDir).isDirectory()) {
|
274
|
+
outputDebug("切换到工作目录:{}", workDir);
|
275
|
+
process.chdir(workDir); // 切换
|
276
|
+
await fn.apply(this, Array.from(arguments));
|
277
|
+
} else {
|
278
|
+
outputDebug("无效的工作目录:{}", workDir);
|
279
|
+
}
|
280
|
+
} catch (e) {
|
281
|
+
throw e;
|
282
|
+
} finally {
|
283
|
+
process.chdir(cwd);
|
284
|
+
}
|
285
|
+
}
|
286
|
+
};
|
287
|
+
}
|
288
|
+
getOption(name: string): MixOption {
|
289
|
+
return this.options.find((option) => option.name() == name) as unknown as MixOption;
|
290
|
+
}
|
291
|
+
/**
|
292
|
+
* 添加一个Before钩子
|
293
|
+
* @param listener
|
294
|
+
* @param scope =false时代表只在本命令执行,=true时代表在本命令及其子命令执行
|
295
|
+
* @returns
|
296
|
+
*/
|
297
|
+
before(listener: BeforeCommandHookListener, scope: boolean = true) {
|
298
|
+
this._beforeHooks.push([listener, scope]);
|
299
|
+
return this;
|
300
|
+
}
|
301
|
+
private async executeBeforeHooks(args: any) {
|
302
|
+
const hooks: [BeforeCommandHookListener, boolean, MixCommand][] = this.beforeHooks.map(
|
303
|
+
([hook, scope]) => [hook, scope, this]
|
304
|
+
);
|
305
|
+
this.getAncestorCommands().forEach((cmd: MixCommand) => {
|
306
|
+
hooks.unshift(
|
307
|
+
...cmd.beforeHooks.map(([hook, scope]) => {
|
308
|
+
return [hook, scope, cmd] as [BeforeCommandHookListener, boolean, MixCommand];
|
309
|
+
})
|
310
|
+
);
|
311
|
+
});
|
312
|
+
for (let [hook, scope, cmd] of hooks) {
|
313
|
+
if (!scope) continue;
|
314
|
+
await hook.call(cmd, args);
|
315
|
+
}
|
316
|
+
}
|
317
|
+
/**
|
318
|
+
* 添加一个After钩子
|
319
|
+
* @param listener
|
320
|
+
* @param scope =false时代表只在本命令执行,=true时代表在本命令及其子命令执行
|
321
|
+
* @returns
|
322
|
+
*/
|
323
|
+
after(listener: AfterCommandHookListener, scope: boolean = true) {
|
324
|
+
this._afterHooks.push([listener, scope]);
|
325
|
+
return this;
|
326
|
+
}
|
327
|
+
private async executeAfterHooks(args: any) {
|
328
|
+
const hooks: [AfterCommandHookListener, boolean, MixCommand][] = this.afterHooks.map(
|
329
|
+
([hook, scope]) => [hook, scope, this]
|
330
|
+
);
|
331
|
+
this.getAncestorCommands().forEach((cmd: MixCommand) => {
|
332
|
+
hooks.push(
|
333
|
+
...cmd.afterHooks.map(([hook, scope]) => {
|
334
|
+
return [hook, scope, cmd] as [BeforeCommandHookListener, boolean, MixCommand];
|
335
|
+
})
|
336
|
+
);
|
337
|
+
});
|
338
|
+
for (let [hook, scope, cmd] of hooks) {
|
339
|
+
if (!scope) continue; //=false时不执行
|
340
|
+
await hook.call(cmd, args);
|
341
|
+
}
|
342
|
+
}
|
343
|
+
private async preActionHook(thisCommand: Command, actionCommand: Command) {
|
344
|
+
if (this.isEnablePrompts()) {
|
345
|
+
// 自动生成提示
|
346
|
+
const questions: PromptObject[] = [
|
347
|
+
...this.generateAutoPrompts(),
|
348
|
+
...this._customPrompts,
|
349
|
+
];
|
350
|
+
// 用户提示
|
351
|
+
if (questions.length > 0) {
|
352
|
+
const results = await prompts(questions);
|
353
|
+
Object.entries(results).forEach(([key, value]) => {
|
354
|
+
thisCommand.setOptionValue(key, value);
|
355
|
+
});
|
356
|
+
}
|
357
|
+
}
|
358
|
+
}
|
359
|
+
|
360
|
+
private isEnablePrompts() {
|
361
|
+
if (isEnablePrompts() === false) {
|
362
|
+
// 命令行参数禁用了提示,优先级最高
|
363
|
+
return false;
|
364
|
+
} else {
|
365
|
+
return this._enable_prompts;
|
366
|
+
}
|
367
|
+
}
|
368
|
+
|
369
|
+
/**
|
370
|
+
* 生成选项自动提示
|
371
|
+
*
|
372
|
+
* @remarks
|
373
|
+
* FlexCli要求所有未提供默认值的Option自动生成提示
|
374
|
+
*
|
375
|
+
* - 未提供默认值,并且是必选的参数Option
|
376
|
+
* - 指定了choices但未提供有效值的Option
|
377
|
+
*
|
378
|
+
*/
|
379
|
+
private generateAutoPrompts(): PromptObject[] {
|
380
|
+
const options = this.options as unknown as MixOption[];
|
381
|
+
const optionPromports = options
|
382
|
+
.filter((option) => !option.hidden && option instanceof MixOption)
|
383
|
+
.map((option) => option.getPrompt(this._optionValues[option.name()]))
|
384
|
+
.filter((prompt) => prompt) as PromptObject[];
|
385
|
+
outputDebug("命令<{}>自动生成{}个选项提示:{}", [
|
386
|
+
this.name(),
|
387
|
+
optionPromports.length,
|
388
|
+
optionPromports.map((prompt) => `${prompt.name}(${prompt.type})`).join(","),
|
389
|
+
]);
|
390
|
+
return optionPromports;
|
391
|
+
}
|
392
|
+
option(flags: string, description?: string | undefined, defaultValue?: any): this;
|
393
|
+
option(flags: string, description?: string | undefined, options?: MixedOptionParams): this {
|
394
|
+
// @ts-ignore
|
395
|
+
const option = new MixOption(...arguments);
|
396
|
+
if (option.required && !this.isEnablePrompts()) option.mandatory = true;
|
397
|
+
return this.addOption(option as unknown as Option);
|
398
|
+
}
|
399
|
+
|
400
|
+
/**
|
401
|
+
* 添加提示
|
402
|
+
*
|
403
|
+
* @remarks
|
404
|
+
*
|
405
|
+
* 添加一些自定义提示
|
406
|
+
*
|
407
|
+
*
|
408
|
+
* @param questions
|
409
|
+
* @param show 是否显示提示信息,auto表示只有在用户没有提供option的值时才显示提示信息,always表示总是显示提示信息,never表示不显示提示信息
|
410
|
+
* @returns
|
411
|
+
*/
|
412
|
+
prompt(questions: PromptObject | PromptObject[]) {
|
413
|
+
this._customPrompts.push(...(Array.isArray(questions) ? questions : [questions]));
|
414
|
+
return this;
|
415
|
+
}
|
416
|
+
/**
|
417
|
+
*
|
418
|
+
* 选择命令并执行
|
419
|
+
*
|
420
|
+
* @remorks
|
421
|
+
*
|
422
|
+
* 当命令具有多个子命令时,并且没有提供默认子命令时,提示用户选择一个子命令
|
423
|
+
*
|
424
|
+
*/
|
425
|
+
async selectCommands() {
|
426
|
+
const choices = this.commands.map((command) => ({
|
427
|
+
title: `${command.description()}(${command.name()})`,
|
428
|
+
value: command.name(),
|
429
|
+
}));
|
430
|
+
const result = await prompts({
|
431
|
+
type: "select",
|
432
|
+
name: "command",
|
433
|
+
message: "请选择命令:",
|
434
|
+
choices,
|
435
|
+
});
|
436
|
+
// 重新解析命令行参数标志,
|
437
|
+
const command = this.commands.find((command) => command.name() === result.command);
|
438
|
+
await command?.parseAsync([result.command], { from: "user" });
|
439
|
+
}
|
440
|
+
/**
|
441
|
+
* 禁用/启用所有提示
|
442
|
+
*/
|
443
|
+
disablePrompts() {
|
444
|
+
this._enable_prompts = false;
|
445
|
+
return this;
|
446
|
+
}
|
447
|
+
enablePrompts() {
|
448
|
+
this._enable_prompts = true;
|
449
|
+
return this;
|
450
|
+
}
|
451
|
+
}
|
package/src/finder.ts
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
import { getPackageRootPath } from 'flex-tools';
|
2
|
+
import type { MixCli } from './cli';
|
3
|
+
import { globSync } from 'glob'
|
4
|
+
import { MixCliCommand } from './cli';
|
5
|
+
import { isDebug, outputDebug } from './utils';
|
6
|
+
import fs from "node:fs"
|
7
|
+
import path from "node:path"
|
8
|
+
import { getPackageJson } from "flex-tools/package/getPackageJson"
|
9
|
+
|
10
|
+
/**
|
11
|
+
*
|
12
|
+
* 在当前工程中查找符合FlexCli.prefix约定的命令
|
13
|
+
*
|
14
|
+
* - 读取当前包的package.json
|
15
|
+
* - 找出所有以cli.prefix开头的依赖
|
16
|
+
* - 加载这些依赖的目录下的匹配cli.pattern的命令
|
17
|
+
* - 加载加载这样命令
|
18
|
+
*
|
19
|
+
*/
|
20
|
+
|
21
|
+
|
22
|
+
export function getMatchedDependencies(this:MixCli,entry:string):string[]{
|
23
|
+
const pacakgeMacher = this.options.include
|
24
|
+
if(!(pacakgeMacher instanceof RegExp)) return []
|
25
|
+
|
26
|
+
// 找出当前包的所有依赖
|
27
|
+
const { dependencies={},devDependencies={},peerDependencies={},optionalDependencies={},bundleDependencies={} } = getPackageJson(entry)
|
28
|
+
const packageNames = [
|
29
|
+
...Object.keys(dependencies),
|
30
|
+
...Object.keys(devDependencies),
|
31
|
+
...Object.keys(peerDependencies),
|
32
|
+
...Object.keys(optionalDependencies),
|
33
|
+
...Object.keys(bundleDependencies)
|
34
|
+
]
|
35
|
+
return packageNames.filter(name=>name!=="@voerka/cli" && pacakgeMacher.test(name))
|
36
|
+
}
|
37
|
+
|
38
|
+
function isMatched(str:string,reg?:string | RegExp | string[] | RegExp[]):boolean{
|
39
|
+
// let regexps:RegExp[]=[]
|
40
|
+
const regexps = reg ? (Array.isArray(reg) ? reg : [reg]) : []
|
41
|
+
return regexps.some(regexp=>{
|
42
|
+
if(typeof regexp === "string"){
|
43
|
+
return (new RegExp(regexp)).test(str)
|
44
|
+
}else if(regexp instanceof RegExp){
|
45
|
+
return regexp.test(str)
|
46
|
+
}else{
|
47
|
+
return false
|
48
|
+
}
|
49
|
+
})
|
50
|
+
}
|
51
|
+
|
52
|
+
export function findCliPaths(this:MixCli,packageName?:string ,entry?:string):string[]{
|
53
|
+
const includeMacher = this.options.include
|
54
|
+
const excludeMacher = this.options.exclude
|
55
|
+
if(!includeMacher) return []
|
56
|
+
const packageRoot = getPackageRootPath(entry || process.cwd())
|
57
|
+
const packagePath = packageName ? path.dirname(require.resolve(packageName,{paths:[packageRoot as string]})) : packageName!
|
58
|
+
|
59
|
+
// 找出当前包的所有依赖
|
60
|
+
const packageNames = getMatchedDependencies.call(this,packagePath)
|
61
|
+
|
62
|
+
const cliDirs:string[]=[]
|
63
|
+
|
64
|
+
if(entry!==undefined) cliDirs.push(path.join(packagePath,this.options.cliDir))
|
65
|
+
packageNames.filter(name=>{
|
66
|
+
return isMatched(name,includeMacher) && !isMatched(name,excludeMacher)
|
67
|
+
})
|
68
|
+
.forEach(name=>{
|
69
|
+
outputDebug("匹配包:{}",`${packageName ? name+" <- "+packageName : name}`)
|
70
|
+
try{
|
71
|
+
const packageEntry = path.dirname(require.resolve(name,{paths:packagePath ? [packagePath] : [process.cwd()]}))
|
72
|
+
const packageCliDir =path.join(packageEntry,this.options.cliDir!)
|
73
|
+
// 查找当前包的所属工程的依赖
|
74
|
+
let dependencies = getMatchedDependencies.call(this,packageEntry)
|
75
|
+
cliDirs.push(...dependencies.reduce<string[]>((result,dependencie)=>{
|
76
|
+
outputDebug("匹配包:{}",`${dependencie} <- ${name}`)
|
77
|
+
result.push(...findCliPaths.call(this,dependencie,packageEntry))
|
78
|
+
return result
|
79
|
+
},[]))
|
80
|
+
if(fs.existsSync(packageCliDir)){
|
81
|
+
cliDirs.push(packageCliDir)
|
82
|
+
}
|
83
|
+
}catch(e:any){
|
84
|
+
outputDebug("解析包<{}>路径出错:{}",[name,e.stack])
|
85
|
+
}
|
86
|
+
})
|
87
|
+
// 由于一些包可能存在循环依赖,所以需要去重
|
88
|
+
return [...new Set(cliDirs)]
|
89
|
+
}
|
90
|
+
|
91
|
+
|
92
|
+
function showError(e:any){
|
93
|
+
if(isDebug()){
|
94
|
+
outputDebug("导入命令<>出错:{}",e.stack)
|
95
|
+
}else{
|
96
|
+
console.error(e)
|
97
|
+
}
|
98
|
+
|
99
|
+
}
|
100
|
+
|
101
|
+
/**
|
102
|
+
*
|
103
|
+
* 扫描当前工程中所有符合条件的命令
|
104
|
+
*
|
105
|
+
* @param cli
|
106
|
+
*
|
107
|
+
*/
|
108
|
+
export async function findCommands(cli:MixCli){
|
109
|
+
const cliDirs = findCliPaths.call(cli)
|
110
|
+
const commands:MixCliCommand[] = []
|
111
|
+
const files = [] as string[]
|
112
|
+
cliDirs.forEach(dir=>{
|
113
|
+
files.push(...globSync("*",{
|
114
|
+
cwd:dir,
|
115
|
+
absolute :true
|
116
|
+
}).filter((file:string)=>(file.endsWith(".js") || file.endsWith(".cjs") || file.endsWith(".mjs")) && !fs.statSync(file).isDirectory()))
|
117
|
+
})
|
118
|
+
for(let file of files){
|
119
|
+
try{
|
120
|
+
outputDebug("导入命令:{}",file)
|
121
|
+
if(file.endsWith(".cjs") || file.endsWith(".js")){
|
122
|
+
commands.push(require(file))
|
123
|
+
}else if(file.endsWith(".mjs")){
|
124
|
+
const cmd = await import(`file://${file}`)
|
125
|
+
commands.push(cmd.default)
|
126
|
+
}
|
127
|
+
}catch(e:any){
|
128
|
+
if(e.code==="ERR_REQUIRE_ESM"){
|
129
|
+
try{
|
130
|
+
const cmd = await import(`file://${file.replace(".js",".mjs")}`)
|
131
|
+
commands.push(cmd.default)
|
132
|
+
}catch(err){
|
133
|
+
showError(err)
|
134
|
+
}
|
135
|
+
}else{
|
136
|
+
showError(e)
|
137
|
+
}
|
138
|
+
}
|
139
|
+
}
|
140
|
+
return commands
|
141
|
+
}
|
142
|
+
|
package/src/index.ts
ADDED
package/src/option.ts
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
import { Option } from 'commander'
|
2
|
+
import { PromptObject } from 'prompts'
|
3
|
+
import { IPromptable, IPromptableOptions, PromptChoice, PromptManager } from './prompt'
|
4
|
+
|
5
|
+
|
6
|
+
export interface MixedOptionParams extends IPromptableOptions{
|
7
|
+
hidden?:boolean
|
8
|
+
defaultDescription?:string // 默认值的描述
|
9
|
+
conflicts?:string | string[]
|
10
|
+
env?:string
|
11
|
+
argParser?:<T>(value: string, previous: T) => T
|
12
|
+
hideHelp?:boolean
|
13
|
+
mandatory?: boolean
|
14
|
+
implies?:{[key:string]:any}
|
15
|
+
}
|
16
|
+
|
17
|
+
|
18
|
+
export class MixOption extends Option implements IPromptable{
|
19
|
+
// 是否提示用户输入
|
20
|
+
prompt?: PromptManager
|
21
|
+
promptChoices?:PromptChoice[]
|
22
|
+
private _validate?: (value: any) => boolean
|
23
|
+
constructor(flags: string, description?: string | undefined,optsOrDefault?:any) {
|
24
|
+
super(flags, description)
|
25
|
+
let params:MixedOptionParams = {}
|
26
|
+
if(arguments.length==3 && typeof arguments[2] == "object"){
|
27
|
+
params = Object.assign({ },arguments[2])
|
28
|
+
}else if(arguments.length==3){
|
29
|
+
params.default = arguments[2]
|
30
|
+
}
|
31
|
+
if(params.prompt===undefined) params.prompt = 'auto'
|
32
|
+
if(params.default) this.default(params.default,params.defaultDescription)
|
33
|
+
if(params.choices) this.choices(params.choices)
|
34
|
+
if(params.conflicts) this.conflicts(params.conflicts)
|
35
|
+
if(params.env) this.env(params.env)
|
36
|
+
if(params.argParser) this.argParser(params.argParser)
|
37
|
+
if(params.hideHelp) this.hideHelp(params.hideHelp)
|
38
|
+
if(params.hidden) this.hidden = params.hidden
|
39
|
+
if(params.mandatory) this.makeOptionMandatory(params.mandatory)
|
40
|
+
if(params.implies) this.implies(params.implies)
|
41
|
+
if(params.optional) this.optional=params.optional
|
42
|
+
if(typeof(params.validate)=='function') this._validate = params.validate.bind(this)
|
43
|
+
if(params.required) {
|
44
|
+
this.required = params.required
|
45
|
+
if(!this._validate ) this._validate = (value:any)=>String(value).length>0
|
46
|
+
}
|
47
|
+
this.prompt = new PromptManager(this as IPromptable,params.prompt)
|
48
|
+
}
|
49
|
+
validate(value: any): boolean {
|
50
|
+
if(typeof(this._validate)=='function'){
|
51
|
+
return this._validate(value)
|
52
|
+
}else{
|
53
|
+
return true
|
54
|
+
}
|
55
|
+
}
|
56
|
+
// @ts-ignore
|
57
|
+
choices(values:(PromptChoice | string)[]){
|
58
|
+
if(!this.promptChoices){
|
59
|
+
this.promptChoices = values.map(choice=>{
|
60
|
+
if(typeof(choice)=='object'){
|
61
|
+
return choice
|
62
|
+
}else{
|
63
|
+
return {title:choice,value:choice}
|
64
|
+
}
|
65
|
+
})
|
66
|
+
}
|
67
|
+
super.choices(this.promptChoices.map((item:any)=>item.value))
|
68
|
+
}
|
69
|
+
|
70
|
+
private resetChoices(){
|
71
|
+
super.choices(this.promptChoices!.map((item:any)=>item.value))
|
72
|
+
}
|
73
|
+
|
74
|
+
addChoice(value:PromptChoice | string){
|
75
|
+
if(!this.promptChoices || !Array.isArray(this.promptChoices)) this.promptChoices = []
|
76
|
+
this.promptChoices!.push(typeof(value)=='string' ? {title:value,value} : value)
|
77
|
+
this.resetChoices()
|
78
|
+
}
|
79
|
+
removeChoice(value:any){
|
80
|
+
this.promptChoices =this.promptChoices?.filter(choice=>choice.value!==value)
|
81
|
+
this.resetChoices()
|
82
|
+
}
|
83
|
+
clearChoice(){
|
84
|
+
this.promptChoices = []
|
85
|
+
this.resetChoices()
|
86
|
+
}
|
87
|
+
|
88
|
+
|
89
|
+
/**
|
90
|
+
* 返回选项的提示对象
|
91
|
+
*
|
92
|
+
* @remarks
|
93
|
+
*
|
94
|
+
*
|
95
|
+
*
|
96
|
+
* @param inputValue
|
97
|
+
* @returns
|
98
|
+
*/
|
99
|
+
getPrompt(inputValue?:any): PromptObject | undefined {
|
100
|
+
return this.prompt?.get(inputValue)
|
101
|
+
}
|
102
|
+
}
|