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 support for melos_overrides.yaml + command/bootstrap/dependencyOverridePaths #410

Merged
merged 11 commits into from
Dec 20, 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
20 changes: 20 additions & 0 deletions docs/configuration/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ description: Configure Melos using the `melos.yaml` file.
Every project requires a `melos.yaml` project in the root. The below outlines
all the configurable fields and their purpose.

Additionally, projects may include a `melos_overrides.yaml` file to override any
`melos.yaml` field. This is useful for making untracked customizations to a
project.

## `name`

> required
Expand Down Expand Up @@ -155,6 +159,22 @@ Configuration relating to specific Melos commands such as versioning.

Configuration for the `bootstrap` command.

### `command/bootstrap/dependencyOverridePaths`

> optional

A list of paths to local packages realtive to the workspace directory that
should be added to each workspace package's dependency overrides. Each entry can
be a specific path or a [glob] pattern.

**Tip:** External local packages can be referenced using paths relative to the
workspace root.

```yaml
dependencyOverridePaths:
- '../external_project/packages/**'
```

### `command/bootstrap/runPubGetInParallel`

Whether to run `pub get` in parallel during bootstrapping.
Expand Down
20 changes: 16 additions & 4 deletions packages/melos/lib/src/commands/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,30 @@ mixin _BootstrapMixin on _CleanMixin {
) async {
final allTransitiveDependencies =
package.allTransitiveDependenciesInWorkspace;
final melosDependencyOverrides = {...package.pubSpec.dependencyOverrides};
final melosDependencyOverrides = <String, DependencyReference>{};

// Traversing all packages so that transitive dependencies for the
// bootstraped packages are setup properly.
// bootstrapped packages are setup properly.
for (final otherPackage in workspace.allPackages.values) {
if (allTransitiveDependencies.containsKey(otherPackage.name) &&
!melosDependencyOverrides.containsKey(otherPackage.name)) {
if (allTransitiveDependencies.containsKey(otherPackage.name)) {
melosDependencyOverrides[otherPackage.name] =
PathReference(utils.relativePath(otherPackage.path, package.path));
}
}

// Add custom workspace overrides.
for (final dependencyOverride
in workspace.dependencyOverridePackages.values) {
melosDependencyOverrides[dependencyOverride.name] = PathReference(
utils.relativePath(dependencyOverride.path, package.path),
);
}

// Add existing dependency overrides from pubspec.yaml last, overwriting
// overrides that would be made by Melos, to provide granular control at a
// package level.
melosDependencyOverrides.addAll(package.pubSpec.dependencyOverrides);

// Load current pubspec_overrides.yaml.
final pubspecOverridesFile =
utils.pubspecOverridesPathForDirectory(package.path);
Expand Down
50 changes: 50 additions & 0 deletions packages/melos/lib/src/common/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ String melosYamlPathForDirectory(String directory) =>
String melosStatePathForDirectory(String directory) =>
p.join(directory, '.melos');

String melosOverridesYamlPathForDirectory(String directory) =>
p.join(directory, 'melos_overrides.yaml');

String pubspecPathForDirectory(String directory) =>
p.join(directory, 'pubspec.yaml');

Expand Down Expand Up @@ -347,6 +350,53 @@ String listAsPaddedTable(List<List<String>> table, {int paddingSize = 1}) {
return output.join('\n');
}

extension YamlUtils on YamlNode {
/// Converts a YAML node to a regular mutable Dart object.
Object? toPlainObject() {
final node = this;
if (node is YamlScalar) {
return node.value;
}
if (node is YamlMap) {
return {
for (final entry in node.nodes.entries)
(entry.key as YamlNode).toPlainObject(): entry.value.toPlainObject(),
};
}
if (node is YamlList) {
return node.nodes.map((node) => node.toPlainObject()).toList();
}
throw FormatException(
'Unsupported YAML node type encountered: ${node.runtimeType}',
this,
);
}
}

/// Merges two maps together, overriding any values in [base] with those
/// with the same key in [overlay].
void mergeMap(Map<Object?, Object?> base, Map<Object?, Object?> overlay) {
for (final entry in overlay.entries) {
final overlayValue = entry.value;
final baseValue = base[entry.key];
if (overlayValue is Map<Object?, Object?>) {
if (baseValue is Map<Object?, Object?>) {
mergeMap(baseValue, overlayValue);
} else {
base[entry.key] = overlayValue;
}
} else if (overlayValue is List<Object?>) {
if (baseValue is List<Object?>) {
baseValue.addAll(overlayValue);
} else {
base[entry.key] = overlayValue;
}
} else {
base[entry.key] = overlayValue;
}
}
}

/// Generate a link for display in a terminal.
///
/// Similar to `<a href="$url">$text</a>` in HTML.
Expand Down
20 changes: 18 additions & 2 deletions packages/melos/lib/src/workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class MelosWorkspace {
required this.config,
required this.allPackages,
required this.filteredPackages,
required this.dependencyOverridePackages,
required this.sdkPath,
required this.logger,
});
Expand All @@ -65,6 +66,12 @@ class MelosWorkspace {
ignore: workspaceConfig.ignore,
logger: logger,
);
final dependencyOverridePackages = await PackageMap.resolvePackages(
workspacePath: workspaceConfig.path,
packages: workspaceConfig.commands.bootstrap.dependencyOverridePaths,
ignore: const [],
logger: logger,
);

final filteredPackages = await allPackages.applyFilter(filter);

Expand All @@ -75,6 +82,7 @@ class MelosWorkspace {
allPackages: allPackages,
logger: logger,
filteredPackages: filteredPackages,
dependencyOverridePackages: dependencyOverridePackages,
sdkPath: resolveSdkPath(
configSdkPath: workspaceConfig.sdkPath,
envSdkPath: currentPlatform.environment[utils.envKeyMelosSdkPath],
Expand All @@ -96,13 +104,21 @@ class MelosWorkspace {
/// Configuration as defined in the "melos.yaml" file if it exists.
final MelosWorkspaceConfig config;

/// All packages according to [MelosWorkspaceConfig].
/// All packages managed in this Melos workspace.
///
/// Packages filtered by [MelosWorkspaceConfig.ignore] are not included.
/// Packages specified in [MelosWorkspaceConfig.packages] are included,
/// except for those specified in [MelosWorkspaceConfig.ignore].
final PackageMap allPackages;

/// The packages in this Melos workspace after applying filters.
///
/// Filters are typically specified on the command line.
final PackageMap filteredPackages;

/// The packages specified in
/// [BootstrapCommandConfigs.dependencyOverridePaths].
final PackageMap dependencyOverridePackages;

late final IdeWorkspace ide = IdeWorkspace._(this);

/// Returns true if this workspace contains ANY Flutter package.
Expand Down
79 changes: 70 additions & 9 deletions packages/melos/lib/src/workspace_configs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,10 @@ class CommandConfigs {
);

return CommandConfigs(
bootstrap: BootstrapCommandConfigs.fromYaml(bootstrapMap ?? const {}),
bootstrap: BootstrapCommandConfigs.fromYaml(
bootstrapMap ?? const {},
workspacePath: workspacePath,
),
version: VersionCommandConfigs.fromYaml(
versionMap ?? const {},
workspacePath: workspacePath,
Expand Down Expand Up @@ -216,9 +219,13 @@ class BootstrapCommandConfigs {
const BootstrapCommandConfigs({
this.runPubGetInParallel = true,
this.runPubGetOffline = false,
this.dependencyOverridePaths = const [],
});

factory BootstrapCommandConfigs.fromYaml(Map<Object?, Object?> yaml) {
factory BootstrapCommandConfigs.fromYaml(
Map<Object?, Object?> yaml, {
required String workspacePath,
}) {
final runPubGetInParallel = assertKeyIsA<bool?>(
key: 'runPubGetInParallel',
map: yaml,
Expand All @@ -233,9 +240,26 @@ class BootstrapCommandConfigs {
) ??
false;

final dependencyOverridePaths = assertListIsA<String>(
key: 'dependencyOverridePaths',
map: yaml,
isRequired: false,
assertItemIsA: (index, value) => assertIsA<String>(
value: value,
index: index,
path: 'dependencyOverridePaths',
),
);

return BootstrapCommandConfigs(
runPubGetInParallel: runPubGetInParallel,
runPubGetOffline: runPubGetOffline,
dependencyOverridePaths: dependencyOverridePaths
.map(
(override) =>
createGlob(override, currentDirectoryPath: workspacePath),
)
.toList(),
);
}

Expand All @@ -252,10 +276,17 @@ class BootstrapCommandConfigs {
/// The default is `false`.
final bool runPubGetOffline;

/// 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;

Map<String, Object?> toJson() {
return {
'runPubGetInParallel': runPubGetInParallel,
'runPubGetOffline': runPubGetOffline,
if (dependencyOverridePaths.isNotEmpty)
'dependencyOverridePaths':
dependencyOverridePaths.map((path) => path.toString()).toList(),
};
}

Expand All @@ -264,20 +295,25 @@ class BootstrapCommandConfigs {
other is BootstrapCommandConfigs &&
runtimeType == other.runtimeType &&
other.runPubGetInParallel == runPubGetInParallel &&
other.runPubGetOffline == runPubGetOffline;
other.runPubGetOffline == runPubGetOffline &&
const DeepCollectionEquality(_GlobEquality())
.equals(other.dependencyOverridePaths, dependencyOverridePaths);

@override
int get hashCode =>
runtimeType.hashCode ^
runPubGetInParallel.hashCode ^
runPubGetOffline.hashCode;
runPubGetOffline.hashCode ^
const DeepCollectionEquality(_GlobEquality())
.hash(dependencyOverridePaths);

@override
String toString() {
return '''
BootstrapCommandConfigs(
runPubGetInParallel: $runPubGetInParallel,
runPubGetOffline: $runPubGetOffline,
dependencyOverridePaths: $dependencyOverridePaths,
)''';
}
}
Expand Down Expand Up @@ -755,12 +791,21 @@ You must have one of the following to be a valid Melos workspace:

final melosYamlPath =
melosYamlPathForDirectory(melosWorkspaceDirectory.path);
final yamlContents = await loadYamlFile(melosYamlPath);
final yamlContents = (await loadYamlFile(melosYamlPath))?.toPlainObject()
as Map<Object?, Object?>?;

if (yamlContents == null) {
throw MelosConfigException('Failed to parse the melos.yaml file');
}

final melosOverridesYamlPath =
melosOverridesYamlPathForDirectory(melosWorkspaceDirectory.path);
final overridesYamlContents = (await loadYamlFile(melosOverridesYamlPath))
?.toPlainObject() as Map<Object?, Object?>?;
if (overridesYamlContents != null) {
mergeMap(yamlContents, overridesYamlContents);
}

return MelosWorkspaceConfig.fromYaml(
yamlContents,
path: melosWorkspaceDirectory.path,
Expand Down Expand Up @@ -833,8 +878,10 @@ You must have one of the following to be a valid Melos workspace:
other.path == path &&
other.name == name &&
other.repository == repository &&
const DeepCollectionEquality().equals(other.packages, packages) &&
const DeepCollectionEquality().equals(other.ignore, ignore) &&
const DeepCollectionEquality(_GlobEquality())
.equals(other.packages, packages) &&
const DeepCollectionEquality(_GlobEquality())
.equals(other.ignore, ignore) &&
other.scripts == scripts &&
other.ide == ide &&
other.commands == commands;
Expand All @@ -845,8 +892,8 @@ You must have one of the following to be a valid Melos workspace:
path.hashCode ^
name.hashCode ^
repository.hashCode ^
const DeepCollectionEquality().hash(packages) &
const DeepCollectionEquality().hash(ignore) ^
const DeepCollectionEquality(_GlobEquality()).hash(packages) &
const DeepCollectionEquality(_GlobEquality()).hash(ignore) ^
scripts.hashCode ^
ide.hashCode ^
commands.hashCode;
Expand Down Expand Up @@ -879,3 +926,17 @@ MelosWorkspaceConfig(
)''';
}
}

class _GlobEquality implements Equality<Glob> {
const _GlobEquality();

@override
bool equals(Glob e1, Glob e2) =>
e1.pattern == e2.pattern && e1.context.current == e2.context.current;

@override
int hash(Glob e) => e.pattern.hashCode ^ e.context.current.hashCode;

@override
bool isValidKey(Object? o) => true;
}
1 change: 1 addition & 0 deletions packages/melos/test/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ class VirtualWorkspaceBuilder {
config: config,
allPackages: packageMap,
filteredPackages: packageMap,
dependencyOverridePackages: _buildVirtualPackageMap(const [], logger),
logger: logger,
sdkPath: sdkPath,
);
Expand Down
31 changes: 31 additions & 0 deletions packages/melos/test/utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,35 @@ void main() {
);
});
});

group('mergeYaml', () {
test('correctly handles value overriding', () {
final base = {
'abc': 123,
'def': [4, 5, 6],
'ghi': {'j': 'k', 'l': 'm', 'n': 'o'},
'pqr': ['1', '2', '3'],
'stu': 'aStringValue',
'vwx': true,
};
const overlay = {
'abc': 098,
'def': [7],
'ghi': {'j': 'i', 'l': 'm', 'n': 'o', 'p': 'q'},
'pqr': 'differentType',
'stu': ['another', 'different', 'type'],
'yza': false,
};
mergeMap(base, overlay);
expect(base, const {
'abc': 098,
'def': [4, 5, 6, 7],
'ghi': {'j': 'i', 'l': 'm', 'n': 'o', 'p': 'q'},
'pqr': 'differentType',
'stu': ['another', 'different', 'type'],
'vwx': true,
'yza': false,
});
});
});
}
Loading