Skip to content

Commit 6e829a7

Browse files
committed
fix(cli): ensure generated firebase_options.dart is perfectly formatted
- Refine string generation in FirebaseDartConfigurationWrite to match dart format output by construction. - Fix extra newline before closing brace of DefaultFirebaseOptions. - Fix file update logic to remove trailing empty lines and insert content at the correct position. - Ensure the generated file ends with a trailing newline. - Add a golden test for web-only configuration and a test for update logic cleanup. - Refactor tests to reduce code duplication using a helper.
1 parent 63b367a commit 6e829a7

File tree

3 files changed

+162
-40
lines changed

3 files changed

+162
-40
lines changed

packages/flutterfire_cli/lib/src/firebase/firebase_dart_configuration_write.dart

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,14 @@ class FirebaseDartConfigurationWrite {
111111
}
112112

113113
fileConfigurationLines.removeRange(startIndex, endIndex + 1);
114+
while (startIndex < fileConfigurationLines.length &&
115+
fileConfigurationLines[startIndex].trim().isEmpty) {
116+
fileConfigurationLines.removeAt(startIndex);
117+
}
114118

115119
// Insert the new platform configuration
116120
fileConfigurationLines.insertAll(
117-
startIndex - 1,
121+
startIndex,
118122
_buildFirebaseOptions(
119123
options,
120124
platform.toLowerCase(),
@@ -177,14 +181,16 @@ class FirebaseDartConfigurationWrite {
177181
}
178182
}
179183

180-
return formatList(fileConfigurationLines).join('\n');
184+
final result = formatList(fileConfigurationLines).join('\n');
185+
return result.endsWith('\n') ? result : '$result\n';
181186
}
182187

183188
String _buildConfigurationFile() {
184189
_stringBuffer.clear();
185190
_writeHeader();
186191
_writeClass();
187-
return formatList(_stringBuffer.toString().split('\n')).join('\n');
192+
final result = formatList(_stringBuffer.toString().split('\n')).join('\n');
193+
return result.endsWith('\n') ? result : '$result\n';
188194
}
189195

190196
// ensure only one empty line between each static property
@@ -208,7 +214,6 @@ class FirebaseDartConfigurationWrite {
208214
.where((entry) => entry.value != null)
209215
.map((entry) => " ${entry.key}: '${entry.value}',"),
210216
' );', // FirebaseOptions
211-
'',
212217
];
213218
}
214219

@@ -281,14 +286,8 @@ class FirebaseDartConfigurationWrite {
281286
}
282287

283288
void _writeClass() {
284-
_stringBuffer.writeAll(
285-
<String>[
286-
'class DefaultFirebaseOptions {',
287-
' static FirebaseOptions get currentPlatform {',
288-
'',
289-
],
290-
'\n',
291-
);
289+
_stringBuffer.writeln('class DefaultFirebaseOptions {');
290+
_stringBuffer.writeln(' static FirebaseOptions get currentPlatform {');
292291
_writeCurrentPlatformWeb();
293292
_stringBuffer.writeln(' switch (defaultTargetPlatform) {');
294293
_writeCurrentPlatformSwitchAndroid();
@@ -321,6 +320,7 @@ class FirebaseDartConfigurationWrite {
321320
_buildFirebaseOptions(options, platform),
322321
'\n',
323322
);
323+
_stringBuffer.writeln();
324324
}
325325

