Skip to content

Commit

Permalink
feat: format built in command (#657)
Browse files Browse the repository at this point in the history
This PR introduces a new built-in command called "format" that follows the same implementation logic used in the development of the "analyze" command, which was merged in a previous (PR). The purpose of this change is to eliminate the need to set up a separate script in order to format each package.

For example, instead of using the following YAML configuration:

scripts: format: description: Format Dart code. run: dart format . format:check: description: Check formatting of Dart code. run: dart format --output none --set-exit-if-changed .

The first script can now be replaced with a simpler command: melos format. Similarly, the second script can be replaced with melos format --output none --set-exit-if-changed.

Additionally, this new command supports all melos filtering options and concurrency.
  • Loading branch information
jessicatarra authored Mar 12, 2024
1 parent d733616 commit e0491f5
Show file tree
Hide file tree
Showing 9 changed files with 539 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
- name: Install Tools
run: ./.github/workflows/scripts/install-tools.sh
- name: Check formatting
run: melos format:check
run: melos format --output none --set-exit-if-changed

test_linux:
runs-on: ubuntu-latest
Expand Down
72 changes: 72 additions & 0 deletions docs/commands/format.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
title: Format Command
description: Learn more about the `format` command in Melos.
---

# Format Command

<Info>Supports all [Melos filtering](/filters) flags.</Info>

The format command is used to format the code in your Melos workspace according
to Dart's formatting standards.

```bash
melos format
```

<Info>
To learn more, visit the [Dart format](https://dart.dev/tools/dart-format)
documentation.
</Info>


## --set-exit-if-changed
Return exit code 1 if there are any formatting changes. This flag is
particularly useful in CI/CD pipelines to automatically detect and reject
commits that do not adhere to the formatting standards, ensuring code quality.

```bash
melos format --set-exit-if-changed
```

<Info>
By default, dart format overwrites the Dart files.
</Info>

## --output
This option is useful when you want to review formatting changes without
directly overwriting your files.

```bash
melos format --output
# or
melos format --o
```

Outputs the formatted code to the console.

```bash
melos format -o show
```

Outputs the formatted code as a JSON object

```bash
melos format -o json
```

Lists the files that would be formatted, without showing the formatted content
or making changes.

```bash
melos format -o none
```

## concurrency (-c)
Defines the max concurrency value of how many packages will execute the command
in at any one time. Defaults to `1`.

```bash
# Set a 5 concurrency
melos format -c 5
```
8 changes: 0 additions & 8 deletions melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,6 @@ ide:
intellij: true

scripts:
format:
description: Format Dart code.
run: dart format .

format:check:
description: Check formatting of Dart code.
run: dart format --output none --set-exit-if-changed .

test:
description: Run tests in a specific package.
run: dart test
Expand Down
1 change: 1 addition & 0 deletions packages/melos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ Available commands:
clean Clean this workspace and all packages. This deletes the temporary pub & ide files such
as ".packages" & ".flutter-plugins". Supports all package filtering options.
exec Execute an arbitrary command in each package. Supports all package filtering options.
format Idiomatically format Dart source code.
list List local packages in various output formats. Supports all package filtering options.
publish Publish any unpublished packages or package versions in your repository to pub.dev. Dry
run is on by default.
Expand Down
2 changes: 2 additions & 0 deletions packages/melos/lib/src/command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import 'command_runner/analyze.dart';
import 'command_runner/bootstrap.dart';
import 'command_runner/clean.dart';
import 'command_runner/exec.dart';
import 'command_runner/format.dart';
import 'command_runner/list.dart';
import 'command_runner/publish.dart';
import 'command_runner/run.dart';
Expand Down Expand Up @@ -79,6 +80,7 @@ class MelosCommandRunner extends CommandRunner<void> {
addCommand(PublishCommand(config));
addCommand(VersionCommand(config));
addCommand(AnalyzeCommand(config));
addCommand(FormatCommand(config));

// Keep this last to exclude all built-in commands listed above
final script = ScriptCommand.fromConfig(config, exclude: commands.keys);
Expand Down
63 changes: 63 additions & 0 deletions packages/melos/lib/src/command_runner/format.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2016-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import '../commands/runner.dart';
import 'base.dart';

class FormatCommand extends MelosCommand {
FormatCommand(super.config) {
setupPackageFilterParser();
argParser.addOption('concurrency', defaultsTo: '1', abbr: 'c');
argParser.addFlag(
'set-exit-if-changed',
negatable: false,
help: 'Return exit code 1 if there are any formatting changes.',
);
argParser.addOption(
'output',
help: 'Set where to write formatted output.\n'
'[json] Print code and selection as JSON.\n'
'[none] Discard output.\n'
'[show] Print code to terminal.\n'
'[write] Overwrite formatted files on disk.\n',
abbr: 'o',
);
}

@override
final String name = 'format';

@override
final String description = 'Idiomatically format Dart source code.';

@override
Future<void> run() async {
final setExitIfChanged = argResults?['set-exit-if-changed'] as bool;
final output = argResults?['output'] as String?;
final concurrency = int.parse(argResults!['concurrency'] as String);

final melos = Melos(logger: logger, config: config);

return melos.format(
global: global,
packageFilters: parsePackageFilters(config.path),
concurrency: concurrency,
setExitIfChanged: setExitIfChanged,
output: output,
);
}
}
130 changes: 130 additions & 0 deletions packages/melos/lib/src/commands/format.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
part of 'runner.dart';

mixin _FormatMixin on _Melos {
Future<void> format({
GlobalOptions? global,
PackageFilters? packageFilters,
int concurrency = 1,
bool setExitIfChanged = false,
String? output,
}) async {
final workspace =
await createWorkspace(global: global, packageFilters: packageFilters);
final packages = workspace.filteredPackages.values;

await _formatForAllPackages(
workspace,
packages,
concurrency: concurrency,
setExitIfChanged: setExitIfChanged,
output: output,
);
}

Future<void> _formatForAllPackages(
MelosWorkspace workspace,
Iterable<Package> packages, {
required int concurrency,
required bool setExitIfChanged,
String? output,
}) async {
final failures = <String, int?>{};
final pool = Pool(concurrency);
final formatArgs = [
'dart',
'format',
if (setExitIfChanged) '--set-exit-if-changed',
if (output != null) '--output $output',
'.',
];
final formatArgsString = formatArgs.join(' ');
final prefixLogs = concurrency != 1 && packages.length != 1;

logger.command('melos format', withDollarSign: true);

logger
.child(targetStyle(formatArgsString))
.child('$runningLabel (in ${packages.length} packages)')
.newLine();
if (prefixLogs) {
logger.horizontalLine();
}

final packageResults = Map.fromEntries(
packages.map((package) => MapEntry(package.name, Completer<int?>())),
);

await pool.forEach<Package, void>(packages, (package) async {
if (!prefixLogs) {
logger
..horizontalLine()
..log(AnsiStyles.bgBlack.bold.italic('${package.name}:'));
}

final packageExitCode = await _formatForPackage(
workspace,
package,
formatArgs,
prefixLogs: prefixLogs,
);

packageResults[package.name]?.complete(packageExitCode);

if (packageExitCode > 0) {
failures[package.name] = packageExitCode;
} else if (!prefixLogs) {
logger.log(
AnsiStyles.bgBlack.bold.italic('${package.name}: ') +
AnsiStyles.bgBlack(successLabel),
);
}
}).drain<void>();

logger
..horizontalLine()
..newLine()
..command('melos format', withDollarSign: true);

final resultLogger = logger.child(targetStyle(formatArgsString));

if (failures.isNotEmpty) {
final failuresLogger =
resultLogger.child('$failedLabel (in ${failures.length} packages)');
for (final packageName in failures.keys) {
failuresLogger.child(
'${errorPackageNameStyle(packageName)} '
'${failures[packageName] == null ? '(dependency failed)' : '('
'with exit code ${failures[packageName]})'}',
);
}
exitCode = 1;
} else {
resultLogger.child(successLabel);
}
}

Future<int> _formatForPackage(
MelosWorkspace workspace,
Package package,
List<String> formatArgs, {
bool prefixLogs = true,
}) async {
final packagePrefix = '[${AnsiStyles.blue.bold(package.name)}]: ';

final environment = {
EnvironmentVariableKey.melosRootPath: config.path,
if (workspace.sdkPath != null)
EnvironmentVariableKey.melosSdkPath: workspace.sdkPath!,
if (workspace.childProcessPath != null)
EnvironmentVariableKey.path: workspace.childProcessPath!,
};

return startCommand(
formatArgs,
logger: logger,
environment: environment,
workingDirectory: package.path,
prefix: prefixLogs ? packagePrefix : null,
);
}
}
4 changes: 3 additions & 1 deletion packages/melos/lib/src/commands/runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ part 'publish.dart';
part 'run.dart';
part 'version.dart';
part 'analyze.dart';
part 'format.dart';

enum CommandWithLifecycle {
bootstrap,
Expand All @@ -66,7 +67,8 @@ class Melos extends _Melos
_ExecMixin,
_VersionMixin,
_PublishMixin,
_AnalyzeMixin {
_AnalyzeMixin,
_FormatMixin {
Melos({
required this.config,
Logger? logger,
Expand Down
Loading

0 comments on commit e0491f5

Please sign in to comment.