packs 0.0.6 → 0.0.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +120 -4
  3. data/bin/packs +10 -0
  4. data/bin/rubocop +29 -0
  5. data/bin/tapioca +29 -0
  6. data/lib/packs/cli.rb +164 -0
  7. data/lib/packs/code_ownership_post_processor.rb +58 -0
  8. data/lib/packs/configuration.rb +61 -0
  9. data/lib/packs/default_user_event_logger.rb +7 -0
  10. data/lib/packs/logging.rb +37 -0
  11. data/lib/packs/per_file_processor_interface.rb +18 -0
  12. data/lib/packs/private/file_move_operation.rb +80 -0
  13. data/lib/packs/private/interactive_cli/file_selector.rb +26 -0
  14. data/lib/packs/private/interactive_cli/pack_selector.rb +55 -0
  15. data/lib/packs/private/interactive_cli/team_selector.rb +58 -0
  16. data/lib/packs/private/interactive_cli/use_cases/add_dependency.rb +30 -0
  17. data/lib/packs/private/interactive_cli/use_cases/check.rb +25 -0
  18. data/lib/packs/private/interactive_cli/use_cases/create.rb +27 -0
  19. data/lib/packs/private/interactive_cli/use_cases/get_info.rb +37 -0
  20. data/lib/packs/private/interactive_cli/use_cases/interface.rb +34 -0
  21. data/lib/packs/private/interactive_cli/use_cases/lint_package_todo_yml_files.rb +25 -0
  22. data/lib/packs/private/interactive_cli/use_cases/lint_package_yml_files.rb +26 -0
  23. data/lib/packs/private/interactive_cli/use_cases/make_public.rb +30 -0
  24. data/lib/packs/private/interactive_cli/use_cases/move.rb +32 -0
  25. data/lib/packs/private/interactive_cli/use_cases/move_to_parent.rb +31 -0
  26. data/lib/packs/private/interactive_cli/use_cases/query.rb +51 -0
  27. data/lib/packs/private/interactive_cli/use_cases/rename.rb +25 -0
  28. data/lib/packs/private/interactive_cli/use_cases/update.rb +25 -0
  29. data/lib/packs/private/interactive_cli/use_cases/validate.rb +25 -0
  30. data/lib/packs/private/interactive_cli/use_cases/visualize.rb +44 -0
  31. data/lib/packs/private/interactive_cli.rb +52 -0
  32. data/lib/packs/private/pack_relationship_analyzer.rb +135 -0
  33. data/lib/packs/private/packwerk_wrapper/offenses_aggregator_formatter.rb +44 -0
  34. data/lib/packs/private/packwerk_wrapper.rb +70 -0
  35. data/lib/packs/private.rb +606 -4
  36. data/lib/packs/rubocop_post_processor.rb +30 -0
  37. data/lib/packs/user_event_logger.rb +199 -0
  38. data/lib/packs.rb +233 -53
  39. metadata +225 -14
  40. data/lib/packs/pack.rb +0 -48
  41. data/lib/packs/private/configuration.rb +0 -36
  42. data/lib/packs/rspec/fixture_helper.rb +0 -33
  43. data/lib/packs/rspec/support.rb +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e86a9d19597b2dfda0fbec5013807777eff3e36d4fee72dfd445134a87f84d5
4
- data.tar.gz: 1720911eabcb3bba3ea855ad953778c7f25085b581a1a4f5f3bbedfcc6458c92
3
+ metadata.gz: b263107f82d4798835847954ad1e85d0e62312ca9a8064ebf5ba789f522e3b3f
4
+ data.tar.gz: 90613f4569258bbfce8bcf2ceab4ac1038fc7a05ba2610376253ac7c8bd6e264
5
5
  SHA512:
