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 5b2b5d19..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'; @@ -188,6 +187,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 +350,10 @@ class ConfigCommand extends FlutterFireCommand { return argResults!['out'] as String; } + String get firebaseJsonPath { + return resolveFirebaseJsonPath(argResults![kFirebaseOutFlag] as String?); + } + bool get overwriteFirebaseOptions { if (argResults!['overwrite-firebase-options'] == null) { return false; @@ -536,8 +545,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 +557,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 +712,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..4d123e53 100644 --- a/packages/flutterfire_cli/lib/src/commands/reconfigure.dart +++ b/packages/flutterfire_cli/lib/src/commands/reconfigure.dart @@ -56,9 +56,11 @@ 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 +68,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 that will be used for reconfiguration.', + ); } + String? _firebaseJsonPath; + @override final String description = 'Updates the configurations for all build variants included in the "firebase.json" added by running `flutterfire configure`.'; @@ -378,12 +387,12 @@ class Reconfigure extends FlutterFireCommand { Future run() async { try { commandRequiresFlutterApp(); - final firebaseJson = File( - 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/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); + }); } 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), + ), + ); }