Repository: VeryGoodOpenSource/very_good_cli (dart)

Issue:
feat(create)! subcommand: Very Good Flutter Plugin Usage
Split the create command to use this subcommand: 

```sh
# Very Good Flutter Plugin Usage
very_good create flutter_plugin <project-name> <options>

options:
    --org-name
    --description
    --platforms
```

Pull request description:
<!--
  Thanks for contributing!

  Provide a description of your changes below and a general summary in the title

  Please look at the following checklist to ensure that your PR can be accepted quickly:
-->

## Description

closes #587 

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore

Patch:
diff --git a/lib/src/commands/create/commands/commands.dart b/lib/src/commands/create/commands/commands.dart
index 2245b0c08..b391a421e 100644
--- a/lib/src/commands/create/commands/commands.dart
+++ b/lib/src/commands/create/commands/commands.dart
@@ -4,4 +4,5 @@ export 'dart_package.dart';
 export 'docs_site.dart';
 export 'flutter_app.dart';
 export 'flutter_package.dart';
+export 'flutter_plugin.dart';
 export 'legacy.dart';
diff --git a/lib/src/commands/create/commands/flutter_plugin.dart b/lib/src/commands/create/commands/flutter_plugin.dart
new file mode 100644
index 000000000..a4e52eba4
--- /dev/null
+++ b/lib/src/commands/create/commands/flutter_plugin.dart
@@ -0,0 +1,60 @@
+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_flutter_plugin_command}
+/// A [CreateSubCommand] for creating Flutter plugins.
+/// {@endtemplate}
+class CreateFlutterPlugin extends CreateSubCommand with Publishable {
+  /// {@macro very_good_create_flutter_plugin_command}
+  CreateFlutterPlugin({
+    required Analytics analytics,
+    required Logger logger,
+    required MasonGeneratorFromBundle? generatorFromBundle,
+    required MasonGeneratorFromBrick? generatorFromBrick,
+  }) : super(
+          analytics: analytics,
+          logger: logger,
+          generatorFromBundle: generatorFromBundle,
+          generatorFromBrick: generatorFromBrick,
+        ) {
+    argParser.addMultiOption(
+      'platforms',
+      help: 'The platforms supported by the plugin. By default, all platforms '
+          'are enabled. Example: --platforms=android,ios',
+      defaultsTo: ['android', 'ios', 'web', 'linux', 'macos', 'windows'],
+      allowed: ['android', 'ios', 'web', 'linux', 'macos', 'windows'],
+      allowedHelp: {
+        'android': 'The plugin supports the Android platform.',
+        'ios': 'The plugin supports the iOS platform.',
+        'web': 'The plugin supports the Web platform.',
+        'linux': 'The plugin supports the Linux platform.',
+        'macos': 'The plugin supports the macOS platform.',
+        'windows': 'The plugin supports the Windows platform.',
+      },
+    );
+  }
+
+  @override
+  String get name => 'flutter_plugin';
+
+  @override
+  String get description =>
+      'Creates a new very good federated Flutter plugin in the specified '
+      'directory.';
+
+  @override
+  Template get template => FlutterPluginTemplate();
+
+  @override
+  Map<String, dynamic> getTemplateVars() {
+    final vars = super.getTemplateVars();
+
+    final platforms = argResults['platforms'] as List<String>;
+
+    vars['platforms'] = platforms;
+
+    return vars;
+  }
+}
diff --git a/lib/src/commands/create/create.dart b/lib/src/commands/create/create.dart
index 37e3e7e87..689cf447a 100644
--- a/lib/src/commands/create/create.dart
+++ b/lib/src/commands/create/create.dart
@@ -78,6 +78,16 @@ class CreateCommand extends Command<int> {
         generatorFromBrick: generatorFromBrick,
       ),
     );
+
+    // very_good create flutter_plugin <args>
+    addSubcommand(
+      CreateFlutterPlugin(
+        analytics: analytics,
+        logger: logger,
+        generatorFromBundle: generatorFromBundle,
+        generatorFromBrick: generatorFromBrick,
+      ),
+    );
   }
 
   @override


