Nuxt3 + Tailwind CSS + Storybook ver7 環境の作り方

# はじめに
少し前にNuxt3 + Tailwind CSS + Storybook ver7の環境を作ったことがありました。
storybookの導入時NuxtのAutoImportの対応が思いの外時間がかかったので備忘録として環境構築の流れを書いてみました。
# 環境
```
node v18.15.0
nuxt ^3.3.2
tailwindcss ^3.3.0
storybook ^7.0.5
```
# Nuxt3プロジェクト作成
まずはNuxt3プロジェクトを作ります。
## プロジェクト作成
[公式ドキュメント](https://nuxt.com/docs/getting-started/installation)に従ってプロジェクトを作成します。
```shell-session
$ npx nuxi init <project-name>
$ cd <project-name>
$ yarn install
```
インストールできたら動作確認します。
```shell-session
$ yarn dev
```
`http://localhost:3000/`にアクセスするとWelcome to Nuxt!とページが出ました。
これでNuxt3の土台はできました。
# Tailwind CSS導入
こちらも[公式ドキュメント](https://tailwindcss.com/docs/installation)に従って導入します。
```shell-session
$ yarn add -D tailwindcss postcss autoprefixer

$ npx tailwindcss init
// tailwind.config.jsが作成される
```
### postcss.config.jsを作成
StorybookでTailwind CSSを読み込むために`postcss.config.js`を作成します。
```js:postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};
```
## nuxt.config.tsに追記
`postcss.config.js`を作成しましたが、Nuxt3では読み込むために`nuxt.config.ts`に書く必要があります。
```diff_typescript:nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
+  postcss: {
+    plugins: {
+      tailwindcss: {},
+      autoprefixer: {},
+    },
+  },
});
```
## tailwind.config.jsのcontentを書き換える
ここはディレクトリ構成によるので、自分の構成に合わせて書いてみてください。一般的な構成の場合は以下でいいと思います。
```diff_javascript:tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
+   "./*.{vue,js,ts,jsx,tsx}",
+   "./components/**/*.{vue,js,ts,jsx,tsx}",
+   "./layouts/**/*.{vue,js,ts,jsx,tsx}",
+   "./pages/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

```
Tailwind CSSをCSSファイルに追加します。
今回は`assets/css/main.css`にしました。
```css:main.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
nuxt.config.tsでmain.cssを読み込みます。
```diff_typescript:nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  postcss: {
    plugins: {
      tailwindcss: {},
      autoprefixer: {},
    },
  },
+ css: ["~/assets/css/main.css"],
});
```
Tailwind CSSが適用されているか`app.vue`を書き換えて確認しましょう。
```vue:app.vue
<template>
  <div class="text-red-500">Hello tailwind CSS!</div>
</template>
```
文字が赤くなったのでうまくいきました。
# Storybook導入
一番沼ったところです。Nuxtの自動インポートをStorybookに適応したりする部分が割と面倒ですがやっていきましょう。
## インストール
[公式ドキュメント](https://storybook.js.org/docs/vue/get-started/install/)を見ながら以下コマンドでインストールします。
```shell-session
$ npx storybook@latest init --type vue3
// .storybookディレクトリとstoriesディレクトリが作成されます。
```
Storybookを起動して、インストールできているか確認します。
```shell-session
$ yarn dev
```
http://localhost:6006 にアクセスしてみるとintroductionページが開きました。
## Storybookの設定ファイルを書く
今回`components`は以下のようなディレクトリ構成にします。
```
components/
  ├ <コンポーネント名>/
  |  ├ index.vue
  |  └ index.stories.ts
