Upgrading to Rails7 cssbunding, jsbundling, and turbo-rails

I had a legacy Rails app that uses Javascript, Turbolinks, Tailwind, Webpacker to glue it all together, and Docker for deployment. It all felt a bit rickety to where a light breeze or a yarn or gem update could bring it all down. Having just upgraded from Rails 6 to 7, I felt like it would be good to try out Rails 7’s new methods of handling css and javascript–namely jsbundling-rails, cssbundling-rails, and turbo-rails.

There are better guides out there for this but, because every project has its own unique dependencies, I thought this may help a poor soul like me with 70+ tabs open looking to cobble together the working recipe for upgrading their app.

1. Update Gemfile



- gem 'coffee-rails' (I wasn't using this)
- gem 'sass-rails'
- gem 'uglifier', '>= 1.3.0'
- gem 'webpacker', '~> 5.x'


+ gem 'cssbundling-rails'
+ gem 'jsbundling-rails'
+ gem 'sprockets-rails'
+ gem 'turbo-rails'

2. Update package.json



1. dependencies:
    1. "@tailwindcss/aspect-ratio": "^0.2.0",
    2. "@tailwindcss/forms": "^0.2.1", 
    3. "@tailwindcss/postcss7-compat": "npm:@tailwindcss/postcss7-compat",
    4. "@tailwindcss/typography": "^0.3.1",
    5. "@rails/ujs": "^5.0.2-2",
    6. "@rails/webpacker": "5.4.3",
    7. "postcss": "^8",  
    8. "tailwindcss": "npm:@tailwindcss/postcss7-compat",
    9. "turbolinks": "^5.2.0",
    10. "webpack": "^4.46.0", 
    11. "webpack-cli": "^3.3.12"
    12. "moment-locales-webpack-plugin": "^1.2.0",
2. devDependencies:
    1. "webpack-bundle-analyzer": "^4.5.0",  
    2. "webpack-dev-server": "^3.11.2"

3. Run:

bundle install./bin/rails javascript:install:esbuild

(Note the output warnings. Those are helpful in pointing out issues proactively and suggests fixes.)

4. Move app/javascript/packs/application.js up a folder to app/javascript/application.js

Repeat this for any other files in that folder and then remove the folder.

5. Edit app/javascript/application.js

  1. Remove require('@rails/ujs').start() and require('turbolinks').start()
  2. Change any import paths inside of that file (in my case I had import '../src/filename.js' so needed to remove a leading period to import './src/filename.js')
  3. Remove import '../stylesheets/application' from application.js as that is going to be handled by cssbundling and sprockets
  4. Remove require.context('../../assets/images', true); from application.js as images will just be served by sprockets.
  5. Repeat for secondary packs (I had a docker.js and static_pages.js packs as well)

6. Run ./bin/rails css:install:tailwind (note that this removes app/assets/stylesheets/application.css)

  1. This asks to overwrite tailwind.config.js
  2. This will add app/assets/stylesheets/application.tailwind.css and include the Tailwind css imports in it. (Again, note that your previous app/assets/stylesheets/application.css is now gone)
  3. This adds <%= stylesheet_link_tag “application” %> to layouts/application.html.erb

7. Run ./bin/rails turbo:install

8. Run ./bin/rails turbo:install:redis

9. Edit config/cable.yml

Under development change adapter: async to adapter: redis

10. Edit app/views/layouts/application.html.erb

  1. Remove the pack tag: <%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> as installing Tailwind added above what you need.
  2. Remove the pack tag: <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'false' %>
    1. This was replaced by <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %> when running rails javascript:install:esbuild
      1. Note on this when using jQuery that “defer: true” will cause issues when other code that relies on jQuery tries to run before jQuery is available.
  3. If you have a Javascript dependency such as jQuery (which needs to be loaded before everything that uses it) then you may need to rearrange the order your javascript is included inside of this file.

11. Rails previously generated .scss files for each new scaffolding so need to change all .scss files in assets/stylesheets to .css. I didn’t need scss.

12. I has some css files that weren’t in my asset pipeline (they were loaded via webpack) so I added them to config/initializers/assets.rb. (the other option was to add them to app/assets/config/manifest.js as //= link custom.css)

13. To include other css files (which I used to @import in app/javascript/stylesheets/application.scss) I can just add them to my stylesheet tag in layouts/application.html.erb:

<%= stylesheet_link_tag "application", "users", "widgets", "whatchamacallits", "data-turbo-track": "reload" %>

14. My custom.css had some cases where I used @apply to create a single css class that has multiple Tailwind properties in it. This was helpful for elements in my app that were reused such as buttons. That wouldn’t work but this comment showed me that I could move those all into application.tailwind.css and wrap them in `@layer components {

15. Change image_pack_tag to image_tag throughout the application.

16. Because I am changing to Turbo from Turbolinks I had to change all $(document).on('turbolinks:load', function() { to $(document).on('turbo:load', function() {

17. Remove or rename app/assets/javascripts/application.js (that will conflict with app/assets/builds/application.js)

18. I had a secondary pack for my Devise layout:

<%= javascript_pack_tag 'devise', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'devise', 'data-turbo-track': 'reload' %>

19. I had to add this to the top of my application.js file:

import "@hotwired/turbo-rails"

(I feel like this was supposed to be added automatically)

20. I was being served old assets so I had to run rails assets:clobber

21. I had a spot where I triggered a report generation (using Sidekiq) with a link (using a GET request and data-remote=”true”). After moving to Turbo, clicking the link would still hit the controller and trigger Sidekiq to generate the report in the background but Turbo would then try to redirect me to the report trigger URL. I had to change that from link_to to button_to and update routes.rb from ‘get’ to ‘post’.

Also had to change in the controller:

respond_to do |format| format.html { head :ok } end
    respond_to do |format|  
     format.turbo_stream {}  

22. Update all delete links (they are being processed as a Turbo stream rather than HTML now):

From: <%= link_to t('views.common.delete'), @widget, method: :delete, data: { confirm: t('views.common.are_you_sure') } %>To: <%= link_to t('views.common.delete'), @widget, data: { turbo_method: :delete, turbo_confirm: t('views.common.are_you_sure') } %>

23. Update delete methods in controllers:

def destroy  
   respond_to do |format|  
      format.turbo_stream { redirect_to widgets_url, status: 303, success: I18n.t('controllers.common.destroy.flash.notice', object_name: I18n.t('views.common.widget')) }  
      format.json { head :no_content }  

24. Update controller tests to handle the turbo_stream format:

  1. From: `delete widget_path(@widget.id)
  2. To: `delete widget_path(@widget.id, format: :turbo_stream)

That got my app into a working state. I am still working through changing some aspects of the app over to using turbo+stimulus. That is significantly reducing the amount of Javascript in the app. This should allow me to be more strategic about where I use Javascript rather than needing to use it for basic functionality as I was before.

I hadn’t been on the leading edge of Rails, since basically ever, but I’m pleased with everything Rails 7 has to offer as well as the upgrade process.