diff --git a/docs/rules/index.md b/docs/rules/index.md index abc3419a4..5e9e91dbb 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -255,6 +255,7 @@ For example: | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | :lipstick: | | [vue/padding-line-between-tags](./padding-line-between-tags.md) | require or disallow newlines between sibling tags in template | :wrench: | :lipstick: | | [vue/padding-lines-in-component-definition](./padding-lines-in-component-definition.md) | require or disallow padding lines in component definition | :wrench: | :lipstick: | +| [vue/prefer-define-options](./prefer-define-options.md) | enforce use of `defineOptions` instead of default export. | :wrench: | :warning: | | [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: | | [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | :hammer: | | [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: | diff --git a/docs/rules/prefer-define-options.md b/docs/rules/prefer-define-options.md new file mode 100644 index 000000000..5cb75d2a6 --- /dev/null +++ b/docs/rules/prefer-define-options.md @@ -0,0 +1,56 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/prefer-define-options +description: enforce use of `defineOptions` instead of default export. +--- +# vue/prefer-define-options + +> enforce use of `defineOptions` instead of default export. + +- :exclamation: ***This rule has not been released yet.*** +- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule. + +## :book: Rule Details + +This rule aims to enforce use of `defineOptions` instead of default export in ` +``` + + + + + +```vue + + +``` + + + +## :wrench: Options + +Nothing. + +## :books: Further Reading + +- [API - defineOptions()](https://vuejs.org/api/sfc-script-setup.html#defineoptions) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-define-options.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-define-options.js) diff --git a/lib/index.js b/lib/index.js index c7b580868..a09e73019 100644 --- a/lib/index.js +++ b/lib/index.js @@ -166,6 +166,7 @@ module.exports = { 'padding-line-between-blocks': require('./rules/padding-line-between-blocks'), 'padding-line-between-tags': require('./rules/padding-line-between-tags'), 'padding-lines-in-component-definition': require('./rules/padding-lines-in-component-definition'), + 'prefer-define-options': require('./rules/prefer-define-options'), 'prefer-import-from-vue': require('./rules/prefer-import-from-vue'), 'prefer-prop-type-boolean-first': require('./rules/prefer-prop-type-boolean-first'), 'prefer-separate-static-class': require('./rules/prefer-separate-static-class'), diff --git a/lib/rules/prefer-define-options.js b/lib/rules/prefer-define-options.js new file mode 100644 index 000000000..cd9240445 --- /dev/null +++ b/lib/rules/prefer-define-options.js @@ -0,0 +1,122 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'enforce use of `defineOptions` instead of default export.', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/prefer-define-options.html' + }, + fixable: 'code', + schema: [], + messages: { + preferDefineOptions: 'Use `defineOptions` instead of default export.' + } + }, + /** + * @param {RuleContext} context + * @returns {RuleListener} + */ + create(context) { + const scriptSetup = utils.getScriptSetupElement(context) + if (!scriptSetup) { + return {} + } + + /** @type {CallExpression | null} */ + let defineOptionsNode = null + /** @type {ExportDefaultDeclaration | null} */ + let exportDefaultDeclaration = null + + return utils.compositingVisitors( + utils.defineScriptSetupVisitor(context, { + onDefineOptionsEnter(node) { + defineOptionsNode = node + } + }), + { + ExportDefaultDeclaration(node) { + exportDefaultDeclaration = node + }, + 'Program:exit'() { + if (!exportDefaultDeclaration) { + return + } + context.report({ + node: exportDefaultDeclaration, + messageId: 'preferDefineOptions', + fix: defineOptionsNode + ? null + : buildFix(exportDefaultDeclaration, scriptSetup) + }) + } + } + ) + + /** + * @param {ExportDefaultDeclaration} node + * @param {VElement} scriptSetup + * @returns {(fixer: RuleFixer) => Fix[]} + */ + function buildFix(node, scriptSetup) { + return (fixer) => { + const sourceCode = context.getSourceCode() + + // Calc remove range + /** @type {Range} */ + let removeRange = [...node.range] + + const script = scriptSetup.parent.children + .filter(utils.isVElement) + .find( + (node) => + node.name === 'script' && !utils.hasAttribute(node, 'setup') + ) + if ( + script && + script.endTag && + sourceCode + .getTokensBetween(script.startTag, script.endTag, { + includeComments: true + }) + .every( + (token) => + removeRange[0] <= token.range[0] && + token.range[1] <= removeRange[1] + ) + ) { + removeRange = [...script.range] + } + const removeStartLoc = sourceCode.getLocFromIndex(removeRange[0]) + if ( + sourceCode.lines[removeStartLoc.line - 1] + .slice(0, removeStartLoc.column) + .trim() === '' + ) { + removeRange[0] = + removeStartLoc.line === 1 + ? 0 + : sourceCode.getIndexFromLoc({ + line: removeStartLoc.line - 1, + column: sourceCode.lines[removeStartLoc.line - 2].length + }) + } + + return [ + fixer.removeRange(removeRange), + fixer.insertTextAfter( + scriptSetup.startTag, + `\ndefineOptions(${sourceCode.getText(node.declaration)})\n` + ) + ] + } + } + } +} diff --git a/tests/lib/rules/prefer-define-options.js b/tests/lib/rules/prefer-define-options.js new file mode 100644 index 000000000..2fd9bdfec --- /dev/null +++ b/tests/lib/rules/prefer-define-options.js @@ -0,0 +1,109 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/prefer-define-options') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('prefer-define-options', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + + `, + output: ` + + `, + errors: [ + { + message: 'Use `defineOptions` instead of default export.', + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + + + `, + output: null, + errors: [ + { + message: 'Use `defineOptions` instead of default export.', + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + + + `, + output: ` + + + `, + errors: [ + { + message: 'Use `defineOptions` instead of default export.', + line: 4 + } + ] + } + ] +})