Repository: tailwindlabs/tailwindcss (TypeScript)
Date: 2019-03-16 20:32:00+00:00
Task type: Bug Fix

Issue:
Replacing all colors breaks the build
This bug occured while beta testing the new v1 config, it's not an issue in the stable release.

With the following config:

```js
module.exports = {
  theme: {
    colors: {
      blue: "#084062",
      red: "#ee585f",
      paper: "#f5f2ea"
    }
  }
};
```

The build breaks because it expects a specific shade of grey to exist:

https://github.com/tailwindcss/tailwindcss/blob/v1.0.0-beta.1/stubs/defaultConfig.stub.js#L241

```
TypeError: Cannot read property '700' of undefined
    at Array.reduce (<anonymous>)
    at new Promise (<anonymous>)
```

Currently working around the issue by extending the colors instead.


Issue discussion:
You can work around this by explicitly defining `borderColor` in your theme.

```
module.exports = {
  theme: {
    colors: {
      blue: "#084062",
      red: "#ee585f",
      paper: "#f5f2ea"
    },
    borderColor: "#333",
  }
}
```
Trying to think through the best way to solve this problem at a general level, I wonder if it would make more sense for closures to receive a theme _function_ instead of a theme _object_, something that behaves just like the theme function we make available in CSS or the config function we pass to plugins, where you can use dot notation and provide a default?

Then our definition for borderColor could be something like this:

```js
borderColor: theme => {
  return global.Object.assign({
    default: theme('colors.gray.700', 'currentColor')
  }, theme.colors)
},
```


On the one hand, I like an object because it opens more doors for potential auto-completion, on the other hand for cases like this a function seems to make sense. You could support both, but not sure if it's worth it to have two ways to do the same thing 🤷‍♂️ 
What if the `theme` passed to these closures is a `theme` object with added `get` property that provides this functionality.

```js
borderColor: theme => Object.assign(
  { default: theme.get('colors.gray.700', 'currentColor') }, 
  theme.colors
)
```


Yeah not a bad option either 👍 I like the idea of a function because it's consistent with other areas in the framework, but it's sort of annoying we need to do anything at all just to handle this one weird default. I also like that with the object you can destructure it.

One easy option is just relying on lodash inside the default config, but that means when we let the user generate a full config they'd have lodash as a dependency in there which sucks.

Wondering if there are other options that could let us just avoid the problem altogether (hardcoding the default border color for example instead of making it dependent), or if there would be value to making it possible to do stuff like `theme('foo.bar', 'default')` for the end-user's benefit.
The other thing I like about just using a function is it makes this mistake a little more impossible — you'll always get _some_ kind of value instead of an error and it's more obvious you should provide a default in case the key you are looking for doesn't exist. Going to work on a PR for that now.

Solution patch:
diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js
--- a/src/util/resolveConfig.js
+++ b/src/util/resolveConfig.js
@@ -2,12 +2,15 @@ import mergeWith from 'lodash/mergeWith'
 import isFunction from 'lodash/isFunction'
 import defaults from 'lodash/defaults'
 import map from 'lodash/map'
+import get from 'lodash/get'
 
 function resolveFunctionKeys(object) {
+  const getKey = (key, defaultValue) => get(object, key, defaultValue)
+
   return Object.keys(object).reduce((resolved, key) => {
     return {
       ...resolved,
-      [key]: isFunction(object[key]) ? object[key](object) : object[key],
+      [key]: isFunction(object[key]) ? object[key](getKey) : object[key],
     }
   }, {})
 }
diff --git a/stubs/defaultConfig.stub.js b/stubs/defaultConfig.stub.js
--- a/stubs/defaultConfig.stub.js
+++ b/stubs/defaultConfig.stub.js
@@ -212,8 +212,8 @@ module.exports = {
       wider: '.05em',
       widest: '.1em',
     },
