Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
- test/commands/create/flame_game/flame_game_test.dart
- test/commands/create/flutter_package/flutter_pkg_test.dart
- test/commands/create/flutter_plugin/flutter_plugin_test.dart
- test/commands/create/very_good_config/very_good_config_test.dart

# E2E tests for the `packages check licenses` command
- test/commands/packages/check/licenses/licenses_allowed_test.dart
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
create:
description: A project configured via very_good.yaml.
publishable: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- not
- a
- map
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import 'package:mason/mason.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:universal_io/io.dart';

import '../../../../helpers/helpers.dart';

void main() {
group('very_good.yaml', () {
test(
'applies create defaults from very_good.yaml when flags are not passed',
timeout: const Timeout(Duration(minutes: 2)),
withRunner((commandRunner, logger, updater, logs, progressLogs) async {
final tempDirectory = Directory.systemTemp.createTempSync(
'very_good_config_create',
);
addTearDown(() => tempDirectory.deleteSync(recursive: true));

final fixture = Directory(
path.join(
Directory.current.path,
'test/commands/create/very_good_config/fixture',
),
);

await copyDirectory(fixture, tempDirectory);

final cwd = Directory.current;
Directory.current = tempDirectory;
addTearDown(() => Directory.current = cwd);

final result = await commandRunner.run([
'create',
'dart_package',
'very_good_dart',
]);
expect(result, equals(ExitCode.success.code));

final pubspec = File(
path.join(tempDirectory.path, 'very_good_dart', 'pubspec.yaml'),
);
expect(pubspec.existsSync(), isTrue);
expect(
pubspec.readAsStringSync(),
contains('A project configured via very_good.yaml.'),
);
}),
);

test(
'fails with config exit code when very_good.yaml is malformed',
timeout: const Timeout(Duration(minutes: 2)),
withRunner((commandRunner, logger, updater, logs, progressLogs) async {
final tempDirectory = Directory.systemTemp.createTempSync(
'very_good_config_create_malformed',
);
addTearDown(() => tempDirectory.deleteSync(recursive: true));

final fixture = Directory(
path.join(
Directory.current.path,
'test/commands/create/very_good_config/malformed_fixture',
),
);

await copyDirectory(fixture, tempDirectory);

final cwd = Directory.current;
Directory.current = tempDirectory;
addTearDown(() => Directory.current = cwd);

await expectLater(
commandRunner.run(['create', 'dart_package', 'very_good_dart']),
completion(equals(ExitCode.config.code)),
);
verify(
() => logger.err(
any(that: contains('Could not read `very_good.yaml`')),
),
).called(1);
}),
);
});
}
59 changes: 52 additions & 7 deletions lib/src/commands/create/commands/create_subcommand.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:very_good_cli/src/commands/commands.dart';
import 'package:very_good_cli/src/commands/create/templates/templates.dart';
import 'package:very_good_cli/src/very_good_config/very_good_config.dart';

