-
Notifications
You must be signed in to change notification settings - Fork 236
feat: add workspace support for packages check licenses #1540
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
base: main
Are you sure you want to change the base?
Changes from 15 commits
2a58446
ce7e027
6177eb4
d625cb6
b8d019f
f32c5e9
7bb1147
ad7f11c
2cad6f0
9b9cfa2
f2760e4
33a4973
df0bda5
8ca2652
5cb355b
1242593
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ import 'package:package_config/package_config.dart' as package_config; | |
| import 'package:pana/src/license_detection/license_detector.dart' as detector; | ||
| import 'package:path/path.dart' as path; | ||
| import 'package:very_good_cli/src/pub_license/spdx_license.gen.dart'; | ||
| import 'package:very_good_cli/src/pubspec/pubspec.dart'; | ||
| import 'package:very_good_cli/src/pubspec_lock/pubspec_lock.dart'; | ||
|
|
||
| /// Overrides the [package_config.findPackageConfig] function for testing. | ||
|
|
@@ -192,11 +193,48 @@ class PackagesCheckLicensesCommand extends Command<int> { | |
| return ExitCode.noInput.code; | ||
| } | ||
|
|
||
| // Check if this is a workspace root and collect dependencies accordingly | ||
| final pubspecFile = File(path.join(targetPath, pubspecBasename)); | ||
| final pubspec = tryParsePubspec(pubspecFile); | ||
|
|
||
| // Collect workspace dependencies if this is a workspace root | ||
| final workspaceDependencies = _collectWorkspaceDependencies( | ||
| pubspec: pubspec, | ||
| targetDirectory: targetDirectory, | ||
| dependencyTypes: dependencyTypes, | ||
| ); | ||
|
|
||
| final filteredDependencies = pubspecLock.packages.where((dependency) { | ||
| if (!dependency.isPubHosted) return false; | ||
|
|
||
| if (skippedPackages.contains(dependency.name)) return false; | ||
|
|
||
| // If we have workspace dependencies, use them for filtering direct deps | ||
| if (workspaceDependencies != null) { | ||
| // For direct-main and direct-dev, check against workspace dependencies | ||
| if (dependencyTypes.contains('direct-main') || | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This outer |
||
| dependencyTypes.contains('direct-dev')) { | ||
| if (workspaceDependencies.contains(dependency.name)) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| // For transitive and direct-overridden, still use pubspec.lock types | ||
| final dependencyType = dependency.type; | ||
| if (dependencyTypes.contains('transitive') && | ||
| dependencyType == PubspecLockPackageDependencyType.transitive) { | ||
| return true; | ||
| } | ||
| if (dependencyTypes.contains('direct-overridden') && | ||
| dependencyType == | ||
| PubspecLockPackageDependencyType.directOverridden) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| // Non-workspace: use the original filtering logic | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Subtle behavioral divergence worth calling out in a comment or doc: in non-workspace mode, |
||
| final dependencyType = dependency.type; | ||
| return (dependencyTypes.contains('direct-main') && | ||
| dependencyType == PubspecLockPackageDependencyType.directMain) || | ||
|
|
@@ -531,6 +569,60 @@ extension on List<Object> { | |
| } | ||
| } | ||
|
|
||
| /// Collects dependencies from a workspace. | ||
| /// | ||
| /// If [pubspec] is not a workspace root, returns `null`. | ||
| /// Otherwise, returns a set of dependency names collected from all workspace | ||
| /// members based on the requested [dependencyTypes]. | ||
| Set<String>? _collectWorkspaceDependencies({ | ||
| required Pubspec? pubspec, | ||
| required Directory targetDirectory, | ||
| required List<String> dependencyTypes, | ||
| }) { | ||
| if (pubspec == null || !pubspec.isWorkspaceRoot) return null; | ||
|
|
||
| final dependencies = <String>{}; | ||
|
|
||
| // Collect dependencies from the root pubspec itself | ||
| if (dependencyTypes.contains('direct-main')) { | ||
| dependencies.addAll(pubspec.dependencies.keys); | ||
| } | ||
| if (dependencyTypes.contains('direct-dev')) { | ||
| dependencies.addAll(pubspec.devDependencies.keys); | ||
| } | ||
|
|
||
| // Collect dependencies from workspace members | ||
| final members = resolveWorkspaceMembers(pubspec, targetDirectory); | ||
| for (final memberDirectory in members) { | ||
| final memberPubspecFile = File( | ||
| path.join(memberDirectory.path, pubspecBasename), | ||
| ); | ||
| final memberPubspec = tryParsePubspec(memberPubspecFile); | ||
| if (memberPubspec == null) continue; | ||
|
|
||
| if (dependencyTypes.contains('direct-main')) { | ||
| dependencies.addAll(memberPubspec.dependencies.keys); | ||
| } | ||
| if (dependencyTypes.contains('direct-dev')) { | ||
| dependencies.addAll(memberPubspec.devDependencies.keys); | ||
| } | ||
|
|
||
| // Handle nested workspaces recursively | ||
| if (memberPubspec.isWorkspaceRoot) { | ||
| final nestedDeps = _collectWorkspaceDependencies( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no cycle protection here. If a member's |
||
| pubspec: memberPubspec, | ||
| targetDirectory: memberDirectory, | ||
| dependencyTypes: dependencyTypes, | ||
| ); | ||
| if (nestedDeps != null) { | ||
| dependencies.addAll(nestedDeps); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return dependencies; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This helper is purely about workspace traversal and doesn't depend on anything in this command — it would fit better in |
||
|
|
||
| /// Format type for listing all licenses via --reporter option. | ||
| enum ReporterOutputFormat { | ||
| /// List all licenses separated by a dash. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| /// Workspace-aware helpers around [package:pubspec_parse]. | ||
| /// | ||
| /// Parsing is delegated to [Pubspec.parse]; this library only adds the | ||
| /// filesystem and glob expansion helpers needed by | ||
| /// `packages check licenses` to walk workspace members. | ||
| library; | ||
|
|
||
| import 'dart:io'; | ||
|
|
||
| import 'package:glob/glob.dart'; | ||
| import 'package:glob/list_local_fs.dart'; | ||
| import 'package:path/path.dart' as path; | ||
| import 'package:pubspec_parse/pubspec_parse.dart'; | ||
|
|
||
| export 'package:pubspec_parse/pubspec_parse.dart' show Pubspec; | ||
|
|
||
| /// The basename of the pubspec file. | ||
| const pubspecBasename = 'pubspec.yaml'; | ||
|
|
||
| const _workspaceResolution = 'workspace'; | ||
|
|
||
| /// Workspace-related conveniences on top of [Pubspec]. | ||
| extension PubspecWorkspace on Pubspec { | ||
| /// Whether this pubspec is a workspace root. | ||
| bool get isWorkspaceRoot => workspace != null; | ||
|
|
||
| /// Whether this pubspec is a workspace member. | ||
| bool get isWorkspaceMember => resolution == _workspaceResolution; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| /// Attempts to read and parse a [Pubspec] from [file]. | ||
| /// | ||
| /// Returns `null` when the file does not exist or cannot be parsed. | ||
| Pubspec? tryParsePubspec(File file) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. additional to the bot's comments, I'd say all the pubspec related methods can be in the existing PubspecWorkspace extension since we're just extending |
||
| if (!file.existsSync()) return null; | ||
| try { | ||
| return Pubspec.parse( | ||
| file.readAsStringSync(), | ||
| sourceUrl: file.uri, | ||
| lenient: true, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| ); | ||
| } on Exception { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /// Resolves the workspace members declared by [pubspec], expanding any glob | ||
| /// patterns relative to [root]. | ||
| /// | ||
| /// Returns an empty list when [pubspec] is not a workspace root. | ||
| List<Directory> resolveWorkspaceMembers(Pubspec pubspec, Directory root) { | ||
| final patterns = pubspec.workspace; | ||
| if (patterns == null) return const []; | ||
|
|
||
| final members = <Directory>[]; | ||
| for (final pattern in patterns) { | ||
| if (_isGlobPattern(pattern)) { | ||
| final matches = Glob(pattern).listSync(root: root.path); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Workspace patterns under pub use POSIX-style separators, but on Windows users may write |
||
| for (final match in matches) { | ||
| if (match is Directory) { | ||
| final pubspecFile = File(path.join(match.path, pubspecBasename)); | ||
| if (pubspecFile.existsSync()) { | ||
| members.add(Directory(match.path)); | ||
| } | ||
| } else if (match is File && | ||
| path.basename(match.path) == pubspecBasename) { | ||
| members.add(Directory(match.parent.path)); | ||
| } | ||
| } | ||
| } else { | ||
| final memberDir = Directory(path.join(root.path, pattern)); | ||
| if (memberDir.existsSync()) members.add(memberDir); | ||
| } | ||
| } | ||
|
|
||
| return members; | ||
| } | ||
|
|
||
| bool _isGlobPattern(String pattern) { | ||
| return pattern.contains('*') || | ||
| pattern.contains('?') || | ||
| pattern.contains('[') || | ||
| pattern.contains('{'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hand-rolling glob detection is fragile — e.g. negation patterns starting with |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: you've already computed
targetPathandtargetDirectory.path.join(targetDirectory.path, pubspecBasename)would avoid recomputing the join fromtargetPathand keeps the single source of truth (targetDirectory). Same comment applies topubspecLockFileabove.