Repository: francoismassart/eslint-plugin-tailwindcss
Pull request #26 (2021-05-29 05:22:27)

Add support for custom utilities
# Add support for custom utilities

## Description

This PR adds support for recognizing [custom utilities](https://tailwindcss.com/docs/plugins#adding-utilities) as whitelist classnames for `no-custom-classname` rule.

**Only works on custom utilities and does not support other custom plugins**

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

`npm test`

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
- [x] I have checked my code and corrected any misspellings


zweimach: Please review and refine my small contribution to your preferences, as this is my first time working on ESLint plugins.

Patch:
diff --git a/lib/rules/no-custom-classname.js b/lib/rules/no-custom-classname.js
index 92afe15..3aec4bc 100644
--- a/lib/rules/no-custom-classname.js
+++ b/lib/rules/no-custom-classname.js
@@ -10,6 +10,7 @@ const customConfig = require('../util/customConfig');
 const astUtil = require('../util/ast');
 const attrUtil = require('../util/attr');
 const groupUtil = require('../util/groupMethods');
+const pluginUtil = require('../util/plugin');
 const removeDuplicatesFromArray = require('../util/removeDuplicatesFromArray');
 const getOption = require('../util/settings');
 
@@ -63,6 +64,8 @@ module.exports = {
 
     const mergedConfig = customConfig.resolve(twConfig);
 
+    const customPlugins = pluginUtil.getCustomPlugins(mergedConfig);
+
     //----------------------------------------------------------------------
     // Helpers
     //----------------------------------------------------------------------
@@ -80,7 +83,8 @@ module.exports = {
       classNames.forEach((className) => {
         const idx = groupUtil.getGroupIndex(className, groups, mergedConfig.separator);
         const whitelistIdx = groupUtil.getGroupIndex(className, whitelist, mergedConfig.separator);
-        if (idx === -1 && whitelistIdx === -1) {
+        const customPluginsIdx = groupUtil.getGroupIndex(className, customPlugins, mergedConfig.separator);
+        if (idx === -1 && whitelistIdx === -1 && customPluginsIdx === -1) {
           context.report({
             node,
             messageId: 'customClassnameDetected',
diff --git a/lib/util/groupMethods.js b/lib/util/groupMethods.js
index f54aceb..3c7ce33 100644
--- a/lib/util/groupMethods.js
+++ b/lib/util/groupMethods.js
@@ -41,7 +41,7 @@ function generateOptions(propName, keys, config, isNegative = false) {
     case 'ringOffsetColor':
       const options = [];
       keys.forEach((k) => {
-        const color = config.theme[propName]?.[k] || config.theme.colors[k];
+        const color = config.theme[propName][k] || config.theme.colors[k];
         if (typeof color === 'string') {
           options.push(escapeSpecialChars(k));
         } else {
diff --git a/lib/util/plugin.js b/lib/util/plugin.js
new file mode 100644
index 0000000..b3e95ce
--- /dev/null
+++ b/lib/util/plugin.js
@@ -0,0 +1,59 @@
+/**
+ * Remove dot prefix from custom classnames
+ *
+ * @param {string} className
+ * @returns {string}
+ */
+function removeDotPrefix(className) {
+  if (className[0] !== '.') return className;
+  return className.slice(1);
+}
+
+/**
+ * Apply Tailwind CSS classname prefix
+ *
+ * @param {string} prefix
+ * @returns {Function}
+ */
+function applyPrefix(prefix) {
+  return (className) => prefix + className;
+}
+
+/**
+ * A custom helper function to extract custom utilities
+ *
+ * @param {Object} utilities
+ * @returns {string[]}
+ */
+function addUtilities(utilities) {
+  return Object.keys(utilities);
+}
+
+/**
+ * Extract custom plugins
+ *
+ * @param {Object} config
+ * @returns {Function}
+ */
+function extractPlugins(config) {
+  // TODO Provide other helper functions from https://tailwindcss.com/docs/plugins
+  return (plugin) => plugin.handler({ addUtilities, prefix: config.prefix, config });
+}
+
+/**
+ * Generates an array of classnames from custom plugins
+ *
+ * @param {Object} config
+ * @returns {string[]}
+ */
+function getCustomPlugins(config) {
+  const customPlugins = config.plugins.flatMap(extractPlugins(config)).map(removeDotPrefix);
+  if (config.prefix.length) {
+    return customPlugins.map(applyPrefix(config.prefix));
+  }
+  return customPlugins;
+}
+
+module.exports = {
+  getCustomPlugins,
+};


Test patch:
diff --git a/tests/lib/rules/no-custom-classname.js b/tests/lib/rules/no-custom-classname.js
index f63e047..7965f37 100644
--- a/tests/lib/rules/no-custom-classname.js
+++ b/tests/lib/rules/no-custom-classname.js
@@ -10,6 +10,7 @@
 
 var rule = require("../../../lib/rules/no-custom-classname");
 var RuleTester = require("eslint").RuleTester;
+var plugin = require("tailwindcss/plugin");
 
 //------------------------------------------------------------------------------
 // Tests
@@ -115,6 +116,46 @@ ruleTester.run("no-custom-classname", rule, {
       <p class="text-indigo-500 group-hover:text-gray-500">Create a new project from a variety of starting templates.</p>
       </div>`,
     },
+    {
+      code: `<div class="w-full custom-utilities md:custom-utilities">with custom utilities</div>`,
+      options: [
+        {
+          config: {
+            plugins: [
+              plugin(({ addUtilities }) => {
+                const customUtilities = {
+                  ".custom-utilities": {
+                    "text-align": "center",
+                  },
+                };
+                return addUtilities(customUtilities, ["responsive"]);
+              }),
+            ],
+          },
+        },
+      ],
+    },
+    {
+      code: `<div class="tw-w-full tw-custom-utilities md_tw-custom-utilities">custom utilities with prefix</div>`,
+      options: [
+        {
+          config: {
+            prefix: "tw-",
+            separator: "_",
+            plugins: [
+              plugin(({ addUtilities }) => {
+                const customUtilities = {
+                  ".custom-utilities": {
+                    "text-align": "center",
+                  },
+                };
+                return addUtilities(customUtilities, ["responsive"]);
+              }),
+            ],
+          },
+        },
+      ],
+    },
   ],
 
   invalid: [