10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

初めてのアドベントカレンダーAdvent Calendar 2021

Day 15

【脱 create-react-app】React(+ Redux Toolkit) + Tailwind + TypeScript + webpack + Babel + ESLint + JEST の環境構築

Last updated at Posted at 2021-12-14

本エントリーはAdvent Calendar 2021「初めてのアドベントカレンダー Advent Calendar 2021」の15日目のエントリーです!

合わせてこれまでの「初めてのアドベントカレンダー Advent Calendar 2021」のエントリーも是非読んでみてください!

はじめに

こんにちは。@hironomiuです。

今回は「Teamうどん」さん主催のアドベントカレンダー「初めてのアドベントカレンダー Advent Calendar 2021」の15日目にエントリーしたいと思います!

エントリーテーマ

Reactを書く際にcreate-react-appはとても便利ですよね!仕事柄1日1回はcreate-react-appするほど普段大変お世話になっておりリスペクトしているのですが、たまに自前でReact環境を用意したいなと思うことがあり、今回はタイトルの通り「脱 create-react-app」をゴールにReact(+ Redux Toolkit) + Tailwind + TypeScript + Webpack + Babel + ESLint + JEST の環境構築をエントリーしたいと思います。(とは言え今後もcreate-react-appはガシガシ使いますけどね!)

コード

今回構築したコード全体はGitHubに置いてあります。動作を確認したい方はgit cloneyarn installで環境構築,yarn start で devServerの起動、yarn run buildでサンプルコードのカウンターアプリのビルドの確認ができます。

GitHub@hironomiu typescript-webpack-react-template

以降長いのでwebpackを今回入門する方は先にgit cloneし動作のイメージを掴んでから進むのをオススメします。

またGitHubリポジトリ中のサンプルアプリ(React + Redux Toolkit)はVercelにデプロイしてあります。
一応裏はRedux Toolkitでカウンターを実装していますが、とは言え見た目はただのカウンターアプリなので上でgit cloneしローカルで動作確認するので十分ですが落穂拾いでデプロイについても軽く触れたいと思うので載せておきます。

Vercel サンプルアプリ

利用パッケージ管理ツール、パッケージの紹介

今回利用するパッケージ管理ツール、パッケージは以下です。

パッケージ管理ツール

create-react-appと同様にyarn(v1)でパッケージ管理し構築ました

yarn公式

React

特に説明の必要はないと思います。Reactです。

React公式(JA)
React公式(EN)

Redux Toolkit

Reactで状態の管理は基本Redux Toolkitに任せるのが良いんじゃないかな?と個人的には思ってるぐらい重宝しています。

Redux Toolkit公式

Tailwind

CSSは最近使っていて自分的に使い勝手がよく感じるTailwindを選択しました。
ビルドの章で後述してますが、当初はv2を利用していましたがこのエントリーを書いているところv3に上がっていたためv2,v3の設定を今回は載せてあります。

Tailwind公式

webpack

モジュールバンドラーですね。バージョンはwebpack v5を選択しました。ESBuildも挑戦してみたいと思いましたが今回はwebpackを選択しました。

webpack公式

Babel

トランスパイラですね。
今回開発はTypeScriptなのでts-loaderの選択もありますがbabel-loaderを選択したかっためこちらも導入していきます。

Babel公式

ESLint

静的コード解析ツールですね。
今回このESlintとPrettireはエディタ(自分はVSCodeで開発しています)に任せるだけで良いかな?と考えたのですが最終的にESLintは導入、PrettireはVSCodeに任せるということにしました。

ESLint公式

JEST

テストはcreate-react-appでも採用されているJESTを選択しました。合わせて@testing-library/reactも導入します。

JEST公式(JA)
JEST公式(EN)

Install & Initialize

ここからは上記のJavaScriptパッケージ、各種ツールのインストール、初期化を行っていきます

Initialize

yarnで初期化を行いpackage.jsonを作成します。

yarn init -y

webpack Install

webpackで必要なパッケージのインストールを行います。

webpack-mergeは今回本番、開発、共通でconfigファイルを書き分けるためマージするために利用します。
html-webpack-pluginでバンドルしたファイルを読み込むHTMLを出力させます。
terser-webpack-pluginで本番のみconsole.logを除去します。
clean-webpack-pluginでbuild開始時にpublic配下をクリアします。
webpack-dev-serverで開発サーバの起動(アプリ編集セーブ時にホットリロード)を行います。

yarn add --dev webpack webpack-cli webpack-merge clean-webpack-plugin html-webpack-plugin terser-webpack-plugin webpack-dev-server

またscriptsの実行で利用するnpm-run-allもインストールします。

yarn add --dev npm-run-all

TypeScript Install & Initialize

TypeScriptで必要なパッケージのインストール、初期化(tsconfig.jsの作成)を行います。ts-nodeはJESTで必要なためインストールします。

yarn add --dev typescript ts-node
npx tsc --init

Babel Install

ts-loaderbabel-loaderで悩んだのですが babel-loaderを採用しました。
今回はReact、TypeScriptを使用するため@babel/preset-envだけでなく@babel/preset-typescript,@babel/preset-reactを追加でインストールします。

yarn add --dev babel-loader @babel/core @babel/preset-env @babel/preset-typescript @babel/preset-react

ESLint Install