326326
void _writeThrowUnsupportedForPlatform(String platform, String indentation) {

packages/flutterfire_cli/test/firebase_dart_configuration_write_test.dart

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,44 @@ import 'package:test/test.dart';
77

88
void main() {
99
group('FirebaseDartConfigurationWrite', () {
10-
// Regression test for
11-
// https://github.com/invertase/flutterfire_cli/issues/422
12-
test(
13-
'replaces placeholder firebase_options.dart with generated output',
14-
() {
10+
void testWithWriter({
11+
required String testName,
12+
String? initialContent,
13+
FirebaseOptions? webOptions,
14+
FirebaseOptions? androidOptions,
15+
required void Function(String content) verify,
16+
}) {
17+
test(testName, () {
1518
final tempDir = Directory.systemTemp.createTempSync();
1619
addTearDown(() => tempDir.deleteSync(recursive: true));
1720

1821
final filePath = p.join(tempDir.path, 'lib', 'firebase_options.dart');
19-
File(filePath)
20-
..createSync(recursive: true)
21-
..writeAsStringSync('''
22+
final file = File(filePath)..createSync(recursive: true);
23+
if (initialContent != null) {
24+
file.writeAsStringSync(initialContent);
25+
}
26+
27+
final writer = FirebaseDartConfigurationWrite(
28+
configurationFilePath: filePath,
29+
flutterAppPath: tempDir.path,
30+
firebaseProjectId: 'test-project-id',
31+
webOptions: webOptions,
32+
androidOptions: androidOptions,
33+
);
34+
35+
writer.write();
36+
37+
final updatedContent = file.readAsStringSync();
38+
verify(updatedContent);
39+
});
40+
}
41+
42+
// Regression test for
43+
// https://github.com/invertase/flutterfire_cli/issues/422
44+
testWithWriter(
45+
testName:
46+
'replaces placeholder firebase_options.dart with generated output',
47+
initialContent: '''
2248
// File normally generated by FlutterFire CLI. This is a stand-in.
2349
// See README.md for details.
2450
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
@@ -32,31 +58,65 @@ class DefaultFirebaseOptions {
3258
);
3359
}
3460
}
35-
''');
36-
37-
final writer = FirebaseDartConfigurationWrite(
38-
configurationFilePath: filePath,
39-
flutterAppPath: tempDir.path,
40-
firebaseProjectId: 'test-project-id',
41-
webOptions: const FirebaseOptions(
42-
optionsSourceContent: '{}',
43-
optionsSourceFileName: 'firebase-config.json',
44-
apiKey: 'api-key',
45-
appId: 'app-id',
46-
messagingSenderId: 'sender-id',
47-
projectId: 'test-project-id',
48-
measurementId: 'measurement-id',
49-
),
50-
);
51-
52-
writer.write();
53-
54-
final updatedContent = File(filePath).readAsStringSync();
61+
''',
62+
webOptions: const FirebaseOptions(
63+
optionsSourceContent: '{}',
64+
optionsSourceFileName: 'firebase-config.json',
65+
apiKey: 'api-key',
66+
appId: 'app-id',
67+
messagingSenderId: 'sender-id',
68+
projectId: 'test-project-id',
69+
measurementId: 'measurement-id',
70+
),
71+
verify: (updatedContent) {
5572
expect(updatedContent, contains('if (kIsWeb) {'));
5673
expect(updatedContent, contains('return web;'));
5774
expect(updatedContent, contains('static const FirebaseOptions web'));
5875
expect(updatedContent, isNot(contains('UnimplementedError')));
5976
},
6077
);
78+
79+
testWithWriter(
80+
testName: 'generates correct output for JUST web (golden test)',
81+
webOptions: const FirebaseOptions(
82+
optionsSourceContent: '{}',
83+
optionsSourceFileName: 'firebase-config.json',
84+
apiKey: 'api-key',
85+
appId: 'app-id',
86+
messagingSenderId: 'sender-id',
87+
projectId: 'test-project-id',
88+
measurementId: 'measurement-id',
89+
),
90+
verify: (updatedContent) {
91+
final goldenFile = File('test/goldens/firebase_options_just_web.dart_');
92+
final expectedContent = goldenFile.readAsStringSync();
93+
expect(updatedContent, equals(expectedContent));
94+
},
95+
);
96+
97+
testWithWriter(
98+
testName:
99+
'cleans up trailing empty lines when updating existing configuration',
100+
initialContent: '''
101+
class DefaultFirebaseOptions {
102+
static const FirebaseOptions web = FirebaseOptions(
103+
apiKey: 'old-api-key',
104+
);
105+
106+
}
107+
''',
108+
webOptions: const FirebaseOptions(
109+
optionsSourceContent: '{}',
110+
optionsSourceFileName: 'firebase-config.json',
111+
apiKey: 'new-api-key',
112+
appId: 'app-id',
113+
messagingSenderId: 'sender-id',
114+
projectId: 'test-project-id',
115+
),
116+
verify: (updatedContent) {
117+
expect(updatedContent, contains(');\n}'));
118+
expect(updatedContent, isNot(contains(');\n\n}')));
119+
},
120+
);
61121
});
62122
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// File generated by FlutterFire CLI.
2+
// ignore_for_file: type=lint
3+
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
4+
import 'package:flutter/foundation.dart'
5+
show defaultTargetPlatform, kIsWeb, TargetPlatform;
6+
7+
/// Default [FirebaseOptions] for use with your Firebase apps.
8+
///
9+
/// Example:
10+
/// ```dart
11+
/// import 'firebase_options.dart';
12+
/// // ...
13+
/// await Firebase.initializeApp(
14+
/// options: DefaultFirebaseOptions.currentPlatform,
15+
/// );
16+
/// ```
17+
class DefaultFirebaseOptions {
18+
static FirebaseOptions get currentPlatform {
19+
if (kIsWeb) {
20+
return web;
21+
}
22+
switch (defaultTargetPlatform) {
23+
case TargetPlatform.android:
24+
throw UnsupportedError(
25+
'DefaultFirebaseOptions have not been configured for android - '
26+
'you can reconfigure this by running the FlutterFire CLI again.',
27+
);
28+
case TargetPlatform.iOS:
29+
throw UnsupportedError(
30+
'DefaultFirebaseOptions have not been configured for ios - '
31+
'you can reconfigure this by running the FlutterFire CLI again.',
32+
);
33+
case TargetPlatform.macOS:
34+
throw UnsupportedError(
35+
'DefaultFirebaseOptions have not been configured for macos - '
36+
'you can reconfigure this by running the FlutterFire CLI again.',
37+
);
38+
case TargetPlatform.windows:
39+
throw UnsupportedError(
40+
'DefaultFirebaseOptions have not been configured for windows - '
41+
'you can reconfigure this by running the FlutterFire CLI again.',
42+
);
43+
case TargetPlatform.linux:
44+
throw UnsupportedError(
45+
'DefaultFirebaseOptions have not been configured for linux - '
46+
'you can reconfigure this by running the FlutterFire CLI again.',
47+
);
48+
default:
49+
throw UnsupportedError(
50+
'DefaultFirebaseOptions are not supported for this platform.',
51+
);
52+
}
53+
}
54+
55+
static const FirebaseOptions web = FirebaseOptions(
56+
apiKey: 'api-key',
57+
appId: 'app-id',
58+
messagingSenderId: 'sender-id',
59+
projectId: 'test-project-id',
60+
measurementId: 'measurement-id',
61+
);
62+
}

0 commit comments

Comments
 (0)