Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/flutterfire_cli/lib/src/commands/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -42,6 +43,25 @@ abstract class FlutterFireCommand extends Command<void> {
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');
}
Comment on lines +51 to +63
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The resolveFirebaseJsonPath method uses the null-check operator (!) on flutterApp multiple times. While most commands in this CLI require a Flutter app, FlutterFireCommand allows flutterApp to be null. If this method is called when flutterApp is null, it will throw a runtime error. Consider adding a defensive check or using a fallback like Directory.current.path to determine the project root.

Suggested change
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');
}
String resolveFirebaseJsonPath(String? customPath) {
final projectRoot = flutterApp?.package.path ?? Directory.current.path;
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(projectRoot, 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(projectRoot, 'firebase.json');
}


void setupDefaultFirebaseCliOptions() {
argParser.addOption(
'project',
Expand Down
20 changes: 13 additions & 7 deletions packages/flutterfire_cli/lib/src/commands/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -345,6 +350,10 @@ class ConfigCommand extends FlutterFireCommand {
return argResults!['out'] as String;
}

String get firebaseJsonPath {
return resolveFirebaseJsonPath(argResults![kFirebaseOutFlag] as String?);
}
Comment thread
kevmoo marked this conversation as resolved.

bool get overwriteFirebaseOptions {
if (argResults!['overwrite-firebase-options'] == null) {
return false;
Expand Down Expand Up @@ -536,8 +545,6 @@ class ConfigCommand extends FlutterFireCommand {
}

Future<bool> checkIfUserRequiresReconfigure() async {
final firebaseJsonPath =
path.join(flutterApp!.package.path, 'firebase.json');
final file = File(firebaseJsonPath);

if (file.existsSync()) {
Expand All @@ -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;
Expand Down Expand Up @@ -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,
);
}

Expand Down
23 changes: 16 additions & 7 deletions packages/flutterfire_cli/lib/src/commands/reconfigure.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,27 @@ 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',
hide: true,
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.',
);
Comment on lines +71 to +75
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The help text for the firebase-out flag in the Reconfigure command is slightly inconsistent with the one in ConfigCommand. In ConfigCommand, it mentions that the file will be "generated or updated", whereas here it is just "The path to the firebase.json file.". For better user experience, consider using a consistent and descriptive help message across all commands.

Suggested change
argParser.addOption(
kFirebaseOutFlag,
valueHelp: 'filePath',
help: 'The path to the `firebase.json` file.',
);
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`.';
Expand Down Expand Up @@ -378,12 +387,12 @@ class Reconfigure extends FlutterFireCommand {
Future<void> 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(
Expand Down
1 change: 1 addition & 0 deletions packages/flutterfire_cli/lib/src/common/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
48 changes: 48 additions & 0 deletions packages/flutterfire_cli/test/configure_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, dynamic>;

expect(decodedFirebaseJson[kFlutter], isNotNull);
});
}
73 changes: 73 additions & 0 deletions packages/flutterfire_cli/test/reconfigure_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
),
);
}
Loading