ESLintで必要なパッケージのインストールを行います。
Babelと同様に今回はReact、TypeScriptを使用するため@typescript-eslint/eslint-plugin,@typescript-eslint/parser,eslint-plugin-reactを追加でインストールします。

yarn add --dev eslint eslint-webpack-plugin @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-react

JEST Install & Initialize

JESTで必要なパッケージのインストール、初期化を行います。

npx jest --initでは

質問 回答
? Would you like to use Jest when running "test" script in "package.json"? n
? Would you like to use Typescript for the configuration file? y
? Choose the test environment that will be used for testing jsdom
? Do you want Jest to add coverage reports? n
Which provider should be used to instrument code for coverage? babel
? Automatically clear mock calls, instances and results before every test? n

で進めます。Typoしても後ほどjest.config.jsを書き換えるので大丈夫です。

Which provider should be used to instrument code for coverage?については
JEST公式:coverageProviderを参考にしました。

yarn add --dev jest jsdom eslint-plugin-jest @types/jest @types/jsdom ts-jest
npx jest --init

次に@testing-library/react,@testing-library/jest-domをインストールします。

yarn add --dev @testing-library/react @testing-library/jest-dom

React + Redux Install

ここからはアプリケーション周りのインストールを行います。
まずはReactとRedux Toolkitで必要なパッケージのインストールを行います。

yarn add react react-dom @types/react @types/react-dom react-redux @types/react-redux @reduxjs/toolkit @types/node redux @types/redux

Redux Toolkitが不要な場合はyarn removereact-redux @types/react-redux @reduxjs/toolkit @types/node redux @types/reduxを指定することで削除可能です。

Tailwind Install & Initialize

Tailwindで必要なパッケージ、webpackでCSSをバンドルする際に必要なパッケージをインストール、初期化を行いtailwind.config.js,postcss.config.jsを作成します。

yarn add tailwindcss@latest @types/tailwindcss
yarn add --dev postcss-loader postcss autoprefixer css-loader mini-css-extract-plugin
npx tailwindcss init -p

SetUp

以降の設定ファイルはPathの記載がないものは全てプロジェクトディレクトリ直下に作成(もしくは初期化で作られたファイルを修正)します

Package.json

yarnの初期化で作成されたpackage.json"devDependencies":より上を追加&上書きで記述します。もしJESTの初期化でscriptsが追加された場合はそちらは削除します。nameは適時命名してください。

今回はstart,test,buildを使用します。buildでの型チェックはtsc --noEmitに任せています。

{
  "name": "typescript-webpack-react-template",
  "version": "1.0.0",
  "license": "MIT",
  "scripts": {
    "start": "webpack-dev-server --config webpack.dev.js",
    "test": "jest",
    "webpack:build": "webpack --config webpack.prod.js",
    "build": "run-s no-emit webpack:build",
    "dev": "webpack --config webpack.dev.js",
    "tsc": "tsc",
    "no-emit": "tsc --noEmit"
  },
  "devDependencies": {
    "@babel/core": "^7.16.0",
    "@babel/preset-env": "^7.16.4",
    "@babel/preset-react": "^7.16.0",
    "@babel/preset-typescript": "^7.16.0",
    "@types/jest": "^27.0.3",
    "@types/jsdom": "^16.2.13",
    "@typescript-eslint/eslint-plugin": "^5.4.0",
    "@typescript-eslint/parser": "^5.4.0",
    "autoprefixer": "^10.4.0",
    "babel-loader": "^8.2.3",
    "clean-webpack-plugin": "^4.0.0",
    "css-loader": "^6.5.1",
    "eslint": "^8.3.0",
    "eslint-plugin-jest": "^25.3.0",
    "eslint-plugin-react": "^7.27.1",
    "eslint-webpack-plugin": "^3.1.1",
    "html-webpack-plugin": "^5.5.0",
    "jest": "^27.3.1",
    "jsdom": "^18.1.1",
    "mini-css-extract-plugin": "^2.4.5",
    "npm-run-all": "^4.1.5",
    "postcss": "^8.3.11",
    "postcss-loader": "^6.2.0",
    "terser-webpack-plugin": "^5.2.5",
    "ts-jest": "^27.0.7",
    "ts-node": "^10.4.0",
    "typescript": "^4.5.2",
    "webpack": "^5.64.2",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.5.0",
    "webpack-merge": "^5.8.0"
  },
  "dependencies": {
    "@reduxjs/toolkit": "^1.6.2",
    "@types/node": "^16.11.10",
    "@types/react": "^17.0.36",
    "@types/react-dom": "^17.0.11",
    "@types/react-redux": "^7.1.20",
    "@types/redux": "^3.6.0",
    "@types/tailwindcss": "^2.2.4",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-redux": "^7.2.6",
    "redux": "^4.1.2",
    "tailwindcss": "^3.0.1"
  }
}

git diffの結果(抜粋)

-  "main": "index.js",

+  "scripts": {
+    "start": "webpack-dev-server --config webpack.dev.js",
+    "test": "jest",
+    "webpack:build": "webpack --config webpack.prod.js",
+    "build": "run-s no-emit webpack:build",
+    "dev": "webpack --config webpack.dev.js",
+    "tsc": "tsc",
+    "no-emit": "tsc --noEmit"
+  },

webpack

webpackの設定は本番prod,開発dev,共通commonの3つに分けて作成します。

webpack.common.js

