multi-word-component-names.js 3.6 KB

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