multi-word-component-names.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. /**
  2. * @author Marton Csordas
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const path = require('path')
  7. const casing = require('../utils/casing')
  8. const utils = require('../utils')
  9. const RESERVED_NAMES_IN_VUE3 = new Set(
  10. require('../utils/vue3-builtin-components')
  11. )
  12. module.exports = {
  13. meta: {
  14. type: 'suggestion',
  15. docs: {
  16. description: 'require component names to be always multi-word',
  17. categories: ['vue3-essential', 'vue2-essential'],
  18. url: 'https://eslint.vuejs.org/rules/multi-word-component-names.html'
  19. },
  20. schema: [
  21. {
  22. type: 'object',
  23. properties: {
  24. ignores: {
  25. type: 'array',
  26. items: { type: 'string' },
  27. uniqueItems: true,
  28. additionalItems: false
  29. }
  30. },
  31. additionalProperties: false
  32. }
  33. ],
  34. messages: {
  35. unexpected: 'Component name "{{value}}" should always be multi-word.'
  36. }
  37. },
  38. /** @param {RuleContext} context */
  39. create(context) {
  40. /** @type {Set<string>} */
  41. const ignores = new Set()
  42. ignores.add('App')
  43. ignores.add('app')
  44. for (const ignore of (context.options[0] && context.options[0].ignores) ||
  45. []) {
  46. ignores.add(ignore)
  47. if (casing.isPascalCase(ignore)) {
  48. // PascalCase
  49. ignores.add(casing.kebabCase(ignore))
  50. }
  51. }
  52. let hasVue = utils.isScriptSetup(context)
  53. let hasName = false
  54. /**
  55. * Returns true if the given component name is valid, otherwise false.
  56. * @param {string} name
  57. * */
  58. function isValidComponentName(name) {
  59. if (ignores.has(name) || RESERVED_NAMES_IN_VUE3.has(name)) {
  60. return true
  61. }
  62. const elements = casing.kebabCase(name).split('-')
  63. return elements.length > 1
  64. }
  65. /**
  66. * @param {Expression | SpreadElement} nameNode
  67. */
  68. function validateName(nameNode) {
  69. if (nameNode.type !== 'Literal') return
  70. const componentName = `${nameNode.value}`
  71. if (!isValidComponentName(componentName)) {
  72. context.report({
  73. node: nameNode,
  74. messageId: 'unexpected',
  75. data: {
  76. value: componentName
  77. }
  78. })
  79. }
  80. }
  81. return utils.compositingVisitors(
  82. utils.executeOnCallVueComponent(context, (node) => {
  83. hasVue = true
  84. if (node.arguments.length !== 2) return
  85. hasName = true
  86. validateName(node.arguments[0])
  87. }),
  88. utils.executeOnVue(context, (obj) => {
  89. hasVue = true
  90. const node = utils.findProperty(obj, 'name')
  91. if (!node) return
  92. hasName = true
  93. validateName(node.value)
  94. }),
  95. utils.defineScriptSetupVisitor(context, {
  96. onDefineOptionsEnter(node) {
  97. if (node.arguments.length === 0) return
  98. const define = node.arguments[0]
  99. if (define.type !== 'ObjectExpression') return
  100. const nameNode = utils.findProperty(define, 'name')
  101. if (!nameNode) return
  102. hasName = true
  103. validateName(nameNode.value)
  104. }
  105. }),
  106. {
  107. /** @param {Program} node */
  108. 'Program:exit'(node) {
  109. if (hasName) return
  110. if (!hasVue && node.body.length > 0) return
  111. const fileName = context.getFilename()
  112. const componentName = path.basename(fileName, path.extname(fileName))
  113. if (
  114. utils.isVueFile(fileName) &&
  115. !isValidComponentName(componentName)
  116. ) {
  117. context.report({
  118. messageId: 'unexpected',
  119. data: {
  120. value: componentName
  121. },
  122. loc: { line: 1, column: 0 }
  123. })
  124. }
  125. }
  126. }
  127. )
  128. }
  129. }