Open23

【Nuxt3】DockerでNuxt+αの環境構築をしたい

airRnotairRnot

目的

Dockerで環境構築すればいろいろ使いまわせて楽そう

入れたいもの

  • Nuxt3
  • TailwindCSS
  • ESLint + Prettier
  • daisyui
  • Vitest
  • Storybook
airRnotairRnot

Docker

ディレクトリ構成

├── docker
│   └── nuxt
│       └── Dockerfile
├── docker-compose.yml
└── front
airRnotairRnot

Dockerfile

docker/nuxt/Dockerfile
FROM node:16-slim

ENV TZ Asia/Tokyo

WORKDIR /app

RUN apt-get update \
    && apt-get install -y \
    git \
    vim
airRnotairRnot

docker-compose.yml

docker-compose.yml
version: "3.9"

services:
  nuxt:
    container_name: nuxt
    build: ./docker/nuxt/
    volumes:
      - ./front:/app:cached
    ports:
      - "80:3000"
      - "24678:24678"
    tty: true
    environment:
      - HOST=0.0.0.0
      - port=80
      - CHOKIDAR_USEPOLLING=true
#    command: sh -c "yarn && yarn dev -o"

参考

airRnotairRnot

Docker起動

docker-compose up -d
docker-compose exec nuxt bash
airRnotairRnot

Nuxtのセットアップ

※以下からはコンテナ内で実行

npx nuxi init .
yarn install

@tsconfig/strictest のインストール

yarn add -D @tsconfig/strictest
airRnotairRnot

scrDirの設定とtsconfigの設定

nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  nitro: {
    preset: 'node',
  },
  devServer: {
    host: '0.0.0.0',
  },
  srcDir: 'src',
  typescript: {
    tsConfig: {
      extends: '@tsconfig/strictest/tsconfig.json',
      compilerOptions: {
        noImplicitReturns: false, // For middleware
      },
    },
  },
});

app.vuesrcに移す

./front
├── .nuxt
├── README.md
├── node_modules
├── .gitignore
├── nuxt.config.ts
├── package.json
├── src
│   └── app.vue
├── tsconfig.json
└── yarn.lock
airRnotairRnot

TailwindCSS

インストール

yarn add -D @nuxtjs/tailwindcss

nuxt.config.tsの編集

nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  nitro: {
    preset: 'node',
  },
  devServer: {
    host: '0.0.0.0',
  },
  srcDir: 'src',
  typescript: {
    tsConfig: {
      extends: '@tsconfig/strictest/tsconfig.json',
      compilerOptions: {
        noImplicitReturns: false, // For middleware
      },
    },
  },
  modules: ['@nuxtjs/tailwindcss'],
  tailwindcss: {
    exposeConfig: true,
    configPath: 'tailwind.config',
  },
});

tailwind.config.tsの作成

tailwind.config.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.html', './src/**/*.vue', './src/**/*.jsx'],
  theme: {
    extend: {
      colors: {
        'light-black': '#333333',
      },
    },
  },
};
./front
├── .nuxt
├── README.md
├── node_modules
├── .gitignore
├── nuxt.config.ts
├── package.json
├── src
│   └── app.vue
├── tailwind.config.cjs
├── tsconfig.json
└── yarn.lock
airRnotairRnot

動作確認

yarn run dev
Nuxi 3.0.0
Nuxt 3.0.0 with Nitro 1.0.0
       
  > Local:    http://localhost:3000/
  > Network:  http://172.22.0.2:3000/

ℹ Using default Tailwind CSS file from runtime/tailwind.css
ℹ Tailwind Viewer: http://0.0.0.0:3000/_tailwind/

 ERROR  [postcss] ENOENT: no such file or directory, open '/app/src/app.vue'

ℹ Vite client warmed up in 3795ms
✔ Nitro built in 866 ms

ここでエラー発生。/app/src/app.vueが開けない模様

airRnotairRnot

エラー内容

500
[vite-node] [plugin:vite:css] [@tailwind base; @tailwind components; @tailwind utilities; ] /@fs./node_modules/@nuxtjs/tailwindcss/dist/runtime/tailwind.css