Test patch:
diff --git a/test/src/commands/create/commands/flutter_plugin_test.dart b/test/src/commands/create/commands/flutter_plugin_test.dart
new file mode 100644
index 000000000..2541a9ee4
--- /dev/null
+++ b/test/src/commands/create/commands/flutter_plugin_test.dart
@@ -0,0 +1,228 @@
+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 federated Flutter plugin in the specified directory.
+
+Usage: very_good create flutter_plugin <project-name> [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.
+    --platforms                  The platforms supported by the plugin. By default, all platforms are enabled. Example: --platforms=android,ios
+
+          [android] (default)    The plugin supports the Android platform.
+          [ios] (default)        The plugin supports the iOS platform.
+          [linux] (default)      The plugin supports the Linux platform.
+          [macos] (default)      The plugin supports the macOS platform.
+          [web] (default)        The plugin supports the Web platform.
+          [windows] (default)    The plugin supports the Windows platform.
+
+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 = CreateFlutterPlugin(
+        analytics: analytics,
+        logger: logger,
+        generatorFromBundle: null,
+        generatorFromBrick: null,
+      );
+      expect(command.name, equals('flutter_plugin'));
+      expect(
+        command.description,
+        equals(
+          'Creates a new very good federated Flutter plugin in the specified '
+          'directory.',
+        ),
+      );
+      expect(command.logger, equals(logger));
+      expect(command, isA<Publishable>());
+      expect(command.argParser.options, contains('platforms'));
+    });
+  });
+
+  group('create flutter_plugin', () {
+    test(
+      'help',
+      withRunner((commandRunner, logger, pubUpdater, printLogs) async {
+        final result =
+            await commandRunner.run(['create', 'flutter_plugin', '--help']);
+        expect(printLogs, equals(expectedUsage));
+        expect(result, equals(ExitCode.success.code));
+
+        printLogs.clear();
+
+        final resultAbbr =
+            await commandRunner.run(['create', 'flutter_plugin', '-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_plugin', 'pubspec.yaml'))
+            ..createSync(recursive: true)
+            ..writeAsStringSync(pubspec);
+          return generatedFiles;
+        });
+      });
+
+      test('creates a flutter plugin', () async {
+        final tempDir = Directory.systemTemp.createTempSync();
+        addTearDown(() => tempDir.deleteSync(recursive: true));
+        final argResults = MockArgResults();
+        final command = CreateFlutterPlugin(
+          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_plugin']);
+        when(() => argResults['platforms'] as List<String>)
+            .thenReturn(['android', 'ios', 'windows']);
+
+        final result = await command.run();
+
+        expect(command.template.name, 'flutter_plugin');
+        expect(result, equals(ExitCode.success.code));
+
+        verify(() => logger.progress('Bootstrapping')).called(1);
+        verify(
+          () => hooks.preGen(
+            vars: <String, dynamic>{
+              'project_name': 'my_plugin',
+              'description': '',
+              'publishable': false,
+              'platforms': ['android', 'ios', 'windows'],
+            },
+            onVarsChanged: any(named: 'onVarsChanged'),
+          ),
+        );
+        verify(
+          () => generator.generate(
+            any(),
+            vars: <String, dynamic>{
+              'project_name': 'my_plugin',
+              'description': '',
+              'publishable': false,
+              'platforms': ['android', 'ios', 'windows'],
+            },
+            logger: logger,
+          ),
+        ).called(1);
+        verify(
+          () => logger.info('Created a Very Good Flutter Plugin! 🦄'),
+        ).called(1);
+      });
+    });
+  });
+}
diff --git a/test/src/commands/create/commands/legacy_test.dart b/test/src/commands/create/commands/legacy_test.dart
index 175aaa7d7..cf25f086a 100644
--- a/test/src/commands/create/commands/legacy_test.dart
+++ b/test/src/commands/create/commands/legacy_test.dart
@@ -28,6 +28,7 @@ Available subcommands:
   docs_site         Creates a new very good docs site in the specified directory.
   flutter_app       Creates a new very good Flutter app in the specified directory.
   flutter_package   Creates a new very good Flutter package in the specified directory.
+  flutter_plugin    Creates a new very good federated Flutter plugin in the specified directory.
 
 Run "very_good help" to see global options.'''
 ];
diff --git a/test/src/commands/create/create_test.dart b/test/src/commands/create/create_test.dart
index 03ffee534..2fa248707 100644
--- a/test/src/commands/create/create_test.dart
+++ b/test/src/commands/create/create_test.dart
@@ -34,6 +34,7 @@ Available subcommands:
   docs_site         Creates a new very good docs site in the specified directory.
   flutter_app       Creates a new very good Flutter app in the specified directory.
   flutter_package   Creates a new very good Flutter package in the specified directory.
+  flutter_plugin    Creates a new very good federated Flutter plugin in the specified directory.
 
 Run "very_good help" to see global options.'''
 ];