// A valid Dart identifier that can be used for a package, i.e. no
// capital letters.
Expand Down Expand Up @@ -118,6 +119,14 @@ abstract class CreateSubCommand extends Command<int> {
final Logger logger;
final MasonGeneratorFromBundle _generatorFromBundle;

/// The resolved `very_good create` configuration read from the closest
/// `very_good.yaml` file.
///
/// Values declared here are used as defaults for any option that was not
/// explicitly passed on the command line. It defaults to an empty
/// configuration until [run] loads it.
VeryGoodCreateConfig createConfig = const VeryGoodCreateConfig();

/// [ArgResults] which can be overridden for testing.
@visibleForTesting
ArgResults? argResultOverrides;
Expand Down Expand Up @@ -174,7 +183,11 @@ abstract class CreateSubCommand extends Command<int> {
}

/// Gets the description for the project.
String get projectDescription => argResults['description'] as String? ?? '';
String get projectDescription => argResults.resolve<String>(
'description',
createConfig.description,
fallbackValue: '',
);

/// Should return the desired template to be created during a command run.
///
Expand All @@ -194,6 +207,10 @@ abstract class CreateSubCommand extends Command<int> {

@override
Future<int> run() async {
final config = VeryGoodConfig.load(Directory.current, logger: logger);
if (config == null) return ExitCode.config.code;
createConfig = config.create;

final template = this.template;
final bundle = template.bundle;

Expand Down Expand Up @@ -273,7 +290,11 @@ abstract class CreateSubCommand extends Command<int> {
mixin OrgName on CreateSubCommand {
/// Gets the organization name.
String get orgName {
final orgName = argResults['org-name'] as String? ?? _defaultOrgName;
final orgName = argResults.resolve<String>(
'org-name',
createConfig.orgName,
fallbackValue: _defaultOrgName,
);
_validateOrgName(orgName);
return orgName;
}
Expand Down Expand Up @@ -317,10 +338,26 @@ mixin MultiTemplates on CreateSubCommand {
@nonVirtual
@override
Template get template {
final templateName =
argResults['template'] as String? ?? defaultTemplateName;
final templateName = argResults.resolve<String>(
'template',
createConfig.template,
fallbackValue: defaultTemplateName,
);

return templates.firstWhere((template) => template.name == templateName);
return templates.firstWhere(
(template) => template.name == templateName,
orElse: () {
// When the value came from `very_good.yaml` rather than the command
// line, point the user at the config key instead of the CLI option so
// they debug the right place.
final source = argResults.wasParsed('template')
? 'option "--template"'
: 'the `create.template` key in `$veryGoodConfigFileName`';
usageException(
'"$templateName" is not an allowed value for $source.',
);
},
);
}
}

Expand All @@ -331,7 +368,11 @@ mixin MultiTemplates on CreateSubCommand {
/// to the brick generator.
mixin Publishable on CreateSubCommand {
/// Gets the publishable flag.
bool get publishable => argResults['publishable'] as bool? ?? false;
bool get publishable => argResults.resolve<bool>(
'publishable',
createConfig.publishable,
fallbackValue: false,
);
}

/// Mixin for [CreateSubCommand] subclasses that receives the workspace flag.
Expand All @@ -340,5 +381,9 @@ mixin Publishable on CreateSubCommand {
/// to the brick generator.
mixin Workspace on CreateSubCommand {
/// Gets the workspace flag.
bool get workspace => argResults['workspace'] as bool? ?? false;
bool get workspace => argResults.resolve<bool>(
'workspace',
createConfig.workspace,
fallbackValue: false,
);
}
7 changes: 6 additions & 1 deletion lib/src/commands/create/commands/docs_site.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:very_good_cli/src/commands/commands.dart';
import 'package:very_good_cli/src/commands/create/templates/templates.dart';
import 'package:very_good_cli/src/very_good_config/very_good_config.dart';

/// {@template very_good_create_docs_site}
/// A [CreateSubCommand] for creating Dart command line interfaces.
Expand Down Expand Up @@ -30,7 +31,11 @@ class CreateDocsSite extends CreateSubCommand with Publishable {
Map<String, dynamic> getTemplateVars() {
return <String, dynamic>{
...super.getTemplateVars(),
'org_name': argResults['org-name'],
'org_name': argResults.resolve<String>(
'org-name',
createConfig.orgName,
fallbackValue: _defaultOrgName,
),
};
}

Expand Down
14 changes: 3 additions & 11 deletions lib/src/commands/dart/commands/dart_test_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ class DartTestCommand extends Command<int> {
help:
'Whether to collect coverage from imported files only or all '
'files.',
allowed: ['imports', 'all'],
allowed: collectCoverageFromAllowedValues,
defaultsTo: 'imports',
valueHelp: 'imports|all',
)
Expand Down Expand Up @@ -369,16 +369,8 @@ This command should be run from the root of your Dart project.''');
return ExitCode.noInput.code;
}

final VeryGoodConfig config;
try {
config = VeryGoodConfig.loadFromClosestAncestor(Directory(targetPath));
} on VeryGoodConfigParseException catch (e) {
_logger.err(
'Could not read `$veryGoodConfigFileName`.\n'
'${e.message}',
);
return ExitCode.config.code;
}
final config = VeryGoodConfig.load(Directory(targetPath), logger: _logger);
if (config == null) return ExitCode.config.code;

final isDartInstalled = await _dartInstalled(logger: _logger);

Expand Down
21 changes: 4 additions & 17 deletions lib/src/commands/packages/commands/check/commands/licenses.dart
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,7 @@ class PackagesCheckLicensesCommand extends Command<int> {
..addMultiOption(
'dependency-type',
help: 'The type of dependencies to check licenses for.',
allowed: [
'direct-main',
'direct-dev',
'direct-overridden',
'transitive',
],
allowed: dependencyTypeAllowedValues,
allowedHelp: {
'direct-main': 'Check for direct main dependencies.',
'direct-dev': 'Check for direct dev dependencies.',
Expand All @@ -183,7 +178,7 @@ class PackagesCheckLicensesCommand extends Command<int> {
..addOption(
'reporter',
help: 'Lists all licenses.',
allowed: ['text', 'csv'],
allowed: reporterAllowedValues,
allowedHelp: {
'text': 'Lists licenses without a specific format.',
'csv': 'Lists licenses in a CSV format.',
Expand Down Expand Up @@ -211,16 +206,8 @@ class PackagesCheckLicensesCommand extends Command<int> {
final target = _argResults.rest.length == 1 ? _argResults.rest[0] : '.';
final targetPath = path.normalize(Directory(target).absolute.path);

final VeryGoodConfig config;
try {
config = VeryGoodConfig.loadFromClosestAncestor(Directory(targetPath));
} on VeryGoodConfigParseException catch (e) {
_logger.err(
'Could not read `$veryGoodConfigFileName`.\n'
'${e.message}',
);
return ExitCode.config.code;
}
final config = VeryGoodConfig.load(Directory(targetPath), logger: _logger);
if (config == null) return ExitCode.config.code;

final options = PackagesCheckLicensesOptions.parse(
_argResults,
Expand Down
12 changes: 2 additions & 10 deletions lib/src/commands/packages/commands/get.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,8 @@ class PackagesGetCommand extends Command<int> {
final target = _argResults.rest.length == 1 ? _argResults.rest[0] : '.';
final targetPath = path.normalize(Directory(target).absolute.path);

final VeryGoodConfig config;
try {
config = VeryGoodConfig.loadFromClosestAncestor(Directory(targetPath));
} on VeryGoodConfigParseException catch (e) {
_logger.err(
'Could not read `$veryGoodConfigFileName`.\n'
'${e.message}',
);
return ExitCode.config.code;
}
final config = VeryGoodConfig.load(Directory(targetPath), logger: _logger);
if (config == null) return ExitCode.config.code;

final options = PackagesGetOptions.parse(_argResults, config: config);

Expand Down
14 changes: 3 additions & 11 deletions lib/src/commands/test/test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ class TestCommand extends Command<int> {
help:
'Whether to collect coverage from imported files only or all '
'files.',
allowed: ['imports', 'all'],
allowed: collectCoverageFromAllowedValues,
defaultsTo: 'imports',
valueHelp: 'imports|all',
)
Expand Down Expand Up @@ -458,16 +458,8 @@ This command should be run from the root of your Flutter project.''');
return ExitCode.noInput.code;
}

final VeryGoodConfig config;
try {
config = VeryGoodConfig.loadFromClosestAncestor(Directory(targetPath));
} on VeryGoodConfigParseException catch (e) {
_logger.err(
'Could not read `$veryGoodConfigFileName`.\n'
'${e.message}',
);
return ExitCode.config.code;
}
final config = VeryGoodConfig.load(Directory(targetPath), logger: _logger);
if (config == null) return ExitCode.config.code;

final isFlutterInstalled = await _flutterInstalled(logger: _logger);

Expand Down
24 changes: 20 additions & 4 deletions lib/src/mcp/mcp_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,20 @@ Only one value can be selected.
);
}

/// The `create` subcommands that define the `--workspace` flag.
///
/// `docs_site` is intentionally excluded because it does not mix in the
/// `Workspace` mixin and therefore rejects the flag.
static const _workspaceSubcommands = {
'app_ui_package',
'flame_game',
'flutter_app',
'flutter_package',
'flutter_plugin',
'dart_cli',
'dart_package',
};

List<String> _parseCreate(Map<String, Object?> args) {
final subcommand = args['subcommand']! as String;
final name = args['name']! as String;
Expand All @@ -352,10 +366,12 @@ Only one value can be selected.
if (args['publishable'] == true) {
cliArgs.add('--publishable');
}
if (args['workspace'] == true) {
cliArgs.add('--workspace');
} else if (args['workspace'] == false) {
cliArgs.add('--no-workspace');
if (_workspaceSubcommands.contains(subcommand)) {
if (args['workspace'] == true) {
cliArgs.add('--workspace');
} else if (args['workspace'] == false) {
cliArgs.add('--no-workspace');
}
}
if (args['executable-name'] != null) {
cliArgs.addAll(['--executable-name', args['executable-name']! as String]);
Expand Down
Loading
Loading