Skip to content

Commit 8e78a14

Browse files
feat(create): add create config to very_good.yaml (#1665)
* feat(create): add create config to very_good.yaml Add a `create` section to `very_good.yaml` so `very_good create` and its subcommands can read defaults for `description`, `org_name`, `publishable`, and `template`. CLI arguments continue to take precedence over config values. Closes #1654 * fix: tests * style: format create_subcommand.dart * fix: tests * fix: ci issues * refactor: move config load to VeryGoodConfig * chore: review * fix(create): remove unused workspaceSubcommandNames getter The getter was never referenced, breaking the 100% coverage gate. --------- Co-authored-by: unicoderbot[bot] <269805761+unicoderbot[bot]@users.noreply.github.com> Co-authored-by: marcossevilla <marcossevilla@users.noreply.github.com>
1 parent 19a84dd commit 8e78a14

28 files changed

Lines changed: 1091 additions & 142 deletions

File tree

.github/workflows/e2e.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ jobs:
4343
- test/commands/create/flame_game/flame_game_test.dart
4444
- test/commands/create/flutter_package/flutter_pkg_test.dart
4545
- test/commands/create/flutter_plugin/flutter_plugin_test.dart
46+
- test/commands/create/very_good_config/very_good_config_test.dart
4647

4748
# E2E tests for the `packages check licenses` command
4849
- test/commands/packages/check/licenses/licenses_allowed_test.dart
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
create:
2+
description: A project configured via very_good.yaml.
3+
publishable: true
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- not
2+
- a
3+
- map
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import 'package:mason/mason.dart';
2+
import 'package:mocktail/mocktail.dart';
3+
import 'package:path/path.dart' as path;
4+
import 'package:test/test.dart';
5+
import 'package:universal_io/io.dart';
6+
7+
import '../../../../helpers/helpers.dart';
8+
9+
void main() {
10+
group('very_good.yaml', () {
11+
test(
12+
'applies create defaults from very_good.yaml when flags are not passed',
13+
timeout: const Timeout(Duration(minutes: 2)),
14+
withRunner((commandRunner, logger, updater, logs, progressLogs) async {
15+
final tempDirectory = Directory.systemTemp.createTempSync(
16+
'very_good_config_create',
17+
);
18+
addTearDown(() => tempDirectory.deleteSync(recursive: true));
19+
20+
final fixture = Directory(
21+
path.join(
22+
Directory.current.path,
23+
'test/commands/create/very_good_config/fixture',
24+
),
25+
);
26+
27+
await copyDirectory(fixture, tempDirectory);
28+
29+
final cwd = Directory.current;
30+
Directory.current = tempDirectory;
31+
addTearDown(() => Directory.current = cwd);
32+
33+
final result = await commandRunner.run([
34+
'create',
35+
'dart_package',
36+
'very_good_dart',
37+
]);
38+
expect(result, equals(ExitCode.success.code));
39+
40+
final pubspec = File(
41+
path.join(tempDirectory.path, 'very_good_dart', 'pubspec.yaml'),
42+
);
43+
expect(pubspec.existsSync(), isTrue);
44+
expect(
45+
pubspec.readAsStringSync(),
46+
contains('A project configured via very_good.yaml.'),
47+
);
48+
}),
49+
);
50+
51+
test(
52+
'fails with config exit code when very_good.yaml is malformed',
53+
timeout: const Timeout(Duration(minutes: 2)),
54+
withRunner((commandRunner, logger, updater, logs, progressLogs) async {
55+
final tempDirectory = Directory.systemTemp.createTempSync(
56+
'very_good_config_create_malformed',
57+
);
58+
addTearDown(() => tempDirectory.deleteSync(recursive: true));
59+
60+
final fixture = Directory(
61+
path.join(
62+
Directory.current.path,
63+
'test/commands/create/very_good_config/malformed_fixture',
64+
),
65+
);
66+
67+
await copyDirectory(fixture, tempDirectory);
68+
69+
final cwd = Directory.current;
70+
Directory.current = tempDirectory;
71+
addTearDown(() => Directory.current = cwd);
72+
73+
await expectLater(
74+
commandRunner.run(['create', 'dart_package', 'very_good_dart']),
75+
completion(equals(ExitCode.config.code)),
76+
);
77+
verify(
78+
() => logger.err(
79+
any(that: contains('Could not read `very_good.yaml`')),
80+
),
81+
).called(1);
82+
}),
83+
);
84+
});
85+
}

lib/src/commands/create/commands/create_subcommand.dart

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:meta/meta.dart';
88
import 'package:path/path.dart' as path;
99
import 'package:very_good_cli/src/commands/commands.dart';
1010
import 'package:very_good_cli/src/commands/create/templates/templates.dart';
11+
import 'package:very_good_cli/src/very_good_config/very_good_config.dart';
1112

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

122+
/// The resolved `very_good create` configuration read from the closest
123+
/// `very_good.yaml` file.
124+
///
125+
/// Values declared here are used as defaults for any option that was not
126+
/// explicitly passed on the command line. It defaults to an empty
127+
/// configuration until [run] loads it.
128+
VeryGoodCreateConfig createConfig = const VeryGoodCreateConfig();
129+
121130
/// [ArgResults] which can be overridden for testing.
122131
@visibleForTesting
123132
ArgResults? argResultOverrides;
@@ -174,7 +183,11 @@ abstract class CreateSubCommand extends Command<int> {
174183
}
175184

176185
/// Gets the description for the project.
177-
String get projectDescription => argResults['description'] as String? ?? '';
186+
String get projectDescription => argResults.resolve<String>(
187+
'description',
188+
createConfig.description,
189+
fallbackValue: '',
190+
);
178191

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

195208
@override
196209
Future<int> run() async {
210+
final config = VeryGoodConfig.load(Directory.current, logger: logger);
211+
if (config == null) return ExitCode.config.code;
212+
createConfig = config.create;
213+
197214
final template = this.template;
198215
final bundle = template.bundle;
199216

@@ -273,7 +290,11 @@ abstract class CreateSubCommand extends Command<int> {
273290
mixin OrgName on CreateSubCommand {
274291
/// Gets the organization name.
275292
String get orgName {
276-
final orgName = argResults['org-name'] as String? ?? _defaultOrgName;
293+
final orgName = argResults.resolve<String>(
294+
'org-name',
295+
createConfig.orgName,
296+
fallbackValue: _defaultOrgName,
297+
);
277298
_validateOrgName(orgName);
278299
return orgName;
279300
}
@@ -317,10 +338,26 @@ mixin MultiTemplates on CreateSubCommand {
317338
@nonVirtual
318339
@override
319340
Template get template {
320-
final templateName =
321-
argResults['template'] as String? ?? defaultTemplateName;
341+
final templateName = argResults.resolve<String>(
342+
'template',
343+
createConfig.template,
344+
fallbackValue: defaultTemplateName,
345+
);
322346

323-
return templates.firstWhere((template) => template.name == templateName);
347+
return templates.firstWhere(
348+
(template) => template.name == templateName,
349+
orElse: () {
350+
// When the value came from `very_good.yaml` rather than the command
351+
// line, point the user at the config key instead of the CLI option so
352+
// they debug the right place.
353+
final source = argResults.wasParsed('template')
354+
? 'option "--template"'
355+
: 'the `create.template` key in `$veryGoodConfigFileName`';
356+
usageException(
357+
'"$templateName" is not an allowed value for $source.',
358+
);
359+
},
360+
);
324361
}
325362
}
326363

@@ -331,7 +368,11 @@ mixin MultiTemplates on CreateSubCommand {
331368
/// to the brick generator.
332369
mixin Publishable on CreateSubCommand {
333370
/// Gets the publishable flag.
334-
bool get publishable => argResults['publishable'] as bool? ?? false;
371+
bool get publishable => argResults.resolve<bool>(
372+
'publishable',
373+
createConfig.publishable,
374+
fallbackValue: false,
375+
);
335376
}
336377

337378
/// Mixin for [CreateSubCommand] subclasses that receives the workspace flag.
@@ -340,5 +381,9 @@ mixin Publishable on CreateSubCommand {
340381
/// to the brick generator.
341382
mixin Workspace on CreateSubCommand {
342383
/// Gets the workspace flag.
343-
bool get workspace => argResults['workspace'] as bool? ?? false;
384+
bool get workspace => argResults.resolve<bool>(
385+
'workspace',
386+
createConfig.workspace,
387+
fallbackValue: false,
388+
);
344389
}

lib/src/commands/create/commands/docs_site.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:very_good_cli/src/commands/commands.dart';
22
import 'package:very_good_cli/src/commands/create/templates/templates.dart';
3+
import 'package:very_good_cli/src/very_good_config/very_good_config.dart';
34

45
/// {@template very_good_create_docs_site}
56
/// A [CreateSubCommand] for creating Dart command line interfaces.
@@ -30,7 +31,11 @@ class CreateDocsSite extends CreateSubCommand with Publishable {
3031
Map<String, dynamic> getTemplateVars() {
3132
return <String, dynamic>{
3233
...super.getTemplateVars(),
33-
'org_name': argResults['org-name'],
34+
'org_name': argResults.resolve<String>(
35+
'org-name',
36+
createConfig.orgName,
37+
fallbackValue: _defaultOrgName,
38+
),
3439
};
3540
}
3641

lib/src/commands/dart/commands/dart_test_command.dart

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ class DartTestCommand extends Command<int> {
283283
help:
284284
'Whether to collect coverage from imported files only or all '
285285
'files.',
286-
allowed: ['imports', 'all'],
286+
allowed: collectCoverageFromAllowedValues,
287287
defaultsTo: 'imports',
288288
valueHelp: 'imports|all',
289289
)
@@ -369,16 +369,8 @@ This command should be run from the root of your Dart project.''');
369369
return ExitCode.noInput.code;
370370
}
371371

372-
final VeryGoodConfig config;
373-
try {
374-
config = VeryGoodConfig.loadFromClosestAncestor(Directory(targetPath));
375-
} on VeryGoodConfigParseException catch (e) {
376-
_logger.err(
377-
'Could not read `$veryGoodConfigFileName`.\n'
378-
'${e.message}',
379-
);
380-
return ExitCode.config.code;
381-
}
372+
final config = VeryGoodConfig.load(Directory(targetPath), logger: _logger);
373+
if (config == null) return ExitCode.config.code;
382374

383375
final isDartInstalled = await _dartInstalled(logger: _logger);
384376

lib/src/commands/packages/commands/check/commands/licenses.dart

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,7 @@ class PackagesCheckLicensesCommand extends Command<int> {
157157
..addMultiOption(
158158
'dependency-type',
159159
help: 'The type of dependencies to check licenses for.',
160-
allowed: [
161-
'direct-main',
162-
'direct-dev',
163-
'direct-overridden',
164-
'transitive',
165-
],
160+
allowed: dependencyTypeAllowedValues,
166161
allowedHelp: {
167162
'direct-main': 'Check for direct main dependencies.',
168163
'direct-dev': 'Check for direct dev dependencies.',
@@ -183,7 +178,7 @@ class PackagesCheckLicensesCommand extends Command<int> {
183178
..addOption(
184179
'reporter',
185180
help: 'Lists all licenses.',
186-
allowed: ['text', 'csv'],
181+
allowed: reporterAllowedValues,
187182
allowedHelp: {
188183
'text': 'Lists licenses without a specific format.',
189184
'csv': 'Lists licenses in a CSV format.',
@@ -211,16 +206,8 @@ class PackagesCheckLicensesCommand extends Command<int> {
211206
final target = _argResults.rest.length == 1 ? _argResults.rest[0] : '.';
212207
final targetPath = path.normalize(Directory(target).absolute.path);
213208

214-
final VeryGoodConfig config;
215-
try {
216-
config = VeryGoodConfig.loadFromClosestAncestor(Directory(targetPath));
217-
} on VeryGoodConfigParseException catch (e) {
218-
_logger.err(
219-
'Could not read `$veryGoodConfigFileName`.\n'
220-
'${e.message}',
221-
);
222-
return ExitCode.config.code;
223-
}
209+
final config = VeryGoodConfig.load(Directory(targetPath), logger: _logger);
210+
if (config == null) return ExitCode.config.code;
224211

225212
final options = PackagesCheckLicensesOptions.parse(
226213
_argResults,

lib/src/commands/packages/commands/get.dart

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,8 @@ class PackagesGetCommand extends Command<int> {
8585
final target = _argResults.rest.length == 1 ? _argResults.rest[0] : '.';
8686
final targetPath = path.normalize(Directory(target).absolute.path);
8787

88-
final VeryGoodConfig config;
89-
try {
90-
config = VeryGoodConfig.loadFromClosestAncestor(Directory(targetPath));
91-
} on VeryGoodConfigParseException catch (e) {
92-
_logger.err(
93-
'Could not read `$veryGoodConfigFileName`.\n'
94-
'${e.message}',
95-
);
96-
return ExitCode.config.code;
97-
}
88+
final config = VeryGoodConfig.load(Directory(targetPath), logger: _logger);
89+
if (config == null) return ExitCode.config.code;
9890

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

lib/src/commands/test/test.dart

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ class TestCommand extends Command<int> {
333333
help:
334334
'Whether to collect coverage from imported files only or all '
335335
'files.',
336-
allowed: ['imports', 'all'],
336+
allowed: collectCoverageFromAllowedValues,
337337
defaultsTo: 'imports',
338338
valueHelp: 'imports|all',
339339
)
@@ -458,16 +458,8 @@ This command should be run from the root of your Flutter project.''');
458458
return ExitCode.noInput.code;
459459
}
460460

461-
final VeryGoodConfig config;
462-
try {
463-
config = VeryGoodConfig.loadFromClosestAncestor(Directory(targetPath));
464-
} on VeryGoodConfigParseException catch (e) {
465-
_logger.err(
466-
'Could not read `$veryGoodConfigFileName`.\n'
467-
'${e.message}',
468-
);
469-
return ExitCode.config.code;
470-
}
461+
final config = VeryGoodConfig.load(Directory(targetPath), logger: _logger);
462+
if (config == null) return ExitCode.config.code;
471463

472464
final isFlutterInstalled = await _flutterInstalled(logger: _logger);
473465

0 commit comments

Comments
 (0)