@tailwind base;
@tailwind components;
@tailwind utilities;

at /@fs./node_modules/@nuxtjs/tailwindcss/dist/runtime/tailwind.css
airRnotairRnot

だめでした

そもそもtailwindcss自体読み込まれませんでした

airRnotairRnot

そもそもtailwind関係なしにsrc/app.vueが読み込めないっぽい

airRnotairRnot

んー、
どうやらsrc/app.vueだけが読み込めなくてapp/app.vueとかsrc/src/app.vueだと読み込めました(tailwind有りではまだ試してない)

とりあえずnuxt.config.tssrcDirにはappを指定しておくことにします

airRnotairRnot

無事tailwind動作しました……

@nuxtjs/tailwindcssの方で動作確認できました。やはりsrcDirsrcが指定されているときのみsrc/app.vueが読み込めないみたいです。どういうこと?

dockerのworkdirが/appなのが原因とかってありますかね

./front
├── .gitignore
├── .nuxt
├── README.md
├── app
│   └── app.vue
├── node_modules
├── nuxt.config.ts
├── package.json
├── tailwind.config.cjs
├── tsconfig.json
└── yarn.lock
airRnotairRnot

ESLint + Prettier

インストール

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

.eslintrc.cjsの作成

.eslintrc.cjs
module.exports = {
  env: {
    es2021: true,
    node: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:tailwindcss/recommended',
    '@nuxtjs/eslint-config-typescript',
    'prettier',
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    parser: '@typescript-eslint/parser',
    sourceType: 'module',
  },
  plugins: ['vue', '@typescript-eslint', 'tailwindcss'],
  rules: {
    /* typescript */
    'dot-notation': 'off',
    'no-restricted-imports': [
      'error',
      {
        patterns: [
          '../*',
          '~/*',
          '~~/*',
          './assets/*',
          './components/*',
          './pages/*',
          './plugins/*',
          './router/*',
          './composables/*',
          './server/*',
          './store/*',
          './types/*',
          './utils/*',
          './libs/*',
          './*.vue',
        ],
      },
    ],
    'import/order': [
      'error',
      {
        'groups': [
          'builtin',
          'external',
          'parent',
          'sibling',
          'index',
          'object',
          'type',
        ],
        'pathGroups': [
          {
            pattern: '{vue,vue-router,vite,@vitejs/plugin-vue}',
            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' },
    ],

    /* nuxt */
    'vue/multi-word-component-names': 'off',
    'vue/require-v-for-key': 'off',

    /* tailwindcss */
    'tailwindcss/no-custom-classname': [
      'warn',
      {
        config: './front/tailwind.config.cjs',
      },
    ],
    'tailwindcss/classnames-order': 'off',
  },
};

.prettierrcの作成

.prettierrc
{
  "singleQuote": true,
  "semi": true,
  "tabWidth": 2,
  "quoteProps": "consistent",
  "trailingComma": "es5",
  "vueIndentScriptAndStyle": true
}
airRnotairRnot

daisyui

インストール

yarn add daisyui

global.d.tsの作成

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

tailwind.config.cjsの編集

tailwind.config.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./app/**/*.html', './app/**/*.vue', './app/**/*.jsx'],
  theme: {
    extend: {
      colors: {
        'light-black': '#333333',
      },
    },
  },
  plugins: [require('daisyui')],
};
airRnotairRnot

Vitest

ここからは初挑戦

どうやら@nuxt/test-utilsもあるらしいがまだ情報が少ないので@vue/test-utilsを採用する

参考

インストール

yarn add -D vitest @vue/test-utils

vitest.config.tsの作成

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

import Vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [Vue()],
  test: {
    globals: true,
    environment: 'jsdom',
  },
});

package.jsontestを追加

