@lark-apaas/miaoda-cli 0.1.39-alpha.facb70e → 0.1.39-alpha.fc0f733
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/README.md +16 -0
- package/dist/config/sync-configs/nestjs-react-fullstack.js +37 -0
- package/dist/utils/eslint-ignore.js +169 -0
- package/dist/utils/sync-rule.js +24 -1
- package/dist/utils/typescript-exclude.js +62 -0
- package/package.json +3 -3
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/lint.js +120 -18
package/README.md
CHANGED
|
@@ -52,6 +52,22 @@ miaoda file ls --output json
|
|
|
52
52
|
|
|
53
53
|
完整命令通过 `miaoda --help` 或 `miaoda <domain> --help` 查看。
|
|
54
54
|
|
|
55
|
+
## 全栈应用的 MCP UI 检查
|
|
56
|
+
|
|
57
|
+
`miaoda app sync` 为 `nestjs-react-fullstack` 应用同步 `scripts/lint.js`,在
|
|
58
|
+
`tsconfig.node.json` 中追加 `server/mcp/ui` 排除项,并保留现有配置及继承的排除路径。
|
|
59
|
+
根 `eslint.config.js` / `.mjs` / `.cjs` 会增量追加 UI 全局忽略,直接 ESLint 调用和编辑器检查均生效。
|
|
60
|
+
迁移不执行配置文件;无法静态识别的自定义导出保留并提示手动补齐。
|
|
61
|
+
已知模板版本的 `type:check`、`eslint` 命令会迁移到独立检查入口;自定义命令保留并提示。
|
|
62
|
+
|
|
63
|
+
- `npm run type:check`:检查服务端、客户端及 MCP UI。
|
|
64
|
+
- `npm run type:check:mcp-ui` / `npm run eslint:mcp-ui`:单独检查 MCP UI。
|
|
65
|
+
- `npm run lint -- --files <files...>`:按改动文件选择检查范围。
|
|
66
|
+
|
|
67
|
+
同步不会生成 MCP UI 目录。开发 MCP UI 时,由 Agent 按 MCP 指南创建
|
|
68
|
+
`server/mcp/ui/tsconfig.json` 和根目录 `eslint.mcp-ui.config.cjs`。
|
|
69
|
+
没有 UI 源码时跳过独立检查;存在 UI 源码但缺少配置时明确报错。
|
|
70
|
+
|
|
55
71
|
## 打包可本地访问的静态产物
|
|
56
72
|
|
|
57
73
|
`miaoda app pack`(默认 `--mode standalone`)把当前应用构建成一份自包含静态产物,可脱离妙搭平台本地打开。
|
|
@@ -159,6 +159,43 @@ exports.SYNC_CONFIG = {
|
|
|
159
159
|
to: `npx -y ${fullstack_cli_pin_1.FULLSTACK_CLI_PIN_SPEC} sync --disable-gen-openapi`,
|
|
160
160
|
ifStartsWith: 'npx -y @lark-apaas/fullstack-cli sync',
|
|
161
161
|
},
|
|
162
|
+
{ type: 'eslint-ignore', to: 'eslint.config.js', pattern: 'server/mcp/ui/**' },
|
|
163
|
+
{ type: 'eslint-ignore', to: 'eslint.config.mjs', pattern: 'server/mcp/ui/**' },
|
|
164
|
+
{ type: 'eslint-ignore', to: 'eslint.config.cjs', pattern: 'server/mcp/ui/**' },
|
|
165
|
+
// MCP UI 使用独立浏览器配置;仅迁移已知模板命令,不生成 UI 目录或配置。
|
|
166
|
+
{
|
|
167
|
+
type: 'typescript-exclude',
|
|
168
|
+
to: 'tsconfig.node.json',
|
|
169
|
+
path: 'server/mcp/ui',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
type: 'add-script',
|
|
173
|
+
name: 'type:check:mcp-ui',
|
|
174
|
+
command: 'node ./scripts/lint.js --typecheck-mcp-ui',
|
|
175
|
+
overwrite: false,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
type: 'add-script',
|
|
179
|
+
name: 'eslint:mcp-ui',
|
|
180
|
+
command: 'node ./scripts/lint.js --eslint-mcp-ui',
|
|
181
|
+
overwrite: false,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: 'patch-script',
|
|
185
|
+
name: 'type:check',
|
|
186
|
+
to: 'node ./scripts/lint.js --typecheck',
|
|
187
|
+
ifEquals: [
|
|
188
|
+
'concurrently -n "server,client" -c "blue,green" "npm run type:check:server" "npm run type:check:client"',
|
|
189
|
+
'concurrently "npm run type:check:server" "npm run type:check:client"',
|
|
190
|
+
'concurrently "npm run type:check:client" "npm run type:check:server"',
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
type: 'patch-script',
|
|
195
|
+
name: 'eslint',
|
|
196
|
+
to: 'eslint . --quiet --ignore-pattern "server/mcp/ui/**"',
|
|
197
|
+
ifEquals: ['eslint . --quiet', 'eslint .'],
|
|
198
|
+
},
|
|
162
199
|
// ===== miaoda-cli 本地开发新增规则(fullstack-cli sync 不会执行) =====
|
|
163
200
|
// M1. scripts.dev:local —— 本地用户绕过沙箱判定直接跑本地链路(npm run dev:local)。
|
|
164
201
|
// dev.sh 在 MIAODA_DEP_CACHE_DIR 非空时跑 dev.js(沙箱保活)、否则 exec dev-local.js;显式 dev:local
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.addEslintGlobalIgnore = addEslintGlobalIgnore;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
9
|
+
const error_1 = require("./error");
|
|
10
|
+
/** 只解析源码,不加载用户配置;用局部插入保留自定义规则和注释。 */
|
|
11
|
+
function addEslintGlobalIgnore(filename, pattern) {
|
|
12
|
+
if (!node_fs_1.default.existsSync(filename))
|
|
13
|
+
return { changed: false };
|
|
14
|
+
const source = node_fs_1.default.readFileSync(filename, 'utf8');
|
|
15
|
+
const file = typescript_1.default.createSourceFile(filename, source, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.JS);
|
|
16
|
+
const diagnostics = file
|
|
17
|
+
.parseDiagnostics;
|
|
18
|
+
if (diagnostics.length)
|
|
19
|
+
throw new error_1.AppError('INVALID_CONFIG', `Invalid ESLint configuration: ${filename}`);
|
|
20
|
+
const variables = new Map();
|
|
21
|
+
const declarations = new Map();
|
|
22
|
+
const aliasReferences = new Set();
|
|
23
|
+
const resolvedAliases = new Set();
|
|
24
|
+
const exportTargets = new Set();
|
|
25
|
+
const helpers = new Set();
|
|
26
|
+
const namespaces = new Set();
|
|
27
|
+
const exports = [];
|
|
28
|
+
const moduleName = (node) => {
|
|
29
|
+
if (!typescript_1.default.isCallExpression(node) ||
|
|
30
|
+
!typescript_1.default.isIdentifier(node.expression) ||
|
|
31
|
+
node.expression.text !== 'require')
|
|
32
|
+
return;
|
|
33
|
+
const arg = node.arguments.at(0);
|
|
34
|
+
return arg && typescript_1.default.isStringLiteral(arg) ? arg.text : undefined;
|
|
35
|
+
};
|
|
36
|
+
for (const statement of file.statements) {
|
|
37
|
+
if (typescript_1.default.isImportDeclaration(statement) && typescript_1.default.isStringLiteral(statement.moduleSpecifier)) {
|
|
38
|
+
const module = statement.moduleSpecifier.text;
|
|
39
|
+
const clause = statement.importClause;
|
|
40
|
+
if (module === 'typescript-eslint' && clause?.name)
|
|
41
|
+
namespaces.add(clause.name.text);
|
|
42
|
+
const bindings = clause?.namedBindings;
|
|
43
|
+
if (bindings && typescript_1.default.isNamespaceImport(bindings) && module === 'typescript-eslint')
|
|
44
|
+
namespaces.add(bindings.name.text);
|
|
45
|
+
if (bindings && typescript_1.default.isNamedImports(bindings)) {
|
|
46
|
+
for (const element of bindings.elements) {
|
|
47
|
+
const name = element.propertyName?.text ?? element.name.text;
|
|
48
|
+
if ((module === 'eslint/config' && name === 'defineConfig') ||
|
|
49
|
+
(module === 'typescript-eslint' && name === 'config'))
|
|
50
|
+
helpers.add(element.name.text);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (typescript_1.default.isVariableStatement(statement) && statement.declarationList.flags & typescript_1.default.NodeFlags.Const) {
|
|
55
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
56
|
+
if (!declaration.initializer)
|
|
57
|
+
continue;
|
|
58
|
+
const module = moduleName(declaration.initializer);
|
|
59
|
+
if (typescript_1.default.isIdentifier(declaration.name)) {
|
|
60
|
+
variables.set(declaration.name.text, declaration.initializer);
|
|
61
|
+
declarations.set(declaration.name.text, declaration.name);
|
|
62
|
+
if (module === 'typescript-eslint')
|
|
63
|
+
namespaces.add(declaration.name.text);
|
|
64
|
+
}
|
|
65
|
+
else if (typescript_1.default.isObjectBindingPattern(declaration.name)) {
|
|
66
|
+
for (const element of declaration.name.elements) {
|
|
67
|
+
const name = element.propertyName ?? element.name;
|
|
68
|
+
if (!typescript_1.default.isIdentifier(name) || !typescript_1.default.isIdentifier(element.name))
|
|
69
|
+
continue;
|
|
70
|
+
if ((module === 'eslint/config' && name.text === 'defineConfig') ||
|
|
71
|
+
(module === 'typescript-eslint' && name.text === 'config'))
|
|
72
|
+
helpers.add(element.name.text);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (typescript_1.default.isExportAssignment(statement) && !statement.isExportEquals)
|
|
78
|
+
exports.push(statement.expression);
|
|
79
|
+
if (typescript_1.default.isExpressionStatement(statement) && typescript_1.default.isBinaryExpression(statement.expression)) {
|
|
80
|
+
const { left, right, operatorToken } = statement.expression;
|
|
81
|
+
if (operatorToken.kind === typescript_1.default.SyntaxKind.EqualsToken &&
|
|
82
|
+
typescript_1.default.isPropertyAccessExpression(left) &&
|
|
83
|
+
typescript_1.default.isIdentifier(left.expression) &&
|
|
84
|
+
left.expression.text === 'module' &&
|
|
85
|
+
left.name.text === 'exports') {
|
|
86
|
+
exports.push(right);
|
|
87
|
+
exportTargets.add(left);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function unwrap(node, seen = new Set()) {
|
|
92
|
+
if (typescript_1.default.isParenthesizedExpression(node))
|
|
93
|
+
return unwrap(node.expression, seen);
|
|
94
|
+
if (typescript_1.default.isIdentifier(node) && !seen.has(node.text)) {
|
|
95
|
+
const value = variables.get(node.text);
|
|
96
|
+
if (value) {
|
|
97
|
+
resolvedAliases.add(node.text);
|
|
98
|
+
aliasReferences.add(node);
|
|
99
|
+
seen.add(node.text);
|
|
100
|
+
return unwrap(value, seen);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return node;
|
|
104
|
+
}
|
|
105
|
+
const root = exports.length === 1 ? unwrap(exports[0]) : undefined;
|
|
106
|
+
// const 只固定变量绑定,不能保证数组不被 pop/push 或传给其他函数修改。
|
|
107
|
+
// 有额外引用时保留原配置,避免改变初始化数组影响用户后续操作。
|
|
108
|
+
function checkReferences(node) {
|
|
109
|
+
if (typescript_1.default.isIdentifier(node) &&
|
|
110
|
+
resolvedAliases.has(node.text) &&
|
|
111
|
+
declarations.get(node.text) !== node &&
|
|
112
|
+
!aliasReferences.has(node))
|
|
113
|
+
return true;
|
|
114
|
+
// module["exports"]、(module).exports、module 别名也可能修改导出数组。
|
|
115
|
+
if (typescript_1.default.isIdentifier(node) &&
|
|
116
|
+
node.text === 'module' &&
|
|
117
|
+
!(typescript_1.default.isPropertyAccessExpression(node.parent) &&
|
|
118
|
+
node.parent.expression === node &&
|
|
119
|
+
exportTargets.has(node.parent)))
|
|
120
|
+
return true;
|
|
121
|
+
return typescript_1.default.forEachChild(node, (child) => checkReferences(child) || undefined) ?? false;
|
|
122
|
+
}
|
|
123
|
+
const unsafeReference = checkReferences(file);
|
|
124
|
+
const isHelper = (node) => {
|
|
125
|
+
const callee = node.expression;
|
|
126
|
+
return typescript_1.default.isIdentifier(callee)
|
|
127
|
+
? helpers.has(callee.text)
|
|
128
|
+
: typescript_1.default.isPropertyAccessExpression(callee) &&
|
|
129
|
+
typescript_1.default.isIdentifier(callee.expression) &&
|
|
130
|
+
namespaces.has(callee.expression.text) &&
|
|
131
|
+
callee.name.text === 'config';
|
|
132
|
+
};
|
|
133
|
+
const entries = root && typescript_1.default.isArrayLiteralExpression(root)
|
|
134
|
+
? root.elements
|
|
135
|
+
: root && typescript_1.default.isCallExpression(root) && isHelper(root)
|
|
136
|
+
? root.arguments
|
|
137
|
+
: undefined;
|
|
138
|
+
if (!entries || unsafeReference) {
|
|
139
|
+
return {
|
|
140
|
+
changed: false,
|
|
141
|
+
warning: `Cannot safely migrate ${filename}; add ${JSON.stringify(pattern)} to its global ignores manually.`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// 只认真正的全局 ignores;带 files/rules 的局部忽略不能阻止服务端配置匹配 UI。
|
|
145
|
+
// 仅检查最后一个配置,确保此前的否定规则或动态配置无法撤销新增忽略。
|
|
146
|
+
const last = entries.at(-1);
|
|
147
|
+
if (last && typescript_1.default.isObjectLiteralExpression(last)) {
|
|
148
|
+
const props = last.properties;
|
|
149
|
+
const nameOf = (p) => p.name && (typescript_1.default.isIdentifier(p.name) || typescript_1.default.isStringLiteral(p.name)) ? p.name.text : undefined;
|
|
150
|
+
if (new Set(props.map(nameOf)).size === props.length &&
|
|
151
|
+
props.every((p) => typescript_1.default.isPropertyAssignment(p) && ['ignores', 'name'].includes(nameOf(p) ?? ''))) {
|
|
152
|
+
const ignores = props.find((p) => nameOf(p) === 'ignores');
|
|
153
|
+
if (ignores &&
|
|
154
|
+
typescript_1.default.isPropertyAssignment(ignores) &&
|
|
155
|
+
typescript_1.default.isArrayLiteralExpression(ignores.initializer)) {
|
|
156
|
+
const patterns = ignores.initializer.elements;
|
|
157
|
+
if (patterns.length &&
|
|
158
|
+
patterns.every(typescript_1.default.isStringLiteral) &&
|
|
159
|
+
patterns[patterns.length - 1].text === pattern)
|
|
160
|
+
return { changed: false };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const eol = source.includes('\r\n') ? '\r\n' : '\n';
|
|
165
|
+
const position = last ? last.end : entries.pos;
|
|
166
|
+
const addition = `${last ? ',' : ''}${eol} { ignores: [${JSON.stringify(pattern)}] }`;
|
|
167
|
+
node_fs_1.default.writeFileSync(filename, source.slice(0, position) + addition + source.slice(position));
|
|
168
|
+
return { changed: true };
|
|
169
|
+
}
|
package/dist/utils/sync-rule.js
CHANGED
|
@@ -43,6 +43,8 @@ const jsonc = __importStar(require("jsonc-parser"));
|
|
|
43
43
|
const file_ops_1 = require("./file-ops");
|
|
44
44
|
const merge_json_1 = require("./merge-json");
|
|
45
45
|
const logger_1 = require("./logger");
|
|
46
|
+
const typescript_exclude_1 = require("./typescript-exclude");
|
|
47
|
+
const eslint_ignore_1 = require("./eslint-ignore");
|
|
46
48
|
/**
|
|
47
49
|
* 顺序执行 SyncRule[],逐条 apply。一条 rule 报错不影响后续 rule(catch + 记录),最终
|
|
48
50
|
* 在 handler 层决定是否整体 fail。
|
|
@@ -106,6 +108,24 @@ function applyOne(rule, opts) {
|
|
|
106
108
|
case 'patch-script': {
|
|
107
109
|
return applyPatchScript(rule, targetDir, logPrefix);
|
|
108
110
|
}
|
|
111
|
+
case 'eslint-ignore': {
|
|
112
|
+
const result = (0, eslint_ignore_1.addEslintGlobalIgnore)(node_path_1.default.join(targetDir, rule.to), rule.pattern);
|
|
113
|
+
if (result.warning)
|
|
114
|
+
(0, logger_1.log)(logPrefix, ` ⚠ ${result.warning}`);
|
|
115
|
+
if (result.changed)
|
|
116
|
+
(0, logger_1.log)(logPrefix, ` ✓ ${rule.to} (global ignore ${rule.pattern})`);
|
|
117
|
+
return {
|
|
118
|
+
rule,
|
|
119
|
+
action: result.changed ? 'patched' : result.warning ? 'skipped' : 'noop',
|
|
120
|
+
path: rule.to,
|
|
121
|
+
detail: result.warning,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
case 'typescript-exclude': {
|
|
125
|
+
const changed = (0, typescript_exclude_1.excludeTypeScriptPath)(node_path_1.default.join(targetDir, rule.to), rule.path);
|
|
126
|
+
(0, logger_1.log)(logPrefix, ` ${changed ? '✓' : '○'} ${rule.to} (exclude ${rule.path})`);
|
|
127
|
+
return { rule, action: changed ? 'patched' : 'noop', path: rule.to };
|
|
128
|
+
}
|
|
109
129
|
case 'merge-json': {
|
|
110
130
|
return applyMergeJson(rule, sourceRoot, targetDir, logPrefix);
|
|
111
131
|
}
|
|
@@ -215,7 +235,10 @@ function applyPatchScript(rule, targetDir, logPrefix) {
|
|
|
215
235
|
(0, logger_1.log)(logPrefix, ` ○ scripts.${rule.name} (already patched)`);
|
|
216
236
|
return { rule, action: 'noop', path: 'package.json' };
|
|
217
237
|
}
|
|
218
|
-
|
|
238
|
+
const matches = rule.ifEquals
|
|
239
|
+
? rule.ifEquals.includes(current)
|
|
240
|
+
: rule.ifStartsWith !== undefined && current.startsWith(rule.ifStartsWith);
|
|
241
|
+
if (!matches) {
|
|
219
242
|
(0, logger_1.log)(logPrefix, ` ⚠ scripts.${rule.name} customized, skip patch`);
|
|
220
243
|
return { rule, action: 'skipped', path: 'package.json' };
|
|
221
244
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.excludeTypeScriptPath = excludeTypeScriptPath;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
10
|
+
const jsonc_parser_1 = require("jsonc-parser");
|
|
11
|
+
const error_1 = require("./error");
|
|
12
|
+
/** 追加排除项,保留 JSONC 注释和继承的排除路径;不创建缺失的配置。 */
|
|
13
|
+
function excludeTypeScriptPath(filename, excludedPath) {
|
|
14
|
+
if (!node_fs_1.default.existsSync(filename))
|
|
15
|
+
return false;
|
|
16
|
+
const source = node_fs_1.default.readFileSync(filename, 'utf8');
|
|
17
|
+
const errors = [];
|
|
18
|
+
const raw = (0, jsonc_parser_1.parse)(source, errors, { allowTrailingComma: true });
|
|
19
|
+
if (errors.length || !raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
20
|
+
throw new error_1.AppError('INVALID_CONFIG', `Invalid TypeScript configuration: ${filename}`);
|
|
21
|
+
}
|
|
22
|
+
const parsed = typescript_1.default.getParsedCommandLineOfConfigFile(filename, {}, {
|
|
23
|
+
...typescript_1.default.sys,
|
|
24
|
+
onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
|
|
25
|
+
throw new error_1.AppError('INVALID_CONFIG', typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
// 初始化中的空工程允许迁移,其他错误不能被静默忽略。
|
|
29
|
+
const diagnostics = parsed?.errors.filter((d) => d.code !== 18003 && d.code !== 18002) ?? [];
|
|
30
|
+
if (!parsed || diagnostics.length) {
|
|
31
|
+
throw new error_1.AppError('INVALID_CONFIG', `Cannot migrate ${filename}: ${diagnostics.map((d) => typescript_1.default.flattenDiagnosticMessageText(d.messageText, '\n')).join('; ')}`);
|
|
32
|
+
}
|
|
33
|
+
const effective = parsed.raw;
|
|
34
|
+
const excludes = effective.exclude ?? [
|
|
35
|
+
'node_modules',
|
|
36
|
+
'bower_components',
|
|
37
|
+
'jspm_packages',
|
|
38
|
+
...[parsed.options.outDir, parsed.options.declarationDir]
|
|
39
|
+
.filter((p) => Boolean(p))
|
|
40
|
+
.map((p) => node_path_1.default.relative(node_path_1.default.dirname(filename), p).split(node_path_1.default.sep).join('/')),
|
|
41
|
+
];
|
|
42
|
+
if (!Array.isArray(excludes) || excludes.some((p) => typeof p !== 'string')) {
|
|
43
|
+
throw new error_1.AppError('INVALID_CONFIG', `Invalid exclude array: ${filename}`);
|
|
44
|
+
}
|
|
45
|
+
const paths = excludes;
|
|
46
|
+
if (paths.includes(excludedPath))
|
|
47
|
+
return false;
|
|
48
|
+
const ownExclude = raw.exclude;
|
|
49
|
+
const formattingOptions = {
|
|
50
|
+
insertSpaces: true,
|
|
51
|
+
tabSize: 2,
|
|
52
|
+
eol: source.includes('\r\n') ? '\r\n' : '\n',
|
|
53
|
+
};
|
|
54
|
+
const edits = Array.isArray(ownExclude)
|
|
55
|
+
? (0, jsonc_parser_1.modify)(source, ['exclude', ownExclude.length], excludedPath, {
|
|
56
|
+
formattingOptions,
|
|
57
|
+
isArrayInsertion: true,
|
|
58
|
+
})
|
|
59
|
+
: (0, jsonc_parser_1.modify)(source, ['exclude'], [...paths, excludedPath], { formattingOptions });
|
|
60
|
+
node_fs_1.default.writeFileSync(filename, (0, jsonc_parser_1.applyEdits)(source, edits));
|
|
61
|
+
return true;
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/miaoda-cli",
|
|
3
|
-
"version": "0.1.39-alpha.
|
|
3
|
+
"version": "0.1.39-alpha.fc0f733",
|
|
4
4
|
"description": "Miaoda 平台命令行工具,面向 Agent 调用",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"jsonc-parser": "^3.3.1",
|
|
34
34
|
"ora": "^5.4.1",
|
|
35
35
|
"picocolors": "^1.1.1",
|
|
36
|
-
"which": "^7.0.0"
|
|
36
|
+
"which": "^7.0.0",
|
|
37
|
+
"typescript": "^5.8.3"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/cross-spawn": "^6.0.6",
|
|
@@ -51,7 +52,6 @@
|
|
|
51
52
|
"prettier": "^3.8.3",
|
|
52
53
|
"tsc-alias": "^1.8.11",
|
|
53
54
|
"tsx": "^4.19.4",
|
|
54
|
-
"typescript": "^5.8.3",
|
|
55
55
|
"vitest": "^4.1.4",
|
|
56
56
|
"xml2js": "^0.6.2"
|
|
57
57
|
},
|
|
@@ -18,19 +18,31 @@ function runCommand(command, args) {
|
|
|
18
18
|
shell: false,
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
child.on('close', (code) => resolve(code
|
|
21
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
22
22
|
child.on('error', () => resolve(1));
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// 串行依次执行每个任务,全部跑完后再聚合退出码:
|
|
27
|
+
// 降低并发资源占用、让各任务输出按顺序清晰可读,同时保留“一次暴露所有 lint 问题”的行为。
|
|
28
|
+
async function runTasksSerially(taskSpecs) {
|
|
29
|
+
let exitCode = 0;
|
|
30
|
+
for (const [command, args] of taskSpecs) {
|
|
31
|
+
const code = await runCommand(command, args);
|
|
32
|
+
if (code !== 0) {
|
|
33
|
+
exitCode = 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return exitCode;
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
function normalizeProjectFile(filePath) {
|
|
27
40
|
const absolutePath = path.isAbsolute(filePath)
|
|
28
41
|
? filePath
|
|
29
42
|
: path.resolve(cwd, filePath);
|
|
30
43
|
|
|
31
44
|
if (!fs.existsSync(absolutePath)) {
|
|
32
|
-
console.
|
|
33
|
-
return null;
|
|
45
|
+
console.log(`[lint] Deleted file: ${filePath}; checking affected projects`);
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
const relativePath = path.relative(cwd, absolutePath);
|
|
@@ -94,17 +106,92 @@ function canRunStylelint() {
|
|
|
94
106
|
return STYLELINT_CONFIG_FILES.some(file => fs.existsSync(path.join(cwd, file)));
|
|
95
107
|
}
|
|
96
108
|
|
|
109
|
+
const UI_DIR = 'server/mcp/ui';
|
|
110
|
+
const UI_TYPECHECK = 'node ./scripts/lint.js --typecheck';
|
|
111
|
+
|
|
112
|
+
function isUiFile(file) {
|
|
113
|
+
return file.startsWith(`${UI_DIR}/`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function uiSourceFiles() {
|
|
117
|
+
const files = [];
|
|
118
|
+
function visit(relative) {
|
|
119
|
+
const absolute = path.join(cwd, relative);
|
|
120
|
+
if (!fs.existsSync(absolute)) return;
|
|
121
|
+
if (fs.lstatSync(absolute).isSymbolicLink()) {
|
|
122
|
+
throw new Error(`MCP UI 检查不支持符号链接: ${relative}`);
|
|
123
|
+
}
|
|
124
|
+
for (const entry of fs.readdirSync(absolute, { withFileTypes: true })) {
|
|
125
|
+
if (['node_modules', 'dist', '.git'].includes(entry.name)) continue;
|
|
126
|
+
const file = `${relative}/${entry.name}`;
|
|
127
|
+
if (entry.isSymbolicLink()) throw new Error(`MCP UI 检查不支持符号链接: ${file}`);
|
|
128
|
+
if (entry.isDirectory()) visit(file);
|
|
129
|
+
else if (isEslintTarget(file) && !/\.d\.(?:ts|mts|cts)$/.test(file)) files.push(file);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
visit(UI_DIR);
|
|
133
|
+
return files.sort();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function uiTasks({ types = true, eslint = true } = {}) {
|
|
137
|
+
const sources = uiSourceFiles();
|
|
138
|
+
if (!sources.length) return [];
|
|
139
|
+
const tasks = [];
|
|
140
|
+
if (types) {
|
|
141
|
+
const config = `${UI_DIR}/tsconfig.json`;
|
|
142
|
+
if (!fs.existsSync(path.join(cwd, config))) {
|
|
143
|
+
throw new Error(`MCP UI 缺少 ${config},请按 mcp-guide 补齐独立浏览器 TypeScript 配置。`);
|
|
144
|
+
}
|
|
145
|
+
const ts = require('typescript');
|
|
146
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(path.join(cwd, config), {}, {
|
|
147
|
+
...ts.sys,
|
|
148
|
+
onUnRecoverableConfigFileDiagnostic: d => { throw new Error(ts.flattenDiagnosticMessageText(d.messageText, '\n')); },
|
|
149
|
+
});
|
|
150
|
+
if (!parsed || parsed.errors.length) throw new Error(`MCP UI TypeScript 配置无效: ${config}`);
|
|
151
|
+
const included = new Set(parsed.fileNames.map(file => path.resolve(file)));
|
|
152
|
+
const missing = sources.filter(file => !included.has(path.resolve(cwd, file)));
|
|
153
|
+
if (missing.length) throw new Error(`MCP UI TypeScript 配置未覆盖源码: ${missing.join(', ')};请检查 include/exclude,JS 源码需开启 allowJs。`);
|
|
154
|
+
tasks.push([getBinName('npx'), ['--no-install', 'tsc', '--noEmit', '-p', config]]);
|
|
155
|
+
}
|
|
156
|
+
if (eslint) {
|
|
157
|
+
const config = ['eslint.mcp-ui.config.js', 'eslint.mcp-ui.config.cjs', 'eslint.mcp-ui.config.mjs']
|
|
158
|
+
.find(file => fs.existsSync(path.join(cwd, file)));
|
|
159
|
+
if (!config) throw new Error('MCP UI 缺少独立 ESLint 配置,请按 mcp-guide 创建 eslint.mcp-ui.config.cjs。');
|
|
160
|
+
tasks.push([getBinName('npx'), ['--no-install', 'eslint', '--config', config, '--quiet', '--max-warnings', '0', ...sources]]);
|
|
161
|
+
}
|
|
162
|
+
return tasks;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function packageScripts() {
|
|
166
|
+
return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).scripts || {};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function runTypeCheck() {
|
|
170
|
+
const scripts = packageScripts();
|
|
171
|
+
const tasks = ['type:check:server', 'type:check:client'].map(name => {
|
|
172
|
+
if (!scripts[name]) throw new Error(`缺少 scripts.${name},请保留应用原有类型检查命令。`);
|
|
173
|
+
return [getBinName('npm'), ['run', name]];
|
|
174
|
+
});
|
|
175
|
+
tasks.push(...uiTasks({ eslint: false }));
|
|
176
|
+
process.exit(await runTasksSerially(tasks));
|
|
177
|
+
}
|
|
178
|
+
|
|
97
179
|
async function runDefaultLint() {
|
|
98
|
-
const
|
|
180
|
+
const taskSpecs = [
|
|
181
|
+
[getBinName('npm'), ['run', 'eslint']],
|
|
182
|
+
[getBinName('npm'), ['run', 'type:check']],
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
// Customized type:check remains untouched; ensure full lint still checks UI.
|
|
186
|
+
taskSpecs.push(...uiTasks({ types: packageScripts()['type:check'] !== UI_TYPECHECK }));
|
|
99
187
|
|
|
100
188
|
if (canRunStylelint()) {
|
|
101
|
-
|
|
189
|
+
taskSpecs.push([getBinName('npm'), ['run', 'stylelint']]);
|
|
102
190
|
} else {
|
|
103
191
|
console.warn('[lint] Skip stylelint: missing scripts.stylelint or stylelint config');
|
|
104
192
|
}
|
|
105
193
|
|
|
106
|
-
|
|
107
|
-
process.exit(code);
|
|
194
|
+
process.exit(await runTasksSerially(taskSpecs));
|
|
108
195
|
}
|
|
109
196
|
|
|
110
197
|
async function runSelectiveLint(inputFiles) {
|
|
@@ -117,15 +204,20 @@ async function runSelectiveLint(inputFiles) {
|
|
|
117
204
|
process.exit(0);
|
|
118
205
|
}
|
|
119
206
|
|
|
120
|
-
const eslintFiles = normalizedFiles.filter(isEslintTarget);
|
|
121
|
-
const stylelintFiles = normalizedFiles.filter(isStylelintTarget);
|
|
207
|
+
const eslintFiles = normalizedFiles.filter(file => fs.existsSync(path.join(cwd, file)) && isEslintTarget(file) && !isUiFile(file) && !file.startsWith('eslint.mcp-ui.config.'));
|
|
208
|
+
const stylelintFiles = normalizedFiles.filter(file => fs.existsSync(path.join(cwd, file)) && isStylelintTarget(file));
|
|
122
209
|
const typeCheckFiles = normalizedFiles.filter(isTypeCheckTarget);
|
|
123
210
|
|
|
124
211
|
const clientTypeFiles = [];
|
|
125
212
|
const serverTypeFiles = [];
|
|
213
|
+
const rootConfigChanged = normalizedFiles.some(file => /^tsconfig(?:\.[^/]*)?\.json$/.test(file));
|
|
214
|
+
if (rootConfigChanged) { clientTypeFiles.push('config'); serverTypeFiles.push('config'); }
|
|
215
|
+
let checkUi = rootConfigChanged || normalizedFiles.some(file => isUiFile(file) || file.startsWith('shared/') || file.startsWith('eslint.mcp-ui.config.'));
|
|
126
216
|
|
|
127
217
|
for (const filePath of typeCheckFiles) {
|
|
128
|
-
if (filePath
|
|
218
|
+
if (isUiFile(filePath)) {
|
|
219
|
+
checkUi = true;
|
|
220
|
+
} else if (filePath.startsWith('client/')) {
|
|
129
221
|
clientTypeFiles.push(filePath);
|
|
130
222
|
} else if (filePath.startsWith('server/')) {
|
|
131
223
|
serverTypeFiles.push(filePath);
|
|
@@ -135,36 +227,46 @@ async function runSelectiveLint(inputFiles) {
|
|
|
135
227
|
}
|
|
136
228
|
}
|
|
137
229
|
|
|
138
|
-
const
|
|
230
|
+
const taskSpecs = [];
|
|
139
231
|
|
|
140
232
|
if (eslintFiles.length > 0) {
|
|
141
|
-
|
|
233
|
+
taskSpecs.push([getBinName('npx'), ['eslint', '--quiet', ...eslintFiles]]);
|
|
142
234
|
}
|
|
143
235
|
|
|
144
236
|
if (stylelintFiles.length > 0 && !canRunStylelint()) {
|
|
145
237
|
console.warn('[lint] Skip stylelint: missing scripts.stylelint or stylelint config');
|
|
146
238
|
} else if (stylelintFiles.length > 0) {
|
|
147
|
-
|
|
239
|
+
taskSpecs.push([getBinName('npx'), ['stylelint', '--quiet', ...stylelintFiles]]);
|
|
148
240
|
}
|
|
149
241
|
|
|
150
242
|
if (clientTypeFiles.length > 0) {
|
|
151
|
-
|
|
243
|
+
taskSpecs.push([getBinName('npm'), ['run', 'type:check:client']]);
|
|
152
244
|
}
|
|
153
245
|
|
|
154
246
|
if (serverTypeFiles.length > 0) {
|
|
155
|
-
|
|
247
|
+
taskSpecs.push([getBinName('npm'), ['run', 'type:check:server']]);
|
|
156
248
|
}
|
|
157
249
|
|
|
158
|
-
if (
|
|
250
|
+
if (checkUi) taskSpecs.push(...uiTasks());
|
|
251
|
+
|
|
252
|
+
if (taskSpecs.length === 0) {
|
|
159
253
|
console.log('[lint] No supported files matched for lint');
|
|
160
254
|
process.exit(0);
|
|
161
255
|
}
|
|
162
256
|
|
|
163
|
-
|
|
164
|
-
process.exit(results.some(code => code !== 0) ? 1 : 0);
|
|
257
|
+
process.exit(await runTasksSerially(taskSpecs));
|
|
165
258
|
}
|
|
166
259
|
|
|
167
260
|
async function main() {
|
|
261
|
+
const args = process.argv.slice(2);
|
|
262
|
+
const modes = ['--typecheck', '--typecheck-mcp-ui', '--eslint-mcp-ui'];
|
|
263
|
+
if (args.length && !(args[0] === '--files' && args.length > 1) && !(args.length === 1 && modes.includes(args[0]))) {
|
|
264
|
+
throw new Error('用法: lint.js [--files <paths...> | --typecheck | --typecheck-mcp-ui | --eslint-mcp-ui]');
|
|
265
|
+
}
|
|
266
|
+
if (args[0] === '--files' && args.slice(1).some(arg => arg.startsWith('--'))) throw new Error('--files 不可与其他检查模式混用');
|
|
267
|
+
if (args.includes('--typecheck')) return runTypeCheck();
|
|
268
|
+
if (args.includes('--typecheck-mcp-ui')) return process.exit(await runTasksSerially(uiTasks({ eslint: false })));
|
|
269
|
+
if (args.includes('--eslint-mcp-ui')) return process.exit(await runTasksSerially(uiTasks({ types: false })));
|
|
168
270
|
const files = parseFilesArg(process.argv.slice(2));
|
|
169
271
|
if (files === null) {
|
|
170
272
|
await runDefaultLint();
|