diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index b63a4831c..1a44a54b1 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1234,3 +1234,133 @@ test('custom properties are multiplied by -1 for negative values', () => { variants: {}, }) }) + +test('more than two config objects can be resolved', () => { + const firstConfig = { + theme: { + extend: { + fontFamily: () => ({ + code: ['Menlo', 'monospace'], + }), + colors: { + red: 'red', + }, + backgroundColor: { + customBackgroundOne: '#bada55', + }, + textDecorationColor: { + orange: 'orange' + } + }, + }, + } + + const secondConfig = { + prefix: '-', + important: false, + separator: ':', + theme: { + extend: { + fontFamily: { + quote: ['Helvetica', 'serif'], + }, + colors: { + green: 'green', + }, + backgroundColor: { + customBackgroundTwo: '#facade', + }, + textDecorationColor: theme => theme('colors') + }, + }, + } + + const thirdConfig = { + prefix: '-', + important: false, + separator: ':', + theme: { + extend: { + fontFamily: { + hero: ['Futura', 'sans-serif'], + }, + colors: { + pink: 'pink', + }, + backgroundColor: () => ({ + customBackgroundThree: '#c0ffee', + }), + textDecorationColor: { + lime: 'lime', + } + }, + }, + } + + const defaultConfig = { + prefix: '-', + important: false, + separator: ':', + theme: { + fontFamily: { + body: ['Arial', 'sans-serif'], + display: ['Georgia', 'serif'], + }, + colors: { + blue: 'blue', + }, + backgroundColor: theme => theme('colors'), + }, + variants: { + backgroundColor: ['responsive', 'hover', 'focus'], + }, + } + + const result = resolveConfig([ + firstConfig, + secondConfig, + thirdConfig, + defaultConfig + ]) + + expect(result).toEqual({ + prefix: '-', + important: false, + separator: ':', + theme: { + fontFamily: { + body: ['Arial', 'sans-serif'], + display: ['Georgia', 'serif'], + code: ['Menlo', 'monospace'], + quote: ['Helvetica', 'serif'], + hero: ['Futura', 'sans-serif'], + }, + colors: { + red: 'red', + green: 'green', + blue: 'blue', + pink: 'pink', + }, + backgroundColor: { + red: 'red', + green: 'green', + blue: 'blue', + pink: 'pink', + customBackgroundOne: '#bada55', + customBackgroundTwo: '#facade', + customBackgroundThree: '#c0ffee', + }, + textDecorationColor: { + red: 'red', + green: 'green', + blue: 'blue', + pink: 'pink', + orange: 'orange', + lime: 'lime', + }, + }, + variants: { + backgroundColor: ['responsive', 'hover', 'focus'], + }, + }) +}) \ No newline at end of file diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index 41e41526d..9d5274525 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -1,7 +1,11 @@ +import some from 'lodash/some' import mergeWith from 'lodash/mergeWith' +import assignWith from 'lodash/assignWith' import isFunction from 'lodash/isFunction' +import isUndefined from 'lodash/isUndefined' import defaults from 'lodash/defaults' import map from 'lodash/map' +import reduce from 'lodash/reduce' import toPath from 'lodash/toPath' import negateValue from './negateValue' @@ -23,18 +27,46 @@ function value(valueToResolve, ...args) { return isFunction(valueToResolve) ? valueToResolve(...args) : valueToResolve } +function mergeThemes(themes) { + const theme = (({ extend, ...t }) => t)(themes.reduce((merged, t) => { + return defaults(merged, t) + }, {})) + + // In order to resolve n config objects, we combine all of their `extend` properties + // into arrays instead of objects so they aren't overridden. + const extend = themes.reduce((merged, { extend }) => { + return mergeWith(merged, extend, (mergedValue, extendValue) => { + if (isUndefined(mergedValue)) { + return [extendValue] + } + + if (Array.isArray(mergedValue)) { + return [...mergedValue, extendValue] + } + + return [mergedValue, extendValue] + }) + }, {}) + + return { + ...theme, + extend, + } +} + function mergeExtensions({ extend, ...theme }) { return mergeWith(theme, extend, (themeValue, extensions) => { - if (!isFunction(themeValue) && !isFunction(extensions)) { + // The `extend` property is an array, so we need to check if it contains any functions + if (!isFunction(themeValue) && !some(extensions, isFunction)) { return { ...themeValue, - ...extensions, + ...Object.assign({}, ...extensions), } } return (resolveThemePath, utils) => ({ ...value(themeValue, resolveThemePath, utils), - ...value(extensions, resolveThemePath, utils), + ...Object.assign({}, ...extensions.map(e => value(e, resolveThemePath, utils))), }) }) } @@ -65,7 +97,7 @@ function resolveFunctionKeys(object) { export default function resolveConfig(configs) { return defaults( { - theme: resolveFunctionKeys(mergeExtensions(defaults({}, ...map(configs, 'theme')))), + theme: resolveFunctionKeys(mergeExtensions(mergeThemes(map(configs, 'theme')))), variants: (firstVariants => { return Array.isArray(firstVariants) ? firstVariants