Open16

Nuxt3とStorybookの相性が悪いのでNext.jsに手をだす

airRnotairRnot

問題

Nuxt3のauto importの影響でstorybookが非常に使いづらい

目的

いっそNextに手を出してみたらどうだろうか

参考

airRnotairRnot

Next

npx create-next-app@latest
Need to install the following packages:
  create-next-app@latest
Ok to proceed? (y) y
✔ What is your project named? … next-template-v1
✔ Would you like to use TypeScript with this project? … No / Yes
✔ Would you like to use ESLint with this project? … No / Yes
✔ Would you like to use `src/` directory with this project? … No / Yes
✔ Would you like to use experimental `app/` directory with this project? … No / Yes
✔ What import alias would you like configured? … @/*

app/ directoryは最近できた機能らしい。特に使わないと思うのでスルー

airRnotairRnot
.
├── .eslintrc.json
├── .gitignore
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── public
│   ├── favicon.ico
│   ├── next.svg
│   ├── thirteen.svg
│   └── vercel.svg
├── src
│   ├── pages
│   │   ├── _app.tsx
│   │   ├── _document.tsx
│   │   ├── api
│   │   │   └── hello.ts
│   │   └── index.tsx
│   └── styles
│       ├── Home.module.css
│       └── globals.css
└── tsconfig.json
airRnotairRnot

静的HTMLとして出力できるようにする

package.json
{
  "name": "next-template-v1",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
+   "export": "next build && next export"
  },
  "dependencies": {
    "@next/font": "13.1.4",
    "@types/node": "18.11.18",
    "@types/react": "18.0.27",
    "@types/react-dom": "18.0.10",
    "eslint": "8.32.0",
    "eslint-config-next": "13.1.4",
    "next": "13.1.4",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "typescript": "4.9.4"
  }
}

Nuxtでいうgeneratorかな?

airRnotairRnot

ESLint + Prettier

インストール

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

設定ファイルの作成

.eslintrc.cjs
module.exports = {
  root: true,
  extends: [
    'next/core-web-vitals',
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier',
  ],
  plugins: ['@typescript-eslint'],
  rules: {
    /* typescript */
    'no-restricted-imports': [
      'error',
      {
        patterns: [
          '../*',
          '~/*',
          '~~/*',
          './assets/*',
          './components/*',
          './pages/*',
          './plugins/*',
          './router/*',
          './hooks/*',
          './server/*',
          './store/*',
          './types/*',
          './utils/*',
          './libs/*',
          './*.vue',
        ],
      },
    ],
    'import/order': [
      'error',
      {
        'groups': [
          'builtin',
          'external',
          'parent',
          'sibling',
          'index',
          'object',
          'type',
        ],
        'pathGroups': [
          {
            pattern: '{react,react-dom}',
            group: 'builtin',
            position: 'before',
          },
          {
            pattern: '@src/**',
            group: 'parent',
            position: 'before',
          },
        ],
        'pathGroupsExcludedImportTypes': ['builtin'],
        'alphabetize': {
          order: 'asc',
        },
        'newlines-between': 'always',
      },
    ],
    '@typescript-eslint/consistent-type-imports': [
      'error',
      { prefer: 'type-imports' },
    ],
  },
};
.prettierrc
{
  "singleQuote": true,
  "semi": true,
  "tabWidth": 2,
  "quoteProps": "consistent",
  "trailingComma": "es5",
  "jsxSingleQuote": false,
  "bracketSpacing": true,
  "arrowParens": "always"
}
airRnotairRnot

TypeScript

インストール

yarn add -D @tsconfig/strictest

tsconfigの編集

tsconfig.json
{
+ "extends": "@tsconfig/strictest/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", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}
airRnotairRnot

TailwindCSS

参考

インストール

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

tailwind.config.cjsの編集

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

globals.cssの編集

src/styles/globals.cssの中身を消去して、以下の通り編集する。なお、Home.module.cssは存在ごと消してOK

src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
.
├── .eslintrc.cjs
├── .gitignore
├── .prettierrc
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── public
│   ├── favicon.ico
│   ├── next.svg
│   ├── thirteen.svg
│   └── vercel.svg
├── src
│   ├── pages
│   │   ├── _app.tsx
│   │   ├── _document.tsx
│   │   ├── api
│   │   │   └── hello.ts
│   │   └── index.tsx
│   └── styles
│       └── globals.css
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock
airRnotairRnot

報告は上がっているっぽい

airRnotairRnot

TailwindCSS用のESLintとPrettierの設定

インストール

yarn add -D eslint-plugin-tailwindcss prettier-plugin-tailwindcss

.eslintrc.cjsの編集