package.json
{
  "private": true,
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
+   "test": "vitest"
  },
  "devDependencies": {
    "@nuxtjs/eslint-config-typescript": "^12.0.0",
    "@nuxtjs/tailwindcss": "^6.2.0",
    "@tsconfig/strictest": "^1.0.2",
    "@typescript-eslint/eslint-plugin": "^5.48.1",
    "@typescript-eslint/parser": "^5.48.1",
    "@vue/test-utils": "^2.2.7",
    "eslint": "^8.31.0",
    "eslint-config-prettier": "^8.6.0",
    "eslint-plugin-import": "^2.27.4",
    "eslint-plugin-tailwindcss": "^3.8.0",
    "eslint-plugin-vue": "^9.8.0",
    "jsdom": "^21.0.0",
    "nuxt": "3.0.0",
    "prettier": "^2.8.2",
    "prettier-plugin-tailwindcss": "^0.2.1",
    "typescript": "^4.9.4",
    "vitest": "^0.27.1"
  },
  "dependencies": {
    "daisyui": "^2.46.1"
  }
}

テスト実行

yarn run test

なんかインストールしろと言われたのでyesを入力する

yarn run v1.22.19
$ vitest
 MISSING DEP  Can not find dependency 'jsdom'

✔ Do you want to install jsdom? … yes
[1/4] Resolving packages...
info There appears to be trouble with your network connection. Retrying...
[2/4] Fetching packages...
warning vscode-languageclient@7.0.0: The engine "vscode" appears to be invalid.
[3/4] Linking dependencies...
warning " > daisyui@2.46.1" has unmet peer dependency "autoprefixer@^10.0.2".
warning " > daisyui@2.46.1" has unmet peer dependency "postcss@^8.1.6".
warning "daisyui > postcss-js@4.0.0" has unmet peer dependency "postcss@^8.3.3".
warning "daisyui > tailwindcss@3.2.4" has unmet peer dependency "postcss@^8.0.9".
warning "@nuxtjs/tailwindcss > @nuxt/postcss8 > css-loader@5.2.7" has unmet peer dependency "webpack@^4.27.0 || ^5.0.0".
warning "@nuxtjs/tailwindcss > @nuxt/postcss8 > postcss-loader@4.3.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
warning " > @vue/test-utils@2.2.7" has unmet peer dependency "vue@^3.0.1".
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 30 new dependencies.
info Direct dependencies
└─ jsdom@21.0.0
info All dependencies
├─ @tootallnate/once@2.0.0
├─ acorn-globals@7.0.1
├─ asynckit@0.4.0
├─ combined-stream@1.0.8
├─ cssom@0.5.0
├─ cssstyle@2.3.0
├─ data-urls@3.0.2
├─ decimal.js@10.4.3
├─ delayed-stream@1.0.0
├─ domexception@4.0.0
├─ entities@4.4.0
├─ escodegen@2.0.0
├─ esprima@4.0.1
├─ form-data@4.0.0
├─ html-encoding-sniffer@3.0.0
├─ http-proxy-agent@5.0.0
├─ iconv-lite@0.6.3
├─ is-potential-custom-element-name@1.0.1
├─ jsdom@21.0.0
├─ nwsapi@2.2.2
├─ parse5@7.1.2
├─ psl@1.9.0
├─ querystringify@2.2.0
├─ saxes@6.0.0
├─ symbol-tree@3.2.4
├─ tough-cookie@4.1.2
├─ tr46@3.0.0
├─ url-parse@1.5.10
├─ w3c-xmlserializer@4.0.0
└─ xmlchars@2.2.0
$ nuxt prepare
[log] Nuxi 3.0.0
[info] [nuxt:tailwindcss] Using default Tailwind CSS file from runtime/tailwind.css
[success] Types generated in .nuxt

Package jsdom installed, re-run the command to start.
error Command failed with exit code 43.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

とここでエラー発生。vitest.config.tsでエラーが出ていた。

型 '{ plugins: Plugin_2[]; test: { globals: boolean; environment: string; }; }' の引数を型 'UserConfigExport' のパラメーターに割り当てることはできません。
  オブジェクト リテラルは既知のプロパティのみ指定できます。'test' は型 'UserConfigExport' に存在しません。
airRnotairRnot

defineConfigは引数にUserConfigExport型を受け取るようだが、UserConfigExportにもともとtestはないらしくエラっている模様
/// <reference types="vitest" />を加えると直るらしいが、解決せず

airRnotairRnot

どうやら自分で型を定義することで解決できるっぽい

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

import Vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import type { UserConfig } from 'vite';

