Repository: tailwindlabs/tailwindcss (ts)

Issue:
Support for closure colors in fill and stroke utilities
### Describe the problem:

The `fill` and `stroke` utilities do not currently support closure colors. This can cause an error if you're using the theme function to destructure the entire color configuration.

I believe the color functions are only called by the `withAlphaVariable` utility and since we don't yet have `fill-opacity-` or `stroke-opacity-` utils the color function doesn't get a chance to be called.

https://github.com/tailwindlabs/tailwindcss/blob/0feb064054b6566589e0e5b1a4d469976606be70/src/util/withAlphaVariable.js#L22

### Link to a minimal reproduction:

Currently throws an error: https://play.tailwindcss.com/KpLmGtRf8t?file=config

Destructuring in the entire color config will throw the same error if there are any closure colors:  https://play.tailwindcss.com/pNL2hSFBbc?file=config


Pull request description:
Support closures for colors in all circumstances
Fixes #2535.

Patch:
diff --git a/src/plugins/backgroundColor.js b/src/plugins/backgroundColor.js
index 2b129e94..352ca8f1 100644
--- a/src/plugins/backgroundColor.js
+++ b/src/plugins/backgroundColor.js
@@ -1,6 +1,7 @@
 import _ from 'lodash'
 import flattenColorPalette from '../util/flattenColorPalette'
 import withAlphaVariable from '../util/withAlphaVariable'