共通の設定はこちらに記載します。プロジェクト直下にwebpack.common.jsを作成します。

entry: { app: './src/ts/app.tsx' },がエントリーポイントになります。作ってから思ったのですがcreate-react-appをリスペクトしているのでエントリーポイントはentry: { app: './src/ts/index.tsx' },にしApp.tsxをラップするべきだったかなと若干後悔したりしています。(今回はこのまま行きますがお好みで変更してください )

optimization: {のくだりで今回はnode_moduleで呼び出しているパッケージ、自身で作成したアプリを別々でバンドルするよう設定しています。GitHub@hironomiu typescript-webpack-react-templateではコメントで自身で作成したアプリを更に分けたい場合用の設定を記述してありますので興味ある方はそちらを見てみてください

バンドルするJS,CSSはcontenthashを設定しコード更新時に対応します。

performance:はデフォルトより少し緩めに設定をしています。

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ESLintPlugin = require('eslint-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  entry: { app: './src/ts/app.tsx' },
  output: {
    filename: 'js/[name].[contenthash].js',
    path: path.resolve(__dirname, 'public'),
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'initial',
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          minChunks: 1,
        },
        defaultVendors: {
          filename: 'js/[name].[contenthash].js',
        },
      },
    },
  },
  performance: {
    maxEntrypointSize: 500000,
    maxAssetSize: 500000,
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'babel-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
        ],
      },
    ],
  },
  resolve: {
    extensions: ['.ts', '.js', '.tsx', '.jsx'],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './src/html/index.html',
      chunks: ['app'],
    }),
    new ESLintPlugin({
      extensions: ['.ts', '.js', '.tsx'],
      exclude: 'node_modules',
    }),
    new MiniCssExtractPlugin({
      filename: './css/[name].[contenthash].css',
    }),
  ],
}

webpack.dev.js

開発の設定を記載します。プロジェクト直下にwebpack.dev.jsを作成します。今回はソースマップの指定、devServerの起動設定を記載します。

ソースマップの一覧 webpack Devtool

historyApiFallback: true,はReactでルーティング(react-router-dom)を使用した際に直接ルーティングパスを指定した際にフォールバックすることで404を防ぐよう設定します。

const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.common')
const path = require('path')

module.exports = merge(commonConfig, {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  devServer: {
    open: true,
    port: 'auto',
    static: {
      directory: path.join(__dirname, 'public'),
      serveIndex: true,
    },
    historyApiFallback: true,
  },
})

webpack.prod.js

本番の設定を記載します。プロジェクト直下にwebpack.prod.jsを作成します。今回はconsole.logの除去の設定を記載します。

const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.common')
const TerserPlugin = require('terser-webpack-plugin')

module.exports = merge(commonConfig, {
  mode: 'production',
  optimization: {
    minimizer: [
      new TerserPlugin({
        extractComments: false,
        terserOptions: {
          compress: {
            drop_console: true,
          },
        },
      }),
    ],
  },
})

babel.config.js

babelの設定を記載します。プロジェクト直下に babel.config.jsを作成します。

'@babel/preset-react'runtime: 'automatic'についてはIntroducing the New JSX Transformを参考にしました。

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      { targets: { node: 'current' }, useBuiltIns: 'usage', corejs: 3 },
    ],
    [
      '@babel/preset-react',
      { targets: { node: 'current' }, runtime: 'automatic' },
    ],
    '@babel/preset-typescript',
  ],
}

tsconfig.json

TypeScriptの設定を記載します。初期化で作成されたtsconfig.jsonを以下に修正します。コメントが不要な方は適時削除してください。

tscで型チェックをする場合/* Type Checking */以下は必要に応じて設定してください。

{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig.json to read more about this file */
    /* Projects */
    // "incremental": true,                              /* Enable incremental compilation */
    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
    // "tsBuildInfoFile": "./",                          /* Specify the folder for .tsbuildinfo incremental compilation files. */
    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects */
    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
    /* Language and Environment */
    "target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    "jsx": "react-jsx", /* Specify what JSX code is generated. */
    // "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
    // "reactNamespace": "",                             /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
    /* Modules */
    "module": "esnext", /* Specify what module code is generated. */
    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
    "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
    // "typeRoots": [],                                  /* Specify multiple folders that act like `./node_modules/@types`. */
    "types": [
      "jest",
      "node",
      "@testing-library/jest-dom"
    ], /* Specify type package names to be included without being referenced in a source file. */
    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
    // "resolveJsonModule": true,                        /* Enable importing .json files */
    // "noResolve": true,                                /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
    /* JavaScript Support */
    // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
    /* Emit */
    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
    "sourceMap": true, /* Create source map files for emitted JavaScript files. */
    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
    // "outDir": "./",                                   /* Specify an output folder for all emitted files. */
    // "removeComments": true,                           /* Disable emitting comments. */
    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types */
    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
    // "stripInternal": true,                            /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like `__extends` in compiled output. */
    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
    // "preserveConstEnums": true,                       /* Disable erasing `const enum` declarations in generated code. */
    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
    // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
    /* Interop Constraints */
    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
    "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
    "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
    /* Type Checking */
    "strict": true, /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied `any` type.. */
    // "strictNullChecks": true,                         /* When type checking, take into account `null` and `undefined`. */
    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
    // "strictBindCallApply": true,                      /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
    // "noImplicitThis": true,                           /* Enable error reporting when `this` is given the type `any`. */
    // "useUnknownInCatchVariables": true,               /* Type catch clause variables as 'unknown' instead of 'any'. */
    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
    // "noUnusedLocals": true,                           /* Enable error reporting when a local variables aren't read. */
    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read */
    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
    // "noUncheckedIndexedAccess": true,                 /* Include 'undefined' in index signature results */
    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type */
    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
    /* Completeness */
    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
    "skipLibCheck": true /* Skip type checking all .d.ts files. */
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "**/*.spec.ts",
    "node_modules"
  ]
}

