🐥

Next.js + TypeScript + TailwindCSS + storybook + jest のテンプレートを作った話 後編

2023/04/27に公開

自己紹介

こんにちは、2022 年の秋から駆け出し web エンジニアのです。
現在は都内の SES 企業で働いております。
自分の担当領域は基本的にフロントエンドですが、バックエンドの開発を行うこともあります。

最初にプログラミングに興味を持ち始めたのは緊急事態宣言がきっかけでした。
遊び感覚で始めた Progate が楽しくて、いつの間にか仕事にしたいと思い始めて今に至ります。

拙い文章ではありますが頑張って書いていこうと思うので、お読みいただけると幸いです!

はじめに

こちらの記事は後編です!
前編の記事もよろしければ御覧ください!

https://zenn.dev/xeiculy/articles/a9ff6e39471dc0

簡単に前編の内容をまとめると、

「Next.js の開発環境を毎回構築するのめんどい」
「そうだ、テンプレ作ったろ」

です。

こちらが私が作成したテンプレートです。

https://github.com/XeicuLy/NextJS-TypeScript-TailwindCSS-Template

使用技術はざっと、

ツール名 説明
Next.js v13 React フレームワークで、app ディレクトリは不採用
TypeScript v5 JavaScript の上位互換言語
TailwindCSS v3 CSS フレームワーク。daisyUI というフレームワークを使用
axios API を叩くためのライブラリ
Recoil React 状態管理ライブラリ
Prettier コードフォーマッター
ESLint コード書き方をチェックするツール
storybook v7 コンポーネント単位での UI デザインを確認できるツール
Jest JavaScript のテストフレームワーク
Husky コミットやプッシュ時に任意のコマンドを自動実行できるツール
hygen コンポーネントジェネレーター
Volta Node のバージョン管理ツール

こんな感じですね。

今回は Volta が導入済みであるという前提で作成していきます。

まだの方はこちらの記事を参考にしていただければなと思います。

https://zenn.dev/xeiculy/articles/03871845342228

別に Volta 使わなくていい人はそのへん読み飛ばして頂いたりして構いません。
ご自身の好きなように環境構築していただければと思います!

では早速作成していきましょう!

なお今回は yarn で作成していきます。
npm で作成したい方は適宜読み替えてください。

create next-app でプロジェクトを作成

まずはプロジェクトを作成します。
今回の NextJS のバージョンは 13 ですが、ベータ版の app ディレクトリは採用していません。

yarn create next-app

すると、プロジェクト名を聞かれるので、適当に入力してください。
今回は nextjs-template とします。

yarn create next-app
yarn create v1.22.19
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Installed "create-next-app@13.3.1" with binaries:
      - create-next-app
