With assets in a rails engine it could be Gemfile vs gemspec dependency that is messing it up

You have a rails engine. It has an asset (like a scss file). You include this engine in another engine. How do you test that the assets is correctly resolved?

There is a moment when you simply can not understand where and how the asset is resolved/not resolved. The solution actually is in what are the Gemfile and .gemspec for a rails engine. Both of them could describe dependencies, but they contain different things.

The whole premise of the situation seems strange until you get to building a rails engine that would contain the “theme” of your app. We go in this situation with two of our platforms – FLLCasts.com and 3DAssemblyInstructions.com. We wanted to have different layouts in different gems that are providing different look an feel- separate the main layout from dashboard and from main. We also wanted to have this layouts in a different gems so that we could release them with different versions and test them as separate gems in separate builds.

Let’s get into this step by step tutorial and by the end I am sure you would know much about how assets are resolved, packed and testes when they are within a rails. Again I am writing this tutorial to help spread the knowledge internally, but the problem is so common that it might be of interest to the community as a whole.

Case 1 – simple app with an asset

Just to get the understanding correctly and to have a base we would create a simple rails app with an asset and start it. I am using rails 6 and ruby 2.6.5

$ rails new simple_assets_app
$ cd simple_asset_app
# Create a new asset.scss file with some content
$ echo "asset_style { background-color: white}" > app/assets/stylesheets/asset.scss
$ rails s
=> Booting Puma
=> Rails 6.0.3.1 application starting in development 
=> Run `rails server --help` for more startup options
Puma starting in single mode...
* Version 4.3.5 (ruby 2.6.5-p114), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:3000
* Listening on tcp://[::1]:3000
Use Ctrl-C to stop

In another terminal request the asset

# Request the asset and dislay its content. Everything works. Perfect
$ curl localhost:3000/assets/asset.scss
asset_style { background-color: white}

Case 2 – engine with an asset

Now let’s build a rails plugin that would hold this asset

$ rails plugin new simple_asset_engine --full
$ cd simple_asset_engine/
$ echo "asset_style { background-color: white}" > app/assets/stylesheets/asset.scss
# You have to fix the TODOs in the gemspec. After you are ready do 
$ rails s -p 3001
=> Booting WEBrick
=> Rails 6.0.3.1 application starting in development http://localhost:3001
=> Run `rails server --help` for more startup options
[2020-05-21 19:48:51] INFO  WEBrick 1.4.2
[2020-05-21 19:48:51] INFO  ruby 2.6.5 (2019-10-01) [x86_64-linux]
[2020-05-21 19:48:51] INFO  WEBrick::HTTPServer#start: pid=13394 port=3001

Now that the port is different.

Request the asset

# Nothing strange here. Asset is delivered as expected.
$ curl localhost:3001/assets/asset.scss
asset_style { background-color: white}

Conclusion – engine, no engine the asset is delivered.

Case 3 – require simple_asset_engine in another engine

This could be a requirement. It has happen to us. It is a valid case. It is rather strange to have one engine require another, but we are all grown ups here…

$ rails plugin new host_engine --full
# Fix todos in .gemspec

# Add a dependency to simple_asset_engine. Asset should be found, rigth
$ echo 'gem "simple_asset_engine", path: "~/axles/tmp/simple_asset_engine"' >> Gemfile

# install gems - mainly the simple_asset_engine
$ bundle install

# Start server. Note the different port
$ rails s -p 4010

Request asset

$ curl localhost:4010/assets/asset.scss
asset_style { background-color: white}

Asset is found.

That’s the power of rails. It just works. You can have an asset in an app, asset in an engine, asset in an engine, required in another engine and the asset is always found. Except…

This might sound as a conclusion, because everything works. Until you get to production, which is actually the only time it matters if anything works, but that’s another story.

Our current host_engine depends on a gem with a local path in the local filesystem.

host_engine: $ cat Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

# Declare your gem's dependencies in host_engine.gemspec.
# Bundler will treat runtime dependencies like base dependencies, and
# development dependencies will be added by default to the :development group.
gemspec

# Declare any dependencies that are still in development here instead of in
# your gemspec. These might include edge Rails or gems from your path or
# Git. Remember to move these dependencies to your gemspec before releasing
# your gem to rubygems.org.

# To use a debugger
# gem 'byebug', group: [:development, :test]
gem "simple_asset_engine", path: "~/axles/tmp/simple_asset_engine"

This is something that we can not ship to production. That’s why we pack “simple_asset_engine” as a gem and place this gem in a gemrepo. This is not part of the tutorial so I will simulate it.

We should also modify the host_engine.gemspec to depend on simple_asset_engine. We add a dependency.

# in host_engine.gemspec we add

spec.add_dependency "simple_asset_engine"

This means – ‘add a dependency to “simple_asset_engine”‘. Our gem will host_engine will depend on ‘simple_asset_engine’, but and here is a but, but only for ‘production’ environment. So this means – “production”. To simulate the fact that we don’t have this gem in a repo we would change the dependency in the Gemfile to be only for production.

