🍃

Next.js(v11.1)+TypeScript+Tailwind+SASS+Storybook+Jestのボイラープレート

2021/08/29に公開

先日、Next.js 11.1がリリースされました。
https://nextjs.org/blog/next-11-1

Storybookの最新版インストールを試してみたのですが、中々うまくいかなかったりしたので、環境の整理ついでにボイラープレートとしてまとめてみました。

構成は以下のようになっています。

  • yarn
  • Next.js v11.1.0
  • ESLint v7.32.0(Next.js v11.1から含まれるように)
  • TypeScript v4.4.2
  • Tailwind v2.2.8
  • Storybook v6.3
  • Prettier v2.3.2
  • Jest v27.1.0

コードの全体はこちらです。
https://github.com/otanu/nextjs-boilerplate

Next.js インストール

npx create-next-app --ts nextjs-ts-storybook-tailwind

rm package-lock.json
yarn install
yarn dev

Tailwindの設定

Tailwindのドキュメントに沿って入れていきます。

https://tailwindcss.com/docs/guides/nextjs

yarn add -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p

以下のようにファイルが作られます。

postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
tailwind.config.js
module.exports = {
  purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
};

Next.jsのグローバルのCSSの設定に、Tailwindの設定を入れます。
元にあった値は使わないので、まるごと入れ替えです。

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

サンプルページのスタイルの設定はガラリと変わるので、Next.jsのTailwindのサンプルから拝借。

https://github.com/vercel/next.js/tree/canary/examples/with-tailwindcss
(https://github.com/vercel/next.js/blob/canary/examples/with-tailwindcss/pages/index.js)

画像の部分はESLintのエラーが出るので、気になる場合は以下のように差し替え。

pages/index.tsx
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />

Home.modules.cssは不要になるので削除します。

Sassの設定

SCSSを使うため、Next.jsのドキュメントに沿ってSASSを入れて行きます。

https://nextjs.org/docs/basic-features/built-in-css-support#sass-support

yarn add sass
next.config.js
/** @type {import('next').NextConfig} */

const path = require("path");

module.exports = {
  reactStrictMode: true,
  sassOptions: {
    includePaths: [path.join(__dirname, "styles")],
  },
};

styles/globals.cssstyles/globals.scss に変更。
pages/_app.tsx のように、CSSをimportしている箇所も合わせて修正。

Storybookの設定

初期設定やサンプルのインストールは以下のコマンドで出来ます。

npx sb init

インストール完了後、yarn storybookのように起動コマンドの例が表示されますが、Tailwind(PostCSS)やSASSの設定をしないと起動出来ないので先に設定を行います。

Error: PostCSS plugin tailwindcss requires PostCSS 8.

おそらく、Next.jsの内部でWebpackが色々やっていることを、Storybook側でもやらないといけないようです。
この手順では、以下の設定を追加していきます。

  • StorybookをWebpack5 で起動出来るようにする
  • PostCSS/SASSのloaderを追加する

@storybook/addon-postcssなどStorybook用のプラグインはありますが、パッケージのバージョンの問題が色々発生するので、この方法を選びました。

yarn add -D webpack @storybook/builder-webpack5 @storybook/manager-webpack5
yarn add -D style-loader css-loader postcss-loader sass-loader
.storybook/main.js
module.exports = {
  stories: [
    "../stories/**/*.stories.mdx",
    "../stories/**/*.stories.@(js|jsx|ts|tsx)",
  ],
  addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
  core: {
    builder: "webpack5",
  },
  webpackFinal: (config) => {
    config.module.rules.push({
      test: /\.scss$/,
      sideEffects: true,
      use: [
        "style-loader",
        {
          loader: "css-loader",
          options: {
            importLoaders: 2,
          },
        },
        {
          loader: "postcss-loader",
          options: {
            postcssOptions: {
              plugins: [require("tailwindcss"), require("autoprefixer")],
            },
          },
        },
        {
          loader: "sass-loader",
          options: {
            sourceMap: true,
          },
        },
      ],
    });

    return config;
  },
};
.storybook/preview.js
import "../styles/globals.scss"

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
}

StorybookのサンプルファイルをSCSSに変更します。

stories/button.css ⇒ stories/button.scss

stories/Button.tsx のimportも直します。

Tailwindが効いていることを確認するため、スタイルを入れ替えてみます。

.storybook-button--primary {
  color: white;
  background-color: #1ea7fd;
}
.storybook-button--primary {
  @apply text-white bg-blue-600
}

設定に問題なければ、yarn storybookで以下のように表示されます。

Prettierの設定

https://prettier.io/docs/en/install.html

yarn add -D prettier eslint-config-prettier
echo {}> .prettierrc.json
touch .prettierignore

Prettierの対象にしないディレクトリ・ファイルをここで指定。

.next
node_modules

Prettierの設定を入れます。内容はお好みで。

.prettierrc.json
{
  "endOfLine": "lf",
  "semi": false,
  "trailingComma": "all",
  "singleQuote": true,
  "printWidth": 100,
  "tabWidth": 2
}

Eslintと干渉しないように設定を追加。

.eslintrc.json
{
  "extends": ["next/core-web-vitals", "prettier"]
}

yarn prettier --check . で対象のファイルを確認。対象にしたくないファイルが含まれていたら、.prettierignore に追加します。

yarn prettier で実行できるように、package.json のscriptsに以下の設定を追加します。

package.json
"prettier": "prettier --write .",

Jestの設定

単純な関数のテストが出来る所までの設定です。

ReactやHooksのテストや、Next.jsのCypressを用いたEnd-toEndのテスト などは追加で設定が必要です。

yarn add -D jest @types/jest ts-jest ts-node

Jestの起動コマンドをpackage.jsonscriptsに追加。

package.json
"test": "jest"

Jestの設定を入れます。TypeScriptでも書けるようです。

jest.config.ts
import type { Config } from '@jest/types'

const config: Config.InitialOptions = {
  roots: ['<rootDir>/'],
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
  },
}

export default config

試しに、以下のようにサンプルコードとテストを追加してみます。

lib/sum.ts
export const add = (a: number, b: number) => a + b
lib/sum.test.ts
import { add } from './sum'

test('add', () => {
  expect(add(1, 1)).toEqual(2)
})

yarn test でテストを実行できます。

yarn test
> yarn run v1.22.11
> $ jest
>  PASS  lib/sum.test.ts
>add (3 ms)
> 
> Test Suites: 1 passed, 1 total
> Tests:       1 passed, 1 total
> Snapshots:   0 total
> Time:        1.995 s, estimated 3 s
> Ran all test suites.
> ✨  Done in 6.22s.

Importのエイリアスの設定

エイリアスの設定でコンポーネントのimportの相対パスを以下のように省略出来ます。

import '../styles/globals.scss'
import '@/styles/globals.scss'

tsconfig.json、Storybook、Jestのそれぞれで設定する必要があります。

tsconfig.json
"compilerOptions": {
    ...
    "baseUrl": ".",
    "paths": {
      "@/*": ["*"]
    }
  },
.storybook/main.js
const path = require('path')

module.exports = {
~~省略~~
  webpackFinal: (config) => {
    config.resolve.alias = {
      ...config.resolve.alias,
      '@': path.resolve(__dirname + '/..'),
    }

~~省略~~
jest.config.ts
import type { Config } from '@jest/types'

const config: Config.InitialOptions = {
  roots: ['<rootDir>/'],
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
  },
  moduleNameMapper: {
    '@/(.+)': '<rootDir>/$1',
  },
}

export default config

Discussion