LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

【転職ドラフト】Rails 6.1 + esbuild なアプリケーションに PostCSS を使って TailwindCSS を導入する話

こんにちは。転職ドラフトでエンジニアをしている @verdy_266 です。

転職ドラフトでは、デザインシステムを導入するために、 Bootstrap から TailwindCSS への移行を進めようとしています。その際、思った以上にネットの情報が少なく苦戦した点があったので、導入事例をまとめたいと思います。

サマリ

  • 転職ドラフトでデザインシステムを導入するため、 Bootstrap から TailwindCSS に置き換えていくプロジェクトが進行中です。
  • Rails 6.1 + esbuild を使用しているアプリケーションでは若干 TailwindCSS の導入方法が特殊で、最終的には application.scss からインポートする方法を採用しました。
  • 新たに使用したユーティリティクラスのスタイルが効かない問題を、view テンプレート保存時に application.scss のタイムスタンプを更新することで解消しました。

デザインシステムとは

デザインシステムとは、「一貫したデザインや操作性でウェブサイトやアプリを提供するための仕組み」です。ボタンはこういうルールのもとでデザインしましょう、ボタンの色はこういう基準で選択しましょう、というようなルールを定義しておくことで、デザインに詳しくない人でも一貫性のあるデザインを作ることができます。

デザインシステムとしては、デジタル庁のものが有名です。

www.digital.go.jp

TailwindCSS の導入背景

さまざまな職種のメンバーが施策の立案をする転職ドラフトにおいては、仕様書を作成する際にすり合わせを行う工数が大きくなりがちです。デザインシステムにデザインのルールがまとまっていることで、デザインに関してすり合わせの工数が軽減されるメリットがあります。

そこで転職ドラフトでもデザインシステムを導入しようという動きがあるのですが、デザインシステムを導入するにあたっては、 CSS フレームワークとして何を使用しているのかが重要になってきます。特に今まで転職ドラフトで使用してきた Bootstrap では、見た目用のスタイル( background-color や font-size など)と配置用のスタイル( display: flex や margin など)が分離されていないため、柔軟なスタイルの指定が難しいようでした。そこで、新しい CSS フレームワークとして TailwindCSS を導入することになりました。

既存のスタイルを全て置き換えるには相当な時間がかかるため、Bootstrap の使用を終了するまでは TailwindCSS と Bootstrap が共存した状態を実現する必要があります。

TailwindCSS の導入

TailwindCSS のインストールガイドなどをみていると、CSS をビルドするために $ npx tailwindcss -i input.css -o output.css --watch を実行したり、 PostCSS とともに導入して $ postcss input.css -o output.css を実行したりする方法が紹介されていますが、転職ドラフトでは esbuild を使用しているため、 watch コマンドを別途実行するためには、新たにコンテナを立ち上げるなどの必要が出てきます。

// package.json

"scripts": {
  "build:js": "node esbuild.config.js",
  "watch:js": "node esbuild.config.js --development --watch",
  // ...
}
# docker-compose.yml

services:
  # ...
  dev-server:
    command: ["yarn", "watch:js"]
    entrypoint: ["yarn", "watch:js"]
    # ...

1つのコマンドを実行するためにコンテナを増やすのは、デプロイフローの見直しなどが必要になるので嬉しくありません。できれば esbuild の範囲で TailwindCSS のビルドまで済ませたいものです。

そこで、いくつかの方法を試しました。

context.rebuild を並べてみる

esbuild.config.js では最終的に context.rebuild() を実行しているので、context を増やして並列で rebuild させたらいいんじゃないか?という発想です。結構無理やりな自覚はあります。

// esbuild.config.js

const config = {
    // 省略
}
const config_tailwind = {
    entryPoints: ['app/assets/stylesheets/input.css'],
    bundle: true,
    outfile: 'app/assets/stylesheets/output.css',
    plugins: [ postCssPlugin.default() ],
    loader: { '.css': 'css' },
};

