autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
478 lines (477 loc) • 18 kB
JavaScript
/**
* @module DartDiscoverer
* @description Dart / Flutter 项目结构发现器
*
* 检测信号: pubspec.yaml, pubspec.lock, .dart_tool/, *.dart
* 支持: 单 Package 项目、Flutter 应用、Melos 多包工作区
*/
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { basename, extname, join, relative } from 'node:path';
import { ProjectDiscoverer, } from './ProjectDiscoverer.js';
const SOURCE_EXTENSIONS = new Set(['.dart']);
const EXCLUDE_DIRS = new Set([
'.git',
'.dart_tool',
'.fvm',
'build',
'node_modules',
'.idea',
'.vscode',
'ios',
'android',
'macos',
'windows',
'linux',
'web',
'.pub-cache',
'.pub',
'.cursor',
]);
export class DartDiscoverer extends ProjectDiscoverer {
#projectRoot = null;
#targets = [];
#depGraph = { nodes: [], edges: [] };
#packageName = null;
get id() {
return 'dart';
}
get displayName() {
return 'Dart / Flutter';
}
async detect(projectRoot) {
let confidence = 0;
const reasons = [];
if (existsSync(join(projectRoot, 'pubspec.yaml'))) {
confidence = 0.92;
reasons.push('pubspec.yaml exists');
}
if (existsSync(join(projectRoot, 'pubspec.lock'))) {
confidence = Math.max(confidence, 0.7);
if (confidence < 0.92) {
confidence += 0.1;
}
reasons.push('pubspec.lock exists');
}
if (existsSync(join(projectRoot, '.dart_tool'))) {
confidence = Math.max(confidence, 0.6);
reasons.push('.dart_tool exists');
}
// Melos workspace
if (existsSync(join(projectRoot, 'melos.yaml'))) {
confidence = Math.max(confidence, 0.95);
reasons.push('melos.yaml exists (workspace)');
}
// 兜底: 根目录有 .dart 文件
if (confidence === 0) {
try {
const entries = readdirSync(projectRoot);
if (entries.some((e) => e.endsWith('.dart'))) {
confidence = 0.5;
reasons.push('*.dart files found at root');
}
}
catch {
/* skip */
}
}
return {
match: confidence > 0,
confidence: Math.min(confidence, 1.0),
reason: reasons.join(', ') || 'No Dart markers found',
};
}
async load(projectRoot) {
this.#projectRoot = projectRoot;
this.#targets = [];
this.#depGraph = { nodes: [], edges: [] };
// 解析 pubspec.yaml
const pubspec = this.#parsePubspec(projectRoot);
this.#packageName = pubspec?.name || basename(projectRoot);
const framework = this.#detectFramework(pubspec);
const isFlutter = framework === 'flutter' || !!pubspec?.dependencies?.flutter;
// 主 Target — lib/
this.#targets.push({
name: this.#packageName,
path: join(projectRoot, 'lib'),
type: 'library',
language: 'dart',
framework,
metadata: {
packageName: this.#packageName,
isFlutter,
sdkVersion: pubspec?.environment?.sdk || null,
flutterVersion: pubspec?.environment?.flutter || null,
},
});
this.#depGraph.nodes.push(this.#packageName);
// bin/ — CLI 应用入口
const binDir = join(projectRoot, 'bin');
if (existsSync(binDir)) {
this.#targets.push({
name: 'bin',
path: binDir,
type: 'application',
language: 'dart',
framework,
});
}
// test/ — 测试目录
for (const testDir of ['test', 'test_driver', 'integration_test']) {
const testPath = join(projectRoot, testDir);
if (existsSync(testPath)) {
this.#targets.push({
name: testDir,
path: testPath,
type: 'test',
language: 'dart',
});
}
}
// example/ — 示例项目
const exampleDir = join(projectRoot, 'example');
if (existsSync(exampleDir) && existsSync(join(exampleDir, 'pubspec.yaml'))) {
this.#targets.push({
name: 'example',
path: exampleDir,
type: 'example',
language: 'dart',
framework,
});
}
// Melos 多包工作区
this.#discoverMelosPackages(projectRoot);
// 解析依赖图
this.#parseDependencies(pubspec);
// 解析内部 import 关系
this.#parseInternalImports(projectRoot);
}
async listTargets() {
return this.#targets;
}
async getTargetFiles(target) {
const targetPath = typeof target === 'string'
? this.#targets.find((t) => t.name === target)?.path || this.#projectRoot
: target.path;
if (!targetPath || !existsSync(targetPath)) {
return [];
}
const files = [];
this.#collectDartFiles(targetPath, targetPath, files);
return files;
}
async getDependencyGraph() {
return this.#depGraph;
}
// ── 内部实现 ──
/** 解析 pubspec.yaml(简易 YAML 解析,不引入三方依赖) */
#parsePubspec(projectRoot) {
const pubspecPath = join(projectRoot, 'pubspec.yaml');
if (!existsSync(pubspecPath)) {
return null;
}
try {
const content = readFileSync(pubspecPath, 'utf8');
return this.#parseSimpleYaml(content);
}
catch {
return null;
}
}
/**
* 极简 YAML 解析器 — 仅支持顶层和一层嵌套的 key: value
* 用于解析 pubspec.yaml 中的 name, dependencies, environment 等
*/
#parseSimpleYaml(content) {
const result = {};
let currentSection = null;
for (const line of content.split('\n')) {
// 跳过注释和空行
if (/^\s*#/.test(line) || /^\s*$/.test(line)) {
continue;
}
// 顶层 key(无缩进)
const topMatch = line.match(/^(\w[\w-]*):\s*(.*)/);
if (topMatch) {
const key = topMatch[1];
const value = topMatch[2].trim();
if (value) {
result[key] = value;
currentSection = null;
}
else {
result[key] = {};
currentSection = key;
}
continue;
}
// 嵌套 key(有缩进)
if (currentSection) {
const nestedMatch = line.match(/^\s+(\w[\w-]*):\s*(.*)/);
if (nestedMatch) {
const key = nestedMatch[1];
const value = nestedMatch[2].trim();
if (typeof result[currentSection] === 'object') {
result[currentSection][key] = value || true;
}
}
}
}
return result;
}
/** 检测 Flutter/Dart 框架 */
#detectFramework(pubspec) {
if (!pubspec) {
return null;
}
const deps = {
...(typeof pubspec.dependencies === 'object' ? pubspec.dependencies : {}),
...(typeof pubspec.dev_dependencies === 'object' ? pubspec.dev_dependencies : {}),
};
// Flutter SDK
if (deps.flutter || deps.flutter_test) {
// Sub-framework detection
if (deps.flutter_riverpod || deps.hooks_riverpod || deps.riverpod) {
return 'flutter-riverpod';
}
if (deps.flutter_bloc || deps.bloc) {
return 'flutter-bloc';
}
if (deps.get || deps.getx) {
return 'flutter-getx';
}
if (deps.provider) {
return 'flutter-provider';
}
return 'flutter';
}
// Pure Dart server/CLI
if (deps.shelf || deps.shelf_router) {
return 'shelf';
}
if (deps.dart_frog) {
return 'dart-frog';
}
if (deps.serverpod) {
return 'serverpod';
}
if (deps.args || deps.cli_util) {
return 'dart-cli';
}
return null;
}
/** 发现 Melos 多包工作区中的子包 */
#discoverMelosPackages(projectRoot) {
const melosPath = join(projectRoot, 'melos.yaml');
if (!existsSync(melosPath)) {
return;
}
try {
const content = readFileSync(melosPath, 'utf8');
const _melos = this.#parseSimpleYaml(content);
// Melos packages 字段(简化处理: 扫描 packages/ 目录)
const packagesDir = join(projectRoot, 'packages');
if (existsSync(packagesDir)) {
const entries = readdirSync(packagesDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.')) {
continue;
}
const pkgDir = join(packagesDir, entry.name);
if (existsSync(join(pkgDir, 'pubspec.yaml'))) {
const subPubspec = this.#parsePubspec(pkgDir);
const pkgName = subPubspec?.name || entry.name;
this.#targets.push({
name: `packages/${pkgName}`,
path: join(pkgDir, 'lib'),
type: 'library',
language: 'dart',
metadata: { isMelosPackage: true, packageName: pkgName },
});
this.#depGraph.nodes.push({ id: pkgName, label: pkgName, type: 'internal' });
}
}
}
// 也检查 apps/ 目录(部分 Melos 工作区的约定)
const appsDir = join(projectRoot, 'apps');
if (existsSync(appsDir)) {
const entries = readdirSync(appsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.')) {
continue;
}
const appDir = join(appsDir, entry.name);
if (existsSync(join(appDir, 'pubspec.yaml'))) {
const subPubspec = this.#parsePubspec(appDir);
const appName = subPubspec?.name || entry.name;
this.#targets.push({
name: `apps/${appName}`,
path: join(appDir, 'lib'),
type: 'application',
language: 'dart',
metadata: { isMelosPackage: true, packageName: appName },
});
this.#depGraph.nodes.push({ id: appName, label: appName, type: 'internal' });
}
}
}
}
catch {
/* skip */
}
}
/** 解析 pubspec.yaml 依赖到 depGraph */
#parseDependencies(pubspec) {
if (!pubspec) {
return;
}
const nodeSet = new Set(this.#depGraph.nodes.map((n) => (typeof n === 'string' ? n : n.id)));
const rootNode = this.#packageName;
if (!rootNode) {
return;
}
const addDep = (name, isDev) => {
if (!nodeSet.has(name)) {
this.#depGraph.nodes.push({
id: name,
label: name,
type: 'external',
isDev,
});
nodeSet.add(name);
}
this.#depGraph.edges.push({
from: rootNode,
to: name,
type: isDev ? 'dev-dependency' : 'dependency',
});
};
if (typeof pubspec.dependencies === 'object') {
for (const dep of Object.keys(pubspec.dependencies)) {
if (dep === 'flutter' || dep === 'flutter_localizations') {
continue; // SDK 依赖,不记为外部包
}
addDep(dep, false);
}
}
if (typeof pubspec.dev_dependencies === 'object') {
for (const dep of Object.keys(pubspec.dev_dependencies)) {
if (dep === 'flutter_test' || dep === 'flutter_lints' || dep === 'flutter_driver') {
continue;
}
addDep(dep, true);
}
}
}
/** 解析内部 Dart import 语句,构建包内模块依赖关系 */
#parseInternalImports(projectRoot) {
const libDir = join(projectRoot, 'lib');
if (!existsSync(libDir)) {
return;
}
const nodeSet = new Set(this.#depGraph.nodes.map((n) => (typeof n === 'string' ? n : n.id)));
const edgeSet = new Set();
// 收集 lib/ 下的子目录作为内部模块
try {
const entries = readdirSync(libDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith('.') && !entry.name.startsWith('_')) {
const moduleId = `lib/${entry.name}`;
if (!nodeSet.has(moduleId)) {
this.#depGraph.nodes.push({ id: moduleId, label: entry.name, type: 'internal' });
nodeSet.add(moduleId);
}
}
}
}
catch {
/* skip */
}
// 扫描 import 语句
const scanDir = (dir) => {
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
if (!entry.name.startsWith('.') && !EXCLUDE_DIRS.has(entry.name)) {
scanDir(join(dir, entry.name));
}
}
else if (entry.isFile() && entry.name.endsWith('.dart')) {
try {
const content = readFileSync(join(dir, entry.name), 'utf8');
const relDir = relative(libDir, dir);
const fromModule = relDir ? `lib/${relDir.split('/')[0]}` : (this.#packageName ?? '');
// 匹配 import 'package:xxx/yyy.dart'
const imports = content.matchAll(/import\s+['"]package:(\w+)\/([^'"]+)['"]/g);
for (const m of imports) {
const pkg = m[1];
const filePath = m[2];
if (pkg === this.#packageName) {
// 内部 import
const targetModule = `lib/${filePath.split('/')[0]}`;
if (targetModule !== fromModule && nodeSet.has(targetModule)) {
const edgeKey = `${fromModule}->${targetModule}`;
if (!edgeSet.has(edgeKey)) {
edgeSet.add(edgeKey);
this.#depGraph.edges.push({
from: fromModule,
to: targetModule,
type: 'internal',
});
}
}
}
}
}
catch {
/* skip */
}
}
}
}
catch {
/* skip */
}
};
scanDir(libDir);
}
/** 递归收集 .dart 文件 */
#collectDartFiles(dir, rootDir, files, depth = 0) {
if (depth > 15) {
return;
}
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) {
continue;
}
if (entry.isDirectory()) {
if (EXCLUDE_DIRS.has(entry.name)) {
continue;
}
this.#collectDartFiles(join(dir, entry.name), rootDir, files, depth + 1);
}
else if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name))) {
const fullPath = join(dir, entry.name);
try {
const content = readFileSync(fullPath, 'utf8');
files.push({
name: entry.name,
path: fullPath,
relativePath: relative(rootDir, fullPath),
language: 'dart',
content,
});
}
catch {
/* unreadable */
}
}
}
}
catch {
/* permission error */
}
}
}