# This means the same as spec.add_dependency 'simple_asset_engine' It add the gem as a dependency to production. We are doing it like this to simulate that we have 'spec.add_dependency' and the gem is coming from a gems repo

gem "simple_asset_engine", group: [:production], path: "~/axles/tmp/simple_asset_engine"

Now as you do request the asset a strange error occurs:

$ curl localhost:4010/assets/asset.scss
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Action Controller: Exception caught</title>
    ....
   <div id="Framework-Trace-0" style="display: none;">
      <code style="font-size: 11px;">
          <a class="trace-frames trace-frames-0" data-exception-object-id="47236752082100" data-frame-id="0" href="#">
            actionpack (6.0.3.1) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call'
          </a>
          <br>
          <a class="trace-frames trace-frames-0" data-exception-object-id="47236752082100" data-frame-id="1" href="#">
            actionpack (6.0.3.1) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
          </a>
          <br>
          <a class="trace-frames trace-frames-0" data-exception-object-id="47236752082100" data-frame-id="2" href="#">
            railties (6.0.3.1) lib/rails/rack/logger.rb:37:in `call_app'
          </a>
...
</div>
</body>
</html>

In the log you see

ActionController::RoutingError (No route matches [GET] "/assets/asset.scss"):
  
actionpack (6.0.3.1) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call'
actionpack (6.0.3.1) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
railties (6.0.3.1) lib/rails/rack/logger.rb:37:in `call_app'
railties (6.0.3.1) lib/rails/rack/logger.rb:26:in `block in call'
activesupport (6.0.3.1) lib/active_support/tagged_logging.rb:80:in `block in tagged'
activesupport (6.0.3.1) lib/active_support/tagged_logging.rb:28:in `tagged'
activesupport (6.0.3.1) lib/active_support/tagged_logging.rb:80:in `tagged'
railties (6.0.3.1) lib/rails/rack/logger.rb:26:in `call'
sprockets-rails (3.2.1) lib/sprockets/rails/quiet_assets.rb:11:in `block in call'
activesupport (6.0.3.1) lib/active_support/logger_silence.rb:36:in `silence'
activesupport (6.0.3.1) lib/active_support/logger.rb:64:in `block (3 levels) in broadcast'
activesupport (6.0.3.1) lib/active_support/logger_silence.rb:36:in `silence'
activesupport (6.0.3.1) lib/active_support/logger.rb:62:in `block (2 levels) in broadcast'
sprockets-rails (3.2.1) lib/sprockets/rails/quiet_assets.rb:11:in `call'
actionpack (6.0.3.1) lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
actionpack (6.0.3.1) lib/action_dispatch/middleware/request_id.rb:27:in `call'
rack (2.2.2) lib/rack/method_override.rb:24:in `call'
rack (2.2.2) lib/rack/runtime.rb:22:in `call'
activesupport (6.0.3.1) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'
actionpack (6.0.3.1) lib/action_dispatch/middleware/executor.rb:14:in `call'
actionpack (6.0.3.1) lib/action_dispatch/middleware/static.rb:126:in `call'
rack (2.2.2) lib/rack/sendfile.rb:110:in `call'
actionpack (6.0.3.1) lib/action_dispatch/middleware/host_authorization.rb:82:in `call'
railties (6.0.3.1) lib/rails/engine.rb:527:in `call'
rack (2.2.2) lib/rack/handler/webrick.rb:95:in `service'

It is basically telling us that an error occurred. An error also should occur. It is logical, it is the right thing to do, but it can take hours to debug and understand this.

The error occurs because we are starting the rails server in ‘development’ environment and the dependency to ‘simple_asset_engine’ is only for production.

Case 3.1 – change to production and development

# Change host_engine/Gemfile to have

gem "simple_asset_engine", group: [:production, :development], path: "~/axles/tmp/simple_asset_engine"

Restart server on port 4010.

host_engine: $ rails s -p 4010
=> Booting WEBrick
=> Rails 6.0.3.1 application starting in development http://localhost:4010
=> Run `rails server --help` for more startup options
[2020-05-21 20:14:55] INFO  WEBrick 1.4.2
[2020-05-21 20:14:55] INFO  ruby 2.6.5 (2019-10-01) [x86_64-linux]
[2020-05-21 20:14:55] INFO  WEBrick::HTTPServer#start: pid=14167 port=4010

Request the asset

$ curl localhost:4010/assets/asset.scss
asset_style { background-color: white}

Now it is time for conclusion

spec.add_dependency in a gemspec gives you a dependency for production. There is a ‘spec.add_development_dependency’ that exists, but there are great discussion about it here https://github.com/rubygems/rubygems/issues/1104 Read more there. Really. Read more.

But as we are trying to test assets separately in an engine it is important to understand what Gemfile and gemspec could be used for. If an asset is not found from a dependency of a gemspec, probably the whole dependency is required for a different environment. In the same time if the dependency contains only assets, it might be difficult to get what is happening at first sight and is it your fault, or that the F..k is sprockets doing or event what the bigger f..k is webpacker doing. But for this case it is just plain old ruby dependencies that are messing us up.