use_packwerk 0.50.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +62 -0
- data/bin/use_packwerk +5 -0
- data/lib/use_packwerk/cli.rb +71 -0
- data/lib/use_packwerk/code_ownership_post_processor.rb +58 -0
- data/lib/use_packwerk/configuration.rb +49 -0
- data/lib/use_packwerk/logging.rb +33 -0
- data/lib/use_packwerk/per_file_processor_interface.rb +19 -0
- data/lib/use_packwerk/private/file_move_operation.rb +77 -0
- data/lib/use_packwerk/private/pack_relationship_analyzer.rb +219 -0
- data/lib/use_packwerk/private.rb +446 -0
- data/lib/use_packwerk/rubocop_post_processor.rb +22 -0
- data/lib/use_packwerk.rb +149 -0
- metadata +257 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f4f9377346178fe79c699c9493993c0fb270170dcb200e1e443df752b6d7d32c
|
4
|
+
data.tar.gz: e9bf3060d53092534f2193fe93d68188cf750b2bbb824d60dc2c6ec6205c51f5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0a4ef1cc30ac3db3613a1cb5d294a9bdd5afa2bdf1c70cbfb47db4b592c98d01130d5a3722ac3ebad206c0d36a906016148304cbe84f6fa193186bd4d734a213
|
7
|
+
data.tar.gz: 0430dfe13a6d016ea79b499ad0c28a53fdfa02dbd211c3777a32aae6f8b96b7bc6694c4e03d84add447505080a6d428faac4fffa6d3d861049ba09ed5edc6113
|
data/README.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# UsePackwerk
|
2
|
+
|
3
|
+
UsePackwerk is a gem that helps in creating and maintaining packs. It exists to help perform some basic operations needed for pack setup and configuration. It provides a basic ruby file packager utility for [`packwerk`](https://github.com/Shopify/packwerk/). It assumes you are using [`stimpack`](https://github.com/rubyatscale/stimpack) to organize your packages.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
### General Help
|
7
|
+
`bin/use_packwerk --help`
|
8
|
+
|
9
|
+
### Pack Creation
|
10
|
+
`bin/create_pack -n packs/your_pack_name_here`
|
11
|
+
|
12
|
+
### Moving files to packs
|
13
|
+
`bin/move_to_pack -n packs/your_pack_name_here -f path/to/file.rb,path/to/directory`
|
14
|
+
This is used for moving files into a pack (the pack must already exist).
|
15
|
+
Note this works for moving files to packs from the monolith or from other packs
|
16
|
+
|
17
|
+
Make sure there are no spaces between the comma-separated list of paths of directories.
|
18
|
+
|
19
|
+
### Moving a file to public API
|
20
|
+
`bin/make_public -f path/to/file.rb,path/to/directory`
|
21
|
+
This moves a file or directory to public API (that is -- the `app/public` folder).
|
22
|
+
|
23
|
+
Make sure there are no spaces between the comma-separated list of paths of directories.
|
24
|
+
|
25
|
+
### Listing top privacy violations
|
26
|
+
`bin/list_top_privacy_violations -n packs/my_pack`
|
27
|
+
Want to create interfaces? Not sure how your pack's code is being used?
|
28
|
+
|
29
|
+
You can use this command to list the top privacy violations.
|
30
|
+
|
31
|
+
If no pack name is passed in, this will list out violations across all packs.
|
32
|
+
|
33
|
+
### Listing top dependency violations
|
34
|
+
`bin/list_top_dependency_violations -n packs/my_pack`
|
35
|
+
Want to see who is depending on you? Not sure how your pack's code is being used in an unstated way
|
36
|
+
|
37
|
+
You can use this command to list the top dependency violations.
|
38
|
+
|
39
|
+
If no pack name is passed in, this will list out violations across all packs.
|
40
|
+
|
41
|
+
### Adding a dependency
|
42
|
+
`bin/add_pack_dependency -n packs/my_pack -d packs/dependency_pack_name`
|
43
|
+
|
44
|
+
This can be used to quickly modify a `package.yml` file and add a dependency. It also cleans up the list of dependencies to sort the list and remove redundant entries.
|
45
|
+
|
46
|
+
## Discussions, Issues, Questions, and More
|
47
|
+
To keep things organized, here are some recommended homes:
|
48
|
+
|
49
|
+
### Issues:
|
50
|
+
https://github.com/Gusto/use_packwerk/issues
|
51
|
+
|
52
|
+
### Questions:
|
53
|
+
https://github.com/Gusto/use_packwerk/discussions/categories/q-a
|
54
|
+
|
55
|
+
### General discussions:
|
56
|
+
https://github.com/Gusto/use_packwerk/discussions/categories/general
|
57
|
+
|
58
|
+
### Ideas, new features, requests for change:
|
59
|
+
https://github.com/Gusto/use_packwerk/discussions/categories/ideas
|
60
|
+
|
61
|
+
### Showcasing your work:
|
62
|
+
https://github.com/Gusto/use_packwerk/discussions/categories/show-and-tell
|
data/bin/use_packwerk
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
|
5
|
+
module UsePackwerk
|
6
|
+
class CLI < Thor
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
desc "create packs/your_pack", "Create pack with name packs/your_pack"
|
10
|
+
sig { params(pack_name: String).void }
|
11
|
+
def create(pack_name)
|
12
|
+
UsePackwerk.create_pack!(pack_name: pack_name)
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "add_dependency packs/from_pack packs/to_pack", "Add packs/to_pack to packs/from_pack/package.yml list of dependencies"
|
16
|
+
long_desc <<~LONG_DESC
|
17
|
+
Use this to add a dependency between packs.
|
18
|
+
|
19
|
+
When you use bin/use_packwerk add_dependency packs/from_pack packs/to_pack, this command will
|
20
|
+
modify packs/from_pack/package.yml's list of dependencies and add packs/to_pack.
|
21
|
+
|
22
|
+
This command will also sort the list and make it unique.
|
23
|
+
LONG_DESC
|
24
|
+
sig { params(from_pack: String, to_pack: String).void }
|
25
|
+
def add_dependency(from_pack, to_pack)
|
26
|
+
UsePackwerk.add_dependency!(
|
27
|
+
pack_name: from_pack,
|
28
|
+
dependency_name: to_pack
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "list_top_dependency_violations packs/your_pack", "List the top dependency violations of packs/your_pack"
|
33
|
+
option :limit, type: :numeric, default: 10, aliases: :l, banner: 'Specify the limit of constants to analyze'
|
34
|
+
sig { params(pack_name: String).void }
|
35
|
+
def list_top_dependency_violations(pack_name)
|
36
|
+
UsePackwerk.list_top_dependency_violations(
|
37
|
+
pack_name: pack_name,
|
38
|
+
limit: options[:limit]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
desc "list_top_privacy_violations packs/your_pack", "List the top privacy violations of packs/your_pack"
|
43
|
+
option :limit, type: :numeric, default: 10, aliases: :l, banner: 'Specify the limit of constants to analyze'
|
44
|
+
sig { params(pack_name: String).void }
|
45
|
+
def list_top_privacy_violations(pack_name)
|
46
|
+
UsePackwerk.list_top_privacy_violations(
|
47
|
+
pack_name: pack_name,
|
48
|
+
limit: options[:limit]
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
desc "make_public path/to/file.rb path/to/directory", "Pass in a space-separated list of file or directory paths to make public"
|
53
|
+
sig { params(paths: String).void }
|
54
|
+
def make_public(*paths)
|
55
|
+
UsePackwerk.make_public!(
|
56
|
+
paths_relative_to_root: paths,
|
57
|
+
per_file_processors: [UsePackwerk::RubocopPostProcessor.new, UsePackwerk::CodeOwnershipPostProcessor.new],
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
desc "move packs/destination_pack path/to/file.rb path/to/directory", "Pass in a destination pack and a space-separated list of file or directory paths to move to the destination pack"
|
62
|
+
sig { params(pack_name: String, paths: String).void }
|
63
|
+
def move(pack_name, *paths)
|
64
|
+
UsePackwerk.move_to_pack!(
|
65
|
+
pack_name: pack_name,
|
66
|
+
paths_relative_to_root: paths,
|
67
|
+
per_file_processors: [UsePackwerk::RubocopPostProcessor.new, UsePackwerk::CodeOwnershipPostProcessor.new],
|
68
|
+
)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module UsePackwerk
|
4
|
+
class CodeOwnershipPostProcessor
|
5
|
+
include PerFileProcessorInterface
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { void }
|
9
|
+
def initialize
|
10
|
+
@teams = T.let([], T::Array[String])
|
11
|
+
@did_move_files = T.let(false, T::Boolean)
|
12
|
+
end
|
13
|
+
|
14
|
+
sig { override.params(file_move_operation: Private::FileMoveOperation).void }
|
15
|
+
def before_move_file!(file_move_operation)
|
16
|
+
relative_path_to_origin = file_move_operation.origin_pathname
|
17
|
+
relative_path_to_destination = file_move_operation.destination_pathname
|
18
|
+
|
19
|
+
code_owners_allow_list_file = Pathname.new('config/code_ownership.yml')
|
20
|
+
|
21
|
+
if code_owners_allow_list_file.exist?
|
22
|
+
UsePackwerk.replace_in_file(
|
23
|
+
file: code_owners_allow_list_file.to_s,
|
24
|
+
find: relative_path_to_origin,
|
25
|
+
replace_with: relative_path_to_destination,
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
team = CodeOwnership.for_file(relative_path_to_origin.to_s)
|
30
|
+
|
31
|
+
if team
|
32
|
+
@teams << team.name
|
33
|
+
else
|
34
|
+
@teams << 'Unknown'
|
35
|
+
end
|
36
|
+
|
37
|
+
if !CodeOwnership.for_package(file_move_operation.destination_pack).nil?
|
38
|
+
CodeOwnership.remove_file_annotation!(relative_path_to_origin.to_s)
|
39
|
+
@did_move_files = true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
sig { void }
|
44
|
+
def print_final_message!
|
45
|
+
if @teams.any?
|
46
|
+
Logging.section('Code Ownership') do
|
47
|
+
Logging.print('This section contains info about the current ownership distribution of the moved files.')
|
48
|
+
@teams.group_by { |team| team }.sort_by { |team, instances| -instances.count }.each do |team, instances|
|
49
|
+
Logging.print " #{team} - #{instances.count} files"
|
50
|
+
end
|
51
|
+
if @did_move_files
|
52
|
+
Logging.print "Since the destination package has package-based ownership, file-annotations were removed from moved files."
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module UsePackwerk
|
4
|
+
class Configuration
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig { params(enforce_dependencies: T::Boolean).void }
|
8
|
+
attr_writer :enforce_dependencies
|
9
|
+
|
10
|
+
sig { params(documentation_link: String).void }
|
11
|
+
attr_writer :documentation_link
|
12
|
+
|
13
|
+
sig { void }
|
14
|
+
def initialize
|
15
|
+
@enforce_dependencies = T.let(@enforce_dependencies, T.nilable(T::Boolean))
|
16
|
+
@documentation_link = T.let(documentation_link, T.nilable(String) )
|
17
|
+
end
|
18
|
+
|
19
|
+
sig { returns(T::Boolean) }
|
20
|
+
def enforce_dependencies
|
21
|
+
if !@enforce_dependencies.nil?
|
22
|
+
@enforce_dependencies
|
23
|
+
else
|
24
|
+
true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Configure a link to show up for users who are looking for more info
|
29
|
+
sig { returns(String) }
|
30
|
+
def documentation_link
|
31
|
+
"https://go/packwerk"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
extend T::Sig
|
37
|
+
|
38
|
+
sig { returns(Configuration) }
|
39
|
+
def config
|
40
|
+
@config = T.let(@config, T.nilable(Configuration))
|
41
|
+
@config ||= Configuration.new
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { params(blk: T.proc.params(arg0: Configuration).void).void }
|
45
|
+
def configure(&blk) # rubocop:disable Lint/UnusedMethodArgument
|
46
|
+
yield(config)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
require 'colorized_string'
|
4
|
+
|
5
|
+
module UsePackwerk
|
6
|
+
module Logging
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { params(title: String, block: T.proc.void).void }
|
10
|
+
def self.section(title, &block)
|
11
|
+
print_divider
|
12
|
+
puts ColorizedString.new("#{title}").green.bold
|
13
|
+
puts "\n"
|
14
|
+
yield
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { params(text: String).void }
|
18
|
+
def self.print_bold_green(text)
|
19
|
+
puts ColorizedString.new(text).green.bold
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { params(text: String).void }
|
23
|
+
def self.print(text)
|
24
|
+
puts text
|
25
|
+
end
|
26
|
+
|
27
|
+
sig { void }
|
28
|
+
def self.print_divider
|
29
|
+
puts '=' * 100
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module UsePackwerk
|
4
|
+
module PerFileProcessorInterface
|
5
|
+
extend T::Sig
|
6
|
+
extend T::Helpers
|
7
|
+
|
8
|
+
abstract!
|
9
|
+
|
10
|
+
sig { abstract.params(file_move_operation: Private::FileMoveOperation).void }
|
11
|
+
def before_move_file!(file_move_operation)
|
12
|
+
end
|
13
|
+
|
14
|
+
sig { void }
|
15
|
+
def print_final_message!
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module UsePackwerk
|
4
|
+
module Private
|
5
|
+
class FileMoveOperation < T::Struct
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
const :origin_pathname, Pathname
|
9
|
+
const :destination_pathname, Pathname
|
10
|
+
const :destination_pack, ParsePackwerk::Package
|
11
|
+
|
12
|
+
sig { params(origin_pathname: Pathname, new_package_root: Pathname).returns(Pathname) }
|
13
|
+
def self.destination_pathname_for_package_move(origin_pathname, new_package_root)
|
14
|
+
parts = origin_pathname.to_s.split('/')
|
15
|
+
toplevel_directory = parts[0]
|
16
|
+
|
17
|
+
case toplevel_directory.to_s
|
18
|
+
# This allows us to move files from monolith to packs
|
19
|
+
when 'app', 'spec', 'lib'
|
20
|
+
new_package_root.join(origin_pathname).cleanpath
|
21
|
+
# This allows us to move files from packs to packs
|
22
|
+
when *PERMITTED_PACK_LOCATIONS # parts looks like ['packs', 'organisms', 'app', 'services', 'bird_like', 'eagle.rb']
|
23
|
+
new_package_root.join(T.must(parts[2..]).join('/')).cleanpath
|
24
|
+
else
|
25
|
+
raise StandardError.new("Don't know how to find destination path for #{origin_pathname.inspect}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
sig { params(origin_pathname: Pathname).returns(Pathname) }
|
30
|
+
def self.destination_pathname_for_new_public_api(origin_pathname)
|
31
|
+
parts = origin_pathname.to_s.split('/')
|
32
|
+
toplevel_directory = Pathname.new(parts[0])
|
33
|
+
|
34
|
+
case toplevel_directory.to_s
|
35
|
+
# This allows us to make API in the monolith public
|
36
|
+
when 'app', 'spec'
|
37
|
+
toplevel_directory.join('public').join(T.must(parts[2..]).join('/')).cleanpath
|
38
|
+
# This allows us to make API in existing packs public
|
39
|
+
when *PERMITTED_PACK_LOCATIONS # parts looks like ['packs', 'organisms', 'app', 'services', 'bird_like', 'eagle.rb']
|
40
|
+
pack_name = Pathname.new(parts[1])
|
41
|
+
toplevel_directory.join(pack_name).join('app/public').join(T.must(parts[4..]).join('/')).cleanpath
|
42
|
+
else
|
43
|
+
raise StandardError.new("Don't know how to find destination path for #{origin_pathname.inspect}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { returns(FileMoveOperation) }
|
48
|
+
def spec_file_move_operation
|
49
|
+
# This could probably be implemented by some "strategy pattern" where different extension types are handled by different helpers
|
50
|
+
# Such a thing could also include, for example, when moving a controller, moving its ERB view too.
|
51
|
+
if origin_pathname.extname == '.rake'
|
52
|
+
new_origin_pathname = origin_pathname.sub('/lib/', '/spec/lib/').sub(/^lib\//, 'spec/lib/').sub('.rake', '_spec.rb')
|
53
|
+
new_destination_pathname = destination_pathname.sub('/lib/', '/spec/lib/').sub(/^lib\//, 'spec/lib/').sub('.rake', '_spec.rb')
|
54
|
+
else
|
55
|
+
new_origin_pathname = origin_pathname.sub('/app/', '/spec/').sub(/^app\//, 'spec/').sub('.rb', '_spec.rb')
|
56
|
+
new_destination_pathname = destination_pathname.sub('/app/', '/spec/').sub(/^app\//, 'spec/').sub('.rb', '_spec.rb')
|
57
|
+
end
|
58
|
+
FileMoveOperation.new(
|
59
|
+
origin_pathname: new_origin_pathname,
|
60
|
+
destination_pathname: new_destination_pathname,
|
61
|
+
destination_pack: destination_pack,
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
sig { params(path: Pathname).returns(FileMoveOperation) }
|
68
|
+
def relative_to(path)
|
69
|
+
FileMoveOperation.new(
|
70
|
+
origin_pathname: origin_pathname.relative_path_from(path),
|
71
|
+
destination_pathname: destination_pathname.relative_path_from(path),
|
72
|
+
destination_pack: destination_pack,
|
73
|
+
)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module UsePackwerk
|
4
|
+
module Private
|
5
|
+
module PackRelationshipAnalyzer
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig do
|
9
|
+
params(
|
10
|
+
pack_name: T.nilable(String),
|
11
|
+
limit: Integer,
|
12
|
+
).void
|
13
|
+
end
|
14
|
+
def self.list_top_privacy_violations(pack_name, limit)
|
15
|
+
all_packages = ParsePackwerk.all
|
16
|
+
if !pack_name.nil?
|
17
|
+
pack_name = Private.clean_pack_name(pack_name)
|
18
|
+
package = all_packages.find { |package| package.name == pack_name }
|
19
|
+
if package.nil?
|
20
|
+
raise StandardError.new("Can not find package with name #{pack_name}. Make sure the argument is of the form `packs/my_pack/`")
|
21
|
+
end
|
22
|
+
|
23
|
+
pack_specific_content = <<~PACK_CONTENT
|
24
|
+
You are listing top #{limit} privacy violations for #{pack_name}. See #{UsePackwerk.config.documentation_link} for other utilities!
|
25
|
+
Pass in a limit to display more or less, e.g. `bin/list_top_privacy_violations -n #{pack_name} -l 1000`
|
26
|
+
|
27
|
+
This script is intended to help you find which of YOUR pack's private classes, constants, or modules other packs are using the most.
|
28
|
+
Anything not in #{pack_name}/app/public is considered private API.
|
29
|
+
PACK_CONTENT
|
30
|
+
|
31
|
+
to_package_names = [pack_name]
|
32
|
+
else
|
33
|
+
pack_specific_content = <<~PACK_CONTENT
|
34
|
+
You are listing top #{limit} privacy violations for all packs. See #{UsePackwerk.config.documentation_link} for other utilities!
|
35
|
+
Pass in a limit to display more or less, e.g. `bin/list_top_privacy_violations -n #{pack_name} -l 1000`
|
36
|
+
|
37
|
+
This script is intended to help you find which of YOUR pack's private classes, constants, or modules other packs are using the most.
|
38
|
+
Anything not in pack_name/app/public is considered private API.
|
39
|
+
PACK_CONTENT
|
40
|
+
|
41
|
+
to_package_names = all_packages.map(&:name)
|
42
|
+
end
|
43
|
+
|
44
|
+
violations_by_count = {}
|
45
|
+
total_pack_violation_count = 0
|
46
|
+
|
47
|
+
Logging.section('👋 Hi there') do
|
48
|
+
intro = <<~INTRO
|
49
|
+
#{pack_specific_content}
|
50
|
+
|
51
|
+
When using this script, ask yourself some questions like:
|
52
|
+
- What do I want to support?
|
53
|
+
- What do I *not* want to support?
|
54
|
+
- What is considered simply an implementation detail, and what is essential to the behavior of my pack?
|
55
|
+
- What is a simple, minimialistic API for clients to engage with the behavior of your pack?
|
56
|
+
- How do I ensure my public API is not coupled to specific client's use cases?
|
57
|
+
|
58
|
+
Looking at privacy violations can help guide the development of your public API, but it is just the beginning!
|
59
|
+
|
60
|
+
The script will output in the following format:
|
61
|
+
|
62
|
+
SomeConstant # This is the name of a class, constant, or module defined in your pack, outside of app/public
|
63
|
+
- Total Count: 5 # This is the total number of uses of this outside your pack
|
64
|
+
- By package: # This is a breakdown of the use of this constant by other packages
|
65
|
+
# This is the number of files in this pack that this constant is used.
|
66
|
+
# Check `packs/other_pack_a/deprecated_references.yml` under the '#{pack_name}'.'SomeConstant' key to see where this constant is used
|
67
|
+
- packs/other_pack_a: 3
|
68
|
+
- packs/other_pack_b: 2
|
69
|
+
SomeClass # This is the second most violated class, constant, or module defined in your pack
|
70
|
+
- Total Count: 2
|
71
|
+
- By package:
|
72
|
+
- packs/other_pack_a: 1
|
73
|
+
- packs/other_pack_b: 1
|
74
|
+
|
75
|
+
Lastly, remember you can use `bin/make_public -f #{pack_name}/path/to/file.rb` to make your class, constant, or module public API.
|
76
|
+
INTRO
|
77
|
+
Logging.print_bold_green(intro)
|
78
|
+
end
|
79
|
+
|
80
|
+
# TODO: This is a copy of the implementation below. We may want to refactor out this implementation detail before making changes that apply to both.
|
81
|
+
all_packages.each do |client_package|
|
82
|
+
PackageProtections::ProtectedPackage.from(client_package).violations.select(&:privacy?).each do |violation|
|
83
|
+
next unless to_package_names.include?(violation.to_package_name)
|
84
|
+
if pack_name.nil?
|
85
|
+
violated_symbol = "#{violation.class_name} (#{violation.to_package_name})"
|
86
|
+
else
|
87
|
+
violated_symbol = "#{violation.class_name}"
|
88
|
+
end
|
89
|
+
violations_by_count[violated_symbol] ||= {}
|
90
|
+
violations_by_count[violated_symbol][:total_count] ||= 0
|
91
|
+
violations_by_count[violated_symbol][:by_package] ||= {}
|
92
|
+
violations_by_count[violated_symbol][:by_package][client_package.name] ||= 0
|
93
|
+
violations_by_count[violated_symbol][:total_count] += violation.files.count
|
94
|
+
violations_by_count[violated_symbol][:by_package][client_package.name] += violation.files.count
|
95
|
+
total_pack_violation_count += violation.files.count
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
Logging.print("Total Count: #{total_pack_violation_count}")
|
100
|
+
|
101
|
+
sorted_violations = violations_by_count.sort_by { |violated_symbol, count_info| [-count_info[:total_count], violated_symbol] }
|
102
|
+
sorted_violations.first(limit).each do |violated_symbol, count_info|
|
103
|
+
percentage_of_total = (count_info[:total_count] * 100.0 / total_pack_violation_count).round(2)
|
104
|
+
Logging.print(violated_symbol)
|
105
|
+
Logging.print(" - Total Count: #{count_info[:total_count]} (#{percentage_of_total}% of total)")
|
106
|
+
|
107
|
+
Logging.print(" - By package:")
|
108
|
+
count_info[:by_package].sort_by{ |client_package_name, count| [-count, client_package_name] }.each do |client_package_name, count|
|
109
|
+
Logging.print(" - #{client_package_name}: #{count}")
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
sig do
|
115
|
+
params(
|
116
|
+
pack_name: T.nilable(String),
|
117
|
+
limit: Integer
|
118
|
+
).void
|
119
|
+
end
|
120
|
+
def self.list_top_dependency_violations(pack_name, limit)
|
121
|
+
all_packages = ParsePackwerk.all
|
122
|
+
|
123
|
+
if !pack_name.nil?
|
124
|
+
pack_name = Private.clean_pack_name(pack_name)
|
125
|
+
package = all_packages.find { |package| package.name == pack_name }
|
126
|
+
if package.nil?
|
127
|
+
raise StandardError.new("Can not find package with name #{pack_name}. Make sure the argument is of the form `packs/my_pack/`")
|
128
|
+
end
|
129
|
+
|
130
|
+
pack_specific_content = <<~PACK_CONTENT
|
131
|
+
You are listing top #{limit} dependency violations for #{pack_name}. See #{UsePackwerk.config.documentation_link} for other utilities!
|
132
|
+
Pass in a limit to display more or less, e.g. `bin/list_top_dependency_violations -n #{pack_name} -l 1000`
|
133
|
+
|
134
|
+
This script is intended to help you find which of YOUR pack's private classes, constants, or modules other packs are using the most.
|
135
|
+
Anything not in #{pack_name}/app/public is considered private API.
|
136
|
+
PACK_CONTENT
|
137
|
+
|
138
|
+
to_package_names = [pack_name]
|
139
|
+
else
|
140
|
+
pack_specific_content = <<~PACK_CONTENT
|
141
|
+
You are listing top #{limit} dependency violations for all packs. See #{UsePackwerk.config.documentation_link} for other utilities!
|
142
|
+
Pass in a limit to display more or less, e.g. `bin/list_top_dependency_violations -n #{pack_name} -l 1000`
|
143
|
+
|
144
|
+
This script is intended to help you find which of YOUR pack's private classes, constants, or modules other packs are using the most.
|
145
|
+
Anything not in pack_name/app/public is considered private API.
|
146
|
+
PACK_CONTENT
|
147
|
+
|
148
|
+
to_package_names = all_packages.map(&:name)
|
149
|
+
end
|
150
|
+
|
151
|
+
violations_by_count = {}
|
152
|
+
total_pack_violation_count = 0
|
153
|
+
|
154
|
+
Logging.section('👋 Hi there') do
|
155
|
+
intro = <<~INTRO
|
156
|
+
#{pack_specific_content}
|
157
|
+
|
158
|
+
When using this script, ask yourself some questions like:
|
159
|
+
- What do I want to support?
|
160
|
+
- What do I *not* want to support?
|
161
|
+
- Which direction should a dependency go?
|
162
|
+
- What packs should depend on you, and what packs should not depend on you?
|
163
|
+
- Would it be simpler if other packs only depended on interfaces to your pack rather than implementation?
|
164
|
+
|
165
|
+
Looking at dependency violations can help guide the development of your public API, but it is just the beginning!
|
166
|
+
|
167
|
+
The script will output in the following format:
|
168
|
+
|
169
|
+
SomeConstant # This is the name of a class, constant, or module defined in your pack, outside of app/public
|
170
|
+
- Total Count: 5 # This is the total number of unstated uses of this outside your pack
|
171
|
+
- By package: # This is a breakdown of the use of this constant by other packages
|
172
|
+
# This is the number of files in this pack that this constant is used.
|
173
|
+
# Check `packs/other_pack_a/deprecated_references.yml` under the '#{pack_name}'.'SomeConstant' key to see where this constant is used
|
174
|
+
- packs/other_pack_a: 3
|
175
|
+
- packs/other_pack_b: 2
|
176
|
+
SomeClass # This is the second most violated class, constant, or module defined in your pack
|
177
|
+
- Total Count: 2
|
178
|
+
- By package:
|
179
|
+
- packs/other_pack_a: 1
|
180
|
+
- packs/other_pack_b: 1
|
181
|
+
INTRO
|
182
|
+
Logging.print_bold_green(intro)
|
183
|
+
end
|
184
|
+
|
185
|
+
# TODO: This is a copy of the implementation above. We may want to refactor out this implementation detail before making changes that apply to both.
|
186
|
+
all_packages.each do |client_package|
|
187
|
+
PackageProtections::ProtectedPackage.from(client_package).violations.select(&:dependency?).each do |violation|
|
188
|
+
next unless to_package_names.include?(violation.to_package_name)
|
189
|
+
if pack_name.nil?
|
190
|
+
violated_symbol = "#{violation.class_name} (#{violation.to_package_name})"
|
191
|
+
else
|
192
|
+
violated_symbol = "#{violation.class_name}"
|
193
|
+
end
|
194
|
+
violations_by_count[violated_symbol] ||= {}
|
195
|
+
violations_by_count[violated_symbol][:total_count] ||= 0
|
196
|
+
violations_by_count[violated_symbol][:by_package] ||= {}
|
197
|
+
violations_by_count[violated_symbol][:by_package][client_package.name] ||= 0
|
198
|
+
violations_by_count[violated_symbol][:total_count] += violation.files.count
|
199
|
+
violations_by_count[violated_symbol][:by_package][client_package.name] += violation.files.count
|
200
|
+
total_pack_violation_count += violation.files.count
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
Logging.print("Total Count: #{total_pack_violation_count}")
|
205
|
+
|
206
|
+
sorted_violations = violations_by_count.sort_by { |violated_symbol, count_info| [-count_info[:total_count], violated_symbol] }
|
207
|
+
sorted_violations.first(limit).each do |violated_symbol, count_info|
|
208
|
+
percentage_of_total = (count_info[:total_count] * 100.0 / total_pack_violation_count).round(2)
|
209
|
+
Logging.print(violated_symbol)
|
210
|
+
Logging.print(" - Total Count: #{count_info[:total_count]} (#{percentage_of_total}% of total)")
|
211
|
+
Logging.print(" - By package:")
|
212
|
+
count_info[:by_package].sort_by{ |client_package_name, count| [-count, client_package_name] }.each do |client_package_name, count|
|
213
|
+
Logging.print(" - #{client_package_name}: #{count}")
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|