Trouble collapsing folders in Rails Engine's `app/public` section - need help!

This message was imported from the Ruby/Rails Modularity Slack server. Find more info in the import thread.

Message originally sent by slack user U722GPJPXKC

Hello group - I’m delighted to have found you - I feel like I’ve been looking for packs since the beginning of my journey with sizeable Rails applications and I’m very excited to try it out! I have a small design question though - forgive me if this has been covered elsewhere already - I couldn’t find it. I want to have a mixture of packs and engines (treated as packs). However, converting my first engine I’ve hit a problem. Given the following desired folder structure:

{Rails.root}
  app/
  ...
  engines/
    my_engine/
      app/
        models/
          my_domain/
            private_model.rb
        public/
          my_domain/
            models/
              public_model.rb
            services/
              public_service.rb
      lib/
        my_engine.rb
        my_engine/
          engine.rb

I’m having difficulty selectively collapsing folders under the public section. i.e. I want to collapse models (as per convention) but not collapse some other folders at the same level (let’s say it’s services/ which has a Services namespace) thus:
MyEngine::PublicModel
MyEngine::Services::PublicService
I have Zeitwerk::Loader.for_gem in the my_engine.rb folder and I’ve tried collapsing the models folder:

loader.collapse("#{MyEngine::Engine.root}/app/public/my_engine/models")

but I can’t get past NameError: uninitialized constant MyEngine::PublicModel
If I move public_model.rb up one level to ./engines/my_engine/app/public then everything is fine; but I really want my public models to be in public/models.
Moving it back to public/models and running (from the engine’s root folder):

bundle exec rake app:zeitwerk:check

I get

expected file .../engines/my_engine/app/public/my_engine/models/public_model.rb to define constant MyEngines::Models::PublicModel

Sorry that’s a long explanation - I hoped someone might quickly recognize the issue!

Has anyone encountered something similar? Is there a trick to collapsing a app/public/models folder from a Rails Engine? The same pattern for packs in the {Rails.root}/packs folder does seem to work.

Message originally sent by slack user U71EC4J5IW3

One way you might solve this is by using Rails.autoloaders.main.push_dir(path, namespace: namespace) to tell Rails that files in app/public/models should be inside the MyEngine namespace. We have an opt-in package config that does this for all subfolders inside of our packs app/public folders. Gist code based on the automatic_namespaces gem written by @gpassero

A simplified solution for your case:

# config/initializers/autoloaders.rb

Object.const_set('MyEngine', Module.new) unless Object.const_defined?('MyEngine') # We need to create the namespace if it isn't already created

Rails.autoloaders.main.push_dir('engines/my_engine/app/public/models', namespace: MyEngine)

Message originally sent by slack user U722GPJPXKC

Hey <@U71EC4J5IW3> thank you for the steer here - that’s moved things forward. I’ve still not fully cracked it though:
• to run the engine specs from inside the engines root folder, I have to have that push_dir inside the engine.
• However to run the specs from the main application (e.g. bundle exec rspec engines/my_engine) I have to have the push_dir somewhere in config/initializers/ as you suggested above
But I can’t have it in both places, or something blows up. Obviously I’d prefer to have it defined in the engine and then that work for the main application also.
I’m going to take a look at those two links you provided and dig into Zeitwerk some more as I’m clearly missing something.
But thanks again for your help - it’s greatly appreciated :pray:

Message originally sent by slack user U722GPJPXKC

I think it might be because Packs is adding autoload dirs for the engine as I’ve defined the engine as a pack, and thus the engine’s own push_dir is conflicting with it.

Message excluded from import.