@lark-apaas/coding-template-nestjs-react-fullstack 0.1.41 → 0.1.42-alpha.20260916160525

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.
@@ -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": "concurrently -n \"server,client\" -c \"blue,green\" \"npm run type:check:server\" \"npm run type:check:client\"",
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.61",
33
+ "@lark-apaas/fullstack-nestjs-core": "1.1.62-alpha.20260916155509",
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",
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
- echo " ├─ 启动 server 构建..."
48
- npm run build:server > /tmp/build-server.log 2>&1 &
49
- SERVER_PID=$!
50
-
51
- echo " ├─ 启动 client 构建..."
52
- npm run build:client > /tmp/build-client.log 2>&1 &
53
- CLIENT_PID=$!
54
-
55
- # 等待两个构建完成
56
- SERVER_EXIT=0
57
- CLIENT_EXIT=0
58
-
59
- wait $SERVER_PID || SERVER_EXIT=$?
60
- wait $CLIENT_PID || CLIENT_EXIT=$?
61
-
62
- # 检查构建结果
63
- if [ $SERVER_EXIT -ne 0 ]; then
64
- echo " ❌ Server 构建失败"
65
- cat /tmp/build-server.log
66
- exit 1
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
- mkdir -p "$OUT_DIR/dist/client"
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
- cp -R "$ROOT_DIR/client/public/." "$OUT_DIR/dist/client/"
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
- # 构建产物 HTML 随后拷,覆盖 public 里的同名文件(保证入口页是构建版,不被 public 静默覆盖)
92
- cp "$ROOT_DIR/dist/client/"*.html "$OUT_DIR/dist/client/" || true
101
+ # 拷贝 run.sh 文件
102
+ cp "$ROOT_DIR/scripts/run.sh" "$OUT_DIR/"
93
103
 
94
- # 拷贝 run.sh 文件
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
- node "$ROOT_DIR/scripts/prune-smart.js"
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 "$OUT_DIR" | cut -f1)
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
- echo " node_modules: $NODE_MODULES_SIZE"
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
@@ -18,19 +18,31 @@ function runCommand(command, args) {
18
18
  shell: false,
19
19
  });
20
20
 
21
- child.on('close', (code) => resolve(code || 0));
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.warn(`[lint] Skip missing file: ${filePath}`);
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 commands = ['npm run eslint', 'npm run type:check'];
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
- commands.push('npm run stylelint');
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
- const code = await runCommand(getBinName('npx'), ['concurrently', ...commands]);
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.startsWith('client/')) {
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 tasks = [];
230
+ const taskSpecs = [];
139
231
 
140
232
  if (eslintFiles.length > 0) {
141
- tasks.push(runCommand(getBinName('npx'), ['eslint', '--quiet', ...eslintFiles]));
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
- tasks.push(runCommand(getBinName('npx'), ['stylelint', '--quiet', ...stylelintFiles]));
239
+ taskSpecs.push([getBinName('npx'), ['stylelint', '--quiet', ...stylelintFiles]]);
148
240
  }
149
241
 
150
242
  if (clientTypeFiles.length > 0) {
151
- tasks.push(runCommand(getBinName('npm'), ['run', 'type:check:client']));
243
+ taskSpecs.push([getBinName('npm'), ['run', 'type:check:client']]);
152
244
  }
153
245
 
154
246
  if (serverTypeFiles.length > 0) {
155
- tasks.push(runCommand(getBinName('npm'), ['run', 'type:check:server']));
247
+ taskSpecs.push([getBinName('npm'), ['run', 'type:check:server']]);
156
248
  }
157
249
 
158
- if (tasks.length === 0) {
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
- const results = await Promise.all(tasks);
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();
@@ -13,5 +13,6 @@
13
13
  "excludeDirectories": ["node_modules/**"]
14
14
  },
15
15
  "include": ["server/**/*", "shared/**/*.ts"],
16
- "exclude": ["node_modules", "dist", "client", "source_package", "**/*.spec.ts", "**/*.e2e-spec.ts"],
16
+ // MCP UI 是独立浏览器工程,不使用服务端的 TypeScript 配置。
17
+ "exclude": ["server/mcp/ui", "node_modules", "dist", "client", "source_package", "**/*.spec.ts", "**/*.e2e-spec.ts"],
17
18
  }