git diff抜粋(空行をPrettireで除去含む)

-
-
-    "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+    "target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
-    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
+    "jsx": "react-jsx", /* Specify what JSX code is generated. */
-
-    "module": "commonjs",                                /* Specify what module code is generated. */
+    "module": "esnext", /* Specify what module code is generated. */
-    // "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
+    "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
-    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
+    "types": [
+      "jest",
+      "node",
+      "@testing-library/jest-dom"
+    ], /* Specify type package names to be included without being referenced in a source file. */
-
-
-    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
+    "sourceMap": true, /* Create source map files for emitted JavaScript files. */
-
-    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
+    "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
-    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
-
+    "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
-    "strict": true,                                      /* Enable all strict type-checking options. */
+    "strict": true, /* Enable all strict type-checking options. */
-
-    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
-  }
-}
+    "skipLibCheck": true /* Skip type checking all .d.ts files. */
+  },
+  "include": [
+    "src/**/*"
+  ],
+  "exclude": [
+    "**/*.spec.ts",
+    "node_modules"
+  ]
+}

tsconfig.jest.json

プロジェクト直下にtsconfig.jest.jsonを作成します。
名前の通りJestにtsconfig.jsonをラップして渡しています。

{
    "extends": "./tsconfig.json",
}

.eslintrc.js

ESLintの設定を記載します。プロジェクト直下に.eslintrc.jsを作成します。

JSX TransformのおかげでReactのImportが不要になりましたが、Importしない際に出るエラーはrulesreact/react-in-jsx-scope: 'off'で対応します。

extends含め設定は調べると若干冗長な様ですが無いと無いでエラーで怒られるものもあるのでこの設定にしています。rulesに関してはお好みで追加してください。

module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true,
    node: true,
    'jest/globals': true,
  },
  plugins: ['jest', '@typescript-eslint'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    sourceType: 'module',
    ecmaVersion: 2021,
    project: './tsconfig.json',
  },
  settings: {
    react: {
      version: 'detect',
    },
  },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/eslint-recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
    'plugin:jest/recommended',
  ],
  rules: {
    'prefer-const': 'error',
    'react/react-in-jsx-scope': 'off',
    'jest/no-disabled-tests': 'warn',
    'jest/no-focused-tests': 'error',
    'jest/no-identical-title': 'error',
    'jest/prefer-to-have-length': 'warn',
    'jest/valid-expect': 'error',
  },
}

tailwind.config.js

初期化で作成したtailwind.config.jsにビルドする際にpurgeをする設定を追記します。ビルドの章で後述しますが下記の設定はv2用のためv3をインストールした場合は下記の設定でもワーニングが出るだけですがワーニングを消す場合はビルドの章にある設定に直してください。

