【面倒臭がりさん必見】たった1つのコマンドでRails + React環境構築【作ってみた】

突然ですが、環境構築は好きですか？
...私は苦手です。
端が丸まってくっついちゃったサランラップぐらい苦手です。

恐らく、あなたがこの記事を開いたということは
少なからず環境構築に苦手意識を持っているのではないでしょうか。

**「もっと気軽に環境構築できたらいいのにな...」**

というあなたの心の声を受け
今回は**『コマンド1つでRailsとReactの環境構築ができるシェルスクリプト』**をつくりました。


たった1つのコマンドを叩くだけ！
あとは『Netflix』で好きな動画でも見ていれば
勝手に環境構築が終わっているという"夢の世界"がここにあります!


さらに！

環境構築終了後、すぐに開発が始められるよう
Rails側のトップページとReact側のエントリーポイントも自動で生成される完全親切設計版です！
Reactのコンポーネントが初めからゴリゴリ書ける状態になっています。

以下、本文

## 前提必須条件
Dockerがインストール済みであること

## 主にインストールされるもの
| インストールされるもの | バージョン |
| :----------------- | --------: |
| Ruby               |  2.6.3    |
| Rails              | 6.0.3.2   |
| Node.js            | 10.x      |
| PostgreSQL         | 12.3      |
| foreman（Gem）      | 0.87.2    |

### インストールされるReact周りのパッケージ群
- redux
- react-redux
- react-router-dom
- redux-devtools-extension
- redux-form
- redux-thunk
- axios
- babel-plugin-root-import
- redux-toolkit
- material-ui

