@lark-apaas/coding-template-nestjs-react-fullstack 0.1.40-alpha.20260915093425 → 0.1.40-alpha.20260915170225
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 +46 -0
- package/package.json +1 -1
- package/template/eslint.config.js +1 -1
- package/template/package-lock.json +12 -12
- package/template/package.json +7 -5
- package/template/scripts/build.sh +81 -43
- package/template/scripts/lint.js +120 -18
package/README.md
CHANGED
|
@@ -27,6 +27,25 @@ MIAODA_APP_TYPE=3 npx @lark-apaas/miaoda-cli@<latest> app init \
|
|
|
27
27
|
- `SANDBOX_PUBLIC_URL` —— 沙箱公网域
|
|
28
28
|
- `CLIENT_BASE_PATH=/app/<app_id>/` —— React Router basename + Vite base
|
|
29
29
|
|
|
30
|
+
## MCP UI 的类型与代码检查
|
|
31
|
+
|
|
32
|
+
`server/mcp/ui` 是独立浏览器工程,主应用的 TypeScript 和 ESLint 配置排除该目录。
|
|
33
|
+
`npm run lint` 通过现有 `scripts/lint.js` 统一调度检查:
|
|
34
|
+
|
|
35
|
+
| 命令 | 检查范围 |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `npm run type:check` | 服务端、客户端、MCP UI 类型检查 |
|
|
38
|
+
| `npm run type:check:mcp-ui` | 仅 MCP UI 类型检查 |
|
|
39
|
+
| `npm run eslint:mcp-ui` | 仅 MCP UI ESLint 检查 |
|
|
40
|
+
| `npm run lint -- --files <files...>` | 根据改动文件选择检查范围 |
|
|
41
|
+
|
|
42
|
+
模板不预建 UI 目录。Agent 开发 MCP UI 时,按照 MCP 指南生成
|
|
43
|
+
`server/mcp/ui/tsconfig.json` 和根目录 `eslint.mcp-ui.config.cjs`。
|
|
44
|
+
没有 UI 源码时跳过独立检查;存在源码但配置缺失或未覆盖源码时明确报错。
|
|
45
|
+
|
|
46
|
+
存量应用通过本地 `miaoda app sync` 或沙箱 `fullstack-cli sync` 更新脚本、迁移已知模板命令,
|
|
47
|
+
并保留现有 TS 排除项。自定义检查命令会保留,开发者需自行接入独立 UI 检查。
|
|
48
|
+
|
|
30
49
|
## 包结构
|
|
31
50
|
|
|
32
51
|
```
|
|
@@ -39,3 +58,30 @@ MIAODA_APP_TYPE=3 npx @lark-apaas/miaoda-cli@<latest> app init \
|
|
|
39
58
|
`template/` 内以下划线开头的占位文件(`_gitignore` / `_npmrc` / `_env.local`)在 `miaoda app init`
|
|
40
59
|
渲染时会按 RENAME 映射改回正常名(npm pack 会特殊处理 `.gitignore`、且 `.npmrc` 直接放
|
|
41
60
|
在包里会污染用户 `npm install` 行为,所以都要 underscore 前缀)。
|
|
61
|
+
|
|
62
|
+
## MCP 后端构建标识
|
|
63
|
+
|
|
64
|
+
`npm run build` 的 `scripts/build.sh` 在服务端、客户端构建、产物准备和依赖裁剪全部成功后,
|
|
65
|
+
用 Node 内置 `crypto.randomUUID()` 生成一次纯文本 UUID(36 字符,无需发布环境变量)。
|
|
66
|
+
同次构建写入下列文件,内容完全相同:
|
|
67
|
+
|
|
68
|
+
| 构建文件(相对工程根) | 对应运行目录 |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `dist/mcp-build-id` | 工程根 |
|
|
71
|
+
| `dist/dist/mcp-build-id` | `dist/`,或整个 `dist/` 复制/解压到的 `dist-server/` |
|
|
72
|
+
| `dist/server/dist/mcp-build-id` | 旧布局 `dist/server/`,或该目录复制/解压到的 `dist-server/` |
|
|
73
|
+
|
|
74
|
+
SDK 统一读取 `cwd/dist/mcp-build-id`;production 首次读取后缓存。
|
|
75
|
+
复制/部署同一产物必须保留文件,不在启动时重新生成 UUID;再次成功全构建会得到新的 UUID。
|
|
76
|
+
直接运行 `build:server` 不完成交付流程,不能替代 `npm run build`。
|
|
77
|
+
SDK 在文件缺失时不返回 `appRevision`,标准 MCP 仍可用;开发态使用进程单例 UUID,不读构建文件。
|
|
78
|
+
UI/Skill 内容仍由 SDK 计入总 hash,独立修改这些产物不会重写 `backendBuildID`。
|
|
79
|
+
|
|
80
|
+
`only_frontend_change=true` 跳过服务端构建、依赖裁剪和 UUID 生成。
|
|
81
|
+
构建目录仍按原逻辑清空;平台跳过服务端包上传,所以已部署服务的旧 ID 保持不变。
|
|
82
|
+
`server/mcp/ui`、`server/mcp/skills` 的源码变更应由平台判为全构建;它们不属于 `client/`。
|
|
83
|
+
平台实际根据配置的前端路径前缀匹配变更前后路径,不能把 `server/mcp` 列为前端前缀。
|
|
84
|
+
即使 client build 在本地写出了 `mcp-ui`,纯前端发布也不携带服务端 MCP 产物,**不承诺热更 MCP UI**。
|
|
85
|
+
|
|
86
|
+
存量应用通过既有 `fullstack-cli sync` 或 `miaoda app sync` 的 `scripts/` 目录覆盖同步获得此脚本,
|
|
87
|
+
同步本身不生成 UUID、不修改已有构建标识,无新增用户 CLI 命令。
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ module.exports = tseslint.config(
|
|
|
9
9
|
'node_modules',
|
|
10
10
|
'source_package',
|
|
11
11
|
'client/src/api/gen',
|
|
12
|
-
// MCP UI
|
|
12
|
+
// MCP UI 使用独立浏览器配置,由 lint.js 使用 eslint.mcp-ui.config.cjs 独立检查。
|
|
13
13
|
'server/mcp/ui/**',
|
|
14
14
|
'**/*.d.ts',
|
|
15
15
|
'**/*.js.map',
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"name": "fullstack-nestjs-template",
|
|
9
9
|
"version": "2.3.0",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@lark-apaas/fullstack-nestjs-core": "1.1.62-alpha.
|
|
11
|
+
"@lark-apaas/fullstack-nestjs-core": "1.1.62-alpha.20260915164949",
|
|
12
12
|
"@nestjs/axios": "^4.0.1",
|
|
13
13
|
"@nestjs/cache-manager": "^3.1.3",
|
|
14
14
|
"@nestjs/common": "^10.4.20",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@hookform/resolvers": "^5.2.2",
|
|
35
35
|
"@lark-apaas/client-toolkit": "^1.2.70",
|
|
36
|
-
"@lark-apaas/coding-preset-vite-react": "1.0.23-alpha.
|
|
36
|
+
"@lark-apaas/coding-preset-vite-react": "1.0.23-alpha.20260915164951",
|
|
37
37
|
"@lark-apaas/fullstack-presets": "^1.1.0",
|
|
38
38
|
"@nestjs/cli": "^10.4.9",
|
|
39
39
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
@@ -1634,9 +1634,9 @@
|
|
|
1634
1634
|
"license": "MIT"
|
|
1635
1635
|
},
|
|
1636
1636
|
"node_modules/@lark-apaas/coding-preset-vite-react": {
|
|
1637
|
-
"version": "1.0.23-alpha.
|
|
1638
|
-
"resolved": "https://registry.npmmirror.com/@lark-apaas/coding-preset-vite-react/-/coding-preset-vite-react-1.0.23-alpha.
|
|
1639
|
-
"integrity": "sha512-
|
|
1637
|
+
"version": "1.0.23-alpha.20260915164951",
|
|
1638
|
+
"resolved": "https://registry.npmmirror.com/@lark-apaas/coding-preset-vite-react/-/coding-preset-vite-react-1.0.23-alpha.20260915164951.tgz",
|
|
1639
|
+
"integrity": "sha512-5QRc/9rXGp9GYP7ISD9nN99Jt8/NHzro3BnSj3QezaAzBBzH5OMFQOySK3BKFVqvWvnXTNpTiAzt9EioGiVNWQ==",
|
|
1640
1640
|
"dev": true,
|
|
1641
1641
|
"license": "MIT",
|
|
1642
1642
|
"dependencies": {
|
|
@@ -1697,9 +1697,9 @@
|
|
|
1697
1697
|
}
|
|
1698
1698
|
},
|
|
1699
1699
|
"node_modules/@lark-apaas/fullstack-nestjs-core": {
|
|
1700
|
-
"version": "1.1.62-alpha.
|
|
1701
|
-
"resolved": "https://registry.npmmirror.com/@lark-apaas/fullstack-nestjs-core/-/fullstack-nestjs-core-1.1.62-alpha.
|
|
1702
|
-
"integrity": "sha512-
|
|
1700
|
+
"version": "1.1.62-alpha.20260915164949",
|
|
1701
|
+
"resolved": "https://registry.npmmirror.com/@lark-apaas/fullstack-nestjs-core/-/fullstack-nestjs-core-1.1.62-alpha.20260915164949.tgz",
|
|
1702
|
+
"integrity": "sha512-l5OUcUnbRFWcP8FArwFXkn0kwPDiM4Mu7jSEROGJuHMoOA7DRBZlEfTL9HB6sXXsX92Jo5YJophb2sFCC14KGw==",
|
|
1703
1703
|
"license": "MIT",
|
|
1704
1704
|
"dependencies": {
|
|
1705
1705
|
"@lark-apaas/cache-service": "^0.1.1",
|
|
@@ -1712,7 +1712,7 @@
|
|
|
1712
1712
|
"@lark-apaas/nestjs-datapaas": "^1.0.21",
|
|
1713
1713
|
"@lark-apaas/nestjs-http-forwarder": "^0.1.3",
|
|
1714
1714
|
"@lark-apaas/nestjs-logger": "^1.0.17",
|
|
1715
|
-
"@lark-apaas/nestjs-mcp": "0.1.1-alpha.
|
|
1715
|
+
"@lark-apaas/nestjs-mcp": "0.1.1-alpha.20260915164952",
|
|
1716
1716
|
"@lark-apaas/nestjs-observable": "^0.0.12",
|
|
1717
1717
|
"@lark-apaas/nestjs-openapi-devtools": "^1.0.10",
|
|
1718
1718
|
"@lark-apaas/nestjs-trigger": "^0.0.3",
|
|
@@ -1945,9 +1945,9 @@
|
|
|
1945
1945
|
}
|
|
1946
1946
|
},
|
|
1947
1947
|
"node_modules/@lark-apaas/nestjs-mcp": {
|
|
1948
|
-
"version": "0.1.1-alpha.
|
|
1949
|
-
"resolved": "https://registry.npmmirror.com/@lark-apaas/nestjs-mcp/-/nestjs-mcp-0.1.1-alpha.
|
|
1950
|
-
"integrity": "sha512-
|
|
1948
|
+
"version": "0.1.1-alpha.20260915164952",
|
|
1949
|
+
"resolved": "https://registry.npmmirror.com/@lark-apaas/nestjs-mcp/-/nestjs-mcp-0.1.1-alpha.20260915164952.tgz",
|
|
1950
|
+
"integrity": "sha512-TxZ0BAISQKmjLLLjQ2vFsM41J98qSqgg82/MjB950uxm1UgalYbhLe2ok/CdE1SXziO+gvQb3yfZG6avI8muJA==",
|
|
1951
1951
|
"license": "MIT",
|
|
1952
1952
|
"dependencies": {
|
|
1953
1953
|
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
package/template/package.json
CHANGED
|
@@ -18,17 +18,19 @@
|
|
|
18
18
|
"build:server": "NODE_ENV=production nest build",
|
|
19
19
|
"build:client": "NODE_ENV=production vite build --config vite.config.ts",
|
|
20
20
|
"start": "NODE_ENV=production node main.js",
|
|
21
|
-
"eslint": "eslint . --quiet",
|
|
21
|
+
"eslint": "eslint . --quiet --ignore-pattern \"server/mcp/ui/**\"",
|
|
22
22
|
"stylelint": "stylelint client/src/**/*.css --quiet",
|
|
23
|
-
"type:check": "
|
|
23
|
+
"type:check": "node ./scripts/lint.js --typecheck",
|
|
24
24
|
"type:check:client": "tsc --noEmit --project tsconfig.app.json",
|
|
25
25
|
"type:check:server": "tsc --noEmit --project tsconfig.node.json",
|
|
26
26
|
"lint": "node ./scripts/lint.js",
|
|
27
27
|
"precommit": "node scripts/hooks/run-precommit.js",
|
|
28
|
-
"upgrade": "npx -y @lark-apaas/fullstack-cli sync --disable-gen-openapi"
|
|
28
|
+
"upgrade": "npx -y @lark-apaas/fullstack-cli sync --disable-gen-openapi",
|
|
29
|
+
"type:check:mcp-ui": "node ./scripts/lint.js --typecheck-mcp-ui",
|
|
30
|
+
"eslint:mcp-ui": "node ./scripts/lint.js --eslint-mcp-ui"
|
|
29
31
|
},
|
|
30
32
|
"dependencies": {
|
|
31
|
-
"@lark-apaas/fullstack-nestjs-core": "1.1.62-alpha.
|
|
33
|
+
"@lark-apaas/fullstack-nestjs-core": "1.1.62-alpha.20260915164949",
|
|
32
34
|
"@nestjs/axios": "^4.0.1",
|
|
33
35
|
"@nestjs/cache-manager": "^3.1.3",
|
|
34
36
|
"@nestjs/common": "^10.4.20",
|
|
@@ -53,7 +55,7 @@
|
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"@hookform/resolvers": "^5.2.2",
|
|
55
57
|
"@lark-apaas/client-toolkit": "^1.2.70",
|
|
56
|
-
"@lark-apaas/coding-preset-vite-react": "1.0.23-alpha.
|
|
58
|
+
"@lark-apaas/coding-preset-vite-react": "1.0.23-alpha.20260915164951",
|
|
57
59
|
"@lark-apaas/fullstack-presets": "^1.1.0",
|
|
58
60
|
"@nestjs/cli": "^10.4.9",
|
|
59
61
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
@@ -43,37 +43,43 @@ echo ""
|
|
|
43
43
|
echo "🔨 [3/5] 并行构建 server 和 client"
|
|
44
44
|
STEP_START=$(node -e "console.log(Date.now())")
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
46
|
+
# server/mcp(含 UI/Skill)变更必须由平台判定为全构建。
|
|
47
|
+
if [[ "${only_frontend_change:-false}" == "true" ]]; then
|
|
48
|
+
echo " ├─ 仅构建 client (only_frontend_change=true)"
|
|
49
|
+
npm run build:client > /tmp/build-client.log 2>&1
|
|
50
|
+
else
|
|
51
|
+
# 并行构建
|
|
52
|
+
echo " ├─ 启动 server 构建..."
|
|
53
|
+
npm run build:server > /tmp/build-server.log 2>&1 &
|
|
54
|
+
SERVER_PID=$!
|
|
55
|
+
|
|
56
|
+
echo " ├─ 启动 client 构建..."
|
|
57
|
+
npm run build:client > /tmp/build-client.log 2>&1 &
|
|
58
|
+
CLIENT_PID=$!
|
|
59
|
+
|
|
60
|
+
# 等待两个构建完成
|
|
61
|
+
SERVER_EXIT=0
|
|
62
|
+
CLIENT_EXIT=0
|
|
63
|
+
|
|
64
|
+
wait $SERVER_PID || SERVER_EXIT=$?
|
|
65
|
+
wait $CLIENT_PID || CLIENT_EXIT=$?
|
|
66
|
+
|
|
67
|
+
# 检查构建结果
|
|
68
|
+
if [ $SERVER_EXIT -ne 0 ]; then
|
|
69
|
+
echo " ❌ Server 构建失败"
|
|
70
|
+
cat /tmp/build-server.log
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
if [ $CLIENT_EXIT -ne 0 ]; then
|
|
75
|
+
echo " ❌ Client 构建失败"
|
|
76
|
+
cat /tmp/build-client.log
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
echo " ✅ Server 构建完成"
|
|
81
|
+
echo " ✅ Client 构建完成"
|
|
67
82
|
fi
|
|
68
|
-
|
|
69
|
-
if [ $CLIENT_EXIT -ne 0 ]; then
|
|
70
|
-
echo " ❌ Client 构建失败"
|
|
71
|
-
cat /tmp/build-client.log
|
|
72
|
-
exit 1
|
|
73
|
-
fi
|
|
74
|
-
|
|
75
|
-
echo " ✅ Server 构建完成"
|
|
76
|
-
echo " ✅ Client 构建完成"
|
|
77
83
|
print_time $STEP_START
|
|
78
84
|
echo ""
|
|
79
85
|
|
|
@@ -81,18 +87,21 @@ echo ""
|
|
|
81
87
|
echo "📦 [4/5] 准备 server 依赖产物"
|
|
82
88
|
STEP_START=$(node -e "console.log(Date.now())")
|
|
83
89
|
|
|
84
|
-
|
|
90
|
+
if [[ "${only_frontend_change:-false}" != "true" ]]; then
|
|
91
|
+
mkdir -p "$OUT_DIR/dist/client"
|
|
85
92
|
|
|
86
|
-
# public 先进(server setBaseViewsDir + nestjs-core 中间件同源 serve)
|
|
87
|
-
if [ -d "$ROOT_DIR/client/public" ]; then
|
|
88
|
-
|
|
89
|
-
fi
|
|
93
|
+
# public 先进(server setBaseViewsDir + nestjs-core 中间件同源 serve)
|
|
94
|
+
if [ -d "$ROOT_DIR/client/public" ]; then
|
|
95
|
+
cp -R "$ROOT_DIR/client/public/." "$OUT_DIR/dist/client/"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# 构建产物 HTML 随后拷,覆盖 public 里的同名文件(保证入口页是构建版,不被 public 静默覆盖)
|
|
99
|
+
cp "$ROOT_DIR/dist/client/"*.html "$OUT_DIR/dist/client/" || true
|
|
90
100
|
|
|
91
|
-
#
|
|
92
|
-
cp "$ROOT_DIR/
|
|
101
|
+
# 拷贝 run.sh 文件
|
|
102
|
+
cp "$ROOT_DIR/scripts/run.sh" "$OUT_DIR/"
|
|
93
103
|
|
|
94
|
-
|
|
95
|
-
cp "$ROOT_DIR/scripts/run.sh" "$OUT_DIR/"
|
|
104
|
+
fi
|
|
96
105
|
|
|
97
106
|
# 清理无用文件
|
|
98
107
|
rm -rf "$ROOT_DIR/dist/scripts"
|
|
@@ -106,7 +115,9 @@ echo "✂️ [5/5] 智能依赖裁剪"
|
|
|
106
115
|
STEP_START=$(node -e "console.log(Date.now())")
|
|
107
116
|
|
|
108
117
|
# 分析实际依赖、复制并裁剪 node_modules、生成精简的 package.json
|
|
109
|
-
|
|
118
|
+
if [[ "${only_frontend_change:-false}" != "true" ]]; then
|
|
119
|
+
node "$ROOT_DIR/scripts/prune-smart.js"
|
|
120
|
+
fi
|
|
110
121
|
|
|
111
122
|
print_time $STEP_START
|
|
112
123
|
echo ""
|
|
@@ -116,10 +127,37 @@ echo "构建完成"
|
|
|
116
127
|
print_time $TOTAL_START
|
|
117
128
|
|
|
118
129
|
# 输出产物信息
|
|
119
|
-
DIST_SIZE=$(du -sh "$
|
|
120
|
-
NODE_MODULES_SIZE=$(du -sh "$OUT_DIR/node_modules" | cut -f1)
|
|
130
|
+
DIST_SIZE=$(du -sh "$ROOT_DIR/dist" | cut -f1)
|
|
121
131
|
echo ""
|
|
122
132
|
echo "📊 构建产物统计:"
|
|
123
133
|
echo " 总大小: $DIST_SIZE"
|
|
124
|
-
|
|
134
|
+
if [[ "${only_frontend_change:-false}" != "true" ]]; then
|
|
135
|
+
NODE_MODULES_SIZE=$(du -sh "$OUT_DIR/node_modules" | cut -f1)
|
|
136
|
+
echo " node_modules: $NODE_MODULES_SIZE"
|
|
137
|
+
fi
|
|
125
138
|
echo ""
|
|
139
|
+
|
|
140
|
+
# 仅成功的全构建生成 backendBuildID;必须在构建、产物准备和依赖裁剪之后。
|
|
141
|
+
# UI/Skill 内容由 SDK 独立计入总 hash;纯前端发布不替换已部署的 backendBuildID。
|
|
142
|
+
if [[ "${only_frontend_change:-false}" != "true" ]]; then
|
|
143
|
+
node - "$ROOT_DIR/dist" <<'NODE'
|
|
144
|
+
const fs = require('node:fs');
|
|
145
|
+
const path = require('node:path');
|
|
146
|
+
const { randomUUID } = require('node:crypto');
|
|
147
|
+
const dist = process.argv[2];
|
|
148
|
+
// 防止 build:server 空跑成功、却没有可交付的服务端入口。
|
|
149
|
+
if (!fs.statSync(path.join(dist, 'server/main.js')).isFile()) {
|
|
150
|
+
throw new Error('Missing server build output: dist/server/main.js');
|
|
151
|
+
}
|
|
152
|
+
// runtime cwd 分别为工程根、dist/(或整包解压到 dist-server/)、旧版 dist/server/。
|
|
153
|
+
// 与 MCP UI/Skill 的交付布局一致:复制产物时保留同一个 UUID,不在启动时生成。
|
|
154
|
+
const files = [
|
|
155
|
+
path.join(dist, 'mcp-build-id'),
|
|
156
|
+
path.join(dist, 'dist/mcp-build-id'),
|
|
157
|
+
path.join(dist, 'server/dist/mcp-build-id'),
|
|
158
|
+
];
|
|
159
|
+
for (const file of files) fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
160
|
+
const buildID = randomUUID();
|
|
161
|
+
for (const file of files) fs.writeFileSync(file, buildID, 'utf8');
|
|
162
|
+
NODE
|
|
163
|
+
fi
|
package/template/scripts/lint.js
CHANGED
|
@@ -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();
|