module.exports = {
  purge: {
    enabled: true,
    content: ['./src/**/*.tsx', './public/index.html'],
  },
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

git diff抜粋

-  content: [],
+  purge: {
+    enabled: true,
+    content: ['./src/**/*.tsx', './public/index.html'],
+  },
+  darkMode: false, // or 'media' or 'class'
+  variants: {
+    extend: {},
+  },

postcss.config.js

初期化で作成したpostcss.config.jsをそのまま使用します。(加筆修正は行いません)

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

jest.config.js

初期化で作成したjest.config.jsを以下に書き換えます。コメントが不要な方は適時削除してください。

注意: JESTの初期化でjest.config.jsはECMAScriptでモジュールを宣言export default {していますがCommonJSmodule.exports = {に変更します。ECMAScriptでも動作するのですが手元の環境で動作しないケースが発生したためCommonJSに変更しました。

/*
 * For a detailed explanation regarding each configuration property and type check, visit:
 * https://jestjs.io/docs/configuration
 */

module.exports = {
  // All imported modules in your tests should be mocked automatically
  // automock: false,

  // Stop running tests after `n` failures
  // bail: 0,

  // The directory where Jest should store its cached dependency information
  // cacheDirectory: "/private/var/folders/0f/v4tccdh150s7fpnmb_dthk8w0000gq/T/jest_dz",

  // Automatically clear mock calls, instances and results before every test
  // clearMocks: false,

  // Indicates whether the coverage information should be collected while executing the test
  // collectCoverage: false,

  // An array of glob patterns indicating a set of files for which coverage information should be collected
  // collectCoverageFrom: undefined,

  // The directory where Jest should output its coverage files
  // coverageDirectory: undefined,

  // An array of regexp pattern strings used to skip coverage collection
  // coveragePathIgnorePatterns: [
  //   "/node_modules/"
  // ],

  // Indicates which provider should be used to instrument code for coverage
  // coverageProvider: "babel",

  // A list of reporter names that Jest uses when writing coverage reports
  // coverageReporters: [
  //   "json",
  //   "text",
  //   "lcov",
  //   "clover"
  // ],

  // An object that configures minimum threshold enforcement for coverage results
  // coverageThreshold: undefined,

  // A path to a custom dependency extractor
  // dependencyExtractor: undefined,

  // Make calling deprecated APIs throw helpful error messages
  // errorOnDeprecated: false,

  // Force coverage collection from ignored files using an array of glob patterns
  // forceCoverageMatch: [],

  // A path to a module which exports an async function that is triggered once before all test suites
  // globalSetup: undefined,

  // A path to a module which exports an async function that is triggered once after all test suites
  // globalTeardown: undefined,

  // A set of global variables that need to be available in all test environments
  globals: {
    'ts-jest': {
      tsconfig: 'tsconfig.jest.json',
    },
  },

  // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
  // maxWorkers: "50%",

  // An array of directory names to be searched recursively up from the requiring module's location
  // moduleDirectories: [
  //   "node_modules"
  // ],

  // An array of file extensions your modules use
  // moduleFileExtensions: [
  //   "js",
  //   "jsx",
  //   "ts",
  //   "tsx",
  //   "json",
  //   "node"
  // ],

  // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
  // moduleNameMapper: {},

  // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
  // modulePathIgnorePatterns: [],

  // Activates notifications for test results
  // notify: false,

  // An enum that specifies notification mode. Requires { notify: true }
  // notifyMode: "failure-change",

  // A preset that is used as a base for Jest's configuration
  preset: 'ts-jest',

  // Run tests from one or more projects
  // projects: undefined,

  // Use this configuration option to add custom reporters to Jest
  // reporters: undefined,

  // Automatically reset mock state before every test
  // resetMocks: false,

  // Reset the module registry before running each individual test
  // resetModules: false,

  // A path to a custom resolver
  // resolver: undefined,

  // Automatically restore mock state and implementation before every test
  // restoreMocks: false,

  // The root directory that Jest should scan for tests and modules within
  // rootDir: undefined,

  // A list of paths to directories that Jest should use to search for files in
  // roots: [
  //   "<rootDir>"
  // ],

  // Allows you to use a custom runner instead of Jest's default test runner
  // runner: "jest-runner",

  // The paths to modules that run some code to configure or set up the testing environment before each test
  // setupFiles: [],

  // A list of paths to modules that run some code to configure or set up the testing framework before each test
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],

  // The number of seconds after which a test is considered as slow and reported as such in the results.
  // slowTestThreshold: 5,

  // A list of paths to snapshot serializer modules Jest should use for snapshot testing
  // snapshotSerializers: [],

  // The test environment that will be used for testing
  testEnvironment: "jsdom",

  // Options that will be passed to the testEnvironment
  // testEnvironmentOptions: {},

  // Adds a location field to test results
  // testLocationInResults: false,

  // The glob patterns Jest uses to detect test files
  // testMatch: [
  //   "**/__tests__/**/*.[jt]s?(x)",
  //   "**/?(*.)+(spec|test).[tj]s?(x)"
  // ],

  // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
  // testPathIgnorePatterns: [
  //   "/node_modules/"
  // ],

  // The regexp pattern or array of patterns that Jest uses to detect test files
  // testRegex: [],

  // This option allows the use of a custom results processor
  // testResultsProcessor: undefined,

  // This option allows use of a custom test runner
  // testRunner: "jest-circus/runner",

  // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
  // testURL: "http://localhost",

  // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
  // timers: "real",

  // A map from regular expressions to paths to transformers
  // transform: undefined,

  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
  // transformIgnorePatterns: [
  //   "/node_modules/",
  //   "\\.pnp\\.[^\\/]+$"
  // ],

  // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
  // unmockedModulePathPatterns: undefined,

  // Indicates whether each individual test should be reported during the run
  // verbose: undefined,

  // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
  // watchPathIgnorePatterns: [],

  // Whether to use watchman for file crawling
  // watchman: true,
};

Reactアプリの作成

ここまででバンドル、dev serverの起動、テストなどの設定はできたので動作確認用に実際にReactアプリを作成してみましょう。今回はtypescript-webpack-react-templateにあるサンプルコード(カウンターアプリ)を作成します。

./srcをルートとし以下の構成でアプリを作成します

__tests__ テストを作成
html index.htmlを作成
ts Reactアプリを作成

.
├── __tests__
├── html
│   └── index.html
└── ts

ディレクトリ作成

上のwebpackから参照するディレクトリ、テスト用のディレクトリを作成します。

mkdir -p src/__tests__ src/html src/ts

./src/html/index.html

./src/html配下にindex.htmlを作成します。
このindex.htmlをwebpackでバンドル対象のJS、CSSを組み込んでpublicに出力されます。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Counter App</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

./src/ts/app.tsx

./src/ts配下にapp.tsxを作成します。
app.tsxがエントリーポイントとなります。

import React from 'react'
import ReactDOM from 'react-dom'
import { store } from './app/store'
import { Provider } from 'react-redux'
import Home from './components/Home'
import './app.css'

const app = document.getElementById('app')

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <Home />
    </Provider>
  </React.StrictMode>,
  app
)

./src/ts/app.css

./src/ts配下にapp.cssを作成します。
Tailwindの設定を記述します。

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

./src/ts/components/Home.tsx

./src/ts/componentsディレクトリを作成します。

mkdir -p ./src/ts/components

./src/ts/components配下にHome.tsxを作成します。
次に作成する./src/ts/feature配下に作成したCounterコンポーネントを呼び出します。

import { VFC } from 'react'
import Counter from '../feature/counter/Counter'

const Home: VFC = () => {
  return (
    <div className="flex flex-col items-center">
      <Counter />
    </div>
  )
}

export default Home

./src/ts/feature/counter/Counter.tsx

カウンターアプリのコンポーネントを作成します

./src/ts/feature/counterディレクトリを作成します。

mkdir -p ./src/ts/feature/counter

./src/ts/feature/counter配下にCounter.tsxを作成します。

このディレクトリ構成はcreate-react-appreduxテンプレートcra-template-reduxを参考にしました。

import { VFC } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment, selectCount } from './counterSlice'