✔ What is your project named? … nextjs-template
✔ Would you like to use TypeScript with this project? … No / Yes # Yes
✔ Would you like to use ESLint with this project? … No / Yes # Yes
✔ Would you like to use Tailwind CSS with this project? … No / Yes # Yes
✔ Would you like to use `src/` directory with this project? … No / Yes # Yes
✔ Would you like to use experimental `app/` directory with this project? … No / Yes # No
✔ What import alias would you like configured? … @/* # そのままEnter

これ今知ったんですが、TailwindCSS 使うかどうか聞かれるようになったんですね、、笑

さて、プロジェクトが作成されたら、作成したプロジェクトに移動します。

cd nextjs-template

試しに yarn dev で起動してみましょう。

yarn dev

http://localhost:3000
にアクセスして見てください。おそらくページが表示できているのではないかと思います!

ここから先は必要なことを順不同で行っていきます。

不要なファイルを削除

まずは不要なファイルを削除していきます。

src/pages/api/hello.ts は不要なので削除します。

rm src/pages/api/hello.ts

としてファイルを削除でもいいですし、ディレクトリごと消しちゃってもいいです。

Node と yarn のバージョンを固定する

Volta を使って固定していきます。

volta pin node@18
volta pin yarn@1

とすると、

package.json
{
  "volta": {
    "node": "18.15.0",
    "yarn": "1.22.19"
  }
}

という用になっていると思います。

axios 導入

次に axios を導入します。

yarn add axios

Recoil 導入

次に Recoil を導入します。

yarn add recoil

src/pages/_app.tsxに追記します

src/pages/_app.tsx
import type { AppProps } from 'next/app';
import { RecoilRoot } from 'recoil';
import '@/styles/globals.css';

const App = ({ Component, pageProps }: AppProps) => {
  return (
    <RecoilRoot>
      <Component {...pageProps} />
    </RecoilRoot>
  );
};

export default App;

このようにすることでアプリ全体で使えるようになります。

テスト環境の構築

次にテスト環境を構築していきます。

yarn add -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event @types/jest @types/testing-library__jest-dom eslint-plugin-jest ts-jest

インストールが終わったら、jest.config.jsjest.setup.ts を作成します。

touch jest.config.js && touch jest.setup.ts

そして、以下の内容を記述します。

jest.setup.ts
import '@testing-library/jest-dom';
jest.config.js
const nextJest = require('next/jest');

const createJestConfig = nextJest({
  dir: './',
});

const customJestConfig = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  testEnvironment: 'jest-environment-jsdom',
};

module.exports = createJestConfig(customJestConfig);

そして、package.jsonの scripts に test を追加します。

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:watchAll": "jest --watchAll"
  },
}

tsconfig.ts にも以下の内容を追記しておきましょう

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

storybook 導入

次に storybook を導入します。

yarn add storybook init
yarn add -D @storybook/addon-essentials @storybook/addon-interactions @storybook/addon-links @storybook/blocks @storybook/addon-styling @storybook/nextjs @storybook/react @storybook/testing-library eslint-plugin-storybook

インストールが完了したら、package.jsonの scripts に storybook を追加します。

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:watchAll": "jest --watchAll",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build"
  },
}

storybook の設定ファイルを作成しましょう。

npx sb init

実行が終了すれば、.storybookディレクトリが作成されているはずです。
そしてその中にmain.tspreview.tsが作成されていれば OK です!

下記のようにしてください。

main.ts
import type { StorybookConfig } from '@storybook/nextjs';
const config: StorybookConfig = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-styling',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/nextjs',
    options: {},
  },
  docs: {
    autodocs: 'tag',
  },
};
export default config;

storybook v7.0.5 のときの話ですが、
storybook に tailwindCSS を適用させるには addons に'@storybook/addon-styling'が必要になります。

以前は

addons: [
  {
    name: '@storybook/addon-postcss',
    options: {
      postcssLoaderOptions: {
        implementation: require('postcss'),
      },
    },
  },
];

という感じだったらしいんですけど、前者に変更となりました。

続けて、

preview.ts
import type { Preview } from '@storybook/react';

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

export default preview;

あとはsrcディレクトリに stories というディレクトリが作成されていると思います!

ではここで storybook を試しに起動してみましょう!

yarn storybook

とすると、
http://localhost:6006
にアクセスできるはずです。

こんな感じで使えます。
今はstoriesディレクトリのものを見ているような感じになります。

正常な起動が確認できたらそのディレクトリは基本的には使わないので、削除しちゃって大丈夫です!

husky と lint-staged 導入

まずはインストールしていきましょう

yarn add -D husky lint-staged

インストールが終わったら、commit 時に実行したいコマンドを設定していきましょう

ファイルを作成してください

touch .lintstagedrc.json

そして作成したファイルの中に以下を記述していきます

.lintstagedrc.json
{
  "**/*.{js,jsx,ts,tsx}": "npx eslint --fix",
  "**/*.{js,jsx,ts,tsx,json}": "npx prettier --write",
  "**/*.{spec,test}.{js,jsx,ts,tsx}": "npx jest"
}

整形後、テストするっていう流れにしています。

では次に git hooks を有効化するために以下を実行します。

yarn husky install

こうすると、.huskyディレクトリが作成されたのではないでしょうか?

そうしたら、scripts を登録しましょう

packsge.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:watchAll": "jest --watchAll",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "prepare": "husky install"
  },
}

husky installは GitHub リポジトリから clone して各パッケージをインストールするタイミングで、git hooks を有効化するために設定しています。

では最後に commit 前の hooks を作っていきましょう

yarn husky add .husky/pre-commit "yarn lint-staged"

こうすると、.huskyディレクトリ配下に pre-commit というものが爆誕して、

pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn lint-staged