-    textColor: theme => theme.colors,
-    backgroundColor: theme => theme.colors,
+    textColor: theme => theme('colors'),
+    backgroundColor: theme => theme('colors'),
     backgroundPosition: {
       bottom: 'bottom',
       center: 'center',
@@ -238,7 +238,7 @@ module.exports = {
       '8': '8px',
     },
     borderColor: theme => {
-      return global.Object.assign({ default: theme.colors.gray[700] }, theme.colors)
+      return global.Object.assign({ default: theme('colors.gray.700', 'currentColor') }, theme('colors'))
     },
     borderRadius: {
       none: '0',
@@ -257,7 +257,7 @@ module.exports = {
     },
     width: theme => ({
       auto: 'auto',
-      ...theme.spacing,
+      ...theme('spacing'),
       '1/2': '50%',
       '1/3': '33.33333%',
       '2/3': '66.66667%',
@@ -274,7 +274,7 @@ module.exports = {
     }),
     height: theme => ({
       auto: 'auto',
-      ...theme.spacing,
+      ...theme('spacing'),
       full: '100%',
       screen: '100vh',
     }),
@@ -304,9 +304,9 @@ module.exports = {
       full: '100%',
       screen: '100vh',
     },
-    padding: theme => theme.spacing,
-    margin: theme => ({ auto: 'auto', ...theme.spacing }),
-    negativeMargin: theme => theme.spacing,
+    padding: theme => theme('spacing'),
+    margin: theme => ({ auto: 'auto', ...theme('spacing') }),
+    negativeMargin: theme => theme('spacing'),
     objectPosition: {
       bottom: 'bottom',
       center: 'center',


Test patch:
diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js
--- a/__tests__/resolveConfig.test.js
+++ b/__tests__/resolveConfig.test.js
@@ -322,8 +322,8 @@ test('functions in the default theme section are lazily evaluated', () => {
         magenta: 'magenta',
         yellow: 'yellow',
       },
-      backgroundColors: ({ colors }) => colors,
-      textColors: ({ colors }) => colors,
+      backgroundColors: theme => theme('colors'),
+      textColors: theme => theme('colors'),
     },
     variants: {
       backgroundColors: ['responsive', 'hover', 'focus'],
@@ -369,12 +369,12 @@ test('functions in the user theme section are lazily evaluated', () => {
         green: 'green',
         blue: 'blue',
       },
-      backgroundColors: ({ colors }) => ({
-        ...colors,
+      backgroundColors: theme => ({
+        ...theme('colors'),
         customBackground: '#bada55',
       }),
-      textColors: ({ colors }) => ({
-        ...colors,
+      textColors: theme => ({
+        ...theme('colors'),
         customText: '#facade',
       }),
     },
@@ -461,7 +461,7 @@ test('theme values in the extend section extend the existing theme', () => {
         '50': '.5',
         '100': '1',
       },
-      backgroundColors: ({ colors }) => colors,
+      backgroundColors: theme => theme('colors'),
     },
     variants: {
       backgroundColors: ['responsive', 'hover', 'focus'],
@@ -510,7 +510,7 @@ test('theme values in the extend section extend the user theme', () => {
         '20': '.2',
         '40': '.4',
       },
-      height: theme => theme.width,
+      height: theme => theme('width'),
       extend: {
         opacity: {
           '60': '.6',
@@ -618,7 +618,7 @@ test('theme values in the extend section can extend values that are depended on
         magenta: 'magenta',
         yellow: 'yellow',
       },
-      backgroundColors: ({ colors }) => colors,
+      backgroundColors: theme => theme('colors'),
     },
     variants: {
       backgroundColors: ['responsive', 'hover', 'focus'],
@@ -701,3 +701,59 @@ test('theme values in the extend section are not deeply merged', () => {
     },
   })
 })
+
+test('the theme function can use a default value if the key is missing', () => {
+  const userConfig = {
+    theme: {
+      colors: {
+        red: 'red',
+        green: 'green',
+        blue: 'blue',
+      },
+    },
+  }
+
+  const defaultConfig = {
+    prefix: '-',
+    important: false,
+    separator: ':',
+    theme: {
+      colors: {
+        cyan: 'cyan',
+        magenta: 'magenta',
+        yellow: 'yellow',
+      },
+      borderColor: theme => ({
+        default: theme('colors.gray', 'currentColor'),
+        ...theme('colors'),
+      }),
+    },
+    variants: {
+      borderColor: ['responsive', 'hover', 'focus'],
+    },
+  }
+
+  const result = resolveConfig([userConfig, defaultConfig])
+
+  expect(result).toEqual({
+    prefix: '-',
+    important: false,
+    separator: ':',
+    theme: {
+      colors: {
+        red: 'red',
+        green: 'green',
+        blue: 'blue',
+      },
+      borderColor: {
+        default: 'currentColor',
+        red: 'red',
+        green: 'green',
+        blue: 'blue',
+      },
+    },
+    variants: {
+      borderColor: ['responsive', 'hover', 'focus'],
+    },
+  })
+})