autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
1,304 lines (1,303 loc) • 48.4 kB
JavaScript
/**
* @module CustomConfigDiscoverer
* @description 自研配置文件发现器 — 识别使用非标准/自研构建系统的项目
*
* 两级检测策略:
* Level 1: 已知自研工具指纹匹配 (confidence 0.70-0.80)
* Level 2: 启发式目录结构探测 (confidence 0.50-0.65)
*
* 当前支持:
* - Baidu EasyBox (Boxfile + *.boxspec)
* - Tuist (Project.swift)
* - XcodeGen (project.yml)
*/
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { extname, join, relative } from 'node:path';
import { getProjectSpecPath } from '#infra/config/Paths.js';
import { LanguageService } from '#shared/LanguageService.js';
import { ProjectDiscoverer, } from './ProjectDiscoverer.js';
import { parseCMakeProject } from './parsers/CMakeParser.js';
import { inferConventionRole, parseGradleProject } from './parsers/GradleDslParser.js';
import { parseFlutterPluginsDeps, parseNxWorkspace, parseReactNativeProject, } from './parsers/JsonConfigParser.js';
import { parseBoxfile, parseModuleSpec, } from './parsers/RubyDslParser.js';
import { parseStarlarkBuildFile, RULE_TO_LANGUAGE, } from './parsers/StarlarkParser.js';
import { parseMelosProject, parseXcodeGenProject, parseXcodeGenTarget, } from './parsers/YamlConfigParser.js';
const KNOWN_CUSTOM_SYSTEMS = Object.freeze([
// ── Tier 1: Bazel / Buck2 (Starlark) ──
{
id: 'bazel',
displayName: 'Bazel',
markers: ['MODULE.bazel', 'WORKSPACE', 'WORKSPACE.bazel'],
markerStrategy: 'any',
moduleSpecPattern: 'BUILD.bazel',
language: Object.freeze([]),
confidence: 0.85,
parser: 'starlark',
},
{
id: 'buck2',
displayName: 'Buck2',
markers: ['.buckconfig', '.buckroot'],
markerStrategy: 'any',
moduleSpecPattern: 'BUCK',
language: Object.freeze([]),
confidence: 0.85,
parser: 'starlark',
},
// ── Tier 1: Android Gradle Convention Plugins ──
{
id: 'gradle-convention',
displayName: 'Gradle Convention Plugins',
markers: ['build-logic/convention/', 'buildSrc/src/main/kotlin/'],
markerStrategy: 'any',
moduleSpecPattern: null,
language: Object.freeze(['kotlin', 'java']),
confidence: 0.8,
parser: 'gradle-dsl',
},
// ── Tier 1: Flutter Melos ──
{
id: 'melos',
displayName: 'Melos (Flutter Monorepo)',
markers: ['melos.yaml'],
moduleSpecPattern: null,
language: Object.freeze(['dart']),
confidence: 0.82,
parser: 'yaml',
},
// ── Tier 1: iOS 生态 ──
{
id: 'easybox',
displayName: 'Baidu EasyBox',
markers: ['Boxfile'],
moduleSpecPattern: '*.boxspec',
language: Object.freeze(['objectivec', 'swift']),
confidence: 0.8,
parser: 'ruby-dsl',
},
{
id: 'tuist',
displayName: 'Tuist',
markers: ['Tuist/Config.swift', 'Project.swift'],
moduleSpecPattern: null,
language: Object.freeze(['swift']),
confidence: 0.8,
parser: 'swift-dsl',
},
{
id: 'ks-component',
displayName: 'KSComponent (快手)',
markers: ['KSPodfile', 'Podfile.ks'],
markerStrategy: 'any',
moduleSpecPattern: '*.podspec',
language: Object.freeze(['swift', 'objectivec']),
confidence: 0.8,
parser: 'ruby-dsl',
},
{
id: 'mt-component',
displayName: 'MTComponent (美团)',
markers: ['MTModulefile', 'MTConfig.yml'],
markerStrategy: 'any',
moduleSpecPattern: '*.podspec',
language: Object.freeze(['swift', 'objectivec']),
confidence: 0.78,
parser: 'ruby-dsl',
},
// ── Tier 1: 混合架构 ──
{
id: 'flutter-add-to-app',
displayName: 'Flutter Add-to-App',
markers: ['.flutter-plugins-dependencies', '.flutter-plugins'],
markerStrategy: 'any',
moduleSpecPattern: 'pubspec.yaml',
language: Object.freeze(['dart']),
confidence: 0.78,
parser: 'json-config',
},
{
id: 'react-native-hybrid',
displayName: 'React Native Hybrid',
markers: ['metro.config.js', 'metro.config.ts', 'react-native.config.js'],
markerStrategy: 'any',
moduleSpecPattern: null,
language: Object.freeze(['typescript', 'javascript']),
confidence: 0.78,
parser: 'json-config',
},
{
id: 'kotlin-multiplatform',
displayName: 'Kotlin Multiplatform',
markers: ['shared/build.gradle.kts'],
moduleSpecPattern: null,
language: Object.freeze(['kotlin']),
confidence: 0.78,
parser: 'gradle-dsl',
},
// ── Tier 2: Nx / Pants / CMake ──
{
id: 'nx-monorepo',
displayName: 'Nx Monorepo',
markers: ['nx.json'],
moduleSpecPattern: 'project.json',
language: Object.freeze(['typescript', 'javascript']),
confidence: 0.8,
parser: 'json-config',
},
{
id: 'pants',
displayName: 'Pants Build',
markers: ['pants.toml'],
moduleSpecPattern: 'BUILD',
language: Object.freeze([]),
confidence: 0.8,
parser: 'starlark',
},
{
id: 'cmake-multiproject',
displayName: 'CMake Multi-Project',
markers: ['CMakeLists.txt'],
antiMarkers: ['MODULE.bazel', 'WORKSPACE', 'meson.build'],
moduleSpecPattern: 'CMakeLists.txt',
language: Object.freeze(['cpp', 'c']),
confidence: 0.75,
parser: 'cmake',
},
{
id: 'xcodegen',
displayName: 'XcodeGen',
markers: ['project.yml', 'project.yaml'],
markerStrategy: 'any',
moduleSpecPattern: null,
language: Object.freeze(['swift', 'objectivec']),
confidence: 0.75,
parser: 'yaml',
},
]);
const HEURISTIC_SIGNALS = Object.freeze([
{ pattern: /^(Local)?Modules?$/i, type: 'module-dir', boost: 0.15 },
{ pattern: /^Packages$/i, type: 'module-dir', boost: 0.1 },
{ pattern: /^[A-Z]\w+file$/, type: 'custom-dsl', boost: 0.2 },
{ pattern: /\.\w+spec$/, type: 'spec-file', boost: 0.2 },
{ pattern: /\.xcodeproj$/, type: 'xcode', boost: 0.05 },
]);
// 排除已知的标准 Ruby DSL 文件
const KNOWN_STANDARD_FILES = new Set([
'Gemfile',
'Podfile',
'Fastfile',
'Rakefile',
'Vagrantfile',
'Guardfile',
'Brewfile',
'Berksfile',
'Capfile',
]);
const EXCLUDE_DIRS = new Set([
'node_modules',
'.git',
'.cursor',
'dist',
'build',
'out',
'.build',
'Pods',
'Carthage',
'DerivedData',
'__pycache__',
'.venv',
'venv',
'.gradle',
'coverage',
'.cache',
'.easybox',
]);
const SOURCE_EXTENSIONS = new Set(['.m', '.h', '.swift', '.mm', '.c', '.cpp', '.cc']);
// ── User Custom Systems (boxspec.json) ──────────────
/**
* 从 boxspec.json 读取用户自定义配置系统
*
* boxspec.json 中可选字段:
* ```json
* {
* "customDiscoverer": {
* "id": "my-build-tool",
* "displayName": "MyBuildTool",
* "markers": ["MyBuildfile"],
* "moduleSpecPattern": "*.myspec",
* "language": ["swift"],
* "confidence": 0.85,
* "parser": "ruby-dsl"
* }
* }
* ```
* 或数组形式支持多个自定义系统。
*/
function loadUserCustomSystems(projectRoot) {
try {
const specPath = getProjectSpecPath(projectRoot);
if (!existsSync(specPath)) {
return [];
}
const raw = JSON.parse(readFileSync(specPath, 'utf-8'));
const custom = raw?.customDiscoverer;
if (!custom) {
return [];
}
const items = Array.isArray(custom) ? custom : [custom];
const results = [];
for (const item of items) {
if (!item?.id || !item?.markers || !Array.isArray(item.markers)) {
continue;
}
results.push({
id: String(item.id),
displayName: String(item.displayName ?? item.id),
markers: item.markers.map(String),
moduleSpecPattern: item.moduleSpecPattern ? String(item.moduleSpecPattern) : null,
language: Array.isArray(item.language) ? item.language.map(String) : ['swift'],
confidence: typeof item.confidence === 'number' ? item.confidence : 0.75,
parser: [
'ruby-dsl',
'yaml',
'swift-dsl',
'starlark',
'gradle-dsl',
'cmake',
'json-config',
].includes(item.parser)
? item.parser
: 'ruby-dsl',
markerStrategy: ['all', 'any', 'ordered'].includes(item.markerStrategy)
? item.markerStrategy
: undefined,
antiMarkers: Array.isArray(item.antiMarkers) ? item.antiMarkers.map(String) : undefined,
});
}
return results;
}
catch {
return [];
}
}
/**
* 获取合并后的系统配置表:用户自定义 + 内置
* 用户自定义系统优先匹配
*/
function getEffectiveSystemProfiles(projectRoot) {
const userSystems = loadUserCustomSystems(projectRoot);
if (userSystems.length === 0) {
return KNOWN_CUSTOM_SYSTEMS;
}
return [...userSystems, ...KNOWN_CUSTOM_SYSTEMS];
}
// ── CustomConfigDiscoverer ──────────────────────────
export class CustomConfigDiscoverer extends ProjectDiscoverer {
#projectRoot = null;
#matchedSystem = null;
#parsedConfig = null;
#moduleSpecs = new Map();
#targets = [];
get id() {
return 'customConfig';
}
get displayName() {
if (this.#matchedSystem) {
return `Custom Config (${this.#matchedSystem.displayName})`;
}
return 'Custom Config (Heuristic)';
}
// ── detect ────────────────────────────────────────
async detect(projectRoot) {
// Level 1: 已知自研工具指纹匹配(含用户自定义系统)
const systems = getEffectiveSystemProfiles(projectRoot);
for (const system of systems) {
// antiMarkers 排除检查
if (system.antiMarkers?.some((am) => existsSync(join(projectRoot, am)))) {
continue;
}
const strategy = system.markerStrategy ?? 'all';
let markerFound = false;
if (strategy === 'any') {
markerFound = system.markers.some((marker) => existsSync(join(projectRoot, marker)));
}
else {
// 'all' 和 'ordered' 都要求所有 markers 存在(ordered 未来可扩展)
markerFound = system.markers.every((marker) => existsSync(join(projectRoot, marker)));
}
if (markerFound) {
return {
match: true,
confidence: system.confidence,
reason: `${system.displayName} detected (${system.markers.join(', ')})`,
};
}
}
// Level 2: 启发式目录结构探测
let heuristicScore = 0.35; // 基础分
const signals = [];
try {
const entries = readdirSync(projectRoot, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) {
continue;
}
for (const signal of HEURISTIC_SIGNALS) {
if (signal.pattern.test(entry.name)) {
// 排除已知的标准文件
if (signal.type === 'custom-dsl' && KNOWN_STANDARD_FILES.has(entry.name)) {
continue;
}
// 对 module-dir 类型,要求目录内有多个子目录
if (signal.type === 'module-dir' && entry.isDirectory()) {
const subCount = countSubdirsWithSpecs(join(projectRoot, entry.name));
if (subCount < 2) {
continue;
}
}
heuristicScore += signal.boost;
signals.push(`${entry.name} (${signal.type})`);
}
}
}
}
catch {
/* skip */
}
// 限制最高分
heuristicScore = Math.min(heuristicScore, 0.65);
if (heuristicScore >= 0.5 && signals.length >= 2) {
return {
match: true,
confidence: heuristicScore,
reason: `Heuristic signals: ${signals.join(', ')}`,
};
}
return { match: false, confidence: 0, reason: 'No custom config detected' };
}
// ── load ──────────────────────────────────────────
async load(projectRoot) {
this.#projectRoot = projectRoot;
this.#parsedConfig = null;
this.#moduleSpecs.clear();
this.#targets = [];
// 确定匹配的系统(含用户自定义系统)
this.#matchedSystem = null;
const systems = getEffectiveSystemProfiles(projectRoot);
for (const system of systems) {
if (system.antiMarkers?.some((am) => existsSync(join(projectRoot, am)))) {
continue;
}
const strategy = system.markerStrategy ?? 'all';
const markerFound = strategy === 'any'
? system.markers.some((marker) => existsSync(join(projectRoot, marker)))
: system.markers.every((marker) => existsSync(join(projectRoot, marker)));
if (markerFound) {
this.#matchedSystem = system;
break;
}
}
if (!this.#matchedSystem) {
this.#loadHeuristic(projectRoot);
return;
}
switch (this.#matchedSystem.parser) {
case 'ruby-dsl':
this.#loadRubyDsl(projectRoot);
break;
case 'yaml':
this.#loadYaml(projectRoot);
break;
case 'starlark':
this.#loadStarlark(projectRoot);
break;
case 'gradle-dsl':
this.#loadGradleDsl(projectRoot);
break;
case 'cmake':
this.#loadCMake(projectRoot);
break;
case 'json-config':
this.#loadJsonConfig(projectRoot);
break;
default:
this.#loadHeuristic(projectRoot);
}
}
// ── listTargets ───────────────────────────────────
async listTargets() {
return this.#targets;
}
// ── getTargetFiles ────────────────────────────────
async getTargetFiles(target) {
const targetPath = typeof target === 'string' ? this.#targets.find((t) => t.name === target)?.path : target.path;
if (!targetPath || !existsSync(targetPath)) {
return [];
}
// 如果有 spec 文件,优先使用 sources 字段定位
const targetName = typeof target === 'string' ? target : target.name;
const spec = this.#moduleSpecs.get(targetName);
let sourceDir = targetPath;
if (spec?.sources) {
const specSourceDir = join(targetPath, spec.sources);
if (existsSync(specSourceDir)) {
sourceDir = specSourceDir;
}
}
const files = [];
this.#collectSourceFiles(sourceDir, targetPath, files);
return files;
}
// ── getDependencyGraph ────────────────────────────
async getDependencyGraph() {
if (!this.#parsedConfig) {
return { nodes: this.#targets.map((t) => t.name), edges: [] };
}
const config = this.#parsedConfig;
const nodes = [];
const edges = [];
const nodeIds = new Set();
// 宿主应用节点
if (config.hostApp) {
const hostId = config.hostApp.name;
nodes.push({
id: hostId,
label: hostId,
type: 'host',
version: config.hostApp.version,
});
nodeIds.add(hostId);
}
// 遍历所有层级,添加模块节点
for (const layer of config.layers) {
for (const mod of layer.modules) {
if (nodeIds.has(mod.name)) {
continue;
}
nodeIds.add(mod.name);
nodes.push({
id: mod.name,
label: mod.name,
type: mod.isLocal ? 'local' : 'external',
layer: layer.name,
version: mod.version || undefined,
group: mod.group || undefined,
fullPath: mod.isLocal && mod.localPath && this.#projectRoot
? join(this.#projectRoot, mod.localPath)
: undefined,
});
}
}
// 全局依赖
for (const mod of config.globalDependencies) {
if (nodeIds.has(mod.name)) {
continue;
}
nodeIds.add(mod.name);
nodes.push({
id: mod.name,
label: mod.name,
type: mod.isLocal ? 'local' : 'external',
version: mod.version || undefined,
group: mod.group || undefined,
fullPath: mod.isLocal && mod.localPath && this.#projectRoot
? join(this.#projectRoot, mod.localPath)
: undefined,
});
}
// 从 boxspec 依赖声明生成边
for (const [moduleName, spec] of this.#moduleSpecs) {
for (const depName of spec.dependencies) {
// 确保依赖目标存在于节点列表中
if (!nodeIds.has(depName)) {
nodeIds.add(depName);
nodes.push({
id: depName,
label: depName,
type: 'external',
indirect: true,
});
}
edges.push({
from: moduleName,
to: depName,
type: 'depends_on',
});
}
}
// 宿主应用 → 所有本地模块的 contains 关系
if (config.hostApp) {
for (const layer of config.layers) {
for (const mod of layer.modules) {
if (mod.isLocal) {
edges.push({
from: config.hostApp.name,
to: mod.name,
type: 'contains',
});
}
}
}
}
// 层级元数据
const layers = config.layers.map((l) => ({
name: l.name,
order: l.order,
accessibleLayers: l.accessibleLayers,
}));
return { nodes, edges, layers };
}
// ── Private: Ruby DSL 加载 ─────────────────────────
#loadRubyDsl(projectRoot) {
// 读取 Boxfile
const boxfilePath = join(projectRoot, 'Boxfile');
if (!existsSync(boxfilePath)) {
return;
}
let content;
try {
content = readFileSync(boxfilePath, 'utf8');
}
catch {
return;
}
// 解析 Boxfile
this.#parsedConfig = parseBoxfile(content);
// 尝试合并 Boxfile.local 覆盖
this.#mergeLocalOverrides(projectRoot);
// 遍历本地模块,解析 spec 文件
const allModules = [
...this.#parsedConfig.layers.flatMap((l) => l.modules),
...this.#parsedConfig.globalDependencies,
];
for (const mod of allModules) {
if (!mod.isLocal || !mod.localPath) {
continue;
}
const modulePath = join(projectRoot, mod.localPath);
if (!existsSync(modulePath)) {
continue;
}
// 查找 spec 文件
const specPath = this.#findSpecFile(modulePath, mod.name);
if (specPath) {
try {
const specContent = readFileSync(specPath, 'utf8');
const spec = parseModuleSpec(specContent);
this.#moduleSpecs.set(mod.name, spec);
}
catch {
/* skip unreadable spec */
}
}
}
// 构建 targets(仅 local 模块 + 宿主应用)
this.#buildTargets(projectRoot);
}
/**
* 合并 Boxfile.local 中的覆盖配置
* Boxfile.local 中 :path 覆盖可以将远程依赖切换为本地源码
*/
#mergeLocalOverrides(projectRoot) {
const localPath = join(projectRoot, 'Boxfile.local');
if (!existsSync(localPath)) {
return;
}
try {
const localContent = readFileSync(localPath, 'utf8');
const localConfig = parseBoxfile(localContent);
if (!this.#parsedConfig) {
return;
}
// 合并本地覆盖:将 Boxfile.local 中的 local module 覆盖到主配置
const allLocalModules = localConfig.layers.flatMap((l) => l.modules);
for (const localMod of allLocalModules) {
if (!localMod.isLocal) {
continue;
}
// 查找主配置中的同名模块并覆盖
const configLayers = this.#parsedConfig.layers;
for (const layer of configLayers) {
const existingIdx = layer.modules.findIndex((m) => m.name === localMod.name);
if (existingIdx >= 0) {
layer.modules[existingIdx] = { ...layer.modules[existingIdx], ...localMod };
}
}
}
}
catch {
/* skip */
}
}
/**
* 在模块目录中查找 spec 文件
* 查找顺序: ModuleName.boxspec → ModuleName.podspec → 任意 *.boxspec → 任意 *.podspec
*/
#findSpecFile(modulePath, moduleName) {
// 精确匹配
for (const ext of ['.boxspec', '.podspec']) {
const exactPath = join(modulePath, `${moduleName}${ext}`);
if (existsSync(exactPath)) {
return exactPath;
}
}
// 模糊匹配
try {
const entries = readdirSync(modulePath);
for (const entry of entries) {
if (entry.endsWith('.boxspec') || entry.endsWith('.podspec')) {
return join(modulePath, entry);
}
}
}
catch {
/* skip */
}
return null;
}
/**
* 从解析结果构建 Target 列表
* 仅包含本地模块和宿主应用(有源码可收集的目标)
*/
#buildTargets(projectRoot) {
if (!this.#parsedConfig) {
return;
}
const config = this.#parsedConfig;
const primaryLang = this.#matchedSystem?.language[0] || 'objectivec';
// 宿主应用
if (config.hostApp) {
const hostDir = join(projectRoot, config.hostApp.name);
if (existsSync(hostDir)) {
this.#targets.push({
name: config.hostApp.name,
path: hostDir,
type: 'application',
language: primaryLang,
metadata: {
layer: 'Application',
version: config.hostApp.version,
},
});
}
}
// 所有层级中的本地模块
for (const layer of config.layers) {
for (const mod of layer.modules) {
if (!mod.isLocal || !mod.localPath) {
continue;
}
const modulePath = join(projectRoot, mod.localPath);
if (!existsSync(modulePath)) {
continue;
}
this.#targets.push({
name: mod.name,
path: modulePath,
type: 'library',
language: primaryLang,
metadata: {
layer: layer.name,
version: mod.version,
group: mod.group,
specFile: this.#moduleSpecs.has(mod.name),
},
});
}
}
// 全局本地模块
for (const mod of config.globalDependencies) {
if (!mod.isLocal || !mod.localPath) {
continue;
}
const modulePath = join(projectRoot, mod.localPath);
if (!existsSync(modulePath)) {
continue;
}
// 避免重复
if (this.#targets.some((t) => t.name === mod.name)) {
continue;
}
this.#targets.push({
name: mod.name,
path: modulePath,
type: 'library',
language: primaryLang,
metadata: {
version: mod.version,
group: mod.group,
specFile: this.#moduleSpecs.has(mod.name),
},
});
}
}
// ── Private: YAML 加载 (XcodeGen) ──────────────────
#loadYaml(projectRoot) {
const system = this.#matchedSystem;
// 查找可用的 YAML 配置文件
let yamlContent = null;
for (const marker of system.markers) {
const markerPath = join(projectRoot, marker);
if (existsSync(markerPath)) {
try {
yamlContent = readFileSync(markerPath, 'utf-8');
break;
}
catch {
/* 跳过不可读文件 */
}
}
}
if (!yamlContent) {
this.#loadHeuristic(projectRoot);
return;
}
// Melos 项目走专用加载路径
if (system.id === 'melos') {
this.#loadMelos(projectRoot, yamlContent);
return;
}
// 解析 project.yml
const config = parseXcodeGenProject(yamlContent);
this.#parsedConfig = config;
const primaryLang = system.language[0];
// 遍历 layers → targets
for (const layer of config.layers) {
for (const mod of layer.modules) {
if (!mod.isLocal) {
continue;
}
const modulePath = mod.localPath
? join(projectRoot, mod.localPath)
: join(projectRoot, mod.name);
this.#targets.push({
name: mod.name,
path: modulePath,
type: layer.name === 'App' ? 'application' : 'library',
language: primaryLang,
metadata: {
layer: layer.name,
version: mod.version,
group: mod.group,
},
});
// 为每个 target 构建 ParsedModuleSpec
const targetSpec = parseXcodeGenTarget(mod.name, yamlContent);
if (targetSpec) {
this.#moduleSpecs.set(mod.name, targetSpec);
}
}
}
// 全局 SPM 包依赖 → targets(标记为外部)
for (const dep of config.globalDependencies) {
if (this.#targets.some((t) => t.name === dep.name)) {
}
// 外部包不加入 targets,留给 getDependencyGraph 处理
}
}
// ── Private: Melos 加载 ──────────────────────────────
#loadMelos(projectRoot, yamlContent) {
const melos = parseMelosProject(yamlContent);
// 使用 glob 模式扫描 pubspec.yaml 文件
const pubspecFiles = this.#findBuildFiles(projectRoot, ['pubspec.yaml']);
for (const pf of pubspecFiles) {
// 排除根目录 pubspec
if (pf === join(projectRoot, 'pubspec.yaml')) {
continue;
}
try {
const content = readFileSync(pf, 'utf-8');
const nameMatch = content.match(/^name:\s*(\S+)/m);
if (nameMatch) {
const modDir = join(pf, '..');
const relPath = relative(projectRoot, modDir);
this.#targets.push({
name: nameMatch[1],
path: modDir,
type: 'library',
language: 'dart',
metadata: {
melosProject: melos.name,
pubspecPath: relative(projectRoot, pf),
packageDir: relPath,
},
});
}
}
catch {
/* skip */
}
}
}
// ── Private: Starlark 加载 (Bazel/Buck2/Pants) ──────
#loadStarlark(projectRoot) {
const system = this.#matchedSystem;
const specPattern = system.moduleSpecPattern ?? 'BUILD';
const buildFileNames = specPattern === 'BUCK' ? ['BUCK'] : ['BUILD.bazel', 'BUILD'];
// 扫描所有 BUILD 文件
const buildFiles = this.#findBuildFiles(projectRoot, buildFileNames);
const allTargets = [];
const detectedLanguages = new Set();
for (const buildFile of buildFiles) {
try {
const content = readFileSync(buildFile, 'utf-8');
const parsed = parseStarlarkBuildFile(content);
const dirRelative = relative(projectRoot, buildFile).replace(/\/[^/]+$/, '') || '.';
for (const target of parsed.targets) {
allTargets.push(target);
// 语言推断
const lang = RULE_TO_LANGUAGE[target.rule];
if (lang) {
detectedLanguages.add(lang);
}
const modulePath = join(projectRoot, dirRelative);
this.#targets.push({
name: target.name,
path: modulePath,
type: target.rule.includes('binary') || target.rule.includes('executable')
? 'application'
: 'library',
language: lang ?? 'unknown',
metadata: {
rule: target.rule,
visibility: target.visibility,
buildFile: relative(projectRoot, buildFile),
},
});
}
}
catch {
/* skip unreadable BUILD files */
}
}
}
#findBuildFiles(dir, names, depth = 0) {
if (depth > 8) {
return [];
}
const results = [];
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.') || EXCLUDE_DIRS.has(entry.name)) {
continue;
}
const fullPath = join(dir, entry.name);
if (entry.isFile() && names.includes(entry.name)) {
results.push(fullPath);
}
else if (entry.isDirectory()) {
results.push(...this.#findBuildFiles(fullPath, names, depth + 1));
}
}
}
catch {
/* skip */
}
return results;
}
// ── Private: Gradle DSL 加载 ─────────────────────────
#loadGradleDsl(projectRoot) {
// 查找 settings.gradle.kts 或 settings.gradle
let settingsContent = null;
for (const name of ['settings.gradle.kts', 'settings.gradle']) {
const settingsPath = join(projectRoot, name);
if (existsSync(settingsPath)) {
try {
settingsContent = readFileSync(settingsPath, 'utf-8');
break;
}
catch {
/* skip */
}
}
}
if (!settingsContent) {
this.#loadHeuristic(projectRoot);
return;
}
const project = parseGradleProject(settingsContent);
const primaryLang = this.#matchedSystem?.language[0] || 'kotlin';
// 解析每个模块的 build.gradle.kts
for (const mod of project.includedModules) {
const modulePath = join(projectRoot, mod.directory);
if (!existsSync(modulePath)) {
continue;
}
// 读取 build.gradle.kts 获取 dependencies 和 plugins
for (const buildName of ['build.gradle.kts', 'build.gradle']) {
const buildPath = join(modulePath, buildName);
if (existsSync(buildPath)) {
try {
const buildContent = readFileSync(buildPath, 'utf-8');
const updatedMod = parseGradleProject(buildContent, mod);
// 更新 module 的 convention plugin 和 dependencies
mod.conventionPlugin =
updatedMod.includedModules[0]?.conventionPlugin ?? mod.conventionPlugin;
mod.dependencies = updatedMod.includedModules[0]?.dependencies ?? mod.dependencies;
}
catch {
/* skip */
}
break;
}
}
const inferredRole = mod.conventionPlugin
? inferConventionRole(mod.conventionPlugin)
: undefined;
this.#targets.push({
name: mod.path,
path: modulePath,
type: mod.path === ':app' ? 'application' : 'library',
language: primaryLang,
metadata: {
gradlePath: mod.path,
conventionPlugin: mod.conventionPlugin,
conventionRole: inferredRole,
},
});
}
}
// ── Private: CMake 加载 ──────────────────────────────
#loadCMake(projectRoot) {
const cmakePath = join(projectRoot, 'CMakeLists.txt');
if (!existsSync(cmakePath)) {
this.#loadHeuristic(projectRoot);
return;
}
let content;
try {
content = readFileSync(cmakePath, 'utf-8');
}
catch {
return;
}
const project = parseCMakeProject(content);
const primaryLang = this.#matchedSystem?.language[0] || 'cpp';
// 主目标
for (const target of project.targets) {
this.#targets.push({
name: target.name,
path: projectRoot,
type: target.type === 'executable' ? 'application' : 'library',
language: primaryLang,
metadata: {
cmakeType: target.type,
},
});
}
// 递归解析子目录的 CMakeLists.txt
for (const subdir of project.subdirectories) {
const subdirPath = join(projectRoot, subdir);
const subdirCmakePath = join(subdirPath, 'CMakeLists.txt');
if (!existsSync(subdirCmakePath)) {
continue;
}
try {
const subcontent = readFileSync(subdirCmakePath, 'utf-8');
const subproject = parseCMakeProject(subcontent);
for (const target of subproject.targets) {
this.#targets.push({
name: target.name,
path: subdirPath,
type: target.type === 'executable' ? 'application' : 'library',
language: primaryLang,
metadata: {
cmakeType: target.type,
subdirectory: subdir,
},
});
}
}
catch {
/* skip */
}
}
}
// ── Private: JSON Config 加载 (Nx/Flutter/RN) ────────
#loadJsonConfig(projectRoot) {
const system = this.#matchedSystem;
switch (system.id) {
case 'nx-monorepo':
this.#loadNx(projectRoot);
break;
case 'flutter-add-to-app':
this.#loadFlutterAddToApp(projectRoot);
break;
case 'react-native-hybrid':
this.#loadReactNative(projectRoot);
break;
default:
this.#loadHeuristic(projectRoot);
}
}
#loadNx(projectRoot) {
const nxJsonPath = join(projectRoot, 'nx.json');
if (!existsSync(nxJsonPath)) {
return;
}
// 扫描所有 project.json 文件
const projectJsonFiles = this.#findBuildFiles(projectRoot, ['project.json']);
const projects = [];
for (const pjFile of projectJsonFiles) {
try {
const content = readFileSync(pjFile, 'utf-8');
const parsed = parseNxWorkspace(content);
for (const proj of parsed.projects) {
projects.push(proj);
const modulePath = join(projectRoot, proj.root);
this.#targets.push({
name: proj.name,
path: modulePath,
type: proj.projectType === 'application' ? 'application' : 'library',
language: 'typescript',
metadata: {
tags: proj.tags,
nxProjectType: proj.projectType,
},
});
}
}
catch {
/* skip */
}
}
}
#loadFlutterAddToApp(projectRoot) {
// 解析 .flutter-plugins-dependencies
const depsPath = join(projectRoot, '.flutter-plugins-dependencies');
if (existsSync(depsPath)) {
try {
const content = readFileSync(depsPath, 'utf-8');
const parsed = parseFlutterPluginsDeps(content);
for (const plugin of parsed.plugins) {
this.#targets.push({
name: plugin.name,
path: plugin.path,
type: 'library',
language: 'dart',
metadata: {
platform: plugin.platform,
bridgeType: 'flutter-engine',
},
});
}
}
catch {
/* skip */
}
}
// 查找嵌入的 pubspec.yaml
const pubspecFiles = this.#findBuildFiles(projectRoot, ['pubspec.yaml']);
for (const pf of pubspecFiles) {
// 排除根目录的 pubspec(交给 DartDiscoverer 处理)
if (pf === join(projectRoot, 'pubspec.yaml')) {
continue;
}
try {
const content = readFileSync(pf, 'utf-8');
const nameMatch = content.match(/^name:\s*(\S+)/m);
if (nameMatch) {
const modDir = join(pf, '..');
this.#targets.push({
name: nameMatch[1],
path: modDir,
type: 'library',
language: 'dart',
metadata: {
pubspecPath: relative(projectRoot, pf),
},
});
}
}
catch {
/* skip */
}
}
}
#loadReactNative(projectRoot) {
const pkgJsonPath = join(projectRoot, 'package.json');
if (!existsSync(pkgJsonPath)) {
return;
}
try {
const content = readFileSync(pkgJsonPath, 'utf-8');
const parsed = parseReactNativeProject(content);
if (parsed.isReactNative) {
this.#targets.push({
name: parsed.name,
path: projectRoot,
type: 'application',
language: 'typescript',
metadata: {
rnVersion: parsed.rnVersion,
bridgeType: 'native-module',
},
});
}
}
catch {
/* skip */
}
}
// ── Private: 启发式加载 ────────────────────────────
#loadHeuristic(projectRoot) {
// 扫描根目录中可能包含模块的目录
try {
const entries = readdirSync(projectRoot, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.') || EXCLUDE_DIRS.has(entry.name)) {
continue;
}
// 检查是否是模块容器目录
if (/^(Local)?Modules?$|^Packages$/i.test(entry.name)) {
this.#scanModuleDirectory(join(projectRoot, entry.name));
}
}
}
catch {
/* skip */
}
}
/**
* 扫描模块容器目录,每个有 spec 文件或源码的子目录视为一个模块
*/
#scanModuleDirectory(containerDir) {
try {
const entries = readdirSync(containerDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.')) {
continue;
}
const modulePath = join(containerDir, entry.name);
// 查找 spec 文件
const specPath = this.#findSpecFile(modulePath, entry.name);
if (specPath) {
try {
const specContent = readFileSync(specPath, 'utf8');
const spec = parseModuleSpec(specContent);
this.#moduleSpecs.set(entry.name, spec);
}
catch {
/* skip */
}
}
// 检查目录是否包含源码文件
if (specPath || this.#hasSourceFiles(modulePath)) {
this.#targets.push({
name: entry.name,
path: modulePath,
type: 'library',
language: 'objectivec',
metadata: { specFile: specPath !== null },
});
}
}
}
catch {
/* skip */
}
}
// ── Private: 文件工具 ──────────────────────────────
/**
* 递归收集源码文件
*/
#collectSourceFiles(dir, rootDir, files, depth = 0) {
if (depth > 15 || files.length >= 500) {
return;
}
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) {
continue;
}
if (EXCLUDE_DIRS.has(entry.name)) {
continue;
}
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
this.#collectSourceFiles(fullPath, rootDir, files, depth + 1);
}
else if (entry.isFile()) {
const ext = extname(entry.name);
if (SOURCE_EXTENSIONS.has(ext) || LanguageService.sourceExts.has(ext)) {
const lang = LanguageService.inferLang(entry.name) || 'unknown';
files.push({
name: entry.name,
path: fullPath,
relativePath: relative(rootDir, fullPath),
language: lang,
});
}
}
if (files.length >= 500) {
return;
}
}
}
catch {
/* skip */
}
}
/**
* 检查目录中是否存在源码文件(浅层检查)
*/
#hasSourceFiles(dir, depth = 0) {
if (depth > 3) {
return false;
}
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) {
continue;
}
if (entry.isFile()) {
const ext = extname(entry.name);
if (SOURCE_EXTENSIONS.has(ext)) {
return true;
}
}
else if (entry.isDirectory() && !EXCLUDE_DIRS.has(entry.name)) {
if (this.#hasSourceFiles(join(dir, entry.name), depth + 1)) {
return true;
}
}
}
}
catch {
/* skip */
}
return false;
}
}
// ── Module-level helpers ────────────────────────────
/**
* 计算目录下包含 spec 文件的子目录数量
*/
function countSubdirsWithSpecs(containerDir) {
let count = 0;
try {
const entries = readdirSync(containerDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.')) {
continue;
}
try {
const subEntries = readdirSync(join(containerDir, entry.name));
const hasSpec = subEntries.some((e) => e.endsWith('.boxspec') || e.endsWith('.podspec'));
if (hasSpec) {
count++;
}
}
catch {
/* skip */
}
}
}
catch {
/* skip */
}
return count;
}