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

Support Pub Workspaces #138

Merged
merged 21 commits into from
Jan 10, 2025
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
9 changes: 5 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ on:
jobs:
build:
uses: Workiva/gha-dart-oss/.github/workflows/build.yaml@v0.1.7
with:
sdk: stable

checks:
uses: Workiva/gha-dart-oss/.github/workflows/checks.yaml@v0.1.7
with:
sdk: stable

unit-tests:
strategy:
matrix:
sdk: [2.19.6, stable]
uses: Workiva/gha-dart-oss/.github/workflows/test-unit.yaml@v0.1.7
with:
sdk: ${{ matrix.sdk }}
sdk: stable
4 changes: 3 additions & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ permissions:

jobs:
publish:
uses: Workiva/gha-dart-oss/.github/workflows/publish.yaml@v0.1.7
uses: Workiva/gha-dart-oss/.github/workflows/publish.yaml@v0.1.7
with:
sdk: stable
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 5.0.0

- **Breaking change**: Requires Dart 3.0 or above
- Added support for Pub Workspaces (monorepos)

# 4.1.1

- Update the output of parse failures to include the path to the file which failed to parse
Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,24 @@ ignore:
- analyzer
```

> Note: Previously this configuration lived in the `pubspec.yaml`, but that
> [!Note]
> Previously this configuration lived in the `pubspec.yaml`, but that
> option was deprecated because `pub publish` warns about unrecognized keys.

## Pub Workspaces (monorepos)

This package supports [Pub Workspaces](https://dart.dev/tools/pub/workspaces), a collection of packages in one repository. Workspaces allow Pub to share dependencies between your packages. Your top-level package's `pubspec.yaml` should have a `workspace` field that indicates which sub-packages should be included, like this:

```yaml
workspace:
- pkg1
- pkg2
```

and your sub-packages should have `resolution: workspace` in their `pubspec.yaml`s. For more information, see the linked documentation.

**Running `dependency_validator` will always validate the package your terminal is in**. If you run the tool on the top-level workspace package, it will analyze the workspace package _and_ its sub-packages. To just analyze a sub-package, run the tool in its folder, or pass the `-C` argument:

```bash
$ dart run dependency_validator -C pkg1
```
12 changes: 11 additions & 1 deletion bin/dependency_validator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'package:logging/logging.dart';

const String helpArg = 'help';
const String verboseArg = 'verbose';
const String rootDirArg = 'directory';
const String helpMessage =
'''Dependency Validator 2.0 is configured statically via the pubspec.yaml
example:
Expand All @@ -45,6 +46,12 @@ final ArgParser argParser = ArgParser()
verboseArg,
defaultsTo: false,
help: 'Display extra information for debugging.',
)
..addOption(
rootDirArg,
abbr: "C",
help: 'Validate dependencies in a subdirectory',
defaultsTo: '.',
);

void showHelpAndExit({ExitCode exitCode = ExitCode.success}) {
Expand Down Expand Up @@ -78,5 +85,8 @@ void main(List<String> args) async {
Logger.root.level = Level.ALL;
}

await run();
Logger.root.info('');
final rootDir = argResults.option(rootDirArg) ?? '.';
final result = await checkPackage(root: rootDir);
exit(result ? 0 : 1);
}
32 changes: 21 additions & 11 deletions example/example.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
Either globally activate this package or add it as a dev_dependency. Then run:
Either globally activate this package or add it as a dev_dependency:
```bash
# Install as a dev dependency on the project -- shared with all collaborators
$ dart pub add --dev dependency_validator

# Install globally on your system -- does not impact the project
$ dart pub global activate dependency_validator
```

Then run:

```bash
# If installed as a dependency:
Expand All @@ -8,16 +17,17 @@ $ dart run dependency_validator
$ dart pub global run dependency_validator
```

If needed, configure dependency_validator in your `pubspec.yaml`:
If needed, add a configuration in `dart_dependency_validator.yaml`:

```yaml
# pubsec.yaml
dependency_validator:
# Exclude one or more paths from being scanned.
# Supports glob syntax.
exclude:
- "app/**"
# Ignore one or more packages.
ignore:
- analyzer
# Exclude one or more paths from being scanned. Supports glob syntax.
exclude:
- "app/**"

# Ignore one or more packages.
ignore:
- analyzer

