🔨

Next.js + TypeScript + Tailwind + Storybook プロジェクトセットアップ

2020/12/23に公開

はじめに

以下の設定を適応したプロジェクトセットアップを行います。

  • Next.js
  • TypeScript
  • Tailwind
  • Storybook
  • ESLint
  • Prettier
  • husky

セットアップ済みのプロジェクトはこちらのリポジトリから「Use this template」をクリックすると以下の手順をベースとしたリポジトリを新規作成することが出来ます。

動作環境

node, yarn はインストール済みとします。

$node -v
v12.16.3
$yarn -v
1.22.10

動作検証したMacOSのバージョンはこちらです。

$sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H2

Next.js プロジェクト作成

まずはNext.jsのプロジェクトを作成します。

npx create-next-app nextjs-with-typescript-tailwind-storybook

このようなメッセージが出たら作成完了です。

作成が出来たらメッセージの通り立ち上げてみます。

cd nextjs-with-typescript-tailwind-storybook
yarn dev

http://localhost:3000 にアクセスすると下記ページが表示されます。

TypeScriptのセットアップ

tsconfig.jsonを作成します。

touch tsconfig.json

作成後、yarn devを実行すると下記のようなエラーメッセージが出るはずです。

It looks like you're trying to use TypeScript but do not have the required package(s) installed.
Please install typescript, @types/react, and @types/node by running:
yarn add --dev typescript @types/react @types/node
If you are not trying to use TypeScript, please remove the tsconfig.json file from your package root (and any TypeScript files in your pages directory).

指示に従い必要なパッケージを追加します。

yarn add --dev typescript @types/react @types/node

パッケージの追加が完了したら再度 yarn dev で立ち上げます。
するとtsconfig.jsonが下記のように設定されます。

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

またnext-env.d.tsが自動で生成されます。

next-env.d.ts
/// <reference types="next" />
/// <reference types="next/types/global" />

既存のファイル拡張子を変更していきます
pages/_app.js -> pages/_app.tsx
pages/index.js -> pages/index.tsx
pages/api/hellow.js -> pages/api/hello.tsx

再度 yarn dev を実行し立ち上がれば TypeScript の導入は完了です。

Tailwind のセットアップ

Tailwind に必要なモジュールを追加します。

yarn add tailwindcss postcss autoprefixer

次に以下コマンドを実行します。

npx tailwindcss init -p

tailwind.config.js postcss.config.jsが作成されます

   tailwindcss 2.0.2
   ✅ Created Tailwind config file: tailwind.config.js
   ✅ Created PostCSS config file: postcss.config.js

tailwind.config.js を変更します

tailwind.config.js
 module.exports = {
-   purge: [],
+   purge: ['./pages/**/*.tsx', './components/**/*.tsx'],
    darkMode: false, // or 'media' or 'class'
    theme: {
      extend: {},
    },
    variants: {
      extend: {},
    },
    plugins: [],
  }

globals.css を下記のように上書きします。

./styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

Tailwindが適応されているかindex.tsxの Welcome to Next.js の下に下記を追加し確認してみます。

pages/index.tsx
<p className="text-3xl text-red-500 font-bold">Hello Tailwind</p>

このようにclassNameに指定したものが反映されていればTailwindセットアップは完了です

Storybook のセットアップ

次に Storybook を追加していきます。

npx sb init

モジュールが追加されたら立ち上げてみます。

yarn storybook

この時 PostCSS エラーが出るはずです。

ERROR in ./stories/button.css (./node_modules/css-loader/dist/cjs.js??ref--11-1!./node_modules/postcss-loader/src??ref--11-2!./stories/button.css)
Module build failed (from ./node_modules/postcss-loader/src/index.js):
Error: PostCSS plugin tailwindcss requires PostCSS 8.

Tailwind と Storybook の PostCSS依存関係を解消する必要があるので、
公式のドキュメント通りにTailwindでPostCSS 7を使用するように変更します。

yarn remove tailwindcss postcss autoprefixer
yarn add tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9

再度 yarn storybook を行い http://localhost:6006/ にアクセスし、
storybookが立ち上がることを確認します。