.eslintrc.cjs
module.exports = {
  root: true,
  extends: [
    'next/core-web-vitals',
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
+   'plugin:tailwindcss/recommended',
    'prettier',
  ],
- plugins: ['@typescript-eslint'],
+ plugins: ['@typescript-eslint', 'tailwindcss'],
  rules: {
    /* typescript */
    'no-restricted-imports': [
      'error',
      {
        patterns: [
          '../*',
          '~/*',
          '~~/*',
          './assets/*',
          './components/*',
          './pages/*',
          './plugins/*',
          './router/*',
          './hooks/*',
          './server/*',
          './store/*',
          './types/*',
          './utils/*',
          './libs/*',
          './*.vue',
        ],
      },
    ],
    'import/order': [
      'error',
      {
        'groups': [
          'builtin',
          'external',
          'parent',
          'sibling',
          'index',
          'object',
          'type',
        ],
        'pathGroups': [
          {
            pattern: '{react,react-dom}',
            group: 'builtin',
            position: 'before',
          },
          {
            pattern: '@src/**',
            group: 'parent',
            position: 'before',
          },
        ],
        'pathGroupsExcludedImportTypes': ['builtin'],
        'alphabetize': {
          order: 'asc',
        },
        'newlines-between': 'always',
      },
    ],
    '@typescript-eslint/consistent-type-imports': [
      'error',
      { prefer: 'type-imports' },
    ],
+
+   /* tailwindcss */
+   'tailwindcss/no-custom-classname': [
+     'warn',
+     {
+       config: 'tailwind.config.cjs',
+     },
+   ],
+   'tailwindcss/classnames-order': 'off',
  },
};
airRnotairRnot

daisyUI

インストール

yarn add daisyui

型定義の追加

src/types/global.d.ts
declare module 'daisyui';

tailwind.config.cjsの編集

tailwind.config.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  theme: {
    extend: {},
  },
- plugins: [],
+ plugins: [require('daisyui')],
};
.
├── .eslintrc.cjs
├── .gitignore
├── .prettierrc
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── postcss.config.cjs
├── public
│   ├── favicon.ico
│   ├── next.svg
│   ├── thirteen.svg
│   └── vercel.svg
├── src
│   ├── pages
│   │   ├── _app.tsx
│   │   ├── _document.tsx
│   │   ├── api
│   │   │   └── hello.ts
│   │   └── index.tsx
│   ├── styles
│   │   └── globals.css
│   └── types
│       └── global.d.ts
├── tailwind.config.cjs
├── tsconfig.json
└── yarn.lock
airRnotairRnot

Storybook

インストール

npx sb init --builder webpack5
✔ Do you want to run the 'eslintPlugin' migration on your project? … yes

ESLintの設定

.eslintrc.cjs
module.exports = {
  root: true,
  extends: [
    'next/core-web-vitals',
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:tailwindcss/recommended',
+   'plugin:storybook/recommended',
    'prettier',
  ],
  plugins: ['@typescript-eslint', 'tailwindcss'],
  rules: {
    /* typescript */
    'no-restricted-imports': [
      'error',
      {
        patterns: [
          '../*',
          '~/*',
          '~~/*',
          './assets/*',
          './components/*',
          './pages/*',
          './plugins/*',
          './router/*',
          './hooks/*',
          './server/*',
          './store/*',
          './types/*',
          './utils/*',
          './libs/*',
          './*.vue',
        ],
      },
    ],
    'import/order': [
      'error',
      {
        'groups': [
          'builtin',
          'external',
          'parent',
          'sibling',
          'index',
          'object',
          'type',
        ],
        'pathGroups': [
          {
            pattern: '{react,react-dom}',
            group: 'builtin',
            position: 'before',
          },
          {
            pattern: '@src/**',
            group: 'parent',
            position: 'before',
          },
        ],
        'pathGroupsExcludedImportTypes': ['builtin'],
        'alphabetize': {
          order: 'asc',
        },
        'newlines-between': 'always',
      },
    ],
    '@typescript-eslint/consistent-type-imports': [
      'error',
      {
        prefer: 'type-imports',
      },
    ],
    /* tailwindcss */
    'tailwindcss/no-custom-classname': [
      'warn',
      {
        config: 'tailwind.config.cjs',
      },
    ],
    'tailwindcss/classnames-order': 'off',
  },
};

staticDirsの追加

.storybook/main.js
module.exports = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
+ staticDirs: ['../public'],
};

next/routerに対応させる

yarn add -D storybook-addon-next-router
.storybook/main.js
module.exports = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
+   'storybook-addon-next-router',
  ],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
  staticDirs: ['../public'],
};
.storybook/preview.js
+import { RouterContext } from 'next/dist/shared/lib/router-context';

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

aliasの設定

.storybook/main.js
+const path = require('path');
+const rootPath = path.resolve(__dirname, '../src/');

module.exports = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    'storybook-addon-next-router',
  ],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
  staticDirs: ['../public'],
