Skip to content
Open
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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,33 @@ and your sub-packages should have `resolution: workspace` in their `pubspec.yaml
```bash
$ dart run dependency_validator -C pkg1
```

### Workspace Configuration

When working with workspaces, you can configure workspace-specific settings in the root package's `dart_dependency_validator.yaml` file:

```yaml
# dart_dependency_validator.yaml (in workspace root)

# Allow pinned packages across the workspace
allow_pins: true

# Ignore specific packages in all workspace sub-packages
workspace_global_ignore:
- some_package
- another_package

# Skip validation for specific workspace packages
workspace_package_ignore:
- pkg1
- pkg2
```

#### Configuration Inheritance

By default, sub-packages inherit certain configuration settings from the workspace root:
- `workspace_global_ignore`: Packages listed here will be ignored in all sub-packages
- `allow_pins`: If set to `true` in the workspace root, sub-packages will also allow pinned dependencies
- `ignore`: The standard ignore list from the workspace root is also inherited

**Important**: If a sub-package has its own `dart_dependency_validator.yaml` file, it will take complete precedence over the workspace configuration. The local config file is always prioritized over any inherited workspace settings.
14 changes: 13 additions & 1 deletion example/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,17 @@ ignore:
- analyzer

# Allow dependencies to be pinned to a specific version instead of a range
allowPins: true
allow_pins: true

# Workspace-specific configuration (only applies to workspace root packages):

# Ignore specific packages in all workspace sub-packages
workspace_global_ignore:
- some_package
- another_package

# Skip validation for specific workspace packages
workspace_package_ignore:
- pkg1
- pkg2
```
46 changes: 40 additions & 6 deletions lib/src/dependency_validator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ import 'pubspec_config.dart';
import 'utils.dart';

/// Check for missing, under-promoted, over-promoted, and unused dependencies.
Future<bool> checkPackage({required String root}) async {
Future<bool> checkPackage(
{required String root,
List<String>? inheritedWorkspaceGlobalIgnore,
bool inheritedAllowedPins = false}) async {
var result = true;
if (!File('$root/pubspec.yaml').existsSync()) {
logger.shout(red.wrap('pubspec.yaml not found'));
Expand All @@ -37,7 +40,9 @@ Future<bool> checkPackage({required String root}) async {

DepValidatorConfig config;
final configFile = File('$root/dart_dependency_validator.yaml');
var hasLocalFileConfig = false;
if (configFile.existsSync()) {
hasLocalFileConfig = true;
config = DepValidatorConfig.fromYaml(configFile.readAsStringSync());
} else {
final pubspecConfig = PubspecDepValidatorConfig.fromYaml(
Expand Down Expand Up @@ -66,7 +71,9 @@ Future<bool> checkPackage({required String root}) async {
.nonNulls
.toList();
logger.fine('excludes:\n${bulletItems(excludes.map((g) => g.pattern))}\n');
final ignoredPackages = config.ignore;
final ignoredPackages = hasLocalFileConfig
? config.ignore
: inheritedWorkspaceGlobalIgnore ?? config.ignore;
logger.fine('ignored packages:\n${bulletItems(ignoredPackages)}\n');

// Read and parse the analysis_options.yaml in the current working directory.
Expand All @@ -81,16 +88,43 @@ Future<bool> checkPackage({required String root}) async {

var subResult = true;
if (pubspec.isWorkspaceRoot) {
final workspacePackageIgnore = config.workspacePackageIgnore;
logger.fine('In a workspace. Recursing through sub-packages...');
for (final package in pubspec.workspace ?? []) {
subResult &= await checkPackage(root: '$root/$package');

final subPackages = <String>[];
for (final workspacePattern in pubspec.workspace ?? []) {
if (workspacePattern.contains('*')) {
final glob = makeGlob('$root/$workspacePattern');
final matchingDirs = Directory(root)
.listSync(recursive: true)
.whereType<Directory>()
.where((dir) => glob.matches(dir.path))
.where((dir) => File('${dir.path}/pubspec.yaml').existsSync())
.map((dir) => p.relative(dir.path, from: root))
.toList();
subPackages.addAll(matchingDirs);
} else {
subPackages.add(workspacePattern);
}
}

for (final package in subPackages) {
if (workspacePackageIgnore.contains(package)) {
logger.info('Skipping ${package} because it is ignored');
} else {
subResult &= await checkPackage(
root: '$root/$package',
inheritedWorkspaceGlobalIgnore: config.workspaceGlobalIgnore,
inheritedAllowedPins: config.allowPins);
}
logger.info('');
}
}

logger.info('Validating dependencies for ${pubspec.name}...');

if (!config.allowPins) {
final allowedPins =
hasLocalFileConfig ? config.allowPins : inheritedAllowedPins;
if (!allowedPins) {
checkPubspecForPins(pubspec, ignoredPackages: ignoredPackages);
}

Expand Down
8 changes: 8 additions & 0 deletions lib/src/pubspec_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,20 @@ class DepValidatorConfig {
@JsonKey(defaultValue: [])
final List<String> ignore;

@JsonKey(defaultValue: [])
final List<String> workspaceGlobalIgnore;

@JsonKey(defaultValue: [])
final List<String> workspacePackageIgnore;

@JsonKey(defaultValue: false)
final bool allowPins;

const DepValidatorConfig({
this.exclude = const [],
this.ignore = const [],
this.workspaceGlobalIgnore = const [],
this.workspacePackageIgnore = const [],
this.allowPins = false,
});

Expand Down
58 changes: 39 additions & 19 deletions lib/src/pubspec_config.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 67 additions & 0 deletions test/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,70 @@ Future<void> checkWorkspace({
final result = await checkPackage(root: '${d.sandbox}/workspace');
expect(result, matcher);
}

Future<void> checkWorkspaceWithMultiplePackages({
required Map<String, Dependency> workspaceDeps,
required List<String> workspacePatterns,
required Map<String, SubpackageConfig> subpackages,
required List<d.Descriptor> workspace,
DepValidatorConfig? workspaceConfig,
Level logLevel = Level.OFF,
Matcher matcher = isTrue,
}) async {
final workspacePubspec = Pubspec(
'workspace',
environment: requireDart36,
dependencies: workspaceDeps,
workspace: workspacePatterns,
);

final subpackageDirs = <d.Descriptor>[];
for (final entry in subpackages.entries) {
final packageName = entry.key;
final config = entry.value;
final subpackagePubspec = Pubspec(
packageName,
environment: requireDart36,
dependencies: config.dependencies,
resolution: 'workspace',
);
subpackageDirs.add(
d.dir(packageName, [
...config.descriptors,
d.file('pubspec.yaml', jsonEncode(subpackagePubspec.toJson())),
if (config.config != null)
d.file(
'dart_dependency_validator.yaml',
jsonEncode(config.config!.toJson()),
),
]),
);
}

final dir = d.dir('workspace', [
...workspace,
d.file('pubspec.yaml', jsonEncode(workspacePubspec.toJson())),
if (workspaceConfig != null)
d.file(
'dart_dependency_validator.yaml',
jsonEncode(workspaceConfig.toJson()),
),
...subpackageDirs,
]);
await dir.create();
Logger.root.level = logLevel;
final result = await checkPackage(root: '${d.sandbox}/workspace');
expect(result, matcher);
}

class SubpackageConfig {
final Map<String, Dependency> dependencies;
final List<d.Descriptor> descriptors;
final DepValidatorConfig? config;

SubpackageConfig({
required this.dependencies,
required this.descriptors,
this.config,
});
}
Loading