diff --git a/config/nativephp.php b/config/nativephp.php index 7ae7ca0..f3ab13f 100644 --- a/config/nativephp.php +++ b/config/nativephp.php @@ -128,7 +128,7 @@ 'storage/framework/sessions', 'storage/framework/cache', 'storage/framework/testing', - 'storage/logs/laravel.log' + 'storage/logs/laravel.log', ], /* diff --git a/src/Commands/PluginRegisterCommand.php b/src/Commands/PluginRegisterCommand.php index a85ebaf..5f6e62b 100644 --- a/src/Commands/PluginRegisterCommand.php +++ b/src/Commands/PluginRegisterCommand.php @@ -292,6 +292,102 @@ protected function detectConflicts(Plugin $newPlugin): array } } + // Check native dependency version conflicts + foreach ($this->registry->all() as $existing) { + $conflicts = array_merge($conflicts, $this->detectDependencyConflictsBetween($newPlugin, $existing)); + } + return $conflicts; } + + /** + * Detect native dependency version conflicts between two plugins. + * + * @return array Conflict messages + */ + protected function detectDependencyConflictsBetween(Plugin $newPlugin, Plugin $existing): array + { + $conflicts = []; + + // Android Gradle dependencies + $newAndroidDeps = $this->parseAndroidDeps($newPlugin); + $existingAndroidDeps = $this->parseAndroidDeps($existing); + + foreach ($newAndroidDeps as $artifact => $version) { + if (isset($existingAndroidDeps[$artifact]) && $existingAndroidDeps[$artifact] !== $version) { + $conflicts[] = "Android dependency '{$artifact}' version conflict: {$version} vs {$existingAndroidDeps[$artifact]} (from '{$existing->name}')"; + } + } + + // CocoaPods + $newPods = $this->parsePodDeps($newPlugin); + $existingPods = $this->parsePodDeps($existing); + + foreach ($newPods as $podName => $version) { + if ($version !== null && isset($existingPods[$podName]) && $existingPods[$podName] !== null && $existingPods[$podName] !== $version) { + $conflicts[] = "CocoaPods dependency '{$podName}' version conflict: {$version} vs {$existingPods[$podName]} (from '{$existing->name}')"; + } + } + + // Swift Packages + $newSwiftPkgs = $this->parseSwiftPackageDeps($newPlugin); + $existingSwiftPkgs = $this->parseSwiftPackageDeps($existing); + + foreach ($newSwiftPkgs as $url => $version) { + if ($version !== null && isset($existingSwiftPkgs[$url]) && $existingSwiftPkgs[$url] !== null && $existingSwiftPkgs[$url] !== $version) { + $conflicts[] = "Swift Package '{$url}' version conflict: {$version} vs {$existingSwiftPkgs[$url]} (from '{$existing->name}')"; + } + } + + return $conflicts; + } + + /** + * Parse Android Gradle dependencies into artifact => version map. + */ + protected function parseAndroidDeps(Plugin $plugin): array + { + $deps = []; + foreach ($plugin->getAndroidDependencies() as $type => $libraries) { + foreach ($libraries as $library) { + $parts = explode(':', $library); + if (count($parts) >= 3) { + $deps[$parts[0].':'.$parts[1]] = $parts[2]; + } + } + } + + return $deps; + } + + /** + * Parse CocoaPods dependencies into name => version map. + */ + protected function parsePodDeps(Plugin $plugin): array + { + $deps = []; + foreach ($plugin->getIosDependencies()['pods'] ?? [] as $pod) { + if (isset($pod['name'])) { + $deps[$pod['name']] = $pod['version'] ?? null; + } + } + + return $deps; + } + + /** + * Parse Swift Package dependencies into normalizedUrl => version map. + */ + protected function parseSwiftPackageDeps(Plugin $plugin): array + { + $deps = []; + foreach ($plugin->getIosDependencies()['swift_packages'] ?? [] as $pkg) { + if (isset($pkg['url'])) { + $normalizedUrl = rtrim(preg_replace('/\.git$/', '', $pkg['url']), '/'); + $deps[$normalizedUrl] = $pkg['version'] ?? null; + } + } + + return $deps; + } } diff --git a/src/Exceptions/PluginConflictException.php b/src/Exceptions/PluginConflictException.php index b5cf768..891ff70 100644 --- a/src/Exceptions/PluginConflictException.php +++ b/src/Exceptions/PluginConflictException.php @@ -15,11 +15,13 @@ public function __construct(array $conflicts) $messages = []; foreach ($conflicts as $conflict) { $plugins = implode(' and ', $conflict['plugins']); - if ($conflict['type'] === 'namespace') { - $messages[] = "Namespace '{$conflict['value']}' is used by both {$plugins}"; - } else { - $messages[] = "Bridge function '{$conflict['value']}' is registered by both {$plugins}"; - } + $messages[] = match ($conflict['type']) { + 'namespace' => "Namespace '{$conflict['value']}' is used by both {$plugins}", + 'android_dependency' => "Android dependency '{$conflict['value']}' has version conflicts between {$plugins}", + 'ios_pod_dependency' => "CocoaPods dependency '{$conflict['value']}' has version conflicts between {$plugins}", + 'ios_swift_package_dependency' => "Swift Package '{$conflict['value']}' has version conflicts between {$plugins}", + default => "Bridge function '{$conflict['value']}' is registered by both {$plugins}", + }; } parent::__construct( diff --git a/src/Plugins/PluginRegistry.php b/src/Plugins/PluginRegistry.php index cf7c9d6..55ddc2e 100644 --- a/src/Plugins/PluginRegistry.php +++ b/src/Plugins/PluginRegistry.php @@ -214,6 +214,110 @@ public function detectConflicts(): array } } + return array_merge($conflicts, $this->detectDependencyConflicts($plugins)); + } + + /** + * Detect native dependency version conflicts between plugins. + * + * @return array}> + */ + protected function detectDependencyConflicts(Collection $plugins): array + { + $conflicts = []; + + // Collect all dependencies grouped by artifact identifier + $androidDeps = []; // keyed by "group:artifact" + $podDeps = []; // keyed by pod name + $swiftPkgDeps = []; // keyed by normalized URL + + foreach ($plugins as $plugin) { + // Android Gradle dependencies + foreach ($plugin->getAndroidDependencies() as $type => $libraries) { + foreach ($libraries as $library) { + $parts = explode(':', $library); + if (count($parts) >= 3) { + $artifact = $parts[0].':'.$parts[1]; + $version = $parts[2]; + $androidDeps[$artifact][] = [ + 'version' => $version, + 'plugin' => $plugin->name, + ]; + } + } + } + + // iOS dependencies + $iosDeps = $plugin->getIosDependencies(); + + // CocoaPods + foreach ($iosDeps['pods'] ?? [] as $pod) { + if (! isset($pod['name'])) { + continue; + } + $podDeps[$pod['name']][] = [ + 'version' => $pod['version'] ?? null, + 'plugin' => $plugin->name, + ]; + } + + // Swift Packages + foreach ($iosDeps['swift_packages'] ?? [] as $pkg) { + if (! isset($pkg['url'])) { + continue; + } + $normalizedUrl = rtrim(preg_replace('/\.git$/', '', $pkg['url']), '/'); + $swiftPkgDeps[$normalizedUrl][] = [ + 'version' => $pkg['version'] ?? null, + 'plugin' => $plugin->name, + ]; + } + } + + // Check for Android dependency version conflicts + foreach ($androidDeps as $artifact => $entries) { + $versions = array_unique(array_column($entries, 'version')); + if (count($versions) > 1) { + $pluginNames = array_unique(array_column($entries, 'plugin')); + $versionList = implode(' vs ', $versions); + $conflicts[] = [ + 'type' => 'android_dependency', + 'value' => "{$artifact} ({$versionList})", + 'plugins' => array_values($pluginNames), + ]; + } + } + + // Check for CocoaPods version conflicts + foreach ($podDeps as $podName => $entries) { + $versioned = array_filter($entries, fn ($e) => $e['version'] !== null); + $versions = array_unique(array_column($versioned, 'version')); + if (count($versions) > 1) { + $pluginNames = array_unique(array_column($entries, 'plugin')); + $versionList = implode(' vs ', $versions); + $conflicts[] = [ + 'type' => 'ios_pod_dependency', + 'value' => "{$podName} ({$versionList})", + 'plugins' => array_values($pluginNames), + ]; + } + } + + // Check for Swift Package version conflicts + foreach ($swiftPkgDeps as $url => $entries) { + $versioned = array_filter($entries, fn ($e) => $e['version'] !== null); + $versions = array_unique(array_column($versioned, 'version')); + if (count($versions) > 1) { + $pluginNames = array_unique(array_column($entries, 'plugin')); + $versionList = implode(' vs ', $versions); + $conflicts[] = [ + 'type' => 'ios_swift_package_dependency', + 'value' => "{$url} ({$versionList})", + 'plugins' => array_values($pluginNames), + ]; + } + } + return $conflicts; } diff --git a/tests/Feature/Plugins/PluginRegistryTest.php b/tests/Feature/Plugins/PluginRegistryTest.php index fee577d..05a300c 100644 --- a/tests/Feature/Plugins/PluginRegistryTest.php +++ b/tests/Feature/Plugins/PluginRegistryTest.php @@ -473,6 +473,200 @@ public function it_can_be_accessed_via_facade(): void $this->assertInstanceOf(PluginRegistry::class, app(PluginRegistry::class)); } + /** + * @test + * + * Should detect Android dependency version conflicts between plugins. + */ + public function it_detects_android_dependency_version_conflicts(): void + { + $pluginA = $this->createMockPlugin('vendor/plugin-a', [ + 'android' => [ + 'dependencies' => ['implementation' => ['com.google.firebase:firebase-messaging:23.1.0']], + ], + ]); + $pluginB = $this->createMockPlugin('vendor/plugin-b', [ + 'android' => [ + 'dependencies' => ['implementation' => ['com.google.firebase:firebase-messaging:24.0.0']], + ], + ]); + + $this->mockDiscovery + ->shouldReceive('discover') + ->andReturn(collect([$pluginA, $pluginB])); + + $conflicts = $this->registry->detectConflicts(); + + $this->assertNotEmpty($conflicts); + $depConflicts = array_filter($conflicts, fn ($c) => $c['type'] === 'android_dependency'); + $this->assertCount(1, $depConflicts); + + $conflict = array_values($depConflicts)[0]; + $this->assertStringContainsString('com.google.firebase:firebase-messaging', $conflict['value']); + $this->assertContains('vendor/plugin-a', $conflict['plugins']); + $this->assertContains('vendor/plugin-b', $conflict['plugins']); + } + + /** + * @test + * + * Should detect iOS CocoaPods version conflicts between plugins. + */ + public function it_detects_ios_pod_version_conflicts(): void + { + $pluginA = $this->createMockPlugin('vendor/plugin-a', [ + 'ios' => [ + 'dependencies' => [ + 'pods' => [['name' => 'FirebaseMessaging', 'version' => '10.0']], + ], + ], + ]); + $pluginB = $this->createMockPlugin('vendor/plugin-b', [ + 'ios' => [ + 'dependencies' => [ + 'pods' => [['name' => 'FirebaseMessaging', 'version' => '11.0']], + ], + ], + ]); + + $this->mockDiscovery + ->shouldReceive('discover') + ->andReturn(collect([$pluginA, $pluginB])); + + $conflicts = $this->registry->detectConflicts(); + + $depConflicts = array_filter($conflicts, fn ($c) => $c['type'] === 'ios_pod_dependency'); + $this->assertCount(1, $depConflicts); + + $conflict = array_values($depConflicts)[0]; + $this->assertStringContainsString('FirebaseMessaging', $conflict['value']); + } + + /** + * @test + * + * Should detect iOS Swift Package version conflicts between plugins. + */ + public function it_detects_ios_swift_package_version_conflicts(): void + { + $pluginA = $this->createMockPlugin('vendor/plugin-a', [ + 'ios' => [ + 'dependencies' => [ + 'swift_packages' => [ + ['url' => 'https://github.com/example/package.git', 'version' => '1.0.0'], + ], + ], + ], + ]); + $pluginB = $this->createMockPlugin('vendor/plugin-b', [ + 'ios' => [ + 'dependencies' => [ + 'swift_packages' => [ + ['url' => 'https://github.com/example/package', 'version' => '2.0.0'], + ], + ], + ], + ]); + + $this->mockDiscovery + ->shouldReceive('discover') + ->andReturn(collect([$pluginA, $pluginB])); + + $conflicts = $this->registry->detectConflicts(); + + $depConflicts = array_filter($conflicts, fn ($c) => $c['type'] === 'ios_swift_package_dependency'); + $this->assertCount(1, $depConflicts); + + $conflict = array_values($depConflicts)[0]; + $this->assertStringContainsString('github.com/example/package', $conflict['value']); + } + + /** + * @test + * + * Should allow identical dependencies across plugins without conflict. + */ + public function it_allows_identical_dependencies_across_plugins(): void + { + $pluginA = $this->createMockPlugin('vendor/plugin-a', [ + 'android' => [ + 'dependencies' => ['implementation' => ['com.google.firebase:firebase-core:21.0.0']], + ], + 'ios' => [ + 'dependencies' => [ + 'pods' => [['name' => 'FirebaseCore', 'version' => '10.0']], + 'swift_packages' => [ + ['url' => 'https://github.com/example/shared', 'version' => '1.0.0'], + ], + ], + ], + ]); + $pluginB = $this->createMockPlugin('vendor/plugin-b', [ + 'android' => [ + 'dependencies' => ['implementation' => ['com.google.firebase:firebase-core:21.0.0']], + ], + 'ios' => [ + 'dependencies' => [ + 'pods' => [['name' => 'FirebaseCore', 'version' => '10.0']], + 'swift_packages' => [ + ['url' => 'https://github.com/example/shared', 'version' => '1.0.0'], + ], + ], + ], + ]); + + $this->mockDiscovery + ->shouldReceive('discover') + ->andReturn(collect([$pluginA, $pluginB])); + + $conflicts = $this->registry->detectConflicts(); + + $depConflicts = array_filter($conflicts, fn ($c) => in_array($c['type'], [ + 'android_dependency', 'ios_pod_dependency', 'ios_swift_package_dependency', + ])); + $this->assertEmpty($depConflicts); + } + + /** + * @test + * + * Should not conflict when pods have no version constraints. + */ + public function it_ignores_dependencies_without_versions(): void + { + $pluginA = $this->createMockPlugin('vendor/plugin-a', [ + 'ios' => [ + 'dependencies' => [ + 'pods' => [['name' => 'SomePod']], + 'swift_packages' => [ + ['url' => 'https://github.com/example/pkg'], + ], + ], + ], + ]); + $pluginB = $this->createMockPlugin('vendor/plugin-b', [ + 'ios' => [ + 'dependencies' => [ + 'pods' => [['name' => 'SomePod']], + 'swift_packages' => [ + ['url' => 'https://github.com/example/pkg'], + ], + ], + ], + ]); + + $this->mockDiscovery + ->shouldReceive('discover') + ->andReturn(collect([$pluginA, $pluginB])); + + $conflicts = $this->registry->detectConflicts(); + + $depConflicts = array_filter($conflicts, fn ($c) => in_array($c['type'], [ + 'ios_pod_dependency', 'ios_swift_package_dependency', + ])); + $this->assertEmpty($depConflicts); + } + /** * Helper method to create a mock Plugin instance. */