diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cfc4de9..ce79f68 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,5 +8,5 @@ jobs: - uses: dart-lang/setup-dart@v1 with: sdk: 3.1.0 - - run: make install + - run: make setup - run: make check diff --git a/Makefile b/Makefile index 543a7a5..668c94b 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ -.PHONY: install check +.PHONY: setup check -install: +setup: dart pub get + dart pub global activate melos check: dart format lib --line-length 100 --set-exit-if-changed diff --git a/README.md b/README.md index 8b82106..6b70bf4 100644 --- a/README.md +++ b/README.md @@ -44,4 +44,5 @@ dart run cyclic_dependency_checks defaults to current working directory. * `--mono-repo` or `-m` as an alternative to path, use the path to your Melos based mono-repo to run against all its components. + * `--exclude` or `-x` can be used multiple times or as coma separated values to exclude specified projects from the mono-repo. * `--max-depth` or `-d` to limit the depth of the check, defaults to no max depth. diff --git a/lib/cycle_detection/cycle_detector_runner.dart b/lib/cycle_detection/cycle_detector_runner.dart index 9998198..ddf82db 100644 --- a/lib/cycle_detection/cycle_detector_runner.dart +++ b/lib/cycle_detection/cycle_detector_runner.dart @@ -26,7 +26,8 @@ class CycleDetectorRunner { final parser = ArgParser() ..addOption('path', abbr: 'p') ..addOption('mono-repo', abbr: 'm') - ..addOption('max-depth', abbr: 'd'); + ..addOption('max-depth', abbr: 'd') + ..addMultiOption('exclude', abbr: 'x'); final parsedArgs = parser.tryParse(args); if (parsedArgs == null) { @@ -36,8 +37,9 @@ class CycleDetectorRunner { final pathArg = parsedArgs['path']; final monorepoArg = parsedArgs['mono-repo']; final maxDepthArg = parsedArgs.tryGetInt('max-depth'); + final exclusions = parsedArgs['exclude']; - final inferredPaths = await _tryGetPaths(pathArg, monorepoArg); + final inferredPaths = await _tryGetPaths(pathArg, monorepoArg, exclusions); if (inferredPaths == null) { printer.err( 'Failed to infer path from arguments, only one of path or monorepo can be specified', @@ -55,11 +57,15 @@ class CycleDetectorRunner { return success; } - Future?> _tryGetPaths(String? packagePath, String? monorepoPath) async => + Future?> _tryGetPaths( + String? packagePath, + String? monorepoPath, + List exclusions, + ) async => switch ((packagePath, monorepoPath)) { (null, null) => ['.'], (String p, null) => [p], - (null, String p) => await MelosPaths.tryGet(p), + (null, String p) => await MelosPaths.tryGet(p, exclusions), (_, _) => null, }; diff --git a/lib/cycle_detection/melos_paths.dart b/lib/cycle_detection/melos_paths.dart index 2b2b9f4..a83b7a3 100644 --- a/lib/cycle_detection/melos_paths.dart +++ b/lib/cycle_detection/melos_paths.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:path/path.dart' as path; final class MelosPaths { - static Future?> tryGet(String monorepoPath) async { + static Future?> tryGet(String monorepoPath, List exclusions) async { final melos = Platform.isWindows ? 'melos.bat' : 'melos'; final listResult = await Process.run( @@ -21,6 +21,7 @@ final class MelosPaths { return output .split('\n') .where((p) => p.trim().length > 0) + .where((p) => !exclusions.contains(path.basename(p))) .map((p) => path.relative(p, from: currentDir.absolute.path)) .toList(); } diff --git a/pubspec.yaml b/pubspec.yaml index 9c0977f..1b01a5c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,6 +6,8 @@ environment: sdk: '>=3.0.5 <4.0.0' dev_dependencies: + melos: ^6.1.0 + path: ^1.9.0 test: ^1.24.5 dependencies: args: ^2.4.2 diff --git a/test/cycle_detector_runner_test.dart b/test/cycle_detector_runner_test.dart index b7d0652..7869075 100644 --- a/test/cycle_detector_runner_test.dart +++ b/test/cycle_detector_runner_test.dart @@ -2,16 +2,17 @@ import 'package:cyclic_dependency_checks/cycle_detection/cycle_detector.dart'; import 'package:cyclic_dependency_checks/cycle_detection/cycle_detector_runner.dart'; import 'package:cyclic_dependency_checks/cycle_detection/module_dependency.dart'; import 'package:test/test.dart'; +import 'package:path/path.dart' as path; class TestCycleDetector extends CycleDetector { List> stubbedCycles = []; - String? recordedPackagePath; - int? recordedMaxDepth; + List recordedPackagePaths = []; + List recordedMaxDepths = []; @override Future>> detect(String packagePath, {int? maxDepth}) async { - recordedPackagePath = packagePath; - recordedMaxDepth = maxDepth; + recordedPackagePaths.add(packagePath); + recordedMaxDepths.add(maxDepth); return stubbedCycles; } } @@ -35,8 +36,8 @@ void main() { expect(success, isTrue); expect(printer.errLog, isEmpty); - expect(detector.recordedPackagePath, equals('.')); - expect(detector.recordedMaxDepth, isNull); + expect(detector.recordedPackagePaths, equals(['.'])); + expect(detector.recordedMaxDepths, equals([null])); }); test('with no arguments, on error', () async { @@ -54,32 +55,76 @@ void main() { expect(printer.errLog, contains('module:a -> module:b -> module:a')); }); - test('with full length args', () async { - final printer = RecordingPrinter(); - final detector = TestCycleDetector(); - final runner = CycleDetectorRunner(detector: detector, printer: printer); + group('specifying the path', () { + test('with full length args', () async { + final printer = RecordingPrinter(); + final detector = TestCycleDetector(); + final runner = CycleDetectorRunner(detector: detector, printer: printer); + + await runner.run(['--path', '/some/path', '--max-depth', '10']); + + expect(detector.recordedPackagePaths, equals(['/some/path'])); + expect(detector.recordedMaxDepths, equals([10])); + }); - await runner.run([ - '--path', '/some/path', - '--max-depth', '10', - ]); + test('with short name args', () async { + final printer = RecordingPrinter(); + final detector = TestCycleDetector(); + final runner = CycleDetectorRunner(detector: detector, printer: printer); - expect(detector.recordedPackagePath, equals('/some/path')); - expect(detector.recordedMaxDepth, equals(10)); + await runner.run(['-p', '/some/path', '-d', '10']); + + expect(detector.recordedPackagePaths, equals(['/some/path'])); + expect(detector.recordedMaxDepths, equals([10])); + }); }); - test('with short name args', () async { - final printer = RecordingPrinter(); - final detector = TestCycleDetector(); - final runner = CycleDetectorRunner(detector: detector, printer: printer); + group('specifying the path to a melos mono repo', () { + test('with long argument', () async { + final printer = RecordingPrinter(); + final detector = TestCycleDetector(); + final runner = CycleDetectorRunner(detector: detector, printer: printer); + + await runner.run([ + '--mono-repo', + 'test_resources/example_melos_codebase', + '--max-depth', + '5', + ]); + + expect(detector.recordedPackagePaths, [ + path.join('test_resources', 'example_melos_codebase', 'project_a'), + path.join('test_resources', 'example_melos_codebase', 'project_b'), + ]); + expect(detector.recordedMaxDepths, [5, 5]); + }); - await runner.run([ - '-p', '/some/path', - '-d', '10', - ]); + test('with short argument', () async { + final printer = RecordingPrinter(); + final detector = TestCycleDetector(); + final runner = CycleDetectorRunner(detector: detector, printer: printer); - expect(detector.recordedPackagePath, equals('/some/path')); - expect(detector.recordedMaxDepth, equals(10)); + await runner.run(['-m', 'test_resources/example_melos_codebase', '-d', '5']); + + expect(detector.recordedPackagePaths, [ + path.join('test_resources', 'example_melos_codebase', 'project_a'), + path.join('test_resources', 'example_melos_codebase', 'project_b'), + ]); + expect(detector.recordedMaxDepths, [5, 5]); + }); + + test('excluding a specific subproject', () async { + final printer = RecordingPrinter(); + final detector = TestCycleDetector(); + final runner = CycleDetectorRunner(detector: detector, printer: printer); + + final projectPath = 'test_resources/example_melos_codebase'; + await runner.run(['-m', projectPath, '-x', 'project_a', '-x', 'project_c']); + + expect(detector.recordedPackagePaths, [ + path.join('test_resources', 'example_melos_codebase', 'project_b'), + ]); + }); }); test('with invalid arguments', () async { @@ -87,12 +132,10 @@ void main() { final detector = TestCycleDetector(); final runner = CycleDetectorRunner(detector: detector, printer: printer); - final success = await runner.run([ - '--oops' - ]); + final success = await runner.run(['--oops']); expect(success, equals(false)); - expect(detector.recordedPackagePath, isNull); - expect(detector.recordedMaxDepth, isNull); + expect(detector.recordedPackagePaths, []); + expect(detector.recordedMaxDepths, []); }); } diff --git a/test_resources/example_melos_codebase/melos.yaml b/test_resources/example_melos_codebase/melos.yaml new file mode 100644 index 0000000..0c746a9 --- /dev/null +++ b/test_resources/example_melos_codebase/melos.yaml @@ -0,0 +1,5 @@ +name: example_melos_codebase + +packages: + - project_a + - project_b diff --git a/test_resources/example_melos_codebase/project_a/lib/feature_a/a.dart b/test_resources/example_melos_codebase/project_a/lib/feature_a/a.dart new file mode 100644 index 0000000..862a0b2 --- /dev/null +++ b/test_resources/example_melos_codebase/project_a/lib/feature_a/a.dart @@ -0,0 +1 @@ +import 'package:example_codebase_no_cycles/feature_b/b.dart'; diff --git a/test_resources/example_melos_codebase/project_a/lib/feature_b/b.dart b/test_resources/example_melos_codebase/project_a/lib/feature_b/b.dart new file mode 100644 index 0000000..afc54a2 --- /dev/null +++ b/test_resources/example_melos_codebase/project_a/lib/feature_b/b.dart @@ -0,0 +1 @@ +import 'package:example_codebase_no_cycles/feature_c/c.dart'; diff --git a/test_resources/example_melos_codebase/project_a/lib/feature_c/c.dart b/test_resources/example_melos_codebase/project_a/lib/feature_c/c.dart new file mode 100644 index 0000000..e69de29 diff --git a/test_resources/example_melos_codebase/project_a/pubspec.yaml b/test_resources/example_melos_codebase/project_a/pubspec.yaml new file mode 100644 index 0000000..45bccdc --- /dev/null +++ b/test_resources/example_melos_codebase/project_a/pubspec.yaml @@ -0,0 +1,4 @@ +name: project_a + +environment: + sdk: '>=3.0.5 <4.0.0' diff --git a/test_resources/example_melos_codebase/project_b/lib/feature_a/a.dart b/test_resources/example_melos_codebase/project_b/lib/feature_a/a.dart new file mode 100644 index 0000000..05f6e55 --- /dev/null +++ b/test_resources/example_melos_codebase/project_b/lib/feature_a/a.dart @@ -0,0 +1 @@ +import 'package:example_codebase_with_cycles/feature_b/b.dart'; diff --git a/test_resources/example_melos_codebase/project_b/lib/feature_b/b.dart b/test_resources/example_melos_codebase/project_b/lib/feature_b/b.dart new file mode 100644 index 0000000..c39cac6 --- /dev/null +++ b/test_resources/example_melos_codebase/project_b/lib/feature_b/b.dart @@ -0,0 +1 @@ +import 'package:example_codebase_with_cycles/feature_c/c.dart'; diff --git a/test_resources/example_melos_codebase/project_b/lib/feature_c/c.dart b/test_resources/example_melos_codebase/project_b/lib/feature_c/c.dart new file mode 100644 index 0000000..ccd9766 --- /dev/null +++ b/test_resources/example_melos_codebase/project_b/lib/feature_c/c.dart @@ -0,0 +1 @@ +import 'package:example_codebase_with_cycles/feature_a/a.dart'; diff --git a/test_resources/example_melos_codebase/project_b/pubspec.yaml b/test_resources/example_melos_codebase/project_b/pubspec.yaml new file mode 100644 index 0000000..b764648 --- /dev/null +++ b/test_resources/example_melos_codebase/project_b/pubspec.yaml @@ -0,0 +1,4 @@ +name: project_b + +environment: + sdk: '>=3.0.5 <4.0.0' diff --git a/test_resources/example_melos_codebase/pubspec.yaml b/test_resources/example_melos_codebase/pubspec.yaml new file mode 100644 index 0000000..d483f7e --- /dev/null +++ b/test_resources/example_melos_codebase/pubspec.yaml @@ -0,0 +1,6 @@ +name: example_melos_codebase + +environment: + sdk: '>=3.0.5 <4.0.0' +dev_dependencies: + melos: ^6.1.0