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
2 changes: 1 addition & 1 deletion .github/workflows/celest_cli.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
run: tool/setup-ci.sh
- name: Get Packages
working-directory: apps/cli
run: dart pub upgrade
run: dart pub upgrade --verbose
- name: Test
working-directory: apps/cli
run: dart test -x e2e --fail-fast -j 1
Expand Down
34 changes: 33 additions & 1 deletion apps/cli/lib/src/analyzer/celest_analyzer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:celest_ast/celest_ast.dart' as ast;
import 'package:celest_cli/src/analyzer/analysis_error.dart';
import 'package:celest_cli/src/analyzer/analysis_result.dart';
Expand Down Expand Up @@ -112,6 +113,29 @@ const project = Project(name: 'cache_warmup');
CelestProjectResolver? _resolver;
CelestProjectResolver get resolver => _resolver!;

/// Whether [code] and [message] represent a possible false-positive for
/// missing code generation.
bool _missingCodegenError(AnalysisError error) {
switch (error.errorCode) {
case CompileTimeErrorCode.URI_DOES_NOT_EXIST:
final regex = RegExp(r'''Target of URI doesn't exist: '(.+?)'\.''');
final match = regex.firstMatch(error.message);
final uri = match?.group(1);
if (uri == null) {
return false;
}
final path =
context.currentSession.uriConverter.uriToPath(Uri.parse(uri));
if (path == null) {
_logger.fine('Failed to convert URI to path: $uri');
return false;
}
return p.isWithin(projectPaths.generatedDir, path);
}

return false;
}