+ webpackFinal: async (config, { configType }) => {
+   config.resolve.alias['@'] = rootPath;
+   return config;
+ }, 
};

TailwindCSSに対応させる

yarn add -D @storybook/addon-postcss
.storybook/main.js
const path = require('path');
const rootPath = path.resolve(__dirname, '../src/');

module.exports = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    'storybook-addon-next-router',
+   {
+     name: '@storybook/addon-postcss',
+     options: {
+       postcssLoaderOptions: {
+         implementation: require('postcss'),
+       },
+     },
+   },
  ],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
  staticDirs: ['../public'],
  webpackFinal: async (config, { configType }) => {
    config.resolve.alias['@'] = rootPath;
    return config;
  },
};
.storybook/preview.js
+import '@/styles/globals.css';

import { RouterContext } from 'next/dist/shared/lib/router-context';

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

Vitest

参考

インストール

yarn add -D vitest jsdom @vitejs/plugin-react @testing-library/react @testing-library/jest-dom @testing-library/user-event @types/testing-library__user-event

package.jsonの編集

package.json
{
  "name": "next-template-v1",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "export": "next build && next export",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook",
+   "test": "vitest",
+   "coverage": "vitest run --coverage"
  },
  ...
}

setup.tsの作成

src/__tests__/setup.ts
import '@testing-library/jest-dom';

vitest.config.tsの作成

vitest.config.ts
/// <reference types="vitest" />

import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/__tests__/setup.ts',
  },
});

aliasに対応させる

vitest.config.ts
/// <reference types="vitest" />

import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/__tests__/setup.ts',
  },
+ resolve: {
+   alias: {
+     '@': '/src',
+   },
+ },
});
airRnotairRnot
.
├── .eslintrc.cjs
├── .gitignore
├── .prettierrc
├── .storybook
│   ├── main.js
│   └── preview.js
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── postcss.config.cjs
├── public
│   ├── favicon.ico
│   ├── next.svg
│   ├── thirteen.svg
│   └── vercel.svg
├── src
│   ├── __tests__
│   │   └── setup.ts
│   ├── pages
│   │   ├── _app.tsx
│   │   ├── _document.tsx
│   │   ├── api
│   │   │   └── hello.ts
│   │   └── index.tsx
│   ├── styles
│   │   └── globals.css
│   └── types
│       └── global.d.ts
├── tailwind.config.cjs
├── tsconfig.json
├── vitest.config.ts
└── yarn.lock
airRnotairRnot

Vitest用のESLintの設定

インストール

yarn add -D eslint-plugin-vitest

.eslintrc.cjsの編集

.eslintrc.cjs
module.exports = {
  root: true,
  extends: [
    'next/core-web-vitals',
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:tailwindcss/recommended',
    'plugin:storybook/recommended',
    'prettier',
  ],
- plugins: ['@typescript-eslint', 'tailwindcss'],
+ plugins: ['@typescript-eslint', 'tailwindcss', 'vitest'],
  rules: {
    /* typescript */
    'no-restricted-imports': [
      'error',
      {
        patterns: [
          '../*',
          '~/*',
          '~~/*',
          './assets/*',
          './components/*',
          './pages/*',
          './plugins/*',
          './router/*',
          './hooks/*',
          './server/*',
          './store/*',
          './types/*',
          './utils/*',
          './libs/*',
          './*.vue',
        ],
      },
    ],
    'import/order': [
      'error',
      {
        'groups': [
          'builtin',
          'external',
          'parent',
          'sibling',
          'index',
          'object',
          'type',
        ],
        'pathGroups': [
          {
            pattern: '{react,react-dom}',
            group: 'builtin',
            position: 'before',
          },
          {
            pattern: '@src/**',
            group: 'parent',
            position: 'before',
          },
        ],
        'pathGroupsExcludedImportTypes': ['builtin'],
        'alphabetize': {
          order: 'asc',
        },
        'newlines-between': 'always',
      },
    ],
    '@typescript-eslint/consistent-type-imports': [
      'error',
      {
        prefer: 'type-imports',
      },
    ],
    /* tailwindcss */
    'tailwindcss/no-custom-classname': [
      'warn',
      {
        config: 'tailwind.config.cjs',
      },
    ],
    'tailwindcss/classnames-order': 'off',
+   /* vitest */
+   'vitest/consistent-test-it': [
+     'error',
+     {
+       fn: 'test',
+     },
+   ],
+   'vitest/expect-expect': 'warn',
+   'vitest/lower-case-title': 'off',
+   'vitest/max-nested-describe': [
+     'error',
+     {
+       max: 2,
+     },
+   ],
+   'vitest/no-conditional-tests': 'error',
+   'vitest/no-focused-tests': 'warn',
+   'vitest/no-identical-title': 'error',
+   'vitest/no-skipped-tests': 'warn',
  },
};