+import toColorValue from '../util/toColorValue'
 
 export default function() {
   return function({ addUtilities, e, theme, variants, target, corePlugins }) {
@@ -8,7 +9,7 @@ export default function() {
 
     const getProperties = value => {
       if (target('backgroundColor') === 'ie11') {
-        return { 'background-color': value }
+        return { 'background-color': toColorValue(value) }
       }
 
       if (corePlugins('backgroundOpacity')) {
@@ -19,7 +20,7 @@ export default function() {
         })
       }
 
-      return { 'background-color': value }
+      return { 'background-color': toColorValue(value) }
     }
 
     const utilities = _.fromPairs(
diff --git a/src/plugins/borderColor.js b/src/plugins/borderColor.js
index ac855c38..4ab1d5b4 100644
--- a/src/plugins/borderColor.js
+++ b/src/plugins/borderColor.js
@@ -1,5 +1,6 @@
 import _ from 'lodash'
 import flattenColorPalette from '../util/flattenColorPalette'
+import toColorValue from '../util/toColorValue'
 import withAlphaVariable from '../util/withAlphaVariable'
 
 export default function() {
@@ -8,7 +9,7 @@ export default function() {
 
     const getProperties = value => {
       if (target('borderColor') === 'ie11') {
-        return { 'border-color': value }
+        return { 'border-color': toColorValue(value) }
       }
 
       if (corePlugins('borderOpacity')) {
@@ -19,7 +20,7 @@ export default function() {
         })
       }
 
-      return { 'border-color': value }
+      return { 'border-color': toColorValue(value) }
     }
 
     const utilities = _.fromPairs(
diff --git a/src/plugins/divideColor.js b/src/plugins/divideColor.js
index 5b8922ab..e5f32998 100644
--- a/src/plugins/divideColor.js
+++ b/src/plugins/divideColor.js
@@ -1,5 +1,6 @@
 import _ from 'lodash'
 import flattenColorPalette from '../util/flattenColorPalette'
+import toColorValue from '../util/toColorValue'
 import withAlphaVariable from '../util/withAlphaVariable'
 
 export default function() {
@@ -8,7 +9,7 @@ export default function() {
 
     const getProperties = value => {
       if (target('divideColor') === 'ie11') {
-        return { 'border-color': value }
+        return { 'border-color': toColorValue(value) }
       }
 
       if (corePlugins('divideOpacity')) {
@@ -19,7 +20,7 @@ export default function() {
         })
       }
 
-      return { 'border-color': value }
+      return { 'border-color': toColorValue(value) }
     }
 
     const utilities = _.fromPairs(
diff --git a/src/plugins/fill.js b/src/plugins/fill.js
index ce0d208b..84bd170c 100644
--- a/src/plugins/fill.js
+++ b/src/plugins/fill.js
@@ -1,5 +1,6 @@
 import _ from 'lodash'
 import flattenColorPalette from '../util/flattenColorPalette'
+import toColorValue from '../util/toColorValue'
 
 export default function() {
   return function({ addUtilities, e, theme, variants }) {
@@ -7,7 +8,7 @@ export default function() {
 
     const utilities = _.fromPairs(
       _.map(colors, (value, modifier) => {
-        return [`.${e(`fill-${modifier}`)}`, { fill: value }]
+        return [`.${e(`fill-${modifier}`)}`, { fill: toColorValue(value) }]
       })
     )
 
diff --git a/src/plugins/gradientColorStops.js b/src/plugins/gradientColorStops.js
index b333dce3..1a167f92 100644
--- a/src/plugins/gradientColorStops.js
+++ b/src/plugins/gradientColorStops.js
@@ -1,5 +1,6 @@
 import _ from 'lodash'
 import flattenColorPalette from '../util/flattenColorPalette'
+import toColorValue from '../util/toColorValue'
 import { toRgba } from '../util/withAlphaVariable'
 
 export default function() {
@@ -12,14 +13,6 @@ export default function() {
 
     const utilities = _(colors)
       .map((value, modifier) => {
-        const getColorValue = color => {
-          if (_.isFunction(color)) {
-            return value({})
-          }
-
-          return color
-        }
-
         const transparentTo = (() => {
           if (_.isFunction(value)) {
             return value({ opacityValue: 0 })
@@ -37,21 +30,21 @@ export default function() {
           [
             `.${e(`from-${modifier}`)}`,
             {
-              '--gradient-from-color': getColorValue(value, 'from'),
+              '--gradient-from-color': toColorValue(value, 'from'),
               '--gradient-color-stops': `var(--gradient-from-color), var(--gradient-to-color, ${transparentTo})`,
             },
           ],
           [
             `.${e(`via-${modifier}`)}`,
             {
-              '--gradient-via-color': getColorValue(value, 'via'),
+              '--gradient-via-color': toColorValue(value, 'via'),
               '--gradient-color-stops': `var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, ${transparentTo})`,
             },
           ],
           [
             `.${e(`to-${modifier}`)}`,
             {
-              '--gradient-to-color': getColorValue(value, 'to'),
+              '--gradient-to-color': toColorValue(value, 'to'),
             },
           ],
         ]
diff --git a/src/plugins/placeholderColor.js b/src/plugins/placeholderColor.js
index fea66ba4..256f2d94 100644
--- a/src/plugins/placeholderColor.js
+++ b/src/plugins/placeholderColor.js
@@ -1,5 +1,6 @@
 import _ from 'lodash'
 import flattenColorPalette from '../util/flattenColorPalette'
+import toColorValue from '../util/toColorValue'
 import withAlphaVariable from '../util/withAlphaVariable'
 
 export default function() {
@@ -8,7 +9,7 @@ export default function() {
 
     const getProperties = value => {
       if (target('placeholderColor') === 'ie11') {
-        return { color: value }
+        return { color: toColorValue(value) }
       }
 
       if (corePlugins('placeholderOpacity')) {
@@ -19,7 +20,7 @@ export default function() {
         })
       }
 
-      return { color: value }
+      return { color: toColorValue(value) }
     }
 
     const utilities = _.fromPairs(
diff --git a/src/plugins/stroke.js b/src/plugins/stroke.js
index 1944a84e..43ab8fc6 100644
--- a/src/plugins/stroke.js
+++ b/src/plugins/stroke.js
@@ -1,5 +1,6 @@
 import _ from 'lodash'
 import flattenColorPalette from '../util/flattenColorPalette'
+import toColorValue from '../util/toColorValue'
 
 export default function() {
   return function({ addUtilities, e, theme, variants }) {
@@ -7,7 +8,7 @@ export default function() {
 
     const utilities = _.fromPairs(
       _.map(colors, (value, modifier) => {
-        return [`.${e(`stroke-${modifier}`)}`, { stroke: value }]
+        return [`.${e(`stroke-${modifier}`)}`, { stroke: toColorValue(value) }]
       })
     )
 
diff --git a/src/plugins/textColor.js b/src/plugins/textColor.js
index f52c27dc..d6417fcb 100644
--- a/src/plugins/textColor.js
+++ b/src/plugins/textColor.js
@@ -1,5 +1,6 @@
 import _ from 'lodash'
 import flattenColorPalette from '../util/flattenColorPalette'
+import toColorValue from '../util/toColorValue'
 import withAlphaVariable from '../util/withAlphaVariable'
 
 export default function() {
@@ -8,7 +9,7 @@ export default function() {
 
     const getProperties = value => {
       if (target('textColor') === 'ie11') {
-        return { color: value }
+        return { color: toColorValue(value) }
       }
 
       if (corePlugins('textOpacity')) {
@@ -19,7 +20,7 @@ export default function() {
         })
       }
 
-      return { color: value }
+      return { color: toColorValue(value) }
     }
 
     const utilities = _.fromPairs(
diff --git a/src/util/toColorValue.js b/src/util/toColorValue.js
new file mode 100644
index 00000000..a721d2c8
--- /dev/null
+++ b/src/util/toColorValue.js
@@ -0,0 +1,5 @@
+import _ from 'lodash'
+
+export default function toColorValue(maybeFunction) {
+  return _.isFunction(maybeFunction) ? maybeFunction({}) : maybeFunction
+}


Test patch:
diff --git a/__tests__/plugins/backgroundColor.test.js b/__tests__/plugins/backgroundColor.test.js
new file mode 100644
index 00000000..41fe51a6
--- /dev/null
+++ b/__tests__/plugins/backgroundColor.test.js
@@ -0,0 +1,56 @@
+import invokePlugin from '../util/invokePlugin'
+import plugin from '../../src/plugins/backgroundColor'
+
+test('defining color as a function', () => {
+  const config = {
+    target: 'relaxed',
+    theme: {
+      backgroundColor: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      backgroundColor: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.bg-black': {
+          'background-color': 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
+
+test('defining color as a function in ie11 mode', () => {
+  const config = {
+    target: 'ie11',
+    theme: {
+      backgroundColor: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      backgroundColor: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.bg-black': {
+          'background-color': 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
diff --git a/__tests__/plugins/borderColor.test.js b/__tests__/plugins/borderColor.test.js
new file mode 100644
index 00000000..b3efa95a
--- /dev/null
+++ b/__tests__/plugins/borderColor.test.js
@@ -0,0 +1,56 @@
+import invokePlugin from '../util/invokePlugin'
+import plugin from '../../src/plugins/borderColor'
+
+test('defining color as a function', () => {
+  const config = {
+    target: 'relaxed',
+    theme: {
+      borderColor: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      borderColor: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.border-black': {
+          'border-color': 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
+
+test('defining color as a function in ie11 mode', () => {
+  const config = {
+    target: 'ie11',
+    theme: {
+      borderColor: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      borderColor: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.border-black': {
+          'border-color': 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
diff --git a/__tests__/plugins/divideColor.test.js b/__tests__/plugins/divideColor.test.js
new file mode 100644
index 00000000..d09609fb
--- /dev/null
+++ b/__tests__/plugins/divideColor.test.js
@@ -0,0 +1,56 @@
+import invokePlugin from '../util/invokePlugin'
+import plugin from '../../src/plugins/divideColor'
+
+test('defining color as a function', () => {
+  const config = {
+    target: 'relaxed',
+    theme: {
+      divideColor: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      divideColor: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.divide-black > :not(template) ~ :not(template)': {
+          'border-color': 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
+
+test('defining color as a function in ie11 mode', () => {
+  const config = {
+    target: 'ie11',
+    theme: {
+      divideColor: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      divideColor: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.divide-black > :not(template) ~ :not(template)': {
+          'border-color': 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
diff --git a/__tests__/plugins/fill.test.js b/__tests__/plugins/fill.test.js
new file mode 100644
index 00000000..8093d1b7
--- /dev/null
+++ b/__tests__/plugins/fill.test.js
@@ -0,0 +1,56 @@
+import invokePlugin from '../util/invokePlugin'
+import plugin from '../../src/plugins/fill'
+
+test('defining color as a function', () => {
+  const config = {
+    target: 'relaxed',
+    theme: {
+      fill: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      fill: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.fill-black': {
+          fill: 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
+
+test('defining color as a function in ie11 mode', () => {
+  const config = {
+    target: 'ie11',
+    theme: {
+      fill: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      fill: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.fill-black': {
+          fill: 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
diff --git a/__tests__/plugins/placeholderColor.test.js b/__tests__/plugins/placeholderColor.test.js
new file mode 100644
index 00000000..513222f8
--- /dev/null
+++ b/__tests__/plugins/placeholderColor.test.js
@@ -0,0 +1,56 @@
+import invokePlugin from '../util/invokePlugin'
+import plugin from '../../src/plugins/placeholderColor'
+
+test('defining color as a function', () => {
+  const config = {
+    target: 'relaxed',
+    theme: {
+      placeholderColor: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      placeholderColor: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.placeholder-black::placeholder': {
+          color: 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
+
+test('defining color as a function in ie11 mode', () => {
+  const config = {
+    target: 'ie11',
+    theme: {
+      placeholderColor: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      placeholderColor: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.placeholder-black::placeholder': {
+          color: 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
diff --git a/__tests__/plugins/stroke.test.js b/__tests__/plugins/stroke.test.js
new file mode 100644
index 00000000..b3091ea0
--- /dev/null
+++ b/__tests__/plugins/stroke.test.js
@@ -0,0 +1,56 @@
+import invokePlugin from '../util/invokePlugin'
+import plugin from '../../src/plugins/stroke'
+
+test('defining color as a function', () => {
+  const config = {
+    target: 'relaxed',
+    theme: {
+      stroke: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      stroke: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.stroke-black': {
+          stroke: 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
+
+test('defining color as a function in ie11 mode', () => {
+  const config = {
+    target: 'ie11',
+    theme: {
+      stroke: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      stroke: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.stroke-black': {
+          stroke: 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
diff --git a/__tests__/plugins/textColor.test.js b/__tests__/plugins/textColor.test.js
new file mode 100644
index 00000000..2bb0fb12
--- /dev/null
+++ b/__tests__/plugins/textColor.test.js
@@ -0,0 +1,56 @@
+import invokePlugin from '../util/invokePlugin'
+import plugin from '../../src/plugins/textColor'
+
+test('defining color as a function', () => {
+  const config = {
+    target: 'relaxed',
+    theme: {
+      textColor: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      textColor: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.text-black': {
+          color: 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
+
+test('defining color as a function in ie11 mode', () => {
+  const config = {
+    target: 'ie11',
+    theme: {
+      textColor: {
+        black: ({ opacityVariable: _ }) => 'black',
+      },
+    },
+    variants: {
+      textColor: [],
+    },
+  }
+
+  const { utilities } = invokePlugin(plugin(), config)
+
+  expect(utilities).toEqual([
+    [
+      {
+        '.text-black': {
+          color: 'black',
+        },
+      },
+      [],
+    ],
+  ])
+})
diff --git a/__tests__/util/invokePlugin.js b/__tests__/util/invokePlugin.js
index 7be75212..e2569c6a 100644
--- a/__tests__/util/invokePlugin.js
+++ b/__tests__/util/invokePlugin.js
@@ -28,6 +28,17 @@ export default function(plugin, config) {
     addUtilities(utilities, variants) {
       addedUtilities.push([utilities, variants])
     },
+    corePlugins(corePlugin) {
+      if (config.corePlugins === undefined) {
+        return false
+      }
+
+      if (Array.isArray(config.corePlugins)) {
+        return config.corePlugins.includes(corePlugin)
+      }
+
+      return config.corePlugins[corePlugin] !== false
+    },
   }
 
   plugin(pluginApi)