Open9

Vue+shadcn-vue+Storybookのテンプレートを作ってみる検証スクラップ

e_fulle_full

目的

以下でNuxt+shadcn-vue+Storybookの環境を構築していたけどVueに切り替えたいため別リポでテンプレートを作成する。
https://zenn.dev/e_full/scraps/fab3c4ce818c84

目標

  • 以下の技術スタックが同梱されている
  • ✅上記のスタックでStorybookを公開
  • ✅お試しで一つコンポーネントとstoryを公開(リンク)
  • テストファイルの作成
  • huskyでコンポーネントのテンプレートを作成
  • ✅shadcn-vueでbuttonコンポーネントを追加
  • ✅shadcn-vueで追加したコンポーネントをラップするコンポーネントの作成
  • ✅上記コンポーネントのStory化
  • (more)FigmaのvariablesをExport Filtered Variablesでjson化してコピペし、Style-Dictionaryでcss変数化する
  • (more)storycapでVRTの実装
  • (more)Laravelへの移植
  • (more)Gitに絵文字プレフィクスをつける

フォルダ構成

フォルダ構成はvueのデフォルト(create-vue)+Nuxtを参考にしたい気持ち
https://nuxt.com/docs/guide/directory-structure/app

リポジトリ

https://github.com/dip-erina-furusawa/vue-template

e_fulle_full

Vueのインストール

https://vuejs.org/guide/quick-start#creating-a-vue-application
コマンド1つで質問に答えると以下のツールがインストールされる

  • Vue
  • TypeScript
  • Vitest
  • ESLint
  • prettier
npm create vue@latest
Need to install the following packages:
  create-vue@3.10.3
Ok to proceed? (y) 

Vue.js - The Progressive JavaScript Framework

✔ Project name: … vue-template
✔ Add TypeScript? … Yes
✔ Add JSX Support? … Yes
✔ Add Vue Router for Single Page Application development? … No
✔ Add Pinia for state management? … No
✔ Add Vitest for Unit Testing? … Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … Yes
✔ Add Prettier for code formatting? … Yes
✔ Add Vue DevTools 7 extension for debugging? (experimental) … Yes

Scaffolding project in /Users/erina-furusawa/Develop/study/vue-template...

Done. Now run:

  cd vue-template
  npm install
  npm run format
  npm run dev

デフォルトで作成されるコンポーネントファイルなどは削除する。

e_fulle_full

shadcn-vueを設定する前に初めのコンポーネントを作成する

src/components/common/BasicButton/BasicButton.vue
<script setup lang="ts">
const props = defineProps({
  label: {
    type: String,
    required: true
  }
})
</script>
<template>
  <Button class="bg-blue-500">
    {{ label }}
  </Button>
</template>
<style scoped></style>
src/components/common/BasicButton/BasicButton.spec.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';

import Button from './BasicButton.vue';

const defaultProps = {
  label: 'Hello world',
};

describe('render button', () => {
  it('is contain label', () => {
    const wrapper = mount(Button, { props: defaultProps });
    const buttonElement = wrapper.find('button');
    const buttonText = buttonElement.text();
    expect(buttonText).toBe(defaultProps.label);
  });
});
e_fulle_full

unplugin-auto-importとunplugin-vue-componentsのインストール

https://github.com/unplugin/unplugin-auto-import
https://github.com/unplugin/unplugin-vue-components

unplugin-auto-import

npm i -D unplugin-auto-import

vite.configの変更

vite.config.ts
import { fileURLToPath, URL } from 'node:url'
+import AutoImport from 'unplugin-auto-import/vite'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import VueDevTools from 'vite-plugin-vue-devtools'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    VueDevTools(),
+    AutoImport({
+      imports: ['vue']
+    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

ESLintのための設定を行う

.eslintrc.cjs
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-typescript',
    '@vue/eslint-config-prettier/skip-formatting',
+    './.eslintrc-auto-import.json'
  ],
  parserOptions: {
    ecmaVersion: 'latest'
  }
}

unplugin-vue-components

npm i unplugin-vue-components -D

vite.configの変更

vite.config.ts
+
...
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    VueDevTools(),
    AutoImport({...}),
+    Components({
+      dts: true,
+    })
e_fulle_full

auto importは効くけど、コンポーネントの型参照が効かない・・・

e_fulle_full

shadcn-vueのインストール

https://www.shadcn-vue.com/docs/installation/vite.html

npm install -D tailwindcss autoprefixer

vite.configの修正