という感じになっているのではと思います。

これで設定は OK です!

TailwindCSS 導入

インストールした時に勝手にやってくれていますが、いくつか手を加えましょう!

まずインストールします

yarn add -D prettier-plugin-tailwindcss

後ほど導入します。

では、next.config.js に手を加えます

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  swcMinify: true,
  reactStrictMode: true,
}

module.exports = nextConfig

postcss.config.js に手を加えます

postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
    'postcss-nested': {},
  },
};

tailwind.config.jsに手を加えます

tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

tsconfig.jsonに手を加えます

tsconfig.json
{
  "include": [
    "next-env.d.ts",
    "**/*.js",
    "**/*.jsx",
    "**/*.ts",
    "**/*.tsx",
    "postcss.config.js",
    "jest.config.js"
  ],
}

そうしましたら、src/styles/global.cssを書き換えます。

src/styles/global.css
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

この 3 行で終了です。

daisyUI

では daisyUI を導入します。

yarn add daisyui

tailwind.config.jsに手を加えます

tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [require('daisyui')],
}

これで終了です。

hygen 導入

まずはインストールをしていきましょう

yarn add -D hygen

scriptsにも登録します。

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:watchAll": "jest --watchAll",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "prepare": "husky install",
    "gen": "hygen generator components"
  },
}

では早速テンプレートを作成していきます。

yarn hygen init self

そうすると、
_templateディレクトリが出来上がっているのではないでしょうか?

そうしたらディレクトリ構成はこんな感じにしてください。

_templates
 └─ generator
      └─ components
         ├─ component.ejs.t
         ├─ component.stories.ejs.t
         ├─ component.test.ejs.t
         └─ prompt.js

では最初は、CLI での対話をどんなものにするのかを決めるため、prompt.jsを編集しましょう。

_template/generator/components/prompt.js
module.exports = [
  {
    type: 'select',
    name: 'atomic',
    message: 'select directory',
    choices: ['atoms', 'molecules', 'organisms', 'templates'],
  },
  {
    type: 'input',
    name: 'component_name',
    message: 'input component name',
    validate: input => input !== '',
  },
];
  • type: 自分が入力する input や選択型の select など
  • name: input や select で入力した値が入る変数名
  • message: 対話に出てくるメッセージ

という感じになっているので、そのように設定しています。

  • 1 つ目の質問: どのディレクトリにする?
  • 2 つ目の質問: コンポーネントの名前は?

って感じです。
inputに関しては必須入力としたいので、バリデーションかけてます。

それでは、

  • コンポーネントファイル
  • テスト用ファイル
  • storybook 用ファイル

の雛形を作成していきます。

component.ejs.t
---
to: src/components/<%= atomic %>/<%= h.changeCase.pascal(component_name) %>/index.tsx
---

import { FC } from 'react';

export const <%= h.changeCase.pascal(component_name) %>: FC = () => {
  return (
    <>
      <h1><%= h.changeCase.pascal(component_name) %></h1>
    </>
  );
};
component.stories.ejs.t
---
to: src/components/<%= atomic %>/<%= h.changeCase.pascal(component_name) %>/index.stories.tsx
---

import type { Meta, StoryObj } from '@storybook/react';
import { <%= h.changeCase.pascal(component_name) %> } from '.';

const meta = {
  title: 'components/<%= atomic %>/<%= h.changeCase.pascal(component_name) %>',
  component: <%= h.changeCase.pascal(component_name) %>,
  parameters: {},
  args: {},
} satisfies Meta<typeof <%= h.changeCase.pascal(component_name) %>>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
  args: {
    primary: true,
  },
};
component.test.ejs.t
---
to: src/components/<%= atomic %>/<%= h.changeCase.pascal(component_name) %>/index.test.tsx
---

import { render } from '@testing-library/react';
import { <%= h.changeCase.pascal(component_name) %> } from '.';

describe('<%= atomic %>/<%= h.changeCase.pascal(component_name) %>', () => {
  it('renders correctly', () => {
    const { container } = render(<<%= h.changeCase.pascal(component_name) %> />);
    expect(container).toMatchSnapshot();
  });
});

これで雛形ができました。
これは一例なので、皆さんが好きなように作っていただければと思います。

