diff --git a/README.md b/README.md index 80462138..4a3b4279 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Learn more about each supported rules by reading their documentation: - [`no-arbitrary-value`](docs/rules/no-arbitrary-value.md): forbid using arbitrary values in classnames (turned off by default) - [`no-custom-classname`](docs/rules/no-custom-classname.md): only allow classnames from Tailwind CSS and the values from the `whitelist` option - [`no-contradicting-classname`](docs/rules/no-contradicting-classname.md): e.g. avoid `p-2 p-3`, different Tailwind CSS classnames (`pt-2` & `pt-3`) but targeting the same property several times for the same variant. +- [`no-string-interpolation`](docs/rules/no-string-interpolation.md): forbid dynamic Tailwind class construction - [`no-unnecessary-arbitrary-value`](docs/rules/no-unnecessary-arbitrary-value.md): e.g. replacing `m-[1.25rem]` by its configuration based classname `m-5` Using ESLint extension for Visual Studio Code, you will get these messages @@ -175,7 +176,6 @@ Our recommendations: #### For `eslint.config.js` - For `js[x]`, `ts[x]`: - - Install the parser: `npm i -D @eslint/js typescript-eslint` - Assign it to your files in `eslint.config.js`: @@ -196,7 +196,6 @@ Our recommendations: ``` - For `vue.js`: - - Install the parser: `npm i -D eslint-plugin-vue` - Assign it to your files in `eslint.config.js`: @@ -315,6 +314,5 @@ The plugin will look for each setting in this order and stops searching as soon - `no-redundant-variant`: e.g. avoid `mx-5 sm:mx-5`, no need to redefine `mx` in `sm:` variant as it uses the same value (`5`) - `only-valid-arbitrary-values`: - - e.g. avoid `top-[42]`, only `0` value can be unitless. - e.g. avoid `text-[rgba(10%,20%,30,50%)]`, can't mix `%` and `0-255`. diff --git a/docs/rules/no-string-interpolation.md b/docs/rules/no-string-interpolation.md new file mode 100644 index 00000000..fe8c5602 --- /dev/null +++ b/docs/rules/no-string-interpolation.md @@ -0,0 +1,32 @@ +# Disallow dynamic Tailwind class construction (no-string-interpolation) + +Detect whether invalid string interpolation is used to generate Tailwind classnames. + +## Rule Details + +This rule reports use of string interpolation in class attributes when the interpolated value is part of the class name string (e.g. `bg-${color}`). +Tailwind CSS scans your source code for class names. If you use string interpolation to construct class names, Tailwind CSS will not find them and will not generate the corresponding CSS. + +Examples of **incorrect** code for this rule: + +```jsx +
+ +``` + +Examples of **correct** code for this rule: + +```jsx +// Use full class names + + +// Or use a library like `classnames` or `clsx` (if configured) + + +// Interpolation is allowed if it's not part of a class name construction + +``` + +## Further Reading + +- [Tailwind CSS Documentation - Dynamic class names](https://tailwindcss.com/docs/content-configuration#dynamic-class-names) diff --git a/lib/index.js b/lib/index.js index 53824e76..681117c6 100644 --- a/lib/index.js +++ b/lib/index.js @@ -15,6 +15,7 @@ module.exports = { 'classnames-order': require(base + 'classnames-order'), 'enforces-negative-arbitrary-values': require(base + 'enforces-negative-arbitrary-values'), 'enforces-shorthand': require(base + 'enforces-shorthand'), + 'no-string-interpolation': require(base + 'no-string-interpolation'), 'migration-from-tailwind-2': require(base + 'migration-from-tailwind-2'), 'no-arbitrary-value': require(base + 'no-arbitrary-value'), 'no-contradicting-classname': require(base + 'no-contradicting-classname'), diff --git a/lib/rules/no-string-interpolation.js b/lib/rules/no-string-interpolation.js new file mode 100644 index 00000000..9b17c84f --- /dev/null +++ b/lib/rules/no-string-interpolation.js @@ -0,0 +1,74 @@ +/** + * @fileoverview Detect whether invalid string interpolation is used to generate Tailwind classnames. + * @author Angelos Athanasakos + */ +'use strict'; + + +const docsUrl = require('../util/docsUrl'); + + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +// Predefine message for use in context.report conditional. +// messageId will still be usable in tests. +const INVALID_STRING_INTERPOLATION_MESSAGE = 'Dynamic Tailwind class construction is not allowed. Use conditional classnames instead.'; + +module.exports = { + meta: { + type: 'problem', + category: 'Best Practices', + recommended: false, + url: docsUrl('no-string-interpolation'), + docs: { + description: 'Disallow dynamic Tailwind class construction inside class attributes', + }, + schema: [], + messages: { + noInterpolation: INVALID_STRING_INTERPOLATION_MESSAGE, + }, + }, + + create(context) { + function isClassAttribute(name) { + return name === 'class' || name === 'className'; + } + function hasDynamicTailwindConstruction(templateLiteral) { + const { quasis, expressions } = templateLiteral; + + if (!expressions.length) return false; + + for (let i = 0; i < expressions.length; i++) { + const before = quasis[i].value.raw; + const after = quasis[i + 1].value.raw; + + const beforeEndsWithSpace = /\s$/.test(before); + const afterStartsWithSpace = /^\s/.test(after); + + if (!beforeEndsWithSpace && before !== '') return true; + if (!afterStartsWithSpace && after !== '') return true; + } + + return false; + } + + return { + JSXAttribute(node) { + const name = node.name?.name; + if (!isClassAttribute(name)) return; + if (!node.value) return; + + if (node.value.type === 'JSXExpressionContainer' && node.value.expression.type === 'TemplateLiteral') { + if (hasDynamicTailwindConstruction(node.value.expression)) { + context.report({ + node, + messageId: 'noInterpolation', + }); + } + } + }, + }; + }, +}; diff --git a/tests/lib/rules/no-string-interpolation.js b/tests/lib/rules/no-string-interpolation.js new file mode 100644 index 00000000..65fe43eb --- /dev/null +++ b/tests/lib/rules/no-string-interpolation.js @@ -0,0 +1,96 @@ +/** + * @fileoverview Disallow dynamic Tailwind class construction inside class attributes + * @author Angelos Athanasakos + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var { RuleTester } = require("eslint"); +var rule = require("../../../lib/rules/no-string-interpolation"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +var RuleTester = require("eslint").RuleTester; + +var parserOptions = { + ecmaVersion: 2019, + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, +}; + +var ruleTester = new RuleTester({ parserOptions }); + +ruleTester.run("no-string-interpolation", rule, { + valid: [ + { + code: ``, + }, + + { + code: ``, + }, + + { + code: ``, + }, + + { + code: ``, + }, + + { + code: ``, + }, + + { + code: ` + + `, + }, + + { + code: ` + const Spinner = ({ circlesClassName = 'bg-primary', size }) => ( + <> +