要約
eslint-plugin-vue の no-unused-components を クラススタイル Vue コンポーネント でも動くようにした。
今回の成果物
こちらに置いてあります
negibouze/customize-no-unused-components-rule
詳細
1. vue/no-unused-components を入れる
Vue CLI で ESLint を有効にしてプロジェクトを作成している場合は、ここは飛ばして良いです。
1-1. eslint 関連を入れる
yarn add -D @vue/cli-plugin-eslint @vue/eslint-config-typescript babel-eslint eslint eslint-plugin-vue
1-2. 設定ファイルを追加
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/essential'
],
rules: {},
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
1-3. 動作確認
no-unused-components の動作が確認できたので、カスタムルールを作成していきます。
2. カスタムルールの準備
2-1. EsLint ディレクトリの作成
カスタムルールを入れるディレクトリを作成します(今回はプロジェクト直下に作成)。
eslint
├── rules
└── tests
2-2. rule、test ファイルの追加
各ディレクトリ配下に必要なファイルを追加します。
eslint
├── rules
│ └── no-unused-components.js
└── tests
└── no-unused-components.js
2.3. 初期状態の作成
eslint-plugin-vue のルールをベースにさせてもらうので、まずはそのままコピーします。
元ルールの動きを壊していないことを確認するために、テストは2ファイル用意。
eslint
├── rules
│ └── no-unused-components.js
└── tests
├── no-unused-components-original.js <= eslint-plugin-vue のファイル
└── no-unused-components.js
2.4. ルールの修正
utils, casing は使わせてもらうのでパスを変更します。
// 変更前
const utils = require('../utils')
const casing = require('../utils/casing')
// 変更後
const utils = require('eslint-plugin-vue/lib/utils')
const casing = require('eslint-plugin-vue/lib/utils/casing')
2.5. 設定変更
自作ルールを使うように設定を変更します。
rules: {
'vue/no-unused-components': 'off',
'no-unused-components': 'error'
},
{
"scripts": {
"lint": "vue-cli-service lint --rulesdir eslint/rules"
}
}
2.6. 動作確認
yarn run lint
error: The "HelloWorld" component has been registered but not used (no-unused-components) at src/components/Example2.vue:10:5:
8 | export default {
9 | components: {
> 10 | HelloWorld
| ^
11 | }
12 | }
13 | </script>
カスタムルールで動くことが確認できたので、いよいよルールを変更していきます。
3. カスタムルールの追加
3-1. 調査
少し調べると registeredComponents と usedComponents あたりが見つかります。
もう少し調べると、
- クラススタイルの場合は登録しても registeredComponents に入っていない
- そもそも utils.executeOnVue が呼ばれない
あたりが分かりますので、そこを解決すれば良さそうです。
create (context) {
// 中略
return utils.defineTemplateBodyVisitor(context, {
"VElement[name='template']:exit" (rootNode) {
// 中略
registeredComponents
.filter(({ name }) => {
if (casing.pascalCase(name) === name || casing.camelCase(name) === name) {
return ![...usedComponents].some(n => {
return n.indexOf('_') === -1 && (name === casing.pascalCase(n) || casing.camelCase(n) === name)
})
} else {
return !usedComponents.has(name)
}
})
.forEach(({ node, name }) => context.report({
node,
message: 'The "{{name}}" component has been registered but not used.',
data: {
name
}
}))
}
}, utils.executeOnVue(context, (obj) => {
// クラススタイルだとここまで来ない。
registeredComponents = utils.getRegisteredComponents(obj)
}))
}
がんばって調べると、isVueComponentFile あたりに手を入れれば良さそうなことが分かります。
executeOnVue -> executeOnVueComponent -> isVueComponentFile
isVueComponentFile (node, path) {
return this.isVueFile(path) &&
node.type === 'ExportDefaultDeclaration' &&
node.declaration.type === 'ObjectExpression'
},
3-2. ルールの編集
調査の結果、isVueComponentFile を変更すれば良さそうなことが分かりましたが、
utils は使わせてもらいたいので、こんな感じに変更します。
const cloneDeep = require('lodash/cloneDeep')
const myUtils = cloneDeep(utils)
const originalIsVueComponentFile = myUtils.isVueComponentFile
myUtils.isVueComponentFile = (node, path) => {
return (
originalIsVueComponentFile.call(myUtils, node, path) ||
node.declaration.type === 'ClassDeclaration'
)
}
// 以下、utils を使っているところを myUtils に変更
上記の変更で executeOnVue が呼ばれるようになりましたが、下記のエラーが発生しました。
TypeError: Cannot read property 'find' of undefined
ただ、ここは分岐を追加するだけで良さそうです。
// 変更
myUtils.executeOnVue(context, (obj) => {
if (obj.type === 'ClassDeclaration') {
registeredComponents = getRegisteredComponentsFromClassDeclaration(obj)
} else {
registeredComponents = myUtils.getRegisteredComponents(obj)
}
}
// 追加
const getRegisteredComponentsFromClassDeclaration = obj => {
const decorators = obj.decorators
if (!decorators) return []
const expression = decorators[0].expression
const hasArguments = expression && expression.arguments && expression.arguments.length >= 1
return hasArguments ? myUtils.getRegisteredComponents(expression.arguments[0]) : []
}
3-3. 動作確認
yarn run lint
error: The "HelloWorld" component has been registered but not used (no-unused-components) at src/components/Example.vue:11:5:
9 | @Component({
10 | components: {
> 11 | HelloWorld
| ^
12 | }
13 | })
14 | export default class Example extends Vue { }
error: The "HelloWorld" component has been registered but not used (no-unused-components) at src/components/Example3.vue:10:5:
8 | export default {
9 | components: {
> 10 | HelloWorld
| ^
11 | }
12 | }
13 | </script>
両方検知できました!
4. Visual Studio Code で動くようにする
4-1. eslint-plugin-rulesdir を入れる
yarn add -D eslint-plugin-rulesdir
4-2. 設定変更
// 追加
const rulesDirPlugin = require('eslint-plugin-rulesdir')
rulesDirPlugin.RULES_DIR = 'eslint/rules'
module.exports = {
// 追加
plugins: [
'rulesdir'
],
rules: {
'vue/no-unused-components': 'off',
'rulesdir/no-unused-components': 'error' // <= 変更
},
}
4-3. 動作確認
検知できました!
!動かない場合は VSCode を再起動すると直るかも
5. 積み残し
1.この書き方だと AST の構造が変わるため動きません。
<template>
<div />
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import HelloWorld from './HelloWorld.vue'
@Component({
components: {
HelloWorld
}
})
class Example extends Vue { }
export default Example
</script>
2.現在の eslint-plugin-vue の master ブランチ だと動きません
vue-eslint-parser
のバージョンアップにより AST の構造が変わっているため動かなくなります。
"dependencies": {
"vue-eslint-parser": "^6.0.4"
}
rule を下記のように変更すると動くようになります。
// 修正前
"VAttribute[directive=true][key.name='bind'][key.argument='is']" (node) {
}
// 修正後
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']" (node) {
}