packwerk 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (153) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
  3. data/.github/probots.yml +2 -0
  4. data/.github/pull_request_template.md +27 -0
  5. data/.github/workflows/ci.yml +50 -0
  6. data/.gitignore +12 -0
  7. data/.rubocop.yml +46 -0
  8. data/.ruby-version +1 -0
  9. data/CODEOWNERS +1 -0
  10. data/CODE_OF_CONDUCT.md +76 -0
  11. data/CONTRIBUTING.md +17 -0
  12. data/Gemfile +22 -0
  13. data/Gemfile.lock +236 -0
  14. data/LICENSE.md +7 -0
  15. data/README.md +73 -0
  16. data/Rakefile +13 -0
  17. data/TROUBLESHOOT.md +67 -0
  18. data/USAGE.md +250 -0
  19. data/bin/console +15 -0
  20. data/bin/setup +8 -0
  21. data/dev.yml +32 -0
  22. data/docs/cohesion.png +0 -0
  23. data/exe/packwerk +6 -0
  24. data/lib/packwerk.rb +44 -0
  25. data/lib/packwerk/application_validator.rb +343 -0
  26. data/lib/packwerk/association_inspector.rb +44 -0
  27. data/lib/packwerk/checking_deprecated_references.rb +40 -0
  28. data/lib/packwerk/cli.rb +238 -0
  29. data/lib/packwerk/configuration.rb +82 -0
  30. data/lib/packwerk/const_node_inspector.rb +44 -0
  31. data/lib/packwerk/constant_discovery.rb +60 -0
  32. data/lib/packwerk/constant_name_inspector.rb +22 -0
  33. data/lib/packwerk/dependency_checker.rb +28 -0
  34. data/lib/packwerk/deprecated_references.rb +92 -0
  35. data/lib/packwerk/file_processor.rb +43 -0
  36. data/lib/packwerk/files_for_processing.rb +67 -0
  37. data/lib/packwerk/formatters/progress_formatter.rb +46 -0
  38. data/lib/packwerk/generators/application_validation.rb +62 -0
  39. data/lib/packwerk/generators/configuration_file.rb +69 -0
  40. data/lib/packwerk/generators/inflections_file.rb +43 -0
  41. data/lib/packwerk/generators/root_package.rb +37 -0
  42. data/lib/packwerk/generators/templates/inflections.yml +6 -0
  43. data/lib/packwerk/generators/templates/package.yml +17 -0
  44. data/lib/packwerk/generators/templates/packwerk +23 -0
  45. data/lib/packwerk/generators/templates/packwerk.yml.erb +23 -0
  46. data/lib/packwerk/generators/templates/packwerk_validator_test.rb +11 -0
  47. data/lib/packwerk/graph.rb +74 -0
  48. data/lib/packwerk/inflections/custom.rb +33 -0
  49. data/lib/packwerk/inflections/default.rb +73 -0
  50. data/lib/packwerk/inflector.rb +41 -0
  51. data/lib/packwerk/node.rb +259 -0
  52. data/lib/packwerk/node_processor.rb +49 -0
  53. data/lib/packwerk/node_visitor.rb +22 -0
  54. data/lib/packwerk/offense.rb +44 -0
  55. data/lib/packwerk/output_styles.rb +41 -0
  56. data/lib/packwerk/package.rb +56 -0
  57. data/lib/packwerk/package_set.rb +59 -0
  58. data/lib/packwerk/parsed_constant_definitions.rb +62 -0
  59. data/lib/packwerk/parsers.rb +23 -0
  60. data/lib/packwerk/parsers/erb.rb +66 -0
  61. data/lib/packwerk/parsers/factory.rb +34 -0
  62. data/lib/packwerk/parsers/ruby.rb +42 -0
  63. data/lib/packwerk/privacy_checker.rb +45 -0
  64. data/lib/packwerk/reference.rb +6 -0
  65. data/lib/packwerk/reference_extractor.rb +81 -0
  66. data/lib/packwerk/reference_lister.rb +23 -0
  67. data/lib/packwerk/run_context.rb +103 -0
  68. data/lib/packwerk/sanity_checker.rb +10 -0
  69. data/lib/packwerk/spring_command.rb +28 -0
  70. data/lib/packwerk/updating_deprecated_references.rb +51 -0
  71. data/lib/packwerk/version.rb +6 -0
  72. data/lib/packwerk/violation_type.rb +13 -0
  73. data/library.yml +6 -0
  74. data/packwerk.gemspec +58 -0
  75. data/service.yml +6 -0
  76. data/shipit.rubygems.yml +1 -0
  77. data/sorbet/config +2 -0
  78. data/sorbet/rbi/gems/actioncable@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +840 -0
  79. data/sorbet/rbi/gems/actionmailbox@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +571 -0
  80. data/sorbet/rbi/gems/actionmailer@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +568 -0
  81. data/sorbet/rbi/gems/actionpack@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +5216 -0
  82. data/sorbet/rbi/gems/actiontext@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +663 -0
  83. data/sorbet/rbi/gems/actionview@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +2504 -0
  84. data/sorbet/rbi/gems/activejob@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +635 -0
  85. data/sorbet/rbi/gems/activemodel@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +1201 -0
  86. data/sorbet/rbi/gems/activerecord@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +8011 -0
  87. data/sorbet/rbi/gems/activestorage@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +904 -0
  88. data/sorbet/rbi/gems/activesupport@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +3888 -0
  89. data/sorbet/rbi/gems/ast@2.4.1.rbi +54 -0
  90. data/sorbet/rbi/gems/better_html@1.0.15.rbi +317 -0
  91. data/sorbet/rbi/gems/builder@3.2.4.rbi +8 -0
  92. data/sorbet/rbi/gems/byebug@11.1.3.rbi +8 -0
  93. data/sorbet/rbi/gems/coderay@1.1.3.rbi +8 -0
  94. data/sorbet/rbi/gems/colorize@0.8.1.rbi +40 -0
  95. data/sorbet/rbi/gems/commander@4.5.2.rbi +8 -0
  96. data/sorbet/rbi/gems/concurrent-ruby@1.1.6.rbi +1966 -0
  97. data/sorbet/rbi/gems/constant_resolver@0.1.5.rbi +26 -0
  98. data/sorbet/rbi/gems/crass@1.0.6.rbi +138 -0
  99. data/sorbet/rbi/gems/erubi@1.9.0.rbi +39 -0
  100. data/sorbet/rbi/gems/globalid@0.4.2.rbi +178 -0
  101. data/sorbet/rbi/gems/highline@2.0.3.rbi +8 -0
  102. data/sorbet/rbi/gems/html_tokenizer@0.0.7.rbi +46 -0
  103. data/sorbet/rbi/gems/i18n@1.8.2.rbi +633 -0
  104. data/sorbet/rbi/gems/jaro_winkler@1.5.4.rbi +8 -0
  105. data/sorbet/rbi/gems/loofah@2.5.0.rbi +272 -0
  106. data/sorbet/rbi/gems/m@1.5.1.rbi +108 -0
  107. data/sorbet/rbi/gems/mail@2.7.1.rbi +2490 -0
  108. data/sorbet/rbi/gems/marcel@0.3.3.rbi +30 -0
  109. data/sorbet/rbi/gems/method_source@1.0.0.rbi +76 -0
  110. data/sorbet/rbi/gems/mimemagic@0.3.5.rbi +47 -0
  111. data/sorbet/rbi/gems/mini_mime@1.0.2.rbi +71 -0
  112. data/sorbet/rbi/gems/mini_portile2@2.4.0.rbi +8 -0
  113. data/sorbet/rbi/gems/minitest@5.14.0.rbi +542 -0
  114. data/sorbet/rbi/gems/mocha@1.11.2.rbi +964 -0
  115. data/sorbet/rbi/gems/nio4r@2.5.2.rbi +89 -0
  116. data/sorbet/rbi/gems/nokogiri@1.10.9.rbi +1608 -0
  117. data/sorbet/rbi/gems/parallel@1.19.1.rbi +8 -0
  118. data/sorbet/rbi/gems/parlour@4.0.1.rbi +561 -0
  119. data/sorbet/rbi/gems/parser@2.7.1.4.rbi +1632 -0
  120. data/sorbet/rbi/gems/pry@0.13.1.rbi +8 -0
  121. data/sorbet/rbi/gems/rack-test@1.1.0.rbi +335 -0
  122. data/sorbet/rbi/gems/rack@2.2.2.rbi +1730 -0
  123. data/sorbet/rbi/gems/rails-dom-testing@2.0.3.rbi +123 -0
  124. data/sorbet/rbi/gems/rails-html-sanitizer@1.3.0.rbi +213 -0
  125. data/sorbet/rbi/gems/rails@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +8 -0
  126. data/sorbet/rbi/gems/railties@6.1.0.alpha-d80c18a391e33552ae2d943e68af56946f883f65.rbi +869 -0
  127. data/sorbet/rbi/gems/rainbow@3.0.0.rbi +155 -0
  128. data/sorbet/rbi/gems/rake@13.0.1.rbi +841 -0
  129. data/sorbet/rbi/gems/rexml@3.2.4.rbi +8 -0
  130. data/sorbet/rbi/gems/rubocop-performance@1.5.2.rbi +8 -0
  131. data/sorbet/rbi/gems/rubocop-shopify@1.0.2.rbi +8 -0
  132. data/sorbet/rbi/gems/rubocop-sorbet@0.3.7.rbi +8 -0
  133. data/sorbet/rbi/gems/rubocop@0.82.0.rbi +8 -0
  134. data/sorbet/rbi/gems/ruby-progressbar@1.10.1.rbi +8 -0
  135. data/sorbet/rbi/gems/smart_properties@1.15.0.rbi +168 -0
  136. data/sorbet/rbi/gems/spoom@1.0.4.rbi +418 -0
  137. data/sorbet/rbi/gems/spring@2.1.0.rbi +160 -0
  138. data/sorbet/rbi/gems/sprockets-rails@3.2.1.rbi +431 -0
  139. data/sorbet/rbi/gems/sprockets@4.0.0.rbi +1132 -0
  140. data/sorbet/rbi/gems/tapioca@0.4.5.rbi +518 -0
  141. data/sorbet/rbi/gems/thor@1.0.1.rbi +892 -0
  142. data/sorbet/rbi/gems/tzinfo@2.0.2.rbi +547 -0
  143. data/sorbet/rbi/gems/unicode-display_width@1.7.0.rbi +8 -0
  144. data/sorbet/rbi/gems/websocket-driver@0.7.1.rbi +438 -0
  145. data/sorbet/rbi/gems/websocket-extensions@0.1.4.rbi +71 -0
  146. data/sorbet/rbi/gems/zeitwerk@2.3.0.rbi +8 -0
  147. data/sorbet/tapioca/require.rb +25 -0
  148. data/static/packwerk-check-demo.png +0 -0
  149. data/static/packwerk_check.gif +0 -0
  150. data/static/packwerk_check_violation.gif +0 -0
  151. data/static/packwerk_update.gif +0 -0
  152. data/static/packwerk_validate.gif +0 -0
  153. metadata +341 -0
