@mpis/run 0.0.1
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/LICENSE +21 -0
- package/README.md +102 -0
- package/commands.schema.json +114 -0
- package/config/rig.json +5 -0
- package/lib/autoindex.d.ts +7 -0
- package/lib/autoindex.d.ts.map +1 -0
- package/lib/autoindex.js +14 -0
- package/lib/autoindex.js.map +1 -0
- package/lib/bin.d.ts +2 -0
- package/lib/bin.d.ts.map +1 -0
- package/lib/bin.js +231 -0
- package/lib/bin.js.map +1 -0
- package/lib/common/args.d.ts +12 -0
- package/lib/common/args.d.ts.map +1 -0
- package/lib/common/args.js +55 -0
- package/lib/common/args.js.map +1 -0
- package/lib/common/config-file.d.ts +14 -0
- package/lib/common/config-file.d.ts.map +1 -0
- package/lib/common/config-file.js +144 -0
- package/lib/common/config-file.js.map +1 -0
- package/lib/common/paths.d.ts +3 -0
- package/lib/common/paths.d.ts.map +1 -0
- package/lib/common/paths.js +13 -0
- package/lib/common/paths.js.map +1 -0
- package/lib/common/stdin.d.ts +9 -0
- package/lib/common/stdin.d.ts.map +1 -0
- package/lib/common/stdin.js +53 -0
- package/lib/common/stdin.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/loader/bin.devel.js +8 -0
- package/loader/bin.js +8 -0
- package/package.json +40 -0
- package/src/autoindex.ts +18 -0
- package/src/bin.ts +268 -0
- package/src/common/args.ts +67 -0
- package/src/common/config-file.ts +198 -0
- package/src/common/paths.ts +15 -0
- package/src/common/stdin.ts +65 -0
- package/src/tsconfig.json +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mpis/run",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"bin": {
|
|
7
|
+
"run": "./loader/bin.devel.js",
|
|
8
|
+
"mpis-run": "./loader/bin.devel.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"execa": "^9.6.0",
|
|
15
|
+
"source-map-support": "^0.5.21",
|
|
16
|
+
"split-cmd": "^1.1.0",
|
|
17
|
+
"@build-script/rushstack-config-loader": "^0.0.20",
|
|
18
|
+
"@idlebox/args": "^0.0.9",
|
|
19
|
+
"@idlebox/node": "^1.4.7",
|
|
20
|
+
"@idlebox/logger": "^0.0.1",
|
|
21
|
+
"@idlebox/common": "^1.4.14",
|
|
22
|
+
"@mpis/client": "^0.0.1",
|
|
23
|
+
"@mpis/server": "^0.0.1",
|
|
24
|
+
"@mpis/shared": "^0.0.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^24.0.14",
|
|
28
|
+
"@build-script/single-dog-asset": "^1.0.35",
|
|
29
|
+
"@idlebox/esbuild-executer": "^0.0.2",
|
|
30
|
+
"@internal/local-rig": "^1.0.1"
|
|
31
|
+
},
|
|
32
|
+
"decoupledDependencies": "*",
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "node loader/bin.devel.js build",
|
|
35
|
+
"watch": "node loader/bin.devel.js watch",
|
|
36
|
+
"clean": "node loader/bin.devel.js clean",
|
|
37
|
+
"prepublishHook": "internal-prepublish-hook",
|
|
38
|
+
"lint": "internal-lint"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/autoindex.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// DO NOT EDIT THIS FILE
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
/* eslint-disable */
|
|
4
|
+
|
|
5
|
+
/* common/args.ts */
|
|
6
|
+
// Identifiers
|
|
7
|
+
export { printUsage } from "./common/args.js";
|
|
8
|
+
/* common/paths.ts */
|
|
9
|
+
// Identifiers
|
|
10
|
+
export { projectRoot } from "./common/paths.js";
|
|
11
|
+
export { selfRoot } from "./common/paths.js";
|
|
12
|
+
/* common/config-file.ts */
|
|
13
|
+
// Identifiers
|
|
14
|
+
export type { ICommand } from "./common/config-file.js";
|
|
15
|
+
export type { IConfigFile } from "./common/config-file.js";
|
|
16
|
+
export { loadConfigFile } from "./common/config-file.js";
|
|
17
|
+
/* bin.ts */
|
|
18
|
+
// Identifiers
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { humanDate, prettyFormatError, registerGlobalLifecycle, toDisposable } from '@idlebox/common';
|
|
2
|
+
import { logger } from '@idlebox/logger';
|
|
3
|
+
import { registerNodejsExitHandler } from '@idlebox/node';
|
|
4
|
+
import { channelClient } from '@mpis/client';
|
|
5
|
+
import { CompileError, ModeKind, ProcessIPCClient, WorkersManager } from '@mpis/server';
|
|
6
|
+
import { rmSync } from 'node:fs';
|
|
7
|
+
import { context, parseCliArgs } from './common/args.js';
|
|
8
|
+
import { loadConfigFile } from './common/config-file.js';
|
|
9
|
+
import { projectRoot } from './common/paths.js';
|
|
10
|
+
import { initializeStdin, registerCommand } from './common/stdin.js';
|
|
11
|
+
|
|
12
|
+
registerNodejsExitHandler();
|
|
13
|
+
|
|
14
|
+
parseCliArgs();
|
|
15
|
+
|
|
16
|
+
const start = Date.now();
|
|
17
|
+
registerGlobalLifecycle(
|
|
18
|
+
toDisposable(() => {
|
|
19
|
+
logger.info`Operation completed in ${humanDate.delta(Date.now() - start)} (${process.exitCode ? 'failed' : 'success'}).`;
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
registerCommand({
|
|
24
|
+
name: ['status', 's'],
|
|
25
|
+
description: '显示当前状态',
|
|
26
|
+
callback: () => reprintWatchModeError(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
process.title = `MpisRun`;
|
|
30
|
+
|
|
31
|
+
logger.info`Running command "${context.command}" in ${projectRoot}`;
|
|
32
|
+
|
|
33
|
+
const defaultNoClear = logger.debug.isEnabled;
|
|
34
|
+
let workersManager: WorkersManager;
|
|
35
|
+
|
|
36
|
+
const config = loadConfigFile(context.watchMode);
|
|
37
|
+
logger.verbose`loaded config file: ${config}`;
|
|
38
|
+
const errors = new Map<ProcessIPCClient, Error | null>();
|
|
39
|
+
|
|
40
|
+
switch (context.command) {
|
|
41
|
+
case 'clean':
|
|
42
|
+
executeClean();
|
|
43
|
+
break;
|
|
44
|
+
case 'build':
|
|
45
|
+
{
|
|
46
|
+
if (context.withCleanup) executeClean();
|
|
47
|
+
|
|
48
|
+
await executeBuild();
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
case 'watch':
|
|
52
|
+
initializeStdin();
|
|
53
|
+
await executeBuild();
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// channelClient.displayName = `MpisRun`;
|
|
58
|
+
|
|
59
|
+
async function executeBuild() {
|
|
60
|
+
workersManager = new WorkersManager(context.watchMode ? ModeKind.Watch : ModeKind.Build);
|
|
61
|
+
|
|
62
|
+
initializeWorkers();
|
|
63
|
+
|
|
64
|
+
workersManager.onTerminate((w) => {
|
|
65
|
+
if (!ProcessIPCClient.is(w)) {
|
|
66
|
+
logger.fatal`worker "${w._id}" is not a ProcessIPCClient, this is a bug.`;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const times = `(+${humanDate.delta(w.time.executeEnd! - w.time.executeStart!)})`;
|
|
71
|
+
|
|
72
|
+
if (context.watchMode) {
|
|
73
|
+
printFailedRunError(w, `unexpected exit in watch mode ${times}`);
|
|
74
|
+
} else if (!w.isSuccess) {
|
|
75
|
+
printFailedRunError(w, `failed to execute ${times}`);
|
|
76
|
+
} else {
|
|
77
|
+
logger.success`"${w._id}" successfully finished ${times}.`;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
workersManager.finalize();
|
|
82
|
+
|
|
83
|
+
channelClient.start();
|
|
84
|
+
|
|
85
|
+
if (context.breakMode) {
|
|
86
|
+
logger.warn`Break mode enabled, waiting for input command...`;
|
|
87
|
+
addDebugCommand();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
logger.verbose`Workers initialized, starting execution...`;
|
|
91
|
+
await workersManager.startup();
|
|
92
|
+
|
|
93
|
+
reprintWatchModeError();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function executeClean() {
|
|
97
|
+
for (const folder of config.clean) {
|
|
98
|
+
logger.log` * removing folder: ${folder}`;
|
|
99
|
+
rmSync(folder, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
logger.success`Cleaned up ${config.clean.length} folders.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function initializeWorkers() {
|
|
105
|
+
let last: ProcessIPCClient | undefined;
|
|
106
|
+
for (const title of config.buildTitles) {
|
|
107
|
+
const cmds = config.build.get(title);
|
|
108
|
+
if (!cmds) throw logger.fatal`program state error, no build command "${title}"`;
|
|
109
|
+
|
|
110
|
+
if (!cmds.env['DEBUG']) cmds.env['DEBUG'] = '';
|
|
111
|
+
if (!cmds.env['DEBUG_LEVEL']) cmds.env['DEBUG_LEVEL'] = '';
|
|
112
|
+
const worker = new ProcessIPCClient(title.replace(/\s+/g, ''), cmds.command, cmds.cwd, cmds.env);
|
|
113
|
+
|
|
114
|
+
for (const path of config.additionalPaths) {
|
|
115
|
+
worker.pathvar.add(path);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const cmd0 = typeof cmds.command === 'string' ? cmds.command.split(' ')[0] : cmds.command[0];
|
|
119
|
+
worker.displayTitle = `run:${cmd0}`;
|
|
120
|
+
|
|
121
|
+
workersManager.addWorker(worker, last ? [last._id] : []);
|
|
122
|
+
|
|
123
|
+
let nodeFirstTime = true;
|
|
124
|
+
worker.onFailure((e) => {
|
|
125
|
+
errors.set(worker, e);
|
|
126
|
+
reprintWatchModeError(nodeFirstTime);
|
|
127
|
+
nodeFirstTime = false;
|
|
128
|
+
sendStatus();
|
|
129
|
+
});
|
|
130
|
+
worker.onSuccess(() => {
|
|
131
|
+
errors.set(worker, null);
|
|
132
|
+
if (nodeFirstTime) {
|
|
133
|
+
nodeFirstTime = false;
|
|
134
|
+
} else {
|
|
135
|
+
reprintWatchModeError();
|
|
136
|
+
}
|
|
137
|
+
sendStatus();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
last = worker;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function printFailedRunError(worker: ProcessIPCClient, message: string) {
|
|
145
|
+
if (context.watchMode) process.stderr.write('\x1Bc');
|
|
146
|
+
|
|
147
|
+
const text = worker.outputStream.toString().trimEnd();
|
|
148
|
+
|
|
149
|
+
if (text) {
|
|
150
|
+
console.error(
|
|
151
|
+
'\n\x1B[48;5;1m%s\r \x1B[0;38;5;9;1m %s \x1B[0m',
|
|
152
|
+
' '.repeat(process.stderr.columns || 80),
|
|
153
|
+
`below is output of ${worker._id}`,
|
|
154
|
+
);
|
|
155
|
+
console.error(text);
|
|
156
|
+
|
|
157
|
+
console.error(
|
|
158
|
+
'\x1B[48;5;1m%s\r \x1B[0;38;5;9;1m %s \x1B[0m\n',
|
|
159
|
+
' '.repeat(process.stderr.columns || 80),
|
|
160
|
+
`ending output of ${worker._id}`,
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
console.error(
|
|
164
|
+
'\n\x1B[48;5;1m%s\r \x1B[0;38;5;9;1m %s \x1B[0m',
|
|
165
|
+
' '.repeat(process.stderr.columns || 80),
|
|
166
|
+
`no output from ${worker._id}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.error(workersManager.formatDebugGraph());
|
|
171
|
+
logger.fatal`"${worker._id}" ${message}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function reprintWatchModeError(noClear?: boolean) {
|
|
175
|
+
if (context.watchMode) {
|
|
176
|
+
if (!noClear && !defaultNoClear) process.stderr.write('\x1Bc');
|
|
177
|
+
}
|
|
178
|
+
console.error(workersManager.formatDebugList());
|
|
179
|
+
printAllErrors();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function addDebugCommand() {
|
|
183
|
+
registerCommand({
|
|
184
|
+
name: ['continue', 'c'],
|
|
185
|
+
description: '开始执行',
|
|
186
|
+
callback: () => {
|
|
187
|
+
workersManager.startup();
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
registerCommand({
|
|
191
|
+
name: ['debug'],
|
|
192
|
+
description: '切换调试模式(仅在启动前有效)',
|
|
193
|
+
callback: (text: string) => {
|
|
194
|
+
const [_, index, on_off] = text.split(/\s+/);
|
|
195
|
+
const list: ProcessIPCClient[] = workersManager.allWorkers as ProcessIPCClient[];
|
|
196
|
+
const worker = list[Number(index)];
|
|
197
|
+
if (!worker) {
|
|
198
|
+
logger.error`worker index out of range: ${index}`;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (on_off === 'on') {
|
|
202
|
+
worker.env['DEBUG'] = '*,-executer:*,-dispose:*';
|
|
203
|
+
worker.env['DEBUG_LEVEL'] = 'verbose';
|
|
204
|
+
logger.success`debug mode enabled for worker "${worker._id}"`;
|
|
205
|
+
} else if (on_off === 'off') {
|
|
206
|
+
worker.env['DEBUG'] = '';
|
|
207
|
+
worker.env['DEBUG_LEVEL'] = '';
|
|
208
|
+
logger.success`debug mode disabled for worker "${worker._id}"`;
|
|
209
|
+
} else {
|
|
210
|
+
logger.error`invalid argument: ${text}`;
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
// registerCommand({
|
|
215
|
+
// name: ['print', 'p'],
|
|
216
|
+
// description: '显示命令执行输出',
|
|
217
|
+
// callback: () => {
|
|
218
|
+
// },
|
|
219
|
+
// });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sendStatus() {
|
|
223
|
+
const noError = errors.values().every((e) => !e);
|
|
224
|
+
if (noError) {
|
|
225
|
+
channelClient.success(`All workers completed successfully.`);
|
|
226
|
+
} else {
|
|
227
|
+
const errorCnt = errors.values().filter((e) => !!e);
|
|
228
|
+
channelClient.failed(`${errorCnt} (of ${workersManager.size}) workers error.`, formatAllErrors());
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function formatAllErrors() {
|
|
233
|
+
const lines: string[] = [];
|
|
234
|
+
const colorEnabled = logger.colorEnabled;
|
|
235
|
+
let index = 0;
|
|
236
|
+
for (const [worker, error] of errors) {
|
|
237
|
+
if (error === null) continue;
|
|
238
|
+
|
|
239
|
+
index++;
|
|
240
|
+
|
|
241
|
+
let tag = '';
|
|
242
|
+
if (error.name !== 'Error') {
|
|
243
|
+
tag = ` (${error.name})`;
|
|
244
|
+
}
|
|
245
|
+
const banner = colorEnabled ? `\x1B[48;5;9m ERROR ${index} \x1B[0m` : `ERROR ${index}`;
|
|
246
|
+
lines.push(`\n${banner}${tag} ${worker._id}`);
|
|
247
|
+
if (error instanceof CompileError) {
|
|
248
|
+
lines.push(error.toString());
|
|
249
|
+
} else if (error instanceof Error) {
|
|
250
|
+
lines.push(prettyFormatError(error));
|
|
251
|
+
} else {
|
|
252
|
+
lines.push(`can not handle error: ${error}`);
|
|
253
|
+
}
|
|
254
|
+
lines.push(`\n${banner} ${worker._id}`);
|
|
255
|
+
}
|
|
256
|
+
return lines.join('\n');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function printAllErrors() {
|
|
260
|
+
const numFailed = [...errors.values().filter((e) => !!e)].length;
|
|
261
|
+
if (numFailed !== 0) {
|
|
262
|
+
console.error(formatAllErrors());
|
|
263
|
+
|
|
264
|
+
logger.error(`💥 ${numFailed} of ${workersManager.size} worker failed`);
|
|
265
|
+
} else {
|
|
266
|
+
logger.success(`✅ no error in ${workersManager.size} workers`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { argv } from '@idlebox/args/default';
|
|
2
|
+
import { createRootLogger, EnableLogLevel, logger } from '@idlebox/logger';
|
|
3
|
+
|
|
4
|
+
export function printUsage() {
|
|
5
|
+
console.log('Usage: my-cli <command>');
|
|
6
|
+
console.log();
|
|
7
|
+
console.log('Commands:');
|
|
8
|
+
console.log(' build run build');
|
|
9
|
+
console.log(' watch start watch mode');
|
|
10
|
+
console.log(' clean cleanup the project');
|
|
11
|
+
console.log(' init create config/commands.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseCliArgs() {
|
|
15
|
+
const debugLevel = argv.flag(['-d', '--debug']);
|
|
16
|
+
|
|
17
|
+
const debugMode = debugLevel > 0;
|
|
18
|
+
const verboseMode = debugLevel > 1;
|
|
19
|
+
|
|
20
|
+
let level = EnableLogLevel.log;
|
|
21
|
+
if (verboseMode) {
|
|
22
|
+
level = EnableLogLevel.verbose;
|
|
23
|
+
} else if (debugMode) {
|
|
24
|
+
level = EnableLogLevel.debug;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
createRootLogger('', level);
|
|
28
|
+
|
|
29
|
+
const command = argv.command(['build', 'watch', 'clean', 'init']);
|
|
30
|
+
if (!command) {
|
|
31
|
+
printUsage();
|
|
32
|
+
throw logger.fatal`No command provided. Please specify a command to run.`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const watchMode = command.value === 'watch';
|
|
36
|
+
const buildMode = command.value === 'build';
|
|
37
|
+
|
|
38
|
+
let breakMode = false;
|
|
39
|
+
if (watchMode) {
|
|
40
|
+
breakMode = argv.flag('--break') > 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let withCleanup = false;
|
|
44
|
+
if (buildMode) {
|
|
45
|
+
withCleanup = argv.flag('--clean') > 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (argv.unused().length > 0) {
|
|
49
|
+
throw logger.fatal`Unknown arguments: ${argv.unused().join(' ')}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const r = {
|
|
53
|
+
command: command.value,
|
|
54
|
+
debugMode,
|
|
55
|
+
verboseMode,
|
|
56
|
+
watchMode,
|
|
57
|
+
buildMode,
|
|
58
|
+
breakMode,
|
|
59
|
+
withCleanup,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
context = r;
|
|
63
|
+
|
|
64
|
+
return r;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export let context: Readonly<ReturnType<typeof parseCliArgs>>;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { ProjectConfig } from '@build-script/rushstack-config-loader';
|
|
2
|
+
import { logger } from '@idlebox/logger';
|
|
3
|
+
import { findUpUntilSync } from '@idlebox/node';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { projectRoot, selfRoot } from './paths.js';
|
|
8
|
+
|
|
9
|
+
interface IPackageBinary {
|
|
10
|
+
package: string;
|
|
11
|
+
binary?: string;
|
|
12
|
+
arguments?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ICommandInput {
|
|
16
|
+
title?: string;
|
|
17
|
+
command: string | readonly string[] | IPackageBinary;
|
|
18
|
+
watch?: string | readonly string[];
|
|
19
|
+
cwd?: string;
|
|
20
|
+
env?: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
interface IConfigFileInput {
|
|
23
|
+
build: (string | ICommandInput)[];
|
|
24
|
+
commands: Record<string, ICommandInput>;
|
|
25
|
+
clean: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ICommand {
|
|
29
|
+
title: string;
|
|
30
|
+
command: string | readonly string[];
|
|
31
|
+
cwd: string;
|
|
32
|
+
env: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
export interface IConfigFile {
|
|
35
|
+
buildTitles: readonly string[];
|
|
36
|
+
build: ReadonlyMap<string, ICommand>;
|
|
37
|
+
clean: readonly string[];
|
|
38
|
+
additionalPaths: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function watchModeCmd(
|
|
42
|
+
command: string | readonly string[],
|
|
43
|
+
watch?: string | readonly string[],
|
|
44
|
+
watchMode?: boolean,
|
|
45
|
+
): string | readonly string[] {
|
|
46
|
+
if (!watchMode) {
|
|
47
|
+
return command;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof command === 'string') {
|
|
51
|
+
if (!watch) watch = '-w';
|
|
52
|
+
|
|
53
|
+
return `${command} ${watch}`;
|
|
54
|
+
} else {
|
|
55
|
+
if (!watch) watch = ['-w'];
|
|
56
|
+
|
|
57
|
+
return [...command, ...watch];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function loadConfigFile(watchMode: boolean): IConfigFile {
|
|
62
|
+
const config = new ProjectConfig(projectRoot, undefined, logger);
|
|
63
|
+
const schemaFile = resolve(selfRoot, 'commands.schema.json');
|
|
64
|
+
|
|
65
|
+
const configFile = config.getJsonConfigInfo('commands');
|
|
66
|
+
logger.debug`using config file long<${configFile.effective}>`;
|
|
67
|
+
|
|
68
|
+
const input: IConfigFileInput = config.loadBothJson('commands', schemaFile, {
|
|
69
|
+
arrayMerge(target, _source) {
|
|
70
|
+
return target;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const buildMap = new Map<string, ICommand>();
|
|
75
|
+
const buildTitles: string[] = [];
|
|
76
|
+
function set(cmd: ICommand) {
|
|
77
|
+
if (buildMap.has(cmd.title)) {
|
|
78
|
+
throw new Error(`duplicate command "${cmd.title}", rename it before continue`);
|
|
79
|
+
}
|
|
80
|
+
buildMap.set(cmd.title, cmd);
|
|
81
|
+
buildTitles.push(cmd.title);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (let item of input.build) {
|
|
85
|
+
if (typeof item === 'string') {
|
|
86
|
+
item = input.commands[item];
|
|
87
|
+
if (!item) {
|
|
88
|
+
logger.fatal`command "${item}" not found in commands`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const cmd = item.command;
|
|
93
|
+
if (Array.isArray(cmd)) {
|
|
94
|
+
const copy = cmd.slice();
|
|
95
|
+
resolveCommandIsFile(config, copy);
|
|
96
|
+
set({
|
|
97
|
+
title: item.title ?? guessTitle(cmd),
|
|
98
|
+
command: watchModeCmd(copy, item.watch, watchMode),
|
|
99
|
+
cwd: resolve(projectRoot, item.cwd || '.'),
|
|
100
|
+
env: item.env ?? {},
|
|
101
|
+
});
|
|
102
|
+
} else if (typeof cmd === 'object' && 'package' in cmd) {
|
|
103
|
+
const obj = parsePackagedBinary(config, item, watchMode);
|
|
104
|
+
set(obj);
|
|
105
|
+
} else {
|
|
106
|
+
throw TypeError(`Invalid command type: ${typeof cmd}. Expected string or array or object.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const additionalPaths: string[] = [];
|
|
111
|
+
if (config.rigConfig.rigFound) {
|
|
112
|
+
const nmPath = findUpUntilSync({ file: 'node_modules', from: config.rigConfig.getResolvedProfileFolder() });
|
|
113
|
+
if (!nmPath) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Failed to find "node_modules" folder in rig profile "${config.rigConfig.getResolvedProfileFolder()}".`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
additionalPaths.push(resolve(nmPath, '.bin'));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const clean = [];
|
|
122
|
+
for (const item of input.clean) {
|
|
123
|
+
const abs = resolve(projectRoot, item);
|
|
124
|
+
if (!abs.startsWith(projectRoot)) {
|
|
125
|
+
throw new Error(`invalid clean path "${item}", out of project "${projectRoot}"`);
|
|
126
|
+
}
|
|
127
|
+
clean.push(abs);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
buildTitles,
|
|
132
|
+
build: buildMap,
|
|
133
|
+
clean,
|
|
134
|
+
additionalPaths: additionalPaths.toReversed(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function guessTitle(command: string | readonly string[]): string {
|
|
139
|
+
if (typeof command === 'string') {
|
|
140
|
+
return command.split(' ')[0];
|
|
141
|
+
}
|
|
142
|
+
if (Array.isArray(command)) {
|
|
143
|
+
return command[0];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw new Error(`Invalid command: ${Array.isArray(command) ? command.join(' ') : command}.`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parsePackagedBinary(config: ProjectConfig, item: ICommandInput, watchMode: boolean): ICommand {
|
|
150
|
+
const cmd = item.command as IPackageBinary;
|
|
151
|
+
|
|
152
|
+
const pkgJsonPath = fileURLToPath(config.resolve(`${cmd.package}/package.json`));
|
|
153
|
+
let title = item.title;
|
|
154
|
+
if (!title) {
|
|
155
|
+
title = cmd.package.split('/').pop();
|
|
156
|
+
if (cmd.binary && cmd.binary !== title) {
|
|
157
|
+
title += `:${cmd.binary}`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
162
|
+
const type1 = typeof pkg.bin === 'string';
|
|
163
|
+
const type2 = !!cmd.binary;
|
|
164
|
+
if (type1 && type2) {
|
|
165
|
+
throw new Error(`"${pkgJsonPath}" "bin" field is string, can not specify "binary" in "commands.json".`);
|
|
166
|
+
} else if (!type1 && !type2) {
|
|
167
|
+
throw new Error(`"${pkgJsonPath}" "bin" field is not string, must specify "binary" in "commands.json".`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const binVal = type1 ? pkg.bin : pkg.bin[cmd.binary as string];
|
|
171
|
+
const binPath = resolve(pkgJsonPath, '..', binVal);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
title: title!,
|
|
175
|
+
command: watchModeCmd([process.execPath, binPath, ...(cmd.arguments ?? [])], item.watch, watchMode),
|
|
176
|
+
cwd: resolve(projectRoot, item.cwd || '.'),
|
|
177
|
+
env: item.env ?? {},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 如果command第一个元素看似是一个文件,则解析成绝对路径并添加node前缀。
|
|
183
|
+
* 直接修改command数组。
|
|
184
|
+
*
|
|
185
|
+
* @param config
|
|
186
|
+
* @param command
|
|
187
|
+
*/
|
|
188
|
+
function resolveCommandIsFile(config: ProjectConfig, command: string[]) {
|
|
189
|
+
if (!command[0].endsWith('.ts')) return;
|
|
190
|
+
|
|
191
|
+
const r = config.getFileInfo(command[0]);
|
|
192
|
+
|
|
193
|
+
if (!r.effective) {
|
|
194
|
+
return; // will error later
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
command.splice(0, 1, process.execPath, r.effective);
|
|
198
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { findUpUntilSync } from '@idlebox/node';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const root = findUpUntilSync({ file: 'package.json', from: process.cwd() });
|
|
5
|
+
if (!root) {
|
|
6
|
+
throw new Error('Could not find project root directory');
|
|
7
|
+
}
|
|
8
|
+
export const projectRoot = dirname(root);
|
|
9
|
+
|
|
10
|
+
const self = findUpUntilSync({ file: 'package.json', from: import.meta.dirname });
|
|
11
|
+
if (!self) {
|
|
12
|
+
throw new Error('Could not find self directory');
|
|
13
|
+
}
|
|
14
|
+
export const selfRoot = dirname(self);
|
|
15
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { logger } from '@idlebox/logger';
|
|
2
|
+
import { projectRoot } from './paths.js';
|
|
3
|
+
|
|
4
|
+
interface ICommand {
|
|
5
|
+
name: string | readonly string[];
|
|
6
|
+
description: string;
|
|
7
|
+
callback(input: string): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const stdinCommands: ICommand[] = [];
|
|
11
|
+
export function registerCommand(command: ICommand) {
|
|
12
|
+
stdinCommands.push(command);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
registerCommand({
|
|
16
|
+
name: 'help',
|
|
17
|
+
description: '显示帮助信息',
|
|
18
|
+
callback: printHelp,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function initializeStdin() {
|
|
22
|
+
if (!process.stdin.isTTY) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
process.stdin.setEncoding('utf8');
|
|
26
|
+
process.stdin.on('data', (data: string) => {
|
|
27
|
+
const text = data.trim();
|
|
28
|
+
if (!text) return;
|
|
29
|
+
|
|
30
|
+
logger.debug`recv: ${text}`;
|
|
31
|
+
|
|
32
|
+
const firstWord = text.split(/\s/, 1)[0];
|
|
33
|
+
|
|
34
|
+
for (const command of stdinCommands) {
|
|
35
|
+
let match = false;
|
|
36
|
+
if (Array.isArray(command.name)) {
|
|
37
|
+
match = command.name.includes(firstWord);
|
|
38
|
+
} else {
|
|
39
|
+
match = command.name === firstWord;
|
|
40
|
+
}
|
|
41
|
+
if (match) {
|
|
42
|
+
command.callback(text);
|
|
43
|
+
console.log('');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
logger.warn`Unknown command: ${text} [type "help"]`;
|
|
49
|
+
console.log('');
|
|
50
|
+
});
|
|
51
|
+
process.stdin.on('end', () => {
|
|
52
|
+
console.log('End of input stream');
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function printHelp() {
|
|
57
|
+
console.log('This is `mpis-run watch`');
|
|
58
|
+
console.log(`working directory: ${projectRoot}`);
|
|
59
|
+
|
|
60
|
+
console.log('Available commands:');
|
|
61
|
+
for (const command of stdinCommands) {
|
|
62
|
+
const n = Array.isArray(command.name) ? command.name.join(', ') : command.name;
|
|
63
|
+
console.log(` - ${n}: ${command.description}`);
|
|
64
|
+
}
|
|
65
|
+
}
|