From 520251d2b0b97c34eb7a462f91bf753616e2f5b8 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 6 Jan 2025 18:03:03 +0100 Subject: [PATCH 1/8] feat!: Migrate to use the Pub workspaces feature --- melos.yaml | 43 +- packages/conventional_commit/pubspec.yaml | 5 +- .../lib/src/command_runner/bootstrap.dart | 6 - .../melos/lib/src/commands/bootstrap.dart | 398 +++----------- packages/melos/lib/src/commands/clean.dart | 15 - packages/melos/lib/src/commands/runner.dart | 2 - packages/melos/lib/src/package.dart | 13 + packages/melos/lib/src/workspace.dart | 14 +- packages/melos/pubspec.yaml | 43 +- packages/melos/test/command_runner_test.dart | 2 + .../melos/test/commands/analyze_test.dart | 16 +- .../melos/test/commands/bootstrap_test.dart | 511 ++---------------- packages/melos/test/commands/clean_test.dart | 56 -- packages/melos/test/commands/exec_test.dart | 40 +- packages/melos/test/commands/format_test.dart | 6 +- packages/melos/test/commands/list_test.dart | 210 +++---- .../melos/test/commands/publish_test.dart | 1 + packages/melos/test/commands/run_test.dart | 170 ++---- packages/melos/test/commands/script_test.dart | 3 + .../melos/test/commands/version_test.dart | 1 + packages/melos/test/mock_fs.dart | 89 --- packages/melos/test/mock_workspace_fs.dart | 183 ------- packages/melos/test/package_filter_test.dart | 13 +- packages/melos/test/package_test.dart | 29 +- packages/melos/test/pubspec_extension.dart | 6 + packages/melos/test/utils.dart | 47 +- packages/melos/test/utils_test.dart | 4 +- packages/melos/test/workspace_test.dart | 219 +++++--- pubspec.yaml | 9 +- 29 files changed, 641 insertions(+), 1513 deletions(-) delete mode 100644 packages/melos/test/commands/clean_test.dart delete mode 100644 packages/melos/test/mock_fs.dart delete mode 100644 packages/melos/test/mock_workspace_fs.dart diff --git a/melos.yaml b/melos.yaml index 874ed8bfe..26647835d 100644 --- a/melos.yaml +++ b/melos.yaml @@ -11,35 +11,38 @@ categories: command: bootstrap: environment: - sdk: ">=3.2.0 <4.0.0" + sdk: ^3.6.0 dependencies: ansi_styles: ^0.3.2+1 - args: ^2.4.2 + args: ^2.6.0 cli_launcher: ^0.3.1 - cli_util: ^0.4.1 - collection: ^1.18.0 + cli_util: ^0.4.2 + collection: ^1.19.0 conventional_commit: ^0.6.0+1 - file: ^7.0.0 + file: ^7.0.1 glob: ^2.1.2 - graphs: ^2.3.1 - http: ^1.1.0 - meta: ^1.10.0 + graphs: ^2.3.2 + http: ^1.2.2 + meta: ^1.16.0 mustache_template: ^2.0.0 - path: ^1.8.3 - platform: ^3.1.4 + path: ^1.9.1 + platform: ^3.1.6 pool: ^1.5.1 prompts: ^2.0.0 - pub_semver: ^2.1.4 - pub_updater: ^0.4.0 - pubspec_parse: ^1.4.0 - string_scanner: ^1.2.0 - yaml: ^3.1.2 - yaml_edit: ^2.1.1 + pub_semver: ^2.1.5 + pub_updater: ^0.5.0 + pubspec_parse: + git: + url: https://github.com/dart-lang/tools.git + path: pkgs/pubspec_parse + string_scanner: ^1.4.1 + yaml: ^3.1.3 + yaml_edit: ^2.2.2 dev_dependencies: - mockito: ^5.4.2 - test: ^1.24.9 - path: ^1.9.0 - yaml: ^3.1.2 + mockito: ^5.4.5 + test: ^1.25.14 + path: ^1.9.1 + yaml: ^3.1.3 version: # Generate commit links in package changelogs. linkToCommits: true diff --git a/packages/conventional_commit/pubspec.yaml b/packages/conventional_commit/pubspec.yaml index e335a8f0b..92dbfce90 100644 --- a/packages/conventional_commit/pubspec.yaml +++ b/packages/conventional_commit/pubspec.yaml @@ -4,9 +4,10 @@ description: version: 0.6.0+1 repository: https://github.com/invertase/melos/tree/main/packages/conventional_commit issue_tracker: https://github.com/invertase/melos/issues +resolution: workspace environment: - sdk: ">=3.2.0 <4.0.0" + sdk: ^3.6.0 dev_dependencies: - test: ^1.24.9 + test: ^1.25.14 diff --git a/packages/melos/lib/src/command_runner/bootstrap.dart b/packages/melos/lib/src/command_runner/bootstrap.dart index 7074950c4..ef7716cf0 100644 --- a/packages/melos/lib/src/command_runner/bootstrap.dart +++ b/packages/melos/lib/src/command_runner/bootstrap.dart @@ -18,11 +18,6 @@ class BootstrapCommand extends MelosCommand { '--no-enforce-lockfile can be used to temporarily disregard the ' 'lockfile versions.', ); - argParser.addFlag( - 'skip-linking', - negatable: false, - help: 'Skips locally linking workspace packages.', - ); argParser.addFlag( 'offline', negatable: false, @@ -50,7 +45,6 @@ class BootstrapCommand extends MelosCommand { packageFilters: parsePackageFilters(config.path), enforceLockfile: argResults?['enforce-lockfile'] as bool?, noExample: argResults?['no-example'] as bool, - skipLinking: argResults?['skip-linking'] as bool, offline: argResults?['offline'] as bool, ); } diff --git a/packages/melos/lib/src/commands/bootstrap.dart b/packages/melos/lib/src/commands/bootstrap.dart index e22e5d18a..fe891e691 100644 --- a/packages/melos/lib/src/commands/bootstrap.dart +++ b/packages/melos/lib/src/commands/bootstrap.dart @@ -6,7 +6,6 @@ mixin _BootstrapMixin on _CleanMixin { PackageFilters? packageFilters, bool noExample = false, bool? enforceLockfile, - bool skipLinking = false, bool offline = false, }) async { final workspace = @@ -25,28 +24,20 @@ mixin _BootstrapMixin on _CleanMixin { final shouldEnforceLockfile = (enforceLockfile ?? enforceLockfileConfigValue) && hasLockFile; - final pubCommandForLogging = [ - ...pubCommandExecArgs( - useFlutter: workspace.isFlutterWorkspace, - workspace: workspace, - ), - 'get', - if (noExample) '--no-example', - if (runOffline) '--offline', - if (shouldEnforceLockfile) '--enforce-lockfile', - ].join(' '); + final pubCommandForLogging = _buildPubGetCommand( + workspace: workspace, + noExample: noExample, + runOffline: runOffline, + enforceLockfile: shouldEnforceLockfile, + ).join(' '); logger ..command('melos bootstrap') ..child(targetStyle(workspace.path)) ..newLine(); - if (!utils.isCI && workspace.filteredPackages.keys.length > 20) { - logger.warning( - 'Note: this may take a while in large workspaces such as this one.', - label: false, - ); - } + final filteredPackages = workspace.filteredPackages.values; + // TODO(spydon): Add resolution: workspace to pubspecs try { if (bootstrapCommandConfig.environment != null || @@ -54,7 +45,6 @@ mixin _BootstrapMixin on _CleanMixin { bootstrapCommandConfig.devDependencies != null) { logger.log('Updating common dependencies in workspace packages...'); - final filteredPackages = workspace.filteredPackages.values; await Stream.fromIterable(filteredPackages).parallel((package) { return _setSharedDependenciesForPackage( package, @@ -69,21 +59,20 @@ mixin _BootstrapMixin on _CleanMixin { ..newLine(); } - if (!skipLinking) { - logger.log( - 'Running "$pubCommandForLogging" in workspace packages...', - ); + logger.log( + 'Running "$pubCommandForLogging" in workspace packages...', + ); - await _linkPackagesWithPubspecOverrides( - workspace, - enforceLockfile: shouldEnforceLockfile, - noExample: noExample, - ); + await _runPubGetForWorkspace( + workspace, + noExample: noExample, + runOffline: runOffline, + enforceLockfile: shouldEnforceLockfile, + ); - logger - ..child(successLabel, prefix: '> ') - ..newLine(); - } + logger + ..child(successLabel, prefix: '> ') + ..newLine(); } on BootstrapException catch (exception) { _logBootstrapException(exception, workspace); rethrow; @@ -105,136 +94,59 @@ mixin _BootstrapMixin on _CleanMixin { ); } - Future _linkPackagesWithPubspecOverrides( + Future _runPubGetForWorkspace( MelosWorkspace workspace, { - required bool enforceLockfile, required bool noExample, + required bool runOffline, + required bool enforceLockfile, }) async { - final filteredPackages = workspace.filteredPackages.values; + await runPubGetForPackage( + workspace, + workspace.rootPackage, + noExample: noExample, + runOffline: runOffline, + enforceLockfile: enforceLockfile, + ); + final filteredPackages = workspace.filteredPackages.values; await Stream.fromIterable(filteredPackages).parallel( (package) async { - if (package.isExample) { - final enclosingPackage = package.enclosingPackage!; - if (enclosingPackage.isFlutterPackage && - filteredPackages.contains(enclosingPackage)) { - // This package will be bootstrapped as part of bootstrapping - // the enclosing package. - return; - } - } - - final bootstrappedPackages = [package]; - await _generatePubspecOverrides(workspace, package); - if (package.isFlutterPackage) { - final example = package.examplePackage; - if (example != null && filteredPackages.contains(example)) { - // The flutter tool bootstraps the example package as part of - // bootstrapping the enclosing package, so we need to generate - // the pubspec overrides for the example package as well. - await _generatePubspecOverrides(workspace, example); - bootstrappedPackages.add(example); - } - } - await _runPubGetForPackage( + await runPubGetForPackage( workspace, package, - enforceLockfile: enforceLockfile, noExample: noExample, + runOffline: runOffline, + enforceLockfile: enforceLockfile, ); - bootstrappedPackages.forEach(_logBootstrapSuccess); + _logPackagePubGetSuccess(package); }, parallelism: workspace.config.commands.bootstrap.runPubGetInParallel ? null : 1, ).drain(); } - Future _generatePubspecOverrides( - MelosWorkspace workspace, - Package package, - ) async { - final allTransitiveDependencies = - package.allTransitiveDependenciesInWorkspace; - final melosDependencyOverrides = {}; - - // Traversing all packages so that transitive dependencies for the - // bootstrapped packages are setup properly. - for (final otherPackage in workspace.allPackages.values) { - if (allTransitiveDependencies.containsKey(otherPackage.name)) { - melosDependencyOverrides[otherPackage.name] = PathDependency( - utils.relativePath(otherPackage.path, package.path), - ); - } - } - - // Add custom workspace overrides. - for (final dependencyOverride - in workspace.dependencyOverridePackages.values) { - melosDependencyOverrides[dependencyOverride.name] = PathDependency( - utils.relativePath(dependencyOverride.path, package.path), - ); - } - - // Add existing dependency overrides from pubspec.yaml last, overwriting - // overrides that would be made by Melos, to provide granular control at a - // package level. - melosDependencyOverrides.addAll(package.pubspec.dependencyOverrides); - - // Load current pubspec_overrides.yaml. - final pubspecOverridesFile = - utils.pubspecOverridesPathForDirectory(package.path); - final pubspecOverridesContents = fileExists(pubspecOverridesFile) - ? await readTextFileAsync(pubspecOverridesFile) - : null; - - // Write new version of pubspec_overrides.yaml if it has changed. - final updatedPubspecOverridesContents = mergeMelosPubspecOverrides( - melosDependencyOverrides, - pubspecOverridesContents, - ); - if (updatedPubspecOverridesContents != null) { - if (updatedPubspecOverridesContents.isEmpty) { - deleteEntry(pubspecOverridesFile); - } else { - await writeTextFileAsync( - pubspecOverridesFile, - updatedPubspecOverridesContents, - ); - } - } - } - - Future _runPubGetForPackage( + @visibleForTesting + Future runPubGetForPackage( MelosWorkspace workspace, Package package, { - required bool enforceLockfile, required bool noExample, + required bool runOffline, + required bool enforceLockfile, }) async { - late final hasLockFile = - File(p.join(package.path, 'pubspec.lock')).existsSync(); - final enforceLockfileConfigValue = - workspace.config.commands.bootstrap.enforceLockfile; - final shouldEnforceLockfile = - (enforceLockfileConfigValue || enforceLockfile) && hasLockFile; - final command = [ - ...pubCommandExecArgs( - useFlutter: package.isFlutterPackage, - workspace: workspace, - ), - 'get', - if (noExample) '--no-example', - if (workspace.config.commands.bootstrap.runPubGetOffline) '--offline', - if (shouldEnforceLockfile) '--enforce-lockfile', - ]; - + final command = _buildPubGetCommand( + workspace: workspace, + noExample: noExample, + runOffline: runOffline, + enforceLockfile: enforceLockfile, + ); final process = await startCommandRaw( command, workingDirectory: package.path, ); const logTimeout = Duration(seconds: 10); - final packagePrefix = '[${AnsiStyles.blue.bold(package.name)}]: '; + final packagePrefix = '[${AnsiStyles.blue.bold(workspace.name)}]: '; void Function(String) logLineTo(void Function(String) log) => (line) => log.call('$packagePrefix$line'); @@ -254,13 +166,31 @@ mixin _BootstrapMixin on _CleanMixin { if (exitCode != 0) { throw BootstrapException._( package, - 'Failed to install.', + 'Failed to run pub get.', stdout: await stdout, stderr: await stderr, ); } } + List _buildPubGetCommand({ + required MelosWorkspace workspace, + required bool noExample, + required bool runOffline, + required bool enforceLockfile, + }) { + return [ + ...pubCommandExecArgs( + useFlutter: workspace.isFlutterWorkspace, + workspace: workspace, + ), + 'get', + if (noExample) '--no-example', + if (runOffline) '--offline', + if (enforceLockfile) '--enforce-lockfile', + ]; + } + Future _setSharedDependenciesForPackage( Package package, { required Map? environment, @@ -380,7 +310,7 @@ mixin _BootstrapMixin on _CleanMixin { return dependenciesToUpdate.length; } - void _logBootstrapSuccess(Package package) { + void _logPackagePubGetSuccess(Package package) { logger.child(packageNameStyle(package.name), prefix: '$checkLabel ').child( packagePathStyle(printablePath(package.pathRelativeToWorkspace)), ); @@ -447,195 +377,14 @@ mixin _BootstrapMixin on _CleanMixin { } } -const _managedDependencyOverridesMarker = 'melos_managed_dependency_overrides'; -final _managedDependencyOverridesRegex = RegExp( - '^# $_managedDependencyOverridesMarker: (.*)\n', - multiLine: true, -); - -/// Merges the [melosDependencyOverrides] for other workspace packages into the -/// `pubspec_overrides.yaml` file for a package. -/// -/// [melosDependencyOverrides] must contain a mapping of workspace package names -/// to their paths relative to the package. -/// -/// [pubspecOverridesContent] are the current contents of the package's -/// `pubspec_overrides.yaml` and may be `null` if the file does not exist. -/// -/// Whitespace and comments in an existing `pubspec_overrides.yaml` file are -/// preserved. -/// -/// Dependency overrides for a melos workspace package that have not been added -/// by melos are not changed or removed. To mark a dependency override as being -/// managed by melos, it is added to marker comment when first added by this -/// function: -/// -/// ```yaml -/// # melos_managed_dependency_overrides: a -/// dependency_overrides: -/// a: -/// path: ../a -/// ``` -/// -/// This function also takes care of removing any dependency overrides that are -/// obsolete from `dependency_overrides` and the marker comment. -@visibleForTesting -String? mergeMelosPubspecOverrides( - Map melosDependencyOverrides, - String? pubspecOverridesContent, -) { - final pubspecOverridesEditor = YamlEditor(pubspecOverridesContent ?? ''); - final pubspecOverrides = pubspecOverridesEditor - .parseAt([], orElse: () => wrapAsYamlNode(null)).value as Object?; - - final dependencyOverrides = pubspecOverridesContent?.isEmpty ?? true - ? null - : PubspecOverrides.parse(pubspecOverridesContent!).dependencyOverrides; - final currentManagedDependencyOverrides = _managedDependencyOverridesRegex - .firstMatch(pubspecOverridesContent ?? '') - ?.group(1) - ?.split(',') - .toSet() ?? - {}; - final newManagedDependencyOverrides = {...currentManagedDependencyOverrides}; - - if (dependencyOverrides != null) { - for (final dependencyOverride in dependencyOverrides.entries.toList()) { - final packageName = dependencyOverride.key; - - if (currentManagedDependencyOverrides.contains(packageName)) { - // This dependency override is managed by melos and might need to be - // updated. - - if (melosDependencyOverrides.containsKey(packageName)) { - // Update changed dependency override. - final currentRef = dependencyOverride.value; - final newRef = melosDependencyOverrides[packageName]; - if (currentRef != newRef) { - pubspecOverridesEditor.update( - ['dependency_overrides', packageName], - wrapAsYamlNode( - newRef!.toJson(), - collectionStyle: CollectionStyle.BLOCK, - ), - ); - } - } else { - // Remove obsolete dependency override. - pubspecOverridesEditor.remove(['dependency_overrides', packageName]); - dependencyOverrides.remove(packageName); - newManagedDependencyOverrides.remove(packageName); - } - } - - // Remove this dependency from the list of workspace dependency overrides, - // so we only add new overrides later on. - melosDependencyOverrides.remove(packageName); - } - } - - if (melosDependencyOverrides.isNotEmpty) { - // Now melosDependencyOverrides only contains new dependencies that need to - // be added to the `pubspec_overrides.yaml` file. - - newManagedDependencyOverrides.addAll(melosDependencyOverrides.keys); - - if (pubspecOverrides == null) { - pubspecOverridesEditor.update( - [], - wrapAsYamlNode( - { - 'dependency_overrides': { - for (final dependencyOverride in melosDependencyOverrides.entries) - dependencyOverride.key: dependencyOverride.value.toJson(), - }, - }, - collectionStyle: CollectionStyle.BLOCK, - ), - ); - } else { - if (dependencyOverrides == null) { - pubspecOverridesEditor.update( - ['dependency_overrides'], - wrapAsYamlNode( - { - for (final dependencyOverride in melosDependencyOverrides.entries) - dependencyOverride.key: dependencyOverride.value.toJson(), - }, - collectionStyle: CollectionStyle.BLOCK, - ), - ); - } else { - for (final dependencyOverride in melosDependencyOverrides.entries) { - pubspecOverridesEditor.update( - ['dependency_overrides', dependencyOverride.key], - wrapAsYamlNode( - dependencyOverride.value.toJson(), - collectionStyle: CollectionStyle.BLOCK, - ), - ); - } - } - } - } else { - // No dependencies need to be added to the `pubspec_overrides.yaml` file. - // This means it is possible that dependency_overrides and/or - // melos_managed_dependency_overrides are now empty. - if (dependencyOverrides?.isEmpty ?? false) { - pubspecOverridesEditor.remove(['dependency_overrides']); - } - } - - if (pubspecOverridesEditor.edits.isNotEmpty) { - var result = pubspecOverridesEditor.toString(); - - // The changes to the `pubspec_overrides.yaml` file might require a change - // in the managed dependencies marker comment. - final setOfManagedDependenciesChanged = - !const DeepCollectionEquality.unordered().equals( - currentManagedDependencyOverrides, - newManagedDependencyOverrides, - ); - if (setOfManagedDependenciesChanged) { - if (newManagedDependencyOverrides.isEmpty) { - // When there are no managed dependencies, remove the marker comment. - result = result.replaceAll(_managedDependencyOverridesRegex, ''); - } else { - if (!_managedDependencyOverridesRegex.hasMatch(result)) { - // When there is no marker comment, add one. - result = '# $_managedDependencyOverridesMarker: ' - '${newManagedDependencyOverrides.join(',')}\n$result'; - } else { - // When there is a marker comment, update it. - result = result.replaceFirstMapped( - _managedDependencyOverridesRegex, - (match) => '# $_managedDependencyOverridesMarker: ' - '${newManagedDependencyOverrides.join(',')}\n', - ); - } - } - } - - if (result.trim() == '{}') { - // YamlEditor uses an empty dictionary ({}) when all properties have been - // removed and the file is essentially empty. - return ''; - } - - // Make sure the `pubspec_overrides.yaml` file always ends with a newline. - if (result.isEmpty || !result.endsWith('\n')) { - result += '\n'; - } - - return result; - } else { - return null; - } -} - /// An exception for when `pub get` for a package failed. class BootstrapException implements MelosException { - BootstrapException._(this.package, this.message, {this.stdout, this.stderr}); + BootstrapException._( + this.package, + this.message, { + this.stdout, + this.stderr, + }); /// The package that failed final Package package; @@ -645,6 +394,7 @@ class BootstrapException implements MelosException { @override String toString() { - return 'BootstrapException: $message: ${package.name} at ${package.path}.'; + return 'BootstrapException: $message: ${package.name} at ' + '${package.path}.'; } } diff --git a/packages/melos/lib/src/commands/clean.dart b/packages/melos/lib/src/commands/clean.dart index 21e558e94..7b9f1b634 100644 --- a/packages/melos/lib/src/commands/clean.dart +++ b/packages/melos/lib/src/commands/clean.dart @@ -37,21 +37,6 @@ mixin _CleanMixin on _Melos { for (final generatedPubFilePath in pathsToClean) { deleteEntry(p.join(package.path, generatedPubFilePath)); } - - // Remove any Melos generated dependency overrides from - // `pubspec_overrides.yaml`. - final pubspecOverridesFile = p.join(package.path, 'pubspec_overrides.yaml'); - if (fileExists(pubspecOverridesFile)) { - final contents = await readTextFileAsync(pubspecOverridesFile); - final updatedContents = mergeMelosPubspecOverrides({}, contents); - if (updatedContents != null) { - if (updatedContents.isEmpty) { - deleteEntry(pubspecOverridesFile); - } else { - await writeTextFileAsync(pubspecOverridesFile, updatedContents); - } - } - } } Future cleanIntelliJ(MelosWorkspace workspace) async { diff --git a/packages/melos/lib/src/commands/runner.dart b/packages/melos/lib/src/commands/runner.dart index 06604c475..2f6c79236 100644 --- a/packages/melos/lib/src/commands/runner.dart +++ b/packages/melos/lib/src/commands/runner.dart @@ -6,7 +6,6 @@ import 'dart:math'; import 'package:ansi_styles/ansi_styles.dart'; import 'package:async/async.dart'; import 'package:cli_util/cli_logging.dart'; -import 'package:collection/collection.dart'; import 'package:file/local.dart'; import 'package:meta/meta.dart'; import 'package:mustache_template/mustache.dart'; @@ -34,7 +33,6 @@ import '../common/io.dart'; import '../common/pending_package_update.dart'; import '../common/persistent_shell.dart'; import '../common/platform.dart'; -import '../common/pubspec_overrides.dart'; import '../common/utils.dart' as utils; import '../common/utils.dart'; import '../common/versioning.dart' as versioning; diff --git a/packages/melos/lib/src/package.dart b/packages/melos/lib/src/package.dart index 751290524..7a614f9bc 100644 --- a/packages/melos/lib/src/package.dart +++ b/packages/melos/lib/src/package.dart @@ -512,6 +512,19 @@ class PackageMap { }; } + static Future resolveRootPackage({ + required String workspacePath, + required MelosLogger logger, + }) async { + return PackageMap.resolvePackages( + workspacePath: workspacePath, + packages: [createGlob('.', currentDirectoryPath: workspacePath)], + ignore: [], + categories: {}, + logger: logger, + ).then((packageMap) => packageMap.values.first); + } + static Future resolvePackages({ required String workspacePath, required List packages, diff --git a/packages/melos/lib/src/workspace.dart b/packages/melos/lib/src/workspace.dart index bfe46a983..5b8cf54b0 100644 --- a/packages/melos/lib/src/workspace.dart +++ b/packages/melos/lib/src/workspace.dart @@ -24,8 +24,9 @@ class IdeWorkspace { IntellijProject.fromWorkspace(_workspace); } -/// A representation of a workspace. This includes its packages, configuration -/// such as scripts and more. +/// A representation of a workspace. +/// +/// This includes its packages, configuration such as scripts and more. class MelosWorkspace { MelosWorkspace({ required this.name, @@ -33,6 +34,7 @@ class MelosWorkspace { required this.config, required this.allPackages, required this.filteredPackages, + required this.rootPackage, required this.dependencyOverridePackages, required this.sdkPath, required this.logger, @@ -59,6 +61,10 @@ class MelosWorkspace { categories: const {}, logger: logger, ); + final rootPackage = await PackageMap.resolveRootPackage( + workspacePath: workspaceConfig.path, + logger: logger, + ); final filteredPackages = await allPackages.applyFilters(packageFilters); @@ -68,6 +74,7 @@ class MelosWorkspace { config: workspaceConfig, allPackages: allPackages, logger: logger, + rootPackage: rootPackage, filteredPackages: filteredPackages, dependencyOverridePackages: dependencyOverridePackages, sdkPath: resolveSdkPath( @@ -92,6 +99,9 @@ class MelosWorkspace { /// Configuration as defined in the "melos.yaml" file if it exists. final MelosWorkspaceConfig config; + /// The root package of this workspace. + final Package rootPackage; + /// All packages managed in this Melos workspace. /// /// Packages specified in [MelosWorkspaceConfig.packages] are included, diff --git a/packages/melos/pubspec.yaml b/packages/melos/pubspec.yaml index 7abf032c0..1a83b2787 100644 --- a/packages/melos/pubspec.yaml +++ b/packages/melos/pubspec.yaml @@ -1,4 +1,5 @@ name: melos +publish_to: none description: A tool for managing Dart & Flutter repositories with multiple packages (monorepo). Supports automated versioning via Conventional Commits. Inspired @@ -7,6 +8,7 @@ version: 6.3.0 homepage: https://melos.invertase.dev/~melos-latest repository: https://github.com/invertase/melos/tree/main/packages/melos issue_tracker: https://github.com/invertase/melos/issues +resolution: workspace topics: - tool @@ -16,37 +18,40 @@ topics: - lerna environment: - sdk: ">=3.2.0 <4.0.0" + sdk: ^3.6.0 executables: melos: dependencies: ansi_styles: ^0.3.2+1 - args: ^2.4.2 + args: ^2.6.0 async: ^2.11.0 cli_launcher: ^0.3.1 - cli_util: ^0.4.1 - collection: ^1.18.0 + cli_util: ^0.4.2 + collection: any conventional_commit: ^0.6.0+1 - file: ^7.0.0 + file: ^7.0.1 glob: ^2.1.2 - graphs: ^2.3.1 - http: ^1.1.0 - intl: ^0.19.0 - meta: ^1.10.0 + graphs: ^2.3.2 + http: ^1.2.2 + intl: any + meta: any mustache_template: ^2.0.0 - path: ^1.8.3 - platform: ^3.1.4 + path: ^1.9.1 + platform: ^3.1.6 pool: ^1.5.1 prompts: ^2.0.0 - pub_semver: ^2.1.4 - pub_updater: ^0.4.0 - pubspec_parse: ^1.4.0 - string_scanner: ^1.2.0 - yaml: ^3.1.2 - yaml_edit: ^2.1.1 + pub_semver: ^2.1.5 + pub_updater: ^0.5.0 + pubspec_parse: + git: + url: https://github.com/dart-lang/tools.git + path: pkgs/pubspec_parse + string_scanner: ^1.4.1 + yaml: ^3.1.3 + yaml_edit: ^2.2.2 dev_dependencies: - mockito: ^5.4.2 - test: ^1.24.9 + mockito: ^5.4.5 + test: ^1.25.14 diff --git a/packages/melos/test/command_runner_test.dart b/packages/melos/test/command_runner_test.dart index 444c5fbbd..c9a4357ab 100644 --- a/packages/melos/test/command_runner_test.dart +++ b/packages/melos/test/command_runner_test.dart @@ -20,6 +20,7 @@ void main() { 'test_script2': Script(name: 'test_script2', run: ''), }), ), + workspacePackages: [], ); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); @@ -47,6 +48,7 @@ void main() { 'run': Script(name: 'run', run: ''), }), ), + workspacePackages: [], ); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); diff --git a/packages/melos/test/commands/analyze_test.dart b/packages/melos/test/commands/analyze_test.dart index 5ba8820cc..705423853 100644 --- a/packages/melos/test/commands/analyze_test.dart +++ b/packages/melos/test/commands/analyze_test.dart @@ -19,7 +19,9 @@ void main() { late Directory aDir; setUp(() async { - workspaceDir = await createTemporaryWorkspace(); + workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], + ); aDir = await createProject( workspaceDir, @@ -277,7 +279,9 @@ ${'-' * terminalWidth} }); test('should run analysis using flutter & dart', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b'], + ); await createProject( workspaceDir, @@ -321,7 +325,9 @@ ${'-' * terminalWidth} }); test('should run analysis using flutter', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a'], + ); await createProject( workspaceDir, @@ -348,7 +354,9 @@ ${'-' * terminalWidth} }); test('should run analysis using dart', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a'], + ); await createProject( workspaceDir, Pubspec('a'), diff --git a/packages/melos/test/commands/bootstrap_test.dart b/packages/melos/test/commands/bootstrap_test.dart index e14b92429..ac0e7d2e4 100644 --- a/packages/melos/test/commands/bootstrap_test.dart +++ b/packages/melos/test/commands/bootstrap_test.dart @@ -2,7 +2,6 @@ import 'dart:io' as io; import 'package:melos/melos.dart'; import 'package:melos/src/command_configs/command_configs.dart'; -import 'package:melos/src/commands/runner.dart'; import 'package:melos/src/common/glob.dart'; import 'package:melos/src/common/io.dart'; import 'package:melos/src/common/utils.dart'; @@ -47,11 +46,13 @@ void main() { path: '', ); - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a'], + ); final aPath = p.join(workspaceDir.path, 'packages', 'a'); - final aDir = await createProject( + await createProject( workspaceDir, Pubspec( 'a', @@ -108,11 +109,11 @@ Generating IntelliJ IDE files... ), ); - final aPackageConfigPath = packageConfigPath(aDir.path); + final workspaceConfigPath = packageConfigPath(workspaceDir.path); String resolvePathRelativeToPackageConfig(String path) => - p.canonicalize(p.join(p.dirname(aPackageConfigPath), path)); + p.canonicalize(p.join(p.dirname(workspaceConfigPath), path)); - final aConfig = packageConfigForPackageAt(aDir); + final aConfig = packageConfigForPackageAt(workspaceDir); final actualAbsolutePath = p.prettyUri( aConfig.packages.firstWhere((p) => p.name == 'absolute').rootUri, ); @@ -148,120 +149,12 @@ Generating IntelliJ IDE files... ); }); - test( - 'resolves workspace packages with path dependency', - () async { - final workspaceDir = await createTemporaryWorkspace(); - - final aDir = await createProject( - workspaceDir, - Pubspec( - 'a', - dependencies: { - 'b': HostedDependency(version: VersionConstraint.any), - }, - ), - ); - await createProject( - workspaceDir, - Pubspec('b'), - ); - - await createProject( - workspaceDir, - pubspecFromJsonFile(fileName: 'add_to_app_json.json'), - ); - - await createProject( - workspaceDir, - pubspecFromJsonFile(fileName: 'plugin_json.json'), - ); - - final logger = TestLogger(); - final config = - await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); - final melos = Melos( - logger: logger, - config: config, - ); - - await runMelosBootstrap(melos, logger); - - expect( - logger.output, - ignoringAnsii( - allOf( - [ - ''' -melos bootstrap - └> ${workspaceDir.path} - -Running "flutter pub get" in workspace packages...''', - ''' - ✓ a - └> packages/a -''', - ''' - ✓ b - └> packages/b -''', - ''' - ✓ c - └> packages/c -''', - ''' - ✓ d - └> packages/d -''', - ''' - > SUCCESS - -Generating IntelliJ IDE files... - > SUCCESS - - -> 4 packages bootstrapped -''', - ].map(contains).toList(), - ), - ), - ); - - final aConfig = packageConfigForPackageAt(aDir); - - expect( - aConfig.packages.firstWhere((p) => p.name == 'b').rootUri, - '../../b', - ); - }, - timeout: - io.Platform.isLinux ? const Timeout(Duration(seconds: 45)) : null, - ); - - test( - 'bootstrap transitive dependencies', - () async => dependencyResolutionTest( - { - 'a': [], - 'b': ['a'], - 'c': ['b'], - }, - ), - ); - - test( - 'bootstrap cyclic dependencies', - () async => dependencyResolutionTest( - { - 'a': ['b'], - 'b': ['a'], - }, - ), - ); - test('respects user dependency_overrides', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a'], + ); - final pkgA = await createProject( + await createProject( workspaceDir, Pubspec( 'a', @@ -277,6 +170,7 @@ Generating IntelliJ IDE files... await createProject( workspaceDir, Pubspec('path'), + inWorkspace: false, ); final logger = TestLogger(); @@ -288,7 +182,7 @@ Generating IntelliJ IDE files... await runMelosBootstrap(melos, logger); - final packageConfig = packageConfigForPackageAt(pkgA); + final packageConfig = packageConfigForPackageAt(workspaceDir); expect( packageConfig.packages .firstWhere((package) => package.name == 'path') @@ -298,7 +192,9 @@ Generating IntelliJ IDE files... }); test('bootstrap flutter example packages', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'a/example'], + ); await createProject( workspaceDir, @@ -311,7 +207,7 @@ Generating IntelliJ IDE files... path: 'packages/a', ); - final examplePkg = await createProject( + await createProject( workspaceDir, Pubspec( 'example', @@ -331,159 +227,16 @@ Generating IntelliJ IDE files... await runMelosBootstrap(melos, logger); - final examplePkgConfig = packageConfigForPackageAt(examplePkg); - final aPkgDependencyConfig = examplePkgConfig.packages + final workspacePkgConfig = packageConfigForPackageAt(workspaceDir); + final aPkgDependencyConfig = workspacePkgConfig.packages .firstWhere((package) => package.name == 'a'); - expect(aPkgDependencyConfig.rootUri, '../../'); - }); - - group('mergeMelosPubspecOverrides', () { - void expectMergedMelosPubspecOverrides({ - required Map melosDependencyOverrides, - required String? currentPubspecOverrides, - required String? updatedPubspecOverrides, - }) { - expect( - mergeMelosPubspecOverrides( - { - for (final entry in melosDependencyOverrides.entries) - entry.key: PathDependency(entry.value), - }, - currentPubspecOverrides, - ), - updatedPubspecOverrides, - ); - } - - test('pubspec_overrides.yaml does not exist', () { - expectMergedMelosPubspecOverrides( - melosDependencyOverrides: {}, - currentPubspecOverrides: null, - updatedPubspecOverrides: null, - ); - expectMergedMelosPubspecOverrides( - melosDependencyOverrides: {'a': '../a'}, - currentPubspecOverrides: null, - updatedPubspecOverrides: ''' -# melos_managed_dependency_overrides: a -dependency_overrides: - a: - path: ../a -''', - ); - }); - - test('existing pubspec_overrides.yaml is empty', () { - expectMergedMelosPubspecOverrides( - melosDependencyOverrides: {}, - currentPubspecOverrides: '', - updatedPubspecOverrides: null, - ); - expectMergedMelosPubspecOverrides( - melosDependencyOverrides: {'a': '../a'}, - currentPubspecOverrides: '', - updatedPubspecOverrides: ''' -# melos_managed_dependency_overrides: a -dependency_overrides: - a: - path: ../a -''', - ); - }); - - test('existing pubspec_overrides.yaml has dependency_overrides', () { - expectMergedMelosPubspecOverrides( - melosDependencyOverrides: {'a': '../a'}, - currentPubspecOverrides: ''' -dependency_overrides: - x: any -''', - updatedPubspecOverrides: ''' -# melos_managed_dependency_overrides: a -dependency_overrides: - x: any - a: - path: ../a -''', - ); - }); - - test('add melos managed dependency', () { - expectMergedMelosPubspecOverrides( - melosDependencyOverrides: {'a': '../a', 'b': '../b'}, - currentPubspecOverrides: ''' -# melos_managed_dependency_overrides: a -dependency_overrides: - a: - path: ../a -''', - updatedPubspecOverrides: ''' -# melos_managed_dependency_overrides: a,b -dependency_overrides: - a: - path: ../a - b: - path: ../b -''', - ); - }); - - test('remove melos managed dependency', () { - expectMergedMelosPubspecOverrides( - melosDependencyOverrides: {}, - currentPubspecOverrides: ''' -# melos_managed_dependency_overrides: a -dependency_overrides: - a: - path: ../a -''', - updatedPubspecOverrides: '', - ); - }); - - test('update melos managed dependency', () { - expectMergedMelosPubspecOverrides( - melosDependencyOverrides: {'a': '../aa'}, - currentPubspecOverrides: ''' -# melos_managed_dependency_overrides: a -dependency_overrides: - a: - path: ../a -''', - updatedPubspecOverrides: ''' -# melos_managed_dependency_overrides: a -dependency_overrides: - a: - path: ../aa -''', - ); - }); - - test('add, update and remove melos managed dependency', () { - expectMergedMelosPubspecOverrides( - melosDependencyOverrides: {'b': '../bb', 'c': '../c'}, - currentPubspecOverrides: ''' -# melos_managed_dependency_overrides: a,b -dependency_overrides: - a: - path: ../a - b: - path: ../b -''', - updatedPubspecOverrides: ''' -# melos_managed_dependency_overrides: b,c -dependency_overrides: - b: - path: ../bb - c: - path: ../c -''', - ); - }); + expect(aPkgDependencyConfig.rootUri, '../packages/a'); }); test('handles errors in pub get', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a'], + ); await createProject( workspaceDir, @@ -516,7 +269,7 @@ dependency_overrides: melos.bootstrap(), throwsA( isA() - .having((e) => e.package.name, 'package.name', 'a'), + .having((e) => e.package.name, 'package.name', 'workspace'), ), ); @@ -528,9 +281,9 @@ melos bootstrap └> ${workspaceDir.path} Running "${pubExecArgs.join(' ')} get" in workspace packages... - - a - └> packages/a -e- └> Failed to install. + - workspace + └> . +e- └> Failed to run pub get. Resolving dependencies... e-Because a depends on package_that_does_not_exists any which doesn't exist (could not find package package_that_does_not_exists at https://pub.dev), version solving failed. @@ -541,6 +294,7 @@ e-Because a depends on package_that_does_not_exists any which doesn't exist (cou test('can run pub get offline', () async { final workspaceDir = await createTemporaryWorkspace( + workspacePackages: [], configBuilder: (path) => MelosWorkspaceConfig.fromYaml( createYamlMap( { @@ -591,6 +345,7 @@ Generating IntelliJ IDE files... test('can run pub get --enforce-lockfile', () async { final workspaceDir = await createTemporaryWorkspace( + workspacePackages: [], configBuilder: (path) => MelosWorkspaceConfig.fromYaml( createYamlMap( { @@ -619,6 +374,14 @@ Generating IntelliJ IDE files... workspace: workspace, ); + await melos.runPubGetForPackage( + workspace, + workspace.rootPackage, + noExample: true, + runOffline: false, + enforceLockfile: false, + ); + await runMelosBootstrap(melos, logger); expect( @@ -643,6 +406,7 @@ Generating IntelliJ IDE files... test('can run pub get --no-enforce-lockfile when enforced in config', () async { final workspaceDir = await createTemporaryWorkspace( + workspacePackages: [], configBuilder: (path) => MelosWorkspaceConfig.fromYaml( createYamlMap( { @@ -698,6 +462,7 @@ Generating IntelliJ IDE files... test('can run pub get --enforce-lockfile without lockfile', () async { final workspaceDir = await createTemporaryWorkspace( + workspacePackages: [], configBuilder: (path) => MelosWorkspaceConfig.fromYaml( createYamlMap( { @@ -750,6 +515,7 @@ Generating IntelliJ IDE files... 'applies shared dependencies from melos config', () async { final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b'], configBuilder: (path) => MelosWorkspaceConfig( name: 'Melos', packages: [ @@ -758,7 +524,7 @@ Generating IntelliJ IDE files... commands: CommandConfigs( bootstrap: BootstrapCommandConfigs( environment: { - 'sdk': VersionConstraint.parse('>=2.18.0 <3.0.0'), + 'sdk': VersionConstraint.parse('>=3.6.0 <4.0.0'), 'flutter': VersionConstraint.parse('>=2.18.0 <3.0.0'), }, dependencies: { @@ -779,9 +545,9 @@ Generating IntelliJ IDE files... ), }, devDependencies: { - 'build_runner': HostedDependency( + 'flame_lint': HostedDependency( version: VersionConstraint.compatibleWith( - Version.parse('2.4.6'), + Version.parse('1.2.1'), ), ), }, @@ -807,9 +573,9 @@ Generating IntelliJ IDE files... ), }, devDependencies: { - 'build_runner': HostedDependency( + 'flame_lint': HostedDependency( version: - VersionConstraint.compatibleWith(Version.parse('2.4.0')), + VersionConstraint.compatibleWith(Version.parse('1.2.0')), ), }, ), @@ -859,7 +625,7 @@ Generating IntelliJ IDE files... ); expect( pubspecA.environment['sdk'], - equals(VersionConstraint.parse('>=2.18.0 <3.0.0')), + equals(VersionConstraint.parse('>=3.6.0 <4.0.0')), ); expect( pubspecA.dependencies, @@ -876,8 +642,8 @@ Generating IntelliJ IDE files... expect( pubspecA.devDependencies, equals({ - 'build_runner': HostedDependency( - version: VersionConstraint.compatibleWith(Version.parse('2.4.6')), + 'flame_lint': HostedDependency( + version: VersionConstraint.compatibleWith(Version.parse('1.2.1')), ), }), ); @@ -915,6 +681,7 @@ Generating IntelliJ IDE files... test('correctly inlines shared dependencies', () async { final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a'], configBuilder: (path) => MelosWorkspaceConfig.fromYaml( createYamlMap( { @@ -959,91 +726,11 @@ Generating IntelliJ IDE files... }); }); - group('melos bs --skip-linking', () { - test('should skip package linking', () async { - final workspaceDir = await createTemporaryWorkspace(); - await createProject( - workspaceDir, - Pubspec('a'), - ); - - final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); - final melos = Melos(logger: logger, config: config); - - await runMelosBootstrap(melos, logger, skipLinking: true); - - expect( - logger.output, - ignoringAnsii( - ''' -melos bootstrap - └> ${workspaceDir.path} - -Generating IntelliJ IDE files... - > SUCCESS - - -> 1 packages bootstrapped -''', - ), - ); - }); - - test('should work fine with environment config', () async { - final workspaceDir = await createTemporaryWorkspace( - configBuilder: (path) => MelosWorkspaceConfig( - name: 'Melos', - packages: [ - createGlob('packages/**', currentDirectoryPath: path), - ], - commands: CommandConfigs( - bootstrap: BootstrapCommandConfigs( - environment: { - 'sdk': VersionConstraint.parse('>=2.18.0 <3.0.0'), - 'flutter': VersionConstraint.parse('>=2.18.0 <3.0.0'), - }, - ), - ), - path: path, - ), - ); - - await createProject( - workspaceDir, - Pubspec('a'), - ); - - final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); - final melos = Melos(logger: logger, config: config); - - await runMelosBootstrap(melos, logger, skipLinking: true); - - expect( - logger.output, - ignoringAnsii( - ''' -melos bootstrap - └> ${workspaceDir.path} - -Updating common dependencies in workspace packages... - ✓ a - └> Updated environment - > SUCCESS - -Generating IntelliJ IDE files... - > SUCCESS - - -> 1 packages bootstrapped -''', - ), - ); - }); - }); - group('melos bs --offline', () { test('should run pub get with --offline', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a'], + ); await createProject( workspaceDir, Pubspec('a'), @@ -1081,13 +768,11 @@ Generating IntelliJ IDE files... Future runMelosBootstrap( Melos melos, TestLogger logger, { - bool skipLinking = false, bool? enforceLockfile, bool offline = false, }) async { try { await melos.bootstrap( - skipLinking: skipLinking, enforceLockfile: enforceLockfile, offline: offline, ); @@ -1098,98 +783,6 @@ Future runMelosBootstrap( } } -/// Tests whether dependencies are resolved correctly. -/// -/// [packages] is a map where keys are package names and values are lists of -/// packages names on which the package in the corresponding key depends. -/// -/// In the example below **a** has no dependencies and **b** depends only on -/// **a**: -/// -/// ```dart main -/// final packages = { -/// 'a': [], -/// 'b': ['a'] -/// }; -/// ``` -/// -/// For each entry in [packages] a package with the key as the name will be -/// generated. -/// -/// After running `melos bootstrap`, for each package it is verified that all -/// direct and transitive dependencies are path dependencies with the correct -/// path. -Future dependencyResolutionTest( - Map> packages, -) async { - final workspaceDir = await createTemporaryWorkspace(); - - Future> createPackage( - MapEntry> entry, - ) async { - final package = entry.key; - final dependencies = entry.value; - final directory = await createProject( - workspaceDir, - Pubspec( - package, - dependencies: { - for (final dependency in dependencies) - dependency: HostedDependency(version: VersionConstraint.any), - }, - ), - ); - - return MapEntry(package, directory); - } - - final packageDirs = Map.fromEntries( - await Future.wait(packages.entries.map(createPackage)), - ); - - List transitiveDependenciesOfPackage(String root) { - final transitiveDependencies = []; - final workingSet = packages[root]!.toList(); - - while (workingSet.isNotEmpty) { - final current = workingSet.removeLast(); - - if (current == root) { - continue; - } - - if (!transitiveDependencies.contains(current)) { - transitiveDependencies.add(current); - workingSet.addAll(packages[current]!); - } - } - - return transitiveDependencies; - } - - Future validatePackage(String package) async { - final packageConfig = packageConfigForPackageAt(packageDirs[package]!); - final transitiveDependencies = transitiveDependenciesOfPackage(package); - - for (final dependency in transitiveDependencies) { - final dependencyConfig = - packageConfig.packages.firstWhere((e) => e.name == dependency); - expect(dependencyConfig.rootUri, '../../$dependency'); - } - } - - final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); - final melos = Melos( - logger: logger, - config: config, - ); - - await runMelosBootstrap(melos, logger); - - await Future.wait(packages.keys.map(validatePackage)); -} - YamlMap _pubspecContent(io.Directory directory) { final source = readTextFile(pubspecPath(directory.path)); return loadYaml(source) as YamlMap; diff --git a/packages/melos/test/commands/clean_test.dart b/packages/melos/test/commands/clean_test.dart deleted file mode 100644 index e0836a54a..000000000 --- a/packages/melos/test/commands/clean_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:glob/glob.dart'; -import 'package:melos/melos.dart'; -import 'package:melos/src/common/utils.dart'; -import 'package:path/path.dart' as p; -import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; -import 'package:test/test.dart'; - -import '../matchers.dart'; -import '../utils.dart'; - -void main() { - group('clean', () { - test('removes dependency overrides from pubspec_overrides.yaml', () async { - final workspaceDir = await createTemporaryWorkspace( - configBuilder: (path) => MelosWorkspaceConfig( - path: path, - name: 'test_workspace', - packages: [Glob('packages/**')], - ), - ); - - final packageADir = await createProject(workspaceDir, Pubspec('a')); - final packageBDir = await createProject( - workspaceDir, - Pubspec( - 'b', - dependencies: {'a': HostedDependency(version: VersionConstraint.any)}, - ), - ); - final pubspecOverrides = - p.join(packageBDir.path, 'pubspec_overrides.yaml'); - - final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); - final logger = TestLogger(); - final melos = Melos(config: config, logger: logger); - await melos.bootstrap(); - - expect( - pubspecOverrides, - yamlFile({ - 'dependency_overrides': { - 'a': {'path': relativePath(packageADir.path, packageBDir.path)}, - }, - }), - ); - - await melos.clean(); - - expect( - pubspecOverrides, - isNot(fileExists), - ); - }); - }); -} diff --git a/packages/melos/test/commands/exec_test.dart b/packages/melos/test/commands/exec_test.dart index 8b3600715..8d5791538 100644 --- a/packages/melos/test/commands/exec_test.dart +++ b/packages/melos/test/commands/exec_test.dart @@ -14,7 +14,9 @@ import '../utils.dart'; void main() { group('exec', () { test('supports package filters', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], + ); final aDir = await createProject( workspaceDir, @@ -92,7 +94,9 @@ ${'-' * terminalWidth} } test('get cancel on first fail when fail fast is enabled', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], + ); final a = await createProject( workspaceDir, @@ -154,7 +158,9 @@ ${'-' * terminalWidth} }); test('keep running when fail fast is not enabled', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], + ); final a = await createProject( workspaceDir, @@ -216,7 +222,9 @@ ${'-' * terminalWidth} group('fail fast', () { test('print error codes correctly', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], + ); await createProject( workspaceDir, @@ -271,7 +279,9 @@ ${'-' * terminalWidth} }); test('propagate error code when fail fast is enabled', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], + ); await createProject( workspaceDir, @@ -301,7 +311,9 @@ ${'-' * terminalWidth} group('order dependents', () { test('sorts execution order topologically', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], + ); await createProject( workspaceDir, @@ -367,7 +379,9 @@ ${'-' * terminalWidth} test( 'sorts execution order topologically with cyclic dependencies', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], + ); await createProject( workspaceDir, @@ -434,7 +448,9 @@ ${'-' * terminalWidth} test( 'sorts execution order topologically with larger cyclic dependencies', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c', 'd', 'e'], + ); await createProject( workspaceDir, @@ -521,7 +537,9 @@ ${'-' * terminalWidth} ); test('fails fast if dependencies fail', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], + ); await createProject( workspaceDir, @@ -585,7 +603,9 @@ ${'-' * terminalWidth} }); test('does not fail fast if dependencies is not run', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], + ); final aDir = await createProject( workspaceDir, diff --git a/packages/melos/test/commands/format_test.dart b/packages/melos/test/commands/format_test.dart index 10b11820f..2d0a706b0 100644 --- a/packages/melos/test/commands/format_test.dart +++ b/packages/melos/test/commands/format_test.dart @@ -23,7 +23,9 @@ void main() { late Directory aDir; setUp(() async { - workspaceDir = await createTemporaryWorkspace(); + workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], + ); aDir = await createProject( workspaceDir, @@ -319,6 +321,7 @@ $ melos format ), ), ), + workspacePackages: ['a'], ); final aDir = await createProject( @@ -373,6 +376,7 @@ $ melos format ), ), ), + workspacePackages: ['a'], ); final aDir = await createProject( diff --git a/packages/melos/test/commands/list_test.dart b/packages/melos/test/commands/list_test.dart index dea83d3a5..d1890cf1f 100644 --- a/packages/melos/test/commands/list_test.dart +++ b/packages/melos/test/commands/list_test.dart @@ -6,11 +6,10 @@ import 'package:melos/src/package.dart'; import 'package:melos/src/workspace_configs.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/test.dart'; import '../matchers.dart'; -import '../mock_fs.dart'; -import '../mock_workspace_fs.dart'; import '../utils.dart'; void main() { @@ -22,14 +21,15 @@ void main() { group('with no format option', () { test( 'logs public packages by default', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a', version: Version.none), - MockPackageFs(name: 'b', version: Version.none), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b'], ); + await createProject(workspaceDir, Pubspec('a')); + await createProject(workspaceDir, Pubspec('b')); + + final logger = TestLogger(); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); @@ -45,26 +45,21 @@ b ''', ), ); - }), + }, ); test( 'logs private packages by default', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a', version: Version.none), - // b has no version, so it is considered private - MockPackageFs(name: 'b'), - // c has a version but publish_to:none so is private - MockPackageFs( - name: 'c', - version: Version.none, - publishToNone: true, - ), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], ); + await createProject(workspaceDir, Pubspec('a')); + await createProject(workspaceDir, Pubspec('b')); + await createProject(workspaceDir, Pubspec('c', publishTo: 'none')); + + final logger = TestLogger(); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); @@ -81,20 +76,20 @@ c ''', ), ); - }), + }, ); test( 'applies package filters', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a'), - MockPackageFs(name: 'b'), - MockPackageFs(name: 'c'), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], ); + await createProject(workspaceDir, Pubspec('a')); + await createProject(workspaceDir, Pubspec('b')); + await createProject(workspaceDir, Pubspec('c')); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); @@ -117,20 +112,26 @@ c ''', ), ); - }), + }, ); test( 'supports long flag for extra information', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a', version: Version(1, 2, 3)), - MockPackageFs(name: 'b', dependencies: ['a']), - MockPackageFs(name: 'long_name'), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'long_name'], ); + await createProject( + workspaceDir, + Pubspec('a', version: Version(1, 2, 3)), + ); + await createProject( + workspaceDir, + Pubspec('b', dependencies: {'a': HostedDependency()}), + ); + await createProject(workspaceDir, Pubspec('long_name')); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); @@ -149,22 +150,22 @@ long_name 0.0.0 packages/long_name PRIVATE ''', ), ); - }), + }, ); }); group('parsable', () { test( 'relativePaths flag prints relative paths only if true', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a'), - MockPackageFs(name: 'b'), - MockPackageFs(name: 'c'), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], ); + await createProject(workspaceDir, Pubspec('a')); + await createProject(workspaceDir, Pubspec('b')); + await createProject(workspaceDir, Pubspec('c')); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); @@ -183,22 +184,24 @@ packages/c ''', ), ); - }), + }, ); test( 'full package path is printed by default if relativePaths is false or ' 'not set', - withMockFs(() async { - final packages = [ - MockPackageFs(name: 'a'), - MockPackageFs(name: 'b'), - MockPackageFs(name: 'c'), - ]; - final workspaceDir = createMockWorkspaceFs( - packages: packages, + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], ); - final packagePaths = packages + + final packageDirs = [ + await createProject(workspaceDir, Pubspec('a')), + await createProject(workspaceDir, Pubspec('b')), + await createProject(workspaceDir, Pubspec('c')), + ]; + + final packagePaths = packageDirs .map((package) => p.join(workspaceDir.path, package.path)) .map(p.canonicalize); @@ -217,26 +220,29 @@ ${packagePaths.join('\n')} ''', ), ); - }), + }, ); }); group('graph', () { test( 'reports all dependencies in workspace', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a'), - MockPackageFs(name: 'b'), - MockPackageFs(name: 'c'), - MockPackageFs( - name: 'd', - dependencies: ['a'], - devDependencies: ['b'], - dependencyOverrides: ['c'], - ), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c', 'd'], + ); + + await createProject(workspaceDir, Pubspec('a')); + await createProject(workspaceDir, Pubspec('b')); + await createProject(workspaceDir, Pubspec('c')); + await createProject( + workspaceDir, + Pubspec( + 'd', + dependencies: {'a': HostedDependency()}, + devDependencies: {'b': HostedDependency()}, + dependencyOverrides: {'c': HostedDependency()}, + ), ); final config = @@ -261,26 +267,29 @@ ${packagePaths.join('\n')} } ''', ); - }), + }, ); }); group('json', () { test( 'reports all dependencies in workspace', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a'), - MockPackageFs(name: 'b'), - MockPackageFs(name: 'c'), - MockPackageFs( - name: 'd', - dependencies: ['a'], - devDependencies: ['b'], - dependencyOverrides: ['c'], - ), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c', 'd'], + ); + + await createProject(workspaceDir, Pubspec('a')); + await createProject(workspaceDir, Pubspec('b')); + await createProject(workspaceDir, Pubspec('c')); + await createProject( + workspaceDir, + Pubspec( + 'd', + dependencies: {'a': HostedDependency()}, + devDependencies: {'b': HostedDependency()}, + dependencyOverrides: {'c': HostedDependency()}, + ), ); final config = @@ -324,26 +333,29 @@ ${packagePaths.join('\n')} }, ], ); - }), + }, ); }); group('gviz', () { test( 'reports all dependencies in workspace', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a'), - MockPackageFs(name: 'b'), - MockPackageFs(name: 'c'), - MockPackageFs( - name: 'd', - dependencies: ['a'], - devDependencies: ['b'], - dependencyOverrides: ['c'], - ), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c', 'd'], + ); + + await createProject(workspaceDir, Pubspec('a')); + await createProject(workspaceDir, Pubspec('b')); + await createProject(workspaceDir, Pubspec('c')); + await createProject( + workspaceDir, + Pubspec( + 'd', + dependencies: {'a': HostedDependency()}, + devDependencies: {'b': HostedDependency()}, + dependencyOverrides: {'c': HostedDependency()}, + ), ); final config = @@ -376,7 +388,7 @@ digraph packages { } ''', ); - }), + }, ); }); }); diff --git a/packages/melos/test/commands/publish_test.dart b/packages/melos/test/commands/publish_test.dart index 16d5edcc3..16d07ea86 100644 --- a/packages/melos/test/commands/publish_test.dart +++ b/packages/melos/test/commands/publish_test.dart @@ -76,6 +76,7 @@ void main() { ), ), ), + workspacePackages: ['a', 'b'], ); for (final package in ['a', 'b']) { diff --git a/packages/melos/test/commands/run_test.dart b/packages/melos/test/commands/run_test.dart index eecedd1d4..99e773513 100644 --- a/packages/melos/test/commands/run_test.dart +++ b/packages/melos/test/commands/run_test.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:melos/melos.dart'; import 'package:melos/src/commands/runner.dart'; import 'package:melos/src/common/environment_variable_key.dart'; @@ -22,7 +20,6 @@ void main() { 'supports passing package filter options to "melos exec" scripts', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -39,18 +36,14 @@ void main() { ), }), ), + workspacePackages: ['a', 'b'], ); - final aDir = await createProject( - workspaceDir, - Pubspec('a'), - ); + final aDir = await createProject(workspaceDir, Pubspec('a')); writeTextFile(p.join(aDir.path, 'log.txt'), ''); - await createProject( - workspaceDir, - Pubspec('b'), - ); + await createProject(workspaceDir, Pubspec('b')); + await runPubGet(workspaceDir.path); final logger = TestLogger(); final config = @@ -99,7 +92,6 @@ melos run test_script withMockPlatform( () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -116,24 +108,15 @@ melos run test_script ), }), ), + workspacePackages: ['a', 'b', 'c'], ); - final aDir = await createProject( - workspaceDir, - Pubspec('a'), - ); + final aDir = await createProject(workspaceDir, Pubspec('a')); writeTextFile(p.join(aDir.path, 'log.txt'), ''); - - await createProject( - workspaceDir, - Pubspec('b'), - ); - - final cDir = await createProject( - workspaceDir, - Pubspec('c'), - ); + await createProject(workspaceDir, Pubspec('b')); + final cDir = await createProject(workspaceDir, Pubspec('c')); writeTextFile(p.join(cDir.path, 'log.txt'), ''); + await runPubGet(workspaceDir.path); final logger = TestLogger(); final config = @@ -181,7 +164,6 @@ melos run test_script test('supports passing additional arguments to scripts', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -195,6 +177,7 @@ melos run test_script ), }), ), + workspacePackages: [], ); final logger = TestLogger(); @@ -234,7 +217,6 @@ melos run hello test('supports passing additional arguments to scripts (exec)', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -249,9 +231,11 @@ melos run hello ), }), ), + workspacePackages: ['a'], ); await createProject(workspaceDir, Pubspec('a')); + await runPubGet(workspaceDir.path); final logger = TestLogger(); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); @@ -302,7 +286,6 @@ melos run hello test('supports running "melos exec" script with "exec" options', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -319,12 +302,11 @@ melos run hello ), }), ), + workspacePackages: ['a'], ); - await createProject( - workspaceDir, - Pubspec('a'), - ); + await createProject(workspaceDir, Pubspec('a')); + await runPubGet(workspaceDir.path); final logger = TestLogger(); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); @@ -368,7 +350,6 @@ melos run test_script test('throws an error if neither run, steps, nor exec are provided', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -381,12 +362,11 @@ melos run test_script ), }), ), + workspacePackages: ['a'], ); - await createProject( - workspaceDir, - Pubspec('a'), - ); + await createProject(workspaceDir, Pubspec('a')); + await runPubGet(workspaceDir.path); final logger = TestLogger(); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); @@ -402,7 +382,6 @@ melos run test_script 'throws an error if neither run or steps are provided, and exec ' 'are options', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -416,12 +395,11 @@ melos run test_script ), }), ), + workspacePackages: ['a'], ); - await createProject( - workspaceDir, - Pubspec('a'), - ); + await createProject(workspaceDir, Pubspec('a')); + await runPubGet(workspaceDir.path); final logger = TestLogger(); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); @@ -435,17 +413,14 @@ melos run test_script }); group('multiple scripts', () { - late Directory aDir; - test( ''' Verify that multiple script steps are executed sequentially in a persistent shell. When the script changes directory to "packages" and runs "ls -la", -it should list the contents including the package named "this is package A". +it should list the contents including the package named "this_is_package_a". ''', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -459,12 +434,11 @@ it should list the contents including the package named "this is package A". ), }), ), + workspacePackages: ['this_is_package_a'], ); - await createProject( - workspaceDir, - Pubspec('this is package A'), - ); + await createProject(workspaceDir, Pubspec('this_is_package_a')); + await runPubGet(workspaceDir.path); final logger = TestLogger(); final config = @@ -478,7 +452,7 @@ it should list the contents including the package named "this is package A". expect( logger.output.normalizeNewLines(), - contains('this is package A'), + contains('this_is_package_a'), ); }, timeout: const Timeout(Duration(minutes: 1)), @@ -488,7 +462,6 @@ it should list the contents including the package named "this is package A". 'verifies that a melos script can successfully call another ' 'script as a step and execute commands', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -506,12 +479,11 @@ it should list the contents including the package named "this is package A". ), }), ), + workspacePackages: ['a'], ); - await createProject( - workspaceDir, - Pubspec('a'), - ); + await createProject(workspaceDir, Pubspec('a')); + await runPubGet(workspaceDir.path); final logger = TestLogger(); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); @@ -551,7 +523,6 @@ SUCCESS 'throws an error if a script defined with steps also includes exec ' 'options', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -572,12 +543,11 @@ SUCCESS ), }), ), + workspacePackages: ['a'], ); - await createProject( - workspaceDir, - Pubspec('a'), - ); + await createProject(workspaceDir, Pubspec('a')); + await runPubGet(workspaceDir.path); final logger = TestLogger(); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); @@ -597,7 +567,6 @@ SUCCESS 'steps, and ensures all commands in those steps are executed ' 'successfully', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -615,12 +584,11 @@ SUCCESS ), }), ), + workspacePackages: ['a'], ); - await createProject( - workspaceDir, - Pubspec('a'), - ); + await createProject(workspaceDir, Pubspec('a')); + await runPubGet(workspaceDir.path); final logger = TestLogger(); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); @@ -660,7 +628,6 @@ SUCCESS 'melos commands, and ensures the script is successfully executed', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -674,22 +641,13 @@ SUCCESS ), }), ), + workspacePackages: ['a', 'b', 'c'], ); - aDir = await createProject( - workspaceDir, - Pubspec('a'), - ); - - await createProject( - workspaceDir, - Pubspec('b'), - ); - - await createProject( - workspaceDir, - Pubspec('c'), - ); + final aDir = await createProject(workspaceDir, Pubspec('a')); + await createProject(workspaceDir, Pubspec('b')); + await createProject(workspaceDir, Pubspec('c')); + await runPubGet(workspaceDir.path); writeTextFile( p.join(aDir.path, 'main.dart'), @@ -760,7 +718,6 @@ SUCCESS 'a script with a name equal to a melos command, and ensures the ' 'script group successfully runs instead of the command', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -778,22 +735,13 @@ SUCCESS ), }), ), + workspacePackages: ['a', 'b', 'c'], ); - aDir = await createProject( - workspaceDir, - Pubspec('a'), - ); - - await createProject( - workspaceDir, - Pubspec('b'), - ); - - await createProject( - workspaceDir, - Pubspec('c'), - ); + await createProject(workspaceDir, Pubspec('a')); + await createProject(workspaceDir, Pubspec('b')); + await createProject(workspaceDir, Pubspec('c')); + await runPubGet(workspaceDir.path); final logger = TestLogger(); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); @@ -834,7 +782,6 @@ SUCCESS 'melos commands with flags, and ensures the script is successfully ' 'executed', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -848,22 +795,13 @@ SUCCESS ), }), ), + workspacePackages: ['a', 'b', 'c'], ); - aDir = await createProject( - workspaceDir, - Pubspec('a'), - ); - - await createProject( - workspaceDir, - Pubspec('b'), - ); - - await createProject( - workspaceDir, - Pubspec('c'), - ); + final aDir = await createProject(workspaceDir, Pubspec('a')); + await createProject(workspaceDir, Pubspec('b')); + await createProject(workspaceDir, Pubspec('c')); + await runPubGet(workspaceDir.path); writeTextFile( p.join(aDir.path, 'main.dart'), @@ -934,7 +872,6 @@ SUCCESS 'calls itself through another script, leading to a recursive call', () async { final workspaceDir = await createTemporaryWorkspace( - runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -952,12 +889,11 @@ SUCCESS ), }), ), + workspacePackages: ['a'], ); - await createProject( - workspaceDir, - Pubspec('a'), - ); + await createProject(workspaceDir, Pubspec('a')); + await runPubGet(workspaceDir.path); final logger = TestLogger(); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); diff --git a/packages/melos/test/commands/script_test.dart b/packages/melos/test/commands/script_test.dart index 9635eaaa2..a69760b7d 100644 --- a/packages/melos/test/commands/script_test.dart +++ b/packages/melos/test/commands/script_test.dart @@ -22,6 +22,7 @@ void main() { 'test_script3': Script(name: 'test_script3', run: ''), }), ), + workspacePackages: [], ); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); @@ -48,6 +49,7 @@ void main() { 'test_script3': Script(name: 'test_script3', run: ''), }), ), + workspacePackages: [], ); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); @@ -68,6 +70,7 @@ void main() { 'clean': Script(name: 'clean', run: ''), }), ), + workspacePackages: [], ); final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); diff --git a/packages/melos/test/commands/version_test.dart b/packages/melos/test/commands/version_test.dart index b0d8f1546..8103dfef7 100644 --- a/packages/melos/test/commands/version_test.dart +++ b/packages/melos/test/commands/version_test.dart @@ -36,6 +36,7 @@ void main() { ); final workspaceDir = await createTemporaryWorkspace( configBuilder: workspaceConfig, + workspacePackages: ['a', 'b'], ); await createProject( workspaceDir, diff --git a/packages/melos/test/mock_fs.dart b/packages/melos/test/mock_fs.dart deleted file mode 100644 index cd32efa05..000000000 --- a/packages/melos/test/mock_fs.dart +++ /dev/null @@ -1,89 +0,0 @@ -// ignore_for_file: avoid_redundant_argument_values - -import 'dart:async'; -import 'dart:io'; - -import 'package:file/memory.dart'; -import 'package:melos/src/common/platform.dart'; - -/// Overrides the body of a test so that I/O is run against an in-memory file -/// system, not the host's disk. -/// -/// The I/O override is applied only to the code running within [testBody]. -FutureOr Function() withMockFs(FutureOr Function() testBody) { - return () { - return IOOverrides.runWithIOOverrides(testBody, MockFs()); - }; -} - -/// Used to override file I/O with an in-memory file system for testing. -/// -/// Usage: -/// -/// ```dart main -/// test('My FS test', withMockFs(() { -/// File('foo').createSync(); // File created in memory -/// })); -/// ``` -/// -/// Alternatively, set [IOOverrides.global] to a [MockFs] instance in your -/// test's `setUp`, and to `null` in the `tearDown`. -class MockFs extends IOOverrides { - /// Note that we only support [MemoryFileSystem]s, because a local file system - /// would create infinite loops IOOverride -> FS -> IOOverride -> FS... - final MemoryFileSystem fs = MemoryFileSystem( - // Match the platform pathing style - style: currentPlatform.isWindows - ? FileSystemStyle.windows - : FileSystemStyle.posix, - ); - - @override - Directory createDirectory(String path) => fs.directory(path); - - @override - File createFile(String path) => fs.file(path); - - @override - Link createLink(String path) => fs.link(path); - - @override - Stream fsWatch(String path, int events, bool recursive) => - fs.file(path).watch(events: events, recursive: recursive); - - @override - bool fsWatchIsSupported() => fs.isWatchSupported; - - @override - Future fseGetType(String path, bool followLinks) => - fs.type(path, followLinks: followLinks); - - @override - FileSystemEntityType fseGetTypeSync(String path, bool followLinks) => - fs.typeSync(path, followLinks: followLinks); - - @override - Future fseIdentical(String path1, String path2) => - fs.identical(path1, path2); - - @override - bool fseIdenticalSync(String path1, String path2) => - fs.identicalSync(path1, path2); - - @override - Directory getCurrentDirectory() => fs.currentDirectory; - - @override - Directory getSystemTempDirectory() => fs.systemTempDirectory; - - @override - void setCurrentDirectory(String path) { - fs.currentDirectory = path; - } - - @override - Future stat(String path) => fs.stat(path); - - @override - FileStat statSync(String path) => fs.statSync(path); -} diff --git a/packages/melos/test/mock_workspace_fs.dart b/packages/melos/test/mock_workspace_fs.dart deleted file mode 100644 index 1923727a0..000000000 --- a/packages/melos/test/mock_workspace_fs.dart +++ /dev/null @@ -1,183 +0,0 @@ -import 'dart:io'; - -import 'package:melos/src/common/io.dart'; -import 'package:path/path.dart' as p; -import 'package:pub_semver/pub_semver.dart'; - -import 'mock_fs.dart'; - -/// Creates a mock workspace at [workspaceRoot], containing a `melos.yaml` and a -/// set of package folders as described by [packages]. -/// -/// The returned directory represents the workspace root. -Directory createMockWorkspaceFs({ - String workspaceName = 'monorepo', - String? workspaceRoot, - Iterable workspacePackagesGlobs = const ['packages/**'], - Iterable packages = const [], - bool setCwdToWorkspace = true, - bool? intellij, -}) { - assert( - IOOverrides.current is MockFs, - 'Mock workspaces can only be created inside a mock filesystem', - ); - - // ignore: parameter_assignments - workspaceRoot = - Platform.isWindows ? r'C:\melos_workspace' : '/melos_workspace'; - - // Create a `melos.yaml` - _createMelosConfig( - workspaceRoot, - workspaceName, - workspacePackagesGlobs, - intellij: intellij, - ); - - // Synthesize a "package" (enough to satisfy our test requirements) for each - // entry in `packages` - for (final package in packages) { - _createPackage(package, workspaceRoot); - if (package.createExamplePackage) { - _createPackage(package.examplePackage!, workspaceRoot); - } - } - - if (setCwdToWorkspace) { - Directory.current = workspaceRoot; - } - - return Directory(workspaceRoot); -} - -void _createMelosConfig( - String workspaceRoot, - String workspaceName, - Iterable workspacePackagesGlobs, { - required bool? intellij, -}) { - var contents = ''' -name: $workspaceName -packages: -${_yamlStringList(workspacePackagesGlobs)} -'''; - - if (intellij != null) { - contents += ''' -ide: - intellij: $intellij -'''; - } - - writeTextFile(p.join(workspaceRoot, 'melos.yaml'), contents, recursive: true); -} - -void _createPackage(MockPackageFs package, String workspaceRoot) { - final pubspec = StringBuffer(); - pubspec.writeln('name: ${package.name}'); - if (package.publishToNone) { - pubspec.writeln('publish_to: none'); - } - - if (package.version != null) { - pubspec.writeln('version: ${package.version}'); - } - - pubspec.writeln( - ''' -dependencies: -${_yamlMap(package.dependencyMap, indent: 2)} - -dev_dependencies: -${_yamlMap(package.devDependencyMap, indent: 2)} - -dependency_overrides: -${_yamlMap(package.dependencyOverridesMap, indent: 2)} -''', - ); - - writeTextFile( - p.join(workspaceRoot, package.path, 'pubspec.yaml'), - pubspec.toString(), - recursive: true, - ); -} - -String _yamlStringList(Iterable elements) { - return elements.map((element) => '- $element').join('\n'); -} - -String _yamlMap(Map map, {required int indent}) { - final indentString = ' ' * indent; - return map.entries.map((e) => '$indentString${e.key}: ${e.value}').join('\n'); -} - -/// Used to generate a package's on-disk representation via -/// [createMockWorkspaceFs]. -class MockPackageFs { - MockPackageFs({ - required this.name, - String? path, - List? dependencies, - List? devDependencies, - List? dependencyOverrides, - this.version, - this.publishToNone = false, - bool generateExample = false, - }) : _path = path, - dependencies = dependencies ?? const [], - devDependencies = devDependencies ?? const [], - dependencyOverrides = dependencyOverrides ?? const [], - createExamplePackage = generateExample; - - /// Name of the package (must be a valid Dart package name) - final String name; - - final Version? version; - - /// Workspace-root relative path - String get path => _path ?? p.join('packages', name); - final String? _path; - - /// `true` if this package's yaml has a `publish_to: none` setting. - final bool publishToNone; - - /// A list of package names that are dependencies of this one. - final List dependencies; - - /// A list of package names that are dev dependencies of this one. - final List devDependencies; - - /// A list of package names that are dependency overrides of this one. - final List dependencyOverrides; - - /// A mapping of dependency names to their versions (always "any") - Map get dependencyMap => - Map.fromEntries(dependencies.map((name) => MapEntry(name, 'any'))); - - /// A mapping of dev dependency names to their versions (always "any") - Map get devDependencyMap => Map.fromEntries( - devDependencies.map((name) => MapEntry(name, 'any')), - ); - - /// A mapping of dependency overrides names to their versions (always "any") - Map get dependencyOverridesMap => Map.fromEntries( - dependencyOverrides.map((name) => MapEntry(name, 'any')), - ); - - /// `true` if an example package should be generated - final bool createExamplePackage; - - /// Returns a file system description for this package's example - MockPackageFs? get examplePackage { - return createExamplePackage - ? MockPackageFs( - name: '${name}_example', - path: p.join(path, 'example'), - dependencies: [name], - publishToNone: true, - ) - : null; - } -} diff --git a/packages/melos/test/package_filter_test.dart b/packages/melos/test/package_filter_test.dart index 12b236425..338fbbf6f 100644 --- a/packages/melos/test/package_filter_test.dart +++ b/packages/melos/test/package_filter_test.dart @@ -11,7 +11,9 @@ import 'utils.dart'; void main() { group('PackageFilters', () { test('dirExists', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b'], + ); final aDir = await createProject( workspaceDir, @@ -47,7 +49,9 @@ void main() { }); test('fileExists', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b'], + ); final aDir = await createProject( workspaceDir, @@ -83,7 +87,9 @@ void main() { }); test('ignore', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b'], + ); await createProject( workspaceDir, @@ -142,6 +148,7 @@ void main() { final workspaceDir = await createTemporaryWorkspace( configBuilder: configBuilder, + workspacePackages: ['a', 'ab', 'abc', 'b', 'c'], ); await createProject( diff --git a/packages/melos/test/package_test.dart b/packages/melos/test/package_test.dart index b55ebf9b9..4f4e2ae98 100644 --- a/packages/melos/test/package_test.dart +++ b/packages/melos/test/package_test.dart @@ -11,8 +11,6 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/test.dart'; import 'mock_env.dart'; -import 'mock_fs.dart'; -import 'mock_workspace_fs.dart'; import 'utils.dart'; const pubPackageJson = ''' @@ -61,26 +59,25 @@ void main() { late MelosWorkspace workspace; setUp(() async { - IOOverrides.global = MockFs(); - - final config = await MelosWorkspaceConfig.fromWorkspaceRoot( - createMockWorkspaceFs( - packages: [ - MockPackageFs( - name: 'melos', - version: Version(0, 0, 0), - ), - ], - ), + final workspaceDir = + await createTemporaryWorkspace(workspacePackages: ['melos']); + + await createProject( + workspaceDir, + Pubspec('melos', version: Version(0, 0, 0)), ); + + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); + final logger = TestLogger(); + final melos = Melos(logger: logger, config: config); + await melos.bootstrap(); + workspace = await MelosWorkspace.fromConfig( config, - logger: TestLogger().toMelosLogger(), + logger: logger.toMelosLogger(), ); }); - tearDown(() => IOOverrides.global = null); - group('When requests published packages', () { final pubCredentialStoreMock = PubCredentialStore([]); diff --git a/packages/melos/test/pubspec_extension.dart b/packages/melos/test/pubspec_extension.dart index fdc256156..18a8a1963 100644 --- a/packages/melos/test/pubspec_extension.dart +++ b/packages/melos/test/pubspec_extension.dart @@ -21,6 +21,8 @@ extension PubspecExtension on Pubspec { List? screenshots, String? documentation, Map? environment, + List? workspace, + String? resolution, Map? dependencies, Map? devDependencies, Map? dependencyOverrides, @@ -40,6 +42,8 @@ extension PubspecExtension on Pubspec { screenshots: screenshots ?? this.screenshots, documentation: documentation ?? this.documentation, environment: environment ?? this.environment, + workspace: workspace ?? this.workspace, + resolution: resolution ?? this.resolution, dependencies: dependencies ?? this.dependencies, devDependencies: devDependencies ?? this.devDependencies, dependencyOverrides: dependencyOverrides ?? this.dependencyOverrides, @@ -75,6 +79,8 @@ extension PubspecExtension on Pubspec { 'documentation': documentation, 'environment': environment.map((key, value) => MapEntry(key, value?.toString())), + 'workspace': workspace, + 'resolution': resolution, 'dependencies': dependencies.map((key, value) => MapEntry(key, value.toJson())), 'dev_dependencies': diff --git a/packages/melos/test/utils.dart b/packages/melos/test/utils.dart index 0f9c226ae..4141d40bb 100644 --- a/packages/melos/test/utils.dart +++ b/packages/melos/test/utils.dart @@ -109,20 +109,29 @@ MelosWorkspaceConfig _defaultWorkspaceConfigBuilder(String path) => ); Future createTemporaryWorkspace({ + required List workspacePackages, TestWorkspaceConfigBuilder configBuilder = _defaultWorkspaceConfigBuilder, bool runPubGet = false, bool createLockfile = false, + bool withExamples = false, }) async { final tempDir = createTempDir(p.join(Directory.current.path, '.dart_tool')); addTearDown(() => deleteEntry(tempDir)); final workspacePath = tempDir; + final workspacePackagesPaths = workspacePackages + .map((name) => name.contains('packages') ? name : 'packages/$name'); await createProject( Directory(workspacePath), Pubspec( 'workspace', environment: defaultTestEnvironment, + workspace: [ + ...workspacePackagesPaths, + if (withExamples) + ...workspacePackagesPaths.map((path) => '$path/example'), + ], devDependencies: { 'melos': PathDependency(Directory.current.path), }, @@ -151,14 +160,17 @@ Future createProject( Pubspec partialPubspec, { String? path, bool createLockfile = false, + bool inWorkspace = true, }) async { - final pubspec = partialPubspec.environment.isNotEmpty - ? partialPubspec - : partialPubspec.copyWith( - environment: { + final pubspec = partialPubspec.copyWith( + environment: partialPubspec.environment.isEmpty + ? { 'sdk': defaultTestEnvironment['sdk'], - }, - ); + } + : null, + resolution: + inWorkspace && partialPubspec.name != 'workspace' ? 'workspace' : null, + ); final projectDirectory = Directory( p.joinAll([ @@ -222,6 +234,8 @@ String pubspecPath(String directory) { return p.join(directory, 'pubspec.yaml'); } +/// Most of the time [dir] should be the root of the monorepo, after the move to +/// pub workspaces has been done. PackageConfig packageConfigForPackageAt(Directory dir) { final source = readTextFile(packageConfigPath(dir.path)); return PackageConfig.fromJson(json.decode(source) as Map); @@ -356,11 +370,13 @@ class VirtualWorkspaceBuilder { ); final packageMap = _buildVirtualPackageMap(_packages, logger); + final rootPackage = _buildRootPackage(config, logger); return MelosWorkspace( name: config.name, path: config.path, config: config, + rootPackage: rootPackage, allPackages: packageMap, filteredPackages: packageMap, dependencyOverridePackages: _buildVirtualPackageMap(const [], logger), @@ -369,6 +385,23 @@ class VirtualWorkspaceBuilder { ); } + Package _buildRootPackage(MelosWorkspaceConfig config, MelosLogger logger) { + final pubspec = Pubspec.fromJson({'name': config.name}); + return Package( + pubspec: pubspec, + name: config.name, + path: config.path, + version: Version.none, + publishTo: null, + dependencies: [], + devDependencies: ['melos'], + dependencyOverrides: [], + packageMap: {}, + pathRelativeToWorkspace: '.', + categories: [], + ); + } + PackageMap _buildVirtualPackageMap( List<_VirtualPackage> packages, MelosLogger logger, @@ -400,7 +433,7 @@ class VirtualWorkspaceBuilder { } final defaultTestEnvironment = { - 'sdk': VersionConstraint.parse('>=2.12.0 <3.0.0'), + 'sdk': VersionConstraint.parse('^3.6.0'), }; class _VirtualPackage { diff --git a/packages/melos/test/utils_test.dart b/packages/melos/test/utils_test.dart index 256cee25d..bff5be796 100644 --- a/packages/melos/test/utils_test.dart +++ b/packages/melos/test/utils_test.dart @@ -77,7 +77,9 @@ void main() { group('startProcess', () { test('runs command chain in single shell', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: [], + ); final testDir = p.join(workspaceDir.path, 'test'); ensureDir(testDir); diff --git a/packages/melos/test/workspace_test.dart b/packages/melos/test/workspace_test.dart index d3da10bf8..6f093aa50 100644 --- a/packages/melos/test/workspace_test.dart +++ b/packages/melos/test/workspace_test.dart @@ -14,14 +14,14 @@ import 'package:test/test.dart'; import 'matchers.dart'; import 'mock_env.dart'; -import 'mock_fs.dart'; -import 'mock_workspace_fs.dart'; import 'utils.dart'; void main() { group('Workspace', () { test('throws if multiple packages have the same name', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'a/example', 'b/example'], + ); await createProject( workspaceDir, @@ -80,6 +80,7 @@ The packages that caused the problem are: 'packages': ['packages/*'], }, ), + workspacePackages: [], ); final projectDir = await createProject(workspaceDir, Pubspec('a')); @@ -98,19 +99,21 @@ The packages that caused the problem are: test( 'does not include projects inside packages/whatever/.dart_tool when no melos.yaml is specified', - withMockFs(() async { + () async { // regression test for https://github.com/invertase/melos/issues/101 - final mockWorkspaceRootDir = createMockWorkspaceFs( - workspaceRoot: '/root', - packages: [ - MockPackageFs(name: 'a'), - MockPackageFs(name: 'b', path: '/root/packages/a/.dart_tool/b'), - ], + final workspaceRootDir = await createTemporaryWorkspace( + workspacePackages: ['a'], + ); + + await createProject(workspaceRootDir, Pubspec('a')); + await createProject( + Directory('${workspaceRootDir.path}/a/.dart_tool'), + Pubspec('b'), ); final config = - await MelosWorkspaceConfig.fromWorkspaceRoot(mockWorkspaceRootDir); + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceRootDir); final workspace = await MelosWorkspace.fromConfig( config, logger: TestLogger().toMelosLogger(), @@ -120,13 +123,15 @@ The packages that caused the problem are: workspace.filteredPackages.values, [packageNamed('a')], ); - }), + }, ); test( 'load workspace config when workspace contains broken symlink', () async { - final workspaceDir = await createTemporaryWorkspace(); + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: [], + ); final link = Link(p.join(workspaceDir.path, 'link')); await link.create(p.join(workspaceDir.path, 'does-not-exist')); @@ -148,6 +153,7 @@ The packages that caused the problem are: }, path: path, ), + workspacePackages: ['a'], ); await createProject( @@ -173,6 +179,7 @@ The packages that caused the problem are: }, path: path, ), + workspacePackages: ['a'], ); await createProject(workspaceDir, Pubspec('a')); @@ -218,13 +225,17 @@ The packages that caused the problem are: group('--include-dependencies', () { test( 'includes the scoped package', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a', dependencies: ['b']), - MockPackageFs(name: 'b'), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b'], + ); + + await createProject( + workspaceDir, + Pubspec('a', dependencies: {'b': HostedDependency()}), ); + await createProject(workspaceDir, Pubspec('b')); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( @@ -239,18 +250,22 @@ The packages that caused the problem are: ); expect(workspace.filteredPackages.values, [packageNamed('b')]); - }), + }, ); test( 'includes direct dependencies', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a', dependencies: ['b']), - MockPackageFs(name: 'b'), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b'], ); + + await createProject( + workspaceDir, + Pubspec('a', dependencies: {'b': HostedDependency()}), + ); + await createProject(workspaceDir, Pubspec('b')); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( @@ -271,19 +286,26 @@ The packages that caused the problem are: packageNamed('b'), ]), ); - }), + }, ); test( 'includes transient dependencies', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a', dependencies: ['b']), - MockPackageFs(name: 'b', dependencies: ['c']), - MockPackageFs(name: 'c'), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], + ); + + await createProject( + workspaceDir, + Pubspec('a', dependencies: {'b': HostedDependency()}), ); + await createProject( + workspaceDir, + Pubspec('b', dependencies: {'c': HostedDependency()}), + ); + await createProject(workspaceDir, Pubspec('c')); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( @@ -305,20 +327,36 @@ The packages that caused the problem are: packageNamed('c'), // This dep is transitive ]), ); - }), + }, ); test( 'does not include duplicates', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a', dependencies: ['b', 'c']), - MockPackageFs(name: 'b', dependencies: ['d']), - MockPackageFs(name: 'c', dependencies: ['d']), - MockPackageFs(name: 'd'), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c', 'd'], + ); + + await createProject( + workspaceDir, + Pubspec( + 'a', + dependencies: { + 'b': HostedDependency(), + 'c': HostedDependency(), + }, + ), ); + await createProject( + workspaceDir, + Pubspec('b', dependencies: {'d': HostedDependency()}), + ); + await createProject( + workspaceDir, + Pubspec('c', dependencies: {'d': HostedDependency()}), + ); + await createProject(workspaceDir, Pubspec('d')); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( @@ -337,20 +375,24 @@ The packages that caused the problem are: workspace.filteredPackages.values, isNot(containsDuplicates), ); - }), + }, ); }); group('--include-dependents', () { test( 'includes the scoped package', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a', dependencies: ['b']), - MockPackageFs(name: 'b'), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b'], + ); + + await createProject( + workspaceDir, + Pubspec('a', dependencies: {'b': HostedDependency()}), ); + await createProject(workspaceDir, Pubspec('b')); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( @@ -365,18 +407,22 @@ The packages that caused the problem are: ); expect(workspace.filteredPackages.values, [packageNamed('a')]); - }), + }, ); test( 'includes direct dependents', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a', dependencies: ['b']), - MockPackageFs(name: 'b'), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b'], ); + + await createProject( + workspaceDir, + Pubspec('a', dependencies: {'b': HostedDependency()}), + ); + await createProject(workspaceDir, Pubspec('b')); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( @@ -395,19 +441,26 @@ The packages that caused the problem are: workspace.filteredPackages.values, containsAll([packageNamed('a'), packageNamed('b')]), ); - }), + }, ); test( 'includes transient dependents', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a', dependencies: ['b']), - MockPackageFs(name: 'b', dependencies: ['c']), - MockPackageFs(name: 'c'), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c'], + ); + + await createProject( + workspaceDir, + Pubspec('a', dependencies: {'b': HostedDependency()}), + ); + await createProject( + workspaceDir, + Pubspec('b', dependencies: {'c': HostedDependency()}), ); + await createProject(workspaceDir, Pubspec('c')); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( @@ -429,20 +482,36 @@ The packages that caused the problem are: packageNamed('c'), // This dep is transitive ]), ); - }), + }, ); test( 'does not include duplicates', - withMockFs(() async { - final workspaceDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a', dependencies: ['b', 'c']), - MockPackageFs(name: 'b', dependencies: ['d']), - MockPackageFs(name: 'c', dependencies: ['d']), - MockPackageFs(name: 'd'), - ], + () async { + final workspaceDir = await createTemporaryWorkspace( + workspacePackages: ['a', 'b', 'c', 'd'], ); + + await createProject( + workspaceDir, + Pubspec( + 'a', + dependencies: { + 'b': HostedDependency(), + 'c': HostedDependency(), + }, + ), + ); + await createProject( + workspaceDir, + Pubspec('b', dependencies: {'d': HostedDependency()}), + ); + await createProject( + workspaceDir, + Pubspec('c', dependencies: {'d': HostedDependency()}), + ); + await createProject(workspaceDir, Pubspec('d')); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( @@ -461,7 +530,7 @@ The packages that caused the problem are: workspace.filteredPackages.values, isNot(containsDuplicates), ); - }), + }, ); }); }); diff --git a/pubspec.yaml b/pubspec.yaml index 4b3bb8450..da4250332 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,10 @@ name: melos_workspace +workspace: + - packages/conventional_commit + - packages/melos environment: - sdk: ">=3.2.0 <4.0.0" + sdk: ">=3.6.0 <4.0.0" # This allows us to use melos on itself during development. executables: @@ -10,5 +13,5 @@ executables: dev_dependencies: melos: path: packages/melos - path: ^1.9.0 - yaml: ^3.1.2 + path: ^1.9.1 + yaml: ^3.1.3 From a2411e23c9bcac79aac7c03f5ccdcd70cc627705 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 6 Jan 2025 18:17:48 +0100 Subject: [PATCH 2/8] docs: Update readme with migration instructions --- packages/melos/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/melos/README.md b/packages/melos/README.md index 8f00308df..20d66d9f9 100644 --- a/packages/melos/README.md +++ b/packages/melos/README.md @@ -34,6 +34,32 @@ bases into multi-package repositories (sometimes called **Melos is a tool that optimizes the workflow around managing multi-package repositories with git and Pub.** +## Migrate to Melos 7.x.x + +Since the [pub workspaces](https://dart.dev/tools/pub/workspaces) feature has +been released, Melos has been updated to rely on that instead of creating +`pubspec_overrides.yaml` files and thus some migration is needed. + +The main difference is that you now have to add `resolution: workspace` to all +of your packages' `pubspec.yaml` files and add a list of all your packages to +the root `pubspec.yaml` file, similar to this: + +```yaml +name: my_workspace +publish_to: none +environment: + sdk: ^3.6.0 +workspace: + - packages/helper + - packages/client_package + - packages/server_package +dev_dependencies: + melos: ^7.0.0 +``` + +> [!NOTE] +> You have to use Dart SDK 3.6.0 or newer to use pub workspaces. + ## Github Action If you're planning on using Melos in your GitHub Actions workflows, you can use From 77381d8813f07cddf8bbe599ab38e4c7cfc6a596 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 6 Jan 2025 18:18:04 +0100 Subject: [PATCH 3/8] ci: Update CI to use Dart 3.6.0 --- .github/workflows/validate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 82ea5508c..3a7cf5be0 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@v3 - uses: dart-lang/setup-dart@v1 with: - sdk: 3.2.0 # Update when min sdk supported version of `melos` package changes. + sdk: 3.6.0 # Update when min sdk supported version of `melos` package changes. - name: Run Melos run: ./.github/workflows/scripts/install-tools.sh From 3e46bbcbe6e785ef49049b5c26d97fe1b0459566 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 6 Jan 2025 18:21:23 +0100 Subject: [PATCH 4/8] chore: Lower dependency constraints --- melos.yaml | 8 ++++---- packages/conventional_commit/pubspec.yaml | 2 +- packages/melos/pubspec.yaml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/melos.yaml b/melos.yaml index 26647835d..45dfc2c05 100644 --- a/melos.yaml +++ b/melos.yaml @@ -17,13 +17,13 @@ command: args: ^2.6.0 cli_launcher: ^0.3.1 cli_util: ^0.4.2 - collection: ^1.19.0 + collection: any conventional_commit: ^0.6.0+1 file: ^7.0.1 glob: ^2.1.2 graphs: ^2.3.2 http: ^1.2.2 - meta: ^1.16.0 + meta: any mustache_template: ^2.0.0 path: ^1.9.1 platform: ^3.1.6 @@ -35,12 +35,12 @@ command: git: url: https://github.com/dart-lang/tools.git path: pkgs/pubspec_parse - string_scanner: ^1.4.1 + string_scanner: ^1.3.0 yaml: ^3.1.3 yaml_edit: ^2.2.2 dev_dependencies: mockito: ^5.4.5 - test: ^1.25.14 + test: any path: ^1.9.1 yaml: ^3.1.3 version: diff --git a/packages/conventional_commit/pubspec.yaml b/packages/conventional_commit/pubspec.yaml index 92dbfce90..dbfe866ea 100644 --- a/packages/conventional_commit/pubspec.yaml +++ b/packages/conventional_commit/pubspec.yaml @@ -10,4 +10,4 @@ environment: sdk: ^3.6.0 dev_dependencies: - test: ^1.25.14 + test: any diff --git a/packages/melos/pubspec.yaml b/packages/melos/pubspec.yaml index 1a83b2787..3a3cf0580 100644 --- a/packages/melos/pubspec.yaml +++ b/packages/melos/pubspec.yaml @@ -48,10 +48,10 @@ dependencies: git: url: https://github.com/dart-lang/tools.git path: pkgs/pubspec_parse - string_scanner: ^1.4.1 + string_scanner: ^1.3.0 yaml: ^3.1.3 yaml_edit: ^2.2.2 dev_dependencies: mockito: ^5.4.5 - test: ^1.25.14 + test: any From 24f1560ed1d8ba8834c81a7b673ed58d08f2fbac Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 6 Jan 2025 18:44:58 +0100 Subject: [PATCH 5/8] docs: Update docs to reflect 7.0.0 --- docs/commands/bootstrap.mdx | 60 ++++++++++++++----------------- docs/getting-started.mdx | 72 ++++++++++++++++++++----------------- docs/guides/migrations.mdx | 35 ++++++++++++++++++ docs/index.mdx | 3 +- packages/melos/README.md | 9 +++++ 5 files changed, 112 insertions(+), 67 deletions(-) diff --git a/docs/commands/bootstrap.mdx b/docs/commands/bootstrap.mdx index 66234a658..4883128ef 100644 --- a/docs/commands/bootstrap.mdx +++ b/docs/commands/bootstrap.mdx @@ -7,8 +7,8 @@ description: Learn more about the `bootstrap` command in Melos. Supports all [Melos filtering](/filters) flags. -This command initializes the workspace, links local packages together and -installs remaining package dependencies. +This command initializes the workspace and installs remaining package +dependencies. ```bash melos bootstrap @@ -16,39 +16,34 @@ melos bootstrap melos bs ``` -Bootstrapping has two primary functions: +Bootstrapping has three primary functions: 1. Installing all package dependencies (internally using `pub get`). -2. Locally linking any packages together via path dependency overrides _without - having to edit your pubspec.yaml_. +2. Syncing shared dependencies between packages. +3. Running any bootstrap lifecycle scripts. ## Why is bootstrapping required? -In normal projects, packages can be linked by providing a `path` within the -`pubspec.yaml`. This works for small projects however presents a problem at -scale. Packages cannot be published with a locally defined path, meaning once -you're ready to publish your packages you'll need to manually update all the -packages `pubspec.yaml` files with the versions. If your packages are also -tightly coupled (dependencies of each other), you'll also have to manually check -which versions should be updated. Even with a few of packages this can become a -long and error-prone task. - -Melos solves this problem by overriding local files which the Dart analyzer uses -to read packages from. If a local package exists (defined in the `melos.yaml` -file) and a different local package has it listed as a dependency, it will be -linked regardless of whether a version has been specified. +After the [Pub Workspaces feature](https://dart.dev/tools/pub/workspaces) was +introduced in Dart 3.6.0, it is no longer strictly necessary to run `melos +bootstrap`, since all the packages are already linked together. However, there +are still some benefits to running `melos bootstrap`, because you need to run +`pub get` in each package to initialize the workspace, and `melos bootstrap` +will do that for you. ### Benefits -- All local packages in the repository can be interlinked by Melos to point to - their local directories rather than 'remote' _without pubspec.yaml - modifications_. +Why would I want to use a monorepo? + +- All local packages in the repository can be interlinked to point to their + local directories rather than 'remote' _without pubspec.yaml modifications_. - **Example Scenario**: In a repository, package `A` depends on package `B`. Both packages `A` & `B` exist in the monorepo. However, if you `pub get` inside package `A`, `pub` will retrieve package `B` from the pub.dev - registry as it's unaware of `B` existing locally. However, with Melos, it's - aware that package `B` exists locally too, so it will generate the various - pub files to point to a relative path in the local repository. + registry as it's unaware of `B` existing locally. However, with Melos and + pub workspaces, it's aware that package `B` exists locally too, so it will + generate the various pub files to point to a relative path in the local + repository. - If you wanted to use pub you could of course define a dependency override in the pubspec of package `A` that sets a path for package `B` but, then you'd have to do this manually every time and then manually remove it @@ -86,20 +81,19 @@ melos bootstrap --diff="main" ## Bootstrap flags -- The `--no-example` flag is used to exclude flutter package's example's dependencies - (https://github.com/dart-lang/pub/pull/3856) +- The `--no-example` flag is used to exclude flutter package's example's + dependencies (https://github.com/dart-lang/pub/pull/3856) - This will run `pub get` with the `--no-example` flag. - The `--enforce-lockfile` flag is used to enforce versions from `.lock` files. - Ensure .lock files exist, as failure may occur if they're not checked in. -- The `--no-enforce-lockfile` flag is used to disregard versions from `.lock` files if - `enforce-lockfile` is configured in the `melos.yaml` file. -- The `--skip-linking` flag is used to skip the local linking of workspace packages. -- The `--offline` flag is used to only resolve dependencies from the local cache by running - `pub get` with the `--offline` flag. +- The `--no-enforce-lockfile` flag is used to disregard versions from `.lock` + files if `enforce-lockfile` is configured in the `melos.yaml` file. +- The `--offline` flag is used to only resolve dependencies from the local + cache by running `pub get` with the `--offline` flag. -In addition to the above flags, the `melos bootstrap` command supports a few different flags that -can be defined in your `melos.yaml` file. +In addition to the above flags, the `melos bootstrap` command supports a few +different configurations that can be defined in your `melos.yaml` file. ### Shared dependencies diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx index 10d96db72..40d1892c1 100644 --- a/docs/getting-started.mdx +++ b/docs/getting-started.mdx @@ -8,6 +8,12 @@ description: Learn how to start using Melos in your project Melos requires a few one-off steps to be completed before you can start using it. +## Pub Workspaces + +First start by reading the short Pub Workspaces guide for how to get your +monorepo ready to be used with Melos and Pub Workspaces, the guide can be +found on the [Dart website](https://dart.dev/tools/pub/workspaces). + ## Installation Install Melos as a @@ -59,11 +65,18 @@ now: ```yaml name: my_project - +publish_to: none environment: - sdk: '>=3.0.0 <4.0.0' + sdk: ^3.6.0 +workspace: + - packages/helper + - packages/client_package + - packages/server_package ``` +Where `packages/helper`, `packages/client_package` and `packages/server_package` +are the paths to the packages in your workspace. + The corresponding `pubspec.lock` file should also be committed. Make sure to exclude it from the `.gitignore` file. @@ -73,6 +86,18 @@ Add Melos as a development dependency by running the following command: dart pub add melos --dev ``` +### Configure your packages + +Next, in all your packages `pubspec.yaml` files, add the +`resolution: workspace` field: + +```yaml +name: my_package +resolution: workspace + +... +``` + ### Configure the workspace Next create a `melos.yaml` file at the repository root. Within the `melos.yaml` @@ -90,43 +115,23 @@ The `packages` list should contain paths to the individual packages within your project. Each path can be defined using the [glob](https://docs.python.org/3/library/glob.html) pattern expansion format. -Melos generates `pubspec_overrides.yaml` files to link local packages for -development. Typically these files should be ignored by git. To ignore these -files, add the following to your `.gitignore` file: - -``` -pubspec_overrides.yaml -``` - ## Bootstrapping -Once installed & setup, Melos needs to be bootstrapped. Bootstrapping has 2 -primary roles: +Once installed & setup, Melos needs to be bootstrapped. Bootstrapping has +three primary functions: 1. Installing all package dependencies (internally using `pub get`). -2. Locally linking any packages together. +2. Syncing shared dependencies between packages. +3. Running any bootstrap lifecycle scripts. -Bootstrap your project by running the following command: +Bootstrap your project by running the following command: ```bash melos bootstrap ``` -### Why do I need to bootstrap? - -In normal projects, packages can be linked by providing a `path` within the -`pubspec.yaml`. This works for small projects however presents a problem at -scale. Packages cannot be published with a locally defined path, meaning once -you're ready to publish your packages you'll need to manually update all the -packages `pubspec.yaml` files with the versions. If your packages are also -tightly coupled (dependencies of each other), you'll also have to manually check -which versions should be updated. Even with a few packages this can become a -long and error-prone task. - -Melos solves this problem by overriding local files which the Dart analyzer uses -to read packages from. If a local package exists (defined in the `melos.yaml` -file) and a different local package has it listed as a dependency, it will be -linked regardless of whether a version has been specified. +If you wonder why bootrapping is needed you can read more about it in the +[Bootstrap section](/commands/bootstrap). ## Next steps @@ -146,15 +151,16 @@ packages: - packages/** scripts: - analyze: - exec: dart analyze . + generate: + run: melos exec -c 1 --depends-on build_runner -- dart run build_runner build ``` -Then execute the command by running `melos run analyze`. +Then execute the command by running `melos generate`. If you're looking for some inspiration as to what scripts can help with, check out the -[FlutterFire repository](https://github.com/firebase/flutterfire/blob/master/melos.yaml). +[FlutterFire repository](https://github.com/firebase/flutterfire/blob/master/melos.yaml) +or the [Flame repository](https://github.com/flame-engine/flame/blob/main/melos.yaml). If you are using VS Code, there is an [extension](/ide-support#vs-code) available, to integrate Melos with VS Code. diff --git a/docs/guides/migrations.mdx b/docs/guides/migrations.mdx index 93808a9f4..b5b7a804c 100644 --- a/docs/guides/migrations.mdx +++ b/docs/guides/migrations.mdx @@ -5,6 +5,41 @@ description: How to migrate between major versions of Melos. # Migrations +## 6.x.x to 7.x.x + +Since the [pub workspaces](https://dart.dev/tools/pub/workspaces) feature has +been released, Melos has been updated to rely on that instead of creating +`pubspec_overrides.yaml` files and thus some migration is needed. + +The main difference is that you now have to add `resolution: workspace` to all +of your packages' `pubspec.yaml` files and add a list of all your packages to +the root `pubspec.yaml` file, similar to this: + +Package `pubspec.yaml` file: +```yaml +name: my_package +environment: + sdk: ^3.6.0 +resolution: workspace +``` + +Workspace root `pubspec.yaml` file: +```yaml +name: my_workspace +publish_to: none +environment: + sdk: ^3.6.0 +workspace: + - packages/helper + - packages/client_package + - packages/server_package +dev_dependencies: + melos: ^7.0.0 +``` + +> [!NOTE] +> You have to use Dart SDK 3.6.0 or newer to use pub workspaces. + ## 3.0.0 to 4.0.0 ### `--no-git-tag-version` behavior change diff --git a/docs/index.mdx b/docs/index.mdx index 5621214af..68fe6da80 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -30,7 +30,8 @@ other. Features include: - Listing of local packages & their dependencies. Melos also works great on CI/CD environments to help automate complex tasks and -challenges. +challenges. If you're using GitHub you can check out the +[Melos GitHub Action](https://github.com/bluefireteam/melos-action). ## Projects using Melos diff --git a/packages/melos/README.md b/packages/melos/README.md index 20d66d9f9..980fb354a 100644 --- a/packages/melos/README.md +++ b/packages/melos/README.md @@ -44,6 +44,15 @@ The main difference is that you now have to add `resolution: workspace` to all of your packages' `pubspec.yaml` files and add a list of all your packages to the root `pubspec.yaml` file, similar to this: +Package `pubspec.yaml` file: +```yaml +name: my_package +environment: + sdk: ^3.6.0 +resolution: workspace +``` + +Workspace root `pubspec.yaml` file: ```yaml name: my_workspace publish_to: none From bbffbb64444c72394e8c14b8a4cb5eb66fb315f1 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 6 Jan 2025 18:45:50 +0100 Subject: [PATCH 6/8] deps: Set path to any to allow flutter to pin it --- melos.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/melos.yaml b/melos.yaml index 45dfc2c05..f155bc214 100644 --- a/melos.yaml +++ b/melos.yaml @@ -25,7 +25,7 @@ command: http: ^1.2.2 meta: any mustache_template: ^2.0.0 - path: ^1.9.1 + path: any platform: ^3.1.6 pool: ^1.5.1 prompts: ^2.0.0 From 5d7645e6daee32e6093bdfe9f170f39fd9e40358 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 6 Jan 2025 18:47:15 +0100 Subject: [PATCH 7/8] chore: Push path dep version change --- packages/melos/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/melos/pubspec.yaml b/packages/melos/pubspec.yaml index 3a3cf0580..f9b283fe4 100644 --- a/packages/melos/pubspec.yaml +++ b/packages/melos/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: intl: any meta: any mustache_template: ^2.0.0 - path: ^1.9.1 + path: any platform: ^3.1.6 pool: ^1.5.1 prompts: ^2.0.0 From 36019fa33c65cf1b22558877b83246992526ba0b Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 6 Jan 2025 18:53:24 +0100 Subject: [PATCH 8/8] docs: FlutterFire link to point to main branch --- docs/getting-started.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx index 40d1892c1..cde348cfc 100644 --- a/docs/getting-started.mdx +++ b/docs/getting-started.mdx @@ -159,7 +159,7 @@ Then execute the command by running `melos generate`. If you're looking for some inspiration as to what scripts can help with, check out the -[FlutterFire repository](https://github.com/firebase/flutterfire/blob/master/melos.yaml) +[FlutterFire repository](https://github.com/firebase/flutterfire/blob/main/melos.yaml) or the [Flame repository](https://github.com/flame-engine/flame/blob/main/melos.yaml). If you are using VS Code, there is an [extension](/ide-support#vs-code)