6
- metadata.gz: d96c70a719cb28a36dd7f3cc2e4a8c1b4fa0e827ef77a28a8aeab90e877a55208d12483c7e9465c605ffe997243ed1cbdaf0f1ebd593baf2f0da4ad489417272
7
- data.tar.gz: d047fa7748598d907a70fa314aefd6943130999992266c2b28699cd522f45ee661be3b7afd7674edb11cf35cad83b8ae3e3654d905673bb37b73ba8a0b68f0a2
6
+ metadata.gz: 7173bc07d22465d0dc96041adc9e22e704429b1b0cb0b480240e2af8307bf7a47fc43a94e00768903dc588db89f5cb4445d3198de59a0463447b53cf02010716
7
+ data.tar.gz: 3cbfbc39d85ffd72e0c9a595a9dbeb60596b60b55cf909188bbbfc15aa7a44c59a098beda1916773f7cc16334a43d6a029b479dbc6c9bdc789a45f0d4e90ab84
data/README.md CHANGED
@@ -1,16 +1,132 @@
1
1
  # packs
2
2
 
3
- Welcome to `packs`! `packs` are a simple ruby specification for an extensible packaging system to help modularize Ruby applications.
3
+ `packs` are a specification for an extensible packaging system to help modularize Ruby applications.
4
4
 
