From 52bb16c7f9c514c4841ce676eda5613cd8d11ff1 Mon Sep 17 00:00:00 2001 From: Vladimir Zdravkovic Date: Sun, 29 Dec 2024 16:47:37 +0100 Subject: [PATCH] CLI Command that generates all flavors with single flag (#752) * - added support to generate all flavors with a single command * - added more helpers and tests * - updated the readme and changelog --- CHANGELOG.md | 4 + README.md | 40 +++++-- bin/create.dart | 89 +++++++++++++--- bin/remove.dart | 31 +++++- example/ios/Flutter/Debug.xcconfig | 1 + example/ios/Flutter/Release.xcconfig | 1 + example/ios/Podfile | 44 ++++++++ lib/enums.dart | 12 +++ lib/helper_utils.dart | 46 ++++++++ test/helper_utils_test.dart | 151 +++++++++++++++++++++++++++ 10 files changed, 396 insertions(+), 23 deletions(-) create mode 100644 example/ios/Podfile create mode 100644 lib/enums.dart create mode 100644 lib/helper_utils.dart create mode 100644 test/helper_utils_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index bb85e0c..488cc93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [2.4.4] - () + +- Added the ability to generate all flavors with a single flag. Closes [#751](https://github.com/jonbhanson/flutter_native_splash/issues/751) + ## [2.4.3] - (2024-Nov-17) - Add Swift Package Manager support. Closes [#749](https://github.com/jonbhanson/flutter_native_splash/issues/749). diff --git a/README.md b/README.md index bfdc8f5..f02577f 100644 --- a/README.md +++ b/README.md @@ -194,10 +194,20 @@ When the package finishes running, your splash screen is ready. (Optionally), If you added your config to a separate YAML file instead of `pubspec.yaml`, just add --path with the command in the terminal: -``` +```bash dart run flutter_native_splash:create --path=path/to/my/file.yaml ``` +| Command | Description | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| -h, --[no-]help | Show help | +| -p, --path | Path to the flutter project, if the project is not in it's default location. | +| -f, --flavor | Flavor to create the splash for. The flavor must match the pattern flutter_native_splash-*.yaml (where * is the flavor name). | +| -F, --flavors | Comma separated list of flavors to create the splash screens for. Match the pattern flutter_native_splash-*.yaml (where * is the flavor name). | +| -A, --[no-]all-flavors | Create the splash screens for all flavors that match the pattern flutter_native_splash-*.yaml (where * is the flavor name). | + +> Note: Only one flavor option is allowed. + ## 3. Set up app initialization (optional) By default, the splash screen will be removed when Flutter has drawn the first frame. If you would like the splash screen to remain while your app initializes, you can use the `preserve()` and `remove()` methods together. Pass the `preserve()` method the value returned from `WidgetsFlutterBinding.ensureInitialized()` to keep the splash on screen. Later, when your app has initialized, make a call to `remove()` to remove the splash screen. @@ -255,15 +265,15 @@ If you have a project setup that contains multiple flavors or environments, and Instead of maintaining multiple files and copy/pasting images, you can now, using this tool, create different splash screens for different environments. -### Pre-requirements +## Pre-requirements -In order to use the new feature, and generate the desired splash images for you app, a couple of changes are required. +In order to use this feature, and generate the desired splash images for your app, a couple of changes are required. If you want to generate just one flavor and one file you would use either options as described in Step 1. But in order to setup the flavors, you will then be required to move all your setup values to the `flutter_native_splash.yaml` file, but with a prefix. Let's assume for the rest of the setup that you have 3 different flavors, `Production`, `Acceptance`, `Development`. -First this you will need to do is to create a different setup file for all 3 flavors with a suffix like so: +First thing you will need to do is to create a different setup file for all 3 flavors with a suffix like so: ```bash flutter_native_splash-production.yaml @@ -271,7 +281,7 @@ flutter_native_splash-acceptance.yaml flutter_native_splash-development.yaml ``` -You would setup those 3 files the same way as you would the one, but with different assets depending on which environment you would be generating. For example (Note: these are just examples, you can use whatever setup you need for your project that is already supported by the package): +You would setup those 3 files the same way as you would the one, but with different assets depending on which environment you would be generating. For example: ```yaml # flutter_native_splash-development.yaml @@ -328,6 +338,10 @@ flutter_native_splash: web: false ``` +> Note: these are just example values. You should substitute them with real values. + +## One by one + If you'd like to generate only a single flavor (maybe you are testing something out), you can use only the single command like this: @@ -342,6 +356,8 @@ dart run flutter_native_splash:create --flavor acceptance dart run flutter_native_splash:create --flavor development ``` +## More than one + You also have the ability to specify all the flavors in one command as shown bellow: @@ -349,7 +365,19 @@ as shown bellow: dart run flutter_native_splash:create --flavors development,staging,production ``` -Note: the available flavors need to be comma separated for this option to work. +> Note: the available flavors need to be comma separated for this option to work. + +## All flavors + +And if you have many different flavors available in your project, and wish to generate the splash screen for all of them, you can use this command (starting from 2.4.4): + +```bash +dart run flutter_native_splash:create --all-flavors +# OR you can use the shorthand option +dart run flutter_native_splash:create -A +``` + +This will take all files from the root of the project, scan through them and match for the pattern `flutter_native_splash-*.yaml` where the value at the place of the star will be used as the flavor name and will be consumed to generate the files. ### Android setup diff --git a/bin/create.dart b/bin/create.dart index 45f620b..f509f19 100644 --- a/bin/create.dart +++ b/bin/create.dart @@ -1,35 +1,98 @@ +import 'dart:io'; + import 'package:args/args.dart'; import 'package:flutter_native_splash/cli_commands.dart'; +import 'package:flutter_native_splash/enums.dart'; +import 'package:flutter_native_splash/helper_utils.dart'; void main(List args) { final parser = ArgParser(); - parser.addOption('path'); - parser.addOption('flavor'); - parser.addOption('flavors'); + parser + ..addFlag( + ArgEnums.help.name, + abbr: ArgEnums.help.abbr, + help: 'Show help', + ) + ..addOption( + ArgEnums.path.name, + abbr: ArgEnums.path.abbr, + help: + 'Path to the flutter project, if the project is not in it\'s default location.', + ) + ..addOption( + ArgEnums.flavor.name, + abbr: ArgEnums.flavor.abbr, + help: + 'Flavor to create the splash for. The flavor must match the pattern flutter_native_splash-*.yaml (where * is the flavor name).', + ) + ..addOption( + ArgEnums.flavors.name, + abbr: ArgEnums.flavors.abbr, + help: + 'Comma separated list of flavors to create the splash screens for. Match the pattern flutter_native_splash-*.yaml (where * is the flavor name).', + ) + ..addFlag( + ArgEnums.allFlavors.name, + abbr: ArgEnums.allFlavors.abbr, + help: + 'Create the splash screens for all flavors that match the pattern flutter_native_splash-*.yaml (where * is the flavor name).', + ); final parsedArgs = parser.parse(args); - if (parsedArgs['flavor'] != null && parsedArgs['flavors'] != null) { - throw Exception('Cannot use both flavor and flavors arguments'); + final helpArg = parsedArgs[ArgEnums.help.name] as bool?; + + if (helpArg == true) { + print(parser.usage); + return; } - if (parsedArgs['flavor'] != null) { + final pathArg = parsedArgs[ArgEnums.path.name]?.toString(); + final flavorArg = parsedArgs[ArgEnums.flavor.name]?.toString(); + final flavorsArg = parsedArgs[ArgEnums.flavors.name]?.toString(); + final allFlavorsArg = parsedArgs[ArgEnums.allFlavors.name] as bool?; + + // Validate the flavor arguments + HelperUtils.validateFlavorArgs( + flavorArg: flavorArg, + flavorsArg: flavorsArg, + allFlavorsArg: allFlavorsArg, + ); + + if (flavorArg != null) { createSplash( - path: parsedArgs['path']?.toString(), - flavor: parsedArgs['flavor']?.toString(), + path: pathArg, + flavor: flavorArg, ); - } else if (parsedArgs['flavors'] != null) { - final flavors = parsedArgs['flavors']?.toString().split(','); - for (final flavor in flavors!) { + } else if (flavorsArg != null) { + for (final flavor in flavorsArg.split(',')) { + createSplash( + path: pathArg, + flavor: flavor, + ); + } + } else if (allFlavorsArg == true) { + // Find all flavor configurations in current project directory + final flavors = Directory.current + .listSync() + .whereType() + .map((entity) => entity.path.split(Platform.pathSeparator).last) + .where(HelperUtils.isValidFlavorConfigFileName) + .map(HelperUtils.getFlavorNameFromFileName) + .toList(); + + print('Found ${flavors.length} flavor configurations: $flavors'); + + for (final flavor in flavors) { createSplash( - path: parsedArgs['path']?.toString(), + path: pathArg, flavor: flavor, ); } } else { createSplash( - path: parsedArgs['path']?.toString(), + path: pathArg, flavor: null, ); } diff --git a/bin/remove.dart b/bin/remove.dart index b904ca8..d989b1c 100644 --- a/bin/remove.dart +++ b/bin/remove.dart @@ -1,16 +1,39 @@ import 'package:args/args.dart'; import 'package:flutter_native_splash/cli_commands.dart'; +import 'package:flutter_native_splash/enums.dart'; void main(List args) { final parser = ArgParser(); - parser.addOption('path'); - parser.addOption('flavor'); + parser + ..addFlag( + ArgEnums.help.name, + abbr: ArgEnums.help.abbr, + help: 'Show help', + ) + ..addOption( + ArgEnums.path.name, + abbr: ArgEnums.path.abbr, + help: + 'Path to the flutter project, if the project is not in it\'s default location.', + ) + ..addOption( + ArgEnums.flavor.name, + abbr: ArgEnums.flavor.abbr, + help: 'Flavor to remove the splash for.', + ); final parsedArgs = parser.parse(args); + final helpArg = parsedArgs[ArgEnums.help.name]; + + if (helpArg != null) { + print(parser.usage); + return; + } + removeSplash( - path: parsedArgs['path']?.toString(), - flavor: parsedArgs['flavor']?.toString(), + path: parsedArgs[ArgEnums.path.name]?.toString(), + flavor: parsedArgs[ArgEnums.flavor.name]?.toString(), ); } diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/enums.dart b/lib/enums.dart new file mode 100644 index 0000000..6f79bb4 --- /dev/null +++ b/lib/enums.dart @@ -0,0 +1,12 @@ +enum ArgEnums { + help(name: 'help', abbr: 'h'), + path(name: 'path', abbr: 'p'), + flavor(name: 'flavor', abbr: 'f'), + flavors(name: 'flavors', abbr: 'F'), + allFlavors(name: 'all-flavors', abbr: 'A'); + + final String name; + final String abbr; + + const ArgEnums({required this.name, required this.abbr}); +} diff --git a/lib/helper_utils.dart b/lib/helper_utils.dart new file mode 100644 index 0000000..962acf4 --- /dev/null +++ b/lib/helper_utils.dart @@ -0,0 +1,46 @@ +import 'package:flutter_native_splash/enums.dart'; + +class HelperUtils { + const HelperUtils._(); + + /// Checks if a given filename matches the flutter native splash flavor config pattern + /// The pattern is: flutter_native_splash-*.yaml where * is the flavor name + /// + /// Returns true if the filename matches the pattern, false otherwise + static bool isValidFlavorConfigFileName(String fileName) { + return RegExp(r'^flutter_native_splash-[^-]+\.yaml$').hasMatch(fileName); + } + + /// Extracts the flavor name from a valid flavor config filename + /// + /// Throws an exception if the filename is not a valid flavor config filename + static String getFlavorNameFromFileName(String fileName) { + final flavorMatch = + RegExp(r'^flutter_native_splash-(.+)\.yaml$').firstMatch(fileName); + + final flavorName = flavorMatch?.group(1); + + if (flavorName == null) { + throw Exception('Invalid flavor config filename: $fileName'); + } + + return flavorName; + } + + /// Validate the flavor arguments + /// + /// Throws an exception if the arguments are invalid. + static void validateFlavorArgs({ + required String? flavorArg, + required String? flavorsArg, + required bool? allFlavorsArg, + }) { + if ((flavorArg != null && flavorsArg != null) || + (flavorArg != null && allFlavorsArg == true) || + (flavorsArg != null && allFlavorsArg == true)) { + throw Exception( + 'Cannot use multiple flavor options together. Please use only one of: --${ArgEnums.flavor.name}, --${ArgEnums.flavors.name}, or --${ArgEnums.allFlavors.name}.', + ); + } + } +} diff --git a/test/helper_utils_test.dart b/test/helper_utils_test.dart new file mode 100644 index 0000000..c3c6916 --- /dev/null +++ b/test/helper_utils_test.dart @@ -0,0 +1,151 @@ +import 'package:flutter_native_splash/helper_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('HelperUtils.isValidFlavorConfigFileName', () { + test('should return true for valid flavor config filenames', () { + expect( + HelperUtils.isValidFlavorConfigFileName( + 'flutter_native_splash-dev.yaml'), + isTrue, + ); + expect( + HelperUtils.isValidFlavorConfigFileName( + 'flutter_native_splash-prod.yaml'), + isTrue, + ); + expect( + HelperUtils.isValidFlavorConfigFileName( + 'flutter_native_splash-staging.yaml'), + isTrue, + ); + }); + + test('should return false for invalid flavor config filenames', () { + expect( + HelperUtils.isValidFlavorConfigFileName('flutter_native_splash.yaml'), + isFalse, + ); + expect( + HelperUtils.isValidFlavorConfigFileName('flutter_native_splash-.yaml'), + isFalse, + ); + expect( + HelperUtils.isValidFlavorConfigFileName( + 'flutter_native_splash-dev.yml'), + isFalse, + ); + expect( + HelperUtils.isValidFlavorConfigFileName('other-config.yaml'), + isFalse, + ); + expect( + HelperUtils.isValidFlavorConfigFileName('random.txt'), + isFalse, + ); + }); + }); + + group('HelperUtils.getFlavorNameFromFileName', () { + test('should return the flavor name from a valid flavor config filename', + () { + expect( + HelperUtils.getFlavorNameFromFileName('flutter_native_splash-dev.yaml'), + 'dev', + ); + expect( + HelperUtils.getFlavorNameFromFileName( + 'flutter_native_splash-prod.yaml', + ), + 'prod', + ); + expect( + HelperUtils.getFlavorNameFromFileName( + 'flutter_native_splash-staging-flavor.yaml', + ), + 'staging-flavor', + ); + }); + + test( + 'should throw an exception if the filename is not a valid flavor config filename', + () { + expect( + () => HelperUtils.getFlavorNameFromFileName( + 'flutter_native_splash.yaml', + ), + throwsA( + isA().having( + (e) => e.toString(), + 'message', + contains('Invalid flavor config filename'), + ), + ), + ); + }); + }); + + group('Flavor arguments validation', () { + test('throws when flavor and flavors are used together', () { + expect( + () => HelperUtils.validateFlavorArgs( + flavorArg: 'dev', + flavorsArg: 'dev,prod', + allFlavorsArg: false, + ), + throwsA( + isA().having( + (e) => e.toString(), + 'message', + contains('Cannot use multiple flavor options together'), + ), + ), + ); + }); + + test('throws when flavor and allFlavors are used together', () { + expect( + () => HelperUtils.validateFlavorArgs( + flavorArg: 'dev', + flavorsArg: null, + allFlavorsArg: true, + ), + throwsA( + isA().having( + (e) => e.toString(), + 'message', + contains('Cannot use multiple flavor options together'), + ), + ), + ); + }); + + test('throws when flavors and allFlavors are used together', () { + expect( + () => HelperUtils.validateFlavorArgs( + flavorArg: null, + flavorsArg: 'dev,prod', + allFlavorsArg: true, + ), + throwsA( + isA().having( + (e) => e.toString(), + 'message', + contains('Cannot use multiple flavor options together'), + ), + ), + ); + }); + + test('does not throw with single flavor option', () { + expect( + () => HelperUtils.validateFlavorArgs( + flavorArg: 'dev', + flavorsArg: null, + allFlavorsArg: false, + ), + returnsNormally, + ); + }); + }); +}