Upgrading from Rails 6.x Webpacker to Rails 7 Importmaps

I've spend a good part of the last few months trying to figure out how I was going to convert my three semi-private Rails 6.1.x apps to Rails 7. I created a bunch of Rails 7 test apps using ESbuild, Importmaps and even Webpacker. While ESbuild may of been an eaiser path (basically node modeles without webpacker stuff), I leaned towards Importmaps. Other than a few node package I used, almost all of my JS was using Stimulus in some form.

So I marched on with my journey to Rails 7. Along the way I did do a lot of refactoring. I've been moving slowly from W3css to Tailwind.css. I already had the mixed css with some of my own scss. I don't think I'll remember all the steps I took, but I try to decribe the steps I took in some kind of order. It may not be the best way, but I did it my way and it worked

Rail7 Branch

I created and checked out a new rails7 branch. Fortunally I didn't have to make any changes to my deployed master, but merging branches was always in the back of my mind.

I then did bin/rails app:update and it did its thing. I hate to admint this, but I have No tests. I've tried many times to get into the test mode, but failed! So I continued to test how I've tested for the last 40 years! Change something then push the car back up the hill and see if the brakes fail again!!

I can't remember if development worked after the update, but I think it did. The gem 'sass-rails' was still in the Gemfile
so I think it worked.

I did have a problem with the update adding ActiveStorage migrations. Apperently I never installed ActiveStorge and really didn't want to. I set config.active_record.migration_error = false to avoid the error and later on changed require "rails/all" to the documentation way to not require ActiveStorage.

Onward to the next step, Rip out Webpacker and convert Gemfile to a fresh Rails 7 Importmaps version.

Rip out Webpacker.

Seemed scary but pretty simple. I removed:

  • app/javascript/application.css
  • app/javascript/channels/consumer.js
  • app/javascript/channels/index.js
  • app/javascript/packs/application.js
  • babel.config.js
  • bin/spring
  • bin/webpack
  • bin/webpack-dev-server
  • bin/yarn
  • config/webpack/development.js
  • config/webpack/environment.js
  • config/webpack/production.js
  • config/webpack/test.js
  • config/webpacker.yml
  • package-lock.json
  • postcss.config.js
  • yarn.lock

I then basically merged my orginal Gemfile with the Test App Gemfile. The main stuff being (without my specifics):

gem "rails", "~> 7.0.0"

# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem "sprockets-rails"

# Use postgresql as the database for Active Record
gem "pg", "~> 1.1"

# Use the Puma web server [https://github.com/puma/puma]
gem "puma", "~> 5.0"

# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"

# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"

# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"

# Use Tailwind CSS [https://github.com/rails/tailwindcss-rails]
gem "tailwindcss-rails"

# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

gem "redis", "~> 4.0"

I think at this point development worked, but maybe a few errors/problems with css.

SASS CSS, etc

I had a bunch of crap scss. Some problably hadn't been used in years and never found a good way of identifing what wasn't used. I knew the main things I used were colors, buttons, tables and a few almost components. I converted that scss to css with one of the many scss_to_css sites found on the web and added that css to my branch.

Stimulus Controller needed to be converted to the @hotware version. import { Controller } from "@hotwired/stimulus"

I added to config/importmap.rb my unique CDNs

pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
pin "stimulus-flatpickr", to: "https://ga.jspm.io/npm:[email protected]/dist/index.m.js"
pin "flatpickr", to: "https://ga.jspm.io/npm:[email protected]/dist/flatpickr.js"
pin "stimulus-autocomplete", to: "https://ga.jspm.io/npm:[email protected]/src/autocomplete.js"
pin "@hotwired/stimulus", to: "https://ga.jspm.io/npm:@hotwired/[email protected]/dist/stimulus.js"

I also had to add a link to my layout for flatpickr link rel="stylesheet" href="https://ga.jspm.io/npm:[email protected]/dist/flatpickr.min.css"

CSS bundling starting working with some problems. Many of them related to the config/tailwind.js file.

I merged my old tailwing config file with the new verssion

const defaultTheme = require('tailwindcss/defaultTheme')

