component-api-style.js 7.7 KB

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