```
そのため`.storybook/main.js`のstoriesを以下のようにします。
ここは自分の構成に合わせて適切に設定してください。
```diff_javascript:.storybook/main.js
/** @type { import('@storybook/vue3-vite').StorybookConfig } */
const config = {
+ stories: [
+   "../*.stories.@(js|jsx|ts|tsx)",
+   "../components/**/*.stories.@(js|jsx|ts|tsx)",
+   "../layouts/**/*.stories.@(js|jsx|ts|tsx)",
+   "../pages/**/*.stories.@(js|jsx|ts|tsx)",
+ ],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
  ],
  framework: {
    name: "@storybook/vue3-vite",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
};
export default config;
```
preview.jsでTailwind CSSを定義したCSSファイルを読み込みます。
```diff_javascript:preview.js
/** @type { import('@storybook/vue3').Preview } */
+ import "../assets/css/main.css";
const preview = {
  parameters: {
    actions: { argTypesRegex: "^on[A-Z].*" },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
};

export default preview;
```
また自動で作成されたstoriesディレクトリは使わないので削除しましょう。

## Storybookでも自動インポートに対応する
以下を参考にしました。

https://github.com/storybookjs/builder-vite/issues/146

### VueAPI等の自動インポート
Nuxt3では（Nuxt2もですが）[自動インポート機能](https://nuxt.com/docs/guide/concepts/auto-imports)があります。
import文を書く手間がなくなるので開発がスピーディーに進むのですが、Storybook内では読み込んでくれないので`not found`になってしまいます。
これを解決するために`unplugin-auto-import`を使います。

https://www.npmjs.com/package/unplugin-auto-import

```shell-session
$ yarn add -D unplugin-auto-import
```
以下のように設定し、Storybookでも自動インポートされる関数等を読み込めるようにします。
```diff_javascript:.storybook/main.js
/** @type { import('@storybook/vue3-vite').StorybookConfig } */
+ const AutoImport = require("unplugin-auto-import/vite");
const config = {
  stories: [
    "../*.stories.@(js|jsx|ts|tsx)",
    "../components/**/*.stories.@(js|jsx|ts|tsx)",
    "../layouts/**/*.stories.@(js|jsx|ts|tsx)",
    "../pages/**/*.stories.@(js|jsx|ts|tsx)",
  ],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
  ],
  framework: {
    name: "@storybook/vue3-vite",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
+ viteFinal: async (config) => {
+    if (config.plugins !== undefined) {
+     config.plugins.push(
+       AutoImport({ imports: ["vue"], dts: ".storybook/auto-imports.d.ts" })
+     );
+   }
+   return {
+     ...config,
+     define: {
+       ...config.define,
+       global: "window",
+     },
+   };
+ },
};
export default config;
```
### コンポーネントの自動インポートに対応する
Nuxtにはコンポーネントの自動インポートもあり、こちらは`unplugin-auto-components`では解決できません。
コンポーネントの自動インポートに対応するためには`unplugin-vue-components`を使います。
なお、コンポーネントの自動インポートは無効にもできるので無効にする場合、こちらの設定は不要です。

https://www.npmjs.com/package/unplugin-vue-components

```shell-session
$ yarn add -D unplugin-vue-components
```
以下のように追記し、Storybookでも自動インポートされる関数等を読み込めるようにします。
```diff_javascript:.storybook/main.js
/** @type { import('@storybook/vue3-vite').StorybookConfig } */
const AutoImport = require("unplugin-auto-import/vite");
+ const AutoImportComponents = require("unplugin-vue-components/vite");
const config = {
  stories: [
    "../*.stories.@(js|jsx|ts|tsx)",
    "../components/**/*.stories.@(js|jsx|ts|tsx)",
    "../layouts/**/*.stories.@(js|jsx|ts|tsx)",
    "../pages/**/*.stories.@(js|jsx|ts|tsx)",
  ],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
  ],
  framework: {
    name: "@storybook/vue3-vite",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
  viteFinal: async (config) => {
    if (config.plugins !== undefined) {
      config.plugins.push(
        AutoImport({ imports: ["vue"], dts: ".storybook/auto-imports.d.ts" })
      );
+     config.plugins.push(
+       AutoImportComponents({
+         dirs: ["components"],
+         directoryAsNamespace: false, // trueの場合ディレクトリ名もコンポーネント名に含む
+         dts: ".storybook/components.d.ts",
+       })
+     );
+   }
    return {
      ...config,
      define: {
        ...config.define,
        global: "window",
      },
    };
  },
};
export default config;
```
`directoryAsNamespace`はディレクトリが層階構造になっているときにディレクトリ名をコンポーネント名に含むかどうかを決めます。nuxt.comfig.ts側のpathPrefixをfalseにする場合はdirectoryAsNamespaceもfalseにします。

https://nuxt.com/docs/guide/directory-structure/components#component-names

```js
// ディレクトリ名をコンポーネント名に含むとき
// components/Fizz/Bass/index.vueのコンポーネント名は<FizzBuzz />になる
// nuxt.comfig.ts
pathPrefix: true
// .storybook/main.js
directoryAsNamespac:true


// ディレクトリ名をコンポーネント名に含まないとき
// components/Fizz/Bass/index.vueのコンポーネント名は<Buzz />になる
// nuxt.comfig.ts
pathPrefix: false
// .storybook/main.js
directoryAsNamespac:false
```
自動インポートに対応した際に作成されるファイルはコミットの中にあると若干邪魔にもなるのでgitignoreに追記することをおすすめします。
```diff_shell:.gitignore
...
+ .storybook/auto-imports.d.ts
+ .storybook/components.d.ts
```
## ストーリーを作ってみる
ストーリーを作るためにまずコンポーネントを作ります。
今回はかんたんなボタンコンポーネントを作ります。
```vue:components/Button/index.vue
<script setup lang="ts">
type ButtonColor = "green" | "red";
interface Props {
  color?: ButtonColor;
}
const props = withDefaults(defineProps<Props>(), {
  color: undefined,
});
const { color } = toRefs(props);
</script>

<template>
  <button
    class="px-4 py-2 rounded-lg transition-colors"
    :class="{
      'text-white bg-green-500 hover:bg-green-700': color === 'green',
      'text-white bg-red-500 hover:bg-red-700': color === 'red',
      'text-black bg-gray-200 hover:bg-gray-400': !color,
    }"
  >
    <slot></slot>
  </button>
</template>

```
なお本記事ではvue3の大きな変更点であるcomposition apiの解説は省略します。
以下のドキュメント等を見てみてください。

https://ja.vuejs.org/guide/typescript/composition-api.html

コンポーネントができたのでstoriesも作ります。

```ts:components/Button/index.stories.ts
import Button from "./index.vue";

import type { Meta, StoryObj } from "@storybook/vue3";

const meta: Meta<typeof Button> = {
  title: "button",
  component: Button,
  render: (args) => ({
    components: { Button },
    tags: ["autodocs"],  // docsが不要だったら削除してOK
    setup() {
      return { args };
    },
    template: '<Button v-bind="args">ボタン</Button>',
  }),
  argTypes: {
    color: {
      control: {
        type: "inline-radio",
      },
      options: ["green", "red", undefined],
    },
  },
};
export default meta;
type Story = StoryObj<typeof Button>;

export const Component: Story = {
  args: {
    color: "green",
  },
};
```
最後に http://localhost:6006 にアクセスして確認します。
![スクリーン ショット 2023-04-19 に 01.58.22 午前.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669033/40f0d0ff-4eb4-732f-7211-2a8b8a01de5b.png)
うまく行きました。ControlsでPropsに設定した色の選択もできます。

# 最後に
今回はNuxt3 + Tailwind CSS + Storybook ver7の環境構築をしました。
Storybook ver7も2023/04/03に正式リリースされ、今後はスタンダートになると思います。
今回紹介した構成を参考にしていただければ幸いです。