From 070271b3b39565405efb8181b6cd388230f0f2c6 Mon Sep 17 00:00:00 2001 From: AtsushiM Date: Fri, 10 Jan 2025 08:53:27 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20tailwind-variants=E3=81=AE=E4=BD=BF?= =?UTF-8?q?=E3=81=84=E6=96=B9=E3=82=92=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=83=AB=E3=83=BC=E3=83=AB=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/eslint-plugin-smarthr/README.md | 1 + .../README.md | 46 +++++++++++ .../index.js | 78 +++++++++++++++++++ .../best-practice-for-tailwind-variants.js | 28 +++++++ 4 files changed, 153 insertions(+) create mode 100644 packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants/README.md create mode 100644 packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants/index.js create mode 100644 packages/eslint-plugin-smarthr/test/best-practice-for-tailwind-variants.js 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でラップし、メモ化してください` } ] }, + ] +})