vite.config.ts
...
+import tailwind from "tailwindcss"
+import autoprefixer from "autoprefixer"
...
export default defineConfig({
+  css: {
+    postcss: {
+      plugins: [tailwind(), autoprefixer()],
+    },
+  },
...

tsconfigに依存関係解決の記載を追記

tsconfig.json
{
  "compilerOptions": {
    // ...
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
    // ...
  }
}

viteの依存関係解決

npm i -D @types/node
vite.config.ts
...
export default defineConfig({
...
+  resolve: {
+    alias: {
+      "@": path.resolve(__dirname, "./src"),
+    },
+  },
...

shadcnのinit

$ npx shadcn-vue@latest init
✔ Would you like to use TypeScript (recommended)? … yes
✔ Which framework are you using? › Vite
✔ Which style would you like to use? › Default
✔ Which color would you like to use as base color? › Slate
✔ Where is your global CSS file? … src/assets/index.css
✔ Would you like to use CSS variables for colors? … yes
✔ Where is your tailwind.config located? … tailwind.config.js
✔ Configure the import alias for components: … @/components
✔ Configure the import alias for utils: … @/lib/utils
✔ Write configuration to components.json. Proceed? … yes
[0:29:44] 
✔ Writing components.json...
✔ Initializing project...
✔ Installing dependencies...
[0:29:50] 
[0:29:50] ℹ Success! Project initialization completed.
[0:29:50] 

main.tsにcssのimport

main.ts
import { createApp } from 'vue'
import App from './App.vue'
+import './src/assets/index.css'

createApp(App).mount('#app')

試しにbuttonの追加

npx shadcn-vue@latest add button

そのままだとvue/multi-word-component-namesの規約に引っかかるのでインストールしたボタンのファイル名を変更
src/components/ui/button/Button.vuesrc/components/ui/button/ShadButton.vue

前に作っていたButtonの中をShadButtonに差し替えて表示確認

src/components/common/BasicButton/BasicButton.vue
<script setup lang="ts">
const props = defineProps({
  label: {
    type: String,
    required: true
  }
})
</script>
<template>
+  <ShadButton>
    {{ label }}
+  </ShadButton>
</template>
<style scoped></style>

e_fulle_full

Storybookのインストール

https://storybook.js.org/docs/get-started/vue3-vite

npx storybook@latest init

そのままの設定だと依存関係が解決できないので、main.tsを書き換えてStorybookのviteの設定を上書きする。

.storybook/main.ts
import type { StorybookConfig } from '@storybook/vue3-vite'
import { fileURLToPath, URL } from 'node:url'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
const config: StorybookConfig = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@chromatic-com/storybook',
    '@storybook/addon-interactions'
  ],
  framework: {
    name: '@storybook/vue3-vite',
    options: {}
  },
  docs: {
    autodocs: 'tag'
  },
+  async viteFinal(config, options) {
+    config.base = '/'
+    config.assetsInclude = ['/sb-preview/runtime.js']
+    if (config.resolve) {
+      config.resolve.alias = {
+        ...config.resolve.alias,
+        '@': fileURLToPath(new URL('../', import.meta.url))
+      }
+    } else {
+      config.resolve = {
+        alias: {
+          '@': fileURLToPath(new URL('../', import.meta.url))
+        }
+      }
+    }
+
+    if (config.plugins) {
+      config.plugins.push(
+        Components({
+          dts: true,
+          dirs: ['../src/components/common', '../src/components/ui']
+        }),
+        AutoImport({
+          imports: ['vue']
+        })
+      )
+    } else {
+      config.plugins = [
+        Components({
+          dts: true,
+          dirs: ['../src/components/common', '../src/components/ui']
+        }),
+        AutoImport({
+          imports: ['vue']
+        })
+      ]
+    }
+    return config
+  }
}
export default config

また、shadcnで使っているtailwindが読み込めないのでcssの読み込みをpreview.tsに追記する。

.storybook/preview.ts
import type { Preview } from '@storybook/vue3'
+import '../src/assets/index.css'
const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i
      }
    }
  }
}

export default preview

さらにshadcnのコンポーネント内でradix-vueが呼び出せないのでimport文を修正する。

src/components/ui/button/ShadButton.vue
...
+import { Primitive, type PrimitiveProps } from '@/node_modules/radix-vue'
import { type ButtonVariants, buttonVariants } from '.'
+import { cn } from '@/src/lib/utils'
...

storybookファイルを作成して動作確認をする

src/components/common/BasicButton/BasicButton.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'
import BasicButton from './BasicButton.vue'


const meta = {
  title: 'Common/Button',
  component: BasicButton,
  
  tags: ['autodocs'],
  args: {
    label: 'Hello world'
  }
} satisfies Meta<typeof BasicButton>

export default meta
type Story = StoryObj<typeof meta>

export const Primary: Story = {}