diff --git a/docs/rules/multi-word-component-names.md b/docs/rules/multi-word-component-names.md index 0baefe4d5..33e48fb00 100644 --- a/docs/rules/multi-word-component-names.md +++ b/docs/rules/multi-word-component-names.md @@ -65,6 +65,7 @@ export default { ```vue + +``` + + + + + +```vue + +``` + + ## :books: Further Reading -- [Style guide - Multi-word component names](https://vuejs.org/v2/style-guide/#Multi-word-component-names-essential) +- [Style guide - Multi-word component names](https://v3.vuejs.org/style-guide/#multi-word-component-names-essential) ## :rocket: Version diff --git a/lib/rules/multi-word-component-names.js b/lib/rules/multi-word-component-names.js index 46e7bfac0..18cc79601 100644 --- a/lib/rules/multi-word-component-names.js +++ b/lib/rules/multi-word-component-names.js @@ -15,23 +15,6 @@ const RESERVED_NAMES_IN_VUE3 = new Set( require('../utils/vue3-builtin-components') ) -// ------------------------------------------------------------------------------ -// Helpers -// ------------------------------------------------------------------------------ - -/** - * Returns true if the given component name is valid, otherwise false. - * @param {string} name - * */ -function isValidComponentName(name) { - if (name.toLowerCase() === 'app' || RESERVED_NAMES_IN_VUE3.has(name)) { - return true - } else { - const elements = casing.kebabCase(name).split('-') - return elements.length > 1 - } -} - // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -44,22 +27,92 @@ module.exports = { categories: ['vue3-essential', 'essential'], url: 'https://eslint.vuejs.org/rules/multi-word-component-names.html' }, - schema: [], + schema: [ + { + type: 'object', + properties: { + ignores: { + type: 'array', + items: { type: 'string' }, + uniqueItems: true, + additionalItems: false + } + }, + additionalProperties: false + } + ], messages: { unexpected: 'Component name "{{value}}" should always be multi-word.' } }, /** @param {RuleContext} context */ create(context) { - const fileName = context.getFilename() - let componentName = fileName.replace(/\.[^/.]+$/, '') + /** @type {Set} */ + const ignores = new Set() + ignores.add('App') + ignores.add('app') + for (const ignore of (context.options[0] && context.options[0].ignores) || + []) { + ignores.add(ignore) + if (casing.isPascalCase(ignore)) { + // PascalCase + ignores.add(casing.kebabCase(ignore)) + } + } + let hasVue = false + let hasName = false + + /** + * Returns true if the given component name is valid, otherwise false. + * @param {string} name + * */ + function isValidComponentName(name) { + if (ignores.has(name) || RESERVED_NAMES_IN_VUE3.has(name)) { + return true + } + const elements = casing.kebabCase(name).split('-') + return elements.length > 1 + } + + /** + * @param {Expression | SpreadElement} nameNode + */ + function validateName(nameNode) { + if (nameNode.type !== 'Literal') return + const componentName = `${nameNode.value}` + if (!isValidComponentName(componentName)) { + context.report({ + node: nameNode, + messageId: 'unexpected', + data: { + value: componentName + } + }) + } + } return utils.compositingVisitors( + utils.executeOnCallVueComponent(context, (node) => { + hasVue = true + if (node.arguments.length !== 2) return + hasName = true + validateName(node.arguments[0]) + }), + utils.executeOnVue(context, (obj) => { + hasVue = true + const node = utils.findProperty(obj, 'name') + if (!node) return + hasName = true + validateName(node.value) + }), { /** @param {Program} node */ - Program(node) { + 'Program:exit'(node) { + if (hasName) return + if (!hasVue && node.body.length > 0) return + const fileName = context.getFilename() + const componentName = fileName.replace(/\.[^/.]+$/, '') if ( - !node.body.length && utils.isVueFile(fileName) && !isValidComponentName(componentName) ) { @@ -72,44 +125,7 @@ module.exports = { }) } } - }, - - utils.executeOnVue(context, (obj) => { - const node = utils.findProperty(obj, 'name') - - /** @type {SourceLocation | null} */ - let loc = null - - // Check if the component has a name property. - if (node) { - const valueNode = node.value - if (valueNode.type !== 'Literal') return - - componentName = `${valueNode.value}` - loc = node.loc - } else if ( - obj.parent.type === 'CallExpression' && - obj.parent.arguments.length === 2 - ) { - // The component is registered globally with 'Vue.component', where - // the first paremter is the component name. - const argument = obj.parent.arguments[0] - if (argument.type !== 'Literal') return - - componentName = `${argument.value}` - loc = argument.loc - } - - if (!isValidComponentName(componentName)) { - context.report({ - messageId: 'unexpected', - data: { - value: componentName - }, - loc: loc || { line: 1, column: 0 } - }) - } - }) + } ) } } diff --git a/tests/lib/rules/multi-word-component-names.js b/tests/lib/rules/multi-word-component-names.js index 65b777984..d143f6388 100644 --- a/tests/lib/rules/multi-word-component-names.js +++ b/tests/lib/rules/multi-word-component-names.js @@ -158,6 +158,17 @@ tester.run('multi-word-component-names', rule, { Vue.component('TheTest', {}) ` + }, + { + filename: 'test.vue', + options: [{ ignores: ['Todo'] }], + code: ` + + ` } ], invalid: [ @@ -248,6 +259,23 @@ tester.run('multi-word-component-names', rule, { line: 3 } ] + }, + { + filename: 'test.vue', + options: [{ ignores: ['Todo'] }], + code: ` + + `, + errors: [ + { + message: 'Component name "Item" should always be multi-word.', + line: 4 + } + ] } ] })