Ikkyu's Tech Blog

技術系に関するブログです。

Nuxt.js × Vuetify × TypeScript × ESLint × Prettier × Storybook × Jestを使用して開発環境を構築

[追記]

この記事の情報が古くなったので、新しく記事を書き直してみました。(2020/4/3)

ikkyu.hateblo.jp


タイトルがごちゃごちゃしてますね...

最近Nuxt.jsを触り始めまして、アプリを作ることにしました。

なんとなく、「今時なものを使用して開発したい!」と思ってこの構成にしました。

いざ開発をしてみると、導入などで色々とハマりまして...😇

やったことを忘れないために、環境構築の内容を紹介しようと思います。

正しい設定ができていない場合もあるかもしれませんので、そこはご了承ください😅

対象となる人

  • この構成で開発しようとしている人

ぐらいかなと思います。

なお、今回はyarnではなくnpmを使用して説明していくものとします。

動作確認環境

  • Mac10.14.6
  • VSCode
    • Veturをインストール済み

それではやっていきます💪

プロジェクトを作成

create-nuxt-appコマンドを使用してプロジェクトを作成

コマンドは以下の通り

npx create-nuxt-app <project-name>

以下のコマンドを実行します(プロジェクト名は任意のものに変更しても構いません)

npx create-nuxt-app nuxt-typescript-starter

そうするといくつか質問されますので、
? Choose features to installLinter / FormatterPrettier
? Use a custom UI frameworkvuetify
? Use a custom test frameworkjest

をそれぞれ選択します。他の質問は任意に回答してOKです。

今回はこのように回答しました。

? Project name nuxt-typescript-starter
? Project description My stellar Nuxt.js project
? Use a custom server framework none
? Choose features to install Progressive Web App (PWA) Support, Linter / Formatter, Prettier, Axios
? Use a custom UI framework vuetify
? Use a custom test framework jest
? Choose rendering mode Single Page App
? Author name ikkyu-3
? Choose a package manager npm

質問に回答し終わるとプロジェクトが作成されます🔨

作成されたプロジェクトは、以下のようなディレクトリ構成になっています。

.
├── .babelrc
├── .editorconfig
├── .eslintrc.js
├── .git
├── .gitignore
├── .prettierrc
├── README.md
├── assets/
├── components/
├── jest.config.js
├── layouts/
├── middleware/
├── node_modules/
├── nuxt.config.js
├── package-lock.json
├── package.json
├── pages/
├── plugins/
├── static/
├── store/
└── test/

プロジェクトができましたので、npm run devと入力し実行します。

サーバが起動し、http://localhost:3000/にアクセスしてページが表示されていることを確認します。

表示されたページ
Welcome to the Vuetify + Nuxt.js

非常に簡単ですね🎉

テストを実行

では次にテストを実行してみます。Ctrl + Cでサーバを停止させたら、テストを実行してみます。

$ npm test

> jest

 PASS  test/Logo.spec.js
  Logo
    ✓ is a Vue instance (8ms)

-----------|----------|----------|----------|----------|-------------------|
File       |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files  |        0 |      100 |      100 |        0 |                   |
 index.vue |        0 |      100 |      100 |        0 |             61,62 |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.562s
Ran all test suites.

Jestを使ってテストが実行できました🎉

Lintを実行

次にnpm run lintを実行してみます。

$ npm run lint
> eslint --ext .js,.vue --ignore-path .gitignore .

問題なくなく実行できています。eslintの設定は、プロジェクト作成時にPrettierの連携設定もされています。

とうことで、Nuxt.jsとVuetifyとESLintとPrettierとJestが使用できることを確認できました👏

デフォルトでここまでやってくれるのは、本当に助かります👌

では、ここで一旦使用できるようになったものをまとめます。

  • [x] Nuxt.js
  • [x] Vuetify
  • [x] ESLint
  • [x] Prettier
  • [ ] TypeScript
  • [ ] Storybook
  • [x] Jest

次はコミット時にESLintを実行させたいので、その設定を行います。

コミット時にESLintを実行

必要なパッケージをインストール

Huskylint-stagedをインストールします。

npm i -D husky lint-staged

packege.jsonに設定を追加

package.jsonにHuxkyとlint-stagedの設定を追加します。

{
  ...
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{js,vue}": [
      "npm run lint --fix",
      "git add"
    ]
  }
}

これでコミット時にjs, vueのファイルの場合 npm run lint --fixが強制的に実行され、ソースコードを綺麗に保つことが、できるようになりました。

次はTypeScriptを導入します。

TypeScriptを導入

Nuxt.jsはTypeScriptをサポートしてますので、公式ドキュメントTypeScript サポート - Nuxt.jsを見ながら導入していきます。

必要なパッケージをインストール

npm i -D @nuxt/typescript
npm i ts-node

tsconfig.jsonを作成

touch tsconfig.json

tsconfig.jsonの中身はnuxtコマンド実行時にデフォルト値が設定されるので、空ファイルのままで大丈夫です👌

nuxt.config.js => nuxt.config.tsにファイル名を変更

nuxt.config.jsをnuxt.config.tsにリネームし、ファイルを変更します。

import NuxtConfiguration from '@nuxt/config'
import colors from 'vuetify/es5/util/colors'

const config: NuxtConfiguration = {
  ... // 中身は変更せずそのままでOK
}

export default config;

tsconfig.jsonにデフォルト値を設定

先ほど作成したtsconfig.jsonにデフォルト値を設定させたいので、nuxtコマンドを実行します。

npm run dev

tsconfig.jsonにデフォルト値が設定されますので、Ctrl + C で起動したサーバを停止させます。

TypeScriptで.vueファイルを読み込めるように設定

typesディレクトリを作成し、shims-vue.d.tsを作成します。

declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}

次にtypesディレクトリの型ファイルを読み込めるようにするため、tsconfig.jsonを変更します。
すでにtypesで指定されていますが、新しいパッケージの型を入れたらその都度typesに追加する必要があるため、削除します。
変わりにtypeRootsを使用します。1

{
  "compilerOptions": {
    ...,
    "typeRoots": [
      "./node_modules/@types",
      "./node_modules/@nuxt/vue-app/types",
      "./types"
    ]
  }
}

eslintの設定を変更

TypeScriptファイルをLintできるように、パッケージをインストールします。

npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser

インストール後、.eslintrc.jsを変更します。

module.exports = {
  root: true,
  env: {
    browser: true,
    node: true
  },
  parserOptions: {
    parser: '@typescript-eslint/parser'
  },
  extends: [
    '@nuxtjs',
    'plugin:nuxt/recommended',
    'plugin:prettier/recommended',
    'prettier',
    'prettier/vue',
    'prettier/@typescript-eslint'
  ],
  plugins: [
    'prettier',
    '@typescript-eslint'
  ],
  // add your custom rules here
  rules: {
    'no-unused-vars': 'off'
  }
}

vue-property-decoratorの使用

vue-property-decoratorを使用して、Vueコンポーネントをclass構文でかけるようにします。

npm i vue-property-decorator

vueファイルで、TypeScriptを使用できるように変更していきます。

例として、pages/index.vueを変更してます。

Vueファイル内でTypeScriptを使用するには、Scriptタグを<script lang="ts">と書きます。

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import Logo from '../components/Logo.vue'
import VuetifyLogo from '../components/VuetifyLogo.vue'

@Component({
  components: {
    Logo,
    VuetifyLogo
  }
})
export default class Index extends Vue {}
</script>

変更後、以下のようなエラーが発生する場合があります。

File '/Users/[ユーザ名]/nuxt-typesciprt-starter/components/Logo.vue' is not a module.Vetur(2306)

読み込むファイルに<script>が定義されていなかったので、Logo.vueVuetifyLogo.vue<script lang="ts">を追加します。そうするとエラーが消えるようです🤔

// Logo.vue
...
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

@Component
export default class Logo extends Vue {}
</script>
...
// VuetifyLogo.vue
...
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

@Component
export default class VuetifyLogo extends Vue {}
</script>
...

package.jsonを変更

scriptslintlint-stagedを変更します。

{
  "scripts": {
    "lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore .", // tsファイルを対象に追加します
    ...
  },
  ...
  "lint-staged": {
    "*.{js,ts,vue}": [  // tsファイルを対象に追加します
      "npm run lint --fix",
      "git add"
    ]
  }
}

jestの設定を変更

ts-jestを使用して、jestでTypeScriptが使用できるようにします。

npm i -D ts-jest @types/jest

jest.config.jsを変更します。

module.exports = {
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
    '^~/(.*)$': '<rootDir>/$1',
    '^vue$': 'vue/dist/vue.common.js'
  },
  testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
  moduleFileExtensions: ['js', 'vue', 'ts', 'json'],
  transform: {
    '^.+\\.js$': 'babel-jest',
    '.*\\.(vue)$': 'vue-jest',
    '^.+\\.tsx?$': 'ts-jest'
  },
  collectCoverage: true,
  collectCoverageFrom: [
    '<rootDir>/components/**/*.vue',
    '<rootDir>/pages/**/*.vue'
  ]
}

test/Logo.spec.jsをtsファイルに変更して、テストを実行します。

$ npm test

> jest

 PASS  test/Logo.spec.ts
  Logo
    ✓ is a Vue instance (9ms)

------------------|----------|----------|----------|----------|-------------------|
File              |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
------------------|----------|----------|----------|----------|-------------------|
All files         |    22.22 |      100 |      100 |       25 |                   |
 components       |       50 |      100 |      100 |       50 |                   |
  Logo.vue        |      100 |      100 |      100 |      100 |                   |
  VuetifyLogo.vue |        0 |      100 |      100 |        0 |             10,13 |
 pages            |        0 |      100 |      100 |        0 |                   |
  index.vue       |        0 |      100 |      100 |        0 |       61,62,63,71 |
------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.732s
Ran all test suites.

できました🎉これでjestでTypeScriptが使えます。

Vuetifyのコンポーネントを使用する時のjestの設定

jestでTypeScriptを使用できるようになりましたが、vuetifyのコンポーネントを使用すると警告が発生します。

例えば<v-btn>を使用したcomponents/Button.vueというファイルを作成します。

<template>
  <v-btn>Button</v-btn>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

@Component
export default class Button extends Vue {}
</script>

次にテストファイルtest/Button.spec.tsを作成します。

import { mount } from '@vue/test-utils'
import Button from '@/components/Button.vue'

describe('Button.vue', () => {
  it('snapshot', () => {
    const wrapper = mount(Button)
    expect(wrapper.html()).toMatchSnapshot()
  })
})

テストを実行します。

$ npm run test test/Button.spec.ts 

> nuxt-typescript-starter@1.0.0 test /Users/toru/workspace/nuxt-typesciprt-starter
> jest "test/Button.spec.ts"

 PASS  test/Button.spec.ts
  Button.vue
    ✓ snapshot (11ms)

 › 1 snapshot written.
  console.error node_modules/vue/dist/vue.common.dev.js:630
    [Vue warn]: Unknown custom element: <v-btn> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
    
    found in
    
    ---> <Button>
           <Root>

------------------|----------|----------|----------|----------|-------------------|
File              |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
------------------|----------|----------|----------|----------|-------------------|
All files         |    18.18 |      100 |      100 |       20 |                   |
 components       |    33.33 |      100 |      100 |    33.33 |                   |
  Button.vue      |      100 |      100 |      100 |      100 |                   |
  Logo.vue        |        0 |      100 |      100 |        0 |             11,14 |
  VuetifyLogo.vue |        0 |      100 |      100 |        0 |             10,13 |
 pages            |        0 |      100 |      100 |        0 |                   |
  index.vue       |        0 |      100 |      100 |        0 |       61,62,63,71 |
------------------|----------|----------|----------|----------|-------------------|
Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 written, 1 total
Time:        1.333s
Ran all test suites matching /test\/Button.spec.ts/i.

スナップショットのみなのでテストは通ってますが、どうやらVuetifyのコンポーネントを読み込めてないっぽいです。

Vuetify公式ドキュメントのUnit testing — Vuetify.jsにjestでテストを行う方法が載っていましたが、試してみたら動きませんでした。。。😇

[Feature Request] Avoid importing globally Vuetify in all tests · Issue #4964 · vuetifyjs/vuetifyを参考にしたら動きましたので、その設定を行います。

testディレクトリsetup.jsを作成します。

import Vue from 'vue'
import Vuetify from 'vuetify'

Vue.use(Vuetify)

次に、jest.config.jsに設定を追加します。

module.exports = {
  setupFilesAfterEnv: ['./test/setup.js'], // 追加
  ...
}

再度テストを実行します。

$ npm run test test/Button.spec.ts -- -u

> jest "test/Button.spec.ts" "-u"

 PASS  test/Button.spec.ts
  Button.vue
    ✓ snapshot (31ms)

 › 1 snapshot updated.
------------------|----------|----------|----------|----------|-------------------|
File              |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
------------------|----------|----------|----------|----------|-------------------|
All files         |    18.18 |      100 |      100 |       20 |                   |
 components       |    33.33 |      100 |      100 |    33.33 |                   |
  Button.vue      |      100 |      100 |      100 |      100 |                   |
  Logo.vue        |        0 |      100 |      100 |        0 |             11,14 |
  VuetifyLogo.vue |        0 |      100 |      100 |        0 |             10,13 |
 pages            |        0 |      100 |      100 |        0 |                   |
  index.vue       |        0 |      100 |      100 |        0 |       61,62,63,71 |
------------------|----------|----------|----------|----------|-------------------|
Snapshot Summary
 › 1 snapshot updated from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 updated, 1 total
Time:        1.488s
Ran all test suites matching /test\/Button.spec.ts/i.

警告がでなくなりました🎉

※ Vuetifyのコンポーネントを読み込めているので、snapshotを更新する必要があります。

これでVuetifyを使用した、単一コンポーネントファイルのテストも行えるようになりました🎉

ここまでで、使用できるようになったもは、

  • [x] Nuxt.js
  • [x] Vuetify
  • [x] ESLint
  • [x] Prettier
  • [x] TypeScript
  • [ ] Storybook
  • [x] Jest

残るはStorybookを導入です。あとちょい😊

Storybookを導入

StorybookでVueを使用する方法は公式のドキュメントにあります。これを見ながら導入していきます。

自動セットアップと手動セットアップがありますが、自動の方が楽なので今回はこちらを使用します。

npx -p @storybook/cli sb init --type vue

実行すると必要なパッケージがインストールされ、.storybookstoriesというディレクトリが作成されて、その中にいくつかのファイルが作成されます。

また、package.jsonstorybook, build-storybookというscriptsが新たに追加されています。

とりあえずnpm run storybookを実行してみます。

サーバが起動したらhttp://localhost:6006/にアクセスします。

Storybook
Storybookのキャプチャ画面

表示できました。Ctrl + Cで起動しているサーバを終了させます。

では次に、JavaScriptで書かれたStorybookをTypeScriptに置き換えてみます。

TypeScriptでStorybookをかけるようにする

必要なパッケージをインストール

ts-loader, fork-ts-checker-webpack-pluginをインストールします。

npm i -D ts-loader fork-ts-checker-webpack-plugin

.storybookディレクトリ内にwebpack.config.jsを作成

.storybookwebpack.config.jsを追加し、TypeScriptでかかれたStorybookを読み込めるようにします。

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')

module.exports = ({ config }) => {
  config.resolve.extensions.push('.ts')

  config.module.rules.push({
    test: /\.ts$/,
    exclude: /node_modules/,
    use: [
      {
        loader: 'ts-loader',
        options: {
          appendTsSuffixTo: [/\.vue$/],
          transpileOnly: true
        }
      }
    ]
  })

  config.plugins.push(new ForkTsCheckerWebpackPlugin())

  return config
}

StorybookでVuetifyを使用できるようにする

StorybookでVuetifyを使用するため、.storybook/config.jsを以下のように変更します。

import { configure, addDecorator } from '@storybook/vue'
import 'vuetify/dist/vuetify.css'

import Vue from 'vue'
import Vuetify from 'vuetify'

Vue.use(Vuetify)

addDecorator(() => ({
  template: '<v-app><story/></v-app>'
}))

// automatically import all files ending in *.stories.js
const req = require.context('../', true, /\.stories\.ts$/)
function loadStories() {
  req.keys().forEach((filename) => req(filename))
}

configure(loadStories, module)

ここまでできたら、StorybookをTypeScriptで書いて表示してみます。

TypeScriptを使用してStorybookを書いてみる

components配下のLogo.vueVuetifyLogo.vueをStorybookで表示させてみます。
storiesディレクトリindex.stories.jsindex.stories.tsにリネームし、以下のように書き換えます。

※ storiesディレクトリ内のMyButton.js, Welcome.jsは使用しないので削除します。

import { storiesOf } from '@storybook/vue'
import Logo from '../components/Logo.vue'
import VuetifyLogo from '../components/VuetifyLogo.vue'

storiesOf('Logo', module).add('to Storybook', () => ({
  components: { Logo },
  template: '<logo />'
}))

storiesOf('VuetifyLogo', module).add('to Storybook', () => ({
  components: { VuetifyLogo },
  template: '<vuetify-logo />'
}))

npm run storybook を実行してStorybookを表示してみます。

storybook_2
Storybookその2

表示できました!

Storybookを使用すれば、コンポーネントの開発が楽になります👏

ということで

  • [x] Nuxt.js
  • [x] Vuetify
  • [x] ESLint
  • [x] Prettier
  • [x] TypeScript
  • [x] Storybook
  • [x] Jest

すべて利用できるようになりました!!👏

以上です。

長くなりました。。。

これで快適に開発ができるかなと思っています。間違っていたら、都度追記や修正していきます。

今回作成したプロジェクトは、GitHubにあげてあります。参考にどうぞ。

リンク以外の参考サイト:

TypeScriptでVue.jsを書く – Vue CLIを使った開発のポイントを紹介 | maesblog