Hello World (EnterpriseEdition)

thumbnail

「ハローワールドするためにどこまで複雑にできるか」を目指して HelloWorldEnterpriseEdition というレポジトリを作って遊んでいたのですが、いろいろなエコシステムに触れることができて勉強になるのでそのまとめです。

FYI: https://github.com/sadnessOjisan/HelloWorldEnterpriseEdition

HelloWorldEnterpriseEdition とは

  • 依存する設定やライブラリの数を増やしたい
  • しかし無駄なものは入れない

という前提で作っているレポジトリです。

hello

という画面を作るために、だいたいこれくらいのファイル数になります。

file

一見単純な機能だけど裏側ではとてつもなくめんどくさいことをしているというのが推しポイントです。 これは FizzBuzzEnterpriseEdition というただ FizzBuzz するだけなのに大量のデザインパターンを使って実装しているプロジェクトに影響を受けています。

FizzBuzz Enterprise Edition is a no-nonsense implementation of FizzBuzz made by serious businessmen for serious business purposes.

FYI: https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition

この前提を元に、ブラウザに Hello World を表示する Web ページを作ってみましょう。

babel の設定

設定を増やしたいので TS + React ベースの SPA で作ります。 ただ tsc を使うと設定が増えないので、babel でビルドします。

TS, react を JS に変換するので、

  • @babel/preset-env
  • @babel/preset-typescript
  • @babel/preset-react

を使いたいです。

しかし、preset は plugin にバラすことができます。 バラした方が設定を複雑にできるのでバラします。

そのため入れるプラグインは、

  • @babel/preset-env
  • @babel/plugin-typescript
  • @babel/plugin-react

です。

設定ファイルは

module.exports = {
  plugins: [
    ["@babel/plugin-transform-typescript", { isTSX: true }],
    "@babel/plugin-transform-react-jsx",
  ],
  presets: ["@babel/env"],
}

となります。

FYI: Babel の Plugin で .tsx をビルドする

また preset-env はビルドターゲットを .browserslictrc で制御できるのでその設定ファイルも足します。

defaults
not IE 11
not IE_Mob 11
maintained node versions

webpack の設定

babel だけでは React のアプリケーションを動かせないので、webpack でモジュールの依存を解決します。

babel-loader を入れて babel の実行、css-loader, style-loader でスタイルの解決、html-webpack-plugin で HTML への読み込みも行います。 そして設定ファイルを増やすためにファイルは分割します。 分割したものを merge するためには webpack-merge を使います。

const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const outputPath = path.resolve(__dirname, "dist")

exports.outputPath = outputPath

module.exports = {
  entry: "./src/index.tsx",
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "build.js",
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        use: [
          {
            loader: "babel-loader",
          },
        ],
      },
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  resolve: {
    extensions: [".ts", ".css", ".tsx", ".js"],
  },
  plugins: [new HtmlWebpackPlugin({ template: "./src/index.html" })],
}
const wm = require("webpack-merge")
const common = require("./webpack.common")
const outputPath = require("./webpack.common").outputPath

module.exports = wm.merge(common, {
  mode: "development",
  devtool: "source-map",
  devServer: {
    contentBase: outputPath,
  },
})
const wm = require("webpack-merge")
const common = require("./webpack.common")

module.exports = wm.merge(common, {
  mode: "production",
})

このように webpack-merge を使うと分離することができます。 本番だけの設定とかを入れられるので覚えておきましょう。

ビルド対象を作る

index.tsx, index.html, App.tsx を作るだけです。

<html>
  <head>
    <meta charset="utf-8" />
    <title>Hello World Enterprise Edition</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
import * as React from "react"
import * as ReactDOM from "react-dom"

import { App } from "./App"
import "./style.css"

ReactDOM.render(
  <App message="Hello World !"></App>,
  document.getElementById("root")
)
body {
  background-color: antiquewhite;
}
import * as React from "react"

interface Props {
  message: string
}

export const App: React.FC<Props> = props => <p>{props.message}</p>

はい、これで HelloWorld できるようになりました。

hello

Format

Prettier を入れます。

npm i -D prettier

設定ファイルも生成します。(標準に乗りたいから書かないけど)

touch .prettierrc .prettierignore

prettierignore には md ファイルなどを指定しておくと、英数字後に半角スペースが入らないようにできたりします。

ESLint

ではここから静的に縛っていきましょう。

npx eslint --init

