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
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
@@ -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.
@@ -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
```
3 changes: 2 additions & 1 deletion docs/getting-started.mdx
Original file line number Diff line number Diff line change
@@ -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`.
4 changes: 2 additions & 2 deletions packages/melos/lib/src/commands/run.dart
Original file line number Diff line number Diff line change
@@ -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) {
@@ -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}'));
166 changes: 143 additions & 23 deletions packages/melos/lib/src/scripts.dart
Original file line number Diff line number Diff line change
@@ -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(
@@ -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,
@@ -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');
}
@@ -163,6 +225,7 @@ class Script {
description: description,
env: env,
filter: packageFilter,
exec: exec,
);
}

@@ -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) {
@@ -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(
@@ -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(),
};
}

@@ -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 =>
@@ -316,7 +434,8 @@ class Script {
run.hashCode ^
description.hashCode ^
const DeepCollectionEquality().hash(env) ^
filter.hashCode;
filter.hashCode ^
exec.hashCode;

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