Repository: VeryGoodOpenSource/very_good_cli (dart)

Issue:
feat: Add to the create command --org flag to name the package 
**Description**
To avoid changing the package name and package identifier after the project is created, example:
`very_good create --org com.yourdomain appname`

Pull request:
<!--
  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
Modified CLI to support additional argument for a custom organization name.

## Type of Change

- [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.dart b/lib/src/commands/create.dart
index 57dbd1623..24adf3c64 100644
--- a/lib/src/commands/create.dart
+++ b/lib/src/commands/create.dart
@@ -12,10 +12,14 @@ import 'package:very_good_cli/src/command_runner.dart';
 import 'package:very_good_cli/src/flutter_cli.dart';
 import 'package:very_good_cli/src/templates/very_good_core_bundle.dart';
 
+const _defaultOrgName = 'com.example.verygoodcore';
+
 // A valid Dart identifier that can be used for a package, i.e. no
 // capital letters.
 // https://dart.dev/guides/language/language-tour#important-concepts
 final RegExp _identifierRegExp = RegExp('[a-z_][a-z0-9_]*');
+final RegExp _orgNameRegExp =
+    RegExp(r'[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+');
 
 /// A method which returns a [Future<MasonGenerator>] given a [MasonBundle].
 typedef GeneratorBuilder = Future<MasonGenerator> Function(MasonBundle);
@@ -32,12 +36,18 @@ class CreateCommand extends Command<int> {
   })  : _analytics = analytics,
         _logger = logger ?? Logger(),
         _generator = generator ?? MasonGenerator.fromBundle {
-    argParser.addOption(
-      'project-name',
-      help: 'The project name for this new Flutter project. '
-          'This must be a valid dart package name.',
-      defaultsTo: null,
-    );
+    argParser
+      ..addOption(
+        'project-name',
+        help: 'The project name for this new Flutter project. '
+            'This must be a valid dart package name.',
+        defaultsTo: null,
+      )
+      ..addOption(
+        'org-name',
+        help: 'The organization for this new Flutter project.',
+        defaultsTo: 'com.example.verygoodcore',
+      );
   }
 
   final Analytics _analytics;
@@ -67,11 +77,12 @@ class CreateCommand extends Command<int> {
   Future<int> run() async {
     final outputDirectory = _outputDirectory;
     final projectName = _projectName;
+    final orgName = _orgName;
     final generateDone = _logger.progress('Bootstrapping');
     final generator = await _generator(veryGoodCoreBundle);
     final fileCount = await generator.generate(
       DirectoryGeneratorTarget(outputDirectory, _logger),
-      vars: {'project_name': projectName},
+      vars: {'project_name': projectName, 'org_name': orgName},
     );
     generateDone('Generated $fileCount file(s)');
 
@@ -126,6 +137,28 @@ class CreateCommand extends Command<int> {
     return projectName;
   }
 
+  /// Gets the organization name.
+  List<String> get _orgName {
+    if (_argResults['org-name'] == null) return _defaultOrgName.split('.');
+
+    final orgName = _argResults['org-name'] as String;
+    _validateOrgName(orgName);
+    return orgName.split('.');
+  }
+
+  void _validateOrgName(String name) {
+    final isValidOrgName = _isValidOrgName(name);
+    if (!isValidOrgName) {
+      throw UsageException(
+        '"$name" is not a valid org name.\n\n'
+        'A valid org name has 3 parts separated by "."'
+        'and only includes alphanumeric characters and underscores'
+        '(ex. very.good.org)',
+        usage,
+      );
+    }
+  }
+
   void _validateProjectName(String name) {
     final isValidProjectName = _isValidPackageName(name);
     if (!isValidProjectName) {
@@ -137,6 +170,11 @@ class CreateCommand extends Command<int> {
     }
   }
 
+  bool _isValidOrgName(String name) {
+    final match = _orgNameRegExp.matchAsPrefix(name);
+    return match != null && match.end == name.length;
+  }
+
   bool _isValidPackageName(String name) {
     final match = _identifierRegExp.matchAsPrefix(name);
     return match != null && match.end == name.length;


Test patch:
diff --git a/test/src/commands/create_test.dart b/test/src/commands/create_test.dart
index d3c10743d..cf43f538f 100644
--- a/test/src/commands/create_test.dart
+++ b/test/src/commands/create_test.dart
@@ -126,7 +126,10 @@ void main() {
               '.tmp',
             ),
           ),
-          vars: {'project_name': 'my_app'},
+          vars: {
+            'project_name': 'my_app',
+            'org_name': ['com', 'example', 'verygoodcore'],
+          },
         ),
       ).called(1);
       verify(
@@ -140,5 +143,101 @@ void main() {
         () => analytics.waitForLastPing(timeout: VeryGoodCommandRunner.timeout),
       ).called(1);
     });
