detectManagedContentIssues function
Scans Plugin.kt and Plugin.swift for managed sections (JniBridge import, register() call, Registry.register) that are expected for non-cpp modules but are currently missing. Returns each gap as a ManagedContentIssue.
Called before the link TUI starts so the user can confirm re-injection.
Implementation
List<ManagedContentIssue> detectManagedContentIssues({String baseDir = '.'}) {
final issues = <ManagedContentIssue>[];
final libDir = Directory(p.join(baseDir, 'lib'));
if (!libDir.existsSync()) return issues;
final allSpecFiles = libDir
.listSync(recursive: true)
.whereType<File>()
.where((f) => f.path.endsWith('.native.dart'))
.toList();
if (allSpecFiles.isEmpty) return issues;
// For Android Plugin.kt: a module needs JniBridge registration when it does NOT
// use a native C++ impl on android/linux (isNativeCppModule). A module like
// `benchmark` (android: kotlin, windows: cpp) is correctly included here because
// isNativeCppModule checks android/linux only — isCppModule (broad) would
// falsely exclude it due to the windows: cpp entry.
final androidSpecFiles = allSpecFiles
.where((f) => !isNativeCppModule(f))
.toList();
// For iOS Plugin.swift: a module needs Registry.register when it does NOT use
// NativeImpl.cpp specifically on iOS (mixed ios:swift/macos:cpp still needs iOS registration).
final iosSpecFiles = allSpecFiles.where((f) => !isIosCppModule(f)).toList();
// ── Android: Plugin.kt ────────────────────────────────────────────────────
final ktDir = Directory(p.join(baseDir, 'android', 'src', 'main', 'kotlin'));
if (ktDir.existsSync()) {
final pluginFiles = ktDir
.listSync(recursive: true)
.whereType<File>()
.where((f) => f.path.endsWith('Plugin.kt'))
.toList();
if (pluginFiles.isNotEmpty) {
final kt = pluginFiles.first.readAsStringSync();
final ktPath = p.relative(pluginFiles.first.path, from: baseDir);
for (final specFile in androidSpecFiles) {
final stem = p
.basename(specFile.path)
.replaceAll(RegExp(r'\.native\.dart$'), '');
final lib = (extractLibNameFromSpec(specFile) ?? stem).replaceAll(
'-',
'_',
);
final moduleMatch = RegExp(
r'abstract class (\w+) extends HybridObject',
).firstMatch(specFile.readAsStringSync());
final moduleName = moduleMatch?.group(1) ?? _toPascalCase(stem);
final importLine = 'import nitro.${lib}_module.${moduleName}JniBridge';
final registerCall = '${moduleName}JniBridge.register(';
if (!kt.contains(importLine)) {
issues.add(
ManagedContentIssue(
file: ktPath,
description: 'Missing import: $importLine',
),
);
}
if (!kt.contains(registerCall)) {
issues.add(
ManagedContentIssue(
file: ktPath,
description:
'Missing registration: ${moduleName}JniBridge.register(${moduleName}Impl(...))',
),
);
}
}
}
}
// ── iOS: Plugin.swift ─────────────────────────────────────────────────────
final iosDir = Directory(p.join(baseDir, 'ios'));
if (iosDir.existsSync()) {
final swiftFiles = iosDir
.listSync(recursive: true, followLinks: false)
.whereType<File>()
.where(
(f) =>
!f.path.contains('.symlinks') && f.path.endsWith('Plugin.swift'),
)
.toList();
if (swiftFiles.isNotEmpty) {
final swift = swiftFiles.first.readAsStringSync();
final swiftPath = p.relative(swiftFiles.first.path, from: baseDir);
for (final specFile in iosSpecFiles) {
final stem = p
.basename(specFile.path)
.replaceAll(RegExp(r'\.native\.dart$'), '');
final moduleMatch = RegExp(
r'abstract class (\w+) extends HybridObject',
).firstMatch(specFile.readAsStringSync());
final moduleName = moduleMatch?.group(1) ?? _toPascalCase(stem);
if (!swift.contains('${moduleName}Registry.register(')) {
issues.add(
ManagedContentIssue(
file: swiftPath,
description:
'Missing registration: ${moduleName}Registry.register(${moduleName}ModuleImpl())',
),
);
}
}
}
}
return issues;
}