From 3361bcfe086ef21e32eb2d4cd8a1e3b5ccf2a2d9 Mon Sep 17 00:00:00 2001 From: Dillon Nys Date: Fri, 29 Aug 2025 12:14:07 -0700 Subject: [PATCH] fix(cli): Ensure all dependencies are correctly resolved When auth or a database are added to a project for the first time, ensure that the necessary dependencies are added to the pubspec and resolvable by the analyzer before analysis. --- apps/cli/CHANGELOG.md | 1 + apps/cli/lib/src/analyzer/project_parser.dart | 69 +++++++++++++++++++ .../src/codegen/client_code_generator.dart | 47 +++++++++++++ apps/cli/lib/src/codegen/code_outputs.dart | 52 +------------- .../cli/lib/src/frontend/celest_frontend.dart | 21 ++++++ apps/cli/lib/src/project/celest_project.dart | 19 ++++- apps/cli/lib/src/pub/project_dependency.dart | 4 +- 7 files changed, 156 insertions(+), 57 deletions(-) create mode 100644 apps/cli/lib/src/analyzer/project_parser.dart diff --git a/apps/cli/CHANGELOG.md b/apps/cli/CHANGELOG.md index 114f53189..0dcc487c1 100644 --- a/apps/cli/CHANGELOG.md +++ b/apps/cli/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT - fix: Improved reload handling +- fix: Ensure all dependencies are correctly resolved ## 1.0.14 diff --git a/apps/cli/lib/src/analyzer/project_parser.dart b/apps/cli/lib/src/analyzer/project_parser.dart new file mode 100644 index 000000000..6b12c6f36 --- /dev/null +++ b/apps/cli/lib/src/analyzer/project_parser.dart @@ -0,0 +1,69 @@ +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart'; +import 'package:celest_cli/src/codegen/client_code_generator.dart'; + +final class CelestProjectParser { + CelestProjectParser({ + required String projectRoot, + required this.projectDart, + required DriverBasedAnalysisContext analysisContext, + }) : _analysisContext = analysisContext, + _dependencies = CodegenDependencies(rootDir: projectRoot); + + final String projectDart; + final DriverBasedAnalysisContext _analysisContext; + final CodegenDependencies _dependencies; + + CodegenDependencies parseDependencies() { + final projectDartLibrary = _analysisContext.currentSession.getParsedLibrary( + projectDart, + ); + if (projectDartLibrary is! ParsedLibraryResult) { + throw StateError('Could not parse $projectDart'); + } + + final topLevelVariables = + projectDartLibrary.units + .expand((unit) => unit.unit.declarations) + .whereType() + .expand((declaration) => declaration.variables.variables) + .toList(); + + if (_findDatabases(topLevelVariables)) { + _dependencies.add('drift_hrana'); + } + if (_findAuth(topLevelVariables)) { + _dependencies.add('celest_cloud_auth'); + _dependencies.add('drift_hrana'); + } + + return _dependencies; + } + + bool _findAuth(List topLevelVariables) { + for (final variable in topLevelVariables) { + if (variable case VariableDeclaration( + initializer: MethodInvocation( + methodName: SimpleIdentifier(name: 'Auth'), + ), + )) { + return true; + } + } + return false; + } + + bool _findDatabases(List topLevelVariables) { + for (final variable in topLevelVariables) { + if (variable case VariableDeclaration( + initializer: MethodInvocation( + methodName: SimpleIdentifier(name: 'Database'), + ), + )) { + return true; + } + } + return false; + } +} diff --git a/apps/cli/lib/src/codegen/client_code_generator.dart b/apps/cli/lib/src/codegen/client_code_generator.dart index 680a4df9a..09e339422 100644 --- a/apps/cli/lib/src/codegen/client_code_generator.dart +++ b/apps/cli/lib/src/codegen/client_code_generator.dart @@ -7,7 +7,13 @@ import 'package:celest_cli/src/codegen/code_generator.dart'; import 'package:celest_cli/src/codegen/code_outputs.dart'; import 'package:celest_cli/src/context.dart'; import 'package:celest_cli/src/project/celest_project.dart'; +import 'package:celest_cli/src/pub/project_dependency.dart'; +import 'package:celest_cli/src/pub/pub_action.dart'; +import 'package:celest_cli/src/pub/pubspec.dart'; import 'package:collection/collection.dart'; +import 'package:logging/logging.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; final class ClientCodeGenerator { ClientCodeGenerator({ @@ -55,4 +61,45 @@ final class CodegenDependencies extends DelegatingSet { static CodegenDependencies get current { return Zone.current[CodegenDependencies] as CodegenDependencies; } + + static final Logger _logger = Logger('CodegenDependencies'); + + Future save() async { + var (pubspec, pubspecYaml) = + p.equals(rootDir, projectPaths.projectRoot) + ? (celestProject.pubspec, celestProject.pubspecYaml) + : (celestProject.clientPubspec, celestProject.clientPubspecYaml); + final pubspecFile = fileSystem.directory(rootDir).childFile('pubspec.yaml'); + final currentDependencies = Set.of(pubspec.dependencies.keys); + final missingDependencies = difference(currentDependencies); + final needsUpdate = missingDependencies.isNotEmpty; + if (needsUpdate) { + _logger.fine( + 'Adding dependencies to ${pubspecFile.path}: ' + '${missingDependencies.toList()}', + ); + pubspec = pubspec.addDeps( + dependencies: { + for (final dependency in this) + dependency: + ProjectDependency.all[dependency]?.pubDependency ?? + HostedDependency(version: VersionConstraint.any), + }, + ); + if (pubspecFile.existsSync()) { + await pubspecFile.writeAsString(pubspec.toYaml(source: pubspecYaml)); + await runPub( + action: PubAction.get, + workingDirectory: pubspecFile.parent.path, + ); + } else { + _logger.fine( + 'Skipping dependency update', + 'Pubspec not found: ${pubspecFile.path}', + StackTrace.current, + ); + } + } + return needsUpdate; + } } diff --git a/apps/cli/lib/src/codegen/code_outputs.dart b/apps/cli/lib/src/codegen/code_outputs.dart index 2acd734e4..abdb3d6ba 100644 --- a/apps/cli/lib/src/codegen/code_outputs.dart +++ b/apps/cli/lib/src/codegen/code_outputs.dart @@ -1,12 +1,6 @@ import 'package:celest_cli/src/codegen/client_code_generator.dart'; import 'package:celest_cli/src/context.dart'; -import 'package:celest_cli/src/pub/project_dependency.dart'; -import 'package:celest_cli/src/pub/pub_action.dart'; -import 'package:celest_cli/src/pub/pubspec.dart'; import 'package:collection/collection.dart'; -import 'package:logging/logging.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; final class CodeOutputs extends DelegatingMap { const CodeOutputs(super.base, {required this.codegenDependencies}); @@ -17,8 +11,6 @@ final class CodeOutputs extends DelegatingMap { /// to ensure referenced types are available. final CodegenDependencies codegenDependencies; - static final Logger _logger = Logger('CodeOutputs'); - Future write() async { final outputFutures = >[]; forEach((path, library) { @@ -32,49 +24,7 @@ final class CodeOutputs extends DelegatingMap { ); }); if (codegenDependencies.isNotEmpty) { - var (pubspec, pubspecYaml) = - p.equals(codegenDependencies.rootDir, projectPaths.projectRoot) - ? (celestProject.pubspec, celestProject.pubspecYaml) - : (celestProject.clientPubspec, celestProject.clientPubspecYaml); - final pubspecFile = fileSystem - .directory(codegenDependencies.rootDir) - .childFile('pubspec.yaml'); - final currentDependencies = Set.of(pubspec.dependencies.keys); - final missingDependencies = codegenDependencies.difference( - currentDependencies, - ); - if (missingDependencies.isNotEmpty) { - _logger.fine( - 'Adding dependencies to ${pubspecFile.path}: ' - '${missingDependencies.toList()}', - ); - pubspec = pubspec.addDeps( - dependencies: { - for (final dependency in codegenDependencies) - dependency: - ProjectDependency.all[dependency]?.pubDependency ?? - HostedDependency(version: VersionConstraint.any), - }, - ); - if (pubspecFile.existsSync()) { - outputFutures.add( - pubspecFile.writeAsString(pubspec.toYaml(source: pubspecYaml)).then( - (_) { - return runPub( - action: PubAction.get, - workingDirectory: pubspecFile.parent.path, - ); - }, - ), - ); - } else { - _logger.fine( - 'Skipping dependency update', - 'Pubspec not found: ${pubspecFile.path}', - StackTrace.current, - ); - } - } + outputFutures.add(codegenDependencies.save()); } await Future.wait(outputFutures); } diff --git a/apps/cli/lib/src/frontend/celest_frontend.dart b/apps/cli/lib/src/frontend/celest_frontend.dart index 776205e89..f66d87050 100644 --- a/apps/cli/lib/src/frontend/celest_frontend.dart +++ b/apps/cli/lib/src/frontend/celest_frontend.dart @@ -11,6 +11,7 @@ import 'package:celest_ast/celest_ast.dart' hide Sdk; import 'package:celest_cli/src/analyzer/analysis_error.dart'; import 'package:celest_cli/src/analyzer/analysis_result.dart'; import 'package:celest_cli/src/analyzer/celest_analyzer.dart'; +import 'package:celest_cli/src/analyzer/project_parser.dart'; import 'package:celest_cli/src/ast/project_diff.dart'; import 'package:celest_cli/src/cli/stop_signal.dart'; import 'package:celest_cli/src/codegen/api/dockerfile_generator.dart'; @@ -334,6 +335,8 @@ final class CelestFrontend with CloudRepository { _logErrors(errors); } + await _updateDependencies(); + final analysisResult = await _analyzeProject( migrateProject: migrateProject, ); @@ -757,6 +760,24 @@ final class CelestFrontend with CloudRepository { } } + /// Parses the project to look for any required analysis dependencies. + Future _updateDependencies() => + performance.trace('CelestFrontend', 'updateDependencies', () async { + logger.fine('Parsing project...'); + final parser = CelestProjectParser( + projectRoot: projectPaths.projectRoot, + projectDart: projectPaths.projectDart, + analysisContext: celestProject.analysisContext, + ); + final dependencies = parser.parseDependencies(); + if (dependencies.isNotEmpty) { + if (await dependencies.save()) { + celestProject.invalidatePubspec(); + } + } + stopSignal.check(); + }); + /// Analyzes the project and reports if there are any errors. Future _analyzeProject({ required bool migrateProject, diff --git a/apps/cli/lib/src/project/celest_project.dart b/apps/cli/lib/src/project/celest_project.dart index f02f9173a..3b406768a 100644 --- a/apps/cli/lib/src/project/celest_project.dart +++ b/apps/cli/lib/src/project/celest_project.dart @@ -137,7 +137,7 @@ final class CelestProject { AnalysisOptions get analysisOptions => _analysisOptions; final ByteStore _byteStore; - late final _analysisContextCollection = AnalysisContextCollectionImpl( + late var _analysisContextCollection = AnalysisContextCollectionImpl( includedPaths: [projectPaths.projectRoot], sdkPath: Sdk.current.sdkPath, // Needed for collecting subtypes. @@ -170,8 +170,8 @@ final class CelestProject { final ParentProject? parentProject; /// The [AnalysisContext] for the current project. - late final DriverBasedAnalysisContext analysisContext = - _analysisContextCollection.contextFor(projectPaths.projectDart); + late DriverBasedAnalysisContext analysisContext = _analysisContextCollection + .contextFor(projectPaths.projectDart); /// The [CelestConfig] for the current project. final CelestConfig config; @@ -221,6 +221,19 @@ final class CelestProject { return changedFiles.toSet(); } + void invalidatePubspec() { + _analysisContextCollection = AnalysisContextCollectionImpl( + includedPaths: [projectPaths.projectRoot], + sdkPath: Sdk.current.sdkPath, + // Needed for collecting subtypes. + enableIndex: true, + byteStore: _byteStore, + ); + analysisContext = _analysisContextCollection.contextFor( + projectPaths.projectDart, + ); + } + /// The name of the project as declared in the `project.dart` file. /// /// We parse the `project.dart` file to retrieve it since it is cheap and diff --git a/apps/cli/lib/src/pub/project_dependency.dart b/apps/cli/lib/src/pub/project_dependency.dart index d6372d765..4e006ed46 100644 --- a/apps/cli/lib/src/pub/project_dependency.dart +++ b/apps/cli/lib/src/pub/project_dependency.dart @@ -107,9 +107,7 @@ final class ProjectDependency { 'celest_cloud_auth', DependencyType.dependency, HostedDependency( - version: VersionConstraint.compatibleWith( - Version.parse('0.3.0').firstPreRelease, - ), + version: VersionConstraint.compatibleWith(Version.parse('0.3.0')), ), );