From e7b831fc09b1b78c5d7661563dbf6d9da4bbfa98 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 11 Oct 2019 17:48:52 -0400 Subject: [PATCH 01/15] Allow resolving an arbitrary number of stacked config files --- __tests__/resolveConfig.test.js | 130 ++++++++++++++++++++++++++++++++ src/util/resolveConfig.js | 40 +++++++++- 2 files changed, 166 insertions(+), 4 deletions(-) diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index b63a4831c736..1a44a54b17d5 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 41e41526d01e..9d5274525b00 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 From eb24d5067c8dca7c06badeeaec29f95e6a8044e9 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sat, 12 Oct 2019 12:53:32 -0400 Subject: [PATCH 02/15] Fix style --- .eslintrc | 1 + __tests__/resolveConfig.test.js | 17 ++++------ src/lib/substituteTailwindAtRules.js | 2 +- src/util/resolveConfig.js | 47 +++++++++++++++------------- 4 files changed, 33 insertions(+), 34 deletions(-) diff --git a/.eslintrc b/.eslintrc index 238612fbddb7..10b89fb05a07 100644 --- a/.eslintrc +++ b/.eslintrc @@ -9,6 +9,7 @@ "extends": ["eslint-config-postcss", "prettier"], "plugins": ["prettier"], "rules": { + "no-unused-vars": [2, {"args": "all", "argsIgnorePattern": "^_"}], "prettier/prettier": [ "error", { diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index 1a44a54b17d5..927ed233848b 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1249,8 +1249,8 @@ test('more than two config objects can be resolved', () => { customBackgroundOne: '#bada55', }, textDecorationColor: { - orange: 'orange' - } + orange: 'orange', + }, }, }, } @@ -1270,7 +1270,7 @@ test('more than two config objects can be resolved', () => { backgroundColor: { customBackgroundTwo: '#facade', }, - textDecorationColor: theme => theme('colors') + textDecorationColor: theme => theme('colors'), }, }, } @@ -1292,7 +1292,7 @@ test('more than two config objects can be resolved', () => { }), textDecorationColor: { lime: 'lime', - } + }, }, }, } @@ -1316,12 +1316,7 @@ test('more than two config objects can be resolved', () => { }, } - const result = resolveConfig([ - firstConfig, - secondConfig, - thirdConfig, - defaultConfig - ]) + const result = resolveConfig([firstConfig, secondConfig, thirdConfig, defaultConfig]) expect(result).toEqual({ prefix: '-', @@ -1363,4 +1358,4 @@ test('more than two config objects can be resolved', () => { backgroundColor: ['responsive', 'hover', 'focus'], }, }) -}) \ No newline at end of file +}) diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index 3aa181d29dcd..6ceb38aa11b1 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -8,7 +8,7 @@ function updateSource(nodes, source) { } export default function( - config, + _config, { base: pluginBase, components: pluginComponents, utilities: pluginUtilities } ) { return function(css) { diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index 9d5274525b00..cbf2f93d3d6b 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -1,11 +1,10 @@ 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 get from 'lodash/get' import toPath from 'lodash/toPath' import negateValue from './negateValue' @@ -28,29 +27,30 @@ function value(valueToResolve, ...args) { } function mergeThemes(themes) { - const theme = (({ extend, ...t }) => t)(themes.reduce((merged, t) => { - return defaults(merged, t) - }, {})) + 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] - } + return { + ...theme, - if (Array.isArray(mergedValue)) { - return [...mergedValue, extendValue] - } + // In order to resolve n config objects, we combine all of their `extend` properties + // into arrays instead of objects so they aren't overridden. + extend: themes.reduce((merged, { extend }) => { + return mergeWith(merged, extend, (mergedValue, extendValue) => { + if (isUndefined(mergedValue)) { + return [extendValue] + } - return [mergedValue, extendValue] - }) - }, {}) + if (Array.isArray(mergedValue)) { + return [...mergedValue, extendValue] + } - return { - ...theme, - extend, + return [mergedValue, extendValue] + }) + }, {}), } } @@ -97,7 +97,10 @@ function resolveFunctionKeys(object) { export default function resolveConfig(configs) { return defaults( { - theme: resolveFunctionKeys(mergeExtensions(mergeThemes(map(configs, 'theme')))), + // Need to get a default empty object if the config has no theme + theme: resolveFunctionKeys( + mergeExtensions(mergeThemes(map(configs, t => get(t, 'theme', {})))) + ), variants: (firstVariants => { return Array.isArray(firstVariants) ? firstVariants From 4b72c733dc08386a21c944d4d17265ff08d2de91 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sat, 12 Oct 2019 13:00:50 -0400 Subject: [PATCH 03/15] Support resolving multiple configs using public resolveConfig function --- resolveConfig.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resolveConfig.js b/resolveConfig.js index 776aa3ba412f..ecd5bf1fd635 100644 --- a/resolveConfig.js +++ b/resolveConfig.js @@ -1,6 +1,6 @@ const resolveConfigObjects = require('./lib/util/resolveConfig').default const defaultConfig = require('./stubs/defaultConfig.stub.js') -module.exports = function resolveConfig(config) { - return resolveConfigObjects([config, defaultConfig]) +module.exports = function resolveConfig(...configs) { + return resolveConfigObjects([...configs, defaultConfig]) } From e130771c320b013e061a8fc6e55c32daf10ebb10 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 11 Oct 2019 13:39:05 -0400 Subject: [PATCH 04/15] Allow plugins to modify the config --- __tests__/modifyConfig.test.js | 58 ++++++++++++++++++++++++++++++++ __tests__/processPlugins.test.js | 40 +++++++++++++++++++++- src/index.js | 13 ++++--- src/util/processPlugins.js | 5 ++- 4 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 __tests__/modifyConfig.test.js diff --git a/__tests__/modifyConfig.test.js b/__tests__/modifyConfig.test.js new file mode 100644 index 000000000000..618838c5ac69 --- /dev/null +++ b/__tests__/modifyConfig.test.js @@ -0,0 +1,58 @@ +import postcss from 'postcss' +import tailwind from '../src/index' + +test('plugins can add new theme values', () => { + return postcss([ + tailwind({ + corePlugins: [], + plugins: [ + { + modifyConfig: function(config) { + return { + ...config, + theme: { + ...config.theme, + rotate: { + '0': '0deg', + '90': '90deg', + '180': '180deg', + '270': '270deg', + } + } + } + }, + handler: function({ addUtilities, theme }) { + addUtilities(Object.entries(theme('rotate')).map(([key, value]) => { + return { + [`.rotate-${key}`]: { + transform: `rotate(${value})`, + }, + } + })) + }, + } + ] + }), + ]) + .process('@tailwind utilities;', + { from: undefined } + ) + .then(result => { + const expected = ` + .rotate-0 { + transform: rotate(0deg) + } + .rotate-90 { + transform: rotate(90deg) + } + .rotate-180 { + transform: rotate(180deg) + } + .rotate-270 { + transform: rotate(270deg) + } + ` + + expect(result.css).toMatchCss(expected) + }) +}) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index 58e8b85e04ad..2c323ed6c320 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -47,7 +47,7 @@ test('plugins can create utilities with object syntax', () => { object-fit: cover } } - `) + `) }) test('plugins can create utilities with arrays of objects', () => { @@ -1206,3 +1206,41 @@ test('prefix will prefix all classes in a selector', () => { } `) }) + +test('plugins can be provided as an object with a handler function', () => { + const { components, utilities } = processPlugins( + [ + { + handler: function({ addUtilities }) { + addUtilities({ + '.object-fill': { + 'object-fit': 'fill', + }, + '.object-contain': { + 'object-fit': 'contain', + }, + '.object-cover': { + 'object-fit': 'cover', + }, + }) + }, + } + ], + makeConfig() + ) + + expect(components.length).toBe(0) + expect(css(utilities)).toMatchCss(` + @variants { + .object-fill { + object-fit: fill + } + .object-contain { + object-fit: contain + } + .object-cover { + object-fit: cover + } + } + `) +}) diff --git a/src/index.js b/src/index.js index b86050c2e690..32225d99f02e 100644 --- a/src/index.js +++ b/src/index.js @@ -44,6 +44,12 @@ function resolveConfigPath(filePath) { } } +function applyPluginConfigModifications(config) { + return [...config.plugins].reduce((modified, plugin) => { + return _.get(plugin, 'modifyConfig', _.identity)(modified) + }, config) +} + const getConfigFunction = config => () => { if (_.isUndefined(config) && !_.isObject(config)) { return resolveConfig([defaultConfig]) @@ -55,10 +61,9 @@ const getConfigFunction = config => () => { }) } - return resolveConfig([ - _.isObject(config) ? _.get(config, 'config', config) : require(config), - defaultConfig, - ]) + const configObject = _.isObject(config) ? _.get(config, 'config', config) : require(config) + + return resolveConfig([applyPluginConfigModifications(configObject), defaultConfig]) } const plugin = postcss.plugin('tailwind', config => { diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index c59af2918a71..3618238ccd83 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -1,6 +1,7 @@ import _ from 'lodash' import postcss from 'postcss' import Node from 'postcss/lib/node' +import isFunction from 'lodash/isFunction' import escapeClassName from '../util/escapeClassName' import generateVariantFunction from '../util/generateVariantFunction' import parseObjectStyles from '../util/parseObjectStyles' @@ -28,7 +29,9 @@ export default function(plugins, config) { const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue) plugins.forEach(plugin => { - plugin({ + const handler = isFunction(plugin) ? plugin : plugin.handler + + handler({ postcss, config: getConfigValue, theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue), From 4c25ca5ed6e563088aed5c92a59f76cfd182aec7 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 11 Oct 2019 14:25:29 -0400 Subject: [PATCH 05/15] Apply config modifications to default config, before resolving config --- __tests__/resolveConfig.test.js | 102 ++++++++++++++++++++++++++++++++ __tmp_4/tailwind.config.js | 7 +++ src/index.js | 8 +-- src/util/resolveConfig.js | 13 +++- 4 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 __tmp_4/tailwind.config.js diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index 927ed233848b..cd9d9dc73ce2 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1359,3 +1359,105 @@ test('more than two config objects can be resolved', () => { }, }) }) +test('plugin config modifications are applied', () => { + const userConfig = { + plugins: [ + { + modifyConfig: function (config) { + return { + ...config, + prefix: 'tw-' + } + }, + handler: function () {} + } + ] + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: { + screens: { + mobile: '400px', + }, + }, + variants: { + appearance: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + }, + } + + const result = resolveConfig([userConfig, defaultConfig]) + + expect(result).toEqual({ + prefix: 'tw-', + important: false, + separator: ':', + theme: { + screens: { + mobile: '400px', + }, + }, + variants: { + appearance: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + }, + plugins: userConfig.plugins, + }) +}) + +test('user config takes precedence over plugin config modifications', () => { + const userConfig = { + prefix: 'user-', + plugins: [ + { + modifyConfig: function (config) { + return { + ...config, + prefix: 'plugin-' + } + }, + handler: function () {} + } + ] + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: { + screens: { + mobile: '400px', + }, + }, + variants: { + appearance: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + }, + } + + const result = resolveConfig([userConfig, defaultConfig]) + + expect(result).toEqual({ + prefix: 'user-', + important: false, + separator: ':', + theme: { + screens: { + mobile: '400px', + }, + }, + variants: { + appearance: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + }, + plugins: userConfig.plugins, + }) +}) diff --git a/__tmp_4/tailwind.config.js b/__tmp_4/tailwind.config.js new file mode 100644 index 000000000000..42791f2cf078 --- /dev/null +++ b/__tmp_4/tailwind.config.js @@ -0,0 +1,7 @@ +module.exports = { + theme: { + screens: { + mobile: '400px', + }, + }, + } \ No newline at end of file diff --git a/src/index.js b/src/index.js index 32225d99f02e..9dd2ab58224a 100644 --- a/src/index.js +++ b/src/index.js @@ -44,12 +44,6 @@ function resolveConfigPath(filePath) { } } -function applyPluginConfigModifications(config) { - return [...config.plugins].reduce((modified, plugin) => { - return _.get(plugin, 'modifyConfig', _.identity)(modified) - }, config) -} - const getConfigFunction = config => () => { if (_.isUndefined(config) && !_.isObject(config)) { return resolveConfig([defaultConfig]) @@ -63,7 +57,7 @@ const getConfigFunction = config => () => { const configObject = _.isObject(config) ? _.get(config, 'config', config) : require(config) - return resolveConfig([applyPluginConfigModifications(configObject), defaultConfig]) + return resolveConfig([configObject, defaultConfig]) } const plugin = postcss.plugin('tailwind', config => { diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index cbf2f93d3d6b..dc649816bf28 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -3,6 +3,8 @@ import mergeWith from 'lodash/mergeWith' import isFunction from 'lodash/isFunction' import isUndefined from 'lodash/isUndefined' import defaults from 'lodash/defaults' +import identity from 'lodash/identity' +import get from 'lodash/get' import map from 'lodash/map' import get from 'lodash/get' import toPath from 'lodash/toPath' @@ -22,6 +24,12 @@ const configUtils = { }, } +function applyPluginConfigModifications(config, plugins) { + return plugins.reduce((modified, plugin) => { + return get(plugin, 'modifyConfig', identity)(modified) + }, config) +} + function value(valueToResolve, ...args) { return isFunction(valueToResolve) ? valueToResolve(...args) : valueToResolve } @@ -94,7 +102,10 @@ function resolveFunctionKeys(object) { }, {}) } -export default function resolveConfig(configs) { +export default function resolveConfig([userConfig, defaultConfig]) { + const modifiedDefaultConfig = applyPluginConfigModifications(defaultConfig, get(userConfig, 'plugins', [])) + const configs = [userConfig, modifiedDefaultConfig] + return defaults( { // Need to get a default empty object if the config has no theme From 33e5546e9bd8380d28dd26491b35cf12e2edb036 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 11 Oct 2019 14:29:38 -0400 Subject: [PATCH 06/15] Fix style --- __tests__/modifyConfig.test.js | 32 ++++++++++++++++---------------- __tests__/processPlugins.test.js | 4 ++-- __tests__/resolveConfig.test.js | 24 ++++++++++++------------ __tmp_4/tailwind.config.js | 12 ++++++------ src/util/resolveConfig.js | 5 ++++- 5 files changed, 40 insertions(+), 37 deletions(-) diff --git a/__tests__/modifyConfig.test.js b/__tests__/modifyConfig.test.js index 618838c5ac69..78d189c9446f 100644 --- a/__tests__/modifyConfig.test.js +++ b/__tests__/modifyConfig.test.js @@ -7,7 +7,7 @@ test('plugins can add new theme values', () => { corePlugins: [], plugins: [ { - modifyConfig: function(config) { + modifyConfig(config) { return { ...config, theme: { @@ -17,26 +17,26 @@ test('plugins can add new theme values', () => { '90': '90deg', '180': '180deg', '270': '270deg', - } - } + }, + }, } }, - handler: function({ addUtilities, theme }) { - addUtilities(Object.entries(theme('rotate')).map(([key, value]) => { - return { - [`.rotate-${key}`]: { - transform: `rotate(${value})`, - }, - } - })) + handler({ addUtilities, theme }) { + addUtilities( + Object.entries(theme('rotate')).map(([key, value]) => { + return { + [`.rotate-${key}`]: { + transform: `rotate(${value})`, + }, + } + }) + ) }, - } - ] + }, + ], }), ]) - .process('@tailwind utilities;', - { from: undefined } - ) + .process('@tailwind utilities;', { from: undefined }) .then(result => { const expected = ` .rotate-0 { diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index 2c323ed6c320..ae8aad847e1f 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -1211,7 +1211,7 @@ test('plugins can be provided as an object with a handler function', () => { const { components, utilities } = processPlugins( [ { - handler: function({ addUtilities }) { + handler({ addUtilities }) { addUtilities({ '.object-fill': { 'object-fit': 'fill', @@ -1224,7 +1224,7 @@ test('plugins can be provided as an object with a handler function', () => { }, }) }, - } + }, ], makeConfig() ) diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index cd9d9dc73ce2..a61923098224 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1363,15 +1363,15 @@ test('plugin config modifications are applied', () => { const userConfig = { plugins: [ { - modifyConfig: function (config) { + modifyConfig(config) { return { ...config, - prefix: 'tw-' + prefix: 'tw-', } }, - handler: function () {} - } - ] + handler() {}, + }, + ], } const defaultConfig = { @@ -1391,7 +1391,7 @@ test('plugin config modifications are applied', () => { } const result = resolveConfig([userConfig, defaultConfig]) - + expect(result).toEqual({ prefix: 'tw-', important: false, @@ -1415,15 +1415,15 @@ test('user config takes precedence over plugin config modifications', () => { prefix: 'user-', plugins: [ { - modifyConfig: function (config) { + modifyConfig(config) { return { ...config, - prefix: 'plugin-' + prefix: 'plugin-', } }, - handler: function () {} - } - ] + handler() {}, + }, + ], } const defaultConfig = { @@ -1443,7 +1443,7 @@ test('user config takes precedence over plugin config modifications', () => { } const result = resolveConfig([userConfig, defaultConfig]) - + expect(result).toEqual({ prefix: 'user-', important: false, diff --git a/__tmp_4/tailwind.config.js b/__tmp_4/tailwind.config.js index 42791f2cf078..ed20e570d37d 100644 --- a/__tmp_4/tailwind.config.js +++ b/__tmp_4/tailwind.config.js @@ -1,7 +1,7 @@ module.exports = { - theme: { - screens: { - mobile: '400px', - }, - }, - } \ No newline at end of file + theme: { + screens: { + mobile: '400px', + }, + }, +} diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index dc649816bf28..414b896bb6ad 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -103,7 +103,10 @@ function resolveFunctionKeys(object) { } export default function resolveConfig([userConfig, defaultConfig]) { - const modifiedDefaultConfig = applyPluginConfigModifications(defaultConfig, get(userConfig, 'plugins', [])) + const modifiedDefaultConfig = applyPluginConfigModifications( + defaultConfig, + get(userConfig, 'plugins', []) + ) const configs = [userConfig, modifiedDefaultConfig] return defaults( From cf9c9915cdac39848c9a8978675082ada138f470 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 11 Oct 2019 14:37:09 -0400 Subject: [PATCH 07/15] Remove temp folder --- __tmp_4/tailwind.config.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 __tmp_4/tailwind.config.js diff --git a/__tmp_4/tailwind.config.js b/__tmp_4/tailwind.config.js deleted file mode 100644 index ed20e570d37d..000000000000 --- a/__tmp_4/tailwind.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - theme: { - screens: { - mobile: '400px', - }, - }, -} From 65d45689aea739e915f624212a5c4e81dd8b2fd2 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sat, 12 Oct 2019 13:40:00 -0400 Subject: [PATCH 08/15] Allow plugins to provide their own config object --- __tests__/resolveConfig.test.js | 133 +++++++++++++++++++++++++++++--- src/index.js | 2 +- src/util/resolveConfig.js | 45 ++++++----- 3 files changed, 151 insertions(+), 29 deletions(-) diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index a61923098224..8aabd8a362fd 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1359,15 +1359,13 @@ test('more than two config objects can be resolved', () => { }, }) }) + test('plugin config modifications are applied', () => { const userConfig = { plugins: [ { - modifyConfig(config) { - return { - ...config, - prefix: 'tw-', - } + config: { + prefix: 'tw-', }, handler() {}, }, @@ -1415,11 +1413,8 @@ test('user config takes precedence over plugin config modifications', () => { prefix: 'user-', plugins: [ { - modifyConfig(config) { - return { - ...config, - prefix: 'plugin-', - } + config: { + prefix: 'tw-', }, handler() {}, }, @@ -1461,3 +1456,121 @@ test('user config takes precedence over plugin config modifications', () => { plugins: userConfig.plugins, }) }) + +test('plugin config can register plugins that also have config', () => { + const userConfig = { + plugins: [ + { + config: { + prefix: 'tw-', + plugins: [ + { + config: { + important: true, + }, + handler() {} + }, + { + config: { + separator: '__', + }, + handler() {} + }, + ] + }, + handler() {}, + }, + ], + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: { + screens: { + mobile: '400px', + }, + }, + variants: { + appearance: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + }, + } + + const result = resolveConfig([userConfig, defaultConfig]) + + expect(result).toEqual({ + prefix: 'tw-', + important: true, + separator: '__', + theme: { + screens: { + mobile: '400px', + }, + }, + variants: { + appearance: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + }, + plugins: userConfig.plugins, + }) +}) + +test('plugin configs take precedence over plugin configs registered by that plugin', () => { + const userConfig = { + plugins: [ + { + config: { + prefix: 'outer-', + plugins: [ + { + config: { + prefix: 'inner-', + }, + handler() {} + } + ] + }, + handler() {}, + }, + ], + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: { + screens: { + mobile: '400px', + }, + }, + variants: { + appearance: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + }, + } + + const result = resolveConfig([userConfig, defaultConfig]) + + expect(result).toEqual({ + prefix: 'outer-', + important: false, + separator: ':', + theme: { + screens: { + mobile: '400px', + }, + }, + variants: { + appearance: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + }, + plugins: userConfig.plugins, + }) +}) diff --git a/src/index.js b/src/index.js index 9dd2ab58224a..e5db638bad58 100644 --- a/src/index.js +++ b/src/index.js @@ -56,7 +56,7 @@ const getConfigFunction = config => () => { } const configObject = _.isObject(config) ? _.get(config, 'config', config) : require(config) - + return resolveConfig([configObject, defaultConfig]) } diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index 414b896bb6ad..1ab7b4b66267 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -1,10 +1,9 @@ import some from 'lodash/some' import mergeWith from 'lodash/mergeWith' +import isEmpty from 'lodash/isEmpty' import isFunction from 'lodash/isFunction' import isUndefined from 'lodash/isUndefined' import defaults from 'lodash/defaults' -import identity from 'lodash/identity' -import get from 'lodash/get' import map from 'lodash/map' import get from 'lodash/get' import toPath from 'lodash/toPath' @@ -24,12 +23,6 @@ const configUtils = { }, } -function applyPluginConfigModifications(config, plugins) { - return plugins.reduce((modified, plugin) => { - return get(plugin, 'modifyConfig', identity)(modified) - }, config) -} - function value(valueToResolve, ...args) { return isFunction(valueToResolve) ? valueToResolve(...args) : valueToResolve } @@ -102,25 +95,41 @@ function resolveFunctionKeys(object) { }, {}) } -export default function resolveConfig([userConfig, defaultConfig]) { - const modifiedDefaultConfig = applyPluginConfigModifications( - defaultConfig, - get(userConfig, 'plugins', []) - ) - const configs = [userConfig, modifiedDefaultConfig] +function extractPluginConfigs(configs) { + let allConfigs = [] + + configs.forEach(config => { + allConfigs = [...allConfigs, config] + + const plugins = get(config, 'plugins', []) + + if (plugins.length === 0) { + return + } + + plugins.forEach(plugin => { + allConfigs = [...allConfigs, ...extractPluginConfigs([get(plugin, 'config', {})])] + }) + }) + + return allConfigs +} + +export default function resolveConfig(configs) { + const allConfigs = extractPluginConfigs(configs) return defaults( { // Need to get a default empty object if the config has no theme theme: resolveFunctionKeys( - mergeExtensions(mergeThemes(map(configs, t => get(t, 'theme', {})))) + mergeExtensions(mergeThemes(map(allConfigs, t => get(t, 'theme', {})))) ), variants: (firstVariants => { return Array.isArray(firstVariants) ? firstVariants - : defaults({}, ...map(configs, 'variants')) - })(defaults({}, ...map(configs)).variants), + : defaults({}, ...map(allConfigs, 'variants')) + })(defaults({}, ...map(allConfigs)).variants), }, - ...configs + ...allConfigs ) } From 6bd32ad595b78bc5ef99a36c5d5b380b04eaa7cf Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sat, 12 Oct 2019 13:41:02 -0400 Subject: [PATCH 09/15] Remove outdated test --- __tests__/modifyConfig.test.js | 58 ---------------------------------- 1 file changed, 58 deletions(-) delete mode 100644 __tests__/modifyConfig.test.js diff --git a/__tests__/modifyConfig.test.js b/__tests__/modifyConfig.test.js deleted file mode 100644 index 78d189c9446f..000000000000 --- a/__tests__/modifyConfig.test.js +++ /dev/null @@ -1,58 +0,0 @@ -import postcss from 'postcss' -import tailwind from '../src/index' - -test('plugins can add new theme values', () => { - return postcss([ - tailwind({ - corePlugins: [], - plugins: [ - { - modifyConfig(config) { - return { - ...config, - theme: { - ...config.theme, - rotate: { - '0': '0deg', - '90': '90deg', - '180': '180deg', - '270': '270deg', - }, - }, - } - }, - handler({ addUtilities, theme }) { - addUtilities( - Object.entries(theme('rotate')).map(([key, value]) => { - return { - [`.rotate-${key}`]: { - transform: `rotate(${value})`, - }, - } - }) - ) - }, - }, - ], - }), - ]) - .process('@tailwind utilities;', { from: undefined }) - .then(result => { - const expected = ` - .rotate-0 { - transform: rotate(0deg) - } - .rotate-90 { - transform: rotate(90deg) - } - .rotate-180 { - transform: rotate(180deg) - } - .rotate-270 { - transform: rotate(270deg) - } - ` - - expect(result.css).toMatchCss(expected) - }) -}) From 53dff626a792e46a0db1da4a0f815c7a40b7a359 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sat, 12 Oct 2019 13:41:28 -0400 Subject: [PATCH 10/15] Fix style --- __tests__/resolveConfig.test.js | 12 ++++++------ src/index.js | 2 +- src/util/resolveConfig.js | 3 +-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index 8aabd8a362fd..fb8d5edd6b46 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1468,15 +1468,15 @@ test('plugin config can register plugins that also have config', () => { config: { important: true, }, - handler() {} + handler() {}, }, { config: { separator: '__', }, - handler() {} + handler() {}, }, - ] + ], }, handler() {}, }, @@ -1530,9 +1530,9 @@ test('plugin configs take precedence over plugin configs registered by that plug config: { prefix: 'inner-', }, - handler() {} - } - ] + handler() {}, + }, + ], }, handler() {}, }, diff --git a/src/index.js b/src/index.js index e5db638bad58..9dd2ab58224a 100644 --- a/src/index.js +++ b/src/index.js @@ -56,7 +56,7 @@ const getConfigFunction = config => () => { } const configObject = _.isObject(config) ? _.get(config, 'config', config) : require(config) - + return resolveConfig([configObject, defaultConfig]) } diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index 1ab7b4b66267..be2c64762180 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -1,6 +1,5 @@ import some from 'lodash/some' import mergeWith from 'lodash/mergeWith' -import isEmpty from 'lodash/isEmpty' import isFunction from 'lodash/isFunction' import isUndefined from 'lodash/isUndefined' import defaults from 'lodash/defaults' @@ -102,7 +101,7 @@ function extractPluginConfigs(configs) { allConfigs = [...allConfigs, config] const plugins = get(config, 'plugins', []) - + if (plugins.length === 0) { return } From 94a1d30809c51e403b607d567ee08d775d12fcbb Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sat, 12 Oct 2019 13:46:51 -0400 Subject: [PATCH 11/15] Make handler optional in object plugins --- __tests__/processPlugins.test.js | 43 ++++++++++++++++++++++++++++++++ __tests__/resolveConfig.test.js | 5 ---- src/util/processPlugins.js | 2 +- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index ae8aad847e1f..f70c12e6f9ed 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -1244,3 +1244,46 @@ test('plugins can be provided as an object with a handler function', () => { } `) }) + +test('plugins can provide a config but no handler', () => { + const { components, utilities } = processPlugins( + [ + { + config: { + prefix: 'tw-', + }, + }, + { + handler({ addUtilities }) { + addUtilities({ + '.object-fill': { + 'object-fit': 'fill', + }, + '.object-contain': { + 'object-fit': 'contain', + }, + '.object-cover': { + 'object-fit': 'cover', + }, + }) + }, + }, + ], + makeConfig() + ) + + expect(components.length).toBe(0) + expect(css(utilities)).toMatchCss(` + @variants { + .object-fill { + object-fit: fill + } + .object-contain { + object-fit: contain + } + .object-cover { + object-fit: cover + } + } + `) +}) diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index fb8d5edd6b46..4b0df5724d05 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1367,7 +1367,6 @@ test('plugin config modifications are applied', () => { config: { prefix: 'tw-', }, - handler() {}, }, ], } @@ -1416,7 +1415,6 @@ test('user config takes precedence over plugin config modifications', () => { config: { prefix: 'tw-', }, - handler() {}, }, ], } @@ -1468,13 +1466,11 @@ test('plugin config can register plugins that also have config', () => { config: { important: true, }, - handler() {}, }, { config: { separator: '__', }, - handler() {}, }, ], }, @@ -1530,7 +1526,6 @@ test('plugin configs take precedence over plugin configs registered by that plug config: { prefix: 'inner-', }, - handler() {}, }, ], }, diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index 3618238ccd83..1dcd7f62735c 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -29,7 +29,7 @@ export default function(plugins, config) { const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue) plugins.forEach(plugin => { - const handler = isFunction(plugin) ? plugin : plugin.handler + const handler = isFunction(plugin) ? plugin : _.get(plugin, 'handler', () => {}) handler({ postcss, From 66ec10edda5513626baabd65779e59a51732c11b Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sun, 13 Oct 2019 13:49:23 -0400 Subject: [PATCH 12/15] Add test to document plugin extend behavior --- __tests__/resolveConfig.test.js | 76 +++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index 4b0df5724d05..703f7857b571 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1569,3 +1569,79 @@ test('plugin configs take precedence over plugin configs registered by that plug plugins: userConfig.plugins, }) }) + +test('plugin theme extensions are added even if user overrides top-level theme config', () => { + const userConfig = { + plugins: [ + { + config: { + theme: { + width: { + '1px': '1px' + } + }, + plugins: [ + { + config: { + theme: { + extend: { + width: { + '2px': '2px', + '3px': '3px', + } + } + }, + }, + }, + ], + }, + handler() {}, + }, + ], + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: { + width: { + sm: '1rem', + md: '2rem', + lg: '3rem', + }, + screens: { + mobile: '400px', + }, + }, + variants: { + appearance: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + }, + } + + const result = resolveConfig([userConfig, defaultConfig]) + + expect(result).toEqual({ + prefix: '', + important: false, + separator: ':', + theme: { + width: { + '1px': '1px', + '2px': '2px', + '3px': '3px', + }, + screens: { + mobile: '400px', + }, + }, + variants: { + appearance: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + }, + plugins: userConfig.plugins, + }) +}) From 872fe2525964ee6a607083d4ec80febfed5bbde5 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sun, 13 Oct 2019 20:29:07 -0400 Subject: [PATCH 13/15] Fix test --- __tests__/resolveConfig.test.js | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index 703f7857b571..9fa7c0581c6f 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1572,28 +1572,22 @@ test('plugin configs take precedence over plugin configs registered by that plug test('plugin theme extensions are added even if user overrides top-level theme config', () => { const userConfig = { + theme: { + width: { + '1px': '1px' + } + }, plugins: [ { config: { theme: { - width: { - '1px': '1px' - } + extend: { + width: { + '2px': '2px', + '3px': '3px', + } + } }, - plugins: [ - { - config: { - theme: { - extend: { - width: { - '2px': '2px', - '3px': '3px', - } - } - }, - }, - }, - ], }, handler() {}, }, From 5911bd704cb00c88f0977e6819b0cdf1f78b28b7 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sun, 13 Oct 2019 20:31:32 -0400 Subject: [PATCH 14/15] Reverse extend array to guarantee correct precedence --- __tests__/resolveConfig.test.js | 72 +++++++++++++++++++++++++++++++++ src/util/resolveConfig.js | 5 +-- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index 9fa7c0581c6f..35b159a5ab4e 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1639,3 +1639,75 @@ test('plugin theme extensions are added even if user overrides top-level theme c plugins: userConfig.plugins, }) }) + +test('user theme extensions take precedence over plugin theme extensions with the same key', () => { + const userConfig = { + theme: { + extend: { + width: { + xl: '6rem' + }, + }, + }, + plugins: [ + { + config: { + theme: { + extend: { + width: { + xl: '4rem', + } + } + }, + }, + handler() {}, + }, + ], + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: { + width: { + sm: '1rem', + md: '2rem', + lg: '3rem', + }, + screens: { + mobile: '400px', + }, + }, + variants: { + appearance: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + }, + } + + const result = resolveConfig([userConfig, defaultConfig]) + + expect(result).toEqual({ + prefix: '', + important: false, + separator: ':', + theme: { + width: { + sm: '1rem', + md: '2rem', + lg: '3rem', + xl: '6rem', + }, + screens: { + mobile: '400px', + }, + }, + variants: { + appearance: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + }, + plugins: userConfig.plugins, + }) +}) diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index be2c64762180..4ad99126b34c 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -45,10 +45,10 @@ function mergeThemes(themes) { } if (Array.isArray(mergedValue)) { - return [...mergedValue, extendValue] + return [extendValue, ...mergedValue] } - return [mergedValue, extendValue] + return [extendValue, mergedValue] }) }, {}), } @@ -119,7 +119,6 @@ export default function resolveConfig(configs) { return defaults( { - // Need to get a default empty object if the config has no theme theme: resolveFunctionKeys( mergeExtensions(mergeThemes(map(allConfigs, t => get(t, 'theme', {})))) ), From 2fa0d4e8218d093f79d5d86305496b70fca921b9 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sun, 13 Oct 2019 20:35:18 -0400 Subject: [PATCH 15/15] Fix style --- __tests__/resolveConfig.test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index 35b159a5ab4e..27d267345a22 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1574,8 +1574,8 @@ test('plugin theme extensions are added even if user overrides top-level theme c const userConfig = { theme: { width: { - '1px': '1px' - } + '1px': '1px', + }, }, plugins: [ { @@ -1585,8 +1585,8 @@ test('plugin theme extensions are added even if user overrides top-level theme c width: { '2px': '2px', '3px': '3px', - } - } + }, + }, }, }, handler() {}, @@ -1645,7 +1645,7 @@ test('user theme extensions take precedence over plugin theme extensions with th theme: { extend: { width: { - xl: '6rem' + xl: '6rem', }, }, }, @@ -1656,8 +1656,8 @@ test('user theme extensions take precedence over plugin theme extensions with th extend: { width: { xl: '4rem', - } - } + }, + }, }, }, handler() {},