const Counter: VFC = () => {
  const count = useSelector(selectCount)
  const dispatch = useDispatch()

  return (
    <div className="flex flex-col text-xl mt-10">
      <div className="text-5xl mx-5 px-5">
        <h1>Counter App</h1>
      </div>
      <div className="flex flex-row justify-center my-10">
        <button
          className="text-5xl mx-5 px-5"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
        <p className="text-5xl">{count}</p>
        <button
          className="text-5xl mx-5 px-5"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
      </div>
    </div>
  )
}

export default Counter

./src/ts/feature/counter/counterSlice.tsx

カウンターアプリのSliceを作成します。
./src/ts/feature/counter 配下に counterSlice.tsx を作成します。

import { createSlice } from '@reduxjs/toolkit'
import { RootState } from '../../app/store'
type state = {
  value: number
  status: string
}
const initialState: state = {
  value: 0,
  status: 'idle',
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    decrement: (state) => {
      state.value -= 1
    },
    increment: (state) => {
      state.value += 1
    },
  },
})

export const { decrement, increment } = counterSlice.actions
export const selectCount = (state: RootState) => state.counter.value

export default counterSlice.reducer

./src/ts/app/store.ts

カウンターアプリのSliceを登録します。

./src/ts/appディレクトリを作成します。

mkdir -p ./src/ts/app

./src/ts/app配下にstore.tsを作成します。

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../feature/counter/counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

export type RootState = ReturnType<typeof store.getState>

動作確認

devServer

それではdevServerを起動しアプリの動作を確認してみましょう。停止する場合はCTRL + Cでできます。

yarn start

devServerから自動でブラウザタブを開きアプリを起動してくれれば成功です。gifではデモしていませんがアプリはWatchされているのでコードを修正しセーブしたタイミングで反映されます。

demo.gif

ビルド

次にビルドの動作を確認してみましょう。

yarn run build

結果

