diff --git a/packages/eslint-plugin-smarthr/README.md b/packages/eslint-plugin-smarthr/README.md index fa1a0e40..df7cdc1b 100644 --- a/packages/eslint-plugin-smarthr/README.md +++ b/packages/eslint-plugin-smarthr/README.md @@ -21,6 +21,7 @@ - [best-practice-for-date](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-date) - [best-practice-for-layouts](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-layouts) - [best-practice-for-remote-trigger-dialog](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-remote-trigger-dialog) +- [best-practice-for-tailwind-variants](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants) - [design-system-guideline-prohibit-double-icons](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/design-system-guideline-prohibit-double-icons) - [format-import-path](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/format-import-path) - [format-translate-component](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/format-translate-component) diff --git a/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants/README.md b/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants/README.md new file mode 100644 index 00000000..8f1717a1 --- /dev/null +++ b/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants/README.md @@ -0,0 +1,46 @@ +# smarthr/best-practice-for-tailwind-variants + +- tailwind-variantsの記法をチェックするルールです +- 現状は以下のチェックを行います + - tailwind-variants(tv) のimport時の名称をtvに固定しているか (asなどでの名称変更の禁止) + - tv の実行結果を格納する変数名を統一 (styleGenerator、もしくはxxxStyleGenerator) + - tvで生成した関数の実行をuseMemo hook でメモ化しているか + + +## rules + +```js +{ + rules: { + 'smarthr/best-practice-for-tailwind-variants': 'error', // 'warn', 'off' + }, +} +``` + +## ❌ Incorrect + +```jsx +import { tv as hoge } from 'tailwind-variants' + +const xxx = tv({ + ... +}) + +... + +const style = xxx() +``` + +## ✅ Correct + +```jsx +import { tv } from 'tailwind-variants' + +const styleGenerator = tv({ + ... +}) + +... + +const style = useMemo(() => styleGenerator(), []) +``` diff --git a/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants/index.js b/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants/index.js new file mode 100644 index 00000000..82da6b23 --- /dev/null +++ b/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants/index.js @@ -0,0 +1,78 @@ +const SCHEMA = [] + +const TV_COMPONENTS_METHOD = 'tv' +const TV_COMPONENTS = 'tailwind-variants' +const TV_RESULT_CONST_NAME_REGEX = /(S|s)tyleGenerator$/ + +const findValidImportNameNode = (s) => s.type === 'ImportSpecifier' && s.local.name === TV_COMPONENTS_METHOD + +const checkImportTailwindVariants = (node, context) => { +} +const findNodeHasId = (node) => { + if (node.id) { + return node + } + + if (node.parent) { + return findNodeHasId(node.parent) + } + + return null +} +const findNodeUseMemo = (node) => { + if (node.type === 'CallExpression' && node.callee.name === 'useMemo') { + return node + } + + if (node.parent) { + return findNodeUseMemo(node.parent) + } + + return null +} + +/** + * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>} + */ +module.exports = { + meta: { + type: 'problem', + schema: SCHEMA, + }, + create(context) { + return { + ImportDeclaration: (node) => { + if (node.source.value === TV_COMPONENTS) { + if (!node.specifiers.some(findValidImportNameNode)) { + context.report({ + node, + message: `${TV_COMPONENTS} をimportする際は、名称が"${TV_COMPONENTS_METHOD}" となるようにしてください。例: "import { ${TV_COMPONENTS_METHOD} } from '${TV_COMPONENTS}'"`, + }); + } + } + }, + CallExpression: (node) => { + if (node.callee.name === TV_COMPONENTS_METHOD) { + const idNode = findNodeHasId(node.parent) + + if (idNode && !TV_RESULT_CONST_NAME_REGEX.test(idNode.id.name)) { + context.report({ + node: idNode, + message: `${TV_COMPONENTS_METHOD}の実行結果を格納する変数名は "${idNode.id.name}" ではなく "${TV_RESULT_CONST_NAME_REGEX}"にmatchする名称に統一してください。`, + }); + } + } else if (TV_RESULT_CONST_NAME_REGEX.test(node.callee.name)) { + const useMemoNode = findNodeUseMemo(node.parent) + + if (!useMemoNode) { + context.report({ + node, + message: `"${node.callee.name}" を実行する際、useMemoでラップし、メモ化してください`, + }); + } + } + }, + } + }, +} +module.exports.schema = SCHEMA diff --git a/packages/eslint-plugin-smarthr/test/best-practice-for-tailwind-variants.js b/packages/eslint-plugin-smarthr/test/best-practice-for-tailwind-variants.js new file mode 100644 index 00000000..50af8a50 --- /dev/null +++ b/packages/eslint-plugin-smarthr/test/best-practice-for-tailwind-variants.js @@ -0,0 +1,28 @@ +const rule = require('../rules/best-practice-for-tailwind-variants') +const RuleTester = require('eslint').RuleTester + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, +}) + +ruleTester.run('best-practice-for-button-element', rule, { + valid: [ + { code: `import { tv } from 'tailwind-variants'` }, + { code: `const styleGenerator = tv()` }, + { code: `const xxxStyleGenerator = tv()` }, + { code: `const hoge = useMemo(() => styleGenerator(), [])` }, + { code: `const xxx = useMemo(() => hogeStyleGenerator(), [])` }, + ], + invalid: [ + { code: `import { tv as hoge } from 'tailwind-variants'`, errors: [ { message: `tailwind-variants をimportする際は、名称が"tv" となるようにしてください。例: "import { tv } from 'tailwind-variants'"` } ] }, + { code: `const hoge = tv()`, errors: [ { message: `tvの実行結果を格納する変数名は "hoge" ではなく "/(S|s)tyleGenerator$/"にmatchする名称に統一してください。` } ] }, + { code: `const hoge = styleGenerator()`, errors: [ { message: `"styleGenerator" を実行する際、useMemoでラップし、メモ化してください` } ] }, + { code: `const hoge = hogeStyleGenerator()`, errors: [ { message: `"hogeStyleGenerator" を実行する際、useMemoでラップし、メモ化してください` } ] }, + ] +})