Rails 7 + React + tailwind のプロジェクト新規作成(esbuild)
Rail7 +esbuildでReactが書けるアプリを新規で構築してみる
環境:
OS: M2 Macbook Air
ruby: v3.2.2
node: 20.9.0
rails newのオプション
結構知らないのが色々ある
今回は最小構成にして、必要なものをあとから使いたいので --minimal
相当で構築することにする
minimalはこの辺がskipされるらしい
Based on the specified options, the following options will also be activated:
--skip-active-job [due to --minimal]
--skip-action-mailer [due to --skip-active-job, --minimal]
--skip-active-storage [due to --skip-active-job, --minimal]
--skip-action-mailbox [due to --skip-active-storage, --minimal]
--skip-action-text [due to --skip-active-storage, --minimal]
--skip-javascript [due to --minimal]
--skip-hotwire [due to --skip-javascript, --minimal]
--skip-action-cable [due to --minimal]
--skip-bootsnap [due to --minimal]
--skip-dev-gems [due to --minimal]
--skip-jbuilder [due to --minimal]
--skip-system-test [due to --minimal]
ただし、今回は --skip--javascript
をしてほしくないので(後述)minimal相当のオプションを明示していく方式にする(--minimal
と --javascript
オプションを同列に並べても --javascript
が負けてしまった)
Reactを書くに当たって、今回はesbuild
をビルド環境に使いたいので、 --javascript esbuild
を明示する
shakapacker(webpackerの後継)と比べると低機能だが軽いらしい
importmapというオプションもあってこれはCDNからjsのモジュールを読み込むのでnodeの実行環境すら必要ないが、reactはjsxのビルドが必要なので単体では機能せず追加でビルド環境を足す必要がある
shakapackerは前に試したので今回はesbuild
ついでにtailwindも使いたいので --css tailwind
も明示する
つまりこう
rails new --name my-new-app \
--skip-active-job \
--skip-action-mailer \
--skip-active-storage \
--skip-action-mailbox \
--skip-action-text \
--skip-hotwire \
--skip-action-cable \
--skip-bootsnap \
--skip-dev-gems \
--skip-jbuilder \
--skip-system-test \
--javascript esbuild \
--css tailwind \
.
(ディレクトリは先に切ってあり、カレントがそこ)
いい感じ
tree . -I node_modules/
.
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── Procfile.dev
├── README.md
├── Rakefile
├── app
│ ├── assets
│ │ ├── builds
│ │ │ ├── application.css
│ │ │ ├── application.js
│ │ │ └── application.js.map
│ │ ├── config
│ │ │ └── manifest.js
│ │ ├── images
│ │ └── stylesheets
│ │ └── application.tailwind.css
│ ├── controllers
│ │ ├── application_controller.rb
│ │ └── concerns
│ ├── helpers
│ │ └── application_helper.rb
│ ├── javascript
│ │ └── application.js
│ ├── models
│ │ ├── application_record.rb
│ │ └── concerns
│ └── views
│ └── layouts
│ └── application.html.erb
├── bin
│ ├── bundle
│ ├── dev
│ ├── docker-entrypoint
│ ├── rails
│ ├── rake
│ └── setup
├── config
│ ├── application.rb
│ ├── boot.rb
│ ├── credentials.yml.enc
│ ├── database.yml
│ ├── environment.rb
│ ├── environments
│ │ ├── development.rb
│ │ ├── production.rb
│ │ └── test.rb
│ ├── initializers
│ │ ├── assets.rb
│ │ ├── content_security_policy.rb
│ │ ├── filter_parameter_logging.rb
│ │ ├── inflections.rb
│ │ └── permissions_policy.rb
│ ├── locales
│ │ └── en.yml
│ ├── master.key
│ ├── puma.rb
│ └── routes.rb
├── config.ru
├── db
│ └── seeds.rb
├── lib
│ ├── assets
│ └── tasks
├── log
│ └── development.log
├── package.json
├── public
│ ├── 404.html
│ ├── 422.html
│ ├── 500.html
│ ├── apple-touch-icon-precomposed.png
│ ├── apple-touch-icon.png
│ ├── favicon.ico
│ └── robots.txt
├── storage
├── tailwind.config.js
├── test
│ ├── controllers
│ ├── fixtures
│ │ └── files
│ ├── helpers
│ ├── integration
│ ├── models
│ └── test_helper.rb
├── tmp
│ ├── cache
│ │ └── assets
│ ├── pids
│ └── storage
├── vendor
└── yarn.lock
40 directories, 53 files
Rails7ではプロジェクト初期構築時にDockerfileも作ってくれて、これが地味に便利
Dockerfile
# syntax = docker/dockerfile:1
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
ARG RUBY_VERSION=3.2.2
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
# Rails app lives here
WORKDIR /rails
# Set production environment
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"
# Throw-away build stage to reduce size of final image
FROM base as build
# Install packages needed to build gems and node modules
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential curl git node-gyp pkg-config python-is-python3
# Install JavaScript dependencies
ARG NODE_VERSION=20.9.0
ARG YARN_VERSION=1.22.19
ENV PATH=/usr/local/node/bin:$PATH
RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \
/tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \
npm install -g yarn@$YARN_VERSION && \
rm -rf /tmp/node-build-master
# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git
# Install node modules
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
# Copy application code
COPY . .
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# Final stage for app image
FROM base
# Install packages needed for deployment
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libsqlite3-0 && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Copy built artifacts: gems, application
COPY /usr/local/bundle /usr/local/bundle
COPY /rails /rails
# Run and own only the runtime files as a non-root user for security
RUN useradd rails --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp
USER rails:rails
# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
# Start the server by default, this can be overwritten at runtime
EXPOSE 3000
CMD ["./bin/rails", "server"]
Procfile.dev というエントリポイントができている
これでrails serverとフロントビルド環境を同時に起動するらしい
web: env RUBY_DEBUG_OPEN=true bin/rails server
js: yarn build --watch
css: yarn build:css --watch
なので、開発環境は rails s
ではなく bin/dev
というエントリポイントを利用する
プロセスマネージャはforemanらしい
#!/usr/bin/env sh
if ! gem list foreman -i --silent; then
echo "Installing foreman..."
gem install foreman
fi
# Default to port 3000 if not specified
export PORT="${PORT:-3000}"
exec foreman start -f Procfile.dev "$@"
とりあえず起動 ほぼまっさらとはいえ開発環境上がるの早いな
> ./bin/dev
01:47:45 web.1 | started with pid 72333
01:47:45 js.1 | started with pid 72334
01:47:45 css.1 | started with pid 72335
01:47:45 js.1 | yarn run v1.22.19
01:47:45 css.1 | yarn run v1.22.19
01:47:45 js.1 | $ esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --watch
01:47:45 css.1 | $ tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --minify --watch
01:47:45 js.1 | [watch] build finished, watching for changes...
01:47:45 css.1 |
01:47:45 css.1 | Rebuilding...
01:47:45 css.1 |
01:47:45 css.1 | warn - No utility classes were detected in your source files. If this is unexpected, double-check the `content` option in your Tailwind CSS configuration.
01:47:45 css.1 | warn - https://tailwindcss.com/docs/content-configuration
01:47:45 css.1 |
01:47:45 css.1 | Done in 100ms.
01:47:45 web.1 | DEBUGGER: Debugger can attach via UNIX domain socket (/var/folders/gq/2k4rl7w53y10zd0g9xb_94j00000gn/T/ruby-debug-sock-501/ruby-debug-s10akir-72333)
01:47:45 web.1 | => Booting Puma
01:47:45 web.1 | => Rails 7.1.2 application starting in development
01:47:45 web.1 | => Run `bin/rails server --h01:47:45 web.1 | Puma starting in single mode...
01:47:45 web.1 | * Puma version: 6.4.0 (ruby 3.2.2-p53) ("The Eagle of Durango")
01:47:45 web.1 | * Min threads: 5
01:47:45 web.1 | * Max threads: 5
01:47:45 web.1 | * Environment: development
01:47:45 web.1 | * PID: 72333
01:47:45 web.1 | * Listening on http://127.0.0.1:3000
01:47:45 web.1 | * Listening on http://[::1]:3000
01:47:45 web.1 | Use Ctrl-C to stop
Yay! You're on Rails! って言わないんだ...(Rails老人会)
tailwindが効いているか確認したいのでとりあえず雑にエンドポイントを追加
> bundle exec rails g controller Top index
create app/controllers/top_controller.rb
route get 'top/index'
invoke erb
create app/views/top
create app/views/top/index.html.erb
invoke test_unit
create test/controllers/top_controller_test.rb
invoke helper
create app/helpers/top_helper.rb
invoke test_unit
routingに追加
Rails.application.routes.draw do
+ root 'top#index'
+
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check
# Defines the root path route ("/")
# root "posts#index"
end
templateを修正
-<h1>Top#index</h1>
+<h1 class="text-4xl font-bold">Top#index</h1>
-<p>Find me in app/views/top/index.html.erb</p>
+<p class="italic">Find me in app/views/top/index.html.erb</p>
全然関係ないけどtailwindのクラス覚えられないのでいつもこれ見てる
いい感じ ヨシ
紛らわしくなるのでtailwindの確認のために行った変更は一回全部破棄
react関連パッケージを追加
yarn add react react-dom
TSで書きたいので型定義も
yarn add -D @types/react @types/react-dom
{
"name": "app",
"private": "true",
"dependencies": {
"autoprefixer": "^10.4.16",
"esbuild": "^0.19.5",
"postcss": "^8.4.31",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
"tailwindcss": "^3.3.5"
},
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets",
"build:css": "tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --minify"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15"
}
}
ちなみにesbuildはpackage.jsonもwatchしてるらしくて勝手に再ビルドが走ってた えらいじゃん
01:49:00 js.1 | [watch] build started (change: "package.json")
01:49:00 js.1 | [watch] build finished
Reactがちゃんと動くか確認したいので雑にエンドポイントを追加(2回目)
> bundle exec rails g controller Top index
create app/controllers/top_controller.rb
route get 'top/index'
invoke erb
create app/views/top
create app/views/top/index.html.erb
invoke test_unit
create test/controllers/top_controller_test.rb
invoke helper
create app/helpers/top_helper.rb
invoke test_unit
routingに追加
Rails.application.routes.draw do
+ root 'top#index'
+
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check
# Defines the root path route ("/")
# root "posts#index"
end
Reactコンポーネントを作成
import React from "react";
import { createRoot } from "react-dom/client";
const container = document.getElementById("root");
if (!container) {
throw new Error("Couldn't find a root element");
}
const root = createRoot(container);
document.addEventListener("DOMContentLoaded", () => {
root.render(
<>
<h1 className="text-4xl font-bold">Yay! You're on Rails!</h1>
</>
);
});
templateを修正
<%= javascript_include_tag "Top", defer: true %>
<div id="root"></div>
React Componentは読まれた、が クラスはちゃんと当たってるのにtailwindのスタイルが当たっていないな?
tailwindは最適化のために実際に使われているclassのみを含んだスタイルを吐き出すが、この動的ビルドを行うときにviewファイルだけを見ていてtsx側のクラス利用状況が読まれてないっぽい
Railsアプリ生成時に初期生成されている tailwind.config.js
が *.js
のファイルしか見ないようになっていた
これを修正する
module.exports = {
content: [
'./app/views/**/*.html.erb',
'./app/helpers/**/*.rb',
'./app/assets/stylesheets/**/*.css',
- './app/javascript/**/*.js'
+ './app/javascript/**/*.*'
]
}
ヨシ!
特に何も考えず拡張子をts/tsxにするだけでTypeScriptが書けるのえらいな
ただしesbuildは型チェックまではしないらしいので、静的解析は別でCIとかに入れたほうがいいかも
少なくともコードを書いてる最中はvimに入れてるlanguage serverが指摘してくれるので問題なし
eslintとprettierを入れる
eslintとprettierの導入 eslintとprettierのルールが殴り合わないようにeslint側のフォーマットルールを無効化するpluginも入れる
これでeslintがlinterとして、prettierがformatterとして責務が分けられる
yarn add -D eslint prettier eslint-config-prettier
eslintの初期設定をする
前は eslint --init
とかしてた記憶があるけど今は @eslint/config
というツールが用意されてる模様
オプションはこんな感じにした
enforce code styleは切ったほうがいいかもと思ったけど、このあと eslint-config-prettierで打ち消すので一旦一番強いconfigで設定
yarn create @eslint/config
yarn create v1.22.19
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Installed "@eslint/create-config@0.4.6" with binaries:
- create-config
✔ How would you like to use ESLint? · style
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · react
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · browser
✔ How would you like to define a style for your project? · guide
✔ Which style guide do you want to follow? · standard-with-typescript
✔ What format do you want your config file to be in? · JavaScript
Checking peerDependencies of eslint-config-standard-with-typescript@latest
The config that you've selected requires the following dependencies:
eslint-plugin-react@latest eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^6.4.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 || ^16.0.0 eslint-plugin-promise@^6.0.0 typescript@*
✔ Would you like to install them now? · No / Yes
✔ Which package manager do you want to use? · yarn
eslintのconfigを書き換えてprettierと競合するルールを落とす
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"standard-with-typescript",
- "plugin:react/recommended"
+ "plugin:react/recommended",
+ "prettier"
],
"overrides": [
{
"env": {
"node": true
},
"files": [
".eslintrc.{js,cjs}"
],
"parserOptions": {
"sourceType": "script"
}
}
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react"
],
"rules": {
}
}
tscを使わなくてもesbuildがトランスパイルをしてくれるので一旦tsconfigを作っていなかったが、作っておかないとeslintが文句を言うようになるのでとりあえず生成
細かい内容は後で調整することにするが、esbuildはtsconfigの一部項目しか参照しないらしいのであんまりこだわる必要はなさそう
yarn tsc --init
eslintが色々文句言ってくれるようになったのでいい感じ
そしてprettierが管轄するフォーマットの文脈には口を出していないことを確認
開発環境をdocker composeであげられるようにした
foremanでプロセス管理されてる部分を個別のコンテナにしている
services:
web:
image: ruby:3.2.2
command: 'env RUBY_DEBUG_OPEN=true bin/rails server --binding 0.0.0.0'
ports:
- 3000:3000
volumes:
- ./:/usr/src/app
- bundler:/usr/local/bundle
working_dir: /usr/src/app
js-build:
image: node:20.9.0
command: 'yarn build --watch'
stdin_open: true
working_dir: /usr/src/app
volumes:
- ./:/usr/src/app
- node_modules:/usr/src/app/node_modules
- yarn_cache:/usr/local/share/.cache/yarn/v6
css-build:
image: node:20.9.0
command: 'yarn build:css --watch'
stdin_open: true
working_dir: /usr/src/app
volumes:
- ./:/usr/src/app
- node_modules:/usr/src/app/node_modules
- yarn_cache:/usr/local/share/.cache/yarn/v6
volumes:
bundler:
node_modules:
yarn_cache:
gemの解決とnode_modulesの解決は
docker compose run --rm web bundle install
docker compose run --rm js-build yarn install
これでdocker volumeに永続化される
node系の依存はjs-buildとcss-buildで共通化しているのでどっちかでやればOK