diff --git a/README.md b/README.md index d483510..0434e3b 100644 --- a/README.md +++ b/README.md @@ -102,4 +102,5 @@ Include all the below rules, as well as all priority rules in above categories, | | Rule ID | Description | |:---|:--------|:------------| -| :wrench: | [nuxt/no-timing-in-fetch-data](./docs/rules/no-timing-in-fetch-data.md) | Disallow `setTimeout/setInterval` in `asyncData/fetch` | \ No newline at end of file +| | [nuxt/no-timing-in-fetch-data](./docs/rules/no-timing-in-fetch-data.md) | Disallow `setTimeout/setInterval` in `asyncData/fetch` | +| | [nuxt/require-func-head](./docs/rules/require-func-head.md) | Enforce `head` property in component to be a function. | diff --git a/docs/rules/require-func-head.md b/docs/rules/require-func-head.md new file mode 100644 index 0000000..005fbf0 --- /dev/null +++ b/docs/rules/require-func-head.md @@ -0,0 +1,40 @@ +# nuxt/require-func-head + +> enforce `head` property in component to be a function. + +- :gear: This rule is included in `"plugin:nuxt/recommended"`. + +## Rule Details + +This rule is enforcing `head` property in component to be a function. + +Examples of **incorrect** code for this rule: + +```js + +export default { + head: { + title: "My page" + } +} + +``` + +Examples of **correct** code for this rule: + +```js + +export default { + head() { + return { + title: "My page" + } + } +} + +``` + +## :mag: Implementation + +- [Rule source](../../lib/rules/require-func-head.js) +- [Test source](../../lib/rules/__tests__/require-func-head.test.js) diff --git a/lib/configs/recommended.js b/lib/configs/recommended.js index 2de652d..4d1b137 100644 --- a/lib/configs/recommended.js +++ b/lib/configs/recommended.js @@ -1,6 +1,7 @@ module.exports = { extends: require.resolve('./base.js'), rules: { - 'nuxt/no-timing-in-fetch-data': 'error' + 'nuxt/no-timing-in-fetch-data': 'error', + 'nuxt/require-func-head': 'error' } } diff --git a/lib/index.js b/lib/index.js index e44ae1c..2c7f7e2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,7 +5,8 @@ module.exports = { 'no-globals-in-created': require('./rules/no-globals-in-created'), 'no-this-in-fetch-data': require('./rules/no-this-in-fetch-data'), 'no-timing-in-fetch-data': require('./rules/no-timing-in-fetch-data'), - 'no-cjs-in-config': require('./rules/no-cjs-in-config') + 'no-cjs-in-config': require('./rules/no-cjs-in-config'), + 'require-func-head': require('./rules/require-func-head') }, configs: { base: require('./configs/base'), diff --git a/lib/rules/__tests__/require-func-head.test.js b/lib/rules/__tests__/require-func-head.test.js new file mode 100644 index 0000000..f0483a9 --- /dev/null +++ b/lib/rules/__tests__/require-func-head.test.js @@ -0,0 +1,60 @@ +/** + * @fileoverview disallow `setTimeout/setInterval` in `asyncData/fetch` + * @author Xin Du + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require('../require-func-head') + +var RuleTester = require('eslint').RuleTester + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module' +} + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +var ruleTester = new RuleTester() +ruleTester.run('require-func-head', rule, { + + valid: [ + { + filename: 'test.vue', + code: ` + export default { + head() { + return { + title: "My page" + } + } + } + `, + parserOptions + } + ], + + invalid: [ + { + filename: 'test.vue', + code: ` + export default { + head: { + title: "My page" + } + } + `, + errors: [{ + message: '`head` property in component must be a function.', + type: 'Property' + }], + parserOptions + } + ] +}) diff --git a/lib/rules/require-func-head.js b/lib/rules/require-func-head.js new file mode 100644 index 0000000..f8b5c2f --- /dev/null +++ b/lib/rules/require-func-head.js @@ -0,0 +1,54 @@ +/** + * @fileoverview enforce component's head property to be a function. + * @author Xin Du + */ +'use strict' + +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce component's head property to be a function", + category: 'recommended' + }, + fixable: 'code', + messages: { + head: '`head` property in component must be a function.' + } + }, + + create (context) { + const sourceCode = context.getSourceCode() + + return utils.executeOnVueComponent(context, (obj) => { + obj.properties + .filter(p => + p.type === 'Property' && + p.key.type === 'Identifier' && + p.key.name === 'head' && + p.value.type !== 'FunctionExpression' && + p.value.type !== 'ArrowFunctionExpression' && + p.value.type !== 'Identifier' + ) + .forEach(p => { + context.report({ + node: p, + messageId: 'head', + fix (fixer) { + const tokens = utils.getFirstAndLastTokens(p.value, sourceCode) + + return [ + fixer.insertTextBefore(tokens.first, 'function() {\nreturn '), + fixer.insertTextAfter(tokens.last, ';\n}') + ] + } + }) + }) + }) + } +} diff --git a/lib/utils/index.js b/lib/utils/index.js index e37c95d..727ab0f 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -43,6 +43,28 @@ module.exports = Object.assign( } } } + }, + isOpenParen (token) { + return token.type === 'Punctuator' && token.value === '(' + }, + isCloseParen (token) { + return token.type === 'Punctuator' && token.value === ')' + }, + getFirstAndLastTokens (node, sourceCode) { + let first = sourceCode.getFirstToken(node) + let last = sourceCode.getLastToken(node) + + // If the value enclosed by parentheses, update the 'first' and 'last' by the parentheses. + while (true) { + const prev = sourceCode.getTokenBefore(first) + const next = sourceCode.getTokenAfter(last) + if (this.isOpenParen(prev) && this.isCloseParen(next)) { + first = prev + last = next + } else { + return { first, last } + } + } } }, utils