## 使い方の手順（この記事のメイン）
1. 好きな名前でフォルダを作成する
2. 1で作成したフォルダの中に『docker_rails_react.sh』という空ファイルを作成する
3. 2で作成したファイルに下記『[シェルスクリプト本体](#シェルスクリプト本体)』を全文コピーして保存する
4. ターミナルを起動
5. 1で作成したフォルダに移動し、コマンド『**bash docker_rails_react.sh**』を実行する
6. Netflixを見て時間を潰す（数分〜数十分間）
7. 『Compiled successfully.』がターミナルに表示されたら[http://localhost:3000/](http://localhost:3000/)にアクセス  
完

※macOS動作検証済み

## シェルスクリプト本体
docker_rails_react.sh

```sh
#!/bin/bash

ENV APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn

APP_NAME=$(basename `pwd`)
echo APP_NAME: $APP_NAME
UPPER_APP_NAME=`echo $APP_NAME | tr "[:lower:]" "[:upper:]"`
echo UPPER_APP_NAME: $UPPER_APP_NAME

# make Procfile.dev
cat <<'EOF' > Procfile.dev
web: bundle exec rails s -p 3000 -b '0.0.0.0'
webpacker: bin/webpack-dev-server
EOF

# make Dockerfile
cat <<EOF > Dockerfile
FROM ruby:2.6.3
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
# install yarn
RUN apt-get update && apt-get install -y curl apt-transport-https wget && \\
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \\
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \\
apt-get update && apt-get install -y yarn
# install Node.js
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && \\
apt-get install -y nodejs
RUN mkdir /$APP_NAME
WORKDIR /$APP_NAME
COPY Gemfile /$APP_NAME/Gemfile
COPY Gemfile.lock /$APP_NAME/Gemfile.lock
RUN bundle install
RUN bundle exec rails webpacker:install
RUN bundle exec rails webpacker:install:react
COPY . /$APP_NAME

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]
EOF

# make Gemfile
cat <<'EOF' > Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 6.0', '>= 6.0.3.2'
EOF

# make Gemfile.lock
touch Gemfile.lock

# make docker-compose.yml
cat <<EOF > docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec foreman start -f Procfile.dev"
    volumes:
      - .:/$APP_NAME
    ports:
      - "3000:3000"
      - "3035:3035"
    environment:
      ${UPPER_APP_NAME}_DB_HOST: db
      ${UPPER_APP_NAME}_DB_USERNAME: postgres
      ${UPPER_APP_NAME}_DB_PASSWORD: password
      ${UPPER_APP_NAME}_DEVELOPMENT_DB: ${APP_NAME}_development
      ${UPPER_APP_NAME}_TEST_DB: ${APP_NAME}_test
    depends_on:
      - db
EOF

# make entrypoint.sh
cat <<EOF > entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /$APP_NAME/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "\$@"
EOF

echo "docker-compose run web rails new . --force --no-deps -–skip-turbolinks --webpack=react --database=postgresql"
docker-compose run web rails new . --force --no-deps -–skip-turbolinks --webpack=react --database=postgresql

# fix config/database.yml
echo "fix config/database.yml"
rm -f config/database.yml
cat <<EOF > config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: <%= ENV['${UPPER_APP_NAME}_DB_HOST'] %>
  username: <%= ENV['${UPPER_APP_NAME}_DB_USERNAME'] %>
  password: <%= ENV['${UPPER_APP_NAME}_DB_PASSWORD'] %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: <%= ENV['${UPPER_APP_NAME}_DEVELOPMENT_DB'] %>

test:
  <<: *default
  database: <%= ENV['${UPPER_APP_NAME}_TEST_DB'] %>

production:
  <<: *default
  database: <%= ENV['${UPPER_APP_NAME}_DB'] %>
  username: <%= ENV['${UPPER_APP_NAME}_DB_USERNAME'] %>
  password: <%= ENV['${UPPER_APP_NAME}_DB_PASSWORD'] %>
EOF

# fix config/webpacker.yml
cat config/webpacker.yml | sed "s/host: localhost/host: 0.0.0.0/g" > __tmpfile__
cat __tmpfile__ > config/webpacker.yml
rm __tmpfile__

# fix config/routes.rb
rm -f config/routes.rb
cat <<'EOF' > config/routes.rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root "top#show"
end
EOF

# make .babelrc
.babelrc
cat <<'EOF' > .babelrc
{
  "plugins": [
    [
      "babel-plugin-root-import",
      {
        "paths": [
          {
            "rootPathSuffix": "./app/javascript/src",
            "rootPathPrefix": "~/"
          },
        ]
      }
    ]
  ]
}
EOF

# fix config/webpack/environment.js
rm -f config/webpack/environment.js
cat <<'EOF' > config/webpack/environment.js
const { environment } = require('@rails/webpacker')
environment.splitChunks();

module.exports = environment
EOF

# make top_controller.rb
cat <<'EOF' > app/controllers/top_controller.rb
class TopController < ApplicationController
  def show
  end
end
EOF

# fix application.html.erb
rm -f app/views/layouts/application.html.erb
cat <<EOF > app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>$APP_NAME</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= javascript_pack_tag 'application' %>
  </head>
  <body style="margin: 0;">
    <div id="root">
      <%= yield %>
    </div>
  </body>
</html>
EOF

# make top.html.erb
mkdir -p app/views/top
cat <<'EOF' > app/views/top/show.html.erb
<%= javascript_packs_with_chunks_tag 'index' %>
EOF

# make entrypoint
cat <<'EOF' > app/javascript/packs/index.jsx
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import {
  Router,
  Route,
  Switch,
  IndexRoute,
  useLocation
} from 'react-router-dom';
import { createBrowserHistory } from 'history';
import { composeWithDevTools } from 'redux-devtools-extension';

// reducer
import rootReducer from '~/reducers/';

// Component
import Top from '~/components/tops/';

const middleWares = [thunk];
const enhancer = process.env.NODE_ENV === 'development' ?
  composeWithDevTools(applyMiddleware(...middleWares)) : applyMiddleware(...middleWares);
const store = createStore(rootReducer, enhancer);
const customHistory = createBrowserHistory();

render(
  <Provider store={store}>
    <Router history={customHistory}>
      <Route render={({ location }) => (
        <Switch location={location}>
          <Route exact path='/' component={Top} />
        </Switch>
      )}/>
    </Router>
  </Provider>,
  document.getElementById('root')
)
EOF

# add foreman-gem
echo "gem 'foreman', '~> 0.87.2'" >> Gemfile

docker-compose run web bundle install

docker-compose build

echo "install package"
echo "docker-compose run web yarn add redux react-redux react-router-dom redux-devtools-extension redux-form redux-thunk axios @babel/preset-react babel-plugin-root-import @reduxjs/toolkit @material-ui/core"
docker-compose run web yarn cache clean
docker-compose run web yarn add redux react-redux react-router-dom redux-devtools-extension redux-form redux-thunk axios @babel/preset-react babel-plugin-root-import @reduxjs/toolkit @material-ui/core

echo "docker-compose run web rake db:create"
docker-compose run web rake db:create

# make styles/images dir
mkdir -p app/javascript/src/styles
mkdir -p app/javascript/src/images

# make component
mkdir -p app/javascript/src/components/tops
cat <<'EOF' > app/javascript/src/components/tops/index.jsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';

import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';

import { topInitial } from '~/modules/tops/';

const useStyles = makeStyles(theme => ({
  successed: {
    color: '#036ab5',
  },
}));

const Top = () => {

  const classes = useStyles();
  const dispatch = useDispatch();
  const topState = useSelector(state => state.top);

  React.useEffect(() => {
    dispatch(topInitial({ initial: true }));
  }, []);

  return (
    <>
      <Typography variant="h1" align="center">Hello Rails on React</Typography>
      { topState.initial &&
        <Typography variant="h5" align="center" className={ classes.successed }>SUCCESSED</Typography>
      }
    </>
  )
}
export default Top;
EOF

# make module
mkdir -p app/javascript/src/modules/tops
cat <<'EOF' > app/javascript/src/modules/tops/index.jsx
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  initial: false,
}

const top = createSlice({
  name: 'top',
  initialState,
  reducers: {
    topInitial(state, action) {
      const { initial } = action.payload;
      return {
        ...state,
        initial: initial,
      }
    }
  }
})
export const { topInitial } = top.actions
export default top.reducer;
EOF

# make reducer
mkdir -p app/javascript/src/reducers
cat <<'EOF' > app/javascript/src/reducers/index.jsx
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';

import top from '~/modules/tops/';

export default combineReducers({
  form: formReducer,
  top,
});
EOF

echo "docker-compose up"
docker-compose up

```

## 初期生成ファイルの仕様（気になる人向け）
- 状態管理はReduxを使用
- ファイル構成はDucksパターンを使用
- Redux Toolkitを使用
- Redux Formを使用
- Material-UIを使用
- React関連のソースパスを『.babelrc』ファイルで管理
- webpackにsplitChunksを使用
- foremanを使用し、railsサーバーとwebpack-dev-serverを同一コンテナで起動
- top_controllerのshowアクションをRootに設定
- database.ymlの環境変数名にはフォルダ名が適用される
- ページタイトルにはフォルダ名が適用される
- TypeScript未対応

## ディレクトリ構成（気になる人向け）
```
任意のフォルダ
├── app
│   ├── controllers
│   │   └── top_controller.rb
│   ├── javascript
│   │   ├── packs
│   │   │   ├── application.js
│   │   │   └── index.jsx
│   │   └── src
│   │       ├── components
│   │       │   └── tops
│   │       │       └── index.jsx
│   │       ├── images
│   │       ├── modules
│   │       │   └── tops
│   │       │       └── index.jsx
│   │       ├── reducers
│   │       │   └── index.jsx
│   │       └── styles
│   └── views
│       ├── layouts
│       │   └── application.html.erb
│       └── top
│           └── show.html.erb
├── config
│   ├── database.yml
│   ├── routes.rb
│   ├── webpack
│   │   └── environment.js
│   └── webpacker.yml
├── docker_rails_react.sh
├── docker-compose.yml
├── Dockerfile
├── entrypoint.sh
├── Gemfile
├── Gemfile.lock
├── package.json
├── Procfile.dev
└── yarn.lock
```

※最適化されていない(無駄な処理がある)のはご容赦ください

### では、楽しいRails on React生活を！