LCOV - code coverage report
Current view: top level - src/commands/create/commands - create_subcommand.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 94 94 100.0 %
Date: 2023-11-15 10:29:52 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:async';
       2             : import 'dart:io';
       3             : 
       4             : import 'package:args/args.dart';
       5             : import 'package:args/command_runner.dart';
       6             : import 'package:mason/mason.dart';
       7             : import 'package:meta/meta.dart';
       8             : import 'package:path/path.dart' as path;
       9             : import 'package:very_good_cli/src/commands/commands.dart';
      10             : import 'package:very_good_cli/src/commands/create/templates/templates.dart';
      11             : 
      12             : // A valid Dart identifier that can be used for a package, i.e. no
      13             : // capital letters.
      14             : // https://dart.dev/guides/language/language-tour#important-concepts
      15          24 : final RegExp _identifierRegExp = RegExp('[a-z_][a-z0-9_]*');
      16          12 : final RegExp _orgNameRegExp = RegExp(r'^[a-zA-Z][\w-]*(\.[a-zA-Z][\w-]*)+$');
      17             : 
      18             : const _defaultOrgName = 'com.example.verygoodcore';
      19             : const _defaultDescription = 'A Very Good Project created by Very Good CLI.';
      20             : 
      21             : /// A method which returns a [Future<MasonGenerator>] given a [MasonBundle].
      22             : typedef MasonGeneratorFromBundle = Future<MasonGenerator> Function(MasonBundle);
      23             : 
      24             : /// A method which returns a [Future<MasonGenerator>] given a [Brick].
      25             : typedef MasonGeneratorFromBrick = Future<MasonGenerator> Function(Brick);
      26             : 
      27             : /// {@template create_subcommand}
      28             : /// Generic class for sub commands of [CreateCommand].
      29             : /// {@endtemplate}
      30             : ///
      31             : /// It contains the common logic for all sub commands of [CreateCommand],
      32             : /// including the [run] and [runCreate] routines.
      33             : ///
      34             : /// By default, adds the following arguments to the [argParser]:
      35             : /// - 'output-directory': the output directory
      36             : /// - 'description': the description of the project
      37             : ///
      38             : /// Sub classes must implement [name], [description] and [template].
      39             : ///
      40             : /// For sub commands with multiple templates, sub classes must mix with
      41             : /// [MultiTemplates].
      42             : ///
      43             : /// For sub commands that receive an org name, sub classes must mix with
      44             : /// [OrgName].
      45             : ///
      46             : /// For sub commands that receive a publishable flag, sub classes must mix with
      47             : /// [Publishable].
      48             : abstract class CreateSubCommand extends Command<int> {
      49             :   /// {@macro create_subcommand}
      50          16 :   CreateSubCommand({
      51             :     required this.logger,
      52             :     @visibleForTesting required MasonGeneratorFromBundle? generatorFromBundle,
      53             :     @visibleForTesting required MasonGeneratorFromBrick? generatorFromBrick,
      54             :   })  : _generatorFromBundle = generatorFromBundle ?? MasonGenerator.fromBundle,
      55             :         _generatorFromBrick = generatorFromBrick ?? MasonGenerator.fromBrick {
      56          16 :     argParser
      57          16 :       ..addOption(
      58             :         'output-directory',
      59             :         abbr: 'o',
      60             :         help: 'The desired output directory when creating a new project.',
      61             :       )
      62          16 :       ..addOption(
      63             :         'description',
      64             :         help: 'The description for this new project.',
      65          16 :         aliases: ['desc'],
      66             :         defaultsTo: _defaultDescription,
      67             :       );
      68             : 
      69             :     // Add the templates arg if the command has multiple templates.
      70          16 :     if (this is MultiTemplates) {
      71             :       final multiTemplates = this as MultiTemplates;
      72          16 :       final defaultTemplateName = multiTemplates.defaultTemplateName;
      73          16 :       final templates = multiTemplates.templates;
      74             : 
      75          32 :       argParser.addOption(
      76             :         'template',
      77             :         abbr: 't',
      78             :         help: 'The template used to generate this new project.',
      79             :         defaultsTo: defaultTemplateName,
      80          64 :         allowed: templates.map((element) => element.name).toList(),
      81          16 :         allowedHelp: templates.fold<Map<String, String>>(
      82          16 :           {},
      83          32 :           (previousValue, element) => {
      84             :             ...previousValue,
      85          48 :             element.name: element.help,
      86             :           },
      87             :         ),
      88             :       );
      89             :     }
      90             : 
      91          16 :     if (this is OrgName) {
      92          32 :       argParser.addOption(
      93             :         'org-name',
      94             :         help: 'The organization for this new project.',
      95             :         defaultsTo: _defaultOrgName,
      96          16 :         aliases: ['org'],
      97             :       );
      98             :     }
      99             : 
     100          16 :     if (this is Publishable) {
     101          32 :       argParser.addFlag(
     102             :         'publishable',
     103             :         negatable: false,
     104             :         help: 'Whether the generated project is intended to be published.',
     105             :       );
     106             :     }
     107             :   }
     108             : 
     109             :   /// The logger user to notify the user of the command's progress.
     110             :   final Logger logger;
     111             :   final MasonGeneratorFromBundle _generatorFromBundle;
     112             :   final MasonGeneratorFromBrick _generatorFromBrick;
     113             : 
     114             :   /// [ArgResults] which can be overridden for testing.
     115             :   @visibleForTesting
     116             :   ArgResults? argResultOverrides;
     117             : 
     118             :   /// Gets the output [Directory].
     119           8 :   Directory get outputDirectory {
     120          16 :     final directory = argResults['output-directory'] as String? ?? '.';
     121           8 :     return Directory(directory);
     122             :   }
     123             : 
     124             :   /// Gets the project name.
     125           8 :   String get projectName {
     126          16 :     final args = argResults.rest;
     127           8 :     _validateProjectName(args);
     128           8 :     return args.first;
     129             :   }
     130             : 
     131             :   /// Gets the description for the project.
     132          24 :   String get projectDescription => argResults['description'] as String? ?? '';
     133             : 
     134             :   /// Should return the desired template to be created during a command run.
     135             :   ///
     136             :   /// For sub commands with multiple templates, see [MultiTemplates].
     137             :   Template get template;
     138             : 
     139           8 :   @override
     140          16 :   String get invocation => 'very_good create $name <project-name> [arguments]';
     141             : 
     142           8 :   @override
     143           9 :   ArgResults get argResults => argResultOverrides ?? super.argResults!;
     144             : 
     145           8 :   bool _isValidPackageName(String name) {
     146          16 :     final match = _identifierRegExp.matchAsPrefix(name);
     147          24 :     return match != null && match.end == name.length;
     148             :   }
     149             : 
     150           8 :   void _validateProjectName(List<String> args) {
     151          24 :     logger.detail('Validating project name; args: $args');
     152             : 
     153           8 :     if (args.isEmpty) {
     154           1 :       usageException('No option specified for the project name.');
     155             :     }
     156             : 
     157          16 :     if (args.length > 1) {
     158           1 :       usageException('Multiple project names specified.');
     159             :     }
     160             : 
     161           8 :     final name = args.first;
     162           8 :     final isValidProjectName = _isValidPackageName(name);
     163             :     if (!isValidProjectName) {
     164           2 :       usageException(
     165             :         '"$name" is not a valid package name.\n\n'
     166             :         'See https://dart.dev/tools/pub/pubspec#name for more information.',
     167             :       );
     168             :     }
     169             :   }
     170             : 
     171           8 :   Future<MasonGenerator> _getGeneratorForTemplate() async {
     172             :     try {
     173           8 :       final brick = Brick.version(
     174          24 :         name: template.bundle.name,
     175          32 :         version: '^${template.bundle.version}',
     176             :       );
     177          16 :       logger.detail(
     178          32 :         '''Building generator from brick: ${brick.name} ${brick.location.version}''',
     179             :       );
     180          16 :       return await _generatorFromBrick(brick);
     181             :     } catch (_) {
     182           3 :       logger.detail('Building generator from brick failed: $_');
     183             :     }
     184           2 :     logger.detail(
     185           7 :       '''Building generator from bundle ${template.bundle.name} ${template.bundle.version}''',
     186             :     );
     187           4 :     return _generatorFromBundle(template.bundle);
     188             :   }
     189             : 
     190           8 :   @override
     191             :   Future<int> run() async {
     192           8 :     final template = this.template;
     193           8 :     final generator = await _getGeneratorForTemplate();
     194           8 :     final result = await runCreate(generator, template);
     195             : 
     196             :     return result;
     197             :   }
     198             : 
     199             :   /// Invoked by [run] to create the project, contains the logic for using
     200             :   /// the template vars obtained by [getTemplateVars] to generate the project
     201             :   /// from the [generator] and [template].
     202           8 :   Future<int> runCreate(MasonGenerator generator, Template template) async {
     203           8 :     var vars = getTemplateVars();
     204             : 
     205          16 :     final generateProgress = logger.progress('Bootstrapping');
     206          16 :     final target = DirectoryGeneratorTarget(outputDirectory);
     207             : 
     208          16 :     await generator.hooks.preGen(vars: vars, onVarsChanged: (v) => vars = v);
     209          16 :     final files = await generator.generate(target, vars: vars, logger: logger);
     210          24 :     generateProgress.complete('Generated ${files.length} file(s)');
     211             : 
     212           8 :     await template.onGenerateComplete(
     213           8 :       logger,
     214          40 :       Directory(path.join(target.dir.path, projectName)),
     215             :     );
     216             : 
     217           8 :     return ExitCode.success.code;
     218             :   }
     219             : 
     220             :   /// Responsible for returns the template parameters to be passed to the
     221             :   /// template brick.
     222             :   ///
     223             :   /// Override if the create sub command requires additional template
     224             :   /// parameters.
     225             :   ///
     226             :   /// For subcommands that mix with [OrgName], it includes 'org_name'.
     227             :   /// For subcommands that mix with [Publishable], it includes 'publishable'.
     228           8 :   @mustCallSuper
     229             :   Map<String, dynamic> getTemplateVars() {
     230           8 :     final projectName = this.projectName;
     231           8 :     final projectDescription = this.projectDescription;
     232             : 
     233           8 :     return <String, dynamic>{
     234           8 :       'project_name': projectName,
     235           8 :       'description': projectDescription,
     236          16 :       if (this is OrgName) 'org_name': (this as OrgName).orgName,
     237          18 :       if (this is Publishable) 'publishable': (this as Publishable).publishable,
     238             :     };
     239             :   }
     240             : }
     241             : 
     242             : /// Mixin for [CreateSubCommand] subclasses that receives the org name
     243             : /// parameter.
     244             : ///
     245             : /// Takes care of parsing from [argResults] and validating the org name.
     246             : mixin OrgName on CreateSubCommand {
     247             :   /// Gets the organization name.
     248           4 :   String get orgName {
     249           8 :     final orgName = argResults['org-name'] as String? ?? _defaultOrgName;
     250           4 :     _validateOrgName(orgName);
     251             :     return orgName;
     252             :   }
     253             : 
     254           4 :   void _validateOrgName(String name) {
     255          12 :     logger.detail('Validating org name; $name');
     256           4 :     final isValidOrgName = _isValidOrgName(name);
     257             :     if (!isValidOrgName) {
     258           2 :       usageException(
     259             :         '"$name" is not a valid org name.\n\n'
     260             :         'A valid org name has at least 2 parts separated by "."\n'
     261             :         'Each part must start with a letter and only include '
     262             :         'alphanumeric characters (A-Z, a-z, 0-9), underscores (_), '
     263             :         'and hyphens (-)\n'
     264             :         '(ex. very.good.org)',
     265             :       );
     266             :     }
     267             :   }
     268             : 
     269           4 :   bool _isValidOrgName(String name) {
     270           8 :     return _orgNameRegExp.hasMatch(name);
     271             :   }
     272             : }
     273             : 
     274             : /// Mixin for [CreateSubCommand] subclasses that receives multiple templates.
     275             : ///
     276             : /// Subcommands that mix with this mixin should override [templates].
     277             : ///
     278             : /// Takes care of parsing the desired template from [argResults] and
     279             : /// validating the org name.
     280             : mixin MultiTemplates on CreateSubCommand {
     281             :   /// Gets the desired template to be created during a command run when the
     282             :   /// template argument is not provided.
     283             :   ///
     284             :   /// Defaults to the first template in [templates].
     285          64 :   String get defaultTemplateName => templates.first.name;
     286             : 
     287             :   /// Gets all the templates to be created during a command run.
     288             :   List<Template> get templates;
     289             : 
     290           2 :   @nonVirtual
     291             :   @override
     292             :   Template get template {
     293             :     final templateName =
     294           4 :         argResults['template'] as String? ?? defaultTemplateName;
     295             : 
     296           4 :     return templates.firstWhere(
     297           6 :       (element) => element.name == templateName,
     298             :     );
     299             :   }
     300             : }
     301             : 
     302             : /// Mixin for [CreateSubCommand] subclasses that receives the publishable
     303             : /// flag.
     304             : ///
     305             : /// Takes care of parsing it from [argResults] and pass it
     306             : /// to the brick generator.
     307             : mixin Publishable on CreateSubCommand {
     308             :   /// Gets the publishable flag.
     309          15 :   bool get publishable => argResults['publishable'] as bool? ?? false;
     310             : }

Generated by: LCOV version 1.16