Skip to content

Commit 2dd85cb

Browse files
committed
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
1 parent 8d8343a commit 2dd85cb

20 files changed

Lines changed: 623 additions & 10 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: 47 additions & 6 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.
@@ -105,6 +106,14 @@ abstract class CreateSubCommand extends Command<int> {
105106
final Logger logger;
106107
final MasonGeneratorFromBundle _generatorFromBundle;
107108

109+
/// The resolved `very_good create` configuration read from the closest
110+
/// `very_good.yaml` file.
111+
///
112+
/// Values declared here are used as defaults for any option that was not
113+
/// explicitly passed on the command line. It defaults to an empty
114+
/// configuration until [run] loads it.
115+
VeryGoodCreateConfig createConfig = const VeryGoodCreateConfig();
116+
108117
/// [ArgResults] which can be overridden for testing.
109118
@visibleForTesting
110119
ArgResults? argResultOverrides;
@@ -160,8 +169,20 @@ abstract class CreateSubCommand extends Command<int> {
160169
return name;
161170
}
162171

172+
/// Resolves the value for the argument named [name] against the matching
173+
/// `very_good.yaml` [configValue].
174+
///
175+
/// A command line argument that was explicitly parsed always wins. Otherwise
176+
/// the [configValue] is used when present, falling back to the argument's
177+
/// default value.
178+
T? resolveArg<T>(String name, T? configValue) {
179+
if (argResults.wasParsed(name)) return argResults[name] as T?;
180+
return configValue ?? argResults[name] as T?;
181+
}
182+
163183
/// Gets the description for the project.
164-
String get projectDescription => argResults['description'] as String? ?? '';
184+
String get projectDescription =>
185+
resolveArg<String>('description', createConfig.description) ?? '';
165186

166187
/// Should return the desired template to be created during a command run.
167188
///
@@ -181,6 +202,18 @@ abstract class CreateSubCommand extends Command<int> {
181202

182203
@override
183204
Future<int> run() async {
205+
try {
206+
createConfig = VeryGoodConfig.loadFromClosestAncestor(
207+
Directory.current,
208+
).create;
209+
} on VeryGoodConfigParseException catch (e) {
210+
logger.err(
211+
'Could not read `$veryGoodConfigFileName`.\n'
212+
'${e.message}',
213+
);
214+
return ExitCode.config.code;
215+
}
216+
184217
final template = this.template;
185218
final bundle = template.bundle;
186219

@@ -258,7 +291,8 @@ abstract class CreateSubCommand extends Command<int> {
258291
mixin OrgName on CreateSubCommand {
259292
/// Gets the organization name.
260293
String get orgName {
261-
final orgName = argResults['org-name'] as String? ?? _defaultOrgName;
294+
final orgName =
295+
resolveArg<String>('org-name', createConfig.orgName) ?? _defaultOrgName;
262296
_validateOrgName(orgName);
263297
return orgName;
264298
}
@@ -303,9 +337,15 @@ mixin MultiTemplates on CreateSubCommand {
303337
@override
304338
Template get template {
305339
final templateName =
306-
argResults['template'] as String? ?? defaultTemplateName;
307-
308-
return templates.firstWhere((template) => template.name == templateName);
340+
resolveArg<String>('template', createConfig.template) ??
341+
defaultTemplateName;
342+
343+
return templates.firstWhere(
344+
(template) => template.name == templateName,
345+
orElse: () => usageException(
346+
'"$templateName" is not an allowed value for option "--template".',
347+
),
348+
);
309349
}
310350
}
311351

@@ -316,5 +356,6 @@ mixin MultiTemplates on CreateSubCommand {
316356
/// to the brick generator.
317357
mixin Publishable on CreateSubCommand {
318358
/// Gets the publishable flag.
319-
bool get publishable => argResults['publishable'] as bool? ?? false;
359+
bool get publishable =>
360+
resolveArg<bool>('publishable', createConfig.publishable) ?? false;
320361
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ class CreateDocsSite extends CreateSubCommand with Publishable {
3030
Map<String, dynamic> getTemplateVars() {
3131
return <String, dynamic>{
3232
...super.getTemplateVars(),
33-
'org_name': argResults['org-name'],
33+
'org_name':
34+
resolveArg<String>('org-name', createConfig.orgName) ??
35+
_defaultOrgName,
3436
};
3537
}
3638

lib/src/very_good_config/very_good_config.dart

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ class VeryGoodConfigParseException implements Exception {
4848
)
4949
class VeryGoodConfig extends Equatable {
5050
/// {@macro very_good_config}
51-
const VeryGoodConfig({this.test = const VeryGoodTestConfig()});
51+
const VeryGoodConfig({
52+
this.test = const VeryGoodTestConfig(),
53+
this.create = const VeryGoodCreateConfig(),
54+
});
5255

5356
/// Creates a [VeryGoodConfig] from a decoded YAML/JSON [json] map.
5457
factory VeryGoodConfig.fromJson(Map<dynamic, dynamic> json) {
@@ -121,8 +124,54 @@ class VeryGoodConfig extends Equatable {
121124
/// Configuration values for the `very_good test` command.
122125
final VeryGoodTestConfig test;
123126

127+
/// Configuration values for the `very_good create` command.
128+
final VeryGoodCreateConfig create;
129+
130+
@override
131+
List<Object?> get props => [test, create];
132+
}
133+
134+
/// {@template very_good_create_config}
135+
/// Configuration values that customize the defaults of the
136+
/// `very_good create` command and its subcommands.
137+
///
138+
/// Any field that is left as `null` retains its CLI default.
139+
/// {@endtemplate}
140+
@JsonSerializable(
141+
anyMap: true,
142+
checked: true,
143+
createToJson: false,
144+
disallowUnrecognizedKeys: true,
145+
fieldRename: FieldRename.snake,
146+
)
147+
class VeryGoodCreateConfig extends Equatable {
148+
/// {@macro very_good_create_config}
149+
const VeryGoodCreateConfig({
150+
this.description,
151+
this.orgName,
152+
this.publishable,
153+
this.template,
154+
});
155+
156+
/// Creates a [VeryGoodCreateConfig] from a decoded YAML/JSON [json] map.
157+
factory VeryGoodCreateConfig.fromJson(Map<dynamic, dynamic> json) {
158+
return _$VeryGoodCreateConfigFromJson(json);
159+
}
160+
161+
/// The description for the generated project.
162+
final String? description;
163+
164+
/// The organization for the generated project.
165+
final String? orgName;
166+
167+
/// Whether the generated project is intended to be published.
168+
final bool? publishable;
169+
170+
/// The template used to generate the project.
171+
final String? template;
172+
124173
@override
125-
List<Object?> get props => [test];
174+
List<Object?> get props => [description, orgName, publishable, template];
126175
}
127176

128177
/// {@template very_good_test_config}

lib/src/very_good_config/very_good_config.g.dart

Lines changed: 26 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/docs/commands/create.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ You cannot combine `.` with `--output-directory`. Very Good CLI will exit with
6868
an error if you specify both.
6969
:::
7070

71+
## Configuring defaults with `very_good.yaml`
72+
73+
To avoid repeating the same flags every time you scaffold a new project, you may create a `very_good.yaml` file at the root of your project. The `create` section accepts the same names as the CLI flags in snake_case (e.g. `--org-name` becomes `org_name`). Values from `very_good.yaml` are used as defaults; anything you pass on the command line takes precedence.
74+
75+
```yaml
76+
# very_good.yaml
77+
create:
78+
description: A Very Good project.
79+
org_name: com.very.good
80+
publishable: true
81+
template: core
82+
```
83+
84+
With the file above, running `very_good create flutter_app my_app` behaves the same as running `very_good create flutter_app my_app --desc 'A Very Good project.' --org-name com.very.good --publishable --template core`. You can still override any of these values on the command line, for example `very_good create flutter_app my_app --org-name com.example` to use a different org name for a single run.
85+
86+
The `very_good.yaml` file is looked up starting from the directory where the command runs and walking up through its ancestors. The closest file wins; configuration from ancestor directories is not merged. This lets a single `very_good.yaml` at the repository root apply to commands run from any nested package.
87+
7188
## Available templates
7289

7390
Each subcommand maps to a specific project template. For detailed usage options

test/helpers/test_multi_template_commands.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Future<void> testMultiTemplateCommand({
3131
final argResults = _MockArgResults();
3232
final command = multiTemplatesCommand..argResultOverrides = argResults;
3333

34+
when(() => argResults.wasParsed(any())).thenReturn(true);
3435
when(() => argResults['template'] as String?).thenReturn(templateName);
3536
when(
3637
() => argResults['output-directory'] as String?,

0 commit comments

Comments
 (0)