diff --git a/analysis_options.yaml b/analysis_options.yaml index ae7ff1a..c00cf11 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,9 +1,5 @@ include: package:flame_lint/analysis_options.yaml -linter: - rules: - avoid_print: false # since it is a CLI application - analyzer: exclude: - - bricks/ \ No newline at end of file + - bricks/**/* diff --git a/bin/ignite.dart b/bin/ignite.dart index 4282bab..c1fdaf3 100644 --- a/bin/ignite.dart +++ b/bin/ignite.dart @@ -1,5 +1,16 @@ -import 'package:ignite_cli/main.dart'; +import 'dart:io'; -void main(List args) { - mainCommand(args); +import 'package:ignite_cli/flame_version_manager.dart'; +import 'package:ignite_cli/ignite_command_runner.dart'; +import 'package:ignite_cli/ignite_context.dart'; +import 'package:mason_logger/mason_logger.dart'; + +Future main(List args) async { + final manager = await FlameVersionManager.fetch(); + final context = IgniteContext(logger: Logger(), flameVersionManager: manager); + final runner = IgniteCommandRunner(context); + final code = await runner.run(args); + + // ensure all buffered output is written before exit + await Future.wait([stdout.close(), stderr.close()]).then((_) => exit(code)); } diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index 5eb6851..e68c42f 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -2,26 +2,97 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:dartlin/dartlin.dart'; +import 'package:ignite_cli/commands/ignite_command.dart'; import 'package:ignite_cli/flame_version_manager.dart'; +import 'package:ignite_cli/ignite_context.dart'; import 'package:ignite_cli/templates/template.dart'; import 'package:ignite_cli/utils.dart'; -import 'package:io/ansi.dart' as ansi; import 'package:mason/mason.dart'; -import 'package:process_run/process_run.dart'; -Future createCommand(ArgResults command) async { +class CreateCommand extends IgniteCommand { + CreateCommand(super.context) { + final packages = context.flameVersionManager.versions; + final flameVersions = packages[Package.flame]!; + + argParser.addFlag( + 'interactive', + abbr: 'i', + help: 'Whether to run in interactive mode or not.', + defaultsTo: true, + ); + argParser.addOption( + 'name', + help: 'The name of your game (valid dart identifier).', + ); + argParser.addOption( + 'org', + help: 'The org name, in reverse domain notation ' + '(package name/bundle identifier).', + ); + argParser.addFlag( + 'create-folder', + abbr: 'f', + help: 'If you want to create a new folder on the current location with ' + "the project name or if you are already on the new project's folder.", + ); + argParser.addOption( + 'template', + help: 'What Flame template you would like to use for your new project', + allowed: ['simple', 'example'], + ); + argParser.addOption( + 'flame-version', + help: 'What Flame version you would like to use.', + allowed: [ + ...flameVersions.versions.take(5), + '...', + flameVersions.versions.last, + ], + ); + argParser.addMultiOption( + 'extra-packages', + help: 'Which packages to use', + allowed: packages.keys.map((e) => e.name).toList(), + ); + } + + @override + String get description => 'Create a new Flame project'; + + @override + String get name => 'create'; + + @override + Future run() async { + final argResults = this.argResults; + if (argResults == null) { + return ExitCode.usage.code; + } + + final code = await createCommand(context, argResults); + + return code; + } +} + +Future createCommand( + IgniteContext context, + ArgResults command, +) async { final interactive = command['interactive'] != 'false'; if (interactive) { - stdout.write('\nWelcome to ${ansi.red.wrap('Ignite CLI')}! 🔥\n'); - stdout.write("Let's create a new project!\n\n"); + context.logger + ..info('\nWelcome to ${red.wrap('Ignite CLI')}! 🔥') + ..info("Let's create a new project!\n"); } final name = getString( isInteractive: interactive, + logger: context.logger, command, 'name', - 'Choose a name for your project: ', + 'Choose a name for your project', desc: 'Note: this must be a valid dart identifier (no dashes). ' 'For example: my_game', validate: (it) => switch (it) { @@ -34,10 +105,11 @@ Future createCommand(ArgResults command) async { ); final org = getString( + logger: context.logger, isInteractive: interactive, command, 'org', - 'Choose an org for your project: ', + 'Choose an org for your project:', desc: 'Note: this is a dot separated list of "packages", ' 'normally in reverse domain notation. ' 'For example: org.flame_engine.games', @@ -48,9 +120,10 @@ Future createCommand(ArgResults command) async { }, ); - final versions = FlameVersionManager.singleton.versions; + final versions = context.flameVersionManager.versions; final flameVersions = versions[Package.flame]!; final flameVersion = getOption( + logger: context.logger, isInteractive: interactive, command, 'flame-version', @@ -60,11 +133,12 @@ Future createCommand(ArgResults command) async { fullOptions: flameVersions.versions.associateWith((e) => e), ); - final extraPackageOptions = FlameVersionManager.singleton.versions.keys + final extraPackageOptions = context.flameVersionManager.versions.keys .where((key) => !Package.includedByDefault.contains(key)) .map((key) => key.name) .toList(); final extraPackages = getMultiOption( + logger: context.logger, isInteractive: interactive, isRequired: false, command, @@ -83,23 +157,21 @@ Future createCommand(ArgResults command) async { final devDependencies = packages.where((e) => e.isDevDependency); final currentDir = Directory.current.path; - print('\nYour current directory is: $currentDir'); - - final createFolder = getOption( - isInteractive: interactive, - command, - 'create-folder', - 'Do you want to put your project files directly on the current dir, ' - 'or do you want to create a folder called $name?', - { - 'Create a folder called $name': 'true', - 'Put the files directly on $currentDir': 'false', - }, - ) == - 'true'; - - print('\n'); + + context.logger.info('Your current directory is: $currentDir'); + + bool createFolder; + if (!interactive) { + createFolder = command['create-folder'] == true; + } else { + createFolder = context.logger.confirm( + 'Create project a folder called $name?', + defaultValue: command['create-folder'] == true, + ); + } + final template = getOption( + logger: context.logger, isInteractive: interactive, command, 'template', @@ -109,60 +181,98 @@ Future createCommand(ArgResults command) async { ); final actualDir = '$currentDir${createFolder ? '/$name' : ''}'; - if (createFolder) { - await runExecutableArguments('mkdir', [actualDir]); - } - print('\nRunning flutter create on $actualDir ...'); - await runExecutableArguments( - 'flutter', - 'create --org $org --project-name $name .'.split(' '), - workingDirectory: actualDir, - verbose: true, - ); - await runExecutableArguments( - 'rm', - '-rf lib test'.split(' '), - workingDirectory: actualDir, - verbose: true, - ); + final progress = context.logger.progress('Generating project'); + ProcessResult? processResult; + var code = ExitCode.success.code; - final bundle = Template.byKey(template).bundle; - final generator = await MasonGenerator.fromBundle(bundle); - final target = DirectoryGeneratorTarget( - Directory(actualDir), - ); - final variables = { - 'name': name, - 'description': 'A simple Flame game.', - 'version': '0.1.0', - 'extra-dependencies': dependencies - .sortedBy((e) => e.name) - .map((package) => package.toMustache(versions, flameVersion)) - .toList(), - 'extra-dev-dependencies': devDependencies - .sortedBy((e) => e.name) - .map((package) => package.toMustache(versions, flameVersion)) - .toList(), - }; - final files = await generator.generate(target, vars: variables); - - final canHaveTests = devDependencies.contains(Package.flameTest); - if (!canHaveTests) { - await runExecutableArguments( + try { + if (createFolder) { + progress.update('Running [mkdir] on $actualDir'); + processResult = await context.run('mkdir', [actualDir]); + if (processResult.exitCode > ExitCode.success.code) { + return code = processResult.exitCode; + } + } + + progress.update('Running [flutter create] on $actualDir'); + processResult = await context.run( + 'flutter', + 'create --org $org --project-name $name .'.split(' '), + workingDirectory: actualDir, + ); + if (processResult.exitCode > ExitCode.success.code) { + return code = processResult.exitCode; + } + + progress.update('Running [rm -rf lib test] on $actualDir'); + processResult = await context.run( 'rm', - '-rf test'.split(' '), + '-rf lib test'.split(' '), workingDirectory: actualDir, - verbose: true, ); - } - await runExecutableArguments( - 'flutter', - 'pub get'.split(' '), - workingDirectory: actualDir, - verbose: true, - ); + if (processResult.exitCode > ExitCode.success.code) { + return code = processResult.exitCode; + } + progress.update('Bundling game template'); + + final bundle = Template.byKey(template).bundle; + final generator = await context.generatorFromBundle(bundle); + final target = context.createTarget(Directory(actualDir)); - print('Updated ${files.length} files on top of flutter create.\n'); - print('Your new Flame project was successfully created!'); + final variables = { + 'name': name, + 'description': 'A simple Flame game.', + 'version': '0.1.0', + 'extra-dependencies': dependencies + .sortedBy((e) => e.name) + .map((package) => package.toMustache(versions, flameVersion)) + .toList(), + 'extra-dev-dependencies': devDependencies + .sortedBy((e) => e.name) + .map((package) => package.toMustache(versions, flameVersion)) + .toList(), + }; + + final files = await generator.generate(target, vars: variables); + final canHaveTests = devDependencies.contains(Package.flameTest); + + if (!canHaveTests) { + progress.update('Removing tests'); + processResult = await context.run( + 'rm', + '-rf test'.split(' '), + workingDirectory: actualDir, + ); + if (processResult.exitCode > ExitCode.success.code) { + return code = processResult.exitCode; + } + } + + progress.update('Removing tests'); + processResult = await context.run( + 'flutter', + 'pub get'.split(' '), + workingDirectory: actualDir, + ); + + if (processResult.exitCode > ExitCode.success.code) { + return code = processResult.exitCode; + } + + progress + ..complete('Updated ${files.length} files on top of flutter create.') + ..complete('Your new Flame project was successfully created!'); + + return code; + } catch (_) { + if (processResult != null) { + progress.fail(processResult.stderr.toString()); + code = processResult.exitCode; + } else { + progress.fail(); + } + + rethrow; + } } diff --git a/lib/commands/ignite_command.dart b/lib/commands/ignite_command.dart new file mode 100644 index 0000000..9638004 --- /dev/null +++ b/lib/commands/ignite_command.dart @@ -0,0 +1,13 @@ +import 'dart:async'; + +import 'package:args/command_runner.dart'; +import 'package:ignite_cli/ignite_context.dart'; + +abstract class IgniteCommand extends Command { + final IgniteContext context; + + IgniteCommand(this.context); + + @override + Future run(); +} diff --git a/lib/commands/version_command.dart b/lib/commands/version_command.dart index 3d0a6e9..b4f521b 100644 --- a/lib/commands/version_command.dart +++ b/lib/commands/version_command.dart @@ -1,11 +1,31 @@ +import 'dart:math'; + +import 'package:ignite_cli/ignite_context.dart'; import 'package:ignite_cli/version.g.dart'; -import 'package:process_run/process_run.dart'; - -Future versionCommand() async { - print(r'$ ignite --version:'); - print(igniteVersion); - print(''); - await runExecutableArguments('dart', ['--version'], verbose: true); - print(''); - await runExecutableArguments('flutter', ['--version'], verbose: true); + +Future versionCommand(IgniteContext context) async { + context.logger.info('ignite --version: $igniteVersion\n'); + + final (dartProcess, flutterProcess) = await ( + context.run('dart', ['--version']), + context.run('flutter', ['--version']), + ).wait; + + if (dartProcess.stdout case final String out) { + context.logger.info(out); + } + + if (flutterProcess.stdout case final String out) { + context.logger.info(out); + } + + if (dartProcess.stderr case final String err when err.isNotEmpty) { + context.logger.err(err); + } + + if (flutterProcess.stderr case final String err when err.isNotEmpty) { + context.logger.err(err); + } + + return max(dartProcess.exitCode, flutterProcess.exitCode); } diff --git a/lib/flame_version_manager.dart b/lib/flame_version_manager.dart index f1d3783..4dcc7c4 100644 --- a/lib/flame_version_manager.dart +++ b/lib/flame_version_manager.dart @@ -3,12 +3,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; class FlameVersionManager { - static late FlameVersionManager singleton; - - static Future init() async { - singleton = await FlameVersionManager.fetch(); - } - final Map versions; const FlameVersionManager._(this.versions); diff --git a/lib/ignite_command_runner.dart b/lib/ignite_command_runner.dart new file mode 100644 index 0000000..c4c1cfd --- /dev/null +++ b/lib/ignite_command_runner.dart @@ -0,0 +1,63 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:ignite_cli/commands/create_command.dart'; +import 'package:ignite_cli/commands/version_command.dart'; +import 'package:ignite_cli/ignite_context.dart'; +import 'package:mason_logger/mason_logger.dart'; + +class IgniteCommandRunner extends CommandRunner { + late IgniteContext context; + + IgniteCommandRunner(this.context) : super('ignite', _description) { + addCommand(CreateCommand(context)); + argParser.addFlag('verbose'); + argParser.addFlag( + 'version', + help: 'Print the current version.', + negatable: false, + ); + } + + @override + void printUsage() => context.logger.info(usage); + + @override + Future run(Iterable args) async { + try { + final parsedArgs = parse(args); + + if (parsedArgs['verbose'] == true) { + context.logger.level = Level.verbose; + } + + if (parsedArgs['version'] == true) { + return await versionCommand(context); + } + + return await runCommand(parsedArgs) ?? ExitCode.success.code; + } on FormatException catch (exception) { + context.logger + ..err(exception.message) + ..info('') + ..info(usage); + return ExitCode.usage.code; + } on UsageException catch (exception) { + context.logger + ..err(exception.message) + ..info('') + ..info(exception.usage); + return ExitCode.usage.code; + } on ProcessException catch (error) { + context.logger.err(error.message); + return ExitCode.unavailable.code; + } on Exception catch (error) { + context.logger.err('$error'); + return ExitCode.software.code; + } + } + + static const _description = 'Ignite your projects with Flame; ' + 'a CLI scaffolding tool to create and setup your Flame projects.'; +} diff --git a/lib/ignite_context.dart b/lib/ignite_context.dart new file mode 100644 index 0000000..31a691d --- /dev/null +++ b/lib/ignite_context.dart @@ -0,0 +1,34 @@ +import 'dart:io'; + +import 'package:ignite_cli/flame_version_manager.dart'; +import 'package:mason/mason.dart'; + +class IgniteContext { + const IgniteContext({ + required this.logger, + required this.flameVersionManager, + }); + + final Logger logger; + final FlameVersionManager flameVersionManager; + + Future run( + String executable, + List arguments, { + String? workingDirectory, + }) { + return Process.run( + executable, + arguments, + workingDirectory: workingDirectory, + ); + } + + Future generatorFromBundle(MasonBundle bundle) { + return MasonGenerator.fromBundle(bundle); + } + + DirectoryGeneratorTarget createTarget(Directory directory) { + return DirectoryGeneratorTarget(directory); + } +} diff --git a/lib/main.dart b/lib/main.dart deleted file mode 100644 index de09018..0000000 --- a/lib/main.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:args/args.dart'; -import 'package:completion/completion.dart' as completion; -import 'package:ignite_cli/commands/create_command.dart'; -import 'package:ignite_cli/commands/version_command.dart'; -import 'package:ignite_cli/flame_version_manager.dart'; - -Future mainCommand(List args) async { - await FlameVersionManager.init(); - - final parser = ArgParser(); - parser.addFlag('help', abbr: 'h', help: 'Displays this message.'); - parser.addFlag('version', abbr: 'v', help: 'Shows relevant version info.'); - - final create = parser.addCommand('create'); - create.addOption( - 'interactive', - abbr: 'i', - help: 'Whether to run in interactive mode or not.', - allowed: ['true', 'false'], - defaultsTo: 'true', - ); - create.addOption( - 'name', - help: 'The name of your game (valid dart identifier).', - ); - create.addOption( - 'org', - help: 'The org name, in reverse domain notation ' - '(package name/bundle identifier).', - ); - create.addOption( - 'create-folder', - abbr: 'f', - help: 'If you want to create a new folder on the current location with ' - "the project name or if you are already on the new project's folder.", - allowed: ['true', 'false'], - ); - create.addOption( - 'template', - help: 'What Flame template you would like to use for your new project', - allowed: ['simple', 'example'], - ); - - final packages = FlameVersionManager.singleton.versions; - final flameVersions = packages[Package.flame]!; - create.addOption( - 'flame-version', - help: 'What Flame version you would like to use.', - allowed: flameVersions.versions, - ); - create.addMultiOption( - 'extra-packages', - help: 'Which packages to use', - allowed: packages.keys.map((e) => e.name).toList(), - ); - - final results = completion.tryArgsCompletion(args, parser); - if (results['help'] as bool) { - print(parser.usage); - print(''); - print('List of available commands:'); - print(''); - print('create:'); - print(' ${create.usage}'); - return; - } else if (results['version'] as bool) { - await versionCommand(); - return; - } - - final command = results.command; - if (command?.name == 'create') { - await createCommand(command!); - } else { - print('Invalid command. Please select an option, use --help for help.'); - } -} diff --git a/lib/utils.dart b/lib/utils.dart index 3ab2d06..afeaa25 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,10 +1,8 @@ import 'dart:io'; import 'package:args/args.dart'; -import 'package:charcode/ascii.dart' as ascii; -import 'package:io/ansi.dart' as ansi; +import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; -import 'package:prompts/prompts.dart' as prompts; String getBundledFile(String name) { final binFolder = p.dirname(p.fromUri(Platform.script)); @@ -16,44 +14,42 @@ String getString( String name, String message, { required bool isInteractive, + required Logger logger, String? desc, String? Function(String)? validate, }) { var value = results[name] as String?; if (!isInteractive) { if (value == null || value.isEmpty) { - print('Missing parameter $name is required.'); - exit(1); + logger.err('Missing parameter $name is required.'); + exit(ExitCode.usage.code); } final error = validate?.call(value); if (error != null) { - print('Invalid value $value provided: $error'); - exit(1); + logger.err('Invalid value $value provided: $error'); + exit(ExitCode.usage.code); } } - while (value == null || value.isEmpty) { + + String? validation; + + do { + desc = darkGray.wrap(desc); + var msg = message; + if (desc != null) { - stdout.write(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); + msg = '$msg ($desc)'; } - value = prompts.get( - message, - validate: (e) { - final error = validate?.call(e); - if (error != null) { - // clear the line - stdout.write('\n\r\u{1B}[K'); - stdout.write(ansi.red.wrap('$error\u{1B}[1A\r')); - return false; - } else { - stdout.write('\n\r\u{1B}[K'); - return true; - } - }, - ); - if (desc != null) { - stdout.write('\r\u{1B}[K'); + + if (validation != null) { + validation = logger.theme.warn(validation); + msg = '$message [$validation]'; // omit the description on errors } - } + + value = logger.prompt(msg); + validation = validate?.call(value); + } while (validation != null); + return value; } @@ -63,35 +59,33 @@ String getOption( String message, Map options, { required bool isInteractive, - String? desc, + required Logger logger, String? defaultsTo, Map fullOptions = const {}, }) { - var value = results[name] as String?; + var value = results[name]?.toString(); + if (!isInteractive) { if (value == null) { if (defaultsTo != null) { return defaultsTo; } else { - print('Missing parameter $name is required.'); - exit(1); + logger.err('Missing parameter $name is required.'); + exit(ExitCode.usage.code); } } } final fullValues = {...options, ...fullOptions}.values; if (value != null && !fullValues.contains(value)) { - print('Invalid value $value provided. Must be in: ${options.values}'); + logger.err('Invalid value $value provided. Must be in: ${options.values}'); value = null; } + while (value == null) { - if (desc != null) { - stdout.write(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); - } - value = options[prompts.choose(message, options.keys)]; - if (desc != null) { - stdout.write('\r\u{1B}[K'); - } + final option = logger.chooseOne(message, choices: options.keys.toList()); + value = options[option]; } + return value; } @@ -114,6 +108,7 @@ List getMultiOption( List options, { required bool isInteractive, required bool isRequired, + required Logger logger, List startingOptions = const [], String? desc, }) { @@ -123,104 +118,29 @@ List getMultiOption( if (startingOptions.isNotEmpty) { return startingOptions; } else { - print('Missing parameter $name is required.'); - exit(1); + logger.info('Missing parameter $name is required.'); + exit(ExitCode.usage.code); } } else { return value; } } + if (value.any((e) => !options.contains(e))) { - print('Invalid value $value provided. Must be in: $options'); + logger.info('Invalid value $value provided. Must be in: $options'); value = []; } + if (desc != null) { - stdout.write(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); - } - final selectedOptions = value.isEmpty ? startingOptions : value; - value = cbx(message, options, selectedOptions); - if (desc != null) { - stdout.write('\r\u{1B}[K'); + logger.info(darkGray.wrap('\n$desc\u{1B}[1A\r')); } - return value; -} -List cbx( - String message, - List keys, - List startingKeys, -) { - final selected = startingKeys; - var hereIdx = 0; - - var needsClear = false; - void writeIt() { - if (needsClear) { - for (var i = 0; i <= keys.length; i++) { - prompts.goUpOneLine(); - prompts.clearLine(); - } - } else { - needsClear = true; - } - print(message); - keys.asMap().forEach((index, option) { - final isSelected = selected.contains(option); - final isHere = index == hereIdx; - final text = ' ${isSelected ? '♦' : '♢'} $option'; - final color = isHere ? ansi.cyan : ansi.darkGray; - print(color.wrap(text)); - }); - } + value = logger.chooseAny(message, choices: options); - final oldEchoMode = stdin.echoMode; - final oldLineMode = stdin.lineMode; - while (true) { - int ch; - writeIt(); - - try { - stdin.lineMode = stdin.echoMode = false; - ch = stdin.readByteSync(); - - if (ch == ascii.$esc) { - ch = stdin.readByteSync(); - if (ch == ascii.$lbracket) { - ch = stdin.readByteSync(); - if (ch == ascii.$A) { - // Up key - hereIdx--; - if (hereIdx < 0) { - hereIdx = keys.length - 1; - } - writeIt(); - } else if (ch == ascii.$B) { - // Down key - hereIdx++; - if (hereIdx >= keys.length) { - hereIdx = 0; - } - writeIt(); - } - } - } else if (ch == ascii.$lf) { - // Enter key pressed - submit - return selected; - } else if (ch == ascii.$space) { - // Space key pressed - selected/unselect - final key = keys[hereIdx]; - if (selected.contains(key)) { - selected.remove(key); - } else { - selected.add(key); - } - writeIt(); - } - } finally { - stdin.lineMode = oldLineMode; - stdin.echoMode = oldEchoMode; - } + if (desc != null) { + logger.info('\r\u{1B}[K'); } + return value; } extension SortedBy on Iterable { diff --git a/pubspec.lock b/pubspec.lock index a3d104d..92b89f2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,14 +49,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" - charcode: - dependency: "direct main" - description: - name: charcode - sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a - url: "https://pub.dev" - source: hosted - version: "1.4.0" checked_yaml: dependency: transitive description: @@ -97,14 +89,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" - completion: - dependency: "direct main" - description: - name: completion - sha256: f11b7a628e6c42b9edc9b0bc3aa490e2d930397546d2f794e8e1325909d11c60 - url: "https://pub.dev" - source: hosted - version: "1.0.1" convert: dependency: transitive description: @@ -234,7 +218,7 @@ packages: source: hosted version: "4.1.2" io: - dependency: "direct main" + dependency: transitive description: name: io sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b @@ -306,7 +290,7 @@ packages: source: hosted version: "0.1.2" mason_logger: - dependency: transitive + dependency: "direct main" description: name: mason_logger sha256: "6d5a989ff41157915cb5162ed6e41196d5e31b070d2f86e1c2edf216996a158c" @@ -337,6 +321,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" mustache_template: dependency: transitive description: @@ -401,22 +393,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" - process_run: - dependency: "direct main" - description: - name: process_run - sha256: "6ec839cdd3e6de4685318e7686cd4abb523c3d3a55af0e8d32a12ae19bc66622" - url: "https://pub.dev" - source: hosted - version: "1.2.4" - prompts: - dependency: "direct main" - description: - name: prompts - sha256: "3773b845e85a849f01e793c4fc18a45d52d7783b4cb6c0569fad19f9d0a774a1" - url: "https://pub.dev" - source: hosted - version: "2.0.0" pub_semver: dependency: transitive description: @@ -513,14 +489,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" - url: "https://pub.dev" - source: hosted - version: "3.3.1" term_glyph: dependency: transitive description: @@ -618,7 +586,7 @@ packages: source: hosted version: "5.12.0" yaml: - dependency: "direct main" + dependency: transitive description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce diff --git a/pubspec.yaml b/pubspec.yaml index 0a03079..0af924c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,19 +12,15 @@ environment: dependencies: args: ^2.7.0 - charcode: ^1.4.0 - completion: ^1.0.1 dartlin: ^0.6.3 http: ^1.3.0 - io: ^1.0.5 mason: ^0.1.1 + mason_logger: ^0.3.3 path: ^1.9.1 - process_run: ^1.2.4 - prompts: ^2.0.0 - yaml: ^3.1.3 dev_dependencies: dartdoc: ^8.0.2 flame_lint: ^1.3.0 mason_cli: ^0.1.2 + mocktail: ^1.0.4 test: ^1.24.9 diff --git a/test/ignite_command_runner_test.dart b/test/ignite_command_runner_test.dart new file mode 100644 index 0000000..6cb751f --- /dev/null +++ b/test/ignite_command_runner_test.dart @@ -0,0 +1,72 @@ +import 'dart:io'; + +import 'package:ignite_cli/flame_version_manager.dart'; +import 'package:ignite_cli/ignite_command_runner.dart'; +import 'package:ignite_cli/ignite_context.dart'; +import 'package:ignite_cli/version.g.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class _MockLogger extends Mock implements Logger {} + +class _MockFlameVersionManager extends Mock implements FlameVersionManager {} + +class _MockStdin extends Mock implements Stdin {} + +class _MockStdout extends Mock implements Stdout {} + +class _MockIgniteContext extends Mock implements IgniteContext {} + +void main() { + group('IgniteCommandRunner', () { + late Logger logger; + late IgniteCommandRunner commandRunner; + late FlameVersionManager flameVersionManager; + late Stdin stdin; + late Stdout stdout; // ignore: close_sinks + late IgniteContext context; + + setUp(() { + logger = _MockLogger(); + flameVersionManager = _MockFlameVersionManager(); + stdin = _MockStdin(); + stdout = _MockStdout(); + context = _MockIgniteContext(); + when(() => context.logger).thenReturn(logger); + when(() => context.flameVersionManager).thenReturn(flameVersionManager); + + when( + () => context.run( + any(), + any(), + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async => ProcessResult(0, 0, stdin, stdout)); + + when(() => flameVersionManager.versions).thenReturn({ + Package.flame: const Versions(['1.28.1']), + }); + + commandRunner = IgniteCommandRunner(context); + }); + + test('exits with code 0 when [version] is called', () async { + final exitCode = await commandRunner.run(['--version']); + expect(exitCode, equals(ExitCode.success.code)); + + verify(() => logger.info('ignite --version: $igniteVersion\n')); + verify(() => context.run('flutter', ['--version'])).called(1); + verify(() => context.run('dart', ['--version'])).called(1); + verifyNever(() => logger.err(any())); + }); + + test('exits with code 0 when [verbose] is set', () async { + final exitCode = await commandRunner.run(['--verbose']); + expect(exitCode, equals(ExitCode.success.code)); + + verify(() => logger.level = Level.verbose).called(1); + verifyNever(() => logger.err(any())); + }); + }); +}