discoverPackageCommands static method

Future<List<NyCommand>> discoverPackageCommands({
  1. Directory? projectDirectory,
  2. void onWarning(
    1. String message
    )?,
})

Discovers commands from packages in .dart_tool/package_config.json that ship a metro_commands.json (like commands.json; scripts must live in bin/).

Implementation

static Future<List<NyCommand>> discoverPackageCommands({
  Directory? projectDirectory,
  void Function(String message)? onWarning,
}) async {
  final void Function(String message) warn =
      onWarning ?? MetroConsole.writeInYellow;

  final Directory project = (projectDirectory ?? Directory.current).absolute;
  final Uri projectUri = project.uri;
  final File packageConfig = File.fromUri(
    projectUri.resolve(packageConfigPath),
  );
  if (!await packageConfig.exists()) return [];

  List<dynamic> packages;
  try {
    final dynamic decoded = jsonDecode(await packageConfig.readAsString());
    packages = (decoded is Map && decoded['packages'] is List)
        ? decoded['packages']
        : const [];
  } catch (e) {
    warn('[Metro] Could not read $packageConfigPath: $e');
    return [];
  }

  final List<NyCommand> commands = [];
  for (final dynamic package in packages) {
    if (package is! Map) continue;
    final dynamic name = package['name'];
    final dynamic rootUri = package['rootUri'];
    if (name is! String || rootUri is! String) continue;

    // rootUri is relative to the package_config.json file itself and may be
    // percent-encoded, so resolve it as a URI rather than joining strings.
    Uri packageRoot;
    try {
      packageRoot = packageConfig.uri.resolveUri(Uri.parse(rootUri));
    } on FormatException {
      continue;
    }
    if (!packageRoot.path.endsWith('/')) {
      packageRoot = packageRoot.replace(path: '${packageRoot.path}/');
    }

    // Skip the project itself - it registers its commands in commands.json.
    if (packageRoot == projectUri) continue;

    final File manifest = File.fromUri(
      packageRoot.resolve(packageCommandsManifest),
    );
    if (!await manifest.exists()) continue;

    try {
      final dynamic decoded = jsonDecode(await manifest.readAsString());
      if (decoded is! List) {
        throw const FormatException('Expected a JSON array of commands');
      }

      final List<dynamic> configs = [];
      for (final dynamic config in decoded) {
        if (config is! Map ||
            config['name'] is! String ||
            config['script'] is! String) {
          warn(
            '[Metro] Ignoring an invalid command in $packageCommandsManifest of package "$name" - "name" and "script" are required',
          );
          continue;
        }
        if (!_isBinScriptName(config['script'])) {
          warn(
            '[Metro] Ignoring "${config['category'] ?? name}:${config['name']}" in $packageCommandsManifest of package "$name" - "script" must name a Dart file directly inside the package\'s bin/ folder, e.g. "my_command.dart"',
          );
          continue;
        }
        configs.add(config);
      }

      commands.addAll(
        await discoverCommands(
          configs,
          scriptDirectory: packageRoot.resolve('bin/').toFilePath(),
          package: name,
          defaultCategory: name,
          workingDirectory: project.path,
        ),
      );
    } catch (e) {
      warn(
        '[Metro] Could not load $packageCommandsManifest from package "$name": $e',
      );
    }
  }

  commands.sort((a, b) {
    final int byPackage = (a.package ?? '').compareTo(b.package ?? '');
    if (byPackage != 0) return byPackage;
    return a.fullName.compareTo(b.fullName);
  });

  return commands;
}