で、TS+React を選択して設定を吐き出します。

module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier",
  ],
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 12,
    sourceType: "module",
  },
  plugins: ["react", "@typescript-eslint"],
  rules: { "react/prop-types": "off" },
}

初期設定では入っていないのですが、 eslint-prettier-config の設定も足しています。

extends: [
  "eslint:recommended",
  "plugin:react/recommended",
  "plugin:@typescript-eslint/recommended",
  "prettier",
],

設定ファイル系は lint 対象から外したいので ignore します。

dist
babel.config.js
webpack.*.js
.eslintrc.js
jest.config.js

commithook

commit 前に prettier, eslint が実行されるようにします。 そのために、

  • lint-staged(staging 領域のコードが触れるようになる)
  • husky(commit hook を作れるようになる)

をインストールします。

そして commit message も lint するように commitlint を入れます。 これは chore: hogefix: fuga といった決まった prefix からしか commit できなくするツールです。

設定はファイルとして分離できる

eslint, prettier, commitlint, husky, lint-staged の設定は package.json にもかけますが、設定ファイルを増やしたいので分離したファイルにしました。 このように package.json に書く設定は分離できることが多いです。

test

jest の設定をしていきます。

UT の環境

jest をいれます。

npm i -D jest

これで jest コマンドが使えます。

設定は

npx jest --init

で生成できます。

いまは babel でコンパイルしているので transformmer の設定は不要です。

FYI: preset: ts-jest とは

テストファイルを __tests__ の中に書くとテストを実行できます。

jest

そしてカバレッジレポートも生成します。 これは後にデプロイします。

cov

https://sadnessojisan.github.io/HelloWorldEnterpriseEdition/

DOM をまたいだ環境

DOM に対してもテストを書きたいので react-testing-library を導入します。

npm i -D @testing-library/jest-dom @testing-library/react

を導入します。

jest-dom は DOM をテストするためのカスタムマッチャです。 「その要素に x が含まれているか」のようなテストがかけます。 @testing-library/react はテスト用の View を作ってくれるライブラリです。 ここから生成した View からは getByText などのメソッドで対象となる要素を取り出すことができ、それをカスタムマッチャに渡すことでテストを行えます。 また、@testing-library/react はイベントの発火もできるので画面の操作をテストすることが可能になります。

import "@testing-library/jest-dom"
import React from "react"
import { render, screen } from "@testing-library/react"
import { App } from "../App"

test("shows the children when the checkbox is checked", () => {
  const testMessage = "Test Message"
  render(<App message="Test Message"></App>)
  expect(screen.getByText(testMessage)).toBeInTheDocument()
})

storybook

コンポーネントカタログを作りましょう。

いまは、

npx sb init

とするだけで設定ができあがります。 昔はもっと複雑な設定が必要だったのですがいまは addon なども addon-essentials としてこのコマンドで入るようになってしまいました。 残念。

あとは storyfile を __stories__ の中に格納すればコンポーネントカタログが出来上がります。

import * as React from "react"
import { App } from "../App"

export const AppComponent = () => {
  return <App message="test"></App>
}

export default {
  title: "App",
}

story

https://enterprise-storybook.netlify.app/

これも後にデプロイします。

CI

テストもカタログも書いたので CI workflow も作りましょう。 GitHub Actions を整備します。

このようにブランチを分けながら 3 環境作ります。

name: DEV

on:
  push:
    branches:
      - "*" # matches every branch that doesn't contain a '/'
      - "*/*" # matches every branch containing a single '/'
      - "**" # matches every branch
      - "!master" # excludes master

jobs:
  release:
    name: check version, add tag and release
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v2
      - name: setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 12.x
          registry-url: "https://registry.npmjs.org"
      - name: install
        run: npm install
      - name: typecheck
        run: npm run typecheck
      - name: lint
        run: npm run lint
      - name: format
        run: npm run format
      - name: test
        run: npm run test
      - name: build
        run: npm run build:dev
name: STG

on:
  push:
    branches: ["master"]

jobs:
  release:
    name: check version, add tag and release
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v2
      - name: setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 12.x
          registry-url: "https://registry.npmjs.org"
      - name: install
        run: npm install
      - name: typecheck
        run: npm run typecheck
      - name: lint
        run: npm run lint
      - name: format
        run: npm run format
      - name: test
        run: npm run test:cov
      - name: build
        run: npm run build:prd
      - name: Deploy Coverage Report
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./coverage/lcov-report
name: PRD
on:
  push:
    tags:
      - "v*"