import type { InlineConfig } from 'vitest';

interface VitestConfigExport extends UserConfig {
  test: InlineConfig;
}

export default defineConfig({
  plugins: [Vue()],
  test: {
    global: true,
    environment: 'jsdom',
  },
} as VitestConfigExport);
airRnotairRnot

テスト実行

試しに適当なコンポーネントを作成しテストを行なってみる

app/components/base/atoms/Text.vue
<script setup lang="ts">
  interface Props {
    content: string;
  }

  const props = defineProps<Props>();

  const { content } = toRefs(props);
</script>

<template>
  <p class="font-bold">{{ content }}</p>
</template>
app/tests/components/base/atoms/Text.spec.ts
import { mount } from '@vue/test-utils';
import { describe, test, expect } from 'vitest';

// eslint-disable-next-line no-restricted-imports
import BaseAtomsText from '../../../../components/base/atoms/Text.vue';

describe('BaseAtomsText', () => {
  test('メッセージが表示される', () => {
    const wrapper = mount(BaseAtomsText, {
      props: {
        content: 'Hello World',
      },
    });
    expect(wrapper.text()).toBe('Hello World');
  });
});

ちなみにpathはaliasでやるとダメみたいです。

……と思いきや設定できるみたいです。

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

import Vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import type { UserConfig } from 'vite';

import type { InlineConfig } from 'vitest';

interface VitestConfigExport extends UserConfig {
  test: InlineConfig;
}

export default defineConfig({
  plugins: [Vue()],
  test: {
    global: true,
    environment: 'jsdom',
  },
  resolve: {
    alias: {
      '@': '/app',
    },
  },
} as VitestConfigExport);

それでは、いざテスト実行!

ReferenceErrorが発生しました。

どうやら、Vue独自の要素はimportされていないみたいです。

それを解決します。

unplugin-auto-importのインストール

yarn add -D unplugin-auto-import

vitest.config.tsの編集

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

import Vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import type { UserConfig } from 'vite';

import AutoImport from 'unplugin-auto-import/vite';

import type { InlineConfig } from 'vitest';

interface VitestConfigExport extends UserConfig {
  test: InlineConfig;
}

export default defineConfig({
  plugins: [Vue(), AutoImport({ imports: ['vue'] })],
  test: {
    global: true,
    environment: 'jsdom',
  },
  resolve: {
    alias: {
      '@': '/app',
    },
  },
} as VitestConfigExport);

これでテストを実行するとauto-imports.d.tsが生成される

しかし、これでもNuxtの独自要素はimportされない。@nuxt/test-utilsを頼る必要があるみたいだが、今回は見送る

そして、テストを実行すると

yarn run v1.22.19
$ vitest

 DEV  v0.27.1 /app

 ✓ app/tests/components/base/atoms/Text.spec.ts (1)

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  20:49:16
   Duration  9.83s (transform 2.49s, setup 2ms, collect 1.10s, tests 78ms)


 PASS  Waiting for file changes...
       press h to show help, press q to quit

無事通過!

airRnotairRnot

Storybook

インストール

npx sb init --type vue3 --builder @storybook/builder-vite

参考

eslintのplugin入れるか? と聞かれるので同意する

✔ Do you want to run the 'eslintPlugin' migration on your project? … yes

すると、.storybookstoriesが生成される。storiesは要らないので消してOK

.storybook/main.jsの編集

.storybook/main.js
module.exports = {
  stories: [
    '../app/components/**/*.stories.mdx',
    '../app/components/**/*.stories.@(js|jsx|ts|tsx)',
  ],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: '@storybook/vue3',
  core: {
    builder: '@storybook/builder-vite',
  },
  features: {
    storyStoreV7: true,
  },
};

Storybookの起動

いざ起動!

yarn storybook

デフォルトで6006ポートで起動するみたい。localhost:6006を開く……

開きません。

docker-compose.ymlの編集

docker-compose.yml
version: "3.9"

services:
  nuxt:
    container_name: nuxt
    build: ./docker/nuxt/
    volumes:
      - ./front:/app:cached
    ports:
      - "80:3000"
      - "24678:24678"
