Skip to content

Commit

Permalink
feat: support syncing common dependency versions (#526)
Browse files Browse the repository at this point in the history
  • Loading branch information
lohnn committed Oct 22, 2023
1 parent 2f19770 commit 39e5e49
Show file tree
Hide file tree
Showing 6 changed files with 444 additions and 1 deletion.
37 changes: 37 additions & 0 deletions docs/commands/bootstrap.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,43 @@ example:
melos bootstrap --diff="main"
```

## Bootstrap flags

Melos bootstrap command supports a few different flags that can be defined in
your `melos.yaml`.


### Shared dependencies

If you want to share dependency versions between your packages in your Melos
project, just add the dependencies you wish to share between the packages to
your bootstrap config in your `melos.yaml` file.

If a dependency from `environment`, `dependencies` or `dev_dependencies` in
your `common_packages.yaml` exists in a package, the dependency version in this
package will be updated to match the version defined in your
bootstrap config every time `melos bootstrap` is run.

```yaml
# melos.yaml
# ...
command:
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: ">=3.0.0 <4.0.0"
bootstrap:
dependencies:
collection: ^1.18.0
integral_isolates: any
uni_links2:
uni_links_macos:
git: https://github.com/SamJakob/uni_links_macos.git

dev_dependencies:
build_runner: ^2.3.3
# ...
```

## Adding a post bootstrap lifecycle script

Melos supports various command [lifecycle hooks](/configuration/scripts#hooks)
Expand Down
32 changes: 32 additions & 0 deletions melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,38 @@ ignore:
- packages/melos_flutter_deps_check

command:
bootstrap:
environment:
sdk: '>=2.18.0 <3.0.0'
dependencies:
ansi_styles: ^0.3.1
args: ^2.0.0
cli_launcher: ^0.3.0
cli_util: '>=0.3.0 <0.5.0'
collection: ^1.14.12
conventional_commit: ^0.6.0+1
file: ^6.1.0
glob: ^2.1.0
graphs: ^2.1.0
http: ">=0.13.1 <2.0.0"
meta: ^1.1.8
mustache_template: ^2.0.0
path: ^1.7.0
platform: ^3.1.0
pool: ^1.4.0
prompts: ^2.0.0
pub_semver: ^2.0.0
pub_updater: ^0.3.0
pubspec: ^2.1.0
string_scanner: ^1.0.5
yaml: ^3.1.0
yaml_edit: ^2.0.2
dev_dependencies:
collection: ^1.15.0
mockito: ^5.1.0
test: ^1.17.5
path: ^1.7.0
yaml: ^3.1.0
version:
# Generate commit links in package changelogs.
linkToCommits: true
Expand Down
140 changes: 139 additions & 1 deletion packages/melos/lib/src/commands/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ mixin _BootstrapMixin on _CleanMixin {
workspace,
_CommandWithLifecycle.bootstrap,
() async {
final bootstrapCommandConfig = workspace.config.commands.bootstrap;
final pubCommandForLogging = [
...pubCommandExecArgs(
useFlutter: workspace.isFlutterWorkspace,
workspace: workspace,
),
'get',
if (workspace.config.commands.bootstrap.runPubGetOffline) '--offline',
if (bootstrapCommandConfig.runPubGetOffline) '--offline',
].join(' ');

logger
Expand All @@ -35,6 +36,20 @@ mixin _BootstrapMixin on _CleanMixin {
}

try {
if (bootstrapCommandConfig.environment != null ||
bootstrapCommandConfig.dependencies != null ||
bootstrapCommandConfig.devDependencies != null) {
final filteredPackages = workspace.filteredPackages.values;
await Stream.fromIterable(filteredPackages).parallel((package) {
return _setSharedDependenciesForPackage(
package,
environment: bootstrapCommandConfig.environment,
dependencies: bootstrapCommandConfig.dependencies,
devDependencies: bootstrapCommandConfig.devDependencies,
);
}).drain<void>();
}

await _linkPackagesWithPubspecOverrides(workspace);
} on BootstrapException catch (exception) {
_logBootstrapException(exception, workspace);
Expand Down Expand Up @@ -199,6 +214,129 @@ mixin _BootstrapMixin on _CleanMixin {
}
}

Future<void> _setSharedDependenciesForPackage(
Package package, {
required Environment? environment,
required Map<String, DependencyReference>? dependencies,
required Map<String, DependencyReference>? devDependencies,
}) async {
final packagePubspecFile = utils.pubspecPathForDirectory(package.path);
final packagePubspecContents = await readTextFileAsync(packagePubspecFile);
final pubspecEditor = YamlEditor(packagePubspecContents);

final updatedEnvironment = _updateEnvironment(
pubspecEditor: pubspecEditor,
workspaceEnvironment: environment,
packageEnvironment: package.pubSpec.environment,
);

final updatedDependenciesCount = _updateDependencies(
pubspecEditor: pubspecEditor,
workspaceDependencies: dependencies,
packageDependencies: package.pubSpec.dependencies,
pubspecKey: 'dependencies',
);

final updatedDevDependenciesCount = _updateDependencies(
pubspecEditor: pubspecEditor,
workspaceDependencies: devDependencies,
packageDependencies: package.pubSpec.devDependencies,
pubspecKey: 'dev_dependencies',
);

if (pubspecEditor.edits.isNotEmpty) {
await writeTextFileAsync(
packagePubspecFile,
pubspecEditor.toString(),
);

final message = <String>[
if (updatedEnvironment) 'Updated environment',
if (updatedDependenciesCount > 0)
'Updated $updatedDependenciesCount dependencies',
if (updatedDevDependenciesCount > 0)
'Updated $updatedDevDependenciesCount dev_dependencies',
];
if (message.isNotEmpty) {
logger
.child(packageNameStyle(package.name), prefix: '')
.child(message.join('\n'));
}
}
}

bool _updateEnvironment({
required YamlEditor pubspecEditor,
required Environment? workspaceEnvironment,
required Environment? packageEnvironment,
}) {
if (workspaceEnvironment == null || packageEnvironment == null) {
return false;
}

var didUpdate = false;

if (workspaceEnvironment.sdkConstraint !=
packageEnvironment.sdkConstraint) {
pubspecEditor.update(
['environment', 'sdk'],
wrapAsYamlNode(
workspaceEnvironment.sdkConstraint.toString(),
collectionStyle: CollectionStyle.BLOCK,
),
);
didUpdate = true;
}

final workspaceUnParsedYaml = workspaceEnvironment.unParsedYaml;
final packageUnParsedYaml = packageEnvironment.unParsedYaml;
if (workspaceUnParsedYaml != null && packageUnParsedYaml != null) {
for (final entry in workspaceUnParsedYaml.entries) {
if (!packageUnParsedYaml.containsKey(entry.key)) continue;
if (packageUnParsedYaml[entry.key] == entry.value) continue;

pubspecEditor.update(
['environment', entry.key],
wrapAsYamlNode(
entry.value.toString(),
collectionStyle: CollectionStyle.BLOCK,
),
);
didUpdate = true;
}
}

return didUpdate;
}

int _updateDependencies({
required YamlEditor pubspecEditor,
required Map<String, DependencyReference>? workspaceDependencies,
required Map<String, DependencyReference> packageDependencies,
required String pubspecKey,
}) {
if (workspaceDependencies == null) return 0;
// Filter out the packages that do not exist in package and only the
// dependencies that have a different version specified in the workspace.
final dependenciesToUpdate = workspaceDependencies.entries.where((entry) {
if (!packageDependencies.containsKey(entry.key)) return false;
if (packageDependencies[entry.key] == entry.value) return false;
return true;
});

for (final entry in dependenciesToUpdate) {
pubspecEditor.update(
[pubspecKey, entry.key],
wrapAsYamlNode(
entry.value.toJson(),
collectionStyle: CollectionStyle.BLOCK,
),
);
}

return dependenciesToUpdate.length;
}

void _logBootstrapSuccess(Package package) {
logger.child(packageNameStyle(package.name), prefix: '$checkLabel ').child(
packagePathStyle(printablePath(package.pathRelativeToWorkspace)),
Expand Down
70 changes: 70 additions & 0 deletions packages/melos/lib/src/workspace_configs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'package:ansi_styles/ansi_styles.dart';
import 'package:collection/collection.dart';
import 'package:glob/glob.dart';
import 'package:meta/meta.dart';
import 'package:pubspec/pubspec.dart';
import 'package:yaml/yaml.dart';

import '../melos.dart';
Expand Down Expand Up @@ -357,6 +358,9 @@ class BootstrapCommandConfigs {
const BootstrapCommandConfigs({
this.runPubGetInParallel = true,
this.runPubGetOffline = false,
this.environment,
this.dependencies,
this.devDependencies,
this.dependencyOverridePaths = const [],
this.hooks = LifecycleHooks.empty,
});
Expand All @@ -379,6 +383,31 @@ class BootstrapCommandConfigs {
) ??
false;

final environment = assertKeyIsA<Map<Object?, Object?>?>(
key: 'environment',
map: yaml,
).let(Environment.fromJson);

final dependencies = assertKeyIsA<Map<Object?, Object?>?>(
key: 'dependencies',
map: yaml,
)?.map(
(key, value) => MapEntry(
key.toString(),
DependencyReference.fromJson(value),
),
);

final devDependencies = assertKeyIsA<Map<Object?, Object?>?>(
key: 'dev_dependencies',
map: yaml,
)?.map(
(key, value) => MapEntry(
key.toString(),
DependencyReference.fromJson(value),
),
);

final dependencyOverridePaths = assertListIsA<String>(
key: 'dependencyOverridePaths',
map: yaml,
Expand All @@ -402,6 +431,9 @@ class BootstrapCommandConfigs {
return BootstrapCommandConfigs(
runPubGetInParallel: runPubGetInParallel,
runPubGetOffline: runPubGetOffline,
environment: environment,
dependencies: dependencies,
devDependencies: devDependencies,
dependencyOverridePaths: dependencyOverridePaths
.map(
(override) =>
Expand All @@ -425,6 +457,15 @@ class BootstrapCommandConfigs {
/// The default is `false`.
final bool runPubGetOffline;

/// Environment configuration to be synced between all packages.
final Environment? environment;

/// Dependencies to be synced between all packages.
final Map<String, DependencyReference>? dependencies;

/// Dev dependencies to be synced between all packages.
final Map<String, DependencyReference>? devDependencies;

/// A list of [Glob]s for paths that contain packages to be used as dependency
/// overrides for all packages managed in the Melos workspace.
final List<Glob> dependencyOverridePaths;
Expand All @@ -436,6 +477,15 @@ class BootstrapCommandConfigs {
return {
'runPubGetInParallel': runPubGetInParallel,
'runPubGetOffline': runPubGetOffline,
if (environment != null) 'environment': environment!.toJson(),
if (dependencies != null)
'dependencies': dependencies!.map(
(key, value) => MapEntry(key, value.toJson()),
),
if (devDependencies != null)
'dev_dependencies': devDependencies!.map(
(key, value) => MapEntry(key, value.toJson()),
),
if (dependencyOverridePaths.isNotEmpty)
'dependencyOverridePaths':
dependencyOverridePaths.map((path) => path.toString()).toList(),
Expand All @@ -449,6 +499,15 @@ class BootstrapCommandConfigs {
runtimeType == other.runtimeType &&
other.runPubGetInParallel == runPubGetInParallel &&
other.runPubGetOffline == runPubGetOffline &&
// Extracting equality from environment here as it does not implement ==
other.environment?.sdkConstraint == environment?.sdkConstraint &&
const DeepCollectionEquality().equals(
other.environment?.unParsedYaml,
environment?.unParsedYaml,
) &&
const DeepCollectionEquality().equals(other.dependencies, dependencies) &&
const DeepCollectionEquality()
.equals(other.devDependencies, devDependencies) &&
const DeepCollectionEquality(_GlobEquality())
.equals(other.dependencyOverridePaths, dependencyOverridePaths) &&
other.hooks == hooks;
Expand All @@ -458,6 +517,14 @@ class BootstrapCommandConfigs {
runtimeType.hashCode ^
runPubGetInParallel.hashCode ^
runPubGetOffline.hashCode ^
// Extracting hashCode from environment here as it does not implement
// hashCode
(environment?.sdkConstraint).hashCode ^
const DeepCollectionEquality().hash(
environment?.unParsedYaml,
) ^
const DeepCollectionEquality().hash(dependencies) ^
const DeepCollectionEquality().hash(devDependencies) ^
const DeepCollectionEquality(_GlobEquality())
.hash(dependencyOverridePaths) ^
hooks.hashCode;
Expand All @@ -468,6 +535,9 @@ class BootstrapCommandConfigs {
BootstrapCommandConfigs(
runPubGetInParallel: $runPubGetInParallel,
runPubGetOffline: $runPubGetOffline,
environment: $environment,
dependencies: $dependencies,
devDependencies: $devDependencies,
dependencyOverridePaths: $dependencyOverridePaths,
hooks: $hooks,
)''';
Expand Down
Loading

0 comments on commit 39e5e49

Please sign in to comment.