@override
void reportError(
String error, {
Expand Down Expand Up @@ -497,7 +521,15 @@ const project = Project(name: 'cache_warmup');
.expand((unit) => unit.errors)
.where((error) => error.severity == Severity.error)
.toList();
if (apiErrors.isNotEmpty) {

// If there's a false positive from missing generated code, which can
// happen for example when starting from a template proejct, then skip
// reporting errors since they may be resolved through generation, and if
// not, they'll be caught by the frontend compiler.
final falsePositiveForGeneratedCode =
apiErrors.isNotEmpty && apiErrors.any(_missingCodegenError);

if (apiErrors.isNotEmpty && !falsePositiveForGeneratedCode) {
for (final apiError in apiErrors) {
reportError(
apiError.message,
Expand Down
20 changes: 17 additions & 3 deletions apps/cli/lib/src/commands/init_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ final class InitCommand extends CelestCommand with Configure, ProjectCreator {
hide: true,
defaultsTo: true,
);
argParser.addOption(
'template',
abbr: 't',
help: 'The project template to use.',
allowed: ['hello', 'data'],
allowedHelp: {
'hello': 'A simple greeting API.',
'data': 'A project with a database and cloud functions.',
},
defaultsTo: 'hello',
);
}

@override
Expand All @@ -29,6 +40,9 @@ final class InitCommand extends CelestCommand with Configure, ProjectCreator {
@override
Progress? currentProgress;

@override
late final String template = argResults!.option('template')!;

/// Precache assets in the background.
Future<void> _precacheInBackground() async {
final command = switch (CliRuntime.current) {
Expand Down Expand Up @@ -97,9 +111,9 @@ final class InitCommand extends CelestCommand with Configure, ProjectCreator {
stdout.writeln();
cliLogger.success('🚀 To start a local development server, run:');
cliLogger
..info(Platform.lineTerminator)
..info(' $command${Platform.lineTerminator}')
..info(Platform.lineTerminator);
..info('')
..info(' $command')
..info('');

return 0;
}
Expand Down
18 changes: 17 additions & 1 deletion apps/cli/lib/src/commands/start_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,20 @@ import 'package:mason_logger/mason_logger.dart';

final class StartCommand extends CelestCommand
with Configure, Migrate, ProjectCreator {
StartCommand();
StartCommand() {
argParser.addOption(
'template',
abbr: 't',
help: 'The project template to use.',
allowed: ['hello', 'data'],
allowedHelp: {
'hello': 'A simple greeting API.',
'data': 'A project with a database and cloud functions.',
},
defaultsTo: 'hello',
hide: true,
);
}

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

@override
late final String template = argResults!.option('template')!;

@override
Future<int> run() async {
await super.run();
Expand Down
33 changes: 30 additions & 3 deletions apps/cli/lib/src/init/project_generator.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import 'package:celest_cli/src/init/migrations/add_analyzer_plugin.dart';
import 'package:celest_cli/src/init/migrations/macos_entitlements.dart';
import 'package:celest_cli/src/init/project_migration.dart';
import 'package:celest_cli/src/init/templates/project_template.dart';
import 'package:celest_cli/src/project/celest_project.dart';
import 'package:celest_cli/src/sdk/dart_sdk.dart';

typedef ProjectTemplateFactory = ProjectTemplate Function(
String projectRoot,
String projectName,
String projectDisplayName,
);

/// Manages the generation of a new Celest project.
class ProjectGenerator {
ProjectGenerator({
required this.projectName,
required this.projectDisplayName,
required this.parentProject,
this.projectTemplate = HelloProject.new,
required this.projectRoot,
});

/// The sanitized name of the project.
final String projectName;

/// The name of the project to initialize, as chosen by the user
/// when running `celest start` for the first time.
final String projectName;
final String projectDisplayName;

/// The root directory of the enclosing Flutter project.
///
Expand All @@ -23,17 +35,32 @@ class ProjectGenerator {
/// Flutter code.
final ParentProject? parentProject;

/// The project template to use for the migration.
final ProjectTemplateFactory projectTemplate;

/// The root directory of the initialized Celest project.
final String projectRoot;

/// Generates a new Celest project.
Future<void> generate() async {
final template = projectTemplate(
projectRoot,
projectName,
projectDisplayName,
);
await Future.wait(
[
ProjectFile.gitIgnore(projectRoot),
ProjectFile.analysisOptions(projectRoot),
ProjectFile.pubspec(projectRoot, projectName, parentProject),
ProjectTemplate.hello(projectRoot, projectName),
ProjectFile.pubspec(
projectRoot,
projectName: projectName,
projectDisplayName: projectDisplayName,
parentProject: parentProject,
additionalDependencies: template.additionalDependencies,
),
ProjectFile.client(projectRoot, projectName),
template,
if (parentProject
case ParentProject(
path: final appRoot,
Expand Down
55 changes: 41 additions & 14 deletions apps/cli/lib/src/init/project_init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:celest_cli/src/context.dart';
import 'package:celest_cli/src/exceptions.dart';
import 'package:celest_cli/src/init/project_generator.dart';
import 'package:celest_cli/src/init/project_migrate.dart';
import 'package:celest_cli/src/init/templates/project_template.dart';
import 'package:celest_cli/src/project/celest_project.dart';
import 'package:celest_cli/src/pub/pub_action.dart';
import 'package:celest_cli/src/sdk/dart_sdk.dart';
Expand All @@ -18,8 +19,12 @@ import 'package:dcli/dcli.dart' as dcli;
import 'package:mason_logger/mason_logger.dart';

base mixin ProjectCreator on Configure {
/// The project template to use when creating a project.
String get template;

Future<String> createProject({
required String projectName,
required String projectDisplayName,
required ParentProject? parentProject,
}) async {
logger.finest(
Expand All @@ -31,6 +36,12 @@ base mixin ProjectCreator on Configure {
parentProject: parentProject,
projectRoot: projectPaths.projectRoot,
projectName: projectName,
projectDisplayName: projectDisplayName,
projectTemplate: switch (template) {
'hello' => HelloProject.new,
'data' => DataProject.new,
_ => unreachable('Invalid project template: $template'),
},
).generate();
logger.fine('Project generated successfully');
});
Expand Down Expand Up @@ -74,13 +85,15 @@ base mixin Configure on CelestCommand {
'To create a new project, run `celest init`.',
);

String newProjectName({String? defaultName}) {
({
String projectNameInput,
String projectName,
}) newProjectName({String? defaultName}) {
if (defaultName != null && defaultName.startsWith('celest')) {
defaultName = null;
}
defaultName ??= 'my_project';
String? projectName;
while (projectName == null) {
defaultName ??= 'My Project';
for (;;) {
final input = dcli
.ask('Enter a name for your project', defaultValue: defaultName)
.trim();
Expand All @@ -90,13 +103,12 @@ base mixin Configure on CelestCommand {
}
final words = input.groupIntoWords();
for (final (index, word) in List.of(words).indexed) {
if (word == 'celest') {
if (word.toLowerCase() == 'celest') {
words.removeAt(index);
}
}
projectName = words.snakeCase;
return (projectNameInput: input, projectName: words.snakeCase);
}
return projectName;
}

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

/// Returns true if the project needs to be migrated.
Stream<ConfigureState> _configure() async* {
final (projectName, projectRoot, isExistingProject, parentProject) =
await _locateProject();
final (
projectNameInput,
projectName,
projectRoot,
isExistingProject,
parentProject
) = await _locateProject();

yield const Initializing();
await init(projectRoot: projectRoot, parentProject: parentProject);
Expand All @@ -141,6 +158,7 @@ base mixin Configure on CelestCommand {
yield const CreatingProject();
await projectCreator.createProject(
projectName: projectName!,
projectDisplayName: projectNameInput!,
parentProject: parentProject,
);
yield const CreatedProject();
Expand Down Expand Up @@ -169,7 +187,7 @@ base mixin Configure on CelestCommand {
yield Initialized(needsAnalyzerMigration: needsAnalyzerMigration);
}

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

String projectRoot;
String? projectName;
String? projectNameInput;
if (isExistingProject) {
if (this is InitCommand) {
cliLogger.success(
Expand Down Expand Up @@ -259,7 +278,8 @@ base mixin Configure on CelestCommand {
if (currentDirIsEmpty) {
defaultProjectName ??= p.basename(currentDir.path);
}
projectName = newProjectName(defaultName: defaultProjectName);
(:projectNameInput, :projectName) =
newProjectName(defaultName: defaultProjectName);

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

return (projectName, projectRoot, isExistingProject, parentProject);
return (
projectNameInput,
projectName,
projectRoot,
isExistingProject,
parentProject
);
}

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