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, `
', + '', + '', + '', + '', + '', + + // 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.' + ] + } + ] +})