jobs:
  release:
    name: check version, add tag and release
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v2
      - name: setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 12.x
          registry-url: "https://registry.npmjs.org"
      - name: install
        run: npm install
      - name: Can Publish
        run: npx can-npm-publish --verbose
      - name: typecheck
        run: npm run typecheck
      - name: lint
        run: npm run lint
      - name: format
        run: npm run format
      - name: test
        run: npm run test
      - name: build
        run: npm run build:stg
      - name: Deploy to Firebase
        uses: w9jds/firebase-action@master
        with:
          args: deploy --only hosting
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}

デプロイ

デプロイします。

GitHub Pages

Github Actions から簡単にデプロイできる選択肢として GitHub Pages があります。 ただしこれは 3 環境用意できないので、テストカバレッジのデプロイだけに使います。

name: Deploy Coverage Report
uses: peaceiris/actions-gh-pages@v3
with:
  github_token: ${{ secrets.GITHUB_TOKEN }}
  publish_dir: ./coverage/lcov-report

このような yml を書けばデプロイできます。

peaceiris/actions-gh-pages は github pages にデプロイするタスク、peaceiris/actions-gh-pages は firebase にデプロイするタスクです。

FYI: GitHub Actions と GitHub Pages で yml をフォルダに入れておくだけのお手軽デプロイ

firebase

本番は firebase を使います。 作ってるいうものは静的ページな上、firebase は早いためです。

デプロイトークンを取得するために firebase コマンドが必要になります。 依存が増えて嬉しいですね。

npm i -D firebase-tools

これで

npx firebae init

とすると、Hosting の設定ファイルを作れます。

その結果、firebase.json と .firebaserc が生成されます。

{
  "projects": {
    "default": "helloworldenterpriseedition"
  }
}
{
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

そして Github Actions からデプロイするためのトークンを払い出します。

npx firebase login:ci

認証後にトークンがもらえるので、これを Github Actions で設定してデプロイしましょう。

- name: Deploy to Firebase
  uses: w9jds/firebase-action@master
  with:
    args: deploy --only hosting
    env:
      FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}

Netflify

Netlify でブランチ連携をするだけでよいです。 ただこれも設定を複雑にしようと思えばできて、_headers ファイルでキャッシュをレスポンスヘッダをコントロールできます。

/*
  X-Frame-Options: DENY
  X-XSS-Protection: 1; mode=block

ビルドキャッシュの設定も作れます。

[build]
  publish = "storybook-static"

[[plugins]]
  package = "netlify-plugin-gatsby-cache"

そしてビルド環境を固定するために .nvmrc も足しておきましょう。

v12.18.4

FYI: Gatsby 製サイトを Netlify にデプロイする前に見ておきたい設定 2 つ(ビルドと表示) FYI: gatsby-plugin-netlify-cache のキャッシュが効かない

Vercel

これもVercel でブランチ連携をするだけでよいです。 そして嬉しいことに Vercel も設定ファイルを足せます。

{
  "public": true
}

こうすることで /_src とすればソースコードを確認できます。 もともとソースコードを公開してるプロジェクトなので問題ないです。

github

もっと複雑にしたいので皆さんからの PR を待っています。 ということで各種テンプレートも入れました。

これはルート直下、もしくは .github 配下に置くことで効果を発揮します。

CODE OF CONDUCT

このレポジトリの行動指針です。 テンプレートから生成できます。

FYI: https://docs.github.com/ja/free-pro-team@latest/github/building-a-strong-community/adding-a-code-of-conduct-to-your-project

CONTRIBUTING

コントリビューターのためのガイドラインです。 PR のフローや開発者向けドキュメントのリンクを公開するものです。

FYI: https://docs.github.com/ja/free-pro-team@latest/github/building-a-strong-community/setting-guidelines-for-repository-contributors

PULL REQUEST TEMPLATE

PR のテンプレートです。

# PR Details

<!--- Provide a general summary of your changes in the Title above -->

## Description

<!--- Describe your changes in detail -->

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->

こういうのを入れておきます。

ISSUE TEMPLATE

PR 同様、ISSUE のテンプレートです。

まとめ

何してるんだろ僕...

おわりに

他にもなにか面白い設定がありましたら、是非とも PR や Issue をいただきたいです。

https://github.com/sadnessOjisan/HelloWorldEnterpriseEdition