环境准备
安装 graphviz
dart
arch -arm64 brew install graphviz
项目根目录pubspec.yaml文件内添加
dart
dev_dependencies:
yaml: ^3.1.1
gviz: ^0.4.0
执行脚本
项目根目录下添加dart文件,运行main函数
dart
import 'dart:io';
import 'dart:convert';
import 'package:yaml/yaml.dart' as yaml;
import 'package:gviz/gviz.dart';
void main() async {
final projectPath = await _getProjectPath();
final file = File('$projectPath/pubspec.yaml');
final fileContent = file.readAsStringSync();
final yamlMap = yaml.loadYaml(fileContent) as yaml.YamlMap;
final appName = yamlMap['name'].toString();
print('开始 ...');
final dependencyContent = await _getComponentDependencyTree(
projectPath: projectPath,
);
print('... 开始遍历组件依赖节点');
print(dependencyContent);
final dependencyNodes = _traversalComponentDependencyTree(dependencyContent);
print('... 完成遍历组件依赖节点');
final graph = Gviz(
name: appName,
graphProperties: {
'pad': '0.5',
'nodesep': '1',
'ranksep': '2',
},
edgeProperties: {
'fontcolor': 'gray',
},
);
print('... 开始转换 dot 节点');
_generateDotByNodes(
dependencyNodes,
graph: graph,
edgeCache: <String>[],
);
print('... 完成转换 dot 节点');
final dotDirectoryPath = '$projectPath/dotGenerateDir';
final dotDirectory = Directory(dotDirectoryPath);
if (!dotDirectory.existsSync()) {
await dotDirectory.create();
print('... 创建 dotGenerate 文件夹');
}
final dotFileName = '$appName.dot';
final dotPngName = '$appName.png';
final dotFile = File('$dotDirectoryPath/$dotFileName');
final dotPngFile = File('$dotDirectoryPath/$dotPngName');
if (dotFile.existsSync()) {
await dotFile.delete();
print('... 删除原有 dot 生成文件');
}
if (dotPngFile.existsSync()) {
await dotPngFile.delete();
print('... 删除原有 dot 依赖关系图');
}
await dotFile.create();
final dotResult = await dotFile.writeAsString(graph.toString());
print('dot 文件生成成功: ${dotResult.path}');
print('... 开始生成 dot png');
await _runCommand(
executable: 'dot',
projectPath: projectPath,
commandArgs: [
'$dotDirectoryPath/$dotFileName',
'-T',
'png',
'-o',
'$dotDirectoryPath/$dotPngName'
],
);
print('png 文件生成成功:$dotDirectoryPath/$dotPngName');
await Process.run(
'open',
[dotDirectoryPath],
);
}
const List<String> ignoreDependency = <String>[
'flutter',
'flutter_test',
'flutter_lints',
'cupertino_icons',
'gviz',
'yaml',
'injectable_generator',
'build_runner',
];
Future<String> _getComponentDependencyTree({
required String projectPath,
}) {
return _runCommand(
projectPath: projectPath,
commandArgs: ['pub', 'deps', '--json'],
).then(
(value) {
if (value.contains('dependencies:') &&
value.contains('dev dependencies:')) {
final start = value.indexOf('dependencies:');
final end = value.indexOf('dev dependencies:');
return value.substring(start, end);
} else {
return value;
}
},
);
}
List<DependencyNode> _traversalComponentDependencyTree(String dependencyContent) {
final dependencyJson = jsonDecode(dependencyContent) as Map<String, dynamic>;
final packages = dependencyJson['packages'] as List<dynamic>;
final nodeMap = <String, DependencyNode>{};
for (var package in packages) {
final node = DependencyNode.fromMap(package);
nodeMap[node.name] = node;
}
final rootNode = nodeMap.values.firstWhere((element) => element.isRootNode);
void mapDependencies(DependencyNode node, Set<String> visitedNodes) {
if (visitedNodes.contains(node.name)) {
return;
}
visitedNodes.add(node.name);
for (final itemName in node.dependencies) {
if (!ignoreDependency.contains(itemName)) {
final itemNode = nodeMap[itemName];
if (itemNode != null) {
mapDependencies(itemNode, visitedNodes);
node.children.add(itemNode);
itemNode.isLevel1Node = false;
}
}
}
}
final visitedNodes = <String>{};
mapDependencies(rootNode, visitedNodes);
// 使用新的 rebuildDependencyTree 函数来创建一个没有重复依赖的新树
DependencyNode newRootNode = rebuildDependencyTree(rootNode, Set<String>());
return [newRootNode];
}
DependencyNode rebuildDependencyTree(DependencyNode originalNode, Set<String> seenDependencies) {
// 创建一个新的节点,复制原始节点的属性
DependencyNode newNode = DependencyNode(
name: originalNode.name,
version: originalNode.version,
kind: originalNode.kind,
source: originalNode.source,
dependencies: originalNode.dependencies,
);
newNode.isLevel1Node = originalNode.isLevel1Node;
// 如果这个节点已经被处理过,直接返回新节点(没有子节点)
if (seenDependencies.contains(newNode.name)) {
return newNode;
}
// 将这个节点添加到已处理集合中
seenDependencies.add(newNode.name);
// 处理子节点
for (var childNode in originalNode.children) {
if (!ignoreDependency.contains(childNode.name)) {
var newChildNode = rebuildDependencyTree(childNode, seenDependencies);
newNode.children.add(newChildNode);
}
}
return newNode;
}
Future<String> _getProjectPath() async {
final originProjectPath = await Process.run(
'pwd',
[],
);
final projectPath = (originProjectPath.stdout as String).replaceAll(
'\n',
'',
);
return projectPath;
}
void _generateDotByNodes(
List<DependencyNode> nodes, {
required Gviz graph,
required List<String> edgeCache,
}) {
if (nodes.isEmpty) {
return;
}
for (int index = 0; index < nodes.length; index++) {
final itemNode = nodes[index];
final from = '${itemNode.name}\n${itemNode.version}';
if (!graph.nodeExists(from)) {
graph.addNode(
from,
properties: {
'color': 'black',
'shape': 'rectangle',
'margin': '1,0.8',
'penwidth': '7',
'style': 'filled',
'fillcolor': 'gray',
'fontsize': itemNode.isLevel1Node ? '60' : '55',
},
);
}
final toArr = itemNode.children.map((e) => '${e.name}\n${e.version}').toList();
for (var element in toArr) {
final edgeKey = '$from-$element';
if (!edgeCache.contains(edgeKey)) {
graph.addEdge(
from,
element,
properties: {
'penwidth': '2',
'style': 'dashed',
'arrowed': 'vee',
},
);
edgeCache.add(edgeKey);
}
}
_generateDotByNodes(
itemNode.children,
graph: graph,
edgeCache: edgeCache,
);
}
}
Future<String> _runCommand({
String executable = 'flutter',
required String projectPath,
required List<String> commandArgs,
}) {
return Process.run(
executable,
commandArgs,
runInShell: true,
workingDirectory: projectPath,
).then((result) => result.stdout as String);
}
class DependencyNode {
final String name;
final String version;
final String kind;
final String source;
final List<String> dependencies;
final children = <DependencyNode>[];
bool isLevel1Node = true;
factory DependencyNode.fromMap(Map<String, dynamic> map) {
return DependencyNode(
name: map['name'] as String,
version: map['version'] as String,
kind: map['kind'] as String,
source: map['source'] as String,
dependencies: (map['dependencies'] as List<dynamic>)
.map((e) => e as String)
.toList(),
);
}
bool get isRootNode => kind == 'root';
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is DependencyNode &&
runtimeType == other.runtimeType &&
name == other.name;
@override
int get hashCode => name.hashCode;
DependencyNode({
required this.name,
required this.version,
required this.kind,
required this.source,
required this.dependencies,
});
}
输出示例