diff --git a/.github/workflows/very_good_cli.yaml b/.github/workflows/very_good_cli.yaml index 36856a30..bb151647 100644 --- a/.github/workflows/very_good_cli.yaml +++ b/.github/workflows/very_good_cli.yaml @@ -55,6 +55,7 @@ jobs: - test/src/commands/test/e2e # E2E tests for the create command - test/src/commands/create/e2e/flutter_app/core_test.dart + - test/src/commands/create/e2e/dart_package/dart_pkg_test.dart # E2E tests for the legacy create command syntax - test/src/commands/create/e2e/legacy/core_test.dart diff --git a/lib/src/commands/create/commands/commands.dart b/lib/src/commands/create/commands/commands.dart index 608f7b6b..8a550abb 100644 --- a/lib/src/commands/create/commands/commands.dart +++ b/lib/src/commands/create/commands/commands.dart @@ -1,3 +1,4 @@ export 'create_subcommand.dart'; +export 'dart_package.dart'; export 'flutter_app.dart'; export 'legacy.dart'; diff --git a/lib/src/commands/create/commands/create_subcommand.dart b/lib/src/commands/create/commands/create_subcommand.dart index aee9e687..d0865a52 100644 --- a/lib/src/commands/create/commands/create_subcommand.dart +++ b/lib/src/commands/create/commands/create_subcommand.dart @@ -97,6 +97,14 @@ abstract class CreateSubCommand extends Command { aliases: ['org'], ); } + + if (this is Publishable) { + argParser.addFlag( + 'publishable', + negatable: false, + help: 'Whether the generated project is intended to be published.', + ); + } } final Analytics _analytics; @@ -236,6 +244,7 @@ abstract class CreateSubCommand extends Command { 'project_name': projectName, 'description': projectDescription, if (this is OrgName) 'org_name': (this as OrgName).orgName, + if (this is Publishable) 'publishable': (this as Publishable).publishable, }; } } @@ -298,3 +307,13 @@ mixin MultiTemplates on CreateSubCommand { ); } } + +/// Mixin for [CreateSubCommand] subclasses that receives the publishable +/// flag. +/// +/// Takes care of parsing it from [argResults] and pass it +/// to the brick generator. +mixin Publishable on CreateSubCommand { + /// Gets the publishable flag. + bool get publishable => argResults['publishable'] as bool? ?? false; +} diff --git a/lib/src/commands/create/commands/dart_package.dart b/lib/src/commands/create/commands/dart_package.dart new file mode 100644 index 00000000..39da542a --- /dev/null +++ b/lib/src/commands/create/commands/dart_package.dart @@ -0,0 +1,35 @@ +import 'package:mason_logger/mason_logger.dart'; +import 'package:usage/usage.dart'; +import 'package:very_good_cli/src/commands/commands.dart'; +import 'package:very_good_cli/src/commands/create/templates/templates.dart'; + +/// {@template very_good_create_dart_package_command} +/// A [CreateSubCommand] for creating Dart packages. +/// {@endtemplate} +class CreateDartPackage extends CreateSubCommand with Publishable { + /// {@macro very_good_create_dart_package_command} + CreateDartPackage({ + required Analytics analytics, + required Logger logger, + required MasonGeneratorFromBundle? generatorFromBundle, + required MasonGeneratorFromBrick? generatorFromBrick, + }) : super( + analytics: analytics, + logger: logger, + generatorFromBundle: generatorFromBundle, + generatorFromBrick: generatorFromBrick, + ); + + @override + String get name => 'dart_package'; + + @override + List get aliases => ['dart_pkg']; + + @override + String get description => + 'Creates a new very good Dart package in the specified directory.'; + + @override + Template get template => DartPkgTemplate(); +} diff --git a/lib/src/commands/create/create.dart b/lib/src/commands/create/create.dart index c3990bb6..ee742535 100644 --- a/lib/src/commands/create/create.dart +++ b/lib/src/commands/create/create.dart @@ -38,6 +38,16 @@ class CreateCommand extends Command { generatorFromBrick: generatorFromBrick, ), ); + + // very_good create dart_pkg + addSubcommand( + CreateDartPackage( + analytics: analytics, + logger: logger, + generatorFromBundle: generatorFromBundle, + generatorFromBrick: generatorFromBrick, + ), + ); } @override diff --git a/test/src/commands/create/commands/dart_package_test.dart b/test/src/commands/create/commands/dart_package_test.dart new file mode 100644 index 00000000..be6113d7 --- /dev/null +++ b/test/src/commands/create/commands/dart_package_test.dart @@ -0,0 +1,214 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:mason/mason.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; +import 'package:usage/usage.dart'; +import 'package:very_good_cli/src/commands/commands.dart'; + +import '../../../../helpers/helpers.dart'; + +class MockAnalytics extends Mock implements Analytics {} + +class MockLogger extends Mock implements Logger {} + +class MockMasonGenerator extends Mock implements MasonGenerator {} + +class MockGeneratorHooks extends Mock implements GeneratorHooks {} + +class MockArgResults extends Mock implements ArgResults {} + +class FakeLogger extends Fake implements Logger {} + +class FakeDirectoryGeneratorTarget extends Fake + implements DirectoryGeneratorTarget {} + +final expectedUsage = [ + ''' +Creates a new very good Dart package in the specified directory. + +Usage: very_good create dart_package [arguments] +-h, --help Print this usage information. +-o, --output-directory The desired output directory when creating a new project. + --description The description for this new project. + (defaults to "A Very Good Project created by Very Good CLI.") + --publishable Whether the generated project is intended to be published. + +Run "very_good help" to see global options.''', +]; + +const pubspec = ''' +name: example +environment: + sdk: ">=2.13.0 <3.0.0" +'''; + +void main() { + late Analytics analytics; + late Logger logger; + + setUpAll(() { + registerFallbackValue(FakeDirectoryGeneratorTarget()); + registerFallbackValue(FakeLogger()); + }); + + setUp(() { + analytics = MockAnalytics(); + when( + () => analytics.sendEvent(any(), any(), label: any(named: 'label')), + ).thenAnswer((_) async {}); + when( + () => analytics.waitForLastPing(timeout: any(named: 'timeout')), + ).thenAnswer((_) async {}); + + logger = MockLogger(); + + final progress = MockProgress(); + + when(() => logger.progress(any())).thenReturn(progress); + }); + + group('can be instantiated', () { + test('with default options', () { + final logger = Logger(); + final command = CreateDartPackage( + analytics: analytics, + logger: logger, + generatorFromBundle: null, + generatorFromBrick: null, + ); + expect(command.name, equals('dart_package')); + expect( + command.description, + equals( + 'Creates a new very good Dart package in the specified directory.', + ), + ); + expect(command.logger, equals(logger)); + expect(command, isA()); + }); + }); + + group('create dart_package', () { + test( + 'help', + withRunner((commandRunner, logger, pubUpdater, printLogs) async { + final result = + await commandRunner.run(['create', 'dart_package', '--help']); + expect(printLogs, equals(expectedUsage)); + expect(result, equals(ExitCode.success.code)); + + printLogs.clear(); + + final resultAbbr = + await commandRunner.run(['create', 'dart_pkg', '-h']); + expect(printLogs, equals(expectedUsage)); + expect(resultAbbr, equals(ExitCode.success.code)); + }), + ); + + group('running the command', () { + final generatedFiles = + List.filled(10, const GeneratedFile.created(path: '')); + + late GeneratorHooks hooks; + late MasonGenerator generator; + + setUp(() { + hooks = MockGeneratorHooks(); + generator = MockMasonGenerator(); + + when(() => generator.hooks).thenReturn(hooks); + when( + () => hooks.preGen( + vars: any(named: 'vars'), + onVarsChanged: any(named: 'onVarsChanged'), + ), + ).thenAnswer((_) async {}); + + when( + () => generator.generate( + any(), + vars: any(named: 'vars'), + logger: any(named: 'logger'), + ), + ).thenAnswer((_) async { + return generatedFiles; + }); + + when(() => generator.id).thenReturn('generator_id'); + when(() => generator.description).thenReturn('generator description'); + when(() => generator.hooks).thenReturn(hooks); + + when( + () => hooks.preGen( + vars: any(named: 'vars'), + onVarsChanged: any(named: 'onVarsChanged'), + ), + ).thenAnswer((_) async {}); + when( + () => generator.generate( + any(), + vars: any(named: 'vars'), + logger: any(named: 'logger'), + ), + ).thenAnswer((_) async { + final target = + _.positionalArguments.first as DirectoryGeneratorTarget; + File(path.join(target.dir.path, 'my_package', 'pubspec.yaml')) + ..createSync(recursive: true) + ..writeAsStringSync(pubspec); + return generatedFiles; + }); + }); + + test('creates dart package', () async { + final tempDir = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDir.deleteSync(recursive: true)); + final argResults = MockArgResults(); + final command = CreateDartPackage( + analytics: analytics, + logger: logger, + generatorFromBundle: (_) async => throw Exception('oops'), + generatorFromBrick: (_) async => generator, + )..argResultOverrides = argResults; + when(() => argResults['output-directory'] as String?) + .thenReturn(tempDir.path); + when(() => argResults.rest).thenReturn(['my_package']); + + final result = await command.run(); + + expect(command.template.name, 'dart_pkg'); + expect(result, equals(ExitCode.success.code)); + + verify(() => logger.progress('Bootstrapping')).called(1); + verify( + () => hooks.preGen( + vars: { + 'project_name': 'my_package', + 'description': '', + 'publishable': false, + }, + onVarsChanged: any(named: 'onVarsChanged'), + ), + ); + verify( + () => generator.generate( + any(), + vars: { + 'project_name': 'my_package', + 'description': '', + 'publishable': false, + }, + logger: logger, + ), + ).called(1); + verify( + () => logger.info('Created a Very Good Dart Package! 🦄'), + ).called(1); + }); + }); + }); +} diff --git a/test/src/commands/create/commands/legacy_test.dart b/test/src/commands/create/commands/legacy_test.dart index 9ac2cabd..cd3f7779 100644 --- a/test/src/commands/create/commands/legacy_test.dart +++ b/test/src/commands/create/commands/legacy_test.dart @@ -23,7 +23,8 @@ Usage: very_good create [arguments] -h, --help Print this usage information. Available subcommands: - flutter_app Creates a new very good Flutter app in the specified directory. + dart_package Creates a new very good Dart package in the specified directory. + flutter_app Creates a new very good Flutter app in the specified directory. Run "very_good help" to see global options.''' ]; diff --git a/test/src/commands/create/create_subcommand_test.dart b/test/src/commands/create/create_subcommand_test.dart index 273489bb..faa18053 100644 --- a/test/src/commands/create/create_subcommand_test.dart +++ b/test/src/commands/create/create_subcommand_test.dart @@ -71,6 +71,23 @@ class _TestCreateSubCommandWithOrgName extends _TestCreateSubCommand ); } +class _TestCreateSubCommandWithPublishable extends _TestCreateSubCommand + with Publishable { + _TestCreateSubCommandWithPublishable({ + required Template template, + required Analytics analytics, + required Logger logger, + required MasonGeneratorFromBundle? generatorFromBundle, + required MasonGeneratorFromBrick? generatorFromBrick, + }) : super( + template: template, + analytics: analytics, + logger: logger, + generatorFromBundle: generatorFromBundle, + generatorFromBrick: generatorFromBrick, + ); +} + class _TestCreateSubCommandMultiTemplate extends CreateSubCommand with MultiTemplates { _TestCreateSubCommandMultiTemplate({ @@ -1082,4 +1099,222 @@ Run "runner help" to see global options.'''; }); }); }); + + group('Publishable', () { + const expectedUsage = ''' +Usage: very_good create create_subcommand [arguments] +-h, --help Print this usage information. +-o, --output-directory The desired output directory when creating a new project. + --description The description for this new project. + (defaults to "A Very Good Project created by Very Good CLI.") + --publishable Whether the generated project is intended to be published. + +Run "runner help" to see global options.'''; + + late Template template; + late MockBundle bundle; + + setUp(() { + bundle = MockBundle(); + when(() => bundle.name).thenReturn('test'); + when(() => bundle.description).thenReturn('Test bundle'); + when(() => bundle.version).thenReturn(''); + template = MockTemplate(); + when(() => template.name).thenReturn('test'); + when(() => template.bundle).thenReturn(bundle); + when(() => template.onGenerateComplete(any(), any())).thenAnswer( + (_) async {}, + ); + when( + () => analytics.sendEvent(any(), any(), label: any(named: 'label')), + ).thenAnswer((_) async {}); + }); + + group('can be instantiated', () { + test('with default options', () { + final command = _TestCreateSubCommandWithPublishable( + template: template, + analytics: analytics, + logger: logger, + generatorFromBundle: null, + generatorFromBrick: null, + ); + + expect( + command.argParser.options['publishable'], + isA