+
+    group('org-name', () {
+      group('invalid --org-name', () {
+        test('no delimiters', () async {
+          const expectedErrorMessage = '"My App" is not a valid org name.\n\n'
+              'A valid org name has 3 parts separated by "."'
+              'and only includes alphanumeric characters and underscores'
+              '(ex. very.good.org)';
+          final result = await commandRunner.run(
+            ['create', '.', '--org-name', 'My App'],
+          );
+          expect(result, equals(ExitCode.usage.code));
+          verify(() => logger.err(expectedErrorMessage)).called(1);
+        });
+
+        test('more than 3 domains', () async {
+          const expectedErrorMessage =
+              '"very.bad.test.case" is not a valid org name.\n\n'
+              'A valid org name has 3 parts separated by "."'
+              'and only includes alphanumeric characters and underscores'
+              '(ex. very.good.org)';
+          final result = await commandRunner.run(
+            ['create', '.', '--org-name', 'very.bad.test.case'],
+          );
+          expect(result, equals(ExitCode.usage.code));
+          verify(() => logger.err(expectedErrorMessage)).called(1);
+        });
+
+        test('invalid characters present', () async {
+          const expectedErrorMessage =
+              '"very%.bad@.#test" is not a valid org name.\n\n'
+              'A valid org name has 3 parts separated by "."'
+              'and only includes alphanumeric characters and underscores'
+              '(ex. very.good.org)';
+          final result = await commandRunner.run(
+            ['create', '.', '--org-name', 'very%.bad@.#test'],
+          );
+          expect(result, equals(ExitCode.usage.code));
+          verify(() => logger.err(expectedErrorMessage)).called(1);
+        });
+      });
+
+      group('valid --org-name', () {
+        test('completes successfully with correct output', () async {
+          final argResults = MockArgResults();
+          final generator = MockMasonGenerator();
+          final command = CreateCommand(
+            analytics: analytics,
+            logger: logger,
+            generator: (_) async => generator,
+          )..argResultOverrides = argResults;
+          when(() => argResults['project-name']).thenReturn('my_app');
+          when(() => argResults['org-name']).thenReturn('very.good.ventures');
+          when(() => argResults.rest).thenReturn(['.tmp']);
+          when(() => generator.id).thenReturn('generator_id');
+          when(() => generator.description).thenReturn('generator description');
+          when(
+            () => generator.generate(any(), vars: any(named: 'vars')),
+          ).thenAnswer((_) async => 62);
+          final result = await command.run();
+          expect(result, equals(ExitCode.success.code));
+          verify(() => logger.progress('Bootstrapping')).called(1);
+          expect(progressLogs, equals(['Generated 62 file(s)']));
+          verify(
+            () => logger.progress('Running "flutter packages get" in .tmp'),
+          ).called(1);
+          verify(() => logger.alert('Created a Very Good App! 🦄')).called(1);
+          verify(
+            () => generator.generate(
+              any(
+                that: isA<DirectoryGeneratorTarget>().having(
+                  (g) => g.dir.path,
+                  'dir',
+                  '.tmp',
+                ),
+              ),
+              vars: {
+                'project_name': 'my_app',
+                'org_name': ['very', 'good', 'ventures'],
+              },
+            ),
+          ).called(1);
+          verify(
+            () => analytics.sendEvent(
+              'create',
+              'generator_id',
+              label: 'generator description',
+            ),
+          ).called(1);
+          verify(
+            () => analytics.waitForLastPing(
+                timeout: VeryGoodCommandRunner.timeout),
+          ).called(1);
+        });
+      });
+    });
   });
 }