+     - "6006:6006"
    tty: true
    environment:
      - HOST=0.0.0.0
      - port=80
      - CHOKIDAR_USEPOLLING=true
    command: sh -c "yarn && yarn dev -o"

docker系を編集したら一度buildし直さないといけない

exit
docker-compose stop
docker-compose up -d --build

これで開くようになります。

.eslintrc.cjsの編集

.eslintrc.cjs
module.exports = {
  env: {
    es2021: true,
    node: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:tailwindcss/recommended',
    '@nuxtjs/eslint-config-typescript',
+   'plugin:storybook/recommended',
    'prettier',
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    parser: '@typescript-eslint/parser',
    sourceType: 'module',
  },
  plugins: ['vue', '@typescript-eslint', 'tailwindcss'],
  rules: {
    /* typescript */
    'dot-notation': 'off',
    'no-restricted-imports': [
      'error',
      {
        patterns: [
          '../*',
          '~/*',
          '~~/*',
          './assets/*',
          './components/*',
          './pages/*',
          './plugins/*',
          './router/*',
          './composables/*',
          './server/*',
          './store/*',
          './types/*',
          './utils/*',
          './libs/*',
          './*.vue',
        ],
      },
    ],
    'import/order': [
      'error',
      {
        'groups': [
          'builtin',
          'external',
          'parent',
          'sibling',
          'index',
          'object',
          'type',
        ],
        'pathGroups': [
          {
            pattern: '{vue,vue-router,vite,@vitejs/plugin-vue}',
            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' },
    ],

    /* nuxt */
    'vue/multi-word-component-names': 'off',
    'vue/require-v-for-key': 'off',

    /* tailwindcss */
    'tailwindcss/no-custom-classname': [
      'warn',
      {
        config: './front/tailwind.config.cjs',
      },
    ],
    'tailwindcss/classnames-order': 'off',
  },
};

先ほど同意したせいで勝手に崩されているので修正する

動作確認

storyの作成

app/components/base/atoms/Text.stories.ts
// eslint-disable-next-line no-restricted-imports
import Text from './Text.vue';

import type { Meta, StoryFn } from '@storybook/vue3';

export default {
  title: 'Base/Atoms/Text',
  component: Text,
  args: {
    content: 'Hello World',
  },
} as Meta<typeof Text>;

const Template: StoryFn<typeof Text> = (args, { argTypes }) => ({
  props: Object.keys(argTypes),
  components: { Text },
  setup() {
    return { args };
  },
  template: `
    <Text v-bind="args"/>
  `,
});

export const Default = Template.bind({});

Default.args = {
  content: 'Hello World',
};

ここでもaliasが効いていないので設定しようとしたが沼ったので保留

ここを参考にしたが解決せず

.storybook/main.jsの編集

またもやReference Errorが出たので直す。こっちは直った

.storybook/main.js
const AutoImport = require('unplugin-auto-import/vite');

module.exports = {
  stories: [
    '../app/components/**/*.stories.mdx',
    '../app/components/**/*.stories.@(js|jsx|ts|tsx)',
  ],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: '@storybook/vue3',
  core: {
    builder: '@storybook/builder-vite',
  },
  features: {
    storyStoreV7: true,
  },
  async viteFinal(config) {
    config.plugins.push(
      AutoImport({ imports: ['vue'], dts: '../auto-imports.d.ts' })
    );

    return config;
  },
};
airRnotairRnot

ここまでのディレクトリ構成

./front
├── .eslintrc.cjs
├── .gitignore
├── .prettierrc
├── .storybook
│   ├── main.js
│   ├── preview-head.html
│   └── preview.js
├── README.md
├── app
│   ├── app.vue
│   ├── components
│   │   └── base
│   │       └── atoms
│   │           ├── Text.stories.ts
│   │           └── Text.vue
│   ├── tests
│   │   └── components
│   │       └── base
│   │           └── atoms
│   │               └── Text.spec.ts
│   └── types
│       └── global.d.ts
├── auto-imports.d.ts
├── nuxt.config.ts
├── package.json
├── tailwind.config.cjs
├── tsconfig.json
├── vitest.config.ts
└── yarn.lock

node_modules.nuxtは省略