$ yarn run build
yarn run v1.22.17
$ run-s no-emit webpack:build
$ tsc --noEmit
$ webpack --config webpack.prod.js
assets by path js/*.js 156 KiB
  asset js/vendor.8695c69d62d0e2463e7a.js 154 KiB [emitted] [immutable] [minimized] (name: vendor) (id hint: vendor)
  asset js/app.87a175b380041ad64933.js 2.03 KiB [emitted] [immutable] [minimized] (name: app)
asset ./css/app.5afb3b34d518ae0becf0.css 12 KiB [emitted] [immutable] (name: app)
asset index.html 456 bytes [emitted]
Entrypoint app 168 KiB = js/vendor.8695c69d62d0e2463e7a.js 154 KiB ./css/app.5afb3b34d518ae0becf0.css 12 KiB js/app.87a175b380041ad64933.js 2.03 KiB
orphan modules 125 KiB (javascript) 937 bytes (runtime) [orphan] 45 modules
runtime modules 2.76 KiB 4 modules
cacheable modules 295 KiB (javascript) 12 KiB (css/mini-extract)
  modules by path ./node_modules/ 293 KiB
    modules by path ./node_modules/react/ 7.63 KiB 4 modules
    modules by path ./node_modules/react-redux/ 48.2 KiB 3 modules
    modules by path ./node_modules/prop-types/ 2.58 KiB 3 modules
    modules by path ./node_modules/react-dom/ 119 KiB 2 modules
    modules by path ./node_modules/scheduler/ 4.91 KiB 2 modules
    modules by path ./node_modules/react-is/ 2.69 KiB 2 modules
    3 modules
  modules by path ./src/ts/ 2.46 KiB (javascript) 12 KiB (css/mini-extract)
    ./src/ts/app.tsx + 4 modules 2.46 KiB [built] [code generated]
    css ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/dist/cjs.js!./src/ts/app.css 12 KiB [built] [code generated]
webpack 5.64.2 compiled successfully in 7283 ms
✨  Done in 8.76s.

余談ですがTailwind.config.jsでpurgeの設定が漏れるとasset ./css/app.451002707eb5607fb8e3.css 3.82 MiBと巨大なサイズになるので注意が必要です(purgeが効いてると上のようにasset ./css/app.5afb3b34d518ae0becf0.css 12 KiBで収まりました)

yarn run v1.22.17
$ run-s no-emit webpack:build
$ tsc --noEmit
$ webpack --config webpack.prod.js
assets by chunk 3.82 MiB (name: app)
  asset ./css/app.451002707eb5607fb8e3.css 3.82 MiB [emitted] [immutable] (name: app)
  asset js/app.87a175b380041ad64933.js 2.03 KiB [emitted] [immutable] [minimized] (name: app)
asset js/vendor.8695c69d62d0e2463e7a.js 154 KiB [emitted] [immutable] [minimized] (name: vendor) (id hint: vendor)
asset index.html 456 bytes [emitted]
Entrypoint app 3.97 MiB = js/vendor.8695c69d62d0e2463e7a.js 154 KiB ./css/app.451002707eb5607fb8e3.css 3.82 MiB js/app.87a175b380041ad64933.js 2.03 KiB
orphan modules 4.17 MiB (javascript) 937 bytes (runtime) [orphan] 45 modules
runtime modules 2.76 KiB 4 modules
cacheable modules 295 KiB (javascript) 3.82 MiB (css/mini-extract)
  modules by path ./node_modules/ 293 KiB
    modules by path ./node_modules/react/ 7.63 KiB 4 modules
    modules by path ./node_modules/react-redux/ 48.2 KiB 3 modules
    modules by path ./node_modules/prop-types/ 2.58 KiB 3 modules
    modules by path ./node_modules/react-dom/ 119 KiB 2 modules
    modules by path ./node_modules/scheduler/ 4.91 KiB 2 modules
    modules by path ./node_modules/react-is/ 2.69 KiB 2 modules
    3 modules
  modules by path ./src/ts/ 2.46 KiB (javascript) 3.82 MiB (css/mini-extract)
    ./src/ts/app.tsx + 4 modules 2.46 KiB [built] [code generated]
    css ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/dist/cjs.js!./src/ts/app.css 3.82 MiB [built] [code generated]
webpack 5.64.2 compiled successfully in 7963 ms
✨  Done in 9.43s.

treeでビルドで作成されるpublic配下を確認しましょう。無事バンドルが成功しました。

$ tree ./public 
./public
├── css
│   └── app.5afb3b34d518ae0becf0.css
├── index.html
└── js
    ├── app.87a175b380041ad64933.js
    └── vendor.8695c69d62d0e2463e7a.js

2 directories, 4 files

Tailwind v3

自分の環境では当初Tailwindはv2を使用していたのですが、このエントリーを実際に検証していた際にv3を使用するようになりました。v3ですと今回のtailwind.config.jsの設定ですと以下のワーニングが出力されると思います。

warn - The `purge`/`content` options have changed in Tailwind CSS v3.0.
warn - Update your configuration file to eliminate this warning.

warn - The `darkMode` option in your Tailwind CSS configuration is set to `false`, which now behaves the same as `media`.
warn - Change `darkMode` to `media` or remove it entirely.

build自体は問題なくされますが気持ち悪いと思うのでtailwind.config.jsを以下に書き直しましょう。darkModeについては必要な場合は適時設定してください。

module.exports = {
  content: ['./src/**/*.tsx', './public/index.html'],
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

build時にワーニングが消えます。CSSのパージも問題なくされています。

