diff --git a/apps/cli/fixtures/fixtures_test.dart b/apps/cli/fixtures/fixtures_test.dart index 54c5eb94e..1db25fc14 100644 --- a/apps/cli/fixtures/fixtures_test.dart +++ b/apps/cli/fixtures/fixtures_test.dart @@ -180,6 +180,7 @@ class TestRunner { projectRoot: projectRoot, projectName: celestProject.projectName, parentProject: parentProject, + upgradeFromVersion: null, ); await (_warmUp(projectRoot), migrator.migrate()).wait; }); diff --git a/apps/cli/lib/src/analyzer/celest_analysis_helpers.dart b/apps/cli/lib/src/analyzer/celest_analysis_helpers.dart index b94070b70..cc11b43be 100644 --- a/apps/cli/lib/src/analyzer/celest_analysis_helpers.dart +++ b/apps/cli/lib/src/analyzer/celest_analysis_helpers.dart @@ -11,6 +11,7 @@ import 'package:analyzer/src/dart/resolver/scope.dart'; import 'package:celest_cli/src/analyzer/analysis_error.dart'; import 'package:celest_cli/src/analyzer/resolver/project_resolver.dart'; import 'package:celest_cli/src/context.dart'; +import 'package:celest_cli/src/init/edits/source_edit.dart'; import 'package:celest_cli/src/utils/analyzer.dart'; import 'package:celest_cli/src/utils/json.dart'; import 'package:logging/logging.dart'; @@ -27,31 +28,6 @@ enum CustomType { }; } -final class SourceEdit { - const SourceEdit(this.offset, this.length, this.replacement); - - final int offset; - final int length; - final String replacement; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - return other is SourceEdit && - other.offset == offset && - other.length == length && - other.replacement == replacement; - } - - @override - int get hashCode => Object.hash(offset, length, replacement); - - @override - String toString() { - return 'SourceEdit(offset: $offset, length: $length, replacement: $replacement)'; - } -} - mixin CelestAnalysisHelpers implements CelestErrorReporter { AnalysisContext get context; Set get customExceptionTypes; diff --git a/apps/cli/lib/src/analyzer/celest_analyzer.dart b/apps/cli/lib/src/analyzer/celest_analyzer.dart index 232d89530..7743a3d4d 100644 --- a/apps/cli/lib/src/analyzer/celest_analyzer.dart +++ b/apps/cli/lib/src/analyzer/celest_analyzer.dart @@ -17,6 +17,7 @@ import 'package:celest_cli/src/ast/ast.dart'; import 'package:celest_cli/src/config/feature_flags.dart'; import 'package:celest_cli/src/context.dart'; import 'package:celest_cli/src/database/cache/cache_database.dart'; +import 'package:celest_cli/src/init/edits/source_edit_applier.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/pub_environment.dart'; @@ -25,7 +26,6 @@ import 'package:celest_cli/src/sdk/dart_sdk.dart'; import 'package:celest_cli/src/types/type_helper.dart'; import 'package:celest_cli/src/utils/analyzer.dart'; import 'package:celest_cli/src/utils/reference.dart'; -import 'package:collection/collection.dart'; import 'package:logging/logging.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:source_span/source_span.dart'; @@ -583,47 +583,7 @@ const project = Project(name: 'cache_warmup'); } Future _applyMigrations() async { - if (resolver.pendingEdits.isEmpty) { - return; - } - - _logger.fine('Applying ${resolver.pendingEdits.length} migrations'); - - final fileChanges = >[]; - for (final entry in resolver.pendingEdits.entries) { - final path = entry.key; - - // Sort edits in reserve order to avoid offset changes. - final edits = entry.value.sorted((a, b) { - return -a.offset.compareTo(b.offset); - }); - - _logger.finest('Applying migrations to $path: $edits'); - - final file = context.currentSession.getFile(path) as FileResult; - var source = file.content; - - for (final edit in edits) { - source = source.replaceRange( - edit.offset, - edit.offset + edit.length, - edit.replacement, - ); - } - - fileChanges.add(fileSystem.file(path).writeAsString(source)); - } - - await Future.wait(fileChanges); - - _logger.finest('Applied migrations to disk'); - - for (final path in pendingEdits.keys) { - context.changeFile(path); - } - final changes = await context.applyPendingFileChanges(); - - _logger.finest('Applied changes in analyzer: $changes'); + await SourceEditApplier(resolver.pendingEdits).apply(); } } diff --git a/apps/cli/lib/src/analyzer/resolver/project_resolver.dart b/apps/cli/lib/src/analyzer/resolver/project_resolver.dart index a9d98cd6e..3a32b8290 100644 --- a/apps/cli/lib/src/analyzer/resolver/project_resolver.dart +++ b/apps/cli/lib/src/analyzer/resolver/project_resolver.dart @@ -16,6 +16,7 @@ import 'package:celest_cli/src/analyzer/celest_analysis_helpers.dart'; import 'package:celest_cli/src/analyzer/resolver/config_value_resolver.dart'; import 'package:celest_cli/src/config/feature_flags.dart'; import 'package:celest_cli/src/context.dart'; +import 'package:celest_cli/src/init/edits/source_edit.dart'; import 'package:celest_cli/src/sdk/dart_sdk.dart'; import 'package:celest_cli/src/serialization/common.dart'; import 'package:celest_cli/src/serialization/serialization_verdict.dart'; diff --git a/apps/cli/lib/src/commands/uninstall/celest_uninstaller.dart b/apps/cli/lib/src/commands/uninstall/celest_uninstaller.dart index f84bf017a..89c481f66 100644 --- a/apps/cli/lib/src/commands/uninstall/celest_uninstaller.dart +++ b/apps/cli/lib/src/commands/uninstall/celest_uninstaller.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:celest_cli/src/cli/cli_runtime.dart'; import 'package:celest_cli/src/context.dart'; import 'package:celest_cli/src/exceptions.dart'; import 'package:celest_cli/src/utils/error.dart'; @@ -17,12 +18,14 @@ class CelestUninstaller { Future uninstall() async { await removeConfig(); - if (fileSystem.path.fromUri(platform.script).endsWith('.snapshot')) { - await _uninstallPubGlobal(); - } else if (platform.executable.contains('dart')) { - // Celest is running from source. Nothing to uninstall. - } else { - await _uninstallAot(); + switch (CliRuntime.current) { + case CliRuntime.pubGlobal: + await _uninstallPubGlobal(); + case CliRuntime.local: + // Celest is running from source. Nothing to uninstall. + break; + case CliRuntime.aot: + await _uninstallAot(); } } diff --git a/apps/cli/lib/src/database/cache/cache_database.dart b/apps/cli/lib/src/database/cache/cache_database.dart index 34a5ca321..dca04c7ed 100644 --- a/apps/cli/lib/src/database/cache/cache_database.dart +++ b/apps/cli/lib/src/database/cache/cache_database.dart @@ -69,12 +69,12 @@ final class CacheDatabase extends $CacheDatabase { await database.clear(); await database._setVersionInfo(update: true); } - if (semver.Version.parse(celest) < semver.Version.parse(packageVersion)) { - database._needsProjectUpgrade = true; + final upgradeFromVersion = semver.Version.parse(celest); + database._upgradeFromVersion = upgradeFromVersion; + if (upgradeFromVersion < currentVersion) { await database._setVersionInfo(update: true); } } else if (versionInfo == null) { - database._needsProjectUpgrade = true; await database._setVersionInfo(update: false); } final rawDb = await rawCompleter.future; @@ -82,8 +82,10 @@ final class CacheDatabase extends $CacheDatabase { return database; } - bool _needsProjectUpgrade = false; - bool get needsProjectUpgrade => _needsProjectUpgrade; + semver.Version? _upgradeFromVersion; + + /// The version of the Celest package before the current one. + semver.Version? get upgradeFromVersion => _upgradeFromVersion; Future _setVersionInfo({required bool update}) async { if (update) { diff --git a/apps/cli/lib/src/init/edits/source_edit.dart b/apps/cli/lib/src/init/edits/source_edit.dart new file mode 100644 index 000000000..f5fd50b0a --- /dev/null +++ b/apps/cli/lib/src/init/edits/source_edit.dart @@ -0,0 +1,24 @@ +final class SourceEdit { + const SourceEdit(this.offset, this.length, this.replacement); + + final int offset; + final int length; + final String replacement; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is SourceEdit && + other.offset == offset && + other.length == length && + other.replacement == replacement; + } + + @override + int get hashCode => Object.hash(offset, length, replacement); + + @override + String toString() { + return 'SourceEdit(offset: $offset, length: $length, replacement: $replacement)'; + } +} diff --git a/apps/cli/lib/src/init/edits/source_edit_applier.dart b/apps/cli/lib/src/init/edits/source_edit_applier.dart new file mode 100644 index 000000000..23b0bf65b --- /dev/null +++ b/apps/cli/lib/src/init/edits/source_edit_applier.dart @@ -0,0 +1,60 @@ +import 'package:celest_cli/src/context.dart'; +import 'package:celest_cli/src/init/edits/source_edit.dart'; +import 'package:collection/collection.dart'; +import 'package:logging/logging.dart'; + +final class SourceEditApplier { + SourceEditApplier(this.edits); + + final Map> edits; + + static final Logger _logger = Logger('SourceEditApplier'); + + Future _applyEdits(String path, List edits) async { + _logger.finest('Applying migrations to $path: $edits'); + + var source = await fileSystem.file(path).readAsString(); + + for (final edit in edits) { + source = source.replaceRange( + edit.offset, + edit.offset + edit.length, + edit.replacement, + ); + } + + await fileSystem.file(path).writeAsString(source); + } + + /// Applies all pendings edits to disk. + Future apply() async { + if (edits.isEmpty) { + _logger.fine('No edits to apply'); + return; + } + + _logger.fine('Applying ${edits.length} migrations'); + + final fileChanges = >[]; + for (final entry in edits.entries) { + final path = entry.key; + if (entry.value.isEmpty) { + _logger.fine('No edits to apply to $path'); + continue; + } + + // Sort edits in reserve order to avoid offset changes. + final edits = entry.value.sorted((a, b) { + return -a.offset.compareTo(b.offset); + }); + + fileChanges.add(_applyEdits(path, edits)); + } + + await Future.wait(fileChanges); + + _logger.finest('Applied migrations to disk'); + + await celestProject.invalidate(edits.keys); + } +} diff --git a/apps/cli/lib/src/init/migrations/pubspec_updater.dart b/apps/cli/lib/src/init/migrations/pubspec_updater.dart index 97aa06000..5dfa9bdff 100644 --- a/apps/cli/lib/src/init/migrations/pubspec_updater.dart +++ b/apps/cli/lib/src/init/migrations/pubspec_updater.dart @@ -13,12 +13,18 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml_edit/yaml_edit.dart' hide SourceEdit; final class PubspecUpdater extends ProjectMigration { - const PubspecUpdater(super.projectRoot, this.parentProject, this.projectName); + const PubspecUpdater( + super.projectRoot, + this.parentProject, + this.projectName, { + this.upgradeFromVersion, + }); static final _logger = Logger('PubspecUpdater'); final ParentProject? parentProject; final String projectName; + final Version? upgradeFromVersion; @override String get name => 'core.project.pubspec'; @@ -55,7 +61,7 @@ final class PubspecUpdater extends ProjectMigration { } /// Returns the version updated from. - Future _updateBackendDependencies({ + Future _updateBackendDependencies({ required Pubspec pubspec, required String pubspecYaml, required File pubspecFile, @@ -65,16 +71,8 @@ final class PubspecUpdater extends ProjectMigration { if (ProjectDependency.backendDependencies.upToDate(pubspec) && currentSdkVersion == requiredSdkVersion) { _logger.fine('Project dependencies are up to date.'); - return null; + return; } - final fromVersion = switch (pubspec.dependencies['celest']) { - final HostedDependency hosted => switch (hosted.version) { - final Version version => version, - final VersionRange range => range.min, - _ => Version.none, - }, - _ => Version.none, - }; _logger.fine('Updating project dependencies to latest versions...'); pubspec = pubspec.copyWith( environment: {'sdk': PubEnvironment.dartSdkConstraint}, @@ -95,7 +93,6 @@ final class PubspecUpdater extends ProjectMigration { ); pubspecYaml = pubspec.toYaml(source: pubspecYaml); await pubspecFile.writeAsString(pubspecYaml); - return fromVersion; } Future _updateClientDependencies({ @@ -173,14 +170,14 @@ final class PubspecUpdater extends ProjectMigration { final pubspecFile = fileSystem.file(p.join(projectRoot, 'pubspec.yaml')); final pubspecYaml = await pubspecFile.readAsString(); final pubspec = Pubspec.parse(pubspecYaml); - final fromVersion = await _updateBackendDependencies( + await _updateBackendDependencies( pubspec: pubspec, pubspecYaml: pubspecYaml, pubspecFile: pubspecFile, ); // await _updateProjectName(); - needsAnalyzerMigration |= - fromVersion != null && fromVersion < Version(1, 0, 0).firstPreRelease; + needsAnalyzerMigration |= upgradeFromVersion != null && + upgradeFromVersion! < Version(1, 0, 0).firstPreRelease; if (needsAnalyzerMigration) { operations.add( runPub(action: PubAction.get, workingDirectory: projectRoot), diff --git a/apps/cli/lib/src/init/project_init.dart b/apps/cli/lib/src/init/project_init.dart index 3bbb7b78f..3157b6dd5 100644 --- a/apps/cli/lib/src/init/project_init.dart +++ b/apps/cli/lib/src/init/project_init.dart @@ -128,6 +128,49 @@ base mixin Configure on CelestCommand { /// Returns true if the project needs to be migrated. Stream _configure() async* { + final (projectName, projectRoot, isExistingProject, parentProject) = + await _locateProject(); + + yield const Initializing(); + await init(projectRoot: projectRoot, parentProject: parentProject); + + var needsAnalyzerMigration = false; + Future? upgradePackages; + if (!isExistingProject) { + if (this case final ProjectCreator projectCreator) { + yield const CreatingProject(); + await projectCreator.createProject( + projectName: projectName!, + parentProject: parentProject, + ); + yield const CreatedProject(); + } else { + _throwNoProject(); + } + } else if (this case final Migrate projectMigrator) { + yield const MigratingProject(); + final result = await projectMigrator.migrateProject( + parentProject: parentProject, + upgradeFromVersion: celestProject.cacheDb.upgradeFromVersion, + ); + needsAnalyzerMigration = result.needsAnalyzerMigration; + await (upgradePackages = _pubUpgrade()); + if (result.needsDartFix && parentProject != null) { + await processManager.run([ + Sdk.current.dart, + 'fix', + '--apply', + ], workingDirectory: parentProject.path); + } + yield const MigratedProject(); + } + await (upgradePackages ??= _pubUpgrade()); + + yield Initialized(needsAnalyzerMigration: needsAnalyzerMigration); + } + + Future<(String? name, String root, bool, ParentProject?)> + _locateProject() async { var currentDir = fileSystem.currentDirectory; final currentDirIsEmpty = await currentDir.list().isEmpty; @@ -243,41 +286,7 @@ base mixin Configure on CelestCommand { }; } - yield const Initializing(); - await init(projectRoot: projectRoot, parentProject: parentProject); - - final postUpgrade = celestProject.cacheDb.needsProjectUpgrade; - var needsAnalyzerMigration = false; - Future? upgradePackages; - if (!isExistingProject) { - if (this case final ProjectCreator projectCreator) { - yield const CreatingProject(); - await projectCreator.createProject( - projectName: projectName!, - parentProject: parentProject, - ); - yield const CreatedProject(); - } else { - _throwNoProject(); - } - } else if (this case final Migrate projectMigrator) { - yield const MigratingProject(); - needsAnalyzerMigration = await projectMigrator.migrateProject( - parentProject: parentProject, - ); - await (upgradePackages = _pubUpgrade()); - if (postUpgrade && parentProject != null) { - await processManager.run([ - Sdk.current.dart, - 'fix', - '--apply', - ], workingDirectory: parentProject.path); - } - yield const MigratedProject(); - } - await (upgradePackages ??= _pubUpgrade()); - - yield Initialized(needsAnalyzerMigration: needsAnalyzerMigration); + return (projectName, projectRoot, isExistingProject, parentProject); } // TODO(dnys1): Improve logic here so that we don't run pub upgrade if diff --git a/apps/cli/lib/src/init/project_migrate.dart b/apps/cli/lib/src/init/project_migrate.dart index c0eae8632..450e382ec 100644 --- a/apps/cli/lib/src/init/project_migrate.dart +++ b/apps/cli/lib/src/init/project_migrate.dart @@ -1,19 +1,25 @@ import 'package:celest_cli/src/context.dart'; import 'package:celest_cli/src/init/project_init.dart'; +import 'package:celest_cli/src/init/project_migration.dart'; import 'package:celest_cli/src/init/project_migrator.dart'; import 'package:celest_cli/src/project/celest_project.dart'; +import 'package:pub_semver/pub_semver.dart'; base mixin Migrate on Configure { - Future migrateProject({required ParentProject? parentProject}) async { + Future migrateProject({ + required ParentProject? parentProject, + required Version? upgradeFromVersion, + }) async { logger.finest('Migrating project at "${projectPaths.projectRoot}"...'); return performance.trace('Migrate', 'migrateProject', () async { final result = await ProjectMigrator( parentProject: parentProject, projectRoot: projectPaths.projectRoot, projectName: celestProject.projectName, + upgradeFromVersion: upgradeFromVersion, ).migrate(); logger.fine('Project migration result: $result'); - return result.needsAnalyzerMigration; + return result; }); } } diff --git a/apps/cli/lib/src/init/project_migration.dart b/apps/cli/lib/src/init/project_migration.dart index c8c5a8bf3..dd0bf889b 100644 --- a/apps/cli/lib/src/init/project_migration.dart +++ b/apps/cli/lib/src/init/project_migration.dart @@ -19,24 +19,35 @@ sealed class ProjectMigrationResult { const ProjectMigrationResult(); bool get needsAnalyzerMigration => false; + bool get needsDartFix => false; ProjectMigrationResult operator &(ProjectMigrationResult other); } final class ProjectMigrationSuccess extends ProjectMigrationResult { - const ProjectMigrationSuccess({this.needsAnalyzerMigration = false}); + const ProjectMigrationSuccess({ + this.needsAnalyzerMigration = false, + this.needsDartFix = false, + }); @override final bool needsAnalyzerMigration; + @override + final bool needsDartFix; + @override ProjectMigrationResult operator &(ProjectMigrationResult other) { return switch (other) { ProjectMigrationSkipped() => this, - ProjectMigrationSuccess(:final needsAnalyzerMigration) => + ProjectMigrationSuccess( + :final needsAnalyzerMigration, + :final needsDartFix + ) => ProjectMigrationSuccess( needsAnalyzerMigration: this.needsAnalyzerMigration || needsAnalyzerMigration, + needsDartFix: this.needsDartFix || needsDartFix, ), }; } @@ -73,8 +84,14 @@ final class ProjectMigrationReport { final buffer = StringBuffer('ProjectMigrationReport(\n'); for (final entry in migrations.entries) { final value = switch (entry.value) { - ProjectMigrationSuccess(needsAnalyzerMigration: false) => 'success', - ProjectMigrationSuccess(needsAnalyzerMigration: true) => 'partial', + ProjectMigrationSuccess( + needsAnalyzerMigration: false, + needsDartFix: false + ) => + 'success', + ProjectMigrationSuccess(needsAnalyzerMigration: true) || + ProjectMigrationSuccess(needsDartFix: true) => + 'partial', ProjectMigrationSkipped() => 'skipped', }; buffer.writeln(' ${entry.key}: $value,'); diff --git a/apps/cli/lib/src/init/project_migrator.dart b/apps/cli/lib/src/init/project_migrator.dart index a8286ac4a..dfa5fcdf7 100644 --- a/apps/cli/lib/src/init/project_migrator.dart +++ b/apps/cli/lib/src/init/project_migrator.dart @@ -5,6 +5,7 @@ import 'package:celest_cli/src/init/migrations/pubspec_updater.dart'; import 'package:celest_cli/src/init/project_migration.dart'; import 'package:celest_cli/src/project/celest_project.dart'; import 'package:logging/logging.dart'; +import 'package:pub_semver/pub_semver.dart'; /// Manages the migration of a Celest project to the latest version. class ProjectMigrator { @@ -12,6 +13,7 @@ class ProjectMigrator { required this.projectRoot, required this.projectName, required this.parentProject, + required this.upgradeFromVersion, }); /// The root directory of the enclosing Flutter project. @@ -27,6 +29,11 @@ class ProjectMigrator { /// The name of the project, as defined by the user. final String projectName; + /// The version of the Celest SDK that the project is being upgraded from. + /// + /// This is used to determine if certain migrations need to be performed. + final Version? upgradeFromVersion; + static final Logger _logger = Logger('ProjectMigrator'); /// Generates a new Celest project. @@ -34,7 +41,12 @@ class ProjectMigrator { /// Returns `true` if the project needs further migration by the analyzer. Future migrate() async { final migrations = [ - PubspecUpdater(projectRoot, parentProject, projectName), + PubspecUpdater( + projectRoot, + parentProject, + projectName, + upgradeFromVersion: upgradeFromVersion, + ), if (parentProject case ParentProject( path: final appRoot, diff --git a/apps/cli/lib/src/project/celest_project.dart b/apps/cli/lib/src/project/celest_project.dart index cf9ba0446..1c136c39b 100644 --- a/apps/cli/lib/src/project/celest_project.dart +++ b/apps/cli/lib/src/project/celest_project.dart @@ -170,9 +170,7 @@ final class CelestProject { /// The [AnalysisContext] for the current project. late final DriverBasedAnalysisContext analysisContext = - _analysisContextCollection.contextFor( - p.join(projectPaths.projectRoot, 'project.dart'), - ); + _analysisContextCollection.contextFor(projectPaths.projectDart); /// The [CelestConfig] for the current project. final CelestConfig config;