autosnippet 3.3.6 → 3.3.7
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/dashboard/dist/assets/icons-FHns2ypa.js +1 -0
- package/dashboard/dist/assets/index-BRJv5Y3r.js +135 -0
- package/dashboard/dist/assets/index-DzoB7kxK.css +1 -0
- package/dashboard/dist/index.html +3 -3
- package/dist/bin/cli.js +1 -0
- package/dist/lib/agent/AgentRuntime.d.ts +2 -2
- package/dist/lib/agent/AgentRuntime.js +26 -18
- package/dist/lib/agent/domain/ChatAgentTasks.js +4 -0
- package/dist/lib/agent/forced-summary.js +7 -2
- package/dist/lib/cli/AiScanService.js +4 -4
- package/dist/lib/core/discovery/ConfigWatcher.d.ts +64 -0
- package/dist/lib/core/discovery/ConfigWatcher.js +336 -0
- package/dist/lib/core/discovery/CustomConfigDiscoverer.d.ts +30 -0
- package/dist/lib/core/discovery/CustomConfigDiscoverer.js +1305 -0
- package/dist/lib/core/discovery/DiscovererPreference.d.ts +44 -0
- package/dist/lib/core/discovery/DiscovererPreference.js +141 -0
- package/dist/lib/core/discovery/DiscovererRegistry.d.ts +10 -1
- package/dist/lib/core/discovery/DiscovererRegistry.js +42 -2
- package/dist/lib/core/discovery/ProjectDiscoverer.d.ts +19 -0
- package/dist/lib/core/discovery/index.d.ts +2 -0
- package/dist/lib/core/discovery/index.js +4 -0
- package/dist/lib/core/discovery/parsers/CMakeParser.d.ts +32 -0
- package/dist/lib/core/discovery/parsers/CMakeParser.js +148 -0
- package/dist/lib/core/discovery/parsers/GradleDslParser.d.ts +43 -0
- package/dist/lib/core/discovery/parsers/GradleDslParser.js +171 -0
- package/dist/lib/core/discovery/parsers/JsonConfigParser.d.ts +45 -0
- package/dist/lib/core/discovery/parsers/JsonConfigParser.js +122 -0
- package/dist/lib/core/discovery/parsers/RubyDslParser.d.ts +49 -0
- package/dist/lib/core/discovery/parsers/RubyDslParser.js +282 -0
- package/dist/lib/core/discovery/parsers/StarlarkParser.d.ts +33 -0
- package/dist/lib/core/discovery/parsers/StarlarkParser.js +229 -0
- package/dist/lib/core/discovery/parsers/YamlConfigParser.d.ts +37 -0
- package/dist/lib/core/discovery/parsers/YamlConfigParser.js +212 -0
- package/dist/lib/domain/knowledge/KnowledgeEntry.d.ts +7 -1
- package/dist/lib/domain/knowledge/KnowledgeEntry.js +17 -3
- package/dist/lib/external/ai/AiProvider.d.ts +12 -0
- package/dist/lib/external/ai/AiProvider.js +24 -0
- package/dist/lib/external/ai/AiProviderManager.d.ts +101 -0
- package/dist/lib/external/ai/AiProviderManager.js +193 -0
- package/dist/lib/external/ai/providers/ClaudeProvider.js +11 -0
- package/dist/lib/external/ai/providers/GoogleGeminiProvider.js +18 -0
- package/dist/lib/external/ai/providers/MockProvider.d.ts +21 -3
- package/dist/lib/external/ai/providers/MockProvider.js +290 -14
- package/dist/lib/external/ai/providers/OpenAiProvider.js +16 -0
- package/dist/lib/external/lark/LarkTransport.d.ts +5 -1
- package/dist/lib/external/lark/LarkTransport.js +10 -2
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/mock-pipeline.d.ts +20 -0
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/mock-pipeline.js +432 -0
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +16 -8
- package/dist/lib/external/mcp/handlers/bootstrap/refine.js +8 -0
- package/dist/lib/external/mcp/handlers/bootstrap-external.d.ts +9 -0
- package/dist/lib/external/mcp/handlers/bootstrap-external.js +2 -0
- package/dist/lib/external/mcp/handlers/bootstrap-internal.js +2 -0
- package/dist/lib/external/mcp/handlers/consolidated.js +2 -1
- package/dist/lib/external/mcp/handlers/dimension-complete-external.js +2 -1
- package/dist/lib/external/mcp/handlers/evolve-external.js +5 -2
- package/dist/lib/external/mcp/handlers/knowledge.js +5 -4
- package/dist/lib/http/routes/ai.js +111 -30
- package/dist/lib/http/routes/candidates.js +11 -4
- package/dist/lib/http/routes/commands.js +10 -1
- package/dist/lib/http/routes/health.js +11 -0
- package/dist/lib/http/routes/modules.js +27 -0
- package/dist/lib/http/routes/recipes.js +7 -0
- package/dist/lib/http/utils/routeHelpers.js +2 -1
- package/dist/lib/injection/ServiceContainer.d.ts +6 -5
- package/dist/lib/injection/ServiceContainer.js +11 -27
- package/dist/lib/injection/ServiceMap.d.ts +2 -0
- package/dist/lib/injection/modules/AiModule.d.ts +6 -9
- package/dist/lib/injection/modules/AiModule.js +82 -39
- package/dist/lib/injection/modules/PanoramaModule.js +1 -1
- package/dist/lib/service/cleanup/CleanupService.d.ts +54 -7
- package/dist/lib/service/cleanup/CleanupService.js +284 -37
- package/dist/lib/service/knowledge/CodeEntityGraph.d.ts +6 -0
- package/dist/lib/service/knowledge/CodeEntityGraph.js +16 -0
- package/dist/lib/service/knowledge/KnowledgeService.js +23 -10
- package/dist/lib/service/module/ModuleService.js +10 -19
- package/dist/lib/service/panorama/CouplingAnalyzer.d.ts +10 -1
- package/dist/lib/service/panorama/CouplingAnalyzer.js +44 -2
- package/dist/lib/service/panorama/DimensionAnalyzer.d.ts +1 -1
- package/dist/lib/service/panorama/DimensionAnalyzer.js +31 -17
- package/dist/lib/service/panorama/LayerInferrer.d.ts +16 -1
- package/dist/lib/service/panorama/LayerInferrer.js +118 -1
- package/dist/lib/service/panorama/ModuleDiscoverer.d.ts +9 -0
- package/dist/lib/service/panorama/ModuleDiscoverer.js +58 -2
- package/dist/lib/service/panorama/PanoramaAggregator.d.ts +6 -2
- package/dist/lib/service/panorama/PanoramaAggregator.js +84 -6
- package/dist/lib/service/panorama/PanoramaScanner.js +28 -0
- package/dist/lib/service/panorama/PanoramaService.js +10 -5
- package/dist/lib/service/panorama/PanoramaTypes.d.ts +38 -0
- package/dist/lib/service/panorama/RoleRefiner.d.ts +2 -0
- package/dist/lib/service/panorama/RoleRefiner.js +41 -0
- package/dist/lib/service/panorama/TechStackProfiler.d.ts +13 -0
- package/dist/lib/service/panorama/TechStackProfiler.js +191 -0
- package/dist/lib/service/skills/SignalCollector.d.ts +1 -0
- package/dist/lib/service/skills/SignalCollector.js +6 -5
- package/dist/lib/service/vector/ContextualEnricher.d.ts +1 -0
- package/dist/lib/service/vector/ContextualEnricher.js +4 -0
- package/dist/lib/shared/LanguageService.js +3 -0
- package/dist/lib/shared/developer-identity.d.ts +18 -0
- package/dist/lib/shared/developer-identity.js +62 -0
- package/dist/lib/shared/schemas/http-requests.d.ts +8 -17
- package/dist/lib/shared/schemas/http-requests.js +9 -6
- package/dist/lib/types/knowledge-wire.d.ts +1 -0
- package/package.json +1 -1
- package/dashboard/dist/assets/icons-D1aVZYFW.js +0 -1
- package/dashboard/dist/assets/index-CxHOu8Hd.css +0 -1
- package/dashboard/dist/assets/index-DDdAOpYT.js +0 -128
|
@@ -0,0 +1,1305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module CustomConfigDiscoverer
|
|
3
|
+
* @description 自研配置文件发现器 — 识别使用非标准/自研构建系统的项目
|
|
4
|
+
*
|
|
5
|
+
* 两级检测策略:
|
|
6
|
+
* Level 1: 已知自研工具指纹匹配 (confidence 0.70-0.80)
|
|
7
|
+
* Level 2: 启发式目录结构探测 (confidence 0.50-0.65)
|
|
8
|
+
*
|
|
9
|
+
* 当前支持:
|
|
10
|
+
* - Baidu EasyBox (Boxfile + *.boxspec)
|
|
11
|
+
* - Tuist (Project.swift)
|
|
12
|
+
* - XcodeGen (project.yml)
|
|
13
|
+
*
|
|
14
|
+
* 设计文档: docs/copilot/custom-config-discoverer-design.md
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { extname, join, relative } from 'node:path';
|
|
18
|
+
import { getProjectSpecPath } from '#infra/config/Paths.js';
|
|
19
|
+
import { LanguageService } from '#shared/LanguageService.js';
|
|
20
|
+
import { ProjectDiscoverer, } from './ProjectDiscoverer.js';
|
|
21
|
+
import { parseCMakeProject } from './parsers/CMakeParser.js';
|
|
22
|
+
import { inferConventionRole, parseGradleProject } from './parsers/GradleDslParser.js';
|
|
23
|
+
import { parseFlutterPluginsDeps, parseNxWorkspace, parseReactNativeProject, } from './parsers/JsonConfigParser.js';
|
|
24
|
+
import { parseBoxfile, parseModuleSpec, } from './parsers/RubyDslParser.js';
|
|
25
|
+
import { parseStarlarkBuildFile, RULE_TO_LANGUAGE, } from './parsers/StarlarkParser.js';
|
|
26
|
+
import { parseMelosProject, parseXcodeGenProject, parseXcodeGenTarget, } from './parsers/YamlConfigParser.js';
|
|
27
|
+
const KNOWN_CUSTOM_SYSTEMS = Object.freeze([
|
|
28
|
+
// ── Tier 1: Bazel / Buck2 (Starlark) ──
|
|
29
|
+
{
|
|
30
|
+
id: 'bazel',
|
|
31
|
+
displayName: 'Bazel',
|
|
32
|
+
markers: ['MODULE.bazel', 'WORKSPACE', 'WORKSPACE.bazel'],
|
|
33
|
+
markerStrategy: 'any',
|
|
34
|
+
moduleSpecPattern: 'BUILD.bazel',
|
|
35
|
+
language: Object.freeze([]),
|
|
36
|
+
confidence: 0.85,
|
|
37
|
+
parser: 'starlark',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'buck2',
|
|
41
|
+
displayName: 'Buck2',
|
|
42
|
+
markers: ['.buckconfig', '.buckroot'],
|
|
43
|
+
markerStrategy: 'any',
|
|
44
|
+
moduleSpecPattern: 'BUCK',
|
|
45
|
+
language: Object.freeze([]),
|
|
46
|
+
confidence: 0.85,
|
|
47
|
+
parser: 'starlark',
|
|
48
|
+
},
|
|
49
|
+
// ── Tier 1: Android Gradle Convention Plugins ──
|
|
50
|
+
{
|
|
51
|
+
id: 'gradle-convention',
|
|
52
|
+
displayName: 'Gradle Convention Plugins',
|
|
53
|
+
markers: ['build-logic/convention/', 'buildSrc/src/main/kotlin/'],
|
|
54
|
+
markerStrategy: 'any',
|
|
55
|
+
moduleSpecPattern: null,
|
|
56
|
+
language: Object.freeze(['kotlin', 'java']),
|
|
57
|
+
confidence: 0.8,
|
|
58
|
+
parser: 'gradle-dsl',
|
|
59
|
+
},
|
|
60
|
+
// ── Tier 1: Flutter Melos ──
|
|
61
|
+
{
|
|
62
|
+
id: 'melos',
|
|
63
|
+
displayName: 'Melos (Flutter Monorepo)',
|
|
64
|
+
markers: ['melos.yaml'],
|
|
65
|
+
moduleSpecPattern: null,
|
|
66
|
+
language: Object.freeze(['dart']),
|
|
67
|
+
confidence: 0.82,
|
|
68
|
+
parser: 'yaml',
|
|
69
|
+
},
|
|
70
|
+
// ── Tier 1: iOS 生态 ──
|
|
71
|
+
{
|
|
72
|
+
id: 'easybox',
|
|
73
|
+
displayName: 'Baidu EasyBox',
|
|
74
|
+
markers: ['Boxfile'],
|
|
75
|
+
moduleSpecPattern: '*.boxspec',
|
|
76
|
+
language: Object.freeze(['objectivec', 'swift']),
|
|
77
|
+
confidence: 0.8,
|
|
78
|
+
parser: 'ruby-dsl',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'tuist',
|
|
82
|
+
displayName: 'Tuist',
|
|
83
|
+
markers: ['Tuist/Config.swift', 'Project.swift'],
|
|
84
|
+
moduleSpecPattern: null,
|
|
85
|
+
language: Object.freeze(['swift']),
|
|
86
|
+
confidence: 0.8,
|
|
87
|
+
parser: 'swift-dsl',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'ks-component',
|
|
91
|
+
displayName: 'KSComponent (快手)',
|
|
92
|
+
markers: ['KSPodfile', 'Podfile.ks'],
|
|
93
|
+
markerStrategy: 'any',
|
|
94
|
+
moduleSpecPattern: '*.podspec',
|
|
95
|
+
language: Object.freeze(['swift', 'objectivec']),
|
|
96
|
+
confidence: 0.8,
|
|
97
|
+
parser: 'ruby-dsl',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'mt-component',
|
|
101
|
+
displayName: 'MTComponent (美团)',
|
|
102
|
+
markers: ['MTModulefile', 'MTConfig.yml'],
|
|
103
|
+
markerStrategy: 'any',
|
|
104
|
+
moduleSpecPattern: '*.podspec',
|
|
105
|
+
language: Object.freeze(['swift', 'objectivec']),
|
|
106
|
+
confidence: 0.78,
|
|
107
|
+
parser: 'ruby-dsl',
|
|
108
|
+
},
|
|
109
|
+
// ── Tier 1: 混合架构 ──
|
|
110
|
+
{
|
|
111
|
+
id: 'flutter-add-to-app',
|
|
112
|
+
displayName: 'Flutter Add-to-App',
|
|
113
|
+
markers: ['.flutter-plugins-dependencies', '.flutter-plugins'],
|
|
114
|
+
markerStrategy: 'any',
|
|
115
|
+
moduleSpecPattern: 'pubspec.yaml',
|
|
116
|
+
language: Object.freeze(['dart']),
|
|
117
|
+
confidence: 0.78,
|
|
118
|
+
parser: 'json-config',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'react-native-hybrid',
|
|
122
|
+
displayName: 'React Native Hybrid',
|
|
123
|
+
markers: ['metro.config.js', 'metro.config.ts', 'react-native.config.js'],
|
|
124
|
+
markerStrategy: 'any',
|
|
125
|
+
moduleSpecPattern: null,
|
|
126
|
+
language: Object.freeze(['typescript', 'javascript']),
|
|
127
|
+
confidence: 0.78,
|
|
128
|
+
parser: 'json-config',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'kotlin-multiplatform',
|
|
132
|
+
displayName: 'Kotlin Multiplatform',
|
|
133
|
+
markers: ['shared/build.gradle.kts'],
|
|
134
|
+
moduleSpecPattern: null,
|
|
135
|
+
language: Object.freeze(['kotlin']),
|
|
136
|
+
confidence: 0.78,
|
|
137
|
+
parser: 'gradle-dsl',
|
|
138
|
+
},
|
|
139
|
+
// ── Tier 2: Nx / Pants / CMake ──
|
|
140
|
+
{
|
|
141
|
+
id: 'nx-monorepo',
|
|
142
|
+
displayName: 'Nx Monorepo',
|
|
143
|
+
markers: ['nx.json'],
|
|
144
|
+
moduleSpecPattern: 'project.json',
|
|
145
|
+
language: Object.freeze(['typescript', 'javascript']),
|
|
146
|
+
confidence: 0.8,
|
|
147
|
+
parser: 'json-config',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'pants',
|
|
151
|
+
displayName: 'Pants Build',
|
|
152
|
+
markers: ['pants.toml'],
|
|
153
|
+
moduleSpecPattern: 'BUILD',
|
|
154
|
+
language: Object.freeze([]),
|
|
155
|
+
confidence: 0.8,
|
|
156
|
+
parser: 'starlark',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'cmake-multiproject',
|
|
160
|
+
displayName: 'CMake Multi-Project',
|
|
161
|
+
markers: ['CMakeLists.txt'],
|
|
162
|
+
antiMarkers: ['MODULE.bazel', 'WORKSPACE', 'meson.build'],
|
|
163
|
+
moduleSpecPattern: 'CMakeLists.txt',
|
|
164
|
+
language: Object.freeze(['cpp', 'c']),
|
|
165
|
+
confidence: 0.75,
|
|
166
|
+
parser: 'cmake',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: 'xcodegen',
|
|
170
|
+
displayName: 'XcodeGen',
|
|
171
|
+
markers: ['project.yml', 'project.yaml'],
|
|
172
|
+
markerStrategy: 'any',
|
|
173
|
+
moduleSpecPattern: null,
|
|
174
|
+
language: Object.freeze(['swift', 'objectivec']),
|
|
175
|
+
confidence: 0.75,
|
|
176
|
+
parser: 'yaml',
|
|
177
|
+
},
|
|
178
|
+
]);
|
|
179
|
+
const HEURISTIC_SIGNALS = Object.freeze([
|
|
180
|
+
{ pattern: /^(Local)?Modules?$/i, type: 'module-dir', boost: 0.15 },
|
|
181
|
+
{ pattern: /^Packages$/i, type: 'module-dir', boost: 0.1 },
|
|
182
|
+
{ pattern: /^[A-Z]\w+file$/, type: 'custom-dsl', boost: 0.2 },
|
|
183
|
+
{ pattern: /\.\w+spec$/, type: 'spec-file', boost: 0.2 },
|
|
184
|
+
{ pattern: /\.xcodeproj$/, type: 'xcode', boost: 0.05 },
|
|
185
|
+
]);
|
|
186
|
+
// 排除已知的标准 Ruby DSL 文件
|
|
187
|
+
const KNOWN_STANDARD_FILES = new Set([
|
|
188
|
+
'Gemfile',
|
|
189
|
+
'Podfile',
|
|
190
|
+
'Fastfile',
|
|
191
|
+
'Rakefile',
|
|
192
|
+
'Vagrantfile',
|
|
193
|
+
'Guardfile',
|
|
194
|
+
'Brewfile',
|
|
195
|
+
'Berksfile',
|
|
196
|
+
'Capfile',
|
|
197
|
+
]);
|
|
198
|
+
const EXCLUDE_DIRS = new Set([
|
|
199
|
+
'node_modules',
|
|
200
|
+
'.git',
|
|
201
|
+
'.cursor',
|
|
202
|
+
'dist',
|
|
203
|
+
'build',
|
|
204
|
+
'out',
|
|
205
|
+
'.build',
|
|
206
|
+
'Pods',
|
|
207
|
+
'Carthage',
|
|
208
|
+
'DerivedData',
|
|
209
|
+
'__pycache__',
|
|
210
|
+
'.venv',
|
|
211
|
+
'venv',
|
|
212
|
+
'.gradle',
|
|
213
|
+
'coverage',
|
|
214
|
+
'.cache',
|
|
215
|
+
'.easybox',
|
|
216
|
+
]);
|
|
217
|
+
const SOURCE_EXTENSIONS = new Set(['.m', '.h', '.swift', '.mm', '.c', '.cpp', '.cc']);
|
|
218
|
+
// ── User Custom Systems (boxspec.json) ──────────────
|
|
219
|
+
/**
|
|
220
|
+
* 从 boxspec.json 读取用户自定义配置系统
|
|
221
|
+
*
|
|
222
|
+
* boxspec.json 中可选字段:
|
|
223
|
+
* ```json
|
|
224
|
+
* {
|
|
225
|
+
* "customDiscoverer": {
|
|
226
|
+
* "id": "my-build-tool",
|
|
227
|
+
* "displayName": "MyBuildTool",
|
|
228
|
+
* "markers": ["MyBuildfile"],
|
|
229
|
+
* "moduleSpecPattern": "*.myspec",
|
|
230
|
+
* "language": ["swift"],
|
|
231
|
+
* "confidence": 0.85,
|
|
232
|
+
* "parser": "ruby-dsl"
|
|
233
|
+
* }
|
|
234
|
+
* }
|
|
235
|
+
* ```
|
|
236
|
+
* 或数组形式支持多个自定义系统。
|
|
237
|
+
*/
|
|
238
|
+
function loadUserCustomSystems(projectRoot) {
|
|
239
|
+
try {
|
|
240
|
+
const specPath = getProjectSpecPath(projectRoot);
|
|
241
|
+
if (!existsSync(specPath)) {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
const raw = JSON.parse(readFileSync(specPath, 'utf-8'));
|
|
245
|
+
const custom = raw?.customDiscoverer;
|
|
246
|
+
if (!custom) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
const items = Array.isArray(custom) ? custom : [custom];
|
|
250
|
+
const results = [];
|
|
251
|
+
for (const item of items) {
|
|
252
|
+
if (!item?.id || !item?.markers || !Array.isArray(item.markers)) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
results.push({
|
|
256
|
+
id: String(item.id),
|
|
257
|
+
displayName: String(item.displayName ?? item.id),
|
|
258
|
+
markers: item.markers.map(String),
|
|
259
|
+
moduleSpecPattern: item.moduleSpecPattern ? String(item.moduleSpecPattern) : null,
|
|
260
|
+
language: Array.isArray(item.language) ? item.language.map(String) : ['swift'],
|
|
261
|
+
confidence: typeof item.confidence === 'number' ? item.confidence : 0.75,
|
|
262
|
+
parser: [
|
|
263
|
+
'ruby-dsl',
|
|
264
|
+
'yaml',
|
|
265
|
+
'swift-dsl',
|
|
266
|
+
'starlark',
|
|
267
|
+
'gradle-dsl',
|
|
268
|
+
'cmake',
|
|
269
|
+
'json-config',
|
|
270
|
+
].includes(item.parser)
|
|
271
|
+
? item.parser
|
|
272
|
+
: 'ruby-dsl',
|
|
273
|
+
markerStrategy: ['all', 'any', 'ordered'].includes(item.markerStrategy)
|
|
274
|
+
? item.markerStrategy
|
|
275
|
+
: undefined,
|
|
276
|
+
antiMarkers: Array.isArray(item.antiMarkers) ? item.antiMarkers.map(String) : undefined,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return results;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* 获取合并后的系统配置表:用户自定义 + 内置
|
|
287
|
+
* 用户自定义系统优先匹配
|
|
288
|
+
*/
|
|
289
|
+
function getEffectiveSystemProfiles(projectRoot) {
|
|
290
|
+
const userSystems = loadUserCustomSystems(projectRoot);
|
|
291
|
+
if (userSystems.length === 0) {
|
|
292
|
+
return KNOWN_CUSTOM_SYSTEMS;
|
|
293
|
+
}
|
|
294
|
+
return [...userSystems, ...KNOWN_CUSTOM_SYSTEMS];
|
|
295
|
+
}
|
|
296
|
+
// ── CustomConfigDiscoverer ──────────────────────────
|
|
297
|
+
export class CustomConfigDiscoverer extends ProjectDiscoverer {
|
|
298
|
+
#projectRoot = null;
|
|
299
|
+
#matchedSystem = null;
|
|
300
|
+
#parsedConfig = null;
|
|
301
|
+
#moduleSpecs = new Map();
|
|
302
|
+
#targets = [];
|
|
303
|
+
get id() {
|
|
304
|
+
return 'customConfig';
|
|
305
|
+
}
|
|
306
|
+
get displayName() {
|
|
307
|
+
if (this.#matchedSystem) {
|
|
308
|
+
return `Custom Config (${this.#matchedSystem.displayName})`;
|
|
309
|
+
}
|
|
310
|
+
return 'Custom Config (Heuristic)';
|
|
311
|
+
}
|
|
312
|
+
// ── detect ────────────────────────────────────────
|
|
313
|
+
async detect(projectRoot) {
|
|
314
|
+
// Level 1: 已知自研工具指纹匹配(含用户自定义系统)
|
|
315
|
+
const systems = getEffectiveSystemProfiles(projectRoot);
|
|
316
|
+
for (const system of systems) {
|
|
317
|
+
// antiMarkers 排除检查
|
|
318
|
+
if (system.antiMarkers?.some((am) => existsSync(join(projectRoot, am)))) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const strategy = system.markerStrategy ?? 'all';
|
|
322
|
+
let markerFound = false;
|
|
323
|
+
if (strategy === 'any') {
|
|
324
|
+
markerFound = system.markers.some((marker) => existsSync(join(projectRoot, marker)));
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
// 'all' 和 'ordered' 都要求所有 markers 存在(ordered 未来可扩展)
|
|
328
|
+
markerFound = system.markers.every((marker) => existsSync(join(projectRoot, marker)));
|
|
329
|
+
}
|
|
330
|
+
if (markerFound) {
|
|
331
|
+
return {
|
|
332
|
+
match: true,
|
|
333
|
+
confidence: system.confidence,
|
|
334
|
+
reason: `${system.displayName} detected (${system.markers.join(', ')})`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Level 2: 启发式目录结构探测
|
|
339
|
+
let heuristicScore = 0.35; // 基础分
|
|
340
|
+
const signals = [];
|
|
341
|
+
try {
|
|
342
|
+
const entries = readdirSync(projectRoot, { withFileTypes: true });
|
|
343
|
+
for (const entry of entries) {
|
|
344
|
+
if (entry.name.startsWith('.')) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
for (const signal of HEURISTIC_SIGNALS) {
|
|
348
|
+
if (signal.pattern.test(entry.name)) {
|
|
349
|
+
// 排除已知的标准文件
|
|
350
|
+
if (signal.type === 'custom-dsl' && KNOWN_STANDARD_FILES.has(entry.name)) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
// 对 module-dir 类型,要求目录内有多个子目录
|
|
354
|
+
if (signal.type === 'module-dir' && entry.isDirectory()) {
|
|
355
|
+
const subCount = countSubdirsWithSpecs(join(projectRoot, entry.name));
|
|
356
|
+
if (subCount < 2) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
heuristicScore += signal.boost;
|
|
361
|
+
signals.push(`${entry.name} (${signal.type})`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
/* skip */
|
|
368
|
+
}
|
|
369
|
+
// 限制最高分
|
|
370
|
+
heuristicScore = Math.min(heuristicScore, 0.65);
|
|
371
|
+
if (heuristicScore >= 0.5 && signals.length >= 2) {
|
|
372
|
+
return {
|
|
373
|
+
match: true,
|
|
374
|
+
confidence: heuristicScore,
|
|
375
|
+
reason: `Heuristic signals: ${signals.join(', ')}`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return { match: false, confidence: 0, reason: 'No custom config detected' };
|
|
379
|
+
}
|
|
380
|
+
// ── load ──────────────────────────────────────────
|
|
381
|
+
async load(projectRoot) {
|
|
382
|
+
this.#projectRoot = projectRoot;
|
|
383
|
+
this.#parsedConfig = null;
|
|
384
|
+
this.#moduleSpecs.clear();
|
|
385
|
+
this.#targets = [];
|
|
386
|
+
// 确定匹配的系统(含用户自定义系统)
|
|
387
|
+
this.#matchedSystem = null;
|
|
388
|
+
const systems = getEffectiveSystemProfiles(projectRoot);
|
|
389
|
+
for (const system of systems) {
|
|
390
|
+
if (system.antiMarkers?.some((am) => existsSync(join(projectRoot, am)))) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const strategy = system.markerStrategy ?? 'all';
|
|
394
|
+
const markerFound = strategy === 'any'
|
|
395
|
+
? system.markers.some((marker) => existsSync(join(projectRoot, marker)))
|
|
396
|
+
: system.markers.every((marker) => existsSync(join(projectRoot, marker)));
|
|
397
|
+
if (markerFound) {
|
|
398
|
+
this.#matchedSystem = system;
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (!this.#matchedSystem) {
|
|
403
|
+
this.#loadHeuristic(projectRoot);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
switch (this.#matchedSystem.parser) {
|
|
407
|
+
case 'ruby-dsl':
|
|
408
|
+
this.#loadRubyDsl(projectRoot);
|
|
409
|
+
break;
|
|
410
|
+
case 'yaml':
|
|
411
|
+
this.#loadYaml(projectRoot);
|
|
412
|
+
break;
|
|
413
|
+
case 'starlark':
|
|
414
|
+
this.#loadStarlark(projectRoot);
|
|
415
|
+
break;
|
|
416
|
+
case 'gradle-dsl':
|
|
417
|
+
this.#loadGradleDsl(projectRoot);
|
|
418
|
+
break;
|
|
419
|
+
case 'cmake':
|
|
420
|
+
this.#loadCMake(projectRoot);
|
|
421
|
+
break;
|
|
422
|
+
case 'json-config':
|
|
423
|
+
this.#loadJsonConfig(projectRoot);
|
|
424
|
+
break;
|
|
425
|
+
default:
|
|
426
|
+
this.#loadHeuristic(projectRoot);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// ── listTargets ───────────────────────────────────
|
|
430
|
+
async listTargets() {
|
|
431
|
+
return this.#targets;
|
|
432
|
+
}
|
|
433
|
+
// ── getTargetFiles ────────────────────────────────
|
|
434
|
+
async getTargetFiles(target) {
|
|
435
|
+
const targetPath = typeof target === 'string' ? this.#targets.find((t) => t.name === target)?.path : target.path;
|
|
436
|
+
if (!targetPath || !existsSync(targetPath)) {
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
// 如果有 spec 文件,优先使用 sources 字段定位
|
|
440
|
+
const targetName = typeof target === 'string' ? target : target.name;
|
|
441
|
+
const spec = this.#moduleSpecs.get(targetName);
|
|
442
|
+
let sourceDir = targetPath;
|
|
443
|
+
if (spec?.sources) {
|
|
444
|
+
const specSourceDir = join(targetPath, spec.sources);
|
|
445
|
+
if (existsSync(specSourceDir)) {
|
|
446
|
+
sourceDir = specSourceDir;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const files = [];
|
|
450
|
+
this.#collectSourceFiles(sourceDir, targetPath, files);
|
|
451
|
+
return files;
|
|
452
|
+
}
|
|
453
|
+
// ── getDependencyGraph ────────────────────────────
|
|
454
|
+
async getDependencyGraph() {
|
|
455
|
+
if (!this.#parsedConfig) {
|
|
456
|
+
return { nodes: this.#targets.map((t) => t.name), edges: [] };
|
|
457
|
+
}
|
|
458
|
+
const config = this.#parsedConfig;
|
|
459
|
+
const nodes = [];
|
|
460
|
+
const edges = [];
|
|
461
|
+
const nodeIds = new Set();
|
|
462
|
+
// 宿主应用节点
|
|
463
|
+
if (config.hostApp) {
|
|
464
|
+
const hostId = config.hostApp.name;
|
|
465
|
+
nodes.push({
|
|
466
|
+
id: hostId,
|
|
467
|
+
label: hostId,
|
|
468
|
+
type: 'host',
|
|
469
|
+
version: config.hostApp.version,
|
|
470
|
+
});
|
|
471
|
+
nodeIds.add(hostId);
|
|
472
|
+
}
|
|
473
|
+
// 遍历所有层级,添加模块节点
|
|
474
|
+
for (const layer of config.layers) {
|
|
475
|
+
for (const mod of layer.modules) {
|
|
476
|
+
if (nodeIds.has(mod.name)) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
nodeIds.add(mod.name);
|
|
480
|
+
nodes.push({
|
|
481
|
+
id: mod.name,
|
|
482
|
+
label: mod.name,
|
|
483
|
+
type: mod.isLocal ? 'local' : 'external',
|
|
484
|
+
layer: layer.name,
|
|
485
|
+
version: mod.version || undefined,
|
|
486
|
+
group: mod.group || undefined,
|
|
487
|
+
fullPath: mod.isLocal && mod.localPath && this.#projectRoot
|
|
488
|
+
? join(this.#projectRoot, mod.localPath)
|
|
489
|
+
: undefined,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// 全局依赖
|
|
494
|
+
for (const mod of config.globalDependencies) {
|
|
495
|
+
if (nodeIds.has(mod.name)) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
nodeIds.add(mod.name);
|
|
499
|
+
nodes.push({
|
|
500
|
+
id: mod.name,
|
|
501
|
+
label: mod.name,
|
|
502
|
+
type: mod.isLocal ? 'local' : 'external',
|
|
503
|
+
version: mod.version || undefined,
|
|
504
|
+
group: mod.group || undefined,
|
|
505
|
+
fullPath: mod.isLocal && mod.localPath && this.#projectRoot
|
|
506
|
+
? join(this.#projectRoot, mod.localPath)
|
|
507
|
+
: undefined,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
// 从 boxspec 依赖声明生成边
|
|
511
|
+
for (const [moduleName, spec] of this.#moduleSpecs) {
|
|
512
|
+
for (const depName of spec.dependencies) {
|
|
513
|
+
// 确保依赖目标存在于节点列表中
|
|
514
|
+
if (!nodeIds.has(depName)) {
|
|
515
|
+
nodeIds.add(depName);
|
|
516
|
+
nodes.push({
|
|
517
|
+
id: depName,
|
|
518
|
+
label: depName,
|
|
519
|
+
type: 'external',
|
|
520
|
+
indirect: true,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
edges.push({
|
|
524
|
+
from: moduleName,
|
|
525
|
+
to: depName,
|
|
526
|
+
type: 'depends_on',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// 宿主应用 → 所有本地模块的 contains 关系
|
|
531
|
+
if (config.hostApp) {
|
|
532
|
+
for (const layer of config.layers) {
|
|
533
|
+
for (const mod of layer.modules) {
|
|
534
|
+
if (mod.isLocal) {
|
|
535
|
+
edges.push({
|
|
536
|
+
from: config.hostApp.name,
|
|
537
|
+
to: mod.name,
|
|
538
|
+
type: 'contains',
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// 层级元数据
|
|
545
|
+
const layers = config.layers.map((l) => ({
|
|
546
|
+
name: l.name,
|
|
547
|
+
order: l.order,
|
|
548
|
+
accessibleLayers: l.accessibleLayers,
|
|
549
|
+
}));
|
|
550
|
+
return { nodes, edges, layers };
|
|
551
|
+
}
|
|
552
|
+
// ── Private: Ruby DSL 加载 ─────────────────────────
|
|
553
|
+
#loadRubyDsl(projectRoot) {
|
|
554
|
+
// 读取 Boxfile
|
|
555
|
+
const boxfilePath = join(projectRoot, 'Boxfile');
|
|
556
|
+
if (!existsSync(boxfilePath)) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
let content;
|
|
560
|
+
try {
|
|
561
|
+
content = readFileSync(boxfilePath, 'utf8');
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
// 解析 Boxfile
|
|
567
|
+
this.#parsedConfig = parseBoxfile(content);
|
|
568
|
+
// 尝试合并 Boxfile.local 覆盖
|
|
569
|
+
this.#mergeLocalOverrides(projectRoot);
|
|
570
|
+
// 遍历本地模块,解析 spec 文件
|
|
571
|
+
const allModules = [
|
|
572
|
+
...this.#parsedConfig.layers.flatMap((l) => l.modules),
|
|
573
|
+
...this.#parsedConfig.globalDependencies,
|
|
574
|
+
];
|
|
575
|
+
for (const mod of allModules) {
|
|
576
|
+
if (!mod.isLocal || !mod.localPath) {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const modulePath = join(projectRoot, mod.localPath);
|
|
580
|
+
if (!existsSync(modulePath)) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
// 查找 spec 文件
|
|
584
|
+
const specPath = this.#findSpecFile(modulePath, mod.name);
|
|
585
|
+
if (specPath) {
|
|
586
|
+
try {
|
|
587
|
+
const specContent = readFileSync(specPath, 'utf8');
|
|
588
|
+
const spec = parseModuleSpec(specContent);
|
|
589
|
+
this.#moduleSpecs.set(mod.name, spec);
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
/* skip unreadable spec */
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// 构建 targets(仅 local 模块 + 宿主应用)
|
|
597
|
+
this.#buildTargets(projectRoot);
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* 合并 Boxfile.local 中的覆盖配置
|
|
601
|
+
* Boxfile.local 中 :path 覆盖可以将远程依赖切换为本地源码
|
|
602
|
+
*/
|
|
603
|
+
#mergeLocalOverrides(projectRoot) {
|
|
604
|
+
const localPath = join(projectRoot, 'Boxfile.local');
|
|
605
|
+
if (!existsSync(localPath)) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
const localContent = readFileSync(localPath, 'utf8');
|
|
610
|
+
const localConfig = parseBoxfile(localContent);
|
|
611
|
+
if (!this.#parsedConfig) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
// 合并本地覆盖:将 Boxfile.local 中的 local module 覆盖到主配置
|
|
615
|
+
const allLocalModules = localConfig.layers.flatMap((l) => l.modules);
|
|
616
|
+
for (const localMod of allLocalModules) {
|
|
617
|
+
if (!localMod.isLocal) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
// 查找主配置中的同名模块并覆盖
|
|
621
|
+
const configLayers = this.#parsedConfig.layers;
|
|
622
|
+
for (const layer of configLayers) {
|
|
623
|
+
const existingIdx = layer.modules.findIndex((m) => m.name === localMod.name);
|
|
624
|
+
if (existingIdx >= 0) {
|
|
625
|
+
layer.modules[existingIdx] = { ...layer.modules[existingIdx], ...localMod };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
/* skip */
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* 在模块目录中查找 spec 文件
|
|
636
|
+
* 查找顺序: ModuleName.boxspec → ModuleName.podspec → 任意 *.boxspec → 任意 *.podspec
|
|
637
|
+
*/
|
|
638
|
+
#findSpecFile(modulePath, moduleName) {
|
|
639
|
+
// 精确匹配
|
|
640
|
+
for (const ext of ['.boxspec', '.podspec']) {
|
|
641
|
+
const exactPath = join(modulePath, `${moduleName}${ext}`);
|
|
642
|
+
if (existsSync(exactPath)) {
|
|
643
|
+
return exactPath;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// 模糊匹配
|
|
647
|
+
try {
|
|
648
|
+
const entries = readdirSync(modulePath);
|
|
649
|
+
for (const entry of entries) {
|
|
650
|
+
if (entry.endsWith('.boxspec') || entry.endsWith('.podspec')) {
|
|
651
|
+
return join(modulePath, entry);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
/* skip */
|
|
657
|
+
}
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* 从解析结果构建 Target 列表
|
|
662
|
+
* 仅包含本地模块和宿主应用(有源码可收集的目标)
|
|
663
|
+
*/
|
|
664
|
+
#buildTargets(projectRoot) {
|
|
665
|
+
if (!this.#parsedConfig) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const config = this.#parsedConfig;
|
|
669
|
+
const primaryLang = this.#matchedSystem?.language[0] || 'objectivec';
|
|
670
|
+
// 宿主应用
|
|
671
|
+
if (config.hostApp) {
|
|
672
|
+
const hostDir = join(projectRoot, config.hostApp.name);
|
|
673
|
+
if (existsSync(hostDir)) {
|
|
674
|
+
this.#targets.push({
|
|
675
|
+
name: config.hostApp.name,
|
|
676
|
+
path: hostDir,
|
|
677
|
+
type: 'application',
|
|
678
|
+
language: primaryLang,
|
|
679
|
+
metadata: {
|
|
680
|
+
layer: 'Application',
|
|
681
|
+
version: config.hostApp.version,
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// 所有层级中的本地模块
|
|
687
|
+
for (const layer of config.layers) {
|
|
688
|
+
for (const mod of layer.modules) {
|
|
689
|
+
if (!mod.isLocal || !mod.localPath) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const modulePath = join(projectRoot, mod.localPath);
|
|
693
|
+
if (!existsSync(modulePath)) {
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
this.#targets.push({
|
|
697
|
+
name: mod.name,
|
|
698
|
+
path: modulePath,
|
|
699
|
+
type: 'library',
|
|
700
|
+
language: primaryLang,
|
|
701
|
+
metadata: {
|
|
702
|
+
layer: layer.name,
|
|
703
|
+
version: mod.version,
|
|
704
|
+
group: mod.group,
|
|
705
|
+
specFile: this.#moduleSpecs.has(mod.name),
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
// 全局本地模块
|
|
711
|
+
for (const mod of config.globalDependencies) {
|
|
712
|
+
if (!mod.isLocal || !mod.localPath) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const modulePath = join(projectRoot, mod.localPath);
|
|
716
|
+
if (!existsSync(modulePath)) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
// 避免重复
|
|
720
|
+
if (this.#targets.some((t) => t.name === mod.name)) {
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
this.#targets.push({
|
|
724
|
+
name: mod.name,
|
|
725
|
+
path: modulePath,
|
|
726
|
+
type: 'library',
|
|
727
|
+
language: primaryLang,
|
|
728
|
+
metadata: {
|
|
729
|
+
version: mod.version,
|
|
730
|
+
group: mod.group,
|
|
731
|
+
specFile: this.#moduleSpecs.has(mod.name),
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// ── Private: YAML 加载 (XcodeGen) ──────────────────
|
|
737
|
+
#loadYaml(projectRoot) {
|
|
738
|
+
const system = this.#matchedSystem;
|
|
739
|
+
// 查找可用的 YAML 配置文件
|
|
740
|
+
let yamlContent = null;
|
|
741
|
+
for (const marker of system.markers) {
|
|
742
|
+
const markerPath = join(projectRoot, marker);
|
|
743
|
+
if (existsSync(markerPath)) {
|
|
744
|
+
try {
|
|
745
|
+
yamlContent = readFileSync(markerPath, 'utf-8');
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
/* 跳过不可读文件 */
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (!yamlContent) {
|
|
754
|
+
this.#loadHeuristic(projectRoot);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
// Melos 项目走专用加载路径
|
|
758
|
+
if (system.id === 'melos') {
|
|
759
|
+
this.#loadMelos(projectRoot, yamlContent);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
// 解析 project.yml
|
|
763
|
+
const config = parseXcodeGenProject(yamlContent);
|
|
764
|
+
this.#parsedConfig = config;
|
|
765
|
+
const primaryLang = system.language[0];
|
|
766
|
+
// 遍历 layers → targets
|
|
767
|
+
for (const layer of config.layers) {
|
|
768
|
+
for (const mod of layer.modules) {
|
|
769
|
+
if (!mod.isLocal) {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const modulePath = mod.localPath
|
|
773
|
+
? join(projectRoot, mod.localPath)
|
|
774
|
+
: join(projectRoot, mod.name);
|
|
775
|
+
this.#targets.push({
|
|
776
|
+
name: mod.name,
|
|
777
|
+
path: modulePath,
|
|
778
|
+
type: layer.name === 'App' ? 'application' : 'library',
|
|
779
|
+
language: primaryLang,
|
|
780
|
+
metadata: {
|
|
781
|
+
layer: layer.name,
|
|
782
|
+
version: mod.version,
|
|
783
|
+
group: mod.group,
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
// 为每个 target 构建 ParsedModuleSpec
|
|
787
|
+
const targetSpec = parseXcodeGenTarget(mod.name, yamlContent);
|
|
788
|
+
if (targetSpec) {
|
|
789
|
+
this.#moduleSpecs.set(mod.name, targetSpec);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
// 全局 SPM 包依赖 → targets(标记为外部)
|
|
794
|
+
for (const dep of config.globalDependencies) {
|
|
795
|
+
if (this.#targets.some((t) => t.name === dep.name)) {
|
|
796
|
+
}
|
|
797
|
+
// 外部包不加入 targets,留给 getDependencyGraph 处理
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// ── Private: Melos 加载 ──────────────────────────────
|
|
801
|
+
#loadMelos(projectRoot, yamlContent) {
|
|
802
|
+
const melos = parseMelosProject(yamlContent);
|
|
803
|
+
// 使用 glob 模式扫描 pubspec.yaml 文件
|
|
804
|
+
const pubspecFiles = this.#findBuildFiles(projectRoot, ['pubspec.yaml']);
|
|
805
|
+
for (const pf of pubspecFiles) {
|
|
806
|
+
// 排除根目录 pubspec
|
|
807
|
+
if (pf === join(projectRoot, 'pubspec.yaml')) {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
const content = readFileSync(pf, 'utf-8');
|
|
812
|
+
const nameMatch = content.match(/^name:\s*(\S+)/m);
|
|
813
|
+
if (nameMatch) {
|
|
814
|
+
const modDir = join(pf, '..');
|
|
815
|
+
const relPath = relative(projectRoot, modDir);
|
|
816
|
+
this.#targets.push({
|
|
817
|
+
name: nameMatch[1],
|
|
818
|
+
path: modDir,
|
|
819
|
+
type: 'library',
|
|
820
|
+
language: 'dart',
|
|
821
|
+
metadata: {
|
|
822
|
+
melosProject: melos.name,
|
|
823
|
+
pubspecPath: relative(projectRoot, pf),
|
|
824
|
+
packageDir: relPath,
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
/* skip */
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
// ── Private: Starlark 加载 (Bazel/Buck2/Pants) ──────
|
|
835
|
+
#loadStarlark(projectRoot) {
|
|
836
|
+
const system = this.#matchedSystem;
|
|
837
|
+
const specPattern = system.moduleSpecPattern ?? 'BUILD';
|
|
838
|
+
const buildFileNames = specPattern === 'BUCK' ? ['BUCK'] : ['BUILD.bazel', 'BUILD'];
|
|
839
|
+
// 扫描所有 BUILD 文件
|
|
840
|
+
const buildFiles = this.#findBuildFiles(projectRoot, buildFileNames);
|
|
841
|
+
const allTargets = [];
|
|
842
|
+
const detectedLanguages = new Set();
|
|
843
|
+
for (const buildFile of buildFiles) {
|
|
844
|
+
try {
|
|
845
|
+
const content = readFileSync(buildFile, 'utf-8');
|
|
846
|
+
const parsed = parseStarlarkBuildFile(content);
|
|
847
|
+
const dirRelative = relative(projectRoot, buildFile).replace(/\/[^/]+$/, '') || '.';
|
|
848
|
+
for (const target of parsed.targets) {
|
|
849
|
+
allTargets.push(target);
|
|
850
|
+
// 语言推断
|
|
851
|
+
const lang = RULE_TO_LANGUAGE[target.rule];
|
|
852
|
+
if (lang) {
|
|
853
|
+
detectedLanguages.add(lang);
|
|
854
|
+
}
|
|
855
|
+
const modulePath = join(projectRoot, dirRelative);
|
|
856
|
+
this.#targets.push({
|
|
857
|
+
name: target.name,
|
|
858
|
+
path: modulePath,
|
|
859
|
+
type: target.rule.includes('binary') || target.rule.includes('executable')
|
|
860
|
+
? 'application'
|
|
861
|
+
: 'library',
|
|
862
|
+
language: lang ?? 'unknown',
|
|
863
|
+
metadata: {
|
|
864
|
+
rule: target.rule,
|
|
865
|
+
visibility: target.visibility,
|
|
866
|
+
buildFile: relative(projectRoot, buildFile),
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
catch {
|
|
872
|
+
/* skip unreadable BUILD files */
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
#findBuildFiles(dir, names, depth = 0) {
|
|
877
|
+
if (depth > 8) {
|
|
878
|
+
return [];
|
|
879
|
+
}
|
|
880
|
+
const results = [];
|
|
881
|
+
try {
|
|
882
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
883
|
+
for (const entry of entries) {
|
|
884
|
+
if (entry.name.startsWith('.') || EXCLUDE_DIRS.has(entry.name)) {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const fullPath = join(dir, entry.name);
|
|
888
|
+
if (entry.isFile() && names.includes(entry.name)) {
|
|
889
|
+
results.push(fullPath);
|
|
890
|
+
}
|
|
891
|
+
else if (entry.isDirectory()) {
|
|
892
|
+
results.push(...this.#findBuildFiles(fullPath, names, depth + 1));
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
/* skip */
|
|
898
|
+
}
|
|
899
|
+
return results;
|
|
900
|
+
}
|
|
901
|
+
// ── Private: Gradle DSL 加载 ─────────────────────────
|
|
902
|
+
#loadGradleDsl(projectRoot) {
|
|
903
|
+
// 查找 settings.gradle.kts 或 settings.gradle
|
|
904
|
+
let settingsContent = null;
|
|
905
|
+
for (const name of ['settings.gradle.kts', 'settings.gradle']) {
|
|
906
|
+
const settingsPath = join(projectRoot, name);
|
|
907
|
+
if (existsSync(settingsPath)) {
|
|
908
|
+
try {
|
|
909
|
+
settingsContent = readFileSync(settingsPath, 'utf-8');
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
/* skip */
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (!settingsContent) {
|
|
918
|
+
this.#loadHeuristic(projectRoot);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const project = parseGradleProject(settingsContent);
|
|
922
|
+
const primaryLang = this.#matchedSystem?.language[0] || 'kotlin';
|
|
923
|
+
// 解析每个模块的 build.gradle.kts
|
|
924
|
+
for (const mod of project.includedModules) {
|
|
925
|
+
const modulePath = join(projectRoot, mod.directory);
|
|
926
|
+
if (!existsSync(modulePath)) {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
// 读取 build.gradle.kts 获取 dependencies 和 plugins
|
|
930
|
+
for (const buildName of ['build.gradle.kts', 'build.gradle']) {
|
|
931
|
+
const buildPath = join(modulePath, buildName);
|
|
932
|
+
if (existsSync(buildPath)) {
|
|
933
|
+
try {
|
|
934
|
+
const buildContent = readFileSync(buildPath, 'utf-8');
|
|
935
|
+
const updatedMod = parseGradleProject(buildContent, mod);
|
|
936
|
+
// 更新 module 的 convention plugin 和 dependencies
|
|
937
|
+
mod.conventionPlugin =
|
|
938
|
+
updatedMod.includedModules[0]?.conventionPlugin ?? mod.conventionPlugin;
|
|
939
|
+
mod.dependencies = updatedMod.includedModules[0]?.dependencies ?? mod.dependencies;
|
|
940
|
+
}
|
|
941
|
+
catch {
|
|
942
|
+
/* skip */
|
|
943
|
+
}
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const inferredRole = mod.conventionPlugin
|
|
948
|
+
? inferConventionRole(mod.conventionPlugin)
|
|
949
|
+
: undefined;
|
|
950
|
+
this.#targets.push({
|
|
951
|
+
name: mod.path,
|
|
952
|
+
path: modulePath,
|
|
953
|
+
type: mod.path === ':app' ? 'application' : 'library',
|
|
954
|
+
language: primaryLang,
|
|
955
|
+
metadata: {
|
|
956
|
+
gradlePath: mod.path,
|
|
957
|
+
conventionPlugin: mod.conventionPlugin,
|
|
958
|
+
conventionRole: inferredRole,
|
|
959
|
+
},
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
// ── Private: CMake 加载 ──────────────────────────────
|
|
964
|
+
#loadCMake(projectRoot) {
|
|
965
|
+
const cmakePath = join(projectRoot, 'CMakeLists.txt');
|
|
966
|
+
if (!existsSync(cmakePath)) {
|
|
967
|
+
this.#loadHeuristic(projectRoot);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
let content;
|
|
971
|
+
try {
|
|
972
|
+
content = readFileSync(cmakePath, 'utf-8');
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const project = parseCMakeProject(content);
|
|
978
|
+
const primaryLang = this.#matchedSystem?.language[0] || 'cpp';
|
|
979
|
+
// 主目标
|
|
980
|
+
for (const target of project.targets) {
|
|
981
|
+
this.#targets.push({
|
|
982
|
+
name: target.name,
|
|
983
|
+
path: projectRoot,
|
|
984
|
+
type: target.type === 'executable' ? 'application' : 'library',
|
|
985
|
+
language: primaryLang,
|
|
986
|
+
metadata: {
|
|
987
|
+
cmakeType: target.type,
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
// 递归解析子目录的 CMakeLists.txt
|
|
992
|
+
for (const subdir of project.subdirectories) {
|
|
993
|
+
const subdirPath = join(projectRoot, subdir);
|
|
994
|
+
const subdirCmakePath = join(subdirPath, 'CMakeLists.txt');
|
|
995
|
+
if (!existsSync(subdirCmakePath)) {
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
try {
|
|
999
|
+
const subcontent = readFileSync(subdirCmakePath, 'utf-8');
|
|
1000
|
+
const subproject = parseCMakeProject(subcontent);
|
|
1001
|
+
for (const target of subproject.targets) {
|
|
1002
|
+
this.#targets.push({
|
|
1003
|
+
name: target.name,
|
|
1004
|
+
path: subdirPath,
|
|
1005
|
+
type: target.type === 'executable' ? 'application' : 'library',
|
|
1006
|
+
language: primaryLang,
|
|
1007
|
+
metadata: {
|
|
1008
|
+
cmakeType: target.type,
|
|
1009
|
+
subdirectory: subdir,
|
|
1010
|
+
},
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
catch {
|
|
1015
|
+
/* skip */
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
// ── Private: JSON Config 加载 (Nx/Flutter/RN) ────────
|
|
1020
|
+
#loadJsonConfig(projectRoot) {
|
|
1021
|
+
const system = this.#matchedSystem;
|
|
1022
|
+
switch (system.id) {
|
|
1023
|
+
case 'nx-monorepo':
|
|
1024
|
+
this.#loadNx(projectRoot);
|
|
1025
|
+
break;
|
|
1026
|
+
case 'flutter-add-to-app':
|
|
1027
|
+
this.#loadFlutterAddToApp(projectRoot);
|
|
1028
|
+
break;
|
|
1029
|
+
case 'react-native-hybrid':
|
|
1030
|
+
this.#loadReactNative(projectRoot);
|
|
1031
|
+
break;
|
|
1032
|
+
default:
|
|
1033
|
+
this.#loadHeuristic(projectRoot);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
#loadNx(projectRoot) {
|
|
1037
|
+
const nxJsonPath = join(projectRoot, 'nx.json');
|
|
1038
|
+
if (!existsSync(nxJsonPath)) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
// 扫描所有 project.json 文件
|
|
1042
|
+
const projectJsonFiles = this.#findBuildFiles(projectRoot, ['project.json']);
|
|
1043
|
+
const projects = [];
|
|
1044
|
+
for (const pjFile of projectJsonFiles) {
|
|
1045
|
+
try {
|
|
1046
|
+
const content = readFileSync(pjFile, 'utf-8');
|
|
1047
|
+
const parsed = parseNxWorkspace(content);
|
|
1048
|
+
for (const proj of parsed.projects) {
|
|
1049
|
+
projects.push(proj);
|
|
1050
|
+
const modulePath = join(projectRoot, proj.root);
|
|
1051
|
+
this.#targets.push({
|
|
1052
|
+
name: proj.name,
|
|
1053
|
+
path: modulePath,
|
|
1054
|
+
type: proj.projectType === 'application' ? 'application' : 'library',
|
|
1055
|
+
language: 'typescript',
|
|
1056
|
+
metadata: {
|
|
1057
|
+
tags: proj.tags,
|
|
1058
|
+
nxProjectType: proj.projectType,
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
catch {
|
|
1064
|
+
/* skip */
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
#loadFlutterAddToApp(projectRoot) {
|
|
1069
|
+
// 解析 .flutter-plugins-dependencies
|
|
1070
|
+
const depsPath = join(projectRoot, '.flutter-plugins-dependencies');
|
|
1071
|
+
if (existsSync(depsPath)) {
|
|
1072
|
+
try {
|
|
1073
|
+
const content = readFileSync(depsPath, 'utf-8');
|
|
1074
|
+
const parsed = parseFlutterPluginsDeps(content);
|
|
1075
|
+
for (const plugin of parsed.plugins) {
|
|
1076
|
+
this.#targets.push({
|
|
1077
|
+
name: plugin.name,
|
|
1078
|
+
path: plugin.path,
|
|
1079
|
+
type: 'library',
|
|
1080
|
+
language: 'dart',
|
|
1081
|
+
metadata: {
|
|
1082
|
+
platform: plugin.platform,
|
|
1083
|
+
bridgeType: 'flutter-engine',
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
/* skip */
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
// 查找嵌入的 pubspec.yaml
|
|
1093
|
+
const pubspecFiles = this.#findBuildFiles(projectRoot, ['pubspec.yaml']);
|
|
1094
|
+
for (const pf of pubspecFiles) {
|
|
1095
|
+
// 排除根目录的 pubspec(交给 DartDiscoverer 处理)
|
|
1096
|
+
if (pf === join(projectRoot, 'pubspec.yaml')) {
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
try {
|
|
1100
|
+
const content = readFileSync(pf, 'utf-8');
|
|
1101
|
+
const nameMatch = content.match(/^name:\s*(\S+)/m);
|
|
1102
|
+
if (nameMatch) {
|
|
1103
|
+
const modDir = join(pf, '..');
|
|
1104
|
+
this.#targets.push({
|
|
1105
|
+
name: nameMatch[1],
|
|
1106
|
+
path: modDir,
|
|
1107
|
+
type: 'library',
|
|
1108
|
+
language: 'dart',
|
|
1109
|
+
metadata: {
|
|
1110
|
+
pubspecPath: relative(projectRoot, pf),
|
|
1111
|
+
},
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
catch {
|
|
1116
|
+
/* skip */
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
#loadReactNative(projectRoot) {
|
|
1121
|
+
const pkgJsonPath = join(projectRoot, 'package.json');
|
|
1122
|
+
if (!existsSync(pkgJsonPath)) {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
try {
|
|
1126
|
+
const content = readFileSync(pkgJsonPath, 'utf-8');
|
|
1127
|
+
const parsed = parseReactNativeProject(content);
|
|
1128
|
+
if (parsed.isReactNative) {
|
|
1129
|
+
this.#targets.push({
|
|
1130
|
+
name: parsed.name,
|
|
1131
|
+
path: projectRoot,
|
|
1132
|
+
type: 'application',
|
|
1133
|
+
language: 'typescript',
|
|
1134
|
+
metadata: {
|
|
1135
|
+
rnVersion: parsed.rnVersion,
|
|
1136
|
+
bridgeType: 'native-module',
|
|
1137
|
+
},
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
catch {
|
|
1142
|
+
/* skip */
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
// ── Private: 启发式加载 ────────────────────────────
|
|
1146
|
+
#loadHeuristic(projectRoot) {
|
|
1147
|
+
// 扫描根目录中可能包含模块的目录
|
|
1148
|
+
try {
|
|
1149
|
+
const entries = readdirSync(projectRoot, { withFileTypes: true });
|
|
1150
|
+
for (const entry of entries) {
|
|
1151
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || EXCLUDE_DIRS.has(entry.name)) {
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
// 检查是否是模块容器目录
|
|
1155
|
+
if (/^(Local)?Modules?$|^Packages$/i.test(entry.name)) {
|
|
1156
|
+
this.#scanModuleDirectory(join(projectRoot, entry.name));
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
catch {
|
|
1161
|
+
/* skip */
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* 扫描模块容器目录,每个有 spec 文件或源码的子目录视为一个模块
|
|
1166
|
+
*/
|
|
1167
|
+
#scanModuleDirectory(containerDir) {
|
|
1168
|
+
try {
|
|
1169
|
+
const entries = readdirSync(containerDir, { withFileTypes: true });
|
|
1170
|
+
for (const entry of entries) {
|
|
1171
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) {
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
const modulePath = join(containerDir, entry.name);
|
|
1175
|
+
// 查找 spec 文件
|
|
1176
|
+
const specPath = this.#findSpecFile(modulePath, entry.name);
|
|
1177
|
+
if (specPath) {
|
|
1178
|
+
try {
|
|
1179
|
+
const specContent = readFileSync(specPath, 'utf8');
|
|
1180
|
+
const spec = parseModuleSpec(specContent);
|
|
1181
|
+
this.#moduleSpecs.set(entry.name, spec);
|
|
1182
|
+
}
|
|
1183
|
+
catch {
|
|
1184
|
+
/* skip */
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
// 检查目录是否包含源码文件
|
|
1188
|
+
if (specPath || this.#hasSourceFiles(modulePath)) {
|
|
1189
|
+
this.#targets.push({
|
|
1190
|
+
name: entry.name,
|
|
1191
|
+
path: modulePath,
|
|
1192
|
+
type: 'library',
|
|
1193
|
+
language: 'objectivec',
|
|
1194
|
+
metadata: { specFile: specPath !== null },
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
catch {
|
|
1200
|
+
/* skip */
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
// ── Private: 文件工具 ──────────────────────────────
|
|
1204
|
+
/**
|
|
1205
|
+
* 递归收集源码文件
|
|
1206
|
+
*/
|
|
1207
|
+
#collectSourceFiles(dir, rootDir, files, depth = 0) {
|
|
1208
|
+
if (depth > 15 || files.length >= 500) {
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
try {
|
|
1212
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1213
|
+
for (const entry of entries) {
|
|
1214
|
+
if (entry.name.startsWith('.')) {
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
if (EXCLUDE_DIRS.has(entry.name)) {
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
const fullPath = join(dir, entry.name);
|
|
1221
|
+
if (entry.isDirectory()) {
|
|
1222
|
+
this.#collectSourceFiles(fullPath, rootDir, files, depth + 1);
|
|
1223
|
+
}
|
|
1224
|
+
else if (entry.isFile()) {
|
|
1225
|
+
const ext = extname(entry.name);
|
|
1226
|
+
if (SOURCE_EXTENSIONS.has(ext) || LanguageService.sourceExts.has(ext)) {
|
|
1227
|
+
const lang = LanguageService.inferLang(entry.name) || 'unknown';
|
|
1228
|
+
files.push({
|
|
1229
|
+
name: entry.name,
|
|
1230
|
+
path: fullPath,
|
|
1231
|
+
relativePath: relative(rootDir, fullPath),
|
|
1232
|
+
language: lang,
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (files.length >= 500) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
catch {
|
|
1242
|
+
/* skip */
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* 检查目录中是否存在源码文件(浅层检查)
|
|
1247
|
+
*/
|
|
1248
|
+
#hasSourceFiles(dir, depth = 0) {
|
|
1249
|
+
if (depth > 3) {
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
try {
|
|
1253
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1254
|
+
for (const entry of entries) {
|
|
1255
|
+
if (entry.name.startsWith('.')) {
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
if (entry.isFile()) {
|
|
1259
|
+
const ext = extname(entry.name);
|
|
1260
|
+
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
1261
|
+
return true;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
else if (entry.isDirectory() && !EXCLUDE_DIRS.has(entry.name)) {
|
|
1265
|
+
if (this.#hasSourceFiles(join(dir, entry.name), depth + 1)) {
|
|
1266
|
+
return true;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
catch {
|
|
1272
|
+
/* skip */
|
|
1273
|
+
}
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
// ── Module-level helpers ────────────────────────────
|
|
1278
|
+
/**
|
|
1279
|
+
* 计算目录下包含 spec 文件的子目录数量
|
|
1280
|
+
*/
|
|
1281
|
+
function countSubdirsWithSpecs(containerDir) {
|
|
1282
|
+
let count = 0;
|
|
1283
|
+
try {
|
|
1284
|
+
const entries = readdirSync(containerDir, { withFileTypes: true });
|
|
1285
|
+
for (const entry of entries) {
|
|
1286
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) {
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
try {
|
|
1290
|
+
const subEntries = readdirSync(join(containerDir, entry.name));
|
|
1291
|
+
const hasSpec = subEntries.some((e) => e.endsWith('.boxspec') || e.endsWith('.podspec'));
|
|
1292
|
+
if (hasSpec) {
|
|
1293
|
+
count++;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
catch {
|
|
1297
|
+
/* skip */
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
catch {
|
|
1302
|
+
/* skip */
|
|
1303
|
+
}
|
|
1304
|
+
return count;
|
|
1305
|
+
}
|