Accommodating Safari Users

ghostly figures with browser icons looking over a fence.  The Safari ghost is standing on a wooden box enabling it to see over the fence.
Image by Annie Ruygt

We’re Fly.io. We run apps for our users on hardware we host around the world. Fly.io happens to be a great place to run Rails applications. Check out how to get started!

When you generate a new Rails app, you are given a choice whether or not you want JavaScript support or to include a CSS framework. The default is to use import maps. What isn’t clear at this time is that there is no documented upgrade path should you change your mind later. In fact, upgrading is difficult as support for things like Turbo, Stimulus, Action Cable and Action Text further lock you into this choice.

Import maps are great until you realize that:

  • Many of your users use MacBooks or iMacs. cool
  • Many of these users don’t choose to upgrade their operating systems understandable
  • Many of these users use Safari ouch

Why ouch? The problem is that Safari is the only major browser that isn’t evergreen. What that means is that if you use Stimulus things like the following will not work for these users of versions of Safari less than 14.1:

static targets = [ "name", "output" ]

And, yes, I do have users on Sierra and High Sierra. Unfortunately even if they upgrade there always will be the possibility that some other JavaScript I write will work on the various browsers that I test with – including recent versions of Safari – just not with the older browsers that they use.

There are ways around this, of course. And there are tools to perform the necessary changes for you automatically. One such tool is esbuild. Unfortunately, Rails makes it difficult for you.

  • Despite not being written in JavaScript, Rails makes you buy into the whole Node.js ecosystem if you want to use esbuild. This being despite the fact that no such buy in is required for Tailwindcss despite that tool being written in JavaScript. There is nothing preventing this from being done, and I have done so for sprockets-esbuild, it is just an explicit choice by the Rails team.
  • Despite Rails import map support being built on sprockets by default, and despite sprockets supporting both transpilation and source maps, import maps explicitly limits inputs to files that don’t require transpilation. Of course it is possible to monkey patch import maps, and I did so back in 2021 and offered this to the Rails team, but once again it was explicitly rejected.

So that’s the problem in a nutshell. I have a demonstrable need. Going back and switching to esbuild both presents a nearly insurmountable challenge and furthermore is undesirable under the terms the Rails team has provided. Workarounds using sprockets is fragile hack involving monkey patching.

So I came up with another solution. I present lib/tasks/esbuild.rake:

 # minify js controllers and target older browsers
Rake::Task['assets:precompile'].enhance do
  Dir.chdir 'public/assets/controllers' do
    files = Dir['*.js'] - 
            Dir['*.js.map'].map {|file| File.basename(file, '.map')}

    unless files.empty?
      sh "esbuild", *files, *%w(
        --outdir=.
        --allow-overwrite
        --minify
        --target=es2020
        --sourcemap
      )
    end
  end    
end

What the above does is run esbuild against all of the controller js files that have yet to converted to es2020 and minimized. Sourcemaps are both produced and used to track whether or not the conversion has yet to be done.

While given the constraints, every solution is going to be a bit of a hack, this is the least hacky solution I have come up with to date in that:

Each of these steps are fully documented and supported.

The options used by this script:

  • --outdir=. directs that the files be updated in place.
  • --allow-overwrite allows the files to be updated in place.
  • --minify will reduce whitespace, shorten identifiers, and use equivalent but shorter syntax whenever possible.
  • --target=es2020 will rewrite js that uses features introduced after this point. Adjust as needed.
  • --sourcemap will generate source map files to enable debugging with the original source

The end result is scripts that not only will run on older browsers, but because they are smaller they will actually download faster.

I do note that sprockets will also create .gz files. The above script will leave those files alone. As far as I can tell those files aren’t used.

All that is left to be done is to install esbuild separately. This can be done with apt, brew, npm or other techniques. An approach that works well for Dockerfiles:

# Install esbuild
RUN chdir /usr/local/bin && \
    curl -fsSL https://esbuild.github.io/dl/latest | sh

That’s it! Even though it is a bit of a hack, it is a set and forget operation.

Now all of the users of my application can enjoy all the interactivity my stimulus controllers provide.

Fly.io ❤️ Rails

Fly.io is a great way to run your Rails HotWired apps. It’s really easy to get started. You can be running in minutes.

Deploy a Rails app today!