componentsへの変更とTailwindの反映

ディレクトリをstoriesからcomponentsに変更します。

mv stories components

storybookで指定するディレクトリも変更します。

.storybook/main.js
  module.exports = {
    "stories": [
-     "../stories/**/*.stories.mdx",
-     "../stories/**/*.stories.@(js|jsx|ts|tsx)"
+     "../components/**/*.stories.mdx",
+     "../components/**/*.stories.@(js|jsx|ts|tsx)"
    ],
    "addons": [
      "@storybook/addon-links",
      "@storybook/addon-essentials"
    ]
  }

tailwindを反映するようにstyles/globals.css をimportします。

./storybook/preview.js
+   import "../styles/globals.css"

    export const parameters = {
      actions: { argTypesRegex: "^on[A-Z].*" },
    }

まずはbuttonのみ作成したいため、Button.stories.tsx、Button.tsx以外のファイルを削除し、このような状態にします。

components/
├── Button.stories.tsx
└── button.tsx

storybookのサンプルとして提供されているbutton のstyleをtailwindに置き換えてみます。

独自のカラー指定とフォント指定はtailwind.jsonのそれぞれcolors, fontFamilyに記述ていきます。

tailwind.config
module.exports = {
  purge: ['./pages/**/*.tsx', './components/**/*.tsx'],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {
      colors: {
        blue: {
          450: '#1ea7fd',
        },
      },
    },
    fontFamily: {
      sans: ['Nunito Sans', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
    },
    boxShadow: {
      inner: 'rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset',
    },
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

ButtonのstyleをTailwindに置き換えていきます

Button.tsx
import React from 'react'

export interface ButtonProps {
  primary?: boolean
  backgroundColor?: string
  size?: 'small' | 'medium' | 'large'
  label: string
  onClick?: () => void
}

export const Button: React.FC<ButtonProps> = ({
  primary = false,
  size = 'medium',
  backgroundColor,
  label,
  ...props
}) => {
  const baseButton = 'rounded-full font-bold'
  const sizeMode =
    size === 'small'
      ? 'py-1.5 px-4 text-xs'
      : size === 'medium'
      ? 'py-2 px-5 text-sm'
      : size === 'large'
      ? 'py-3 px-6 text-base'
      : ''
  return primary ? (
    <div>
      <button
        type="button"
        className={`text-white bg-blue-450 ${baseButton} ${sizeMode}`}
        {...props}
      >
        {label}
      </button>
    </div>
  ) : (
    <button
      type="button"
      className={`text-gray-600 bg-transparent shadow-inner ${baseButton} ${sizeMode}`}
      style={{ backgroundColor }}
      {...props}
    >
      {label}
    </button>
  )
}


yarn storybook で立ち上げし、buttonのstyleがtailwindで適応されていることが確認出来ます。

ESLintのセットアップ

ESLint, Prettierに必要なパッケージを追加していきます。

yarn add --dev eslint prettier eslint-plugin-react eslint-config-prettier eslint-plugin-prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin

dependenciesに追加され下記のようになります。

package.json
{
  ...
   "dependencies": {
     "autoprefixer": "^9",
     "@storybook/react": "^6.1.11",
     "@types/node": "^14.14.10",
     "@types/react": "^17.0.0",
+    "@typescript-eslint/eslint-plugin": "^4.10.0",
+    "@typescript-eslint/parser": "^4.10.0",
     "babel-loader": "^8.2.2",
+    "eslint": "^7.15.0",
+    "eslint-config-prettier": "^7.0.0",
+    "eslint-plugin-prettier": "^3.3.0",
+    "eslint-plugin-react": "^7.21.5",
+    "prettier": "^2.2.1",
     "typescript": "^4.1.2"
   }
}

eslintの設定ファイルとeslintignore を作成します。

touch .eslintrc.json .eslintignore

eslintrc内を下記のように設定していきます。

eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:prettier/recommended",
    "prettier/@typescript-eslint"
  ],
  "settings": {
    "react": {
      "version": "latest"
    }
  },
  "plugins": ["@typescript-eslint"],
  "parser": "@typescript-eslint/parser",
  "env": {
    "browser": true,
    "node": true,
    "es6": true
  },
  "rules": {
    "prettier/prettier": [2, { "singleQuote": true, "semi": false }],
    "react/react-in-jsx-scope": 0,
    "react/display-name": 0,
    "react/prop-types": 0,
    "@typescript-eslint/explicit-function-return-type": 0,
    "@typescript-eslint/explicit-member-accessibility": 0,
    "@typescript-eslint/indent": 0,
    "@typescript-eslint/member-delimiter-style": 0,
    "@typescript-eslint/no-explicit-any": 1,
    "@typescript-eslint/no-var-requires": 2,
    "@typescript-eslint/no-use-before-define": 0,
    "@typescript-eslint/no-unused-vars": [
      2,
      {
        "argsIgnorePattern": "^_"
      }
    ],
    "no-console": [
      2,
      {
        "allow": ["warn", "error"]
      }
    ]
  }
}

