随分前の話にはなるが,Rails7からRailsで使用できるJSのデフォルトがimport-mapとなり,Webpackerは非推奨となった.
現在ではもはやWebpackerはサポートされておらず,今後はNodeやその他パッケージのアップデートによって使用できなくなると思われる.
RailsにJavaScriptを導入するときの選択肢
Rails7からRailsでJavaScriptを使用するときに取れる選択肢は,主に3つある.
- import-map
- Shakapacker
- jsbundling-rails経由の外部バンドラーとJSランタイム
import-map
Rails7からの標準では,このimport-mapを使ったJavaScript環境の仕様が推奨されている.
Webpackerまでは,JSのバンドリングやトランスパイルなどをサーバ側で行うことで,JavaScriptをクライアント側で使用できるようにしていた.
しかし,import-mapからは,「バージョン付けされたファイルに対応する論理名を用いてJavaScriptモジュールをブラウザで直接import」できる.
参考そのため,サーバ側で面倒なことを行わずシンプルな状態を保ちながらJavaScriptを使用することができるようになったことになる.
導入方法としてはRailsガイドにあるように,
-
import-map-railsをbundlerでインストールし, - npmパッケージを
bin/importmap pinでインストールして, -
application.jsをインポートするだけ.
後はJavaScriptを書けば自動でホットリロードまでしてくれる.正直,プレーンな新しく作るRailsアプリケーションに軽くJavaScriptを導入するだけならばこれで十分だと思う.
jsbundling-rails
軽くJSを使う程度のRailsアプリケーションならば上記で対応できるだろうが,TypeScirptからトランスパイルする場合などは引き続きJavaScriptのバンドラーを使用しなければならない.このような場合は,JavaScriptのバンドラーとランタイムの2つを外部からインストールしてこなければならない.
JSバンドラーとRailsの「仲立ち」をするjsbundling-rails
jsbundling-railsは,直接JavaScriptファイルをバンドリングしてくれるのではなく,RailsアプリケーションとJSバンドラーの間の仲立ちをして,下記のことをしてくれる.
- JSバンドラーにRailsアプリケーション上のJavaScriptファイルをバンドルさせる
- JSバンドラーにバンドルされたJSファイルをbuildsディレクトリ上に配置させる
要するに,本来はRailsアプリケーションと関係のないJSバンドラーをRails上で使えるようにしてくれるツールである.
JavaScriptバンドラーの選択肢
JavaScriptファイルを実際にバンドルするJavaScriptバンドラーには,いくつか種類がある.
- Bun: JavaScriptバンドラーと,バンドラーを動作させるためのJavaScriptランタイムが一体になっている.ビルドも高速.
- esbuild: Node.jsと一緒に使う.ビルド時間が早いのが売り
- rollup.js: Tree-Shaking機能によって実際は使われていないコードを自動的に除外してくれるため,バンドル後のコードがシンプルになる
- webpack: Rails6までのWebpackerと一緒に使われていた.昔からあるバンドラー
特徴的なのは,Node.jsとは別のJavaScriptランタイムを内包したBunである.そのほかのJSバンドラーはNode.jsと一緒に使うことを前提としている.
JSを動作させるJavaScriptランタイム
JSバンドラーを使えるようにするには,サーバ上でJSファイルが動作できる環境を提供するJavaScriptランタイムが必要になる.有名どころで言うとNode.jsが使われていて,Bun以外のバンドラーを使用するときには必然的にNode.jsと,それに付随するyarnを導入することになる.
Shakapacker
Shakapakerは,Webpackerとほぼ同等の機能を提供するgemで,Rails標準ではなく、有志による開発であるものの、機能的には実質的な後継といえる.
JavaScriptバンドラーにWebpackを用い,JavaScriptのバンドルとbuildsディレクトリへの移動を今までWebpackerがやってくれていたようにやってくれる.
jsbundling-railsと外部のバンドラーを使うときの違いは,大きなところでは下記3つ.
- Shakapackerではホットリロード(開発中に自動でJSファイルの差分を検知してバンドルし直してくれる)がある
- Shakapackerはコードスプリッティング(必要なJSファイルだけを読み込んでくれる.SEOに効果的)の機能がある
- jsbundling-railsはそのような機能がない分シンプル
詳しくはRails: Webpacker(Shakapacker)とjsbundling-railsの比較(翻訳)が参考になる.
今回とった方法
今回のJS導入では,Bunを使ってみることにした.理由はビルドが高速である点,これまで必要だったNode.jsが必要でなくなると言う点で,導入方法における学びが多そうだと感じたためである.
DockerfileでBunのランタイムをインストール
※この記事のコマンドを実行したコンテナの構築手順は私の過去のブログを参照。
※完成したリポジトリに少し改善を施したものはGitHubリポジトリを参照。
Rails7からは、Bunは標準ではインストールされないため、Dockerfileに記述してインストールする。公式のインストールガイドを参照しながらDockerfileに記述する。
今回は、インストール時のPATH周りがよくわからなかったため、一部Copilotの助けを借りた。
RUN apt-get update -qq \
# Bun、nvm、Node.js、Yarnのインストール
&& curl -fsSL https://bun.sh/install | bash \
&& export BUN_INSTALL="$HOME/.bun" \
&& export PATH="$BUN_INSTALL/bin:$PATH" \
&& bun --version \
jsbundling-railsのインストール
Bunをインストールできたが、このままではRailsからBunを使えるようにはならない。 Gemfile を編集し、jsbundling-railsをインストールしていく。
gem "jsbundling-rails"
これができたら、 bundle install をやり直す。
docker compose exec web bundle install
jsbundling-railsをBun向けに初期化する
これでBunの実行に必要なパッケージとGemは手に入った。
ここからは、BunをRailsアプリケーションで使えるようにするために、Bunの初期化をしていく。
jsbundling-railsの公式ドキュメントの手順に従い、下記のコマンドを実行していく。
docker compose exec web rails javascript:install:bun
このコマンドを実行すると、BunをRailsアプリケーションで使えるようにするための設定ファイル群が作成される。具体的には下記のようなファイルが作成または編集される。
- .gitattribute # Bunのlockファイルに関する設定
- .gitignore # buildsディレクトリの中身とnode_modulesディレクトリをgitから除外するための設定追加
- Procfile.dev # bin/devコマンドがforemanを使ってRailsサーバとBunのビルドを同時に実行させるための設定ファイル
- manifest.js # バンドルされたJSファイルを読み出すために app/assets/buildsのpathを追加
- application.html.erb # バンドルされたJSファイルをHTMLに読み込むためのタグ
- dev # foremanを起動させるためのシェルスクリプト
- bun.config.js # Bun.buildでバンドルを実行するためのスクリプト
- package.json # bun add コマンドでインストールしたパッケージの依存関係を記述
import-map用の設定を削除
Railsアプリケーションは、デフォルトでimport-mapを使うように構築される。今回はデフォルトの状態で作成したRailsアプリケーションに後からjsbundilng-railsとbunの組み合わせをインストールしたので、下記のファイルを編集してimport-mapの設定を削除する。
manifest.js
app/assets/builds ディレクトリを読み込むため、app/assets/config/manifest.js を開き、 app/javascript ディレクトリを読み込む設定を削除する。このapp/javascriptディレクトリは、import-mapがJavaScriptファイルを読み込むためのパス指定のため、もう必要ない。
//= link_tree ../images
//= link_directory ../stylesheets .css
- //= link_tree ../../javascript .js <--- 削除
//= link_tree ../../../vendor/javascript .js
//= link_tree ../builds
application.js
Stinmulusを使う場合、コントローラ類は app/javascript/controllers ディレクトリに書く。しかし、import-mapとbunとではディレクトリの参照方法が異なる。bunでは相対パスで指定しなければならないため、編集する。
import "@hotwired/turbo-rails"
- import "controllers"
+ import "./controllers" // 相対パスに変更
application.html.erb
javascript_include_tag を使うため、 javascript_importmap_tags を削除
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
- <%= javascript_importmap_tags %> <!--削除-->
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %> <!--Bunの初期化で追記されたタグ-->
これで、RailsアプリケーションでBunを使う準備が整った。あとは動作確認をする。
Stimulusを使った動作確認
パッケージのインストール
まずは、Railsアプリケーションを作成したとにデフォルトのjsファイルで参照されているbunでstimulusとturbo-railsをインストールする。既存のjsファイルを消せばstimulusだけでよいかも。
docker compose exec web bash # コンテナのコンソールに入る
bun add @hotwired/stimulus
bun add @hotwired/turbo-rails
新しいページを作成
下記のように、rails generateコマンドを使って新しいページを作成する。
(なおDocker環境の場合、コンテナの作成時に appuser などの名前でホストOS側と同じUID、GIDを持つ実行ユーザを作成し、そのユーザでrails generateコマンドを実行すると、作成したファイルの所有者を変更しなくてもそのまま編集できる。)
docker compose exec web bash # rootユーザで実行する場合
docker compose exec -u appuser web bash # 事前にappuserを作成した倍
rails generate controller Hoge index
こうすると、新しく controllers/hoge_controller.rb と views/hoge/index.html.erb ファイルが作成され、 routes.rb にも新しくルーティングが作成される。
create app/controllers/hoge_controller.rb
route get 'hoge/index'
invoke erb
create app/views/hoge
create app/views/hoge/index.html.erb
invoke test_unit
create test/controllers/hoge_controller_test.rb
invoke helper
create app/helpers/hoge_helper.rb
invoke test_unit
Stimulusコントローラの作成と登録
下記のように、 app/assets/controllers ディレクトリの下に hoge_controller.js を作成する。
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
alert("ほげほげほげ~~~~~")
}
}
新しいStimulus Controllerを作成したら、コンテナの中で下記のコマンドを実行してapp/javascript/controllers/index.jsを更新する
docker compose exec -u appuser web bash
rails stimulus:manifest:update
すると、ファイルが以下のように変化する。
before:
// Import and register all your controllers from the importmap via controllers/**/*_controller
import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)
after:
// This file is auto-generated by ./bin/rails stimulus:manifest:update
// Run that command whenever you add a new controller or create them with
// ./bin/rails generate stimulus controllerName
import { application } from "./application"
import HelloController from "./hello_controller"
application.register("hello", HelloController)
import HogeController from "./hoge_controller"
application.register("hoge", HogeController)
このファイルはStimulusのcontrollerを登録しているとわかるが、beforeではimport-map用の設定だったのが、afterではbunに対応した設定になっている。新しい hoge_controller.js も登録されている。
次に、先ほど作成した views/hoge/index.html.erb に、上記のコントローラを読み込む data-controller 属性で hoge コントローラを指定する。
<h1>Hoge#index</h1>
<p>Find me in app/views/hoge/index.html.erb</p>
+ <div data-controller="hoge">
次に、コンテナ内で bun run build --watch を起動する。
docker compose exec web bash
bun run build --watch
ビルドが成功すれば $ bun bun.config.js --watch 以外 何も表示されないはずなので、そうなればブラウザを開いて localhost:3000/hoge/index を開くと、上記のコントローラに書いた文言がalertになって出てくる。
※もし Asset application.js was not declared to be precompiled in production. というエラーが出てきた場合、コンテナを--no-cacheでbuildし直してからもう一度起動させるとよいかもしれない。これは本番環境でprecompileされたapplication.jsファイルがないというエラーなのだが、本来このエラーはdevelopment環境では発生しないはず。私の場合、このコンテナを作る過程でDockerfileにRAILS_ENV=productionという環境変数を混ぜてしまっていたことがあったので発生したのかもしれないが、原因は定かではない。
おまけ: JSファイルの構文エラーがあるときにビルドスクリプトが落ちる場合
2026年2月時点では、Bun.buildを実行した段階でJSファイル内にエラー箇所があると、例外が出てbuildスクリプト bun.config.js が落ちるようになってしまう。
原因は不明だがこれでは開発が不便なので、下記のように強制的にエラーを catch して握りつぶしてしまい、落ちないようにするとよい。
下記は、Copilotに書いてもらった例である。
import path from 'path';
import fs from 'fs';
const config = {
sourcemap: "external",
entrypoints: ["app/javascript/application.js"],
outdir: path.join(process.cwd(), "app/assets/builds"),
};
const build = async (config) => {
try {
console.log("Build configuration:", config);
const result = await Bun.build(config);
if (!result.success) {
console.error("Build failed with the following logs:");
for (const message of result.logs) {
console.error(message);
}
if (!process.argv.includes('--watch')) {
process.exit(1);
}
return;
} else {
console.log("Build success");
}
} catch (error) {
console.error("An unexpected error occurred during the build process:", error);
if (!process.argv.includes('--watch')) {
process.exit(1);
}
}
};
(async () => {
await build(config);
if (process.argv.includes('--watch')) {
fs.watch(path.join(process.cwd(), "app/javascript"), { recursive: true }, (eventType, filename) => {
console.log(`File changed: ${filename}. Rebuilding...`);
build(config);
});
} else {
process.exit(0);
}
})();
まとめ
Rails7以降ではWebpackerが非推奨となり、TypeScriptを使うなどの事情で標準となったimport-mapを使わない場合は自分で外部からJavaScriptランタイムとJavaScriptバンドラーをインストールしてセットアップしなければならない。そのとき、インストールしたJavaScritpバンドラーを使えるようにするために、jsbundling-rails を使う。
今回は、ビルドの早さに注目してBunを導入してみたが、import-map向けに初期化されたRailsアプリケーションに導入するには、js周りの設定ファイルを書き換える必要がある。
BunはJSバンドラーの中では新しい技術だと思うが、Node.jsとの互換性がどれくらいあるのかが気になるところ。
動作環境
- Ubuntu 24.04.1 LTS (Windows 11 Pro WSL上)
- Docker Enginev28.1.1 (Windows版Docker Desktop上)