では実際に作成してみましょう!

yarn gen

どうでしょう?ご自身が設定された対話が出てきて、終わったらコンポーネント作成されたかと思います!

これでファイル作成の手間がかなり削減できたのではないかと思います。

prettier と eslint の設定

まずはイントールをしていきましょう

yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-import eslint-plugin-unused-imports prettier stylelint-config-prettier

一応 script も登録してきましょう

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "lint:js": "eslint --fix '**/*.@(js|ts|tsx)'",
    "format": "prettier --ignore-path .gitignore --write .",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:watchAll": "jest --watchAll",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "prepare": "husky install",
    "gen": "hygen generator components"
  },
}

次に設定ファイルを作成していきます。

touch .prettierrc.json
mkdir .vscode && touch .vscode/setting.json

設定ファイルに追記していきます。

.eslintrc.json
{
  "env": {
    "browser": true,
    "es6": true,
    "node": true,
    "jest/globals": true
  },
  "plugins": ["unused-imports", "jest"],
  "extends": [
    "plugin:jest/recommended",
    "plugin:jest/style",
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "next/core-web-vitals",
    "plugin:storybook/recommended",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "rules": {
    "jest/consistent-test-it": ["error", { "fn": "it" }],
    "jest/require-top-level-describe": ["error"],
    "no-unused-vars": "off",
    "unused-imports/no-unused-imports": "error",
    "react/jsx-filename-extension": ["error", { "extensions": [".jsx", ".tsx"] }],
    "react/jsx-props-no-spreading": "off",
    "import/prefer-default-export": "off",
    "react/function-component-definition": [
      2,
      {
        "namedComponents": "arrow-function",
        "unnamedComponents": "arrow-function"
      }
    ],
    "arrow-body-style": "off",
    "import/extensions": [
      "error",
      "ignorePackages",
      {
        "js": "never",
        "jsx": "never",
        "ts": "never",
        "tsx": "never"
      }
    ],
    "@typescript-eslint/explicit-function-return-type": 0,
    "@typescript-eslint/no-explicit-any": 0,
    "@typescript-eslint/no-empty-function": 0,
    "react/prop-types": 0,
    "react/react-in-jsx-scope": 0,
    "no-empty-function": 0,
    "@typescript-eslint/ban-ts-comment": 0
  },
  "overrides": [
    {
      "files": ["src/pages/**/*", "*.stories.{ts,tsx}"],
      "rules": { "import/no-default-export": "off" }
    }
  ]
}

rules 等はお好きなのを入れてください。
最初は空でもいいと思いますけどね。
ホントこの辺は十人十色だと思います。

次に prettier の設定ファイルを

.prettierrc.json
{
  "arrowParens": "avoid",
  "trailingComma": "all",
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true,
  "jsxSingleQuote": true,
  "printWidth": 100,
  "importOrder": [
    "^(react(.*)/(.*)$)|^react$",
    "<THIRD_PARTY_TS_TYPES>",
    "<THIRD_PARTY_MODULES>",
    "^types$",
    "^@/(.*)$",
    "^[./]"
  ]
}

Tailwind 導入の時にインストールしたプラグインもここで追加しましょう

touch prettier.config.js
prettier.config.js
module.exports = {
  plugins: [require('prettier-plugin-tailwindcss')],
};

なんとなくこんな感じで。
vscode の設定も

.vscode/setting.json
{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.addMissingImports": true,
    "source.fixAll.eslint": true
  }
}

こんな感じにしておきましょう。

まとめ

さて、これで設定が一通り終わりました。
漏れがあったらすみません、、。

ここで完成形を載せておきます。

うまくいかなかったらコピペして頂くか、リポジトリから clone してください。
https://github.com/XeicuLy/NextJS-TypeScript-TailwindCSS-Template

