Skip to content

Commit

Permalink
feat: Filter by category (#727)
Browse files Browse the repository at this point in the history
* filter by category

* removing comments

* refactor the categories function

* add support to filter flag

* feat: create category filter

* fix: solve lint suggestions

* fix: solve comments

* doc: create doc

* Update package_test.dart

* fix: solve doc comments

* Update filters.mdx

---------

Co-authored-by: Lukas Klingsbo <[email protected]>
  • Loading branch information
tilucasoli and spydon authored Jun 17, 2024
1 parent 6c0b95b commit 71bc610
Show file tree
Hide file tree
Showing 13 changed files with 336 additions and 7 deletions.
22 changes: 18 additions & 4 deletions docs/configuration/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,24 @@ ignore:
- 'packages/**/example'
```

<Info>
You can also expand the scope of packages on a per-command basis via the
[`--scope` filter](/filters#--scope) flag.
</Info>
## categories

Categories are used to group packages together.

To define custom package categories, add a `categories` section in your `melos.yaml` file.
Under this section, you can specify category names as keys, and their corresponding values
should be lists of glob patterns that match the packages you want to include in each category.

```yaml
# melos.yaml
categories:
examples:
- packages/example*
alpha:
- packages/feature_a/*
- packages/feature_b
```

## ide/intellij

Expand Down
9 changes: 9 additions & 0 deletions docs/filters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ repeated.
melos exec --ignore="*internal*" -- flutter build ios
```

## --categories

Filter packages based on categories declared in the `melos.yaml` file.

```bash
# Run `flutter build ios` on all packages in the "examples" category.
melos exec --category="examples" -- flutter build ios
```

## --diff

Filter packages based on whether there were changes between a commit and the
Expand Down
3 changes: 3 additions & 0 deletions melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ packages:
- packages/*
ignore:
- packages/melos_flutter_deps_check
categories:
testando:
- packages/*

command:
bootstrap:
Expand Down
12 changes: 12 additions & 0 deletions packages/melos/lib/src/command_runner/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ abstract class MelosCommand extends Command<void> {
'option can be repeated.',
);

argParser.addMultiOption(
filterOptionCategory,
valueHelp: 'glob',
help:
'Include only packages with categories matching the given glob. This '
'option can be repeated.',
);

argParser.addMultiOption(
filterOptionIgnore,
valueHelp: 'glob',
Expand Down Expand Up @@ -151,6 +159,7 @@ abstract class MelosCommand extends Command<void> {

final diff = diffEnabled ? argResults![filterOptionDiff] as String? : null;
final scope = argResults![filterOptionScope] as List<String>? ?? [];
final categories = argResults![filterOptionCategory] as List<String>? ?? [];
final ignore = argResults![filterOptionIgnore] as List<String>? ?? [];

return PackageFilters(
Expand All @@ -161,6 +170,9 @@ abstract class MelosCommand extends Command<void> {
.map((e) => createGlob(e, currentDirectoryPath: workingDirPath))
.toList()
..addAll(config.ignore),
categories: categories
.map((e) => createGlob(e, currentDirectoryPath: workingDirPath))
.toList(),
diff: diff,
includePrivatePackages: argResults![filterOptionPrivate] as bool?,
published: argResults![filterOptionPublished] as bool?,
Expand Down
1 change: 1 addition & 0 deletions packages/melos/lib/src/common/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const globalOptionSdkPath = 'sdk-path';
const autoSdkPathOptionValue = 'auto';

const filterOptionScope = 'scope';
const filterOptionCategory = 'category';
const filterOptionIgnore = 'ignore';
const filterOptionDirExists = 'dir-exists';
const filterOptionFileExists = 'file-exists';
Expand Down
20 changes: 20 additions & 0 deletions packages/melos/lib/src/common/validation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,26 @@ List<T> assertListIsA<T>({
];
}

Map<T, V> assertMapIsA<T, V>({
String? path,
required Object key,
required Map<Object?, Object?> map,
required bool isRequired,
required T Function(Object? value) assertKey,
required V Function(Object? key, Object? value) assertValue,
}) {
final collection = assertKeyIsA<Map<Object?, Object?>?>(key: key, map: map);

if (isRequired && collection == null) {
throw MelosConfigException.missingKey(key: key, path: path);
}

return <T, V>{
for (final entry in collection?.entries ?? <MapEntry<Object?, Object?>>[])
assertKey(entry.key): assertValue(entry.key, entry.value),
};
}

/// Thrown when `melos.yaml` configuration is malformed.
class MelosConfigException implements MelosException {
MelosConfigException(this.message);
Expand Down
55 changes: 54 additions & 1 deletion packages/melos/lib/src/package.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class PackageFilters {
PackageFilters({
this.scope = const [],
this.ignore = const [],
this.categories = const [],
this.dirExists = const [],
this.fileExists = const [],
List<String> dependsOn = const [],
Expand Down Expand Up @@ -133,6 +134,12 @@ class PackageFilters {
path: path,
);

final category = assertListOrString(
key: filterOptionCategory.camelCased,
map: yaml,
path: path,
);

final ignore = assertListOrString(
key: filterOptionIgnore.camelCased,
map: yaml,
Expand Down Expand Up @@ -247,6 +254,7 @@ class PackageFilters {
published: published,
nullSafe: nullSafe,
flutter: flutter,
categories: category.map(createPackageGlob).toList(),
);
}

Expand All @@ -255,6 +263,7 @@ class PackageFilters {
const PackageFilters._({
required this.scope,
required this.ignore,
required this.categories,
required this.dirExists,
required this.fileExists,
required this.dependsOn,
Expand All @@ -273,6 +282,9 @@ class PackageFilters {
/// Patterns for excluding packages by name.
final List<Glob> ignore;

/// Patterns for filtering packages by category.
final List<Glob> categories;

/// Include a package only if a given directory exists.
final List<String> dirExists;

Expand Down Expand Up @@ -315,6 +327,9 @@ class PackageFilters {
return {
if (scope.isNotEmpty)
filterOptionScope.camelCased: scope.map((e) => e.toString()).toList(),
if (categories.isNotEmpty)
filterOptionCategory.camelCased:
scope.map((e) => e.toString()).toList(),
if (ignore.isNotEmpty)
filterOptionIgnore.camelCased: ignore.map((e) => e.toString()).toList(),
if (dirExists.isNotEmpty) filterOptionDirExists.camelCased: dirExists,
Expand Down Expand Up @@ -346,6 +361,7 @@ class PackageFilters {
diff: diff,
includeDependencies: includeDependencies,
includeDependents: includeDependents,
categories: categories,
);
}

Expand All @@ -363,6 +379,7 @@ class PackageFilters {
diff: diff,
includeDependencies: includeDependencies,
includeDependents: includeDependents,
categories: categories,
);
}

Expand All @@ -379,12 +396,14 @@ class PackageFilters {
String? diff,
bool? includeDependencies,
bool? includeDependents,
List<Glob>? categories,
}) {
return PackageFilters._(
dependsOn: dependsOn ?? this.dependsOn,
dirExists: dirExists ?? this.dirExists,
fileExists: fileExists ?? this.fileExists,
ignore: ignore ?? this.ignore,
categories: categories ?? this.categories,
includePrivatePackages:
includePrivatePackages ?? this.includePrivatePackages,
noDependsOn: noDependsOn ?? this.noDependsOn,
Expand Down Expand Up @@ -412,6 +431,7 @@ class PackageFilters {
const DeepCollectionEquality().equals(other.fileExists, fileExists) &&
const DeepCollectionEquality().equals(other.dependsOn, dependsOn) &&
const DeepCollectionEquality().equals(other.noDependsOn, noDependsOn) &&
const DeepCollectionEquality().equals(other.categories, categories) &&
other.diff == diff;

@override
Expand All @@ -428,6 +448,7 @@ class PackageFilters {
const DeepCollectionEquality().hash(fileExists) ^
const DeepCollectionEquality().hash(dependsOn) ^
const DeepCollectionEquality().hash(noDependsOn) ^
const DeepCollectionEquality().hash(categories) ^
diff.hashCode;

@override
Expand All @@ -440,6 +461,7 @@ PackageFilters(
includeDependents: $includeDependents,
includePrivatePackages: $includePrivatePackages,
scope: $scope,
categories: $categories,
ignore: $ignore,
dirExists: $dirExists,
fileExists: $fileExists,
Expand Down Expand Up @@ -494,6 +516,7 @@ class PackageMap {
required String workspacePath,
required List<Glob> packages,
required List<Glob> ignore,
required Map<String, List<Glob>> categories,
required MelosLogger logger,
}) async {
final pubspecFiles = await _resolvePubspecFiles(
Expand Down Expand Up @@ -528,6 +551,20 @@ The packages that caused the problem are:
);
}

final filteredCategories = <String>[];

categories.forEach((key, value) {
final isCategoryMatching = value.any(
(category) => category.matches(
relativePath(pubspecDirPath, workspacePath),
),
);

if (isCategoryMatching) {
filteredCategories.add(key);
}
});

packageMap[name] = Package(
name: name,
path: pubspecDirPath,
Expand All @@ -539,6 +576,7 @@ The packages that caused the problem are:
devDependencies: pubSpec.devDependencies.keys.toList(),
dependencyOverrides: pubSpec.dependencyOverrides.keys.toList(),
pubSpec: pubSpec,
categories: filteredCategories,
);
}),
);
Expand Down Expand Up @@ -602,6 +640,7 @@ The packages that caused the problem are:
.applyFileExists(filters.fileExists)
.filterPrivatePackages(include: filters.includePrivatePackages)
.applyScope(filters.scope)
.applyCategories(filters.categories)
.applyDependsOn(filters.dependsOn)
.applyNoDependsOn(filters.noDependsOn)
.filterNullSafe(nullSafe: filters.nullSafe)
Expand All @@ -626,7 +665,7 @@ The packages that caused the problem are:
}
}

extension on Iterable<Package> {
extension IterablePackageExt on Iterable<Package> {
Iterable<Package> applyIgnore(List<Glob> ignore) {
if (ignore.isEmpty) return this;

Expand Down Expand Up @@ -747,6 +786,18 @@ extension on Iterable<Package> {
}).toList();
}

Iterable<Package> applyCategories(List<Glob> appliedCategories) {
if (appliedCategories.isEmpty) return this;

return where((package) {
return package.categories.any(
(category) => appliedCategories.any(
(appliedCategory) => appliedCategory.matches(category),
),
);
}).toList();
}

Iterable<Package> applyDependsOn(List<String> dependsOn) {
if (dependsOn.isEmpty) return this;

Expand Down Expand Up @@ -802,6 +853,7 @@ class Package {
required this.version,
required this.publishTo,
required this.pubSpec,
required this.categories,
}) : _packageMap = packageMap,
assert(p.isAbsolute(path));

Expand All @@ -816,6 +868,7 @@ class Package {
final Version version;
final String path;
final PubSpec pubSpec;
final List<String> categories;

/// Package path as a normalized sting relative to the root of the workspace.
/// e.g. "packages/firebase_database".
Expand Down
2 changes: 2 additions & 0 deletions packages/melos/lib/src/workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ class MelosWorkspace {
workspacePath: workspaceConfig.path,
packages: workspaceConfig.packages,
ignore: workspaceConfig.ignore,
categories: workspaceConfig.categories,
logger: logger,
);
final dependencyOverridePackages = await PackageMap.resolvePackages(
workspacePath: workspaceConfig.path,
packages: workspaceConfig.commands.bootstrap.dependencyOverridePaths,
ignore: const [],
categories: const {},
logger: logger,
);

Expand Down
Loading

0 comments on commit 71bc610

Please sign in to comment.