5
- A `pack` (also called `package`) is a folder of Ruby code with a `package.yml` at the root that is intended to represent a well-modularized domain, and the rest of the [rubyatscale](https://github.com/rubyatscale) ecosystem is intended to help make the boundaries between a pack and any other more clear.
5
+ A `pack` (short for `package`) is a folder of Ruby code with a `package.yml` at the root that is intended to represent a well-modularized domain.
6
+
7
+ This gem provides a development CLI, `bin/packs`, to make using `packs` easier.
8
+
9
+ # Configuration
10
+ By default, this library will look for `packs` in the folder `packs/*/package.yml` (as well as nested packs at `packs/*/*/package.yml`). To change where `packs` are located, create a `packs.yml` file:
11
+ ```yml
12
+ pack_paths:
13
+ - "{packs,utilities,deprecated}/*" # packs with multiple roots!
14
+ - "{packs,utilities,deprecated}/*/*" # nested packs!
15
+ - gems/* # gems can be packs too!
16
+ ```
17
+
18
+ # Ecosystem
19
+ The rest of the [rubyatscale](https://github.com/rubyatscale) ecosystem is intended to help make using packs and improving the boundaries between them more clear.
6
20
 
7
21
  Here are some example integrations with `packs`:
8
- - [`stimpack`](https://github.com/rubyatscale/stimpack) can be used to integrate `packs` into your `rails` application
22
+ - [`packs-specification`](https://github.com/rubyatscale/packs-specification) is a low-dependency gem that allows your production environment to query simple information about packs
23
+ - [`packs-rails`](https://github.com/rubyatscale/packs-rails) can be used to integrate `packs` into your `rails` application
9
24
  - [`rubocop-packs`](https://github.com/rubyatscale/rubocop-packs) contains cops to improve boundaries around `packs`
10
25
  - [`packwerk`](https://github.com/Shopify/packwerk) and [`packwerk-extensions`](https://github.com/rubyatscale/packwerk-extensions) help you describe and constrain your package graph in terms of dependencies between packs and pack public API
11
26
  - [`code_ownership`](https://github.com/rubyatscale/code_ownership) gives your application the capability to determine the owner of a pack
12
- - [`use_packs`](https://github.com/rubyatscale/use_packs) gives a CLI, `bin/packs`, that makes it easy to create new packs, move files between packs, and more.
13
27
  - [`pack_stats`](https://github.com/rubyatscale/pack_stats) makes it easy to send metrics about pack adoption and modularization to your favorite metrics provider, such as DataDog (which has built-in support).
14
28
 
15
29
  # How is a pack different from a gem?
16
30
  A ruby [`gem`](https://guides.rubygems.org/what-is-a-gem/) is the Ruby community solution for packaging and distributing Ruby code. A gem is a great place to start new projects, and a great end state for code that's been extracted from an existing codebase. `packs` are intended to help gradually modularize an application that has some conceptual boundaries, but is not yet ready to be factored into gems.
31
+
32
+ ## Usage
33
+ Make sure to run `bundle binstub packs` to generate `bin/packs` within your application.
34
+
35
+ ## CLI Documentation
36
+ ## Describe available commands or one specific command
37
+ `bin/packs help [COMMAND]`
38
+
39
+ ## Create pack with name packs/your_pack
40
+ `bin/packs create packs/your_pack`
41
+
42
+ ## Add packs/to_pack to packs/from_pack/package.yml list of dependencies
43
+ `bin/packs add_dependency packs/from_pack packs/to_pack`
44
+
45
+ Use this to add a dependency between packs.
46
+
47
+ When you use bin/packs add_dependency packs/from_pack packs/to_pack, this command will
48
+ modify packs/from_pack/package.yml's list of dependencies and add packs/to_pack.
49
+
50
+ This command will also sort the list and make it unique.
51
+
52
+ ## List the top dependency violations of packs/your_pack
53
+ `bin/packs list_top_dependency_violations packs/your_pack`
54
+
55
+ Want to see who is depending on you? Not sure how your pack's code is being used in an unstated way
56
+
57
+ You can use this command to list the top dependency violations.
58
+
59
+ If no pack name is passed in, this will list out violations across all packs.
60
+
61
+ ## List the top privacy violations of packs/your_pack
62
+ `bin/packs list_top_privacy_violations packs/your_pack`
63
+
64
+ Want to create interfaces? Not sure how your pack's code is being used?
65
+
66
+ You can use this command to list the top privacy violations.
67
+
68
+ If no pack name is passed in, this will list out violations across all packs.
69
+
70
+ ## Make files or directories public API
71
+ `bin/packs make_public path/to/file.rb path/to/directory`
72
+
73
+ This moves a file or directory to public API (that is -- the `app/public` folder).
74
+
75
+ Make sure there are no spaces between the comma-separated list of paths of directories.
76
+
77
+ ## Move files or directories from one pack to another
78
+ `bin/packs move packs/destination_pack path/to/file.rb path/to/directory`
79
+
80
+ This is used for moving files into a pack (the pack must already exist).
81
+ Note this works for moving files to packs from the monolith or from other packs
82
+
83
+ Make sure there are no spaces between the comma-separated list of paths of directories.
84
+
85
+ ## Lint `package_todo.yml` files to check for formatting issues
86
+ `bin/packs lint_package_todo_yml_files`
87
+
88
+ ## Lint `package.yml` files
89
+ `bin/packs lint_package_yml_files [ packs/my_pack packs/my_other_pack ]`
90
+
91
+ ## Run bin/packwerk validate (detects cycles)
92
+ `bin/packs validate`
93
+
94
+ ## Run bin/packwerk check
95
+ `bin/packs check [ packs/my_pack ]`
96
+
97
+ ## Run bin/packwerk update-todo
98
+ `bin/packs update`
99
+
100
+ ## Get info about size and violations for packs
101
+ `bin/packs get_info [ packs/my_pack packs/my_other_pack ]`
102
+
103
+ ## Visualize packs
104
+ `bin/packs visualize [ packs/my_pack packs/my_other_pack ]`
105
+
106
+ ## Rename a pack
107
+ `bin/packs rename`
108
+
109
+ ## Set packs/child_pack as a child of packs/parent_pack
110
+ `bin/packs move_to_parent packs/child_pack packs/parent_pack `
111
+
112
+
113
+ ## Releasing
114
+ Releases happen automatically through github actions once a version update is committed to `main`.
115
+
116
+ ## Discussions, Issues, Questions, and More
117
+ To keep things organized, here are some recommended homes:
118
+
119
+ ### Issues:
120
+ https://github.com/rubyatscale/packs/issues
121
+
122
+ ### Questions:
123
+ https://github.com/rubyatscale/packs/discussions/categories/q-a
124
+
125
+ ### General discussions:
126
+ https://github.com/rubyatscale/packs/discussions/categories/general
127
+
128
+ ### Ideas, new features, requests for change:
129
+ https://github.com/rubyatscale/packs/discussions/categories/ideas
130
+
131
+ ### Showcasing your work:
132
+ https://github.com/rubyatscale/packs/discussions/categories/show-and-tell
data/bin/packs ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # typed: strict
3
+
4
+ require_relative '../lib/packs'
5
+
6
+ if ARGV.empty?
7
+ Packs.start_interactive_mode!
8
+ else
9
+ Packs::CLI.start(ARGV)
10
+ end
data/bin/rubocop ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path('bundle', __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
+
29
+ load Gem.bin_path('rubocop', 'rubocop')
data/bin/tapioca ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'tapioca' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path('bundle', __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
+
29
+ load Gem.bin_path('tapioca', 'tapioca')
data/lib/packs/cli.rb ADDED
@@ -0,0 +1,164 @@
1
+ # typed: strict
2
+
3
+ require 'thor'
4
+
5
+ module Packs
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
+ Packs.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/packs 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
+ Packs.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
+ long_desc <<~LONG_DESC
34
+ Want to see who is depending on you? Not sure how your pack's code is being used in an unstated way
35
+
36
+ You can use this command to list the top dependency violations.
37
+
38
+ If no pack name is passed in, this will list out violations across all packs.
39
+ LONG_DESC
40
+ option :limit, type: :numeric, default: 10, aliases: :l, banner: 'Specify the limit of constants to analyze'
41
+ sig { params(pack_name: String).void }
42
+ def list_top_dependency_violations(pack_name)
43
+ Packs.list_top_dependency_violations(
44
+ pack_name: pack_name,
45
+ limit: options[:limit]
46
+ )
47
+ end
48
+
49
+ desc 'list_top_privacy_violations packs/your_pack', 'List the top privacy violations of packs/your_pack'
50
+ long_desc <<~LONG_DESC
51
+ Want to create interfaces? Not sure how your pack's code is being used?
52
+
53
+ You can use this command to list the top privacy violations.
54
+
55
+ If no pack name is passed in, this will list out violations across all packs.
56
+ LONG_DESC
57
+ option :limit, type: :numeric, default: 10, aliases: :l, banner: 'Specify the limit of constants to analyze'
58
+ sig { params(pack_name: String).void }
59
+ def list_top_privacy_violations(pack_name)
60
+ Packs.list_top_privacy_violations(
61
+ pack_name: pack_name,
62
+ limit: options[:limit]
63
+ )
64
+ end
65
+
66
+ desc 'make_public path/to/file.rb path/to/directory', 'Make files or directories public API'
67
+ long_desc <<~LONG_DESC
68
+ This moves a file or directory to public API (that is -- the `app/public` folder).
69
+
70
+ Make sure there are no spaces between the comma-separated list of paths of directories.
71
+ LONG_DESC
72
+ sig { params(paths: String).void }
73
+ def make_public(*paths)
74
+ Packs.make_public!(
75
+ paths_relative_to_root: paths,
76
+ per_file_processors: [Packs::RubocopPostProcessor.new, Packs::CodeOwnershipPostProcessor.new]
77
+ )
78
+ end
79
+
80
+ desc 'move packs/destination_pack path/to/file.rb path/to/directory', 'Move files or directories from one pack to another'
81
+ long_desc <<~LONG_DESC
82
+ This is used for moving files into a pack (the pack must already exist).
83
+ Note this works for moving files to packs from the monolith or from other packs
84
+
85
+ Make sure there are no spaces between the comma-separated list of paths of directories.
86
+ LONG_DESC
87
+ sig { params(pack_name: String, paths: String).void }
88
+ def move(pack_name, *paths)
89
+ Packs.move_to_pack!(
90
+ pack_name: pack_name,
91
+ paths_relative_to_root: paths,
92
+ per_file_processors: [Packs::RubocopPostProcessor.new, Packs::CodeOwnershipPostProcessor.new]
93
+ )
94
+ end
95
+
96
+ desc 'lint_package_todo_yml_files', 'Lint `package_todo.yml` files to check for formatting issues'
97
+ sig { void }
98
+ def lint_package_todo_yml_files
99
+ Packs.lint_package_todo_yml_files!
100
+ end
101
+
102
+ desc 'lint_package_yml_files [ packs/my_pack packs/my_other_pack ]', 'Lint `package.yml` files'
103
+ sig { params(pack_names: String).void }
104
+ def lint_package_yml_files(*pack_names)
105
+ Packs.lint_package_yml_files!(parse_pack_names(pack_names))
106
+ end
107
+
108
+ desc 'validate', 'Run bin/packwerk validate (detects cycles)'
109
+ sig { void }
110
+ def validate
111
+ system('bin/packwerk validate')
112
+ end
113
+
114
+ desc 'check [ packs/my_pack ]', 'Run bin/packwerk check'
115
+ sig { params(paths: String).void }
116
+ def check(*paths)
117
+ Packs.execute(['check', *paths])
118
+ end
119
+
120
+ desc 'update', 'Run bin/packwerk update-todo'
121
+ sig { void }
122
+ def update
123
+ system('bin/packwerk update-todo')
124
+ end
125
+
126
+ desc 'get_info [ packs/my_pack packs/my_other_pack ]', 'Get info about size and violations for packs'
127
+ sig { params(pack_names: String).void }
128
+ def get_info(*pack_names)
129
+ Private.get_info(packs: parse_pack_names(pack_names))
130
+ end
131
+
132
+ desc 'visualize [ packs/my_pack packs/my_other_pack ]', 'Visualize packs'
133
+ sig { params(pack_names: String).void }
134
+ def visualize(*pack_names)
135
+ Private.visualize(packs: parse_pack_names(pack_names))
136
+ end
137
+
138
+ desc 'rename', 'Rename a pack'
139
+ sig { void }
140
+ def rename
141
+ puts Private.rename_pack
142
+ end
143
+
144
+ desc 'move_to_parent packs/child_pack packs/parent_pack ', 'Set packs/child_pack as a child of packs/parent_pack'
145
+ sig { params(child_pack_name: String, parent_pack_name: String).void }
146
+ def move_to_parent(child_pack_name, parent_pack_name)
147
+ Packs.move_to_parent!(
148
+ parent_name: parent_pack_name,
149
+ pack_name: child_pack_name,
150
+ per_file_processors: [Packs::RubocopPostProcessor.new, Packs::CodeOwnershipPostProcessor.new]
151
+ )
152
+ end
153
+
154
+ private
155
+
156
+ # This is used by thor to know that these private methods are not intended to be CLI commands
157
+ no_commands do
158
+ sig { params(pack_names: T::Array[String]).returns(T::Array[Packs::Pack]) }
159
+ def parse_pack_names(pack_names)
160
+ pack_names.empty? ? Packs.all : pack_names.map { |p| Packs.find(p.gsub(%r{/$}, '')) }.compact
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,58 @@
1
+ # typed: strict
2
+
3
+ module Packs
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
+ return if !code_owners_allow_list_file.exist?
21
+
22
+ Packs.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
+
28
+ team = CodeOwnership.for_file(relative_path_to_origin.to_s)
29
+
30
+ if team
31
+ @teams << team.name
32
+ else
33
+ @teams << 'Unknown'
34
+ end
35
+
36
+ pack = Packs.find(file_move_operation.destination_pack.name)
37
+ if pack && !CodeOwnership.for_package(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 { params(file_move_operations: T::Array[Private::FileMoveOperation]).void }
44
+ def after_move_files!(file_move_operations)
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,61 @@
1
+ # typed: strict
2
+
3
+ require 'packs/user_event_logger'
4
+ require 'packs/default_user_event_logger'
5
+
6
+ module Packs
7
+ class Configuration
8
+ extend T::Sig
9
+
10
+ sig { params(enforce_dependencies: T::Boolean).void }
11
+ attr_writer :enforce_dependencies
12
+
13
+ sig { returns(UserEventLogger) }
14
+ attr_accessor :user_event_logger
15
+
16
+ OnPackageTodoLintFailure = T.type_alias do
17
+ T.proc.params(output: String).void
18
+ end
19
+
20
+ sig { returns(OnPackageTodoLintFailure) }
21
+ attr_accessor :on_package_todo_lint_failure
22
+
23
+ sig { void }
24
+ def initialize
25
+ @enforce_dependencies = T.let(default_enforce_dependencies, T::Boolean)
26
+ @user_event_logger = T.let(DefaultUserEventLogger.new, UserEventLogger)
27
+ @on_package_todo_lint_failure = T.let(->(output) {}, OnPackageTodoLintFailure)
28
+ end
29
+
30
+ sig { returns(T::Boolean) }
31
+ def enforce_dependencies
32
+ @enforce_dependencies
33
+ end
34
+
35
+ sig { void }
36
+ def bust_cache!
37
+ @enforce_dependencies = default_enforce_dependencies
38
+ end
39
+
40
+ sig { returns(T::Boolean) }
41
+ def default_enforce_dependencies
42
+ true
43
+ end
44
+ end
45
+
46
+ class << self
47
+ extend T::Sig
48
+
49
+ sig { returns(Configuration) }
50
+ def config
51
+ Private.load_client_configuration
52
+ @config = T.let(@config, T.nilable(Configuration))
53
+ @config ||= Configuration.new
54
+ end
55
+
56
+ sig { params(blk: T.proc.params(arg0: Configuration).void).void }
57
+ def configure(&blk)
58
+ yield(config)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ # typed: strict
2
+
3
+ module Packs
4
+ class DefaultUserEventLogger
5
+ include UserEventLogger
6
+ end
7
+ end
@@ -0,0 +1,37 @@
1
+ # typed: strict
2
+
3
+ require 'colorized_string'
4
+
5
+ module Packs
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
+ out ColorizedString.new(title).green.bold
13
+ out "\n"
14
+ yield
15
+ end
16
+
17
+ sig { params(text: String).void }
18
+ def self.print_bold_green(text)
19
+ out ColorizedString.new(text).green.bold
20
+ end
21
+
22
+ sig { params(text: String).void }
23
+ def self.print(text)
24
+ out text
25
+ end
26
+
27
+ sig { void }
28
+ def self.print_divider
29
+ out '=' * 100
30
+ end
31
+
32
+ sig { params(str: String).void }
33
+ def self.out(str)
34
+ puts str
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,18 @@
1
+ # typed: strict
2
+
3
+ module Packs
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); end
12
+
13
+ sig { params(file_move_operations: T::Array[Private::FileMoveOperation]).void }
14
+ def after_move_files!(file_move_operations)
15
+ nil
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,80 @@
1
+ # typed: strict
2
+
3
+ module Packs
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 { returns(ParsePackwerk::Package) }
13
+ def origin_pack
14
+ ParsePackwerk.package_from_path(origin_pathname)
15
+ end
16
+
17
+ sig { params(origin_pathname: Pathname, new_package_root: Pathname).returns(Pathname) }
18
+ def self.destination_pathname_for_package_move(origin_pathname, new_package_root)
19
+ origin_pack = ParsePackwerk.package_from_path(origin_pathname)
20
+
21
+ if origin_pack.name == ParsePackwerk::ROOT_PACKAGE_NAME
22
+ new_package_root.join(origin_pathname).cleanpath
23
+ else
24
+ Pathname.new(origin_pathname.to_s.gsub(origin_pack.name, new_package_root.to_s)).cleanpath
25
+ end
26
+ end
27
+
28
+ sig { params(origin_pathname: Pathname).returns(Pathname) }
29
+ def self.destination_pathname_for_new_public_api(origin_pathname)
30
+ origin_pack = ParsePackwerk.package_from_path(origin_pathname)
31
+ if origin_pack.name == ParsePackwerk::ROOT_PACKAGE_NAME
32
+ filepath_without_pack_name = origin_pathname.to_s
33
+ else
34
+ filepath_without_pack_name = origin_pathname.to_s.gsub("#{origin_pack.name}/", '')
35
+ end
36
+
37
+ # We join the pack name with the rest of the path...
38
+ path_parts = filepath_without_pack_name.split('/')
39
+ Pathname.new(origin_pack.name).join(
40
+ # ... keeping the "app" or "spec"
41
+ T.must(path_parts[0]),
42
+ # ... substituting "controllers," "services," etc. with "public"
43
+ 'public',
44
+ # ... then the rest is the same
45
+ T.must(path_parts[2..]).join('/')
46
+ # and we take the cleanpath so `./app/...` becomes `app/...`
47
+ ).cleanpath
48
+ end
49
+
50
+ sig { returns(FileMoveOperation) }
51
+ def spec_file_move_operation
52
+ # This could probably be implemented by some "strategy pattern" where different extension types are handled by different helpers
53
+ # Such a thing could also include, for example, when moving a controller, moving its ERB view too.
54
+ if origin_pathname.extname == '.rake'
55
+ new_origin_pathname = origin_pathname.sub('/lib/', '/spec/lib/').sub(%r{^lib/}, 'spec/lib/').sub('.rake', '_spec.rb')
56
+ new_destination_pathname = destination_pathname.sub('/lib/', '/spec/lib/').sub(%r{^lib/}, 'spec/lib/').sub('.rake', '_spec.rb')
57
+ else
58
+ new_origin_pathname = origin_pathname.sub('/app/', '/spec/').sub(%r{^app/}, 'spec/').sub('.rb', '_spec.rb')
59
+ new_destination_pathname = destination_pathname.sub('/app/', '/spec/').sub(%r{^app/}, 'spec/').sub('.rb', '_spec.rb')
60
+ end
61
+ FileMoveOperation.new(
62
+ origin_pathname: new_origin_pathname,
63
+ destination_pathname: new_destination_pathname,
64
+ destination_pack: destination_pack
65
+ )
66
+ end
67
+
68
+ private
69
+
70
+ sig { params(path: Pathname).returns(FileMoveOperation) }
71
+ def relative_to(path)
72
+ FileMoveOperation.new(
73
+ origin_pathname: origin_pathname.relative_path_from(path),
74
+ destination_pathname: destination_pathname.relative_path_from(path),
75
+ destination_pack: destination_pack
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,26 @@
1
+ # typed: strict
2
+
3
+ module Packs
4
+ module Private
5
+ module InteractiveCli
6
+ class FileSelector
7
+ extend T::Sig
8
+
9
+ sig { params(prompt: TTY::Prompt).returns(T::Array[String]) }
10
+ def self.select(prompt)
11
+ prompt.on(:keytab) do
12
+ raw_paths_relative_to_root = prompt.multiline('Please copy in a space or new line separated list of files or directories')
13
+ paths_relative_to_root = T.let([], T::Array[String])
14
+ raw_paths_relative_to_root.each do |path|
15
+ paths_relative_to_root += path.chomp.split
16
+ end
17
+
18
+ return paths_relative_to_root
19
+ end
20
+
21
+ [prompt.ask('Please input a file or directory to move (press tab to enter multiline mode)')]
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end