From d6f00070dd6f2b89a7317d648b44dbd8b891c891 Mon Sep 17 00:00:00 2001 From: Josh Burton Date: Tue, 21 Jul 2026 08:42:12 +1200 Subject: [PATCH] fix: keep selected plugins when installing a BoM The install prompt hides plugins that are not available in the selected BoM, but it used indexes from the full plugin list when reading the user's choices. This shifted later selections to different plugins, causing selected plugins to be removed and unselected plugins to be installed. Use the filtered plugin list when reading prompt selections. Also allow install flags alongside a positional BoM version and prevent an install-specific --version option from being handled as a request for the FlutterFire CLI version. --- packages/flutterfire_cli/bin/flutterfire.dart | 10 +++- .../lib/src/commands/install.dart | 44 +++++++++++----- .../test/install_unit_test.dart | 52 +++++++++++++++++++ 3 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 packages/flutterfire_cli/test/install_unit_test.dart diff --git a/packages/flutterfire_cli/bin/flutterfire.dart b/packages/flutterfire_cli/bin/flutterfire.dart index 40248161..3a580fac 100755 --- a/packages/flutterfire_cli/bin/flutterfire.dart +++ b/packages/flutterfire_cli/bin/flutterfire.dart @@ -26,7 +26,7 @@ import 'package:flutterfire_cli/version.g.dart'; import 'package:pub_updater/pub_updater.dart'; Future main(List arguments) async { - if (arguments.contains('--version') || arguments.contains('-v')) { + if (isCliVersionRequest(arguments)) { print(cliVersion); // No version checks on CIs. if (utils.isCI) return; @@ -74,3 +74,11 @@ Future main(List arguments) async { rethrow; } } + +/// Whether the arguments request the FlutterFire CLI version. +/// +/// Only a standalone version flag belongs to the CLI. Subcommands may define +/// their own version option, such as the BoM version accepted by `install`. +bool isCliVersionRequest(List arguments) => + arguments.length == 1 && + (arguments.single == '--version' || arguments.single == '-v'); diff --git a/packages/flutterfire_cli/lib/src/commands/install.dart b/packages/flutterfire_cli/lib/src/commands/install.dart index 1273e94a..8a46c9eb 100644 --- a/packages/flutterfire_cli/lib/src/commands/install.dart +++ b/packages/flutterfire_cli/lib/src/commands/install.dart @@ -19,6 +19,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:ansi_styles/ansi_styles.dart'; +import 'package:args/args.dart'; import 'package:collection/collection.dart'; import '../common/utils.dart'; @@ -85,6 +86,28 @@ enum FlutterFirePlugins { FlutterFirePlugins.values.map((plugin) => plugin.name).toList(); } +/// Maps prompt selection indexes to the exact plugin list shown to the user. +/// +/// The prompt excludes plugins that are unavailable in the selected BoM, so +/// its indexes must not be applied to the unfiltered [FlutterFirePlugins] +/// values. +List pluginsFromSelectionIndexes( + List availablePlugins, + List selectedIndexes, +) => selectedIndexes.map((index) => availablePlugins[index]).toList(); + +/// Returns the BoM version supplied as a positional argument or option. +/// +/// [ArgResults.rest] contains only positional arguments, unlike +/// [ArgResults.arguments], which also contains command options. +String? bomVersionFromArguments(ArgResults arguments) { + if (arguments.rest.length == 1) { + return arguments.rest.single; + } + + return arguments['version'] as String?; +} + class InstallCommand extends FlutterFireCommand { InstallCommand(FlutterApp? flutterApp) : super(flutterApp) { setupDefaultFirebaseCliOptions(); @@ -138,12 +161,15 @@ class InstallCommand extends FlutterFireCommand { return selectedPlugins; } - final selectedPlugins = []; final listAvailablePluginsInVersion = availablePlugins.keys.toList(); - final choices = FlutterFirePlugins.values + // Keep the filtered enum values as well as their labels so prompt indexes + // can be resolved against the same list the user sees. + final availablePluginChoices = FlutterFirePlugins.values .where( (element) => listAvailablePluginsInVersion.contains(element.name), ) + .toList(); + final choices = availablePluginChoices .map((plugin) => plugin.displayName) .toList(); final defaultSelection = List.filled(choices.length, false); @@ -164,10 +190,7 @@ class InstallCommand extends FlutterFireCommand { choices, defaultSelection: defaultSelection, ); - for (final index in selectedChoices) { - selectedPlugins.add(FlutterFirePlugins.values[index]); - } - return selectedPlugins; + return pluginsFromSelectionIndexes(availablePluginChoices, selectedChoices); } Future> _getPluginVersionsFromJSON( @@ -204,13 +227,8 @@ class InstallCommand extends FlutterFireCommand { @override Future run() async { // Get the BoM version number from the arguments - late String bomVersion; - - if (argResults?.arguments.length == 1) { - bomVersion = argResults!.arguments[0]; - } else if (argResults?['version'] != null) { - bomVersion = argResults!['version'] as String; - } else { + final bomVersion = bomVersionFromArguments(argResults!); + if (bomVersion == null) { stderr.writeln( 'Usage for install command: flutterfire install ', ); diff --git a/packages/flutterfire_cli/test/install_unit_test.dart b/packages/flutterfire_cli/test/install_unit_test.dart new file mode 100644 index 00000000..62b24a3a --- /dev/null +++ b/packages/flutterfire_cli/test/install_unit_test.dart @@ -0,0 +1,52 @@ +import 'package:args/args.dart'; +import 'package:flutterfire_cli/src/commands/install.dart'; +import 'package:test/test.dart'; + +import '../bin/flutterfire.dart'; + +void main() { + group('install plugin selection', () { + test('maps prompt indexes against the available plugins', () { + final availablePlugins = [ + for (final plugin in FlutterFirePlugins.values) + if (plugin != FlutterFirePlugins.dynamicLinks) plugin, + ]; + + expect(pluginsFromSelectionIndexes(availablePlugins, [9, 10, 13]), [ + FlutterFirePlugins.inAppMessaging, + FlutterFirePlugins.messaging, + FlutterFirePlugins.remoteConfig, + ]); + }); + }); + + group('install version arguments', () { + final parser = ArgParser() + ..addOption('version') + ..addFlag('only-pubspec-plugins', negatable: false); + + test('accepts a positional version with options', () { + final results = parser.parse(['4.17.1', '--only-pubspec-plugins']); + + expect(bomVersionFromArguments(results), '4.17.1'); + }); + + test('accepts the command version option', () { + final results = parser.parse(['--version', '4.17.1']); + + expect(bomVersionFromArguments(results), '4.17.1'); + }); + + test( + 'only treats a standalone global version flag as a CLI version request', + () { + expect(isCliVersionRequest(['--version']), isTrue); + expect(isCliVersionRequest(['-v']), isTrue); + expect( + isCliVersionRequest(['install', '--version', '4.17.1']), + isFalse, + ); + }, + ); + }); +}