From 63b367a1b2d4ffdf5cc86d559bad8e460e219d08 Mon Sep 17 00:00:00 2001 From: kevmoo Date: Thu, 16 Apr 2026 12:50:26 -0700 Subject: [PATCH 1/3] feat(cli): add --firebase-out flag for custom firebase.json path --- .../lib/src/commands/config.dart | 21 +++++--- .../lib/src/commands/reconfigure.dart | 21 ++++++-- .../flutterfire_cli/lib/src/common/utils.dart | 1 + .../flutterfire_cli/test/configure_test.dart | 48 +++++++++++++++++++ 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/packages/flutterfire_cli/lib/src/commands/config.dart b/packages/flutterfire_cli/lib/src/commands/config.dart index 5b2b5d19..e3458e12 100644 --- a/packages/flutterfire_cli/lib/src/commands/config.dart +++ b/packages/flutterfire_cli/lib/src/commands/config.dart @@ -188,6 +188,12 @@ class ConfigCommand extends FlutterFireCommand { 'Where to write the `google-services.json` file to be written for android platform. Useful for different flavors', ); + argParser.addOption( + kFirebaseOutFlag, + valueHelp: 'filePath', + help: 'The output file path of the `firebase.json` file that will be generated or updated.', + ); + argParser.addFlag( kOverwriteFirebaseOptionsFlag, abbr: 'f', @@ -345,6 +351,12 @@ class ConfigCommand extends FlutterFireCommand { return argResults!['out'] as String; } + String get firebaseJsonPath { + final customPath = argResults![kFirebaseOutFlag] as String?; + if (customPath != null) return customPath; + return path.join(flutterApp!.package.path, 'firebase.json'); + } + bool get overwriteFirebaseOptions { if (argResults!['overwrite-firebase-options'] == null) { return false; @@ -536,8 +548,6 @@ class ConfigCommand extends FlutterFireCommand { } Future checkIfUserRequiresReconfigure() async { - final firebaseJsonPath = - path.join(flutterApp!.package.path, 'firebase.json'); final file = File(firebaseJsonPath); if (file.existsSync()) { @@ -550,7 +560,7 @@ class ConfigCommand extends FlutterFireCommand { ); if (reuseFirebaseJsonValues) { - final reconfigure = Reconfigure(flutterApp, token: testAccessToken); + final reconfigure = Reconfigure(flutterApp, token: testAccessToken, firebaseJsonPath: firebaseJsonPath); reconfigure.logger = logger; await reconfigure.run(); return true; @@ -705,12 +715,11 @@ class ConfigCommand extends FlutterFireCommand { firebaseJsonWrites.add(firebaseJsonWrite); } - // 5. Writes for "firebase.json" file in root of project + // 5. Writes for "firebase.json" file if (firebaseJsonWrites.isNotEmpty) { await writeToFirebaseJson( listOfWrites: firebaseJsonWrites, - firebaseJsonPath: - path.join(flutterApp!.package.path, 'firebase.json'), + firebaseJsonPath: firebaseJsonPath, ); } diff --git a/packages/flutterfire_cli/lib/src/commands/reconfigure.dart b/packages/flutterfire_cli/lib/src/commands/reconfigure.dart index b86ad313..caa0d19e 100644 --- a/packages/flutterfire_cli/lib/src/commands/reconfigure.dart +++ b/packages/flutterfire_cli/lib/src/commands/reconfigure.dart @@ -56,9 +56,10 @@ class ConfigFileWrite { } class Reconfigure extends FlutterFireCommand { - Reconfigure(FlutterApp? flutterApp, {String? token}) : super(flutterApp) { + Reconfigure(FlutterApp? flutterApp, {String? token, String? firebaseJsonPath}) : super(flutterApp) { setupDefaultFirebaseCliOptions(); _accessToken = token; + _firebaseJsonPath = firebaseJsonPath; argParser.addOption( 'ci-access-token', valueHelp: 'ciAccessToken', @@ -66,8 +67,15 @@ class Reconfigure extends FlutterFireCommand { help: 'Set the access token for making Firebase API requests. Required for CI environment.', ); + argParser.addOption( + kFirebaseOutFlag, + valueHelp: 'filePath', + help: 'The path to the `firebase.json` file.', + ); } + String? _firebaseJsonPath; + @override final String description = 'Updates the configurations for all build variants included in the "firebase.json" added by running `flutterfire configure`.'; @@ -378,11 +386,14 @@ class Reconfigure extends FlutterFireCommand { Future run() async { try { commandRequiresFlutterApp(); + final customPath = argResults != null ? argResults![kFirebaseOutFlag] as String? : null; final firebaseJson = File( - path.join( - flutterApp!.package.path, - 'firebase.json', - ), + _firebaseJsonPath ?? + customPath ?? + path.join( + flutterApp!.package.path, + 'firebase.json', + ), ); if (!firebaseJson.existsSync()) { diff --git a/packages/flutterfire_cli/lib/src/common/utils.dart b/packages/flutterfire_cli/lib/src/common/utils.dart index b99caf47..7d690d77 100644 --- a/packages/flutterfire_cli/lib/src/common/utils.dart +++ b/packages/flutterfire_cli/lib/src/common/utils.dart @@ -79,6 +79,7 @@ const String kMacosTargetFlag = 'macos-target'; const String kIosOutFlag = 'ios-out'; const String kMacosOutFlag = 'macos-out'; const String kAndroidOutFlag = 'android-out'; +const String kFirebaseOutFlag = 'firebase-out'; const String kOverwriteFirebaseOptionsFlag = 'overwrite-firebase-options'; const String kTestAccessTokenFlag = 'test-access-token'; diff --git a/packages/flutterfire_cli/test/configure_test.dart b/packages/flutterfire_cli/test/configure_test.dart index a06d3aaf..619a6b1b 100644 --- a/packages/flutterfire_cli/test/configure_test.dart +++ b/packages/flutterfire_cli/test/configure_test.dart @@ -1740,4 +1740,52 @@ void main() { Duration(minutes: 3), ), ); + + test('flutterfire configure: --firebase-out flag should dictate where firebase.json is written', () async { + // Install flutterfire_cli from local path + final installDevDependency = Process.runSync( + 'flutter', + [ + 'pub', + 'add', + '--dev', + 'flutterfire_cli', + '--path=${Directory.current.path}', + ], + workingDirectory: projectPath, + ); + + if (installDevDependency.exitCode != 0) { + fail(installDevDependency.stderr as String); + } + + const customFirebaseJsonPath = 'custom/firebase.json'; + final result = Process.runSync( + 'dart', + [ + 'run', + 'flutterfire_cli:flutterfire', + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=android', + '--firebase-out=$customFirebaseJsonPath', + ], + workingDirectory: projectPath, + runInShell: true, + ); + + if (result.exitCode != 0) { + fail(result.stderr as String); + } + + // check custom "firebase.json" was created and has correct content + final firebaseJsonFile = p.join(projectPath!, 'custom', 'firebase.json'); + expect(File(firebaseJsonFile).existsSync(), true); + + final firebaseJsonFileContent = await File(firebaseJsonFile).readAsString(); + final decodedFirebaseJson = jsonDecode(firebaseJsonFileContent) as Map; + + expect(decodedFirebaseJson[kFlutter], isNotNull); + }); } From 9baf23079b1ce7c7bfb4c48daec885bfc630273c Mon Sep 17 00:00:00 2001 From: kevmoo Date: Thu, 16 Apr 2026 13:12:10 -0700 Subject: [PATCH 2/3] review nits --- .../lib/src/commands/base.dart | 20 +++++ .../lib/src/commands/config.dart | 5 +- .../lib/src/commands/reconfigure.dart | 18 ++--- .../test/reconfigure_test.dart | 73 +++++++++++++++++++ 4 files changed, 102 insertions(+), 14 deletions(-) diff --git a/packages/flutterfire_cli/lib/src/commands/base.dart b/packages/flutterfire_cli/lib/src/commands/base.dart index fcb1fd1e..689e8ac9 100644 --- a/packages/flutterfire_cli/lib/src/commands/base.dart +++ b/packages/flutterfire_cli/lib/src/commands/base.dart @@ -20,6 +20,7 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:cli_util/cli_logging.dart'; +import 'package:path/path.dart' as path; import '../common/strings.dart'; import '../common/utils.dart'; @@ -42,6 +43,25 @@ abstract class FlutterFireCommand extends Command { return argResults!['account'] as String?; } + /// Resolves the path to the `firebase.json` file. + /// + /// If a [customPath] is provided, it is returned directly if it is absolute. + /// If it is relative, it is resolved relative to the project root (`FlutterApp.package.path`). + /// If no [customPath] is provided, it defaults to `firebase.json` in the project root. + String resolveFirebaseJsonPath(String? customPath) { + if (customPath != null) { + // If a custom path was provided, check if it is relative. + if (path.isRelative(customPath)) { + // Resolve relative paths relative to the project root to ensure consistent behavior. + return path.join(flutterApp!.package.path, customPath); + } + // Use absolute paths as provided. + return customPath; + } + // Default to the standard firebase.json in the project root if no custom path is specified. + return path.join(flutterApp!.package.path, 'firebase.json'); + } + void setupDefaultFirebaseCliOptions() { argParser.addOption( 'project', diff --git a/packages/flutterfire_cli/lib/src/commands/config.dart b/packages/flutterfire_cli/lib/src/commands/config.dart index e3458e12..c7094f78 100644 --- a/packages/flutterfire_cli/lib/src/commands/config.dart +++ b/packages/flutterfire_cli/lib/src/commands/config.dart @@ -18,7 +18,6 @@ import 'dart:io'; import 'package:ansi_styles/ansi_styles.dart'; -import 'package:path/path.dart' as path; import '../common/global.dart'; import '../common/inputs.dart'; @@ -352,9 +351,7 @@ class ConfigCommand extends FlutterFireCommand { } String get firebaseJsonPath { - final customPath = argResults![kFirebaseOutFlag] as String?; - if (customPath != null) return customPath; - return path.join(flutterApp!.package.path, 'firebase.json'); + return resolveFirebaseJsonPath(argResults![kFirebaseOutFlag] as String?); } bool get overwriteFirebaseOptions { diff --git a/packages/flutterfire_cli/lib/src/commands/reconfigure.dart b/packages/flutterfire_cli/lib/src/commands/reconfigure.dart index caa0d19e..a9107a3a 100644 --- a/packages/flutterfire_cli/lib/src/commands/reconfigure.dart +++ b/packages/flutterfire_cli/lib/src/commands/reconfigure.dart @@ -56,7 +56,8 @@ class ConfigFileWrite { } class Reconfigure extends FlutterFireCommand { - Reconfigure(FlutterApp? flutterApp, {String? token, String? firebaseJsonPath}) : super(flutterApp) { + Reconfigure(FlutterApp? flutterApp, {String? token, String? firebaseJsonPath}) + : super(flutterApp) { setupDefaultFirebaseCliOptions(); _accessToken = token; _firebaseJsonPath = firebaseJsonPath; @@ -386,15 +387,12 @@ class Reconfigure extends FlutterFireCommand { Future run() async { try { commandRequiresFlutterApp(); - final customPath = argResults != null ? argResults![kFirebaseOutFlag] as String? : null; - final firebaseJson = File( - _firebaseJsonPath ?? - customPath ?? - path.join( - flutterApp!.package.path, - 'firebase.json', - ), - ); + final customPath = + argResults != null ? argResults![kFirebaseOutFlag] as String? : null; + // Determine the raw path, prioritizing the programmatic path passed from the configure command. + final rawPath = _firebaseJsonPath ?? customPath; + + final firebaseJson = File(resolveFirebaseJsonPath(rawPath)); if (!firebaseJson.existsSync()) { throw Exception( diff --git a/packages/flutterfire_cli/test/reconfigure_test.dart b/packages/flutterfire_cli/test/reconfigure_test.dart index 551cd1f9..2b2299d3 100644 --- a/packages/flutterfire_cli/test/reconfigure_test.dart +++ b/packages/flutterfire_cli/test/reconfigure_test.dart @@ -393,4 +393,77 @@ void main() { Duration(minutes: 2), ), ); + + test( + 'flutterfire reconfigure: should use custom firebase.json path specified by --firebase-out', + () async { + const customFirebaseJsonPath = 'custom/firebase.json'; + final scriptPath = p.join(Directory.current.path, 'bin', 'flutterfire.dart'); + + // 1. Run "flutterfire configure" with custom output path + final result = Process.runSync( + 'dart', + [ + scriptPath, + 'configure', + '--yes', + '--platforms=android', + '--project=$firebaseProjectId', + '--firebase-out=$customFirebaseJsonPath', + ], + workingDirectory: projectPath, + runInShell: true, + ); + + if (result.exitCode != 0) { + fail(result.stderr); + } + + // Verify custom file exists + final customFile = File(p.join(projectPath!, 'custom', 'firebase.json')); + expect(customFile.existsSync(), true); + + // Delete generated files to force reconfigure to do work + final firebaseOptionsPath = p.join(projectPath!, 'lib', 'firebase_options.dart'); + final androidServiceFilePath = p.join( + projectPath!, + 'android', + 'app', + androidServiceFileName, + ); + + if (File(firebaseOptionsPath).existsSync()) { + await File(firebaseOptionsPath).delete(); + } + if (File(androidServiceFilePath).existsSync()) { + await File(androidServiceFilePath).delete(); + } + + final accessToken = await generateAccessTokenCI(); + + // 2. Run "flutterfire reconfigure" pointing to the custom path + final result2 = Process.runSync( + 'dart', + [ + scriptPath, + 'reconfigure', + '--firebase-out=$customFirebaseJsonPath', + if (accessToken != null) '--ci-access-token=$accessToken', + ], + workingDirectory: projectPath, + runInShell: true, + ); + + if (result2.exitCode != 0) { + fail(result2.stderr); + } + + // Check the files have been recreated + expect(File(firebaseOptionsPath).existsSync(), true); + expect(File(androidServiceFilePath).existsSync(), true); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); } From 2e0a814e4503b4afbca8c16b5dac9071edd8ca7a Mon Sep 17 00:00:00 2001 From: Russell Wheatley Date: Thu, 23 Apr 2026 13:36:50 +0100 Subject: [PATCH 3/3] Update packages/flutterfire_cli/lib/src/commands/reconfigure.dart Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/flutterfire_cli/lib/src/commands/reconfigure.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutterfire_cli/lib/src/commands/reconfigure.dart b/packages/flutterfire_cli/lib/src/commands/reconfigure.dart index a9107a3a..4d123e53 100644 --- a/packages/flutterfire_cli/lib/src/commands/reconfigure.dart +++ b/packages/flutterfire_cli/lib/src/commands/reconfigure.dart @@ -71,7 +71,7 @@ class Reconfigure extends FlutterFireCommand { argParser.addOption( kFirebaseOutFlag, valueHelp: 'filePath', - help: 'The path to the `firebase.json` file.', + help: 'The path to the `firebase.json` file that will be used for reconfiguration.', ); }