From 9b080a5d509daf7d6baaa28c2eb40ba12e235fd3 Mon Sep 17 00:00:00 2001 From: Gabriel Terwesten Date: Sat, 4 Feb 2023 22:30:21 +0100 Subject: [PATCH] feat!: local installation of melos in workspace (#431) --- .github/workflows/scripts/install-tools.bat | 2 +- .github/workflows/scripts/install-tools.sh | 2 +- CONTRIBUTING.md | 4 +- all_lint_rules.yaml | 9 +- analysis_options.yaml | 3 + docs/getting-started.mdx | 98 ++++++++--- docs/guides/migrations.mdx | 32 ++++ melos.yaml | 2 +- packages/melos/README.md | 11 +- packages/melos/bin/melos.dart | 69 +------- packages/melos/lib/src/command_runner.dart | 94 +++++++++- packages/melos/lib/src/common/utils.dart | 27 ++- packages/melos/lib/src/workspace_configs.dart | 164 +++++++++++------- packages/melos/pubspec.yaml | 1 + packages/melos/test/command_runner_test.dart | 8 +- .../melos/test/commands/bootstrap_test.dart | 34 ++-- packages/melos/test/commands/clean_test.dart | 4 +- packages/melos/test/commands/exec_test.dart | 21 +-- packages/melos/test/commands/list_test.dart | 27 ++- packages/melos/test/commands/run_test.dart | 15 +- packages/melos/test/commands/script_test.dart | 12 +- packages/melos/test/package_filter_test.dart | 8 +- packages/melos/test/package_test.dart | 2 +- packages/melos/test/utils.dart | 78 +++++++-- packages/melos/test/utils_test.dart | 2 +- packages/melos/test/workspace_test.dart | 131 +++++++------- pubspec.yaml | 15 +- 27 files changed, 537 insertions(+), 338 deletions(-) diff --git a/.github/workflows/scripts/install-tools.bat b/.github/workflows/scripts/install-tools.bat index 354d64fb..4c8f185c 100644 --- a/.github/workflows/scripts/install-tools.bat +++ b/.github/workflows/scripts/install-tools.bat @@ -1,4 +1,4 @@ -CMD /C dart pub global activate --source=path . --executable=melos +CMD /C dart pub global activate --source=path . --executable=melos --overwrite REM Workaround an issue when running global executables on Windows for the first time. CMD /C melos > NUL melos bootstrap \ No newline at end of file diff --git a/.github/workflows/scripts/install-tools.sh b/.github/workflows/scripts/install-tools.sh index 75b48594..edb05cc8 100755 --- a/.github/workflows/scripts/install-tools.sh +++ b/.github/workflows/scripts/install-tools.sh @@ -1,4 +1,4 @@ #!/bin/bash -dart pub global activate --source="path" . --executable="melos" +dart pub global activate --source="path" . --executable="melos" --overwrite melos bootstrap diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 643b53bf..63740a40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,6 +61,8 @@ To setup and use this melos mono repo locally for the purposes of contributing, ```bash # Install melos if it's not already installed: dart pub global activate melos +# Bootstrap the workspace. +melos bootstrap # Activate 'melos' from path: melos activate # Confirm you now using a local development version: @@ -90,7 +92,7 @@ To send us a pull request: Please make sure all your check-ins have detailed commit messages explaining the patch. When naming the title of your pull request, please follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/) -guide. +guide. Please also enable **“Allow edits by maintainers”**, this will help to speed-up the review process as well. diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml index cc0804b5..a7df47c1 100644 --- a/all_lint_rules.yaml +++ b/all_lint_rules.yaml @@ -19,11 +19,11 @@ linter: - always_declare_return_types - always_put_control_body_on_new_line - always_put_required_named_parameters_first - - always_require_non_null_named_parameters - always_specify_types - always_use_package_imports - annotate_overrides - avoid_annotating_with_dynamic + # - avoid_as - avoid_bool_literals_in_conditional_expressions - avoid_catches_without_on_clauses - avoid_catching_errors @@ -48,8 +48,6 @@ linter: - avoid_relative_lib_imports - avoid_renaming_method_parameters - avoid_return_types_on_setters - - avoid_returning_null - - avoid_returning_null_for_future - avoid_returning_null_for_void - avoid_returning_this - avoid_setters_without_getters @@ -95,6 +93,7 @@ linter: - hash_and_equals - implementation_imports - implicit_call_tearoffs + - invariant_booleans - iterable_contains_unrelated_type - join_return_with_assignment - leading_newlines_in_multiline_strings @@ -128,6 +127,7 @@ linter: - prefer_adjacent_string_concatenation - prefer_asserts_in_initializer_lists - prefer_asserts_with_message + - prefer_bool_in_asserts - prefer_collection_literals - prefer_conditional_assignment - prefer_const_constructors @@ -137,6 +137,7 @@ linter: - prefer_constructors_over_static_methods - prefer_contains - prefer_double_quotes + - prefer_equal_for_default_values - prefer_expression_function_bodies - prefer_final_fields - prefer_final_in_for_each @@ -176,6 +177,7 @@ linter: - sort_constructors_first - sort_pub_dependencies - sort_unnamed_constructors_first + - super_goes_last - test_types_in_equals - throw_in_finally - tighten_type_of_initializing_formals @@ -184,6 +186,7 @@ linter: - unawaited_futures - unnecessary_await_in_return - unnecessary_brace_in_string_interps + - unnecessary_breaks - unnecessary_const - unnecessary_constructor_name - unnecessary_final diff --git a/analysis_options.yaml b/analysis_options.yaml index f1fe9dc4..b4a92bbc 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -87,3 +87,6 @@ linter: # Not a common style and would add a lot of verbosity to function signature. # 'parameter_assignments' already enforces this to an extent. prefer_final_parameters: false + + # Useful to allow for `multiLine` utility function. + no_adjacent_strings_in_list: false diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx index 4fa3422d..b2e71c4b 100644 --- a/docs/getting-started.mdx +++ b/docs/getting-started.mdx @@ -5,34 +5,72 @@ description: Learn how to start using Melos in your project # Getting Started -Melos requires a few one-off steps to be completed before you can start using it. +Melos requires a few one-off steps to be completed before you can start using +it. ## Installation -Melos can be installed as a global package via [pub.dev](https://pub.dev/): +Installe Melos as a +[global package](https://dart.dev/tools/pub/cmd/pub-global#running-a-script-from-your-path) +via [pub.dev](https://pub.dev/) so it can be used from anywhere on your system: ```bash dart pub global activate melos ``` -### Setup +## Setup a workspace -To set up your project to use Melos, create a `melos.yaml` file in the root of the project. +Melos is designed to work with a workspace. A workspace is a directory which +contains all the packages that are going to be developed together. Its root +directory must contain a `melos.yaml` file. -Within the `melos.yaml` file, add `name` and `packages` fields: +### Install Melos in the workspace + +Different Melos workspaces might use different versions of Melos. To ensure +everyone working in the workspace (as well as CI jobs) is using the same version +of Melos, a dependency on the `melos` package has to be added to the +`pubspec.yaml` file at the workspace root directory. The globally installed +version of Melos will switch to the version specified in the `pubspec.yaml` +file, if both versions are not the same. + +If you don't have a `pubspec.yaml` file at the workspace root yet, create one +now: ```yaml -name: my_project +name: _workspace + +environment: + sdk: '>=2.18.0 <3.0.0' +``` + +The corresponding `pubspec.lock` file should also be committed. Make sure to +exclude it from the `.gitignore` file. + +Add Melos as a development dependency by running the following command: + +```bash +dart pub add melos --dev +``` + +### Configure the workspace + +Next create a `melos.yaml` file at the repository root. Within the `melos.yaml` +file, add the `name` and `packages` fields: + +```yaml +name: packages: - - packages/** + - packages/* ``` -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. +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: +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 @@ -40,29 +78,37 @@ 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 2 +primary roles: 1. Installing all package dependencies (internally using `pub get`). 2. Locally linking any packages together. ### 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. +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. +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. ## Next steps -Once successfully bootstrapped, you can develop your packages side-by-side with changes to a single package immediately reflecting -across other dependent packages. +Once successfully bootstrapped, you can develop your packages side-by-side with +changes to a single package immediately reflecting across other dependent +packages. -Melos also provides other helpful features such as running scripts across all packages. For example, to run dart analyzer in each package, add a new `script` item in your `melos.yaml`: +Melos also provides other helpful features such as running scripts across all +packages. For example, to run dart analyzer in each package, add a new `script` +item in your `melos.yaml`: ```yaml name: my_project @@ -77,7 +123,9 @@ scripts: Then execute the command by running `melos run analyze`. -If you're looking for some inspiration as to what scripts can help with, check out the +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). -If you are using VS Code, there is an [extension](/ide-support#vs-code) available, to integrate Melos with VS Code. +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 12344c1c..6a6d7f6c 100644 --- a/docs/guides/migrations.mdx +++ b/docs/guides/migrations.mdx @@ -7,6 +7,38 @@ description: How to migrate between major versions of Melos. ## 2.0.0 to 3.0.0 +### Versioning of Melos in workspaces + +From Melos 3.0.0, the version of Melos to use in any given workspace must be +specified in a `pubspec.yaml` file next to the `melos.yaml` file, in the +workspace root directory. + +Different Melos workspaces might use different versions of Melos. To ensure +everyone working in the workspace (as well as CI jobs) is using the same version +of Melos, a dependency on the `melos` package has to be added to the +`pubspec.yaml` file at the workspace root directory. The globally installed +version of Melos will switch to the version specified in the `pubspec.yaml` +file, if both versions are not the same. + +If you don't have a `pubspec.yaml` file at the workspace root yet, create one +now: + +```yaml +name: _workspace + +environment: + sdk: '>=2.18.0 <3.0.0' +``` + +The corresponding `pubspec.lock` file should also be committed. Make sure to +exclude it from the `.gitignore` file. + +Add Melos as a development dependency by running the following command: + +```bash +dart pub add melos --dev +``` + ### Local package linking with `pubspec_overrides.yaml` The initial mechanism used by Melos to link local packages for development had diff --git a/melos.yaml b/melos.yaml index 6e0b31fc..78cf7523 100644 --- a/melos.yaml +++ b/melos.yaml @@ -36,7 +36,7 @@ scripts: concurrency: 1 packageFilters: dirExists: - - 'test/' + - test # This tells Melos tests to ignore env variables passed to tests from `melos run test` # as they could change the behaviour of how tests filter packages. env: diff --git a/packages/melos/README.md b/packages/melos/README.md index 99c7f671..a0f9ae8e 100644 --- a/packages/melos/README.md +++ b/packages/melos/README.md @@ -131,15 +131,8 @@ The following projects are using Melos: ## Getting Started -Install the latest Melos version as a global package via -[Pub](https://pub.dev/). - -```bash -dart pub global activate melos - -# Or alternatively to specify a specific version: -# pub global activate melos 0.4.1 -``` +Go to the [Getting Started](https://melos.invertase.dev/getting-started) page of +the [documentation](https://docs.page/invertase/melos) to start using Melos. --- diff --git a/packages/melos/bin/melos.dart b/packages/melos/bin/melos.dart index cdc76c6d..0db70130 100644 --- a/packages/melos/bin/melos.dart +++ b/packages/melos/bin/melos.dart @@ -1,64 +1,11 @@ -// ignore_for_file: avoid_print - -import 'dart:io'; - -import 'package:args/command_runner.dart'; +import 'package:cli_launcher/cli_launcher.dart'; import 'package:melos/src/command_runner.dart'; -import 'package:melos/src/common/exception.dart'; -import 'package:melos/src/common/utils.dart' as utils; -import 'package:melos/src/workspace_configs.dart'; -import 'package:melos/version.g.dart'; -import 'package:pub_updater/pub_updater.dart'; - -Future main(List arguments) async { - if (arguments.contains('--version') || arguments.contains('-v')) { - print(melosVersion); - // No version checks on CIs. - if (utils.isCI) return; - // Check for updates. - final pubUpdater = PubUpdater(); - const packageName = 'melos'; - final isUpToDate = await pubUpdater.isUpToDate( - packageName: packageName, - currentVersion: melosVersion, +Future main(List arguments) async => launchExecutable( + arguments, + LaunchConfig( + name: ExecutableName('melos'), + launchFromSelf: false, + entrypoint: melosEntryPoint, + ), ); - if (!isUpToDate) { - final latestVersion = await pubUpdater.getLatestVersion(packageName); - final shouldUpdate = utils.promptBool( - message: 'There is a new version of $packageName available ' - '($latestVersion). Would you like to update?', - defaultsTo: true, - defaultsToWithoutPrompt: false, - ); - if (shouldUpdate) { - await pubUpdater.update(packageName: packageName); - print('$packageName has been updated to version $latestVersion.'); - } - } - - return; - } - try { - final config = shouldUseEmptyConfig(arguments) - ? MelosWorkspaceConfig.empty() - : await MelosWorkspaceConfig.fromDirectory(Directory.current); - await MelosCommandRunner(config).run(arguments); - } on MelosException catch (err) { - stderr.writeln(err.toString()); - exitCode = 1; - } on UsageException catch (err) { - stderr.writeln(err.toString()); - exitCode = 1; - } catch (err) { - exitCode = 1; - rethrow; - } -} - -bool shouldUseEmptyConfig(List arguments) { - final willShowHelp = arguments.isEmpty || - arguments.contains('--help') || - arguments.contains('-h'); - return willShowHelp; -} diff --git a/packages/melos/lib/src/command_runner.dart b/packages/melos/lib/src/command_runner.dart index 921bc191..4ff9532c 100644 --- a/packages/melos/lib/src/command_runner.dart +++ b/packages/melos/lib/src/command_runner.dart @@ -15,9 +15,16 @@ * */ +import 'dart:async'; +import 'dart:io'; + import 'package:args/args.dart'; import 'package:args/command_runner.dart'; +import 'package:cli_launcher/cli_launcher.dart'; +import 'package:cli_util/cli_logging.dart'; +import 'package:pub_updater/pub_updater.dart'; +import '../version.g.dart'; import 'command_runner/bootstrap.dart'; import 'command_runner/clean.dart'; import 'command_runner/exec.dart'; @@ -26,14 +33,17 @@ import 'command_runner/publish.dart'; import 'command_runner/run.dart'; import 'command_runner/script.dart'; import 'command_runner/version.dart'; +import 'common/exception.dart'; import 'common/utils.dart'; +import 'common/utils.dart' as utils; +import 'logging.dart'; import 'workspace_configs.dart'; /// A class that can run Melos commands. /// /// To run a command, do: /// -/// ```dart main +/// ```dart /// final melos = MelosCommandRunner(); /// /// await melos.run(['bootstrap']); @@ -80,3 +90,85 @@ class MelosCommandRunner extends CommandRunner { await super.runCommand(topLevelResults); } } + +@override +FutureOr melosEntryPoint( + List arguments, + LaunchContext context, +) async { + if (arguments.contains('--version') || arguments.contains('-v')) { + final logger = MelosLogger(Logger.standard()); + + logger.log(melosVersion); + + // No version checks on CIs. + if (utils.isCI) return; + + // Check for updates. + final pubUpdater = PubUpdater(); + const packageName = 'melos'; + final isUpToDate = await pubUpdater.isUpToDate( + packageName: packageName, + currentVersion: melosVersion, + ); + if (!isUpToDate) { + final latestVersion = await pubUpdater.getLatestVersion(packageName); + final isGlobal = context.localInstallation == null; + + if (isGlobal) { + final shouldUpdate = utils.promptBool( + message: 'There is a new version of $packageName available ' + '($latestVersion). Would you like to update?', + defaultsTo: true, + defaultsToWithoutPrompt: false, + ); + if (shouldUpdate) { + await pubUpdater.update(packageName: packageName); + logger.log( + '$packageName has been updated to version $latestVersion.', + ); + } + } else { + logger.log( + 'There is a new version of $packageName available ' + '($latestVersion).', + ); + } + } + return; + } + try { + final config = + await _resolveConfig(arguments, context.localInstallation?.packageRoot); + await MelosCommandRunner(config).run(arguments); + } on MelosException catch (err) { + stderr.writeln(err.toString()); + exitCode = 1; + } on UsageException catch (err) { + stderr.writeln(err.toString()); + exitCode = 1; + } catch (err) { + exitCode = 1; + rethrow; + } +} + +Future _resolveConfig( + List arguments, + Directory? workspaceRoot, +) async { + if (_shouldUseEmptyConfig(arguments)) { + return MelosWorkspaceConfig.empty(); + } + if (workspaceRoot == null) { + return MelosWorkspaceConfig.handleWorkspaceNotFound(Directory.current); + } + return MelosWorkspaceConfig.fromWorkspaceRoot(workspaceRoot); +} + +bool _shouldUseEmptyConfig(List arguments) { + final willShowHelp = arguments.isEmpty || + arguments.contains('--help') || + arguments.contains('-h'); + return willShowHelp; +} diff --git a/packages/melos/lib/src/common/utils.dart b/packages/melos/lib/src/common/utils.dart index 037c4bb8..537773af 100644 --- a/packages/melos/lib/src/common/utils.dart +++ b/packages/melos/lib/src/common/utils.dart @@ -65,6 +65,18 @@ extension Let on T? { String describeEnum(Object value) => value.toString().split('.').last; +/// Utility function to write inline multi-line strings with indentation and +/// without trailing a new line. +/// +/// ```dart +/// print(multiLine([ +/// 'The quick brown fox jumps over the lazy dog.', +/// '', // Empty line +/// 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod.', +/// ])); +/// ``` +String multiLine(List lines) => lines.join('\n'); + // MELOS_PACKAGES environment variable is a comma delimited list of // package names - used instead of filters if it is present. // This can be user defined or can come from package selection in `melos run`. @@ -298,21 +310,6 @@ Future getMelosRoot() async { return p.normalize('${melosPackageFileUri!.toFilePath()}/../..'); } -YamlMap? loadYamlFileSync(String path) { - if (!fileExists(path)) return null; - - return loadYaml(readTextFile(path)) as YamlMap; -} - -Future loadYamlFile(String path) async { - if (!fileExists(path)) return null; - - return loadYaml( - await readTextFileAsync(path), - sourceUrl: Uri.parse(path), - ) as YamlMap; -} - String melosYamlPathForDirectory(String directory) => p.join(directory, 'melos.yaml'); diff --git a/packages/melos/lib/src/workspace_configs.dart b/packages/melos/lib/src/workspace_configs.dart index 3c417e6d..cab79b50 100644 --- a/packages/melos/lib/src/workspace_configs.dart +++ b/packages/melos/lib/src/workspace_configs.dart @@ -17,16 +17,16 @@ import 'dart:io'; +import 'package:ansi_styles/ansi_styles.dart'; import 'package:collection/collection.dart'; import 'package:glob/glob.dart'; import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; import '../melos.dart'; import 'common/git_repository.dart'; import 'common/glob.dart'; import 'common/io.dart'; -import 'common/platform.dart'; import 'common/utils.dart'; import 'common/validation.dart'; import 'scripts.dart'; @@ -752,18 +752,6 @@ class MelosWorkspaceConfig { ); } - MelosWorkspaceConfig.fallback({required String path}) - : this( - name: 'Melos', - packages: [ - createGlob('packages/**', currentDirectoryPath: path), - ], - path: currentPlatform.isWindows - ? p.windows.normalize(path).replaceAll(r'\', r'\\') - : path, - commands: CommandConfigs.empty, - ); - MelosWorkspaceConfig.empty() : this( name: 'Melos', @@ -772,69 +760,109 @@ class MelosWorkspaceConfig { commands: CommandConfigs.empty, ); - static Directory? _searchForAncestorDirectoryWithMelosYaml(Directory from) { - for (var testedDirectory = from; - testedDirectory.path != testedDirectory.parent.path; - testedDirectory = testedDirectory.parent) { - if (isWorkspaceDirectory(testedDirectory.path)) { - return testedDirectory; - } + /// Loads the [MelosWorkspaceConfig] for the workspace at [workspaceRoot]. + static Future fromWorkspaceRoot( + Directory workspaceRoot, + ) async { + final melosYamlFile = File(melosYamlPathForDirectory(workspaceRoot.path)); + + if (!melosYamlFile.existsSync()) { + throw UnresolvedWorkspace( + multiLine([ + 'Found no melos.yaml file in "${workspaceRoot.path}".', + '', + 'You must have a ${AnsiStyles.bold('melos.yaml')} file in the root ' + 'of your workspace.', + '', + 'For more information, see: ' + 'https://melos.invertase.dev/configuration/overview', + ]), + ); } - return null; - } - /// Creates a new configuration from a [Directory]. - /// - /// If no `melos.yaml` is found, but [Directory] contains a `packages/` - /// sub-directory, a configuration for those packages will be created. - static Future fromDirectory( - Directory directory, - ) async { - final melosWorkspaceDirectory = - _searchForAncestorDirectoryWithMelosYaml(directory); + Object? melosYamlContents; + try { + melosYamlContents = loadYamlNode( + await melosYamlFile.readAsString(), + sourceUrl: melosYamlFile.uri, + ).toPlainObject(); + } on YamlException catch (error) { + throw MelosConfigException('Failed to parse melos.yaml:\n$error'); + } - if (melosWorkspaceDirectory == null) { - // Allow melos to use a project without a `melos.yaml` file if a - // `packages` directory exists. - final packagesDirectory = p.joinAll([directory.path, 'packages']); + if (melosYamlContents is! Map) { + throw MelosConfigException('melos.yaml must contain a YAML map.'); + } - if (dirExists(packagesDirectory)) { - return MelosWorkspaceConfig.fallback(path: directory.path) - ..validatePhysicalWorkspace(); + final melosOverridesYamlFile = + File(melosOverridesYamlPathForDirectory(workspaceRoot.path)); + if (melosOverridesYamlFile.existsSync()) { + Object? melosOverridesYamlContents; + try { + melosOverridesYamlContents = loadYamlNode( + await melosOverridesYamlFile.readAsString(), + sourceUrl: melosOverridesYamlFile.uri, + ).toPlainObject(); + } on YamlException catch (error) { + throw MelosConfigException( + 'Failed to parse melos_overrides.yaml:\n$error', + ); } - throw MelosConfigException( - ''' -Your current directory does not appear to be a valid Melos workspace. + if (melosOverridesYamlContents is! Map) { + throw MelosConfigException( + 'melos_overrides.yaml must contain a YAML map.', + ); + } -You must have one of the following to be a valid Melos workspace: - - a "melos.yaml" file in the root with a "packages" option defined - - a "packages" directory -''', - ); + mergeMap(melosYamlContents, melosOverridesYamlContents); } - final melosYamlPath = - melosYamlPathForDirectory(melosWorkspaceDirectory.path); - final yamlContents = (await loadYamlFile(melosYamlPath))?.toPlainObject() - as Map?; + return MelosWorkspaceConfig.fromYaml( + melosYamlContents, + path: workspaceRoot.path, + )..validatePhysicalWorkspace(); + } - if (yamlContents == null) { - throw MelosConfigException('Failed to parse the melos.yaml file'); + /// Handles the case where a workspace could not be found in the [current] + /// or a parent directory by throwing an error with a helpful message. + static Future handleWorkspaceNotFound(Directory current) async { + final legacyWorkspace = await _findMelosYaml(current); + if (legacyWorkspace != null) { + throw UnresolvedWorkspace( + multiLine([ + 'Found a melos.yaml file in "${legacyWorkspace.path}" but no local ' + 'installation of Melos.', + '', + 'From version 3.0.0, the ${AnsiStyles.bold('melos')} package must be ' + 'installed in a ${AnsiStyles.bold('pubspec.yaml')} file next to ' + 'the melos.yaml file.', + '', + 'For more information, see: ' + 'https://melos.invertase.dev/guides/migrations#200-to-300' + ]), + ); } - final melosOverridesYamlPath = - melosOverridesYamlPathForDirectory(melosWorkspaceDirectory.path); - final overridesYamlContents = (await loadYamlFile(melosOverridesYamlPath)) - ?.toPlainObject() as Map?; - if (overridesYamlContents != null) { - mergeMap(yamlContents, overridesYamlContents); + throw UnresolvedWorkspace( + multiLine([ + 'Your current directory does not appear to be within a Melos ' + 'workspace.', + '', + 'For setting up a workspace, see: ' + 'https://melos.invertase.dev/getting-started#setup', + ]), + ); + } + + static Future _findMelosYaml(Directory start) async { + final melosYamlFile = File(melosYamlPathForDirectory(start.path)); + if (melosYamlFile.existsSync()) { + return start; } - return MelosWorkspaceConfig.fromYaml( - yamlContents, - path: melosWorkspaceDirectory.path, - )..validatePhysicalWorkspace(); + final parent = start.parent; + return parent.path == start.path ? null : _findMelosYaml(parent); } /// The absolute path to the workspace folder. @@ -967,3 +995,13 @@ class _GlobEquality implements Equality { @override bool isValidKey(Object? o) => true; } + +/// An exception thrown when a Melos workspace could not be resolved. +class UnresolvedWorkspace implements MelosException { + UnresolvedWorkspace(this.message); + + final String message; + + @override + String toString() => message; +} diff --git a/packages/melos/pubspec.yaml b/packages/melos/pubspec.yaml index cb59225b..92f17294 100644 --- a/packages/melos/pubspec.yaml +++ b/packages/melos/pubspec.yaml @@ -17,6 +17,7 @@ executables: dependencies: ansi_styles: ^0.3.1 args: ^2.0.0 + cli_launcher: ^0.3.0 cli_util: ^0.3.0 collection: ^1.14.12 conventional_commit: ^0.5.0+1 diff --git a/packages/melos/test/command_runner_test.dart b/packages/melos/test/command_runner_test.dart index 513e0e7e..63629768 100644 --- a/packages/melos/test/command_runner_test.dart +++ b/packages/melos/test/command_runner_test.dart @@ -9,7 +9,7 @@ import 'utils.dart'; void main() { group('CommandRunner', () { test('adds hidden script commands', () async { - final workspaceDir = createTemporaryWorkspaceDirectory( + final workspaceDir = await createTemporaryWorkspace( configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -23,7 +23,7 @@ void main() { ), ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final runner = MelosCommandRunner(config); expect( @@ -37,7 +37,7 @@ void main() { }); test('excludes conflicting script commands', () async { - final workspaceDir = createTemporaryWorkspaceDirectory( + final workspaceDir = await createTemporaryWorkspace( configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -50,7 +50,7 @@ void main() { ), ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final runner = MelosCommandRunner(config); final command = runner.commands['run']; diff --git a/packages/melos/test/commands/bootstrap_test.dart b/packages/melos/test/commands/bootstrap_test.dart index fc95f983..5341e982 100644 --- a/packages/melos/test/commands/bootstrap_test.dart +++ b/packages/melos/test/commands/bootstrap_test.dart @@ -50,7 +50,7 @@ void main() { path: '', ); - final workspaceDir = createTemporaryWorkspaceDirectory(); + final workspaceDir = await createTemporaryWorkspace(); final aPath = p.join(workspaceDir.path, 'packages', 'a'); @@ -78,7 +78,7 @@ void main() { ); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( config, logger: logger.toMelosLogger(), @@ -152,7 +152,7 @@ Generating IntelliJ IDE files... }); test('resolves workspace packages with path dependency', () async { - final workspaceDir = createTemporaryWorkspaceDirectory(); + final workspaceDir = await createTemporaryWorkspace(); final aDir = await createProject( workspaceDir, @@ -177,7 +177,7 @@ Generating IntelliJ IDE files... ); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos( logger: logger, config: config, @@ -254,9 +254,7 @@ Generating IntelliJ IDE files... ); test('respects user dependency_overrides', () async { - final workspaceDir = createTemporaryWorkspaceDirectory( - configBuilder: (path) => MelosWorkspaceConfig.fallback(path: path), - ); + final workspaceDir = await createTemporaryWorkspace(); final pkgA = await createProject( workspaceDir, @@ -275,7 +273,7 @@ Generating IntelliJ IDE files... ); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos( logger: logger, config: config, @@ -293,9 +291,7 @@ Generating IntelliJ IDE files... }); test('bootstrap flutter example packages', () async { - final workspaceDir = createTemporaryWorkspaceDirectory( - configBuilder: (path) => MelosWorkspaceConfig.fallback(path: path), - ); + final workspaceDir = await createTemporaryWorkspace(); await createProject( workspaceDir, @@ -320,7 +316,7 @@ Generating IntelliJ IDE files... ); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos( logger: logger, config: config, @@ -493,7 +489,7 @@ dependency_overrides: }); test('handles errors in pub get', () async { - final workspaceDir = createTemporaryWorkspaceDirectory(); + final workspaceDir = await createTemporaryWorkspace(); await createProject( workspaceDir, @@ -508,7 +504,7 @@ dependency_overrides: ); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( config, logger: logger.toMelosLogger(), @@ -550,7 +546,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 = createTemporaryWorkspaceDirectory( + final workspaceDir = await createTemporaryWorkspace( configBuilder: (path) => MelosWorkspaceConfig.fromYaml( createYamlMap( { @@ -567,7 +563,7 @@ e-Because a depends on package_that_does_not_exists any which doesn't exist (cou ); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( config, logger: logger.toMelosLogger(), @@ -635,9 +631,7 @@ Future runMelosBootstrap(Melos melos, TestLogger logger) async { Future dependencyResolutionTest( Map> packages, ) async { - final workspaceDir = createTemporaryWorkspaceDirectory( - configBuilder: (path) => MelosWorkspaceConfig.fallback(path: path), - ); + final workspaceDir = await createTemporaryWorkspace(); Future> createPackage( MapEntry> entry, @@ -694,7 +688,7 @@ Future dependencyResolutionTest( } final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos( logger: logger, config: config, diff --git a/packages/melos/test/commands/clean_test.dart b/packages/melos/test/commands/clean_test.dart index ea307bc2..2d955f3f 100644 --- a/packages/melos/test/commands/clean_test.dart +++ b/packages/melos/test/commands/clean_test.dart @@ -12,7 +12,7 @@ import '../utils.dart'; void main() { group('clean', () { test('removes dependency overrides from pubspec_overrides.yaml', () async { - final workspaceDir = createTemporaryWorkspaceDirectory( + final workspaceDir = await createTemporaryWorkspace( configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_workspace', @@ -32,7 +32,7 @@ void main() { final pubspecOverrides = p.join(packageBDir.path, 'pubspec_overrides.yaml'); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final logger = TestLogger(); final melos = Melos(config: config, logger: logger); await melos.bootstrap(); diff --git a/packages/melos/test/commands/exec_test.dart b/packages/melos/test/commands/exec_test.dart index f575e439..2e345cab 100644 --- a/packages/melos/test/commands/exec_test.dart +++ b/packages/melos/test/commands/exec_test.dart @@ -12,7 +12,7 @@ import '../utils.dart'; void main() { group('exec', () { test('supports package filters', () async { - final workspaceDir = createTemporaryWorkspaceDirectory(); + final workspaceDir = await createTemporaryWorkspace(); final aDir = await createProject( workspaceDir, @@ -32,7 +32,7 @@ void main() { ); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos( logger: logger, config: config, @@ -74,7 +74,7 @@ ${'-' * terminalWidth} group('order dependents', () { test('sorts execution order topologically', () async { - final workspaceDir = createTemporaryWorkspaceDirectory(); + final workspaceDir = await createTemporaryWorkspace(); await createProject( workspaceDir, @@ -98,7 +98,8 @@ ${'-' * terminalWidth} ); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos( logger: logger, config: config, @@ -133,7 +134,7 @@ ${'-' * terminalWidth} }); test('fails fast if dependencies fail', () async { - final workspaceDir = createTemporaryWorkspaceDirectory(); + final workspaceDir = await createTemporaryWorkspace(); await createProject( workspaceDir, @@ -157,7 +158,8 @@ ${'-' * terminalWidth} ); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos( logger: logger, config: config, @@ -192,7 +194,7 @@ ${'-' * terminalWidth} }); test('does not fail fast if dependencies is not run', () async { - final workspaceDir = createTemporaryWorkspaceDirectory(); + final workspaceDir = await createTemporaryWorkspace(); final aDir = await createProject( workspaceDir, @@ -218,7 +220,8 @@ ${'-' * terminalWidth} writeTextFile(p.join(cDir.path, 'log.txt'), ''); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos( logger: logger, config: config, @@ -254,7 +257,5 @@ ${'-' * terminalWidth} ); }); }); - - // TODO test that environment variables are injected }); } diff --git a/packages/melos/test/commands/list_test.dart b/packages/melos/test/commands/list_test.dart index 2850fd2f..dea83d3a 100644 --- a/packages/melos/test/commands/list_test.dart +++ b/packages/melos/test/commands/list_test.dart @@ -30,7 +30,8 @@ void main() { ], ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); await melos.list(); @@ -64,7 +65,8 @@ b ], ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); await melos.list(); @@ -93,7 +95,8 @@ c ], ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); await melos.list( @@ -128,7 +131,8 @@ c ], ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); await melos.list( @@ -161,7 +165,8 @@ long_name 0.0.0 packages/long_name PRIVATE ], ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); await melos.list( kind: ListOutputKind.parsable, @@ -197,7 +202,8 @@ packages/c .map((package) => p.join(workspaceDir.path, package.path)) .map(p.canonicalize); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); await melos.list( kind: ListOutputKind.parsable, @@ -233,7 +239,8 @@ ${packagePaths.join('\n')} ], ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); await melos.list( kind: ListOutputKind.graph, @@ -276,7 +283,8 @@ ${packagePaths.join('\n')} ], ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); await melos.list( kind: ListOutputKind.json, @@ -338,7 +346,8 @@ ${packagePaths.join('\n')} ], ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos(logger: logger, config: config); await melos.list( kind: ListOutputKind.gviz, diff --git a/packages/melos/test/commands/run_test.dart b/packages/melos/test/commands/run_test.dart index 1ef60d1e..9c36ce5d 100644 --- a/packages/melos/test/commands/run_test.dart +++ b/packages/melos/test/commands/run_test.dart @@ -16,7 +16,8 @@ void main() { test( 'supports passing package filter options to "melos exec" scripts', () async { - final workspaceDir = createTemporaryWorkspaceDirectory( + final workspaceDir = await createTemporaryWorkspace( + runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -47,7 +48,8 @@ void main() { ); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos( logger: logger, config: config, @@ -87,7 +89,7 @@ melos run test_script ); test('supports passing additional arguments to run scripts', () async { - final workspaceDir = createTemporaryWorkspaceDirectory( + final workspaceDir = await createTemporaryWorkspace( configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -104,7 +106,7 @@ melos run test_script ); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos( logger: logger, config: config, @@ -139,7 +141,8 @@ melos run test_script }); test('supports running "melos exec" script with "exec" options', () async { - final workspaceDir = createTemporaryWorkspaceDirectory( + final workspaceDir = await createTemporaryWorkspace( + runPubGet: true, configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -164,7 +167,7 @@ melos run test_script ); final logger = TestLogger(); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final melos = Melos( logger: logger, config: config, diff --git a/packages/melos/test/commands/script_test.dart b/packages/melos/test/commands/script_test.dart index 7c53d18c..db1ebe52 100644 --- a/packages/melos/test/commands/script_test.dart +++ b/packages/melos/test/commands/script_test.dart @@ -9,7 +9,7 @@ import '../utils.dart'; void main() { group('Script', () { test('fromConfig creates aliases for all scripts', () async { - final workspaceDir = createTemporaryWorkspaceDirectory( + final workspaceDir = await createTemporaryWorkspace( configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -24,7 +24,7 @@ void main() { ), ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final command = ScriptCommand.fromConfig(config); expect(command, isNotNull); expect( @@ -34,7 +34,7 @@ void main() { }); test('fromConfig excludes given commands', () async { - final workspaceDir = createTemporaryWorkspaceDirectory( + final workspaceDir = await createTemporaryWorkspace( configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -50,14 +50,14 @@ void main() { ), ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final command = ScriptCommand.fromConfig(config, exclude: ['run']); expect(command, isNotNull); expect([command!.name, ...command.aliases], isNot(contains('run'))); }); test('fromConfig does not create an empty command', () async { - final workspaceDir = createTemporaryWorkspaceDirectory( + final workspaceDir = await createTemporaryWorkspace( configBuilder: (path) => MelosWorkspaceConfig( path: path, name: 'test_package', @@ -70,7 +70,7 @@ void main() { ), ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final command = ScriptCommand.fromConfig(config, exclude: ['clean']); expect(command, isNull); }); diff --git a/packages/melos/test/package_filter_test.dart b/packages/melos/test/package_filter_test.dart index 180cdb62..ffa0cbd3 100644 --- a/packages/melos/test/package_filter_test.dart +++ b/packages/melos/test/package_filter_test.dart @@ -9,7 +9,7 @@ import 'utils.dart'; void main() { group('PackageFilters', () { test('dirExists', () async { - final workspaceDir = createTemporaryWorkspaceDirectory(); + final workspaceDir = await createTemporaryWorkspace(); final aDir = await createProject( workspaceDir, @@ -22,7 +22,7 @@ void main() { const PubSpec(name: 'b'), ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( config, logger: TestLogger().toMelosLogger(), @@ -45,7 +45,7 @@ void main() { }); test('fileExists', () async { - final workspaceDir = createTemporaryWorkspaceDirectory(); + final workspaceDir = await createTemporaryWorkspace(); final aDir = await createProject( workspaceDir, @@ -58,7 +58,7 @@ void main() { const PubSpec(name: 'b'), ); - final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( config, logger: TestLogger().toMelosLogger(), diff --git a/packages/melos/test/package_test.dart b/packages/melos/test/package_test.dart index 75520cc2..cc8d897d 100644 --- a/packages/melos/test/package_test.dart +++ b/packages/melos/test/package_test.dart @@ -64,7 +64,7 @@ void main() { reset(httpClientMock); IOOverrides.global = MockFs(); - final config = await MelosWorkspaceConfig.fromDirectory( + final config = await MelosWorkspaceConfig.fromWorkspaceRoot( createMockWorkspaceFs( packages: [ MockPackageFs( diff --git a/packages/melos/test/utils.dart b/packages/melos/test/utils.dart index 6bc83c2f..01b1a578 100644 --- a/packages/melos/test/utils.dart +++ b/packages/melos/test/utils.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:cli_util/cli_logging.dart'; import 'package:http/http.dart' as http; import 'package:melos/melos.dart'; +import 'package:melos/src/common/glob.dart'; import 'package:melos/src/common/io.dart'; import 'package:melos/src/common/platform.dart'; import 'package:melos/src/common/utils.dart'; @@ -11,7 +12,7 @@ import 'package:mockito/mockito.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec/pubspec.dart'; -import 'package:test/scaffolding.dart'; +import 'package:test/test.dart'; import 'package:yaml/yaml.dart'; class TestLogger extends StandardLogger { @@ -70,21 +71,72 @@ class TestLogger extends StandardLogger { } } -Directory createTemporaryWorkspaceDirectory({ - MelosWorkspaceConfig Function(String path)? configBuilder, -}) { - configBuilder ??= (path) => MelosWorkspaceConfig.fallback(path: path); +Future runPubGet(String workspacePath) async { + final result = await Process.run( + 'dart', + ['pub', 'get'], + runInShell: Platform.isWindows, + workingDirectory: workspacePath, + stdoutEncoding: utf8, + stderrEncoding: utf8, + ); + if (result.exitCode != 0) { + throw Exception( + 'Failed to run pub get:\n${result.stdout}\n${result.stderr}', + ); + } +} + +const _runPubGet = runPubGet; + +typedef TestWorkspaceConfigBuilder = MelosWorkspaceConfig Function(String path); + +MelosWorkspaceConfig _defaultWorkspaceConfigBuilder(String path) => + MelosWorkspaceConfig( + name: 'Melos', + packages: [ + createGlob('packages/**', currentDirectoryPath: path), + ], + path: currentPlatform.isWindows + ? p.windows.normalize(path).replaceAll(r'\', r'\\') + : path, + ); + +Future createTemporaryWorkspace({ + TestWorkspaceConfigBuilder configBuilder = _defaultWorkspaceConfigBuilder, + bool runPubGet = false, +}) async { + final tempDir = createTempDir(p.join(Directory.current.path, '.dart_tool')); + addTearDown(() => deleteEntry(tempDir)); + + final workspacePath = currentPlatform.isWindows + ? p.windows.normalize(tempDir).replaceAll(r'\', r'\\') + : tempDir; + + await createProject( + Directory(workspacePath), + PubSpec( + name: 'workspace', + devDependencies: { + 'melos': PathReference(Directory.current.path), + }, + ), + path: '.', + ); + + if (runPubGet) { + await _runPubGet(workspacePath); + } - final dir = createTempDir(p.join(Directory.current.path, '.dart_tool')); - addTearDown(() => deleteEntry(dir)); - final path = currentPlatform.isWindows - ? p.windows.normalize(dir).replaceAll(r'\', r'\\') - : dir; - final config = (configBuilder(path)..validatePhysicalWorkspace()).toJson(); + final config = + (configBuilder(workspacePath)..validatePhysicalWorkspace()).toJson(); - writeTextFile(p.join(path, 'melos.yaml'), prettyEncodeJson(config)); + writeTextFile( + p.join(workspacePath, 'melos.yaml'), + prettyEncodeJson(config), + ); - return Directory(dir); + return Directory(tempDir); } Future createProject( diff --git a/packages/melos/test/utils_test.dart b/packages/melos/test/utils_test.dart index 9e21c4f3..256cee25 100644 --- a/packages/melos/test/utils_test.dart +++ b/packages/melos/test/utils_test.dart @@ -77,7 +77,7 @@ void main() { group('startProcess', () { test('runs command chain in single shell', () async { - final workspaceDir = createTemporaryWorkspaceDirectory(); + final workspaceDir = await createTemporaryWorkspace(); 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 811b2179..903fd7af 100644 --- a/packages/melos/test/workspace_test.dart +++ b/packages/melos/test/workspace_test.dart @@ -14,7 +14,9 @@ * limitations under the License. */ +import 'dart:convert'; import 'dart:io'; +import 'dart:io' as io; import 'package:melos/melos.dart'; import 'package:melos/src/common/glob.dart'; @@ -34,7 +36,7 @@ import 'utils.dart'; void main() { group('Workspace', () { test('throws if multiple packages have the same name', () async { - final workspaceDir = createTemporaryWorkspaceDirectory(); + final workspaceDir = await createTemporaryWorkspace(); await createProject( workspaceDir, @@ -57,7 +59,7 @@ void main() { await expectLater( () async => MelosWorkspace.fromConfig( - await MelosWorkspaceConfig.fromDirectory(workspaceDir), + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir), logger: TestLogger().toMelosLogger(), ), throwsMelosConfigException( @@ -83,32 +85,32 @@ The packages that caused the problem are: ); }); - test( - 'can be accessed from anywhere within a workspace', - withMockFs(() async { - final mockWorkspaceRootDir = createMockWorkspaceFs( - packages: [ - MockPackageFs(name: 'a'), - MockPackageFs(name: 'b'), - ], - ); - - final aDir = Directory('${mockWorkspaceRootDir.path}/packages/a'); - final config = await MelosWorkspaceConfig.fromDirectory(aDir); - final workspace = await MelosWorkspace.fromConfig( - config, - logger: TestLogger().toMelosLogger(), - ); + test('can be accessed from anywhere within a workspace', () async { + final workspaceDir = await createTemporaryWorkspace( + runPubGet: true, + configBuilder: (path) => MelosWorkspaceConfig.fromYaml( + path: path, + const { + 'name': 'test', + 'packages': ['packages/*'], + }, + ), + ); + final projectDir = + await createProject(workspaceDir, const PubSpec(name: 'a')); + + final result = await Process.run( + 'melos', + ['list'], + runInShell: io.Platform.isWindows, + stdoutEncoding: utf8, + stderrEncoding: utf8, + workingDirectory: projectDir.path, + ); - expect( - workspace.filteredPackages.values, - unorderedEquals([ - packageNamed('a'), - packageNamed('b'), - ]), - ); - }), - ); + expect(result.exitCode, 0); + expect(result.stdout, 'a\n'); + }); test( 'does not include projects inside packages/whatever/.dart_tool when no melos.yaml is specified', @@ -123,9 +125,8 @@ The packages that caused the problem are: ], ); - final config = await MelosWorkspaceConfig.fromDirectory( - mockWorkspaceRootDir, - ); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(mockWorkspaceRootDir); final workspace = await MelosWorkspace.fromConfig( config, logger: TestLogger().toMelosLogger(), @@ -138,22 +139,24 @@ The packages that caused the problem are: }), ); - test('load workspace config when workspace contains broken symlink', - () async { - final workspaceDir = createTemporaryWorkspaceDirectory(); + test( + 'load workspace config when workspace contains broken symlink', + () async { + final workspaceDir = await createTemporaryWorkspace(); - final link = Link(p.join(workspaceDir.path, 'link')); - await link.create(p.join(workspaceDir.path, 'does-not-exist')); + final link = Link(p.join(workspaceDir.path, 'link')); + await link.create(p.join(workspaceDir.path, 'does-not-exist')); - await MelosWorkspace.fromConfig( - await MelosWorkspaceConfig.fromDirectory(workspaceDir), - logger: TestLogger().toMelosLogger(), - ); - }); + await MelosWorkspace.fromConfig( + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir), + logger: TestLogger().toMelosLogger(), + ); + }, + ); group('locate packages', () { test('in workspace root', () async { - final workspaceDir = createTemporaryWorkspaceDirectory( + final workspaceDir = await createTemporaryWorkspace( configBuilder: (path) => MelosWorkspaceConfig.fromYaml( const { 'name': 'test', @@ -170,7 +173,7 @@ The packages that caused the problem are: ); final workspace = await MelosWorkspace.fromConfig( - await MelosWorkspaceConfig.fromDirectory(workspaceDir), + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir), logger: TestLogger().toMelosLogger(), ); @@ -178,7 +181,7 @@ The packages that caused the problem are: }); test('in child directory', () async { - final workspaceDir = createTemporaryWorkspaceDirectory( + final workspaceDir = await createTemporaryWorkspace( configBuilder: (path) => MelosWorkspaceConfig.fromYaml( const { 'name': 'test', @@ -191,7 +194,7 @@ The packages that caused the problem are: await createProject(workspaceDir, const PubSpec(name: 'a')); final workspace = await MelosWorkspace.fromConfig( - await MelosWorkspaceConfig.fromDirectory(workspaceDir), + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir), logger: TestLogger().toMelosLogger(), ); @@ -237,9 +240,8 @@ The packages that caused the problem are: MockPackageFs(name: 'b'), ], ); - final config = await MelosWorkspaceConfig.fromDirectory( - workspaceDir, - ); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( config, packageFilters: PackageFilters( @@ -264,9 +266,8 @@ The packages that caused the problem are: MockPackageFs(name: 'b'), ], ); - final config = await MelosWorkspaceConfig.fromDirectory( - workspaceDir, - ); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( config, packageFilters: PackageFilters( @@ -298,9 +299,8 @@ The packages that caused the problem are: MockPackageFs(name: 'c'), ], ); - final config = await MelosWorkspaceConfig.fromDirectory( - workspaceDir, - ); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( config, packageFilters: PackageFilters( @@ -334,9 +334,8 @@ The packages that caused the problem are: MockPackageFs(name: 'd'), ], ); - final config = await MelosWorkspaceConfig.fromDirectory( - workspaceDir, - ); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( config, packageFilters: PackageFilters( @@ -367,9 +366,8 @@ The packages that caused the problem are: MockPackageFs(name: 'b'), ], ); - final config = await MelosWorkspaceConfig.fromDirectory( - workspaceDir, - ); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( config, packageFilters: PackageFilters( @@ -394,9 +392,8 @@ The packages that caused the problem are: MockPackageFs(name: 'b'), ], ); - final config = await MelosWorkspaceConfig.fromDirectory( - workspaceDir, - ); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( config, packageFilters: PackageFilters( @@ -426,9 +423,8 @@ The packages that caused the problem are: MockPackageFs(name: 'c'), ], ); - final config = await MelosWorkspaceConfig.fromDirectory( - workspaceDir, - ); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( config, packageFilters: PackageFilters( @@ -462,9 +458,8 @@ The packages that caused the problem are: MockPackageFs(name: 'd'), ], ); - final config = await MelosWorkspaceConfig.fromDirectory( - workspaceDir, - ); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); final workspace = await MelosWorkspace.fromConfig( config, packageFilters: PackageFilters( diff --git a/pubspec.yaml b/pubspec.yaml index 6a898edd..77efd4fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,4 @@ -name: 'melos_monorepo' -publish_to: none +name: melos_workspace environment: sdk: '>=2.18.0 <3.0.0' @@ -10,16 +9,6 @@ executables: dev_dependencies: melos: - path: ./packages/melos + path: packages/melos path: ^1.7.0 yaml: ^3.1.0 - -# These allow us to use melos on itself during development. -# If you make a new local package in this repo that the melos package -# will depend on, then make sure to add it here otherwise you'll face -# "'pubspec.yaml' has been modified since".. issues. -dependency_overrides: - conventional_commit: - path: ./packages/conventional_commit - melos: - path: ./packages/melos