@@ -0,0 +1,7 @@
1
+ Copyright 2020-present, Shopify Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,73 @@
1
+ # Packwerk [![Build Status](https://github.com/Shopify/packwerk/workflows/CI/badge.svg)](https://github.com/Shopify/packwerk/actions?query=workflow%3ACI)
2
+
3
+ Packwerk is a Ruby gem used to enforce boundaries and modularize Rails applications.
4
+
5
+ Packwerk can be used to:
6
+ * Combine group of files into packages
7
+ * Define package-level constant visibility (i.e. have publicly accessible constants)
8
+ * Enforce privacy (inbound) and dependency (outbound) boundaries between packages
9
+ * Help existing codebases to become more modular without obstructing development
10
+
11
+ ## Prerequisites
12
+
13
+ Packwerk needs [Zeitwerk](https://github.com/fxn/zeitwerk) enabled, which comes with Rails 6.
14
+
15
+ Packwerk supports MRI versions 2.6 and above.
16
+
17
+ ## Demo
18
+
19
+ Watch a [1-minute video demo](https://drive.google.com/file/d/1D-t1nYduwgpHAP4DHY-EwVwVNFHlwRWj/view?usp=sharing) on how Packwerk works.
20
+
21
+ [![](./static/packwerk-check-demo.png)](https://drive.google.com/file/d/1D-t1nYduwgpHAP4DHY-EwVwVNFHlwRWj/view?usp=sharing)
22
+
23
+ ## Installation
24
+
25
+ 1. Add this line to your application's Gemfile:
26
+
27
+ ```ruby
28
+ gem 'packwerk'
29
+ ```
30
+
31
+ 2. Install the gem
32
+
33
+ Execute:
34
+
35
+ $ bundle install
36
+
37
+ Or install it yourself as:
38
+
39
+ $ gem install packwerk
40
+
41
+ 3. Run `bundle exec packwerk init` to generate the configuration files
42
+
43
+ ## Usage
44
+
45
+ Read [USAGE.md](USAGE.md) for usage once Packwerk is installed on your project.
46
+
47
+ ## Pronunciation
48
+
49
+ "Packwerk" is pronounced [[ˈpakvɛʁk]](https://cdn.shopify.com/s/files/1/0258/7469/files/packwerk.mp3).
50
+
51
+ ## Development
52
+
53
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
54
+
55
+ ## Limitations
56
+
57
+ With Ruby being a very dynamic language, static analysis tools such as Packwerk are bound to have limitations.
58
+ To reduce the impact of those limitations, Packwerk is designed to avoid false positives (reporting references as violations that are actually fine) at any cost, and we pay the cost by accepting a small number of false negatives (failing to report actual violations).
59
+
60
+ - Packwerk can only resolve references to constants that are defined in code loaded by the application's Zeitwerk autoloader.
61
+ This is because we rely on [Zeitwerk's conventions](https://github.com/fxn/zeitwerk#file-structure), and code that is loaded differently (like through an explicit `require`) often doesn't follow these conventions.
62
+ - Method calls and objects passed around the application are completely ignored. Packwerk only cares about static constant references. That said, if you want Packwerk to analyze parameters of a method, you can use [Sorbet](https://sorbet.org/) to define a type signature. Sorbet signatures are pure Ruby code and use constants to express types, and Packwerk understands that.
63
+ - Support for custom Zeitwerk configuration is limited. If [custom ActiveSupport inflections](https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#customizing-inflections) are used, Packwerk will understand that and everything is fine. However, if Zeitwerk is directly configured with [custom Zeitwerk inflections](https://github.com/fxn/zeitwerk#inflection) or to [collapse directories](https://github.com/fxn/zeitwerk#collapsing-directories), _Packwerk will get confused and produce false positives_.
64
+
65
+ ## Contributing
66
+
67
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/packwerk.
68
+
69
+ Read and follow the guidelines in [CONTRIBUTING.md](https://github.com/Shopify/packwerk/blob/main/CONTRIBUTING.md).
70
+
71
+ ## License
72
+
73
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ t.warning = true
11
+ end
12
+
13
+ task(default: :test)
@@ -0,0 +1,67 @@
1
+ # Troubleshoot
2
+
3
+ * [Troubleshooting violations](#Troubleshooting-violations)
4
+ * [Feedback loop](#Feedback-loop)
5
+ * [Package Privacy violation](#Package-Privacy-violation)
6
+ * [Interpreting Privacy violation](#Interpreting-Privacy-violation)
7
+ * [Package Dependency violation](#Package-Dependency-violation)
8
+ * [Interpreting Dependency violation](#Interpreting-Dependency-violation)
9
+
10
+ ## Troubleshooting violations
11
+
12
+ ### Feedback loop
13
+ Packwerk can give feedback via continuous integration (CI) if you have it set up, but that is a long feedback cycle. If you want faster feedback, use Packwerk on the command line locally.
14
+
15
+ You can specify folders or packages in Packwerk commands for a shorter run time:
16
+
17
+ bundle exec packwerk check components/your_package
18
+
19
+ bundle exec packwerk update components/your_package
20
+
21
+ _Note: You cannot specify folders or packages for `packwerk validate` because the command runs for the entire application._
22
+
23
+ ![](static/packwerk_check_violation.gif)
24
+
25
+ ### Package Privacy violation
26
+ A constant that is private to its package has been referenced from outside of the package. Constants are declared private in their package’s `package.yml`.
27
+
28
+ See: [USAGE.md - Enforcing privacy boundary](USAGE.md#Enforcing-privacy-boundary)
29
+
30
+ #### Interpreting Privacy violation
31
+
32
+ > /Users/JaneDoe/src/github.com/sample-project/user/app/controllers/labels_controller.rb:170:30
33
+ > Privacy violation: '::Billing::CarrierInvoiceTransaction' is private to 'billing' but referenced from 'user'.
34
+ > Is there a public entrypoint in 'billing/app/public/' that you can use instead?
35
+ >
36
+ > Inference details: 'Billing::CarrierInvoiceTransaction' refers to ::Billing::CarrierInvoiceTransaction which seems to be defined in billing/app/models/billing/carrier_invoice_transaction.rb.
37
+
38
+ There has been a privacy violation of the package `billing` in the package `user`, through the use of the constant `Billing::CarrierInvoiceTransaction` in the file `user/app/controllers/labels_controller.rb`.
39
+
40
+ ##### Suggestions
41
+ You may be accessing the implementation of a piece of functionality that is supposed to be accessed through a public interface on the package. Try to use the public interface instead. A package’s public interface should be defined in its `app/public` folder and documented.
42
+
43
+ The functionality you’re looking for may not be intended to be reused across packages at all. If there is no public interface for it but you have a good reason to use it from outside of its package, find the people responsible for the package and discuss a solution with them.
44
+
45
+ ### Package Dependency violation
46
+ A constant defined in a package A is referenced from a package B that doesn’t define a dependency on A. Packages define their dependencies in their `package.yml`.
47
+
48
+ See: [USAGE.md - Enforcing dependency boundary](USAGE.md#Enforcing-dependency-boundary)
49
+
50
+ #### Interpreting Dependency violation
51
+
52
+ > /Users/JaneDoe/src/github.com/sample-project/billing/app/jobs/document_processing_job.rb:48:6
53
+ > Dependency violation: ::Edi::Source belongs to 'edi', but 'billing' does not specify a dependency on 'edi'.
54
+ > Are we missing an abstraction?
55
+ > Is the code making the reference, and the referenced constant, in the right packages?
56
+ >
57
+ > Inference details: 'Edi::Source' refers to ::Edi::Source which seems to be defined in edi/app/models/edi/source.rb.
58
+
59
+ There has been a dependency violation in the package `billing` to the package `edi`, through the use of the constant `Edi::Source` in the file `billing/app/jobs/document_processing_job.rb`.
60
+
61
+ ##### Suggestions
62
+ If a package declares dependencies, assume that some thought went into that. It is unlikely that the correct fix is to add another dependency.
63
+ Check if the code containing the reference and the code defining the constants are in the right packages. If not, fix that by moving code around.
64
+
65
+ If a dependency A -> B is not desired, but a dependency B -> A is OK, consider using [dependency inversion](https://www.sandimetz.com/blog/2009/03/21/solid-design-principles).
66
+
67
+ If you’re getting stuck, find people with context about the two packages involved and ask for their opinion.
@@ -0,0 +1,250 @@
1
+ # Packwerk usage
2
+
3
+ ## Table of Contents
4
+ * [What problem does Packwerk solve?](#What-problem-does-Packwerk-solve?)
5
+ * [What is a package?](#What-is-a-package?)
6
+ * [Package principles](#Package-principles)
7
+ * [Getting started](#Getting-started)
8
+ * [Setting up the configuration file](#Setting-up-the-configuration-file)
9
+ * [Inflections](#Inflections)
10
+ * [Validating the package system](#Validating-the-package-system)
11
+ * [Defining packages](#Defining-packages)
12
+ * [Package metadata](#Package-metadata)
13
+ * [Types of boundary checks](#Types-of-boundary-checks)
14
+ * [Enforcing privacy boundary](#Enforcing-privacy-boundary)
15
+ * [Using public folders](#Using-public-folders)
16
+ * [Enforcing dependency boundary](#Enforcing-dependency-boundary)
17
+ * [Checking for violations](#Checking-for-violations)
18
+ * [Recording existing violations](#Recording-existing-violations)
19
+ * [Understanding the list of deprecated references](#Understanding-the-list-of-deprecated-references)
20
+
21
+ ## What problem does Packwerk solve?
22
+ Large applications need clear boundaries to avoid turning into a [ball of mud](https://en.wikipedia.org/wiki/Big_ball_of_mud). However, Ruby does not provide a good solution to enforcing boundaries between code.
23
+
24
+ Packwerk is a gem that can be used to enforce boundaries between groups of code we call packages.
25
+
26
+ ## What is a package?
27
+ A package is a folder containing autoloaded code. To decide whether code belongs together in a package, these are some design best practices:
28
+
29
+ - We should package things together that have high functional [cohesion](https://en.wikipedia.org/wiki/Cohesion_(computer_science)).
30
+ - Packages should be relatively loosely coupled to each other.
31
+
32
+ ![cohesion](docs/cohesion.png)
33
+
34
+ ### Package principles
35
+
36
+ Package principles help to guide the organization of classes in a large system. These principles can also be applied to packages in large and complex codebases.
37
+
38
+ The [package principles](https://en.wikipedia.org/wiki/Package_principles) page on Wikipedia does a good job explaining what well designed packages look like.
39
+
40
+ ## Getting started
41
+
42
+ After including Packwerk in the Gemfile, you can generate the necessary files to get Packwerk running by executing:
43
+
44
+ bundle exec packwerk init
45
+
46
+ Here is a list of files generated:
47
+
48
+ | File | Location | Description |
49
+ |-----------------------------|--------------|------------|
50
+ | Packwerk configuration | packwerk.yml | See [Setting up configuration file](#Setting-up-configuration-file) |
51
+ | Root package | package.yml | A package for the root folder |
52
+ | Bin script | bin/packwerk | For Rails applications to run Packwerk validation on CI, see [Validating the package system](#Validating-the-package-system) |
53
+ | Validation test | test/packwerk_validator_test.rb | For Ruby projects to run Packwerk validation using tests, see [Validating the package system](#Validating-the-package-system) |
54
+ | Custom inflections | configs/inflections.yml | A custom inflections file is only required if you have custom inflections in `inflections.rb`, see [Inflections](#Inflections) |
55
+
56
+ After that, you may begin creating packages for your application. See [Defining packages](#Defining-packages)
57
+
58
+ ## Setting up the configuration file
59
+
60
+ Packwerk reads from the `packwerk.yml` configuration file in the root directory. Packwerk will run with the default configuration if any of these settings are not specified.
61
+
62
+ | Key | Default value | Description |
63
+ |----------------------|------------------------------------|--------------|
64
+ | include | **/*.{rb,rake,erb} | list of patterns for folder paths to include |
65
+ | exclude | {bin,node_modules,script,tmp}/**/* | list of patterns for folder paths to exclude |
66
+ | package_paths | **/ | patterns to find package configuration files, see: Defining packages |
67
+ | load_paths | All application autoload paths | list of load paths |
68
+ | custom_associations | N/A | list of custom associations, if any |
69
+
70
+
71
+ ### Inflections
72
+
73
+ Packwerk requires custom inflections to be defined in `inflections.yml` instead of the traditional `inflections.rb`. This is because Packwerk accounts for custom inflections, such as acronyms, when resolving constants. Additionally, Packwerk interprets Active Record associations as references to constants. For example, `has_many :birds` is reference to the `Birds` constant.
74
+
75
+ In order to make your custom inflections compatible with Active Support and Packwerk, you must create an `inflections.yml` file and point `ActiveSupport::Inflector` to that file.
76
+
77
+ In `inflections.rb`, add:
78
+
79
+ ```rb
80
+ ActiveSupport::Inflector.inflections do |inflect|
81
+ # please add all custom inflections in the file below.
82
+ Packwerk::Inflections::Custom.new(
83
+ Rails.root.join("inflections.yml")
84
+ ).apply_to(inflect)
85
+ end
86
+ ```
87
+
88
+ Next, move your existing custom inflections into `inflections.yml`:
89
+
90
+ ```yaml
91
+ acronym:
92
+ - 'GraphQL'
93
+ - 'MRuby'
94
+ - 'TOS'
95
+ irregular:
96
+ - ['analysis', 'analyses']
97
+ - ['reserve', 'reserves']
98
+ uncountable:
99
+ - 'payment_details'
100
+ ```
101
+
102
+ Any new inflectors should be added to `inflections.yml`.
103
+
104
+ ## Validating the package system
105
+
106
+ There are some criteria that an application must meet in order to have a valid package system. These criteria include having a valid autoload path cache, package definition files, and application folder structure. The dependency graph within the package system also has to be acyclic.
107
+
108
+ The package system can be validated through a series of built in validation checks. Currently, the validation checks require the application to be booted either through `spring` or as part of its test suite.
109
+
110
+ We recommend setting up the package system validation for your Rails application in a CI step (or through a test suite for Ruby projects) separate from `packwerk check`.
111
+
112
+ If running `packwerk init` generates a `bin/packwerk` script, proceed to run:
113
+
114
+ bin/packwerk validate
115
+
116
+ ![](static/packwerk_validate.gif)
117
+
118
+ If running `packwerk init` on your Ruby project generates `test/packwerk_validator_test.rb`, you can use this test as validation.
119
+
120
+ ## Defining packages
121
+
122
+ You can create a `package.yml` in any folder to make it a package. The package name is the path to the folder from the project root.
123
+
124
+ _Note: It is helpful to define a namespace that corresponds to the package name and contains at least all the public constants of the package. This makes it more obvious which package a constant is defined in._
125
+
126
+ ### Package metadata
127
+ Package metadata can be included in the `package.yml`. Metadata won't be validated, and can thus be anything. We recommend including information on ownership and stewardship of the package.
128
+
129
+ Example:
130
+ ```yaml
131
+ # components/sales/package.yml
132
+ metadata:
133
+ stewards:
134
+ - "@Shopify/sales"
135
+ slack_channels:
136
+ - "#sales"
137
+ ```
138
+
139
+ ## Types of boundary checks
140
+
141
+ Packwerk can perform two types of boundary checks: privacy and dependency.
142
+
143
+ #### Enforcing privacy boundary
144
+ A package's privacy boundary is violated when there is a reference to the package's private constants from a source outside the package.
145
+
146
+ There are two ways you can enforce privacy for your package:
147
+
148
+ 1. Enforce privacy for all external sources
149
+
150
+ ```yaml
151
+ # components/merchandising/package.yml
152
+ enforce_privacy: true # will make everything private that is not in
153
+ # the components/merchandising/app/public folder
154
+ ```
155
+
156
+ Setting `enforce_privacy` to true will make all references to private constants in your package a violation.
157
+
158
+ 2. Enforce privacy for specific constants
159
+
160
+ ```yaml
161
+ # components/merchandising/package.yml
162
+ enforce_privacy:
163
+ - "::Merchandising::Product"
164
+ - "::SomeNamespace" # enforces privacy for the namespace and
165
+ # everything nested in it
166
+ ```
167
+
168
+ It will be a privacy violation when a file outside of the `components/merchandising` package tries to reference `Merchandising::Product`.
169
+
170
+ ##### Using public folders
171
+ You may enforce privacy either way mentioned above and still expose a public API for your package by placing constants in the `app/public` folder. The constants in the public folder will be made available for use by the rest of the application.
172
+
173
+ #### Enforcing dependency boundary
174
+ A package's dependency boundary is violated whenever it references a constant in some package that has not been declared as a dependency.
175
+
176
+ Specify `enforce_dependencies: true` to start enforcing the dependencies of a package. The intentional dependencies of the package are specified as a list under a `dependencies:` key.
177
+
178
+ Example:
179
+
180
+ ```yaml
181
+ # components/shop_identity/package.yml
182
+ enforce_dependencies: true
183
+ dependencies:
184
+ - components/platform
185
+ ```
186
+
187
+ It will be a dependency violation when `components/shop_identity` tries to reference a constant that is not within `components/platform` or itself.
188
+
189
+ ## Checking for violations
190
+
191
+ After enforcing the boundary checks for a package, you may execute:
192
+
193
+ bundle exec packwerk check
194
+
195
+ Packwerk will check the entire codebase for any violations.
196
+
197
+ You can also specify folders or packages for a shorter run time:
198
+
199
+ bundle exec packwerk check components/your_package
200
+
201
+ ![](static/packwerk_check.gif)
202
+
203
+ In order to keep the package system valid at each version of the application, we recommend running `packwerk check` in your CI pipeline.
204
+
205
+ See: [TROUBLESHOOT.md - Sample violations](TROUBLESHOOT.md#Sample-violations)
206
+
207
+ ## Recording existing violations
208
+
209
+ For existing codebases, packages are likely to have existing boundary violations.
210
+
211
+ If so, you will want to stop the bleeding and prevent more violations from occuring. The existing violations in the codebase can be recorded in a [deprecated references list](#Understanding_the_list_of_deprecated_references) by executing:
212
+
213
+ bundle exec packwerk update
214
+
215
+ Similar to `packwerk check`, you may also run `packwerk update` on folders or packages:
216
+
217
+ bundle exec packwerk update components/your_package
218
+
219
+ ![](static/packwerk_update.gif)
220
+
221
+ _Note: Changing dependencies or enabling dependencies will not require a full update of the codebase, only the package that changed. On the other hand, changing or enabling privacy will require a full update of the codebase._
222
+
223
+ `packwerk update` should only be run to record existing violations and to remove deprecated references that have been worked off. Running `packwerk update` to resolve a violation should be the very last resort.
224
+
225
+ See: [TROUBLESHOOT.md - Troubleshooting violations](TROUBLESHOOT.md#Troubleshooting_violations)
226
+
227
+
228
+ ### Understanding the list of deprecated references
229
+
230
+ The deprecated references list is called `deprecated_references.yml` and can be found in the package folder. The list outlines the constant violations of the package, where the violation is located, and the file defining the violation.
231
+
232
+ The deprecated references list should not be added to, but worked off over time.
233
+
234
+ ```yaml
235
+ components/merchant:
236
+ "::Checkouts::Core::CheckoutId":
237
+ violations:
238
+ - dependency
239
+ files:
240
+ - components/merchant/app/public/merchant/generate_order.rb
241
+ ```
242
+
243
+ Above is an example of a constant violation entry in `deprecated_references.yml`.
244
+
245
+ * `components/merchant` - package where the constant violation is found
246
+ * `::Checkouts::Core::CheckoutId` - violated constant in question
247
+ * `dependency` - type of violation, either dependency or privacy
248
+ * `components/merchant/app/public/merchant/generate_order.rb` - path to the file containing the violated constant
249
+
250
+ Violations exist within the package that makes a violating reference. This means privacy violations of your package can be found listed in `deprecated_references.yml` files in the packages with the reference to a private constant.
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "packwerk"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/dev.yml ADDED
@@ -0,0 +1,32 @@
1
+ name: packwerk
2
+
3
+ type: ruby
4
+
5
+ up:
6
+ - ruby: 2.6.6
7
+ - bundler
8
+
9
+ commands:
10
+ test:
11
+ run: |
12
+ if [[ "$*" =~ ":"[0-9]+ ]];
13
+ then
14
+ # run test by its line number
15
+ bundle exec m "$@"
16
+ elif [[ "$#" -eq 1 && -f "$1" ]];
17
+ then
18
+ # run all tests in given file(s)
19
+ bundle exec rake test TEST="$@"
20
+ else
21
+ # run all tests
22
+ bundle exec rake test
23
+ fi
24
+ style: "bundle exec rubocop -D --auto-correct"
25
+ typecheck:
26
+ desc: "run Sorbet typechecking"
27
+ run: "bundle exec srb tc"
28
+ aliases: ['tc']
29
+ subcommands:
30
+ update:
31
+ desc: "update RBIs for gems"
32
+ run: "bundle exec tapioca sync -c 'dev typecheck update'"