Add .env file support for option --dart-define-from-file
(#128668)
# Proposal I suggest to make possible to specify .env files to the --dart-define-from-file in addition to the Json format. # Issue Close #128667
This commit is contained in:
parent
35085c394d
commit
168d807734
@ -612,17 +612,17 @@ abstract class FlutterCommand extends Command<void> {
|
|||||||
valueHelp: 'foo=bar',
|
valueHelp: 'foo=bar',
|
||||||
splitCommas: false,
|
splitCommas: false,
|
||||||
);
|
);
|
||||||
useDartDefineConfigJsonFileOption();
|
useDartDefineFromFileOption();
|
||||||
}
|
}
|
||||||
|
|
||||||
void useDartDefineConfigJsonFileOption() {
|
void useDartDefineFromFileOption() {
|
||||||
argParser.addMultiOption(
|
argParser.addMultiOption(
|
||||||
FlutterOptions.kDartDefineFromFileOption,
|
FlutterOptions.kDartDefineFromFileOption,
|
||||||
help: 'The path of a json format file where flutter define a global constant pool. '
|
help:
|
||||||
'Json entry will be available as constants from the String.fromEnvironment, bool.fromEnvironment, '
|
'The path of a .json or .env file containing key-value pairs that will be available as environment variables.\n'
|
||||||
'and int.fromEnvironment constructors; the key and field are json values.\n'
|
'These can be accessed using the String.fromEnvironment, bool.fromEnvironment, and int.fromEnvironment constructors.\n'
|
||||||
'Multiple defines can be passed by repeating "--${FlutterOptions.kDartDefineFromFileOption}" multiple times.',
|
'Multiple defines can be passed by repeating "--${FlutterOptions.kDartDefineFromFileOption}" multiple times.',
|
||||||
valueHelp: 'use-define-config.json',
|
valueHelp: 'use-define-config.json|.env',
|
||||||
splitCommas: false,
|
splitCommas: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1341,18 +1341,29 @@ abstract class FlutterCommand extends Command<void> {
|
|||||||
final Map<String, Object?> dartDefineConfigJsonMap = <String, Object?>{};
|
final Map<String, Object?> dartDefineConfigJsonMap = <String, Object?>{};
|
||||||
|
|
||||||
if (argParser.options.containsKey(FlutterOptions.kDartDefineFromFileOption)) {
|
if (argParser.options.containsKey(FlutterOptions.kDartDefineFromFileOption)) {
|
||||||
final List<String> configJsonPaths = stringsArg(
|
final List<String> configFilePaths = stringsArg(
|
||||||
FlutterOptions.kDartDefineFromFileOption,
|
FlutterOptions.kDartDefineFromFileOption,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (final String path in configJsonPaths) {
|
for (final String path in configFilePaths) {
|
||||||
if (!globals.fs.isFileSync(path)) {
|
if (!globals.fs.isFileSync(path)) {
|
||||||
throwToolExit('Json config define file "--${FlutterOptions
|
throwToolExit('Json config define file "--${FlutterOptions
|
||||||
.kDartDefineFromFileOption}=$path" is not a file, '
|
.kDartDefineFromFileOption}=$path" is not a file, '
|
||||||
'please fix first!');
|
'please fix first!');
|
||||||
}
|
}
|
||||||
|
|
||||||
final String configJsonRaw = globals.fs.file(path).readAsStringSync();
|
final String configRaw = globals.fs.file(path).readAsStringSync();
|
||||||
|
|
||||||
|
// Determine whether the file content is JSON or .env format.
|
||||||
|
String configJsonRaw;
|
||||||
|
if (configRaw.trim().startsWith('{')) {
|
||||||
|
configJsonRaw = configRaw;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Convert env file to JSON.
|
||||||
|
configJsonRaw = convertEnvFileToJsonRaw(configRaw);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fix json convert Object value :type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'Map<String, Object>' in type cast
|
// Fix json convert Object value :type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'Map<String, Object>' in type cast
|
||||||
(json.decode(configJsonRaw) as Map<String, dynamic>)
|
(json.decode(configJsonRaw) as Map<String, dynamic>)
|
||||||
@ -1370,6 +1381,88 @@ abstract class FlutterCommand extends Command<void> {
|
|||||||
return dartDefineConfigJsonMap;
|
return dartDefineConfigJsonMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a property line from an env file.
|
||||||
|
/// Supposed property structure should be:
|
||||||
|
/// key=value
|
||||||
|
///
|
||||||
|
/// Where: key is a string without spaces and value is a string.
|
||||||
|
/// Value can also contain '=' char.
|
||||||
|
///
|
||||||
|
/// Returns a record of key and value as strings.
|
||||||
|
MapEntry<String, String> _parseProperty(String line) {
|
||||||
|
final RegExp blockRegExp = RegExp(r'^\s*([a-zA-Z_]+[a-zA-Z0-9_]*)\s*=\s*"""\s*(.*)$');
|
||||||
|
if (blockRegExp.hasMatch(line)) {
|
||||||
|
throwToolExit('Multi-line value is not supported: $line');
|
||||||
|
}
|
||||||
|
|
||||||
|
final RegExp propertyRegExp = RegExp(r'^\s*([a-zA-Z_]+[a-zA-Z0-9_]*)\s*=\s*(.*)?$');
|
||||||
|
final Match? match = propertyRegExp.firstMatch(line);
|
||||||
|
if (match == null) {
|
||||||
|
throwToolExit('Unable to parse file provided for '
|
||||||
|
'--${FlutterOptions.kDartDefineFromFileOption}.\n'
|
||||||
|
'Invalid property line: $line');
|
||||||
|
}
|
||||||
|
|
||||||
|
final String key = match.group(1)!;
|
||||||
|
final String value = match.group(2) ?? '';
|
||||||
|
|
||||||
|
// Remove wrapping quotes and trailing line comment.
|
||||||
|
final RegExp doubleQuoteValueRegExp = RegExp(r'^"(.*)"\s*(\#\s*.*)?$');
|
||||||
|
final Match? doubleQuoteValue = doubleQuoteValueRegExp.firstMatch(value);
|
||||||
|
if (doubleQuoteValue != null) {
|
||||||
|
return MapEntry<String, String>(key, doubleQuoteValue.group(1)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
final RegExp quoteValueRegExp = RegExp(r"^'(.*)'\s*(\#\s*.*)?$");
|
||||||
|
final Match? quoteValue = quoteValueRegExp.firstMatch(value);
|
||||||
|
if (quoteValue != null) {
|
||||||
|
return MapEntry<String, String>(key, quoteValue.group(1)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
final RegExp backQuoteValueRegExp = RegExp(r'^`(.*)`\s*(\#\s*.*)?$');
|
||||||
|
final Match? backQuoteValue = backQuoteValueRegExp.firstMatch(value);
|
||||||
|
if (backQuoteValue != null) {
|
||||||
|
return MapEntry<String, String>(key, backQuoteValue.group(1)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
final RegExp noQuoteValueRegExp = RegExp(r'^([^#\n\s]*)\s*(?:\s*#\s*(.*))?$');
|
||||||
|
final Match? noQuoteValue = noQuoteValueRegExp.firstMatch(value);
|
||||||
|
if (noQuoteValue != null) {
|
||||||
|
return MapEntry<String, String>(key, noQuoteValue.group(1)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MapEntry<String, String>(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts an .env file string to its equivalent JSON string.
|
||||||
|
///
|
||||||
|
/// For example, the .env file string
|
||||||
|
/// key=value # comment
|
||||||
|
/// complexKey="foo#bar=baz"
|
||||||
|
/// would be converted to a JSON string equivalent to:
|
||||||
|
/// {
|
||||||
|
/// "key": "value",
|
||||||
|
/// "complexKey": "foo#bar=baz"
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// Multiline values are not supported.
|
||||||
|
String convertEnvFileToJsonRaw(String configRaw) {
|
||||||
|
final List<String> lines = configRaw
|
||||||
|
.split('\n')
|
||||||
|
.map((String line) => line.trim())
|
||||||
|
.where((String line) => line.isNotEmpty)
|
||||||
|
.where((String line) => !line.startsWith('#')) // Remove comment lines.
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final Map<String, String> propertyMap = <String, String>{};
|
||||||
|
for (final String line in lines) {
|
||||||
|
final MapEntry<String, String> property = _parseProperty(line);
|
||||||
|
propertyMap[property.key] = property.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonEncode(propertyMap);
|
||||||
|
}
|
||||||
|
|
||||||
/// Updates dart-defines based on [webRenderer].
|
/// Updates dart-defines based on [webRenderer].
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
static List<String> updateDartDefines(List<String> dartDefines, WebRendererMode webRenderer) {
|
static List<String> updateDartDefines(List<String> dartDefines, WebRendererMode webRenderer) {
|
||||||
|
@ -518,7 +518,8 @@ void main() {
|
|||||||
"kDouble": 1.1,
|
"kDouble": 1.1,
|
||||||
"name": "denghaizhu",
|
"name": "denghaizhu",
|
||||||
"title": "this is title from config json file",
|
"title": "this is title from config json file",
|
||||||
"nullValue": null
|
"nullValue": null,
|
||||||
|
"containEqual": "sfadsfv=432f"
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
);
|
);
|
||||||
@ -549,6 +550,7 @@ void main() {
|
|||||||
'name=denghaizhu',
|
'name=denghaizhu',
|
||||||
'title=this is title from config json file',
|
'title=this is title from config json file',
|
||||||
'nullValue=null',
|
'nullValue=null',
|
||||||
|
'containEqual=sfadsfv=432f',
|
||||||
'body=this is body from config json file',
|
'body=this is body from config json file',
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
@ -557,6 +559,155 @@ void main() {
|
|||||||
ProcessManager: () => FakeProcessManager.any(),
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testUsingContext('--dart-define-from-file correctly parses a valid env file', () async {
|
||||||
|
globals.fs
|
||||||
|
.file(globals.fs.path.join('lib', 'main.dart'))
|
||||||
|
.createSync(recursive: true);
|
||||||
|
globals.fs.file('pubspec.yaml').createSync();
|
||||||
|
globals.fs.file('.packages').createSync();
|
||||||
|
await globals.fs.file('.env').writeAsString('''
|
||||||
|
# comment
|
||||||
|
kInt=1
|
||||||
|
kDouble=1.1 # should be double
|
||||||
|
|
||||||
|
name=piotrfleury
|
||||||
|
title=this is title from config env file
|
||||||
|
empty=
|
||||||
|
|
||||||
|
doubleQuotes="double quotes 'value'#=" # double quotes
|
||||||
|
singleQuotes='single quotes "value"#=' # single quotes
|
||||||
|
backQuotes=`back quotes "value" '#=` # back quotes
|
||||||
|
|
||||||
|
hashString="some-#-hash-string-value"
|
||||||
|
|
||||||
|
# Play around with spaces around the equals sign.
|
||||||
|
spaceBeforeEqual =value
|
||||||
|
spaceAroundEqual = value
|
||||||
|
spaceAfterEqual= value
|
||||||
|
|
||||||
|
''');
|
||||||
|
await globals.fs.file('.env2').writeAsString('''
|
||||||
|
# second comment
|
||||||
|
|
||||||
|
body=this is body from config env file
|
||||||
|
''');
|
||||||
|
final CommandRunner<void> runner =
|
||||||
|
createTestCommandRunner(BuildBundleCommand(
|
||||||
|
logger: BufferLogger.test(),
|
||||||
|
));
|
||||||
|
|
||||||
|
await runner.run(<String>[
|
||||||
|
'bundle',
|
||||||
|
'--no-pub',
|
||||||
|
'--dart-define-from-file=.env',
|
||||||
|
'--dart-define-from-file=.env2',
|
||||||
|
]);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
BuildSystem: () => TestBuildSystem.all(BuildResult(success: true),
|
||||||
|
(Target target, Environment environment) {
|
||||||
|
expect(
|
||||||
|
_decodeDartDefines(environment),
|
||||||
|
containsAllInOrder(const <String>[
|
||||||
|
'kInt=1',
|
||||||
|
'kDouble=1.1',
|
||||||
|
'name=piotrfleury',
|
||||||
|
'title=this is title from config env file',
|
||||||
|
'empty=',
|
||||||
|
"doubleQuotes=double quotes 'value'#=",
|
||||||
|
'singleQuotes=single quotes "value"#=',
|
||||||
|
'backQuotes=back quotes "value" \'#=',
|
||||||
|
'hashString=some-#-hash-string-value',
|
||||||
|
'spaceBeforeEqual=value',
|
||||||
|
'spaceAroundEqual=value',
|
||||||
|
'spaceAfterEqual=value',
|
||||||
|
'body=this is body from config env file'
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
FileSystem: fsFactory,
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('--dart-define-from-file option env file throws a ToolExit when .env file contains a multiline value', () async {
|
||||||
|
globals.fs
|
||||||
|
.file(globals.fs.path.join('lib', 'main.dart'))
|
||||||
|
.createSync(recursive: true);
|
||||||
|
globals.fs.file('pubspec.yaml').createSync();
|
||||||
|
globals.fs.file('.packages').createSync();
|
||||||
|
await globals.fs.file('.env').writeAsString('''
|
||||||
|
# single line value
|
||||||
|
name=piotrfleury
|
||||||
|
|
||||||
|
# multi-line value
|
||||||
|
multiline = """ Welcome to .env demo
|
||||||
|
a simple counter app with .env file support
|
||||||
|
for more info, check out the README.md file
|
||||||
|
Thanks! """ # This is the welcome message that will be displayed on the counter app
|
||||||
|
|
||||||
|
''');
|
||||||
|
final CommandRunner<void> runner =
|
||||||
|
createTestCommandRunner(BuildBundleCommand(
|
||||||
|
logger: BufferLogger.test(),
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(() => runner.run(<String>[
|
||||||
|
'bundle',
|
||||||
|
'--no-pub',
|
||||||
|
'--dart-define-from-file=.env',
|
||||||
|
]), throwsToolExit(message: 'Multi-line value is not supported: multiline = """ Welcome to .env demo'));
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
BuildSystem: () => TestBuildSystem.all(BuildResult(success: true)),
|
||||||
|
FileSystem: fsFactory,
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('--dart-define-from-file option works with mixed file formats',
|
||||||
|
() async {
|
||||||
|
globals.fs
|
||||||
|
.file(globals.fs.path.join('lib', 'main.dart'))
|
||||||
|
.createSync(recursive: true);
|
||||||
|
globals.fs.file('pubspec.yaml').createSync();
|
||||||
|
globals.fs.file('.packages').createSync();
|
||||||
|
await globals.fs.file('.env').writeAsString('''
|
||||||
|
kInt=1
|
||||||
|
kDouble=1.1
|
||||||
|
name=piotrfleury
|
||||||
|
title=this is title from config env file
|
||||||
|
''');
|
||||||
|
await globals.fs.file('config.json').writeAsString('''
|
||||||
|
{
|
||||||
|
"body": "this is body from config json file"
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
final CommandRunner<void> runner =
|
||||||
|
createTestCommandRunner(BuildBundleCommand(
|
||||||
|
logger: BufferLogger.test(),
|
||||||
|
));
|
||||||
|
|
||||||
|
await runner.run(<String>[
|
||||||
|
'bundle',
|
||||||
|
'--no-pub',
|
||||||
|
'--dart-define-from-file=.env',
|
||||||
|
'--dart-define-from-file=config.json',
|
||||||
|
]);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
BuildSystem: () => TestBuildSystem.all(BuildResult(success: true),
|
||||||
|
(Target target, Environment environment) {
|
||||||
|
expect(
|
||||||
|
_decodeDartDefines(environment),
|
||||||
|
containsAllInOrder(const <String>[
|
||||||
|
'kInt=1',
|
||||||
|
'kDouble=1.1',
|
||||||
|
'name=piotrfleury',
|
||||||
|
'title=this is title from config env file',
|
||||||
|
'body=this is body from config json file',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
FileSystem: fsFactory,
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
});
|
||||||
|
|
||||||
testUsingContext('test --dart-define-from-file option if conflict', () async {
|
testUsingContext('test --dart-define-from-file option if conflict', () async {
|
||||||
globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
|
globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
|
||||||
globals.fs.file('pubspec.yaml').createSync();
|
globals.fs.file('pubspec.yaml').createSync();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user