$ yarn run build
yarn run v1.22.17
$ run-s no-emit webpack:build
$ tsc --noEmit
$ webpack --config webpack.prod.js
assets by path js/*.js 157 KiB
  asset js/vendor.70fe727d6a1415ad5e9f.js 154 KiB [emitted] [immutable] [minimized] (name: vendor) (id hint: vendor)
  asset js/app.63e074b899132527681a.js 2.1 KiB [emitted] [immutable] [minimized] (name: app)
asset ./css/app.6616dda496afbb6533cf.css 7.44 KiB [emitted] [immutable] (name: app)
asset index.html 456 bytes [emitted]
Entrypoint app 164 KiB = js/vendor.70fe727d6a1415ad5e9f.js 154 KiB ./css/app.6616dda496afbb6533cf.css 7.44 KiB js/app.63e074b899132527681a.js 2.1 KiB
orphan modules 120 KiB (javascript) 937 bytes (runtime) [orphan] 45 modules
runtime modules 2.82 KiB 4 modules
cacheable modules 297 KiB (javascript) 7.43 KiB (css/mini-extract)
  modules by path ./node_modules/ 294 KiB
    modules by path ./node_modules/react/ 7.63 KiB 4 modules
    modules by path ./node_modules/react-redux/ 48.2 KiB 3 modules
    modules by path ./node_modules/prop-types/ 2.58 KiB 3 modules
    modules by path ./node_modules/react-dom/ 119 KiB 2 modules
    modules by path ./node_modules/scheduler/ 4.91 KiB 2 modules
    modules by path ./node_modules/react-is/ 2.69 KiB 2 modules
    3 modules
  modules by path ./src/ts/ 2.46 KiB (javascript) 7.43 KiB (css/mini-extract)
    ./src/ts/app.tsx + 4 modules 2.46 KiB [built] [code generated]
    css ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/dist/cjs.js!./src/ts/app.css 7.43 KiB [built] [code generated]
webpack 5.65.0 compiled successfully in 5498 ms

Tips

 Webサーバなどにデプロイ前にpublic配下の動作を確認したい時があると思います。その際にservehttp-serverなど利用すると思いますが
react-router-domを使ってルーティング先を直接呼びだす(リロードなど)状況で404になってしまいます。SPAではなくルーティングをする際は
servorで起動するとよろしくしてくれるのでオススメです。

テスト

JESTを導入しているので簡単なテストコードを実行して動作確認をしましょう。

./src/setupTests.ts

@testing-library/jest-dom用にsetupTests.tsを作成します。

import '@testing-library/jest-dom/extend-expect'

./src/tests/counterSlice.spec.ts

初期値と加算のテストです
./src/__tests__配下にcounterSlice.spec.tsを作成します。

import counterReducer, { increment } from '../ts/feature/counter/counterSlice'

describe('counterReducer', () => {
  const initialState = {
    value: 3,
    status: 'idle',
  }

  it('initial state', () => {
    expect(counterReducer(undefined, { type: 'unknown' })).toEqual({
      value: 0,
      status: 'idle',
    })
  })

  it('increment', () => {
    const actual = counterReducer(initialState, increment())
    expect(actual.value).toEqual(4)
  })
})

テスト実行

全てpassしているので動作としては成功です。

$ yarn test
yarn run v1.22.17
$ jest
 PASS  src/__tests__/counterSlice.spec.ts
  counterReducer
    ✓ initial state (3 ms)
    ✓ increment (2 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.695 s, estimated 3 s
Ran all test suites.
✨  Done in 4.10s.

./src/tests/Counter.spec.tsx

レンダリングの確認のテストでCounter.spec.tsxを作成します。

import { render, screen } from '@testing-library/react'
import Counter from '../ts/feature/counter/Counter'
import { store } from '../ts/app/store'
import { Provider } from 'react-redux'

describe('feature/counter/Counter', () => {
  it('renders Counter App text', () => {
    render(
      <Provider store={store}>
        <Counter />
      </Provider>
    )
    const Element = screen.getByText(/Counter App/i)
    expect(Element).toBeInTheDocument()
  })
})

テスト実行

先ほどのテストと合わせて全てpassしているので動作としては成功です。

$ yarn test
yarn run v1.22.17
$ jest
 PASS  src/__tests__/counterSlice.spec.ts
 PASS  src/__tests__/Counter.spec.tsx

Test Suites: 2 passed, 2 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        4.754 s
Ran all test suites.
✨  Done in 8.26s.

落穂拾い

今回のテーマから少し逸れますがアプリを作ったらデプロイ、CI/CDを回したいと思うと思います。またGitHub@hironomiu typescript-webpack-react-templategit cloneした際にGitHub Actionsの設定があるので、そこについても軽く触れたいと思います。

デプロイ(Vercel)

インターネット上に公開し動作を確認したいことはあると思います。その際に自前でサーバを用意するのは少し手間(HTTPSにするのもちょっと手間)だなと思うことがあると思います。そういうカジュアルに公開したい時に普段自分はVercelを利用することが多いのですが、このエントリーでビルドしたアプリをデプロイする際以下を設定しました。

以下設定内容

設定項目 設定値
FRAMEWORK PRESET Other
BUILD COMMAND yarn run build
OUTPUT DIRECTORY public
INSTALL COMMAND yarn install

Vercelの使い方には触れませんがとても簡単ですのでカジュアルに無料プランでシュッとアプリをデプロイするのには割とオススメです。(割とお世話になってます)

CI/CD(GitHub Actions)

CI/CDとありますがデプロイはしていません。(デプロイはActionsとは別に独立してVercelに任せています。)

冒頭で「たまに自前でReact環境を構築した方が何かと良いかなと思うことがあり」と書きましたが、仕事柄、普段学生さん向けに勉強会を開催するのですがReactをテーマにした時、Dockerコンテナをnode:latestで立ち上げた環境でcreate-react-appで開発をするとnodeのバージョンによって(主にv17)アプリの起動やビルドなどで怒られることがあり、そういう思いに至ったというのがあります。そこで今回作った環境もmainに取り込み時と定期的(1日1回にしています)にGitHub Actionsを使いnodeの各バージョンでビルドの検証を行なっています。Dockerのnode imageのlatestとGitHubで同期が取れているわけではないので現時点では気休め程度ですが一旦buildを通す(おまけでテストも回す)ところまでは行なっています。

git cloneした方で不要な場合は.githubごと削除、デプロイなどする場合は適時編集してください。

main取り込み時(.github/workflows/build.yml)

name: biuld

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [12.x, 14.x, 16.x, 17.x]
    
    steps:
    - uses: actions/checkout@v2

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}

    - name: SetUp
      run: yarn install
      
    - name: Test
      run: yarn test
      
    - name: Build
      run: yarn build

定期、日時(.github/workflows/schedule-build)

name: schedule-build

on:
  schedule:
    - cron:  '25 1 * * *'

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [12.x, 14.x, 16.x, 17.x]
    
    steps:
    - uses: actions/checkout@v2

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}

    - name: SetUp
      run: yarn install
      
    - name: Test
      run: yarn test
      
    - name: Build
      run: yarn build

おわりに

【脱 create-react-app】React(+ Redux Toolkit) + Tailwind + TypeScript + webpack + Babel + ESLint + Jest の環境構築 でした。雰囲気で作っている節が多分にあるので細かいところでより良い設定があると思いますが今回は動作することができたので当初のゴールを達成できたと言うことにしたいと思います。またこのエントリーが少しでも参考になれば幸いです。

そして明日の「初めてのアドベントカレンダー Advent Calendar 2021」はy_a_m_aくんのエントリーです。お楽しみに!

合わせてこれまでの「初めてのアドベントカレンダー Advent Calendar 2021」のエントリーも是非読んでみてください!

それでは良いクリスマスを!!

10
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?