eslintとprettierが競合しないようprettierに関連する extends は必ずextends配列内の最後に記述します。
rulesのprettier/prettierに関しては後述するvscodeの自動整形時にeslintとprettierが競合してしまうため記載しています。
rulesは開発規約や好みに合わせて設定します。

eslintで対象外にするファイルを設定します。

**/node_modules/*
**/out/*
**/.next/*
storybook-static

package.json にlintのscriptを追加します。

package.json
{
  ...
  "scripts": {
     "build": "next build",
     "start": "next start",
     "storybook": "start-storybook -p 6006",
-    "build-storybook": "build-storybook"
+    "build-storybook": "build-storybook",
+    "lint": "eslint . --ext .ts,.js,.tsx,.jsx",
+    "lint:fix": "eslint --fix . --ext .ts,.js,.tsx,.jsx"
   },
   ...
}

lintを実行してみます

yarn lint

たくさんエラーが出ると思いますので、fixしてみます

yarn lint:fix

Doneが出ればokです。

...
✨  Done in 2.25s.

Prettierのセットアップ

touch .prettierrc.json .prettierignore
.prettierrc.json
{
  "semi": false,
  "singleQuote": true,
  "bracketSpacing": true,
  "tabWidth": 2,
  "printWidth": 100
}
.prettierignore
node_modules
.next
yarn.lock
package-lock.json
public
storybook-static

scriptsに下記を追加します。

package.json
...
"scripts":{
  ...
  "format": "prettier --write ."
}

prettierを実行します。

yarn format

コードが整形されます。

yarn format
yarn run v1.22.10
$ prettier --write .
.eslintrc.json 33ms
.prettierrc.json 10ms
.vscode/settings.json 2ms
components/Button.stories.tsx 203ms
components/Button.tsx 40ms
package.json 5ms
pages/_app.tsx 7ms
pages/api/hello.tsx 9ms
pages/index.tsx 42ms
README.md 49ms
tsconfig.json 6ms
✨  Done in 0.83s.

VSCodeで保存時に自動整形

vscodeでsave時に自動整形が出来るように設定します。

VSCode拡張ツールのESLintが事前に必要なのでインストールします。
https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint

mkdir .vscode
touch ./.vscode/settings.json
settings.json
{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}

エラーが検知されているファイルで保存すると、自動で整形されます。

husky のセットアップ

huskyとはgitのcommit時やpush時に特定のコマンドを実行してくれるツールです。
今回はcommit時に先ほど設定したeslintやprettierが走るように設定します。

yarn add --dev husky lint-staged

下記を追加

package.json
{
  "scripts": {
    ...
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "yarn lint",
      "yarn format"
    ]
  },
  ...
}

コミット時にlint-stagedで設定したscriptsが走ります。

next-typescript-storybook-tailwind-sandbox  (main=)  $git commit -m "husky test"
husky > pre-commit (node v12.16.3)
✔ Preparing...
✔ Running tasks...
✔ Applying modifications...
✔ Cleaning up...
[main 015cc25] husky test
 1 file changed, 1 insertion(+)

これでプロジェクトセットアップ完了です。

リファレンス

Discussion