TypeScript and jsbundling and Rails 7
This is a quick bit of service journalism about one thing that seemed less than obvious about converting the code in Modern Front-End Development For Rails to Rails 7.0, namely how to integrate TypeScript with the new tools.
Specifically, the Rails 7.0 version of the code ditches Webpacker in favor of the new jsbundling-rails and cssbundling-rails gems and uses esbuild instead of webpack. See here for an only-slightly out-of-date description of the new tools.
Most of the logistical changes that fall from the new tools involve things like changing some file names and tweaking some configuration, updating how we create manifests and so on.
It was not, however, immediately clear to me how to get TypeScript fully working in this setup.
The way that jsbundling-rails works is that it uses the same package.json
file as before, but instead of using Webpacker and creating an entire webpack configuration in-house, it just passes the whole thing to your bundling tool of choice, and just asks that the bundling tool places the resulting bundled file in the output directory (by default it’s app/builds
) so that the asset handler (Sprockets, by default) can just pass the bundled file down to the browser.
In practice, this is significantly less complicated, especially if you use esbuild instead of webpack, because esbuild has less configuration in general. It’s also significantly faster.
The installation for jsbundling-rails adds a script to the package.json
file that calls esbuild, and also adds a Procfile
with a command that triggers that script if a relevant file changes.
Here’s the scripts
part of a potential package.json
file, with the build commands for both jsbundling and cssbundling — these have been tweaked from the defaults just slightly.
"scripts": {
"build:js": "esbuild app/javascript/*.* --bundle --sourcemap outdir=app/assets/builds",
"build:css": "tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/tailwind.css"
}
And here’s the associated Procfile
:
web: bin/rails server -p 3000
js: yarn build:js --watch
css: yarn build:css --watch
It starts three processes: the Rails server itself, a watcher for JavaScript changes and a watcher for CSS changes.
That’s fine, but when we get to TypeScript, esbuild has less functionality than webpack. Specifically, esbuild will convert a TypeScript file into a JavaScript file by removing type annotations and whatnot, but esbuild will not, by default, run the TypeScript compiler to determine if the code is type safe.
This seems like a problem, since if you are using TypeScript, you presumably are doing so because you want the code to be type safe.
The esbuild docs say “ you will still need to run tsc -noEmit
in parallel with esbuild to check types”, but doesn’t really say how to do that, and random Google searches were of mixed benefit, because a lot of the suggestions on how to do this brought in more complication than seemed necessary.
Happily, I found the tsc-watch package, whose sole purpose in life is to run the TypeScript compiler in watch mode and allow you to specify what to do on success or on failure.
I installed it:
$ yarn add --dev tsc-watch
And then I updated my scripts as follows:
"scripts": {
"build:js": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",
"build:css": "tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/tailwind.css",
"failure:js": "rm ./app/assets/builds/application.js && rm ./app/assets/builds/application.js.map",
"dev": "tsc-watch --noClear -p tsconfig.json --onSuccess \"yarn build:js\" --onFailure \"yarn failure:js\""
}
The build.js
command stays the same, but I’ve added a new dev
command that uses tsc-watch
.
I’m calling tsc-watch
with four arguments:
noClear
, which preventstsc-watch
from clearing the console window. I’d like to do that myself, thank you very much.-p tsconfig.json
, points to the TypeScript config file that should govern the compilation I want to do:--onSuccess \"yarn build:js\"
, controls what you want to have happen if the TypeScript compilation succeeds. In our case, we want the regular esbuildbuild:js
to happen, since we now know the code is type safe.--onFailure \"yarn failure:js\"
, controls what you want to have happen in the TypeScript compilation fails. I guess I had options here, but what I chose to do is"rm ./app/assets/builds/application.js && rm ./app/assets/builds/application.js.map"
, meaning removing the existing esbuild files from the build directory so that the development browser page will error rather than return the most recent successful compilations. I thought that allowing the most recent success to stick around would be confusing.
Then to get this to work, we need the dev
command to be in the Procfile
instead of build:js
web: bin/rails server -p 3000
js: yarn dev
css: yarn build:css --watch
And that works. The yarn dev
sets itself up as a watcher automatically, we don’t need to pass --watch
to it.
When a relevant file changes, tsc-watch
is triggered and runs the TypeScript compiler. If the compile is successful, and only if it is successful, esbuild is called upon to bundle the code into a browser friendly form. If the compile fails, then we delete the last successful compile. The error message goes to the console, and we presumably fix the error.
I’m not sure that this is the 100% best way to make this work, but it seems to be working fine as I mess with the code for Modern Front-End to bring it to Rails 7 features.
Dynamic Ruby is brought to you by Noel Rappin.
Comments and archive at noelrappin.com, or contact me at noelrap@ruby.social on Mastodon or @noelrappin.com on Bluesky.
To support this newsletter, subscribe by following one of these two links: