Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add exec option to scripts #315

Merged
merged 1 commit into from
Jun 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion docs/configuration/scripts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,47 @@ A short description, shown when using `melos run` with no argument.

The command to execute.

## `scripts/*/exec`

Execute a script in multiple packages through `melos exec`.

This options must either contain the command to execute in multiple packages or the options for the
`melos exec` command.

When using the default options for `melos exec`, it's easiest to specify the command in the `exec`
option:

```yaml
scripts:
hello:
exec: echo 'Hello $(dirname $PWD)'
```

If you need to provide options for the `exec` command, specify them in the `exec` option and
specify the command in the `run` option:

```yaml
scripts:
hello:
run: echo 'Hello $(dirname $PWD)'
exec:
concurrency: 1
```

See the [`select-package`](#scriptsselect-package) option for filtering the packages to execute
the command in.

## `scripts/*/exec/concurrency`

Defines the max concurrency value of how many packages will execute the command in at any one time.
Defaults to `5`.

## `scripts/*/exec/failFast`

Whether `exec` should fail fast and not execute the script in further packages if the script fails
in an individual package.
Defaults to `false`.

## `scripts/*/env`

A map of environment variables that will be passed to the executed command.
Expand All @@ -64,7 +105,7 @@ The `hello_flutter` script below is only executed in Flutter packages:
```yaml
scripts:
hello_flutter:
run: melos exec -- "echo 'Hello $(dirname $PWD)'"
exec: echo 'Hello $(dirname $PWD)'
select-package:
flutter: true
```
Expand Down
3 changes: 2 additions & 1 deletion docs/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ packages:
- packages/**

scripts:
analyze: melos exec -- "dart analyze ."
analyze:
exec: dart analyze .
```

Then execute the command by running `melos run analyze`.
Expand Down
4 changes: 2 additions & 2 deletions packages/melos/lib/src/commands/run.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ mixin _RunMixin on _Melos {
logger?.stdout('');
logger?.stdout(AnsiStyles.yellow.bold('melos run ${script.name}'));
logger?.stdout(
' └> ${AnsiStyles.cyan.bold(script.run.replaceAll('\n', ''))}',
' └> ${AnsiStyles.cyan.bold(script.effectiveRun.replaceAll('\n', ''))}',
);

if (exitCode != 0) {
Expand Down Expand Up @@ -146,7 +146,7 @@ mixin _RunMixin on _Melos {
environment[envKeyMelosPackages] = packagesEnv;
}

final scriptSource = script.run;
final scriptSource = script.effectiveRun;
final scriptParts = scriptSource.split(' ');

logger?.stdout(AnsiStyles.yellow.bold('melos run ${script.name}'));
Expand Down
166 changes: 143 additions & 23 deletions packages/melos/lib/src/scripts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,48 @@ class Scripts extends MapView<String, Script> {
}
}

@immutable
class ExecOptions {
ExecOptions({
this.concurrency,
this.failFast,
});

final int? concurrency;
final bool? failFast;

Map<String, Object?> toJson() => {
if (concurrency != null) 'concurrency': concurrency,
if (failFast != null) 'failFast': failFast,
};

@override
bool operator ==(Object other) =>
other is ExecOptions &&
runtimeType == other.runtimeType &&
concurrency == other.concurrency &&
failFast == other.failFast;

@override
int get hashCode =>
runtimeType.hashCode ^ concurrency.hashCode ^ failFast.hashCode;

@override
String toString() => '''
ExecOptions(
concurrency: $concurrency,
failFast: $failFast,
)''';
}

class Script {
Script({
required this.name,
required this.run,
this.description,
this.env = const {},
this.filter,
this.exec,
});

factory Script.fromYaml(
Expand All @@ -110,15 +145,28 @@ class Script {
String? description;
var env = <String, String>{};
PackageFilter? packageFilter;
ExecOptions? exec;

if (yaml is String) {
run = yaml;
} else if (yaml is Map<Object?, Object?>) {
run = assertKeyIsA<String>(
key: 'run',
map: yaml,
path: scriptPath,
);
final execYaml = yaml['exec'];
if (execYaml is String) {
if (yaml['run'] is String) {
throw MelosConfigException(
'The script $name specifies a command in both "run" and "exec". '
'Remove one of them.',
);
}
run = execYaml;
} else {
run = assertKeyIsA<String>(
key: 'run',
map: yaml,
path: scriptPath,
);
}

description = assertKeyIsA<String?>(
key: 'description',
map: yaml,
Expand Down Expand Up @@ -153,6 +201,20 @@ class Script {
scriptName: name,
workspacePath: workspacePath,
);

if (execYaml is String) {
exec = ExecOptions();
} else {
final execMap = assertKeyIsA<Map<Object?, Object?>?>(
key: 'exec',
map: yaml,
path: scriptPath,
);

exec = execMap == null
? null
: execOptionsFromYaml(execMap, scriptName: name);
}
} else {
throw MelosConfigException('Unsupported value for script $name');
}
Expand All @@ -163,6 +225,7 @@ class Script {
description: description,
env: env,
filter: packageFilter,
exec: exec,
);
}

Expand All @@ -175,54 +238,54 @@ class Script {
// necessary for the glob workaround
required String workspacePath,
}) {
final packagePath = 'scripts/$scriptName/select-package';
final filtersPath = 'scripts/$scriptName/select-package';

final scope = assertListOrString(
key: filterOptionScope,
map: yaml,
path: packagePath,
path: filtersPath,
);
final ignore = assertListOrString(
key: filterOptionIgnore,
map: yaml,
path: packagePath,
path: filtersPath,
);
final dirExists = assertListOrString(
key: filterOptionDirExists,
map: yaml,
path: packagePath,
path: filtersPath,
);
final fileExists = assertListOrString(
key: filterOptionFileExists,
map: yaml,
path: packagePath,
path: filtersPath,
);
final dependsOn = assertListOrString(
key: filterOptionDependsOn,
map: yaml,
path: packagePath,
path: filtersPath,
);
final noDependsOn = assertListOrString(
key: filterOptionNoDependsOn,
map: yaml,
path: packagePath,
path: filtersPath,
);

final updatedSince = assertIsA<String?>(
value: yaml[filterOptionSince],
key: filterOptionSince,
path: packagePath,
path: filtersPath,
);

final excludePrivatePackagesTmp = assertIsA<bool?>(
value: yaml[filterOptionNoPrivate],
key: filterOptionNoPrivate,
path: packagePath,
path: filtersPath,
);
final includePrivatePackagesTmp = assertIsA<bool?>(
value: yaml[filterOptionPrivate],
key: filterOptionNoPrivate,
path: packagePath,
path: filtersPath,
);
if (includePrivatePackagesTmp != null &&
excludePrivatePackagesTmp != null) {
Expand All @@ -241,17 +304,17 @@ class Script {
final published = assertIsA<bool?>(
value: yaml[filterOptionPublished],
key: filterOptionPublished,
path: packagePath,
path: filtersPath,
);
final nullSafe = assertIsA<bool?>(
value: yaml[filterOptionNullsafety],
key: filterOptionNullsafety,
path: packagePath,
path: filtersPath,
);
final flutter = assertIsA<bool?>(
value: yaml[filterOptionFlutter],
key: filterOptionFlutter,
path: packagePath,
path: filtersPath,
);

return PackageFilter(
Expand All @@ -273,29 +336,83 @@ class Script {
);
}

/// A unique identifier for the script
@visibleForTesting
static ExecOptions execOptionsFromYaml(
Map<Object?, Object?> yaml, {
required String scriptName,
}) {
final execPath = 'scripts/$scriptName/exec';

final concurrency = assertKeyIsA<int?>(
key: 'concurrency',
map: yaml,
path: execPath,
);

final failFast = assertKeyIsA<bool?>(
key: 'failFast',
map: yaml,
path: execPath,
);

return ExecOptions(
concurrency: concurrency,
failFast: failFast,
);
}

/// A unique identifier for the script.
final String name;

/// The command to execute
/// The command specified by the user.
final String run;

/// The command to run when executing this script.
late final effectiveRun = _buildEffectiveCommand();

/// A short description, shown when using `melos run` with no argument.
final String? description;

/// Environment variables that will be passed to[run].
/// Environment variables that will be passed to [run].
final Map<String, String> env;

/// If the [run] command is a melos command, allows filtering packages
/// that will execute the command.
final PackageFilter? filter;

/// The options for `melos exec`, if [run] should be executed in multiple
/// packages.
final ExecOptions? exec;

String _buildEffectiveCommand() {
String _quoteScript(String script) => '"${script.replaceAll('"', r'\"')}"';

final exec = this.exec;
if (exec != null) {
final parts = ['melos', 'exec'];

if (exec.concurrency != null) {
parts.addAll(['--concurrency', '${exec.concurrency}']);
}
if (exec.failFast != null) {
parts.addAll(['--fail-fast', '${exec.failFast}']);
}

parts.addAll(['--', _quoteScript(run)]);

return parts.join(' ');
}
return run;
}

Map<Object?, Object?> toJson() {
return {
'name': name,
'run': run,
if (description != null) 'description': description,
if (env.isNotEmpty) 'env': env,
if (filter != null) 'select-package': filter!.toJson(),
if (exec != null) 'exec': exec!.toJson(),
};
}

Expand All @@ -307,7 +424,8 @@ class Script {
other.run == run &&
other.description == description &&
const DeepCollectionEquality().equals(other.env, env) &&
other.filter == filter;
other.filter == filter &&
other.exec == exec;

@override
int get hashCode =>
Expand All @@ -316,7 +434,8 @@ class Script {
run.hashCode ^
description.hashCode ^
const DeepCollectionEquality().hash(env) ^
filter.hashCode;
filter.hashCode ^
exec.hashCode;

@override
String toString() {
Expand All @@ -327,6 +446,7 @@ Script(
description: $description,
env: $env,
packageFilter: ${filter.toString().indent(' ')},
exec: ${exec.toString().indent(' ')},
)''';
}
}
Loading