module.exports = {
  content: [
    './app/helpers/**/*.rb',
    './app/javascript/**/*.js',
    './app/views/**/*.html.*',
    './app/components/**/*.html.*'

  ],
  theme: {
    extend: {
      fontFamily: {
        sans: ['Inter var', ...defaultTheme.fontFamily.sans],
      },
      colors: {
        'orange': '#ffa500',
        'malt': '#991A1E',
        'gold': '#A79055',
        'dark-blue': '#0F3E61',
        'success': '#63ed7a',
        'secondary': "#9db3b8",
        'w3green': "#4CAF50",
        'w3red': "#f44336",
        'blue-link': "#00c",
        lime: {
          lightest: '#f1fff1',
          lighter: '#e2ffe2',
          light: '#c9ffc9',
          DEFAULT: '#b8ffb8',
          dark: '#96ff96',
          darker: '#7cff7c',
          darkest: '#49ff49',
        },
        // gray: colors.trueGray,

        green: {
          lighter:'hsla(122, 59%, 64%, 1)',
          light: 'hsla(122, 49%, 54%, 1)',
          DEFAULT: 'hsla(122, 39%, 49%, 1)',
          dark: 'hsla(122, 39%, 39%, 1)',
          darker: 'hsla(122, 39%, 29%, 1)',
        },
      },
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/aspect-ratio'),
    require('@tailwindcss/typography'),
  ]
}

I have a few Web Components and needed to add that folder to the content: array

I also had some tailwind webpack components the used @apply and converted them to rails helpers. Something like:

module ComponentsHelper
  def greenBox
    "box-border box-content m-3 p-4 bg-green-300 border-green-100 border-2 text-black"
  end

  def blueBox
    "box-border box-content m-3 p-4 bg-blue-400 border-blue-200 border-2 text-black"
  end

  def btn
    "py-1 px-2 text-black hover:text-white rounded font-lg font-bold "
  end

  def btnInfo
    btn + "bg-blue-400 text-blue-link hover:text-blue-100"
  end

  def btnWarn
    btn + "bg-orange hover:text-yellow-200"
  end

  def btnGreen
    btn + "bg-green-500 hover:text-green-100"
  end

  def btnDanger
    btn + "bg-red-500 hover:text-red-200"
  end

  def btnSuccess
    btn + "bg-success hover:bg-green-700"
  end

  def btnSecondary
    btn + "bg-secondary"
  end

  def flashAlert(type)
    case type
    when 'danger'
      return "bg-red-200 text-red-600"
    when 'info'
      return "bg-blue-200 text-blue-600"
    when 'success'
      return "bg-green-200 text-green-600"
    when 'warning'
      return "bg-yellow-400 text-yellow-800"
    else
      return "bg-gray-200 text-gray-600"
    end
  end

  def destroyConfirmTag(model,confirm_msg:"",klass:"",prompt:"")
    klass= "#{btnDanger} inline-block" if klass.blank?
    confirm_msg = "Are You Sure?" if confirm_msg.blank?
    prompt = "Delete #{model.class.name}" if prompt.blank?
    node = content_tag(:div, class: klass,
      data:{
        controller:"destroyConfirm", 
        action:"click->destroyConfirm#confirm",
        destroyConfirm_cmsg_value:confirm_msg
      }){
      concat(tag.span(prompt))
      concat(button_to( '',model, method: :delete,class:"hidden",data:{destroyConfirm_target:"submit"}))
    }
    node 
  end

end

The last helper destroyConfirmTag tackled the problem with Rails removing the method::get from delete scaffold. The default change is to change link_to to button_to. But it didn't have a confirm tag. So I just call this on delete links and add back the confirm message. I have a couple delete links that I need to sure they didn't click the wrong link. There is a simple stimulus controller:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = [ "submit"]
  static values = { cmsg: String}

  connect() {
    // console.log("destroy confirm")
    if (this.hasCmsgValue) {
      this.confirm_msg = this.cmsgValue
    }else{
      this.confirm_msg  = "Are you sure?"
    }
  }

  confirm(){
    // console.log(this.submitTarget.closest('form'))
    let ans = confirm(`${this.confirm_msg}`)
    if (ans == true) {
      this.submitTarget.closest('form').submit()
    }
  }

}

I even toyed with simple Ruby class to generate css from tailwind config file - never finished or used it though. Think I saw some post the the rails_tailwind gem site bitching about colors and components.

class TwColors

  attr_accessor :colors, :css

  Colors = {:colors=>                                                  
  {:orange=>"#ffa500",                                      
   :malt=>"#991A1E",                                        
   :gold=>"#A79055",                                        
   :"dark-blue"=>"#0F3E61",                                 
   :lime=>                                                  
    {:lightest=>"#f1fff1",                                  
     :lighter=>"#e2ffe2",                                   
     :light=>"#c9ffc9",                                     
     :DEFAULT=>"#b8ffb8",                                   
     :dark=>"#96ff96",                                      
     :darker=>"#7cff7c",                                    
     :darkest=>"#49ff49"},
   :green=>
    {:lighter=>"hsla(122, 59%, 64%, 1)",
     :light=>"hsla(122, 49%, 54%, 1)",
     :DEFAULT=>"hsla(122, 39%, 49%, 1)",
     :dark=>"hsla(122, 39%, 39%, 1)",
     :darker=>"hsla(122, 39%, 29%, 1)"}}}

  def initialize()
    @colors = Colors
    @css = "not parsed yet \n"
    @colors[:colors].each do |k,v|
      unless v.is_a?(Hash)
        add_color_classes(k,v)
      else
        add_nested_color_classes(k,v)
      end
    end
    puts @css
  end

  def add_color_classes(k,v)
    @css << ".text-#{k} {\n\tcolor: #{v};\n}"
    @css << ".bg-#{k} {\n\t background-color: #{v};\n}\n"
  end

  def add_nested_color_classes(k,v)
    v.each do |kk,vv|
      if kk.to_s == "DEFAULT"
        add_color_classes("#{k}",vv)
      else
        add_color_classes("#{k}-#{kk}",vv)
      end
    end
  end

end

Merging rails7 branch with master

One of my major fears was -> What if I merged and Capistrono would not deploy. I spent way too much time trying to figure out how to deploy a branch to my staging server. I found posts on how to do it (old) and nothing worked.

I then just faked it and ran staging locally.

  • set staging DB to point to develoment
  • bin/rails assets:precompile
  • bin/rails s -e staging

This was where I found I had the bundling issues with css classes in components not being picked up. After a little tuning things were working fine.

Had to remember to bin/rails assets:clobber when I moved back to development.

Everything seemed to be working fine, especially after a bunch of refactoring. It was time to merge. Made sure I had a backup clone of my repo and did a merge after I figured out how to do it (I've been using the OSX version of Fork and am not proficient with git or Fork.

Devepment worked fine, as expected.

cap staging deploy also worked without any changes to the Capistrono config file.

Played with it with a few more refactoring tweeks and the deployed to production!

and I'm fine and

I'm on Rails 7 without any Webpacker crap!!!

107