# Allow dependencies to be pinned to a specific version instead of a range
allowPins: true
```
121 changes: 77 additions & 44 deletions lib/src/dependency_validator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import 'dart:io';

import 'package:build_config/build_config.dart';
import 'package:dependency_validator/src/import_export_ast_visitor.dart';
import 'package:glob/glob.dart';
import 'package:io/ansi.dart';
import 'package:logging/logging.dart';
import 'package:package_config/package_config.dart';
Expand All @@ -28,54 +27,63 @@ import 'pubspec_config.dart';
import 'utils.dart';

/// Check for missing, under-promoted, over-promoted, and unused dependencies.
Future<void> run() async {
if (!File('pubspec.yaml').existsSync()) {
Future<bool> checkPackage({required String root}) async {
var result = true;
if (!File('$root/pubspec.yaml').existsSync()) {
logger.shout(red.wrap('pubspec.yaml not found'));
exit(1);
}
if (!File('.dart_tool/package_config.json').existsSync()) {
logger.shout(red.wrap(
'No .dart_tool/package_config.json file found, please run "pub get" first.'));
exit(1);
logger.fine('Path: $root/pubspec.yaml');
return false;
}

DepValidatorConfig config;
final configFile = File('dart_dependency_validator.yaml');
final configFile = File('$root/dart_dependency_validator.yaml');
if (configFile.existsSync()) {
config = DepValidatorConfig.fromYaml(configFile.readAsStringSync());
} else {
final pubspecConfig = PubspecDepValidatorConfig.fromYaml(
File('pubspec.yaml').readAsStringSync());
File('$root/pubspec.yaml').readAsStringSync(),
);
if (pubspecConfig.isNotEmpty) {
logger.warning(yellow.wrap(
'Configuring dependency_validator in pubspec.yaml is deprecated.\n'
'Use dart_dependency_validator.yaml instead.'));
}
config = pubspecConfig.dependencyValidator;
}

final excludes = config.exclude
matthewnitschke-wk marked this conversation as resolved.
Show resolved Hide resolved
.map((s) {
try {
return Glob(s);
return makeGlob("$root/$s");
} catch (_, __) {
logger.shout(yellow.wrap('invalid glob syntax: "$s"'));
return null;
}
})
.where((g) => g != null)
.cast<Glob>()
.nonNulls
.toList();
logger.fine('excludes:\n${bulletItems(excludes.map((g) => g.pattern))}\n');
final ignoredPackages = config.ignore;
logger.fine('ignored packages:\n${bulletItems(ignoredPackages)}\n');

// Read and parse the analysis_options.yaml in the current working directory.
final optionsIncludePackage = getAnalysisOptionsIncludePackage();
final optionsIncludePackage = getAnalysisOptionsIncludePackage(path: root);

// Read and parse the pubspec.yaml in the current working directory.
final pubspecFile = File('pubspec.yaml');
final pubspec =
Pubspec.parse(pubspecFile.readAsStringSync(), sourceUrl: pubspecFile.uri);
final pubspecFile = File('$root/pubspec.yaml');
final pubspec = Pubspec.parse(
pubspecFile.readAsStringSync(),
sourceUrl: pubspecFile.uri,
);

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

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

Expand All @@ -92,7 +100,8 @@ Future<void> run() async {
logger.fine('dev_dependencies:\n'
'${bulletItems(devDeps)}\n');

final publicDirs = ['bin/', 'lib/'];
final publicDirs = ['$root/bin/', '$root/lib/'];
logger.fine("Excluding: $excludes");
final publicDartFiles = [
for (final dir in publicDirs) ...listDartFilesIn(dir, excludes),
];
Expand Down Expand Up @@ -132,14 +141,29 @@ Future<void> run() async {
logger.fine('packages used in public facing files:\n'
'${bulletItems(packagesUsedInPublicFiles)}\n');

final publicDirGlobs = [for (final dir in publicDirs) Glob('$dir**')];
final publicDirGlobs = [
for (final dir in publicDirs) makeGlob('$dir**'),
];

final subpackageGlobs = [
for (final subpackage in pubspec.workspace ?? [])
makeGlob('$root/$subpackage**'),
];

logger.fine('subpackage globs: $subpackageGlobs');

final nonPublicDartFiles =
listDartFilesIn('./', [...excludes, ...publicDirGlobs]);
final nonPublicScssFiles =
listScssFilesIn('./', [...excludes, ...publicDirGlobs]);
final nonPublicLessFiles =
listLessFilesIn('./', [...excludes, ...publicDirGlobs]);
final nonPublicDartFiles = listDartFilesIn(
'$root/',
[...excludes, ...publicDirGlobs, ...subpackageGlobs],
);
final nonPublicScssFiles = listScssFilesIn(
'$root/',
[...excludes, ...publicDirGlobs, ...subpackageGlobs],
);
final nonPublicLessFiles = listLessFilesIn(
'$root/',
[...excludes, ...publicDirGlobs, ...subpackageGlobs],
);

logger
..fine('non-public dart files:\n'
Expand Down Expand Up @@ -193,7 +217,7 @@ Future<void> run() async {
'These packages are used in lib/ but are not dependencies:',
missingDependencies,
);
exitCode = 1;
result = false;
}

// Packages that are used outside lib/ but are not dev_dependencies.
Expand All @@ -215,7 +239,7 @@ Future<void> run() async {
'These packages are used outside lib/ but are not dev_dependencies:',
missingDevDependencies,
);
exitCode = 1;
result = false;
}

// Packages that are not used in lib/, but are used elsewhere, that are
Expand All @@ -235,7 +259,7 @@ Future<void> run() async {
'These packages are only used outside lib/ and should be downgraded to dev_dependencies:',
overPromotedDependencies,
);
exitCode = 1;
result = false;
}

// Packages that are used in lib/, but are dev_dependencies.
Expand All @@ -251,7 +275,7 @@ Future<void> run() async {
'These packages are used in lib/ and should be promoted to actual dependencies:',
underPromotedDependencies,
);
exitCode = 1;
result = false;
}

// Packages that are not used anywhere but are dependencies.
Expand All @@ -269,8 +293,7 @@ Future<void> run() async {
if (packageConfig == null) {
logger.severe(red.wrap(
'Could not find package config. Make sure you run `dart pub get` first.'));
exitCode = 1;
return;
return false;
}

// Remove deps that provide builders that will be applied
Expand All @@ -285,30 +308,39 @@ Future<void> run() async {
.any((key) => key.startsWith('$dependencyName:'));

final packagesWithConsumedBuilders = Set<String>();
for (final package in unusedDependencies.map((name) => packageConfig[name])) {
for (final name in unusedDependencies) {
final package = packageConfig[name];
if (package == null) continue;
// Check if a builder is used from this package
if (rootPackageReferencesDependencyInBuildYaml(package!.name) ||
if (rootPackageReferencesDependencyInBuildYaml(package.name) ||
await dependencyDefinesAutoAppliedBuilder(p.fromUri(package.root))) {
packagesWithConsumedBuilders.add(package.name);
}
}

logIntersection(
Level.FINE,
'The following packages contain builders that are auto-applied or referenced in "build.yaml"',
unusedDependencies,
packagesWithConsumedBuilders);
Level.FINE,
'The following packages contain builders that are auto-applied or referenced in "build.yaml"',
unusedDependencies,
packagesWithConsumedBuilders,
);
unusedDependencies.removeAll(packagesWithConsumedBuilders);

// Remove deps that provide executables, those are assumed to be used
final packagesWithExecutables = unusedDependencies.where((name) {
bool providesExecutable(String name) {
final package = packageConfig[name];
final binDir = Directory(p.join(p.fromUri(package!.root), 'bin'));
if (package == null) return false;
final binDir = Directory(p.join(p.fromUri(package.root), 'bin'));
if (!binDir.existsSync()) return false;

// Search for executables, if found we assume they are used
return binDir.listSync().any((entity) => entity.path.endsWith('.dart'));
}).toSet();
}

final packagesWithExecutables = {
for (final package in unusedDependencies)
if (providesExecutable(package)) package,
};

final nonDevPackagesWithExecutables =
packagesWithExecutables.where(pubspec.dependencies.containsKey).toSet();
Expand All @@ -319,7 +351,7 @@ Future<void> run() async {
unusedDependencies,
nonDevPackagesWithExecutables,
);
exitCode = 1;
result = false;
}

logIntersection(
Expand Down Expand Up @@ -347,12 +379,13 @@ Future<void> run() async {
'These packages may be unused, or you may be using assets from these packages:',
unusedDependencies,
);
exitCode = 1;
result = false;
}

if (exitCode == 0) {
if (result) {
logger.info(green.wrap('✓ No dependency issues found!'));
}
return result && subResult;
}

/// Whether a dependency at [path] defines an auto applied builder.
Expand Down
Loading
Loading