Skip to content

Commit 789da24

Browse files
committed
feat(cli): Add templates to celest init
- Adds two templates to `celest init`: the exiting `hello` template and a `data` template which is a slim version of the `tasks` example project. - Fixes some false-positive errors when running `celest start` from a generated template where codegen is required.
1 parent 4464cc6 commit 789da24

16 files changed

+928
-175
lines changed

apps/cli/lib/src/analyzer/celest_analyzer.dart

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:analyzer/error/error.dart';
88
import 'package:analyzer/error/listener.dart';
99
import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
1010
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
11+
import 'package:analyzer/src/error/codes.dart';
1112
import 'package:celest_ast/celest_ast.dart' as ast;
1213
import 'package:celest_cli/src/analyzer/analysis_error.dart';
1314
import 'package:celest_cli/src/analyzer/analysis_result.dart';
@@ -112,6 +113,29 @@ const project = Project(name: 'cache_warmup');
112113
CelestProjectResolver? _resolver;
113114
CelestProjectResolver get resolver => _resolver!;
114115

116+
/// Whether [code] and [message] represent a possible false-positive for
117+
/// missing code generation.
118+
bool _missingCodegenError(AnalysisError error) {
119+
switch (error.errorCode) {
120+
case CompileTimeErrorCode.URI_DOES_NOT_EXIST:
121+
final regex = RegExp(r'''Target of URI doesn't exist: '(.+?)'\.''');
122+
final match = regex.firstMatch(error.message);
123+
final uri = match?.group(1);
124+
if (uri == null) {
125+
return false;
126+
}
127+
final path =
128+
context.currentSession.uriConverter.uriToPath(Uri.parse(uri));
129+
if (path == null) {
130+
_logger.fine('Failed to convert URI to path: $uri');
131+
return false;
132+
}
133+
return p.isWithin(projectPaths.generatedDir, path);
134+
}
135+
136+
return false;
137+
}
138+
115139
@override
116140
void reportError(
117141
String error, {
@@ -497,7 +521,15 @@ const project = Project(name: 'cache_warmup');
497521
.expand((unit) => unit.errors)
498522
.where((error) => error.severity == Severity.error)
499523
.toList();
500-
if (apiErrors.isNotEmpty) {
524+
525+
// If there's a false positive from missing generated code, which can
526+
// happen for example when starting from a template proejct, then skip
527+
// reporting errors since they may be resolved through generation, and if
528+
// not, they'll be caught by the frontend compiler.
529+
final falsePositiveForGeneratedCode =
530+
apiErrors.isNotEmpty && apiErrors.any(_missingCodegenError);
531+
532+
if (apiErrors.isNotEmpty && !falsePositiveForGeneratedCode) {
501533
for (final apiError in apiErrors) {
502534
reportError(
503535
apiError.message,

apps/cli/lib/src/commands/init_command.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ final class InitCommand extends CelestCommand with Configure, ProjectCreator {
1515
hide: true,
1616
defaultsTo: true,
1717
);
18+
argParser.addOption(
19+
'template',
20+
abbr: 't',
21+
help: 'The project template to use.',
22+
allowed: ['hello', 'data'],
23+
allowedHelp: {
24+
'hello': 'A simple greeting API.',
25+
'data': 'A project with a database and cloud functions.',
26+
},
27+
defaultsTo: 'hello',
28+
);
1829
}
1930

2031
@override
@@ -29,6 +40,9 @@ final class InitCommand extends CelestCommand with Configure, ProjectCreator {
2940
@override
3041
Progress? currentProgress;
3142

43+
@override
44+
late final String template = argResults!.option('template')!;
45+
3246
/// Precache assets in the background.
3347
Future<void> _precacheInBackground() async {
3448
final command = switch (CliRuntime.current) {
@@ -97,9 +111,9 @@ final class InitCommand extends CelestCommand with Configure, ProjectCreator {
97111
stdout.writeln();
98112
cliLogger.success('🚀 To start a local development server, run:');
99113
cliLogger
100-
..info(Platform.lineTerminator)
101-
..info(' $command${Platform.lineTerminator}')
102-
..info(Platform.lineTerminator);
114+
..info('')
115+
..info(' $command')
116+
..info('');
103117

104118
return 0;
105119
}

apps/cli/lib/src/commands/start_command.dart

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,20 @@ import 'package:mason_logger/mason_logger.dart';
88

99
final class StartCommand extends CelestCommand
1010
with Configure, Migrate, ProjectCreator {
11-
StartCommand();
11+
StartCommand() {
12+
argParser.addOption(
13+
'template',
14+
abbr: 't',
15+
help: 'The project template to use.',
16+
allowed: ['hello', 'data'],
17+
allowedHelp: {
18+
'hello': 'A simple greeting API.',
19+
'data': 'A project with a database and cloud functions.',
20+
},
21+
defaultsTo: 'hello',
22+
hide: true,
23+
);
24+
}
1225

1326
@override
1427
String get description => 'Starts a local Celest environment.';
@@ -22,6 +35,9 @@ final class StartCommand extends CelestCommand
2235
@override
2336
Progress? currentProgress;
2437

38+
@override
39+
late final String template = argResults!.option('template')!;
40+
2541
@override
2642
Future<int> run() async {
2743
await super.run();

apps/cli/lib/src/init/project_generator.dart

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
11
import 'package:celest_cli/src/init/migrations/add_analyzer_plugin.dart';
22
import 'package:celest_cli/src/init/migrations/macos_entitlements.dart';
33
import 'package:celest_cli/src/init/project_migration.dart';
4+
import 'package:celest_cli/src/init/templates/project_template.dart';
45
import 'package:celest_cli/src/project/celest_project.dart';
56
import 'package:celest_cli/src/sdk/dart_sdk.dart';
67

8+
typedef ProjectTemplateFactory = ProjectTemplate Function(
9+
String projectRoot,
10+
String projectName,
11+
String projectDisplayName,
12+
);
13+
714
/// Manages the generation of a new Celest project.
815
class ProjectGenerator {
916
ProjectGenerator({
1017
required this.projectName,
18+
required this.projectDisplayName,
1119
required this.parentProject,
20+
this.projectTemplate = HelloProject.new,
1221
required this.projectRoot,
1322
});
1423

24+
/// The sanitized name of the project.
25+
final String projectName;
26+
1527
/// The name of the project to initialize, as chosen by the user
1628
/// when running `celest start` for the first time.
17-
final String projectName;
29+
final String projectDisplayName;
1830

1931
/// The root directory of the enclosing Flutter project.
2032
///
@@ -23,17 +35,32 @@ class ProjectGenerator {
2335
/// Flutter code.
2436
final ParentProject? parentProject;
2537

38+
/// The project template to use for the migration.
39+
final ProjectTemplateFactory projectTemplate;
40+
2641
/// The root directory of the initialized Celest project.
2742
final String projectRoot;
2843

2944
/// Generates a new Celest project.
3045
Future<void> generate() async {
46+
final template = projectTemplate(
47+
projectRoot,
48+
projectName,
49+
projectDisplayName,
50+
);
3151
await Future.wait(
3252
[
3353
ProjectFile.gitIgnore(projectRoot),
3454
ProjectFile.analysisOptions(projectRoot),
35-
ProjectFile.pubspec(projectRoot, projectName, parentProject),
36-
ProjectTemplate.hello(projectRoot, projectName),
55+
ProjectFile.pubspec(
56+
projectRoot,
57+
projectName: projectName,
58+
projectDisplayName: projectDisplayName,
59+
parentProject: parentProject,
60+
additionalDependencies: template.additionalDependencies,
61+
),
62+
ProjectFile.client(projectRoot, projectName),
63+
template,
3764
if (parentProject
3865
case ParentProject(
3966
path: final appRoot,

apps/cli/lib/src/init/project_init.dart

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:celest_cli/src/context.dart';
88
import 'package:celest_cli/src/exceptions.dart';
99
import 'package:celest_cli/src/init/project_generator.dart';
1010
import 'package:celest_cli/src/init/project_migrate.dart';
11+
import 'package:celest_cli/src/init/templates/project_template.dart';
1112
import 'package:celest_cli/src/project/celest_project.dart';
1213
import 'package:celest_cli/src/pub/pub_action.dart';
1314
import 'package:celest_cli/src/sdk/dart_sdk.dart';
@@ -18,8 +19,12 @@ import 'package:dcli/dcli.dart' as dcli;
1819
import 'package:mason_logger/mason_logger.dart';
1920

2021
base mixin ProjectCreator on Configure {
22+
/// The project template to use when creating a project.
23+
String get template;
24+
2125
Future<String> createProject({
2226
required String projectName,
27+
required String projectDisplayName,
2328
required ParentProject? parentProject,
2429
}) async {
2530
logger.finest(
@@ -31,6 +36,12 @@ base mixin ProjectCreator on Configure {
3136
parentProject: parentProject,
3237
projectRoot: projectPaths.projectRoot,
3338
projectName: projectName,
39+
projectDisplayName: projectDisplayName,
40+
projectTemplate: switch (template) {
41+
'hello' => HelloProject.new,
42+
'data' => DataProject.new,
43+
_ => unreachable('Invalid project template: $template'),
44+
},
3445
).generate();
3546
logger.fine('Project generated successfully');
3647
});
@@ -74,13 +85,15 @@ base mixin Configure on CelestCommand {
7485
'To create a new project, run `celest init`.',
7586
);
7687

77-
String newProjectName({String? defaultName}) {
88+
({
89+
String projectNameInput,
90+
String projectName,
91+
}) newProjectName({String? defaultName}) {
7892
if (defaultName != null && defaultName.startsWith('celest')) {
7993
defaultName = null;
8094
}
81-
defaultName ??= 'my_project';
82-
String? projectName;
83-
while (projectName == null) {
95+
defaultName ??= 'My Project';
96+
for (;;) {
8497
final input = dcli
8598
.ask('Enter a name for your project', defaultValue: defaultName)
8699
.trim();
@@ -90,13 +103,12 @@ base mixin Configure on CelestCommand {
90103
}
91104
final words = input.groupIntoWords();
92105
for (final (index, word) in List.of(words).indexed) {
93-
if (word == 'celest') {
106+
if (word.toLowerCase() == 'celest') {
94107
words.removeAt(index);
95108
}
96109
}
97-
projectName = words.snakeCase;
110+
return (projectNameInput: input, projectName: words.snakeCase);
98111
}
99-
return projectName;
100112
}
101113

102114
Future<bool> configure() async {
@@ -128,8 +140,13 @@ base mixin Configure on CelestCommand {
128140

129141
/// Returns true if the project needs to be migrated.
130142
Stream<ConfigureState> _configure() async* {
131-
final (projectName, projectRoot, isExistingProject, parentProject) =
132-
await _locateProject();
143+
final (
144+
projectNameInput,
145+
projectName,
146+
projectRoot,
147+
isExistingProject,
148+
parentProject
149+
) = await _locateProject();
133150

134151
yield const Initializing();
135152
await init(projectRoot: projectRoot, parentProject: parentProject);
@@ -141,6 +158,7 @@ base mixin Configure on CelestCommand {
141158
yield const CreatingProject();
142159
await projectCreator.createProject(
143160
projectName: projectName!,
161+
projectDisplayName: projectNameInput!,
144162
parentProject: parentProject,
145163
);
146164
yield const CreatedProject();
@@ -169,7 +187,7 @@ base mixin Configure on CelestCommand {
169187
yield Initialized(needsAnalyzerMigration: needsAnalyzerMigration);
170188
}
171189

172-
Future<(String? name, String root, bool, ParentProject?)>
190+
Future<(String? nameInput, String? name, String root, bool, ParentProject?)>
173191
_locateProject() async {
174192
var currentDir = fileSystem.currentDirectory;
175193
final currentDirIsEmpty = await currentDir.list().isEmpty;
@@ -228,6 +246,7 @@ base mixin Configure on CelestCommand {
228246

229247
String projectRoot;
230248
String? projectName;
249+
String? projectNameInput;
231250
if (isExistingProject) {
232251
if (this is InitCommand) {
233252
cliLogger.success(
@@ -259,7 +278,8 @@ base mixin Configure on CelestCommand {
259278
if (currentDirIsEmpty) {
260279
defaultProjectName ??= p.basename(currentDir.path);
261280
}
262-
projectName = newProjectName(defaultName: defaultProjectName);
281+
(:projectNameInput, :projectName) =
282+
newProjectName(defaultName: defaultProjectName);
263283

264284
// Choose where to store the project based on the current directory.
265285
projectRoot = switch (celestDir) {
@@ -269,11 +289,12 @@ base mixin Configure on CelestCommand {
269289
// for the project which is unattached to any parent project, named
270290
// after the project.
271291
null when !currentDirIsEmpty => await run(() async {
272-
final projectRoot = p.join(currentDir.path, projectName);
292+
final directoryName = projectName!.snakeCase;
293+
final projectRoot = p.join(currentDir.path, directoryName);
273294
final projectDir = fileSystem.directory(projectRoot);
274295
if (projectDir.existsSync() && !await projectDir.list().isEmpty) {
275296
throw CliException(
276-
'A directory named "$projectName" already exists. '
297+
'A directory named "$directoryName" already exists. '
277298
'Please choose a different name, or run this command from a '
278299
'different directory.',
279300
);
@@ -286,7 +307,13 @@ base mixin Configure on CelestCommand {
286307
};
287308
}
288309

289-
return (projectName, projectRoot, isExistingProject, parentProject);
310+
return (
311+
projectNameInput,
312+
projectName,
313+
projectRoot,
314+
isExistingProject,
315+
parentProject
316+
);
290317
}
291318

292319
// TODO(dnys1): Improve logic here so that we don't run pub upgrade if

0 commit comments

Comments
 (0)