component-api-style.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. /**
  2. * @author Yosuke Ota <https://github.com/ota-meshi>
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. /**
  8. * @typedef { 'script-setup' | 'composition' | 'options' } PreferOption
  9. *
  10. * @typedef {PreferOption[]} UserPreferOption
  11. *
  12. * @typedef {object} NormalizeOptions
  13. * @property {object} allowsSFC
  14. * @property {boolean} [allowsSFC.scriptSetup]
  15. * @property {boolean} [allowsSFC.composition]
  16. * @property {boolean} [allowsSFC.options]
  17. * @property {object} allowsOther
  18. * @property {boolean} [allowsOther.composition]
  19. * @property {boolean} [allowsOther.options]
  20. */
  21. /** @type {PreferOption[]} */
  22. const STYLE_OPTIONS = ['script-setup', 'composition', 'options']
  23. /**
  24. * Normalize options.
  25. * @param {any[]} options The options user configured.
  26. * @returns {NormalizeOptions} The normalized options.
  27. */
  28. function parseOptions(options) {
  29. /** @type {NormalizeOptions} */
  30. const opts = { allowsSFC: {}, allowsOther: {} }
  31. /** @type {UserPreferOption} */
  32. const preferOptions = options[0] || ['script-setup', 'composition']
  33. for (const prefer of preferOptions) {
  34. if (prefer === 'script-setup') {
  35. opts.allowsSFC.scriptSetup = true
  36. } else if (prefer === 'composition') {
  37. opts.allowsSFC.composition = true
  38. opts.allowsOther.composition = true
  39. } else if (prefer === 'options') {
  40. opts.allowsSFC.options = true
  41. opts.allowsOther.options = true
  42. }
  43. }
  44. if (!opts.allowsOther.composition && !opts.allowsOther.options) {
  45. opts.allowsOther.composition = true
  46. opts.allowsOther.options = true
  47. }
  48. return opts
  49. }
  50. const OPTIONS_API_OPTIONS = new Set([
  51. 'mixins',
  52. 'extends',
  53. // state
  54. 'data',
  55. 'computed',
  56. 'methods',
  57. 'watch',
  58. 'provide',
  59. 'inject',
  60. // lifecycle
  61. 'beforeCreate',
  62. 'created',
  63. 'beforeMount',
  64. 'mounted',
  65. 'beforeUpdate',
  66. 'updated',
  67. 'activated',
  68. 'deactivated',
  69. 'beforeDestroy',
  70. 'beforeUnmount',
  71. 'destroyed',
  72. 'unmounted',
  73. 'render',
  74. 'renderTracked',
  75. 'renderTriggered',
  76. 'errorCaptured',
  77. // public API
  78. 'expose'
  79. ])
  80. const COMPOSITION_API_OPTIONS = new Set(['setup'])
  81. const LIFECYCLE_HOOK_OPTIONS = new Set([
  82. 'beforeCreate',
  83. 'created',
  84. 'beforeMount',
  85. 'mounted',
  86. 'beforeUpdate',
  87. 'updated',
  88. 'activated',
  89. 'deactivated',
  90. 'beforeDestroy',
  91. 'beforeUnmount',
  92. 'destroyed',
  93. 'unmounted',
  94. 'renderTracked',
  95. 'renderTriggered',
  96. 'errorCaptured'
  97. ])
  98. /**
  99. * @typedef { 'script-setup' | 'composition' | 'options' } ApiStyle
  100. */
  101. /**
  102. * @param {object} allowsOpt
  103. * @param {boolean} [allowsOpt.scriptSetup]
  104. * @param {boolean} [allowsOpt.composition]
  105. * @param {boolean} [allowsOpt.options]
  106. */
  107. function buildAllowedPhrase(allowsOpt) {
  108. const phrases = []
  109. if (allowsOpt.scriptSetup) {
  110. phrases.push('`<script setup>`')
  111. }
  112. if (allowsOpt.composition) {
  113. phrases.push('Composition API')
  114. }
  115. if (allowsOpt.options) {
  116. phrases.push('Options API')
  117. }
  118. return phrases.length > 2
  119. ? `${phrases.slice(0, -1).join(',')} or ${phrases.slice(-1)[0]}`
  120. : phrases.join(' or ')
  121. }
  122. /**
  123. * @param {object} allowsOpt
  124. * @param {boolean} [allowsOpt.scriptSetup]
  125. * @param {boolean} [allowsOpt.composition]
  126. * @param {boolean} [allowsOpt.options]
  127. */
  128. function isPreferScriptSetup(allowsOpt) {
  129. if (!allowsOpt.scriptSetup || allowsOpt.composition || allowsOpt.options) {
  130. return false
  131. }
  132. return true
  133. }
  134. /**
  135. * @param {string} name
  136. */
  137. function buildOptionPhrase(name) {
  138. return LIFECYCLE_HOOK_OPTIONS.has(name)
  139. ? `\`${name}\` lifecycle hook`
  140. : name === 'setup' || name === 'render'
  141. ? `\`${name}\` function`
  142. : `\`${name}\` option`
  143. }
  144. module.exports = {
  145. meta: {
  146. type: 'suggestion',
  147. docs: {
  148. description: 'enforce component API style',
  149. categories: undefined,
  150. url: 'https://eslint.vuejs.org/rules/component-api-style.html'
  151. },
  152. fixable: null,
  153. schema: [
  154. {
  155. type: 'array',
  156. items: {
  157. enum: STYLE_OPTIONS,
  158. uniqueItems: true,
  159. additionalItems: false
  160. },
  161. minItems: 1
  162. }
  163. ],
  164. messages: {
  165. disallowScriptSetup:
  166. '`<script setup>` is not allowed in your project. Use {{allowedApis}} instead.',
  167. disallowComponentOption:
  168. '{{disallowedApi}} is not allowed in your project. {{optionPhrase}} is the API of {{disallowedApi}}. Use {{allowedApis}} instead.',
  169. disallowComponentOptionPreferScriptSetup:
  170. '{{disallowedApi}} is not allowed in your project. Use `<script setup>` instead.'
  171. }
  172. },
  173. /** @param {RuleContext} context */
  174. create(context) {
  175. const options = parseOptions(context.options)
  176. return utils.compositingVisitors(
  177. {
  178. Program() {
  179. if (options.allowsSFC.scriptSetup) {
  180. return
  181. }
  182. const scriptSetup = utils.getScriptSetupElement(context)
  183. if (scriptSetup) {
  184. context.report({
  185. node: scriptSetup.startTag,
  186. messageId: 'disallowScriptSetup',
  187. data: {
  188. allowedApis: buildAllowedPhrase(options.allowsSFC)
  189. }
  190. })
  191. }
  192. }
  193. },
  194. utils.defineVueVisitor(context, {
  195. onVueObjectEnter(node) {
  196. const allows = utils.isSFCObject(context, node)
  197. ? options.allowsSFC
  198. : options.allowsOther
  199. if (allows.composition && allows.options) {
  200. return
  201. }
  202. const disallows = [
  203. {
  204. allow: allows.composition,
  205. options: COMPOSITION_API_OPTIONS,
  206. api: 'Composition API'
  207. },
  208. {
  209. allow: allows.options,
  210. options: OPTIONS_API_OPTIONS,
  211. api: 'Options API'
  212. }
  213. ].filter(({ allow }) => !allow)
  214. for (const prop of node.properties) {
  215. if (prop.type !== 'Property') {
  216. continue
  217. }
  218. const name = utils.getStaticPropertyName(prop)
  219. if (!name) {
  220. continue
  221. }
  222. for (const { options, api } of disallows) {
  223. if (options.has(name)) {
  224. context.report({
  225. node: prop.key,
  226. messageId: isPreferScriptSetup(allows)
  227. ? 'disallowComponentOptionPreferScriptSetup'
  228. : 'disallowComponentOption',
  229. data: {
  230. disallowedApi: api,
  231. optionPhrase: buildOptionPhrase(name),
  232. allowedApis: buildAllowedPhrase(allows)
  233. }
  234. })
  235. }
  236. }
  237. }
  238. }
  239. })
  240. )
  241. }
  242. }