ensureModuleCppTargets function

bool ensureModuleCppTargets(
  1. String packageSwiftPath, {
  2. required String pluginName,
  3. required String pluginClass,
  4. required List<String> moduleClasses,
})

Ensures packageSwiftPath declares one <ModuleClass>Cpp target per entry in moduleClasses and that the Swift target (pluginName) depends on each of them. Idempotent: a module whose name: "<ModuleClass>Cpp" already appears anywhere in the manifest is left untouched, so hand-tuned target blocks survive re-runs.

New targets are inserted immediately after the targets: [ opening bracket (each inserted block carries its own trailing comma), which stays valid regardless of how the existing entries are formatted — no trailing-comma or last-bracket heuristics.

Only manifests in the nitrogen-generated shape are patched: the file must contain a targets: [ array AND the plugin-level "<pluginClass>Cpp" target. Anything else (fully hand-authored) is skipped and the exact blocks to paste are printed so the user can apply them deliberately.

Returns true if the file was modified.

Implementation

bool ensureModuleCppTargets(
  String packageSwiftPath, {
  required String pluginName,
  required String pluginClass,
  required List<String> moduleClasses,
}) {
  final file = File(packageSwiftPath);
  if (!file.existsSync()) return false;
  var content = file.readAsStringSync();

  final missing = moduleClasses.where((c) => !content.contains('name: "${c}Cpp"')).toList();
  final missingSwiftDeps = moduleClasses.where((c) => !RegExp('"${c}Cpp"\\s*,').hasMatch(content)).toList();
  if (missing.isEmpty && missingSwiftDeps.isEmpty) return false;

  // Anchor at the PACKAGE-LEVEL targets array — it starts its own line.
  // `products:` also contains a `targets: [...]` (inline, inside .library(...))
  // which must never be the insertion point.
  final targetsMatch = RegExp(r'^[ \t]*targets: \[', multiLine: true).firstMatch(content);
  final targetsIdx = targetsMatch?.start ?? -1;
  if (targetsIdx == -1 || !content.contains('"${pluginClass}Cpp"')) {
    // Hand-authored manifest — refuse surgery, hand the user the blocks.
    stdout.writeln('  ⚠ $packageSwiftPath is not in the nitrogen-generated shape — add these targets manually:');
    for (final c in missing) {
      stdout.writeln(moduleCppTargetBlock(c, pluginClass));
    }
    stdout.writeln('  …and add ${missing.map((c) => '"${c}Cpp"').join(', ')} to the "$pluginName" target\'s dependencies.');
    return false;
  }

  // Match the manifest's target indentation (2-space templates from
  // swift_templates.dart vs 4-space from scaffold_templates.dart).
  final lineStart = content.lastIndexOf('\n', targetsIdx) + 1;
  final baseIndent = RegExp(r'^ *').firstMatch(content.substring(lineStart))!.group(0)!;
  final targetIndent = '$baseIndent  ';

  // 1. Insert missing module targets right after the package-level
  //    `targets: [` (each block carries its own trailing comma, so the
  //    insertion is valid regardless of the existing entries' formatting).
  final openBracketEnd = targetsMatch!.end;
  if (missing.isNotEmpty) {
    final blocks = missing.map((c) => moduleCppTargetBlock(c, pluginClass, indent: targetIndent)).join('\n');
    content = '${content.substring(0, openBracketEnd)}\n$blocks${content.substring(openBracketEnd)}';
  }

  // 2. Add each module target to the Swift target's dependencies array.
  //    The Swift target is the one named after the plugin (snake_case),
  //    searched from the package-level targets array onward.
  final swiftNameIdx = content.indexOf('name: "$pluginName"', targetsIdx);
  if (swiftNameIdx != -1) {
    final depsIdx = content.indexOf('dependencies: [', swiftNameIdx);
    if (depsIdx != -1) {
      final depLineStart = content.lastIndexOf('\n', depsIdx) + 1;
      final depIndent = RegExp(r'^ *').firstMatch(content.substring(depLineStart))!.group(0)!;
      final toAdd = moduleClasses.where((c) {
        final depsRegion = content.substring(depsIdx, content.indexOf(']', depsIdx));
        return !depsRegion.contains('"${c}Cpp"');
      }).toList();
      if (toAdd.isNotEmpty) {
        final deps = toAdd.map((c) => '$depIndent  "${c}Cpp",').join('\n');
        content = content.replaceFirst('dependencies: [', 'dependencies: [\n$deps', depsIdx);
      }
    }
  }

  file.writeAsStringSync(content);
  return true;
}