diff --git a/docs/rules/README.md b/docs/rules/README.md index 5f86b7fc7..85f6e661b 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -288,6 +288,7 @@ For example: |:--------|:------------|:---| | [vue/block-lang](./block-lang.md) | disallow use other than available `lang` | | | [vue/block-tag-newline](./block-tag-newline.md) | enforce line breaks after opening and before closing block-level tags | :wrench: | +| [vue/component-api-style](./component-api-style.md) | enforce component API style | | | [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: | | [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | | | [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | | diff --git a/docs/rules/component-api-style.md b/docs/rules/component-api-style.md new file mode 100644 index 000000000..8472d4c1b --- /dev/null +++ b/docs/rules/component-api-style.md @@ -0,0 +1,144 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/component-api-style +description: enforce component API style +--- +# vue/component-api-style + +> enforce component API style + +- :exclamation: ***This rule has not been released yet.*** + +## :book: Rule Details + +This rule aims to make the API style you use to define Vue components consistent in your project. + +For example, if you want to allow only ` +``` + + + + + +```vue + +``` + + + + + +```vue + +``` + + + +## :wrench: Options + +```json +{ + "vue/component-api-style": ["error", + ["script-setup", "composition"] // "script-setup", "composition", or "options" + ] +} +``` + +- Array options ... Defines the API styles you want to allow. Default is `["script-setup", "composition"]`. You can use the following values. + - `"script-setup"` ... If set, allows ` +``` + + + + + +```vue + + +``` + + + + + +```vue + +``` + + + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/component-api-style.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/component-api-style.js) diff --git a/lib/index.js b/lib/index.js index af29765ca..0015fb579 100644 --- a/lib/index.js +++ b/lib/index.js @@ -21,6 +21,7 @@ module.exports = { 'comma-spacing': require('./rules/comma-spacing'), 'comma-style': require('./rules/comma-style'), 'comment-directive': require('./rules/comment-directive'), + 'component-api-style': require('./rules/component-api-style'), 'component-definition-name-casing': require('./rules/component-definition-name-casing'), 'component-name-in-template-casing': require('./rules/component-name-in-template-casing'), 'component-tags-order': require('./rules/component-tags-order'), diff --git a/lib/rules/component-api-style.js b/lib/rules/component-api-style.js new file mode 100644 index 000000000..4cec3b1ab --- /dev/null +++ b/lib/rules/component-api-style.js @@ -0,0 +1,257 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') + +/** + * @typedef { 'script-setup' | 'composition' | 'options' } PreferOption + * + * @typedef {PreferOption[]} UserPreferOption + * + * @typedef {object} NormalizeOptions + * @property {object} allowsSFC + * @property {boolean} [allowsSFC.scriptSetup] + * @property {boolean} [allowsSFC.composition] + * @property {boolean} [allowsSFC.options] + * @property {object} allowsOther + * @property {boolean} [allowsOther.composition] + * @property {boolean} [allowsOther.options] + */ + +/** @type {PreferOption[]} */ +const STYLE_OPTIONS = ['script-setup', 'composition', 'options'] + +/** + * Normalize options. + * @param {any[]} options The options user configured. + * @returns {NormalizeOptions} The normalized options. + */ +function parseOptions(options) { + /** @type {NormalizeOptions} */ + const opts = { allowsSFC: {}, allowsOther: {} } + + /** @type {UserPreferOption} */ + const preferOptions = options[0] || ['script-setup', 'composition'] + for (const prefer of preferOptions) { + if (prefer === 'script-setup') { + opts.allowsSFC.scriptSetup = true + } else if (prefer === 'composition') { + opts.allowsSFC.composition = true + opts.allowsOther.composition = true + } else if (prefer === 'options') { + opts.allowsSFC.options = true + opts.allowsOther.options = true + } + } + + if (!opts.allowsOther.composition && !opts.allowsOther.options) { + opts.allowsOther.composition = true + opts.allowsOther.options = true + } + + return opts +} + +const OPTIONS_API_OPTIONS = new Set([ + 'mixins', + 'extends', + // state + 'data', + 'computed', + 'methods', + 'watch', + 'provide', + 'inject', + // lifecycle + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'activated', + 'deactivated', + 'beforeDestroy', + 'beforeUnmount', + 'destroyed', + 'unmounted', + 'render', + 'renderTracked', + 'renderTriggered', + 'errorCaptured', + // public API + 'expose' +]) +const COMPOSITION_API_OPTIONS = new Set(['setup']) + +const LIFECYCLE_HOOK_OPTIONS = new Set([ + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'activated', + 'deactivated', + 'beforeDestroy', + 'beforeUnmount', + 'destroyed', + 'unmounted', + 'renderTracked', + 'renderTriggered', + 'errorCaptured' +]) + +/** + * @typedef { 'script-setup' | 'composition' | 'options' } ApiStyle + */ + +/** + * @param {object} allowsOpt + * @param {boolean} [allowsOpt.scriptSetup] + * @param {boolean} [allowsOpt.composition] + * @param {boolean} [allowsOpt.options] + */ +function buildAllowedPhrase(allowsOpt) { + const phrases = [] + if (allowsOpt.scriptSetup) { + phrases.push('` + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + options: [['options']], + code: ` + + ` + }, + { + filename: 'test.js', + code: ` + import { ref, defineComponent } from 'vue' + defineComponent({ + setup() { + const msg = ref('Hello World!') + // ... + return { + msg, + // ... + } + } + }) + ` + }, + { + filename: 'test.js', + options: [['options']], + code: ` + import { defineComponent } from 'vue' + defineComponent({ + data () { + return { + msg: 'Hello World!', + // ... + } + }, + // ... + }) + ` + }, + { + filename: 'test.vue', + options: [['script-setup']], + code: ` + + ` + }, + { + filename: 'test.js', + options: [['script-setup']], + code: ` + import { ref, defineComponent } from 'vue' + defineComponent({ + setup() { + const msg = ref('Hello World!') + // ... + return { + msg, + // ... + } + } + }) + ` + }, + { + filename: 'test.js', + options: [['script-setup']], + code: ` + import { defineComponent } from 'vue' + defineComponent({ + data () { + return { + msg: 'Hello World!', + // ... + } + }, + // ... + }) + ` + }, + { + filename: 'test.vue', + options: [['composition']], + code: ` + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: + 'Options API is not allowed in your project. `data` option is the API of Options API. Use ` + `, + options: [['options']], + errors: [ + { + message: + '` + `, + options: [['options']], + errors: [ + { + message: + 'Composition API is not allowed in your project. `setup` function is the API of Composition API. Use Options API instead.', + line: 5, + column: 9 + } + ] + }, + { + filename: 'test.js', + code: ` + import { defineComponent } from 'vue' + defineComponent({ + data () { + return { + msg: 'Hello World!', + // ... + } + }, + // ... + }) + `, + errors: [ + { + message: + 'Options API is not allowed in your project. `data` option is the API of Options API. Use Composition API instead.', + line: 4, + column: 9 + } + ] + }, + { + filename: 'test.js', + code: ` + import { ref, defineComponent } from 'vue' + defineComponent({ + setup() { + const msg = ref('Hello World!') + // ... + return { + msg, + // ... + } + } + }) + `, + options: [['options']], + errors: [ + { + message: + 'Composition API is not allowed in your project. `setup` function is the API of Composition API. Use Options API instead.', + line: 4, + column: 9 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [['script-setup']], + errors: [ + { + message: + 'Composition API is not allowed in your project. Use ` + `, + errors: [ + { + message: + 'Options API is not allowed in your project. Use ` + `, + options: [['composition']], + errors: [ + { + message: + '` + `, + errors: [ + { + message: + 'Options API is not allowed in your project. `data` option is the API of Options API. Use Composition API instead.', + line: 4, + column: 9 + } + ] + }, + { + filename: 'test.vue', + options: [['composition']], + code: ` + + `, + errors: [ + { + message: + 'Options API is not allowed in your project. `mixins` option is the API of Options API. Use Composition API instead.', + line: 4, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `extends` option is the API of Options API. Use Composition API instead.', + line: 5, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `data` option is the API of Options API. Use Composition API instead.', + line: 7, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `computed` option is the API of Options API. Use Composition API instead.', + line: 8, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `methods` option is the API of Options API. Use Composition API instead.', + line: 9, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `watch` option is the API of Options API. Use Composition API instead.', + line: 10, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `provide` option is the API of Options API. Use Composition API instead.', + line: 11, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `inject` option is the API of Options API. Use Composition API instead.', + line: 12, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `beforeCreate` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 14, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `created` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 15, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `beforeMount` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 16, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `mounted` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 17, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `beforeUpdate` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 18, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `updated` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 19, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `activated` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 20, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `deactivated` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 21, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `beforeDestroy` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 22, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `beforeUnmount` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 23, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `destroyed` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 24, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `unmounted` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 25, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `render` function is the API of Options API. Use Composition API instead.', + line: 26, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `renderTracked` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 27, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `renderTriggered` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 28, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `errorCaptured` lifecycle hook is the API of Options API. Use Composition API instead.', + line: 29, + column: 9 + }, + { + message: + 'Options API is not allowed in your project. `expose` option is the API of Options API. Use Composition API instead.', + line: 31, + column: 9 + } + ] + } + ] +})