package.json
package.json
{
  "name": "nextjs-template",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "lint:js": "eslint --fix '**/*.@(js|ts|tsx)'",
    "format": "prettier --ignore-path .gitignore --write .",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:watchAll": "jest --watchAll",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "prepare": "husky install",
    "gen": "hygen generator components"
  },
  "dependencies": {
    "@types/node": "18.16.1",
    "@types/react": "18.2.0",
    "@types/react-dom": "18.2.1",
    "autoprefixer": "^10.4.14",
    "axios": "^1.3.4",
    "daisyui": "^2.51.5",
    "eslint": "8.37.0",
    "eslint-config-next": "13.2.4",
    "init": "^0.1.2",
    "next": "13.3.1",
    "next-themes": "^0.2.1",
    "postcss": "^8.4.21",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-icons": "^4.8.0",
    "recoil": "^0.7.7",
    "storybook": "^7.0.5",
    "tailwindcss": "^3.3.0",
    "typescript": "5.0.3"
  },
  "volta": {
    "node": "18.16.0",
    "yarn": "1.22.19"
  },
  "devDependencies": {
    "@storybook/addon-essentials": "^7.0.5",
    "@storybook/addon-interactions": "^7.0.5",
    "@storybook/addon-links": "^7.0.5",
    "@storybook/addon-styling": "^1.0.1",
    "@storybook/blocks": "^7.0.5",
    "@storybook/nextjs": "^7.0.5",
    "@storybook/react": "^7.0.5",
    "@storybook/testing-library": "^0.1.0",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^14.0.0",
    "@testing-library/user-event": "^14.4.3",
    "@trivago/prettier-plugin-sort-imports": "^4.1.1",
    "@types/jest": "^29.5.0",
    "@types/testing-library__jest-dom": "^5.14.5",
    "@typescript-eslint/eslint-plugin": "^5.57.0",
    "@typescript-eslint/parser": "^5.57.0",
    "eslint-config-prettier": "^8.7.0",
    "eslint-plugin-import": "^2.27.5",
    "eslint-plugin-jest": "^27.2.1",
    "eslint-plugin-storybook": "^0.6.11",
    "eslint-plugin-unused-imports": "^2.0.0",
    "husky": "^8.0.3",
    "hygen": "^6.2.11",
    "jest": "^29.5.0",
    "jest-environment-jsdom": "^29.5.0",
    "lint-staged": "^13.2.0",
    "prettier": "^2.8.4",
    "prettier-plugin-tailwindcss": "^0.2.5",
    "stylelint-config-prettier": "^9.0.5",
    "ts-jest": "^29.1.0"
  }
}
.eslintrc.json
.eslintrc.json
{
  "env": {
    "browser": true,
    "es6": true,
    "node": true,
    "jest/globals": true
  },
  "plugins": ["unused-imports", "jest"],
  "extends": [
    "plugin:jest/recommended",
    "plugin:jest/style",
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "next/core-web-vitals",
    "plugin:storybook/recommended",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "rules": {
    "jest/consistent-test-it": ["error", { "fn": "it" }],
    "jest/require-top-level-describe": ["error"],
    "no-unused-vars": "off",
    "unused-imports/no-unused-imports": "error",
    "react/jsx-filename-extension": ["error", { "extensions": [".jsx", ".tsx"] }],
    "react/jsx-props-no-spreading": "off",
    "import/prefer-default-export": "off",
    "react/function-component-definition": [
      2,
      {
        "namedComponents": "arrow-function",
        "unnamedComponents": "arrow-function"
      }
    ],
    "arrow-body-style": "off",
    "import/extensions": [
      "error",
      "ignorePackages",
      {
        "js": "never",
        "jsx": "never",
        "ts": "never",
        "tsx": "never"
      }
    ],
    "@typescript-eslint/explicit-function-return-type": 0,
    "@typescript-eslint/no-explicit-any": 0,
    "@typescript-eslint/no-empty-function": 0,
    "react/prop-types": 0,
    "react/react-in-jsx-scope": 0,
    "no-empty-function": 0,
    "@typescript-eslint/ban-ts-comment": 0
  },
  "overrides": [
    {
      "files": ["src/pages/**/*", "*.stories.{ts,tsx}"],
      "rules": { "import/no-default-export": "off" }
    }
  ]
}
.lintstagedrc.json
.lintstagedrc.json
{
  "**/*.{js,jsx,ts,tsx}": "npx eslint --fix",
  "**/*.{js,jsx,ts,tsx,json}": "npx prettier --write",
  "**/*.{spec,test}.{js,jsx,ts,tsx}": "npx jest"
}
.prettierrc.json
.prettierrc.json
{
  "arrowParens": "avoid",
  "trailingComma": "all",
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true,
  "jsxSingleQuote": true,
  "printWidth": 100,
  "importOrder": [
    "^(react(.*)/(.*)$)|^react$",
    "<THIRD_PARTY_TS_TYPES>",
    "<THIRD_PARTY_MODULES>",
    "^types$",
    "^@/(.*)$",
    "^[./]"
  ]
}
jest.config.js
jest.config.js
// eslint-disable-next-line @typescript-eslint/no-var-requires
const nextJest = require('next/jest');

const createJestConfig = nextJest({
  dir: './',
});

const customJestConfig = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  testEnvironment: 'jest-environment-jsdom',
};

