Rails+Stimulus+Vite+TypeScript+Biome+α
最低限動くところまで作る
code:sh
git clone git@github.com:YuheiNakasaka/docker-rails-basis.git sample-sandbox
cd sample-sandbox
必要なGem追加
code:Gemfile
# Gemfileに追加
gem 'vite_rails'
gem 'stimulus-rails'
code:sh
bundle install
必要なnpm追加
code:sh
npm install -D typescript
code:sh
npm install --save-dev --save-exact @biomejs/biome
code:sh
npm i -D @hotwired/stimulus stimulus-vite-helpers vite-plugin-stimulus-hmr vite-plugin-full-reload
code:sh
npm i -D tailwindcss @tailwindcss/forms @tailwindcss/typography @tailwindcss/aspect-ratio @tailwindcss/forms @tailwindcss/line-clamp autoprefixer path
Vite導入
code:sh
bundle exec vite install
code:ts
# vite.config.ts
import { defineConfig } from 'vite'
import RubyPlugin from 'vite-plugin-ruby'
import StimulusHMR from "vite-plugin-stimulus-hmr";
import FullReload from "vite-plugin-full-reload";
export default defineConfig({
clearScreen: false,
plugins: [
RubyPlugin(),
StimulusHMR(),
],
root: "./app/assets",
build: {
manifest: true,
rollupOptions: {
input: "/app/javascript/entrypoints/application.ts"
}
}
})
code:json
# vite.jsonをルートに作成
{
"all": {
"sourceCodeDir": "app/javascript",
"watchAdditionalPaths": []
},
"development": {
"autoBuild": true,
"publicOutputDir": "vite-dev",
"port": 3036
},
"test": {
"autoBuild": true,
"publicOutputDir": "vite-test",
"port": 3037
}
}
TypeScript導入
app/javascript/*.jsファイルを全て.tsに変更。
code:sh
touch tsconfig.json
code:tsconfig
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Node",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"strictNullChecks": true,
"skipLibCheck": true
},
}
code:settings.json
// 必要なら.vscode/settings.jsonに下記を追加
{
"typescript.tsdk": "node_modules/typescript/lib"
}
Biome
code:sh
npx @biomejs/biome init
code:settings.json
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
},
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
},
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
},
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
}
}
code:json
// biome.json
{
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noNonNullAssertion": "off"
}
}
},
"javascript": {
"parser": {
"unsafeParameterDecoratorsEnabled": true
},
"formatter": {
"enabled": true,
"quoteStyle": "single",
"jsxQuoteStyle": "single",
"trailingComma": "all",
"semicolons": "asNeeded",
"arrowParentheses": "asNeeded",
"indentWidth": 2,
"indentStyle": "space",
"lineWidth": 120,
"quoteProperties": "asNeeded"
}
},
"json": {
"parser": { "allowComments": true },
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 120
}
}
}
ViteのStimulus対応
code:sh
rails stimulus:install
code:js
# app/javascript/application.tsの下記の記述(importmap用のやつ)をコメントアウト
// import "@hotwired/turbo-rails"
// import "controllers"
code:js
# app/javascript/controllers/index.ts.に追加
import { Application } from '@hotwired/stimulus'
import { registerControllers } from 'stimulus-vite-helpers'
const application = Application.start()
const controllers = import.meta.glob('./**/*_controller.ts', { eager: true })
registerControllers(application, controllers)
code:js
# app/javascript/entrypoints/application.tsの中に追加
import '../controllers'
code:sh
rm app/javascript/controllers/application.ts rm app/javascript/application.ts
ViteのTailwindCSS対応
code:sh
npx tailwindcss init
code:js
# tailwind.config.jsに追加
/** @type {import('tailwindcss').Config} */
const colors = require('tailwindcss/colors')
const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = {
content: [
'./app/helpers/**/*.rb',
'./app/assets/stylesheets/**/*.css',
'./app/views/**/*.{html,html.erb,erb}',
'./app/javascript/components/**/*.js',
],
theme: {
fontFamily: {
'sans': ["BlinkMacSystemFont", "Avenir Next", "Avenir",
"Nimbus Sans L", "Roboto", "Noto Sans", "Segoe UI", "Arial", "Helvetica",
"Helvetica Neue", "sans-serif"],
},
extend: {
},
},
corePlugins: {
aspectRatio: false,
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/line-clamp'),
],
}
code:css
# app/assets/stylesheets/application.cssに追加
@tailwind base;
@tailwind components;
@tailwind utilities;
code:sh
touch app/javascript/entrypoints/application.css
code:css
# app/javascript/entrypoints/application.cssに追加
@import "../../assets/stylesheets/application.css";
code:sh
touch postcss.config.js
code:js
# postcss.config.jsに追加
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
code:erb
# app/views/layouts/application.html.erbのimportmapのタグをviteのassetsに差し替える
...
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= vite_client_tag %>
<%= vite_typescript_tag 'application' %>
<%= vite_stylesheet_tag 'application', data: { "turbo-track": "reload" } %>
...
再起動
諸々を反映させるためにfn + F1 > Reload ContainerでVSCodeをreloadする。
適当なページを作成
code:sh
rails g controller Root index --no-assets --no-helper --no-test-framework --skip-routes
code:routes.rb
# rootを追加
...
root 'root#index'
...
code:index.html.erb
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="text-center">Hello, Root Page!</h1>
<div data-controller="hello">
<p>Input Your Name</p>
<input class="mb-4" data-hello-target="name" type="text" />
<br />
<button class="mb-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" data-action="click->hello#greet">Greet</button>
<br />
<p>Result</p>
<p class="mb-4" data-hello-target="output"></p>
</div>
</div>
</div>
</div>
code:hello_controller.ts
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
declare readonly nameTarget: HTMLInputElement
declare readonly outputTarget: HTMLElement
connect() {
console.log('connected!')
}
greet() {
this.outputTarget.textContent = Hello, ${this.nameTarget.value}!
}
}
code:sh
bin/dev
テスト
テストフレームワークはrspecを使用する。基本セットは導入済み。E2Eテストフレームワークとしてplaywrightも使う。
E2Eテストの実行フローとしてはsystem spec -> capybara -> capybara driver for playwright -> playwright server -> (headless)browserという流れ。なのでcapybara driverとplaywrightとbrowserが必要。
code:sh
mkdir spec/system spec/support
touch spec/system/root_spec.rb
touch spec/support/capybara.rb
code:Gemfile
group :test do
gem 'capybara-playwright-driver'
end
code:sh
bundle install
code:sh
npx playwright install-deps
supportディレクトリを有効化。
code:rails_helper.rb
..
..
..
Rails.root.glob('spec/support/**/*.rb').sort.each { |f| require f }
..
playwrightをsystemテストで使うようにする設定
code:capybara.rb
require 'capybara/rspec'
RSpec.configure do |config|
config.before(:each, type: :system) do
driven_by(:playwright)
end
end
Capybara.register_driver(:playwright) do |app|
Capybara::Playwright::Driver.new(app, browser_type: :chromium, headless: true)
end
Capybara.default_max_wait_time = 15
Capybara.default_driver = :playwright
Capybara.javascript_driver = :playwright
適当なテスト
code:root_spec.rb
require 'rails_helper'
describe 'Root' do
it 'shows the home page' do
visit '/'
expect(page).to have_content('Hello, Root Page!')
click_link_or_button('Greet')
expect(page).to have_content('Hello, Capybara!')
end
end
実行
code:sh
rspec spec/system/root_spec.rb
CI
GitHub Actionsでlintとtestの実行をさせる。
code:lint.yml
name: Lint
on:
push:
pull_request:
jobs:
build:
strategy:
fail-fast: false
matrix:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: RuboCop
run: bundle exec rubocop
- name: ERB Lint
run: bundle exec erblint --lint-all
code:ci.yml
name: Run rspec
on:
push:
pull_request:
jobs:
build:
strategy:
matrix:
runs-on: ${{ matrix.os }}
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- name: Install PostgreSQL client
run: sudo apt-get install libpq-dev
- name: Install dependencies
run: |
gem install bundler
bundle install --jobs 4 --retry 3
- name: Set up Node
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- name: Install Playwright and dependencies
run: |
npm install
npx playwright install-deps
npx playwright install
- name: Prepare database
env:
RAILS_ENV: test
DATABASE_URL: postgres://postgres:postgres@localhost:5432/app_test
run: |
bin/rails db:create
bin/rails db:schema:load
- name: Run RSpec and Capybara tests
env:
RAILS_ENV: test
DATABASE_URL: postgres://postgres:postgres@localhost:5432/app_test
run: bundle exec rspec
秘匿情報管理
組織によって変わるので一概にこれというのは難しい。いくつか議論がある。ENVでよくね?という流れが優勢。多分ローカルだと良いんだけどCI環境や本番環境に適用するにはcredentialsだと面倒なことがあったりする。変更時に毎度アプリのデプロイしないといけないとか。12 factor的にどうなんというのもあり時代錯誤感がある。
multi environment credentialsでlocalは用意する
SSMを使う
TODO
ViewComponent
lookbook
テスト
component
デプロイ
その他
dependabot
リソース