Flutter【03】图片输出package依赖关系

环境准备

安装 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,
  });
}

输出示例


相关推荐
li_liuliu几秒前
Android4.4 在系统中添加自己的System Service
android
C4rpeDime2 小时前
自建MD5解密平台-续
android
鲤籽鲲4 小时前
C# Random 随机数 全面解析
android·java·c#
m0_548514778 小时前
2024.12.10——攻防世界Web_php_include
android·前端·php
凤邪摩羯8 小时前
Android-性能优化-03-启动优化-启动耗时
android
凤邪摩羯8 小时前
Android-性能优化-02-内存优化-LeakCanary原理解析
android
喀什酱豆腐9 小时前
Handle
android
crasowas9 小时前
iOS - 超好用的隐私清单修复脚本(持续更新)
ios·app store
m0_7482329210 小时前
Android Https和WebView
android·网络协议·https
m0_7482517210 小时前
Android webview 打开本地H5项目(Cocos游戏以及Unity游戏)
android·游戏·unity