diff --git a/packages/flutterfire_cli/lib/src/commands/update.dart b/packages/flutterfire_cli/lib/src/commands/update.dart index 30be4522..0094e09e 100644 --- a/packages/flutterfire_cli/lib/src/commands/update.dart +++ b/packages/flutterfire_cli/lib/src/commands/update.dart @@ -66,8 +66,7 @@ class UpdateCommand extends FlutterFireCommand { final String name = 'update'; @override - final String description = - 'Update the version of firebase plugins in your pubspec ' + final String description = 'Update the version of firebase plugins in your pubspec ' 'to the latest version and clean your workspace to ensure that everything ' 'works properly.'; @@ -80,6 +79,8 @@ class UpdateCommand extends FlutterFireCommand { commandRequiresFlutterApp(); await updateFlutterFirePackages( + cleanBaseCommand: flutterApp!.cleanBaseCommand, + pubBaseCommand: flutterApp!.pubBaseCommand, flutterAppPath: flutterApp!.package.path, logger: logger, ); @@ -98,6 +99,8 @@ typedef ProcessRunner = Future Function( /// Cleans the workspace and upgrades every FlutterFire plugin the app depends /// on to its latest major version. Future updateFlutterFirePackages({ + String? cleanBaseCommand = 'flutter', + String pubBaseCommand = 'flutter', required String flutterAppPath, required Logger logger, ProcessRunner runProcess = Process.run, @@ -105,9 +108,9 @@ Future updateFlutterFirePackages({ /// `runInShell` is required on Windows: the Flutter CLI is `flutter.bat` /// there, and `Process.run` does not resolve it through `PATHEXT`. /// See https://github.com/dart-lang/sdk/issues/31291 - Future runFlutter(List arguments) { + Future runCommand(String command, List arguments) { return runProcess( - 'flutter', + command, arguments, workingDirectory: flutterAppPath, runInShell: true, @@ -115,13 +118,15 @@ Future updateFlutterFirePackages({ } logger.stdout('Cleaning up current workspace ...'); - // A failed clean (a locked "build" directory, for instance) is not worth - // giving up the update over, but it should not pass unnoticed either. - final cleanResult = await runFlutter(['clean']); - if (cleanResult.exitCode != 0) { - logger.stderr( - FlutterCommandException('flutter clean', cleanResult).toString(), - ); + if (cleanBaseCommand case final command?) { + // A failed clean (a locked "build" directory, for instance) is not worth + // giving up the update over, but it should not pass unnoticed either. + final cleanResult = await runCommand(command, ['clean']); + if (cleanResult.exitCode != 0) { + logger.stderr( + FlutterCommandException('flutter clean', cleanResult).toString(), + ); + } } final pubspecLock = File(path.join(flutterAppPath, 'pubspec.lock')); @@ -141,11 +146,11 @@ Future updateFlutterFirePackages({ for (final package in flutterfirePackages) { // We run each package individually because chaining them // will fail at the first package not in the pubspec. - await runFlutter(['pub', 'upgrade', '--major-versions', package]); + await runCommand(pubBaseCommand, ['pub', 'upgrade', '--major-versions', package]); } - logger.stdout("Running 'flutter pub get'..."); - final pubGetResult = await runFlutter(['pub', 'get']); + logger.stdout("Running '$pubBaseCommand pub get'..."); + final pubGetResult = await runCommand(pubBaseCommand, ['pub', 'get']); if (pubGetResult.exitCode != 0) { throw FlutterCommandException('flutter pub get', pubGetResult); } diff --git a/packages/flutterfire_cli/lib/src/common/package.dart b/packages/flutterfire_cli/lib/src/common/package.dart index a22a6b81..a871c333 100644 --- a/packages/flutterfire_cli/lib/src/common/package.dart +++ b/packages/flutterfire_cli/lib/src/common/package.dart @@ -77,11 +77,10 @@ class Package { /// Returns whether this package is a Flutter app. /// This is determined by ensuring all the following conditions are met: - /// a) the package depends on the Flutter SDK. + /// a) the package depends on the Flutter SDK or is a Flutter compatible package. /// b) the package does not define itself as a Flutter plugin inside pubspec.yaml. - /// c) a lib/main.dart file exists in the package. bool get isFlutterApp { - // Must directly depend on the Flutter SDK. + // Must directly depend on the Flutter SDK or be a Flutter compatible package. if (!isFlutterPackage) return false; // Must not have a Flutter plugin definition in it's pubspec.yaml. @@ -90,7 +89,38 @@ class Package { return true; } + /// Returns whether this package is a runtime package. + /// + /// These are packages that are compatible with Flutter plugins, even + /// if they don't depend on the Flutter SDK directly. + /// + /// This is determined by evaluating [runtimePackageChecks] against this package. + late final bool isRuntimePackage = + runtimePackageChecks.any((check) => check(this)); + + bool get isRuntimeApp { + // Must not be a Flutter package. + if (isFlutterApp) return false; + + // Must be a runtime package. + if (!isRuntimePackage) return false; + + return true; + } + /// Returns whether this package is a Flutter plugin. /// This is determined by whether the pubspec contains a flutter.plugin definition. bool get isFlutterPlugin => pubSpec.flutter?.containsKey('plugin') ?? false; } + +/// A list of checks that determine whether a package is a runtime package. +/// +/// This is used to determine if a package is compatible with Flutter plugins, +/// even if it doesn't depend on the Flutter SDK directly. +final List runtimePackageChecks = [ + // The Jaspr framework supports flutter-like development and is compatible + // with Flutter plugins, so we check for any package that lists Jaspr as a + // dependency. + (pkg) => pkg.dependencies.contains('jaspr'), + // Additional checks may be added here. +]; diff --git a/packages/flutterfire_cli/lib/src/flutter_app.dart b/packages/flutterfire_cli/lib/src/flutter_app.dart index 611cc904..b5aee665 100644 --- a/packages/flutterfire_cli/lib/src/flutter_app.dart +++ b/packages/flutterfire_cli/lib/src/flutter_app.dart @@ -22,7 +22,7 @@ import 'common/platform.dart'; import 'common/strings.dart'; import 'common/utils.dart'; -class FlutterApp { +abstract class FlutterApp { FlutterApp({ required this.package, }); @@ -37,20 +37,103 @@ class FlutterApp { throw FlutterAppRequiredException(); } final package = await Package.load(appDirectory); - if (!package.isFlutterApp) { - return null; + if (package.isFlutterApp) { + return FlutterProjectApp(package: package); + } + if (package.isRuntimeApp) { + return FlutterRuntimeApp(package: package); } - return FlutterApp( - package: package, + return null; + } + + String? get iosBundleId; + String? get macosBundleId; + String? get androidApplicationId; + + /// Returns whether the package depends on the given package. + bool dependsOnPackage(String packageName) { + return package.dependencies.contains(packageName) || + package.devDependencies.contains(packageName); + } + + /// Returns whether this Flutter app can run on Android. + bool get android; + + /// Returns the directory where the Android platform specific project exists. + Directory get androidDirectory { + return _platformDirectory(kAndroid); + } + + /// Returns whether this Flutter app can run on Web. + bool get web; + + /// Returns the directory where the Web platform specific project exists. + Directory get webDirectory { + return _platformDirectory(kWeb); + } + + /// Returns whether this Flutter app can run on Windows. + bool get windows; + + /// Returns the directory where the Windows platform specific project exists. + Directory get windowsDirectory { + return _platformDirectory(kWindows); + } + + /// Returns whether this Flutter app can run on MacOS. + bool get macos; + + /// Returns the directory where the macOS platform specific project exists. + Directory get macosDirectory { + return _platformDirectory(kMacos); + } + + /// Returns whether this Flutter app can run on iOS. + bool get ios; + + /// Returns the directory where the iOS platform specific project exists. + Directory get iosDirectory { + return _platformDirectory(kIos); + } + + /// Returns whether this Flutter app can run on Linux. + bool get linux; + + /// Returns the directory where the Linux platform specific project exists. + Directory get linuxDirectory { + return _platformDirectory(kLinux); + } + + Directory _platformDirectory(String platform) { + assert( + platform == kIos || + platform == kAndroid || + platform == kWeb || + platform == kMacos || + platform == kWindows || + platform == kLinux, + ); + return Directory( + '${package.path}${currentPlatform.pathSeparator}$platform', ); } + String? get cleanBaseCommand; + String get pubBaseCommand; +} + +class FlutterProjectApp extends FlutterApp { + FlutterProjectApp({ + required super.package, + }); + // Cached Android package name if available. String? _androidApplicationId; // Cached iOS bundle identifier if available. String? _iosBundleId; + @override String? get iosBundleId { if (!ios) return null; if (_iosBundleId != null) { @@ -62,6 +145,7 @@ class FlutterApp { // Cached macOS bundle identifier if available. String? _macosBundleId; + @override String? get macosBundleId { if (!macos) return null; if (_macosBundleId != null) { @@ -120,6 +204,7 @@ class FlutterApp { /// The Android Application (or Package Name) for this Flutter /// application, or null if one could not be detected or the app /// does not target Android as a supported platform. + @override String? get androidApplicationId { if (!android) return null; if (_androidApplicationId != null) { @@ -186,93 +271,86 @@ class FlutterApp { return _androidApplicationId = applicationId; } - /// Returns whether the package depends on the given package. - bool dependsOnPackage(String packageName) { - return package.dependencies.contains(packageName) || - package.devDependencies.contains(packageName); - } - - /// Returns whether this Flutter app can run on Android. + @override bool get android { if (!package.isFlutterApp) return false; return _supportsPlatform(kAndroid); } - /// Returns the directory where the Android platform specific project exists. - Directory get androidDirectory { - return _platformDirectory(kAndroid); - } - - /// Returns whether this Flutter app can run on Web. + @override bool get web { if (!package.isFlutterApp) return false; return _supportsPlatform(kWeb); } - /// Returns the directory where the Web platform specific project exists. - Directory get webDirectory { - return _platformDirectory(kWeb); - } - - /// Returns whether this Flutter app can run on Windows. + @override bool get windows { if (!package.isFlutterApp) return false; return _supportsPlatform(kWindows); } - /// Returns the directory where the Windows platform specific project exists. - Directory get windowsDirectory { - return _platformDirectory(kWindows); - } - - /// Returns whether this Flutter app can run on MacOS. + @override bool get macos { if (!package.isFlutterApp) return false; return _supportsPlatform(kMacos); } - /// Returns the directory where the macOS platform specific project exists. - Directory get macosDirectory { - return _platformDirectory(kMacos); - } - - /// Returns whether this Flutter app can run on iOS. + @override bool get ios { if (!package.isFlutterApp) return false; return _supportsPlatform(kIos); } - /// Returns the directory where the iOS platform specific project exists. - Directory get iosDirectory { - return _platformDirectory(kIos); - } - - /// Returns whether this Flutter app can run on Linux. + @override bool get linux { if (!package.isFlutterApp) return false; return _supportsPlatform(kLinux); } - /// Returns the directory where the Linux platform specific project exists. - Directory get linuxDirectory { - return _platformDirectory(kLinux); - } - - Directory _platformDirectory(String platform) { - assert( - platform == kIos || - platform == kAndroid || - platform == kWeb || - platform == kMacos || - platform == kWindows || - platform == kLinux, - ); - return Directory( - '${package.path}${currentPlatform.pathSeparator}$platform', - ); - } - bool _supportsPlatform(String platform) { return _platformDirectory(platform).existsSync(); } + + @override + String? get cleanBaseCommand => 'flutter'; + + @override + String get pubBaseCommand => 'flutter'; +} + +class FlutterRuntimeApp extends FlutterApp { + FlutterRuntimeApp({required super.package}); + + @override + bool get android => false; + + @override + String? get androidApplicationId => null; + + @override + bool get ios => false; + + @override + String? get iosBundleId => null; + + @override + bool get linux => false; + + @override + bool get macos => false; + + @override + String? get macosBundleId => null; + + @override + bool get web => true; + + @override + bool get windows => false; + + @override + String? get cleanBaseCommand => null; + + @override + String get pubBaseCommand => 'dart'; } diff --git a/packages/flutterfire_cli/test/unit_test.dart b/packages/flutterfire_cli/test/unit_test.dart index 43dfa71f..532d9a2d 100644 --- a/packages/flutterfire_cli/test/unit_test.dart +++ b/packages/flutterfire_cli/test/unit_test.dart @@ -1,8 +1,10 @@ import 'dart:convert'; import 'package:flutterfire_cli/src/commands/config.dart'; +import 'package:flutterfire_cli/src/common/package.dart'; import 'package:flutterfire_cli/src/common/strings.dart'; import 'package:flutterfire_cli/src/common/utils.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/test.dart'; void main() { @@ -284,4 +286,110 @@ void main() { expect(displayNameError('Acme (EU) - v2'), isNull); }); }); + + group('Package class', () { + test('isFlutterPackage returns true if depends on flutter or flutter_localizations', () { + final package1 = Package( + path: '', + pubSpec: Pubspec.parse(''' +name: test_pkg +dependencies: + flutter: + sdk: flutter +'''), + ); + expect(package1.isFlutterPackage, isTrue); + + final package2 = Package( + path: '', + pubSpec: Pubspec.parse(''' +name: test_pkg +dependencies: + flutter_localizations: + sdk: flutter +'''), + ); + expect(package2.isFlutterPackage, isTrue); + + final package3 = Package( + path: '', + pubSpec: Pubspec.parse(''' +name: test_pkg +dependencies: + http: ^0.13.0 +'''), + ); + expect(package3.isFlutterPackage, isFalse); + }); + + test('isRuntimePackage returns true if depends on jaspr', () { + final package1 = Package( + path: '', + pubSpec: Pubspec.parse(''' +name: test_pkg +dependencies: + jaspr: ^0.23.0 +'''), + ); + expect(package1.isRuntimePackage, isTrue); + + final package2 = Package( + path: '', + pubSpec: Pubspec.parse(''' +name: test_pkg +dependencies: + http: ^0.13.0 +'''), + ); + expect(package2.isRuntimePackage, isFalse); + }); + + test('isFlutterApp returns correctly based on dependencies and plugin definition', () { + final flutterApp = Package( + path: '', + pubSpec: Pubspec.parse(''' +name: test_pkg +dependencies: + flutter: + sdk: flutter +'''), + ); + expect(flutterApp.isFlutterApp, isTrue); + + final jasprApp = Package( + path: '', + pubSpec: Pubspec.parse(''' +name: test_pkg +dependencies: + jaspr: ^0.1.0 +'''), + ); + expect(jasprApp.isFlutterApp, isFalse); + + final dartApp = Package( + path: '', + pubSpec: Pubspec.parse(''' +name: test_pkg +dependencies: + http: ^0.13.0 +'''), + ); + expect(dartApp.isFlutterApp, isFalse); + + final flutterPlugin = Package( + path: '', + pubSpec: Pubspec.parse(''' +name: test_pkg +dependencies: + flutter: + sdk: flutter +flutter: + plugin: + platforms: + android: +'''), + ); + expect(flutterPlugin.isFlutterApp, isFalse); + }); + }); }