(async () => {
    if (args.includes('--watch')) {
        chokidar.watch(path.join(process.cwd(), 'app/assets/{javascripts,stylesheets}/**/*.*')).on("all", async (event, path) => {
            if (event === "change") {
                // ...
                await ctx.rebuild();
                await ctx_tailwind.rebuild();
                // ...
            }
            const ctx = await esbuild.context(config)
            await ctx.watch()
            const ctx_tailwind = await esbuild.context(config_tailwind)
            await ctx_tailwind.watch()
        }
    }
})

やってみると、既存のパイプラインで watch している内容と競合するのか、一瞬は TailwindCSS のスタイルがビルドされるものの、しばらくすると内容が戻ってしまうという不具合が起きてしまいました。

application.scss で import する

インストールガイドなどをみると、 TailwindCSS のインプットファイルは以下のような記述をしましょうと記載があります。

@tailwind base;
@tailwind components;
@tailwind utilities;

ですが、以下のように記載することで、 別のファイルからインポートすることができます。

@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

これを、 Rails プロジェクトで大元に存在する application.scss に記述してあげたら、 esbuild の方は大きくいじる必要はなさそうでした。

一部、esbuild で esbuild-sass-plugin の transform()を使用しました。

// esbuild.config.js

const { sassPlugin } = require('esbuild-sass-plugin')
const postcss = require('postcss');
const tailwindcss = require('tailwindcss');

const config = {
    // ...
    plugins: [
        sassPlugin({
            // ...
            async transform(source, resolveDir) {
                const { css } = await postcss([tailwindcss]).process(source)
                return css
            },
        }),
    ]
}

esbuild-sass-plugin の transform() は、esbuild に CSS を渡す前に実行される関数を定義します。ここでは PostCSS を使って TailwindCSS のビルドを事前に行った上で、 esbuild によって scss のトランスパイルやバンドルの処理をさせるようにします。

これで、基本的には esbuild を使用するプロジェクトに TailwindCSS を導入することができました 🎉

新規に使用したユーティリティクラスのスタイルが適用されない問題

TailwindCSS では、ビルドされる CSS ファイルの容量が肥大化するのを防ぐため、view テンプレートで使用したクラスのみをビルドするようになっています。すると、次のような不都合が生じます。

  1. view テンプレートで、今まで使用していなかったユーティリティクラスを使用してスタイルを適用させようとした
  2. TailwindCSS 関連のファイルは変更されていないため、 view テンプレートを保存しただけでは TailwindCSS のビルドは行われない
  3. 開発環境でブラウザをリロードしても、html 上ではクラスが当たっているのに、スタイルは適用されていないという事態が起こる

これを解決するため、 view テンプレートのファイルを保存した際に application.scss に touch コマンドを送ってタイムスタンプを更新することで、強制的にビルドし直すようにしました。

// esbuild.config.js

const fs = require('fs')

const reloadTailwind = () => {
    const filesImportingTailwind = [
        'app/assets/stylesheets/application.scss'
    ].map(file => path.join(process.cwd(), file));
    const time = new Date();
    filesImportingTailwind.map((file) => fs.utimesSync(file, time, time));
}

(async () => {
    if (args.includes('--watch')) {
        chokidar.watch([
            path.join(process.cwd(), 'app/assets/{javascripts,stylesheets}/**/*.*'),
            path.join(process.cwd(), 'app/views/**/*.{slim,erb}'),
        ]).on("all", async (event, path) => {
            if (event === "change") {
                try {
                    reloadTailwind();
                    await ctx.rebuild();
                }
            }
        }
    }
})

おわりに

転職ドラフトでの TailwindCSS 導入についてお話ししました。
一朝一夕には行かないですが、転職ドラフトでデザインシステムを整備していくことで、より施策が立案しやすい環境が整っていく予定です。今後の転職ドラフトにご期待ください!