diff --git a/README.md b/README.md
index ea0d0c223..1e5ed04da 100644
--- a/README.md
+++ b/README.md
@@ -190,6 +190,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
| | Rule ID | Description |
|:---|:--------|:------------|
| :wrench: | [vue/attribute-hyphenation](./docs/rules/attribute-hyphenation.md) | enforce attribute naming style on custom components in template |
+| :wrench: | [vue/component-name-in-template-casing](./docs/rules/component-name-in-template-casing.md) | enforce specific casing for the component naming style in template |
| :wrench: | [vue/html-closing-bracket-newline](./docs/rules/html-closing-bracket-newline.md) | require or disallow a line break before tag's closing brackets |
| :wrench: | [vue/html-closing-bracket-spacing](./docs/rules/html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets |
| :wrench: | [vue/html-end-tags](./docs/rules/html-end-tags.md) | enforce end tag style |
diff --git a/docs/rules/component-name-in-template-casing.md b/docs/rules/component-name-in-template-casing.md
new file mode 100644
index 000000000..68cfa7911
--- /dev/null
+++ b/docs/rules/component-name-in-template-casing.md
@@ -0,0 +1,74 @@
+# enforce specific casing for the component naming style in template (vue/component-name-in-template-casing)
+
+- :gear: This rule is included in `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.
+- :wrench: The `--fix` option on the [command line](http://eslint.org/docs/user-guide/command-line-interface#fix) can automatically fix some of the problems reported by this rule.
+
+Define a style for the component name in template casing for consistency purposes.
+
+## :book: Rule Details
+
+:+1: Examples of **correct** code for `PascalCase`:
+
+```html
+
+
+
+```
+
+:-1: Examples of **incorrect** code for `PascalCase`:
+
+```html
+
+
+
+
+
+```
+
+:+1: Examples of **correct** code for `kebab-case`:
+
+```html
+
+
+
+```
+
+:-1: Examples of **incorrect** code for `kebab-case`:
+
+```html
+
+
+
+
+
+
+```
+
+## :wrench: Options
+
+Default casing is set to `PascalCase`.
+
+```json
+ "vue/component-name-in-template-casing": ["error",
+ "PascalCase|kebab-case",
+ {
+ "ignores": []
+ }
+ ]
+```
+
+- `ignores` (`string[]`) ... The element name to ignore. Sets the element name to allow. For example, a custom element or a non-Vue component.
+
+
+:+1: Examples of **correct** code for `{ignores: ["custom-element"]}`:
+
+```html
+
+
+
+
+```
+
+## Related links
+
+- [Style guide - Component name casing in templates](https://vuejs.org/v2/style-guide/#Component-name-casing-in-templates-strongly-recommended)
diff --git a/lib/configs/strongly-recommended.js b/lib/configs/strongly-recommended.js
index 53acc859f..57ae9c8e5 100644
--- a/lib/configs/strongly-recommended.js
+++ b/lib/configs/strongly-recommended.js
@@ -7,6 +7,7 @@ module.exports = {
extends: require.resolve('./essential'),
rules: {
'vue/attribute-hyphenation': 'error',
+ 'vue/component-name-in-template-casing': 'error',
'vue/html-closing-bracket-newline': 'error',
'vue/html-closing-bracket-spacing': 'error',
'vue/html-end-tags': 'error',
diff --git a/lib/index.js b/lib/index.js
index 79769f2a3..8e5709a13 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -10,6 +10,7 @@ module.exports = {
'attribute-hyphenation': require('./rules/attribute-hyphenation'),
'attributes-order': require('./rules/attributes-order'),
'comment-directive': require('./rules/comment-directive'),
+ 'component-name-in-template-casing': require('./rules/component-name-in-template-casing'),
'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'),
'html-end-tags': require('./rules/html-end-tags'),
diff --git a/lib/rules/component-name-in-template-casing.js b/lib/rules/component-name-in-template-casing.js
new file mode 100644
index 000000000..689398a79
--- /dev/null
+++ b/lib/rules/component-name-in-template-casing.js
@@ -0,0 +1,108 @@
+/**
+ * @author Yosuke Ota
+ * issue https://github.com/vuejs/eslint-plugin-vue/issues/250
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+
+const allowedCaseOptions = ['PascalCase', 'kebab-case']
+const defaultCase = 'PascalCase'
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ docs: {
+ description: 'enforce specific casing for the component naming style in template',
+ category: undefined, // strongly-recommended
+ url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/component-name-in-template-casing.md'
+ },
+ fixable: 'code',
+ schema: [
+ {
+ enum: allowedCaseOptions
+ },
+ {
+ type: 'object',
+ properties: {
+ ignores: {
+ type: 'array',
+ items: { type: 'string' },
+ uniqueItems: true,
+ additionalItems: false
+ }
+ },
+ additionalProperties: false
+ }
+ ]
+ },
+
+ create (context) {
+ const caseOption = context.options[0]
+ const options = context.options[1] || {}
+ const caseType = allowedCaseOptions.indexOf(caseOption) !== -1 ? caseOption : defaultCase
+ const ignores = options.ignores || []
+ const tokens = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
+ const sourceCode = context.getSourceCode()
+
+ let hasInvalidEOF = false
+
+ return utils.defineTemplateBodyVisitor(context, {
+ 'VElement' (node) {
+ if (hasInvalidEOF) {
+ return
+ }
+
+ if (!utils.isCustomComponent(node)) {
+ return
+ }
+
+ const name = node.rawName
+ if (ignores.indexOf(name) >= 0) {
+ return
+ }
+ const casingName = casing.getConverter(caseType)(name)
+ if (casingName !== name) {
+ const startTag = node.startTag
+ const open = tokens.getFirstToken(startTag)
+
+ context.report({
+ node: open,
+ loc: open.loc,
+ message: 'Component name "{{name}}" is not {{caseType}}.',
+ data: {
+ name,
+ caseType
+ },
+ fix: fixer => {
+ const endTag = node.endTag
+ if (!endTag) {
+ return fixer.replaceText(open, `<${casingName}`)
+ }
+ const endTagOpen = tokens.getFirstToken(endTag)
+ // If we can upgrade requirements to `eslint@>4.1.0`, this code can be replaced by:
+ // return [
+ // fixer.replaceText(open, `<${casingName}`),
+ // fixer.replaceText(endTagOpen, `${casingName}`)
+ // ]
+ const code = `<${casingName}${sourceCode.text.slice(open.range[1], endTagOpen.range[0])}${casingName}`
+ return fixer.replaceTextRange([open.range[0], endTagOpen.range[1]], code)
+ }
+ })
+ }
+ }
+ }, {
+ Program (node) {
+ hasInvalidEOF = utils.hasInvalidEOF(node)
+ }
+ })
+ }
+}
diff --git a/tests/lib/rules/component-name-in-template-casing.js b/tests/lib/rules/component-name-in-template-casing.js
new file mode 100644
index 000000000..b2fc98711
--- /dev/null
+++ b/tests/lib/rules/component-name-in-template-casing.js
@@ -0,0 +1,320 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/component-name-in-template-casing')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+ parser: 'vue-eslint-parser'
+})
+
+tester.run('component-name-in-template-casing', rule, {
+ valid: [
+ // default
+ '',
+ '
',
+ '',
+ '',
+ '',
+ '
',
+
+ // kebab-case
+ {
+ code: '',
+ options: ['kebab-case']
+ },
+ {
+ code: '',
+ options: ['kebab-case']
+ },
+ {
+ code: '
',
+ options: ['kebab-case']
+ },
+ {
+ code: '',
+ options: ['kebab-case']
+ },
+ {
+ code: '',
+ options: ['kebab-case']
+ },
+ // ignores
+ {
+ code: '',
+ options: ['PascalCase', { ignores: ['custom-element'] }]
+ },
+ {
+ code: '',
+ options: ['PascalCase', { ignores: ['custom-element'] }]
+ },
+ // Invalid EOF
+ '
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ errors: ['Component name "the-component" is not PascalCase.']
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: ['Component name "the-component" is not PascalCase.']
+ },
+ {
+ code: `
+
+
+
+
+
+ `,
+ options: ['kebab-case'],
+ output: `
+
+
+
+
+
+ `,
+ errors: ['Component name "TheComponent" is not kebab-case.']
+ },
+ {
+ code: `
+
+
+
+ `,
+ options: ['kebab-case'],
+ output: `
+
+
+
+ `,
+ errors: ['Component name "TheComponent" is not kebab-case.']
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: ['Component name "the-component" is not PascalCase.']
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: ['Component name "the-component" is not PascalCase.']
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: ['Component name "the-component" is not PascalCase.']
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: ['Component name "theComponent" is not PascalCase.']
+ },
+ {
+ code: `
+
+
+
+ `,
+ options: ['kebab-case'],
+ output: `
+
+
+
+ `,
+ errors: ['Component name "theComponent" is not kebab-case.']
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: ['Component name "The-component" is not PascalCase.']
+ },
+ {
+ code: `
+
+
+
+ `,
+ options: ['kebab-case'],
+ output: `
+
+
+
+ `,
+ errors: ['Component name "The-component" is not kebab-case.']
+ },
+ {
+ code: `
+
+
+
+ `,
+ options: ['kebab-case'],
+ output: `
+
+
+
+ `,
+ errors: ['Component name "Thecomponent" is not kebab-case.']
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: ['Component name "the-component" is not PascalCase.']
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: ['Component name "the-component" is not PascalCase.']
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: ['Component name "the-component" is not PascalCase.']
+ },
+
+ // ignores
+ {
+ code: `
+
+
+
+
+ `,
+ output: `
+
+
+
+
+ `,
+ options: ['PascalCase', { ignores: ['custom-element'] }],
+ errors: ['Component name "the-component" is not PascalCase.']
+ },
+ {
+ code: `
+
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+
+ `,
+ options: ['PascalCase', { ignores: ['custom-element1', 'custom-element2'] }],
+ errors: [
+ 'Component name "the-component" is not PascalCase.',
+ 'Component name "the-component" is not PascalCase.'
+ ]
+ }
+ ]
+})