module.exports = createJestConfig(customJestConfig);
jest.setup.ts
jest.setup.ts
import '@testing-library/jest-dom';
next.config.js
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  swcMinify: true,
  reactStrictMode: true,
};

module.exports = nextConfig;
postcss.config.js
postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
    'postcss-nested': {},
  },
};
prettier.config.js
prettier.config.js
module.exports = {
  plugins: [require('prettier-plugin-tailwindcss')],
};
tailwind.config.js
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  mode: 'jit',
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [require('daisyui')],
};
tsconfig.json
tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.js",
    "**/*.jsx",
    "**/*.ts",
    "**/*.tsx",
    "postcss.config.js",
    "jest.config.js"
  ],
  "exclude": ["node_modules"]
}
template/generator/components/component.ejs.t
_template/generator/components/component.ejs.t
---
to: src/components/<%= atomic %>/<%= h.changeCase.pascal(component_name) %>/index.tsx
---

import { FC } from 'react';

export const <%= h.changeCase.pascal(component_name) %>: FC = () => {
  return (
    <>
      <h1><%= h.changeCase.pascal(component_name) %></h1>
    </>
  );
};
template/generator/components/component.stories.ejs.t
_template/generator/components/component.stories.ejs.t
---
to: src/components/<%= atomic %>/<%= h.changeCase.pascal(component_name) %>/index.stories.tsx
---

import type { Meta, StoryObj } from '@storybook/react';
import { <%= h.changeCase.pascal(component_name) %> } from '.';

const meta = {
  title: 'components/<%= atomic %>/<%= h.changeCase.pascal(component_name) %>',
  component: <%= h.changeCase.pascal(component_name) %>,
  parameters: {},
  args: {},
} satisfies Meta<typeof <%= h.changeCase.pascal(component_name) %>>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
  args: {
    primary: true,
  },
};
template/generator/components/component.test.ejs.t
_template/generator/components/component.test.ejs.t
---
to: src/components/<%= atomic %>/<%= h.changeCase.pascal(component_name) %>/index.test.tsx
---

import { render } from '@testing-library/react';
import { <%= h.changeCase.pascal(component_name) %> } from '.';

describe('<%= atomic %>/<%= h.changeCase.pascal(component_name) %>', () => {
  it('renders correctly', () => {
    const { container } = render(<<%= h.changeCase.pascal(component_name) %> />);
    expect(container).toMatchSnapshot();
  });
});

template/generator/components/prompt.js
_template/generator/components/prompt.js
module.exports = [
  {
    type: 'select',
    name: 'atomic',
    message: 'select directory',
    choices: ['atoms', 'molecules', 'organisms', 'templates'],
  },
  {
    type: 'input',
    name: 'component_name',
    message: 'input component name',
    validate: input => input !== '',
  },
];

.husky/pre-commit
.husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn lint-staged
.storybook/main.js
.storybook/main.js
import type { StorybookConfig } from '@storybook/nextjs';

const config: StorybookConfig = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-styling',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/nextjs',
    options: {},
  },
  docs: {
    autodocs: 'tag',
  },
};
export default config;
.storybook/preview.js
.storybook/preview.js
import type { Preview } from '@storybook/react';
import '@/styles/globals.css';

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

export default preview;

.vscode/setting.json
.vscode/setting.json
{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.addMissingImports": true,
    "source.fixAll.eslint": true
  }
}

こんな感じでしょうか?

これも漏れていれば追記いたします!

皆さんの参考になった部分があれば幸いです。

では、良きカタカタライフを: )

GitHubで編集を提案

Discussion