-
Notifications
You must be signed in to change notification settings - Fork 113
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
Switch application load path finding to zeitwerk #218
Conversation
Thank you for the review, Xavier! I feel like I found the line in a blog post (I will try to find that) and didn't look at the source code, so wasn't aware of the privateness. Packwerk operates on a set of directories to find constants defined in the app for which we can then define package boundaries and make dependencies explicit. Any normally autoloader Rails folder works, any that devs add to the config works, even engines in the app repo that are added to the Packwerk doesn't have a config for these folders, it finds them. In most practical scenarios the folders change too frequently for it to not be a nuisance to manage manually. Is there any public way to ask zeitwerk for all the folders it manages? |
Thank you for the pull request. Packwerk being a library for Rails applications has as requirement that libraries that needs to integrate with the Rails application have a least a As one of the main reason why someone would use packwerk is to enable modularity, I personally don't see any downside of requiring gems that want to be understood by packwerk to define a Rails engine, so I prefer to not change this implementation. |
Rails.autoloaders.inject({}) do |h, loader| | ||
h.merge(loader.root_dirs) | ||
end | ||
Zeitwerk::Loader.all_dirs |
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.
Hmmm this isn't going to work for us. Having the default namespace specified for each root dir is a feature GitHub depends on quite a bit.
I think using Zeitwerk::Registry
will work (and if so, it sounds much nicer, pending the feedback above about it being considered private) but I gotta insist on retaining the Hash of paths to modules. Hope that's ok.
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.
I'm willing to help if new use cases arise. For example, if a read-only collection of root directories with their associated namespaces would be a good solution for what this is trying to solve, I'd be open to implement it.
However, I don't know if this is a good solution to the root problem. If a gem is not an engine, and does not use Zeitwerk either, what?
Take also into account that Zeitwerk is used by gems. How are you going to distinguish the loaders that matter among them?
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.
@mclark it feels like we should have used packwerk to define which parts of packwerk are public API and which are not ;) jkjk
Do you mind elaborating as to what you do with the default namespace?
Thanks for offering to support this, Xavier! To answer your questions:
If a gem is not an engine, and does not use Zeitwerk either, what?
Such gems would currently not be included. And I think that is fine because...
Take also into account that Zeitwerk is used by gems. How are you going to distinguish the loaders that matter among them?
... currently packwerk only operates on constants defined with the Rails application. I.e., packwerk is focussed on helping engineers understand and clean up the dependency structures within their own applications, so there is already a boundary built in. #216 is looking to change this and add more options, but the fundamental fact that packwerk has controls for this won't change.
How is any of this useful? One example of an app that uses gems and engines as part of the codebase is https://github.com/instructure/canvas-lms/tree/master/gems, which defines a bunch of gems inline and uses the directly from the Gemfile (https://github.com/instructure/canvas-lms/blob/master/Gemfile.d/app.rb#L160)
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.
@shageman sorry I missed your comment somehow!
Do you mind elaborating as to what you do with the default namespace?
Some legacy directories in GitHub do not fully conform to the patterns established by Zeitwerk and have an implicit "root" namespace. When we migrated to Zeitwerk we leveraged the ability to set a default namespace for each root directory in order to override the default Object
root in these cases.
For Packwerk to resolve constants properly, it too needs to know what the default root namespaces are otherwise the expected and actual namespaces for each file will not match.
This isn't an ideal situation, but for the foreseeable future we will be stuck with this pattern unfortunately. Thanks for taking the time to understand the problem.
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.
Let me say that I believe root namespaces are as good practice as Object
. If I find an API that I like and is backwards compatible, I'd like to provide configuration for them in Rails somehow. Nowadays, you can do this reaching for the autoloaders by hand, but I believe this should be better supported.
I have been consulting for many years, and it is rare the project that does not have app
in the autoload paths. That is a weird configuration, and one that is forced because teams just one app/api
to be API
, for example. The fact that app/models
is not a namespace does not mean people do not want app/components
to be one, because we'll all agree app/components/components
is screaming the current settings are not sufficient.
@@ -10,7 +10,7 @@ We are keeping `packwerk` compatible with current versions of Ruby and Rails, bu | |||
|
|||
-- _Sandi Metz, Practical Object-Oriented Design in Ruby_ | |||
|
|||
Packwerk is a Ruby gem used to enforce boundaries and modularize Rails applications. | |||
Packwerk is a Ruby gem used to enforce boundaries and modularize Ruby applications (including Rails apps!). |
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.
Packwerk is a Ruby gem used to enforce boundaries and modularize Ruby applications (including Rails apps!). | |
Packwerk is a Ruby gem used to enforce boundaries and modularize Rails applications. |
We have no interest of entering the business of modularize arbitrary Ruby applications. I'm ok to allow load path discovery using Zeitwerk given that makes sense in the context of a Rails application, but if people start to ask for "feature X to support Hanami apps" or "Feature Y to support Sinatra apps" the answer at the moment will be an automatic "sorry, you are on your own". So, it is better to not claim in the README that we Packwerk is something it isn't.
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.
Makes sense! I know a lot happened today and this PR is no longer relevant for the moment, but I will make sure to have this fixed when we have an acceptable solution in place.
We had a followup Zoom call with @shageman and @alexevanczuk to discuss a bit ideas around this. Possible directions without ZeitwerkAFAICT, If that was correct, I believe a clean way to open If the gem is managed by Zeitwerk, fine. If it is not, fine too as long as it follows the conventions. If you need support for namespaces associated to root directories, that is as small tweak, and Simplification assuming ZeitwerkAlternatively, if you want to support only projects managed by Zeitwerk, then the gem could simply register its autoloaders (there can be more than one), and Not implying this is something wantedNot saying |
What are you trying to accomplish?
Packwerk currently uses
Rails.autoloaders
to determine the load paths it should include in its analysis.For unbuilt gems that are added to the application via the
path:
option in theGemfile
, this means that they have be turned into an engine for their paths to be picked up. One can either fully turn the gem into an engine or do so opportunistically when Rails is defined like so:Here, for tests,
zeitwerk
is used to ensure that loading is the same (similar?) between tests and prod.That
if
statement is quite gross ... and unnecessary!This PR switches path finding from
Rails.autoloaders
toZeitwerk::Registry.loaders
, with which theif
statement is no longer necessary and the gem doesn't need to be turned into an engine.What approach did you choose and why?
This PR makes
zeitwerk
a runtime dependency ofpackwerk
. By switching fromRails.autoloaders
toZeitwerk::Registry.loaders
any gem that uses zeitwerk for its file loading will be included in hackwork's analysis.What should reviewers focus on?
I have checked this against a large codebase at gusto. Performance and results are the same. I think it makes sense for others to do the same.
Type of Change
Additional Release Notes
Unbuilt gems in a codebase that use zeitwerk will not show up in packwerk violations before this and will show up in violations afterwards.
Checklist