NuxtJSがVuetifyのTree Shakingまで面倒を見てくれる

# Tree Shaking とは？

[Webpack](https://webpack.js.org/guides/tree-shaking/) の仕組みで、パッケージの中で必要なソースコードのみをビルドパッケージに含めて、使わなかったソースコードは除外してくれます。

Vuetify のドキュメントでは、 [A-la-carte](https://vuetifyjs.com/ja/customization/a-la-carte/) として説明されてます。

## `vuetify-loader`

Webpack 向けに `vuetify-loader` が用意されています。
公式ドキュメントによると、

```javascript:webpack.config.js
const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin')

module.exports = {
  plugins: [
    new VuetifyLoaderPlugin()
  ],
}
```

これで、 `vuetify-loader` が Tree Shaking してくれるようになります。

##  `@nuxtjs/vuetify`

NuxtJS が、 [@nuxtjs/vuetify](https://www.npmjs.com/package/@nuxtjs/vuetify) というビルドモジュールを用意してくれています。

`@nuxtjs/vuetify` には、 `vuetify-loader` が組み込まれていて、自動的に呼び出して Tree Shaking するようになっています。

```javascript:nuxt.config.js
{
  buildModules: [
    '@nuxtjs/vuetify'
  ],
  vuetify: {
    /* module options */
  }
}
```

`nuxt.config.js` に `vuetify` という項目が登場して、 `new Vuetify({ ... })` で渡すオプションを指定することができます。

# 最終形の `nuxt.config.js`

Vuetify の Tree Shaking を使って、 `@/components` フォルダ以下のカスタムコンポーネントを自動的に登録させるコードまで含めた最終形がこちらです。

前記事：[Nuxt.jsでcomponentsフォルダのVueコンポーネントを自動でimportする](https://qiita.com/kulikala/items/2f372d733f8525695733)
では、 `glob` を使ってファイルを探していましたが、遅いので、キャッシュするように変更しています。

```javascript:nuxt.config.js
import fs from 'fs'
import path from 'path'
import colors from 'vuetify/es5/util/colors'

const SRC_DIR = 'src'
const COMPONENTS_DIR = 'components'

const srcPath = path.resolve(__dirname, SRC_DIR)
const componentDirs = fs.readdirSync(path.join(srcPath, COMPONENTS_DIR))
const foundComponents = {}

export default {
  srcDir: SRC_DIR,

  ...

  /*
  ** Nuxt.js dev-modules
  */
  buildModules: [
    '@nuxtjs/vuetify'
  ],

  ...

  /*
  ** vuetify module configuration
  ** https://github.com/nuxt-community/vuetify-module
  */
  vuetify: {
    customVariables: [
      '~/assets/variables.sass'
    ],
    theme: {
      options: {
        customProperties: true
      },
      dark: true,
      themes: {
        dark: {
          ...
        }
      }
    },
    treeShake: {
      directives: [
        'Resize'
      ],
      loaderOptions: {
        /**
         * This function will be called for every tag used in each vue component
         * It should return an array, the first element will be inserted into the
         * components array, the second should be a corresponding import
         *
         * originalTag - the tag as it was originally used in the template
         * kebabTag    - the tag normalised to kebab-case
         * camelTag    - the tag normalised to PascalCase
         * path        - a relative path to the current .vue file
         * component   - a parsed representation of the current component
         */
        match (originalTag, { kebabTag, camelTag }) {
          if (kebabTag in foundComponents) {
            return foundComponents[kebabTag]
          }

          const parts = kebabTag.split('-')

          if (parts.length > 1 && componentDirs.includes(parts[0])) {
            for (let i = 1; i < parts.length; i++) {
              const pathPart = parts.slice(0, i)
              const filePart = parts.slice(i)

              const relPath = path.join(COMPONENTS_DIR, ...pathPart, camelTag.substr(-filePart.join('').length))

              const existsJs = fs.existsSync(path.join(srcPath, `${relPath}.js`))
              const existsVue = fs.existsSync(path.join(srcPath, `${relPath}.vue`))

              if (existsJs || existsVue) {
                const ret = [
                  camelTag,
                  `import ${camelTag} from '@/${relPath}'`
                ]

                foundComponents[kebabTag] = ret

                return ret
              }
            }
          }
        }
      }
    }
  },

  ...

}
```