From e13f083c4bc48bf9870d27c966136a9584943127 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Mon, 19 Oct 2020 11:32:22 -0400 Subject: [PATCH] Join arrays when using `theme` consistently --- __tests__/processPlugins.test.js | 372 +++++++++++++++++++++++++++ __tests__/themeFunction.test.js | 115 ++++++++- src/lib/evaluateTailwindFunctions.js | 29 +-- src/plugins/fontFamily.js | 10 +- src/util/createUtilityPlugin.js | 26 +- src/util/processPlugins.js | 8 +- src/util/transformThemeValue.js | 25 ++ 7 files changed, 540 insertions(+), 45 deletions(-) create mode 100644 src/util/transformThemeValue.js diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index 997cd321d890..0f3d862ee7f8 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -2050,3 +2050,375 @@ test('keyframes are not escaped', () => { } `) }) + +test('font sizes are retrieved without default line-heights or letter-spacing using theme function', () => { + const { components } = processPlugins( + [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + fontSize: theme('fontSize.sm'), + }, + }) + }, + ], + makeConfig({ + theme: { + fontSize: { + sm: ['14px', '20px'], + }, + }, + }) + ) + + expect(css(components)).toMatchCss(` + @layer components { + @variants { + .foo { + font-size: 14px; + } + } + } + `) +}) + +test('outlines are retrieved without outline-offset using theme function', () => { + const { components } = processPlugins( + [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + outline: theme('outline.black'), + }, + }) + }, + ], + makeConfig({ + theme: { + outline: { + black: ['2px dotted black', '4px'], + }, + }, + }) + ) + + expect(css(components)).toMatchCss(` + @layer components { + @variants { + .foo { + outline: 2px dotted black; + } + } + } + `) +}) + +test('box-shadow values are joined when retrieved using the theme function', () => { + const { components } = processPlugins( + [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + boxShadow: theme('boxShadow.lol'), + }, + }) + }, + ], + makeConfig({ + theme: { + boxShadow: { + lol: ['width', 'height'], + }, + }, + }) + ) + + expect(css(components)).toMatchCss(` + @layer components { + @variants { + .foo { + box-shadow: width, height; + } + } + } + `) +}) + +test('transition-property values are joined when retrieved using the theme function', () => { + const { components } = processPlugins( + [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + transitionProperty: theme('transitionProperty.lol'), + }, + }) + }, + ], + makeConfig({ + theme: { + transitionProperty: { + lol: ['width', 'height'], + }, + }, + }) + ) + + expect(css(components)).toMatchCss(` + @layer components { + @variants { + .foo { + transition-property: width, height; + } + } + } + `) +}) + +test('transition-duration values are joined when retrieved using the theme function', () => { + const { components } = processPlugins( + [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + transitionDuration: theme('transitionDuration.lol'), + }, + }) + }, + ], + makeConfig({ + theme: { + transitionDuration: { + lol: ['width', 'height'], + }, + }, + }) + ) + + expect(css(components)).toMatchCss(` + @layer components { + @variants { + .foo { + transition-duration: width, height; + } + } + } + `) +}) + +test('transition-delay values are joined when retrieved using the theme function', () => { + const { components } = processPlugins( + [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + transitionDelay: theme('transitionDelay.lol'), + }, + }) + }, + ], + makeConfig({ + theme: { + transitionDelay: { + lol: ['width', 'height'], + }, + }, + }) + ) + + expect(css(components)).toMatchCss(` + @layer components { + @variants { + .foo { + transition-delay: width, height; + } + } + } + `) +}) + +test('transition-timing-function values are joined when retrieved using the theme function', () => { + const { components } = processPlugins( + [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + transitionTimingFunction: theme('transitionTimingFunction.lol'), + }, + }) + }, + ], + makeConfig({ + theme: { + transitionTimingFunction: { + lol: ['width', 'height'], + }, + }, + }) + ) + + expect(css(components)).toMatchCss(` + @layer components { + @variants { + .foo { + transition-timing-function: width, height; + } + } + } + `) +}) + +test('background-image values are joined when retrieved using the theme function', () => { + const { components } = processPlugins( + [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + backgroundImage: theme('backgroundImage.lol'), + }, + }) + }, + ], + makeConfig({ + theme: { + backgroundImage: { + lol: ['width', 'height'], + }, + }, + }) + ) + + expect(css(components)).toMatchCss(` + @layer components { + @variants { + .foo { + background-image: width, height; + } + } + } + `) +}) + +test('background-size values are joined when retrieved using the theme function', () => { + const { components } = processPlugins( + [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + backgroundSize: theme('backgroundSize.lol'), + }, + }) + }, + ], + makeConfig({ + theme: { + backgroundSize: { + lol: ['width', 'height'], + }, + }, + }) + ) + + expect(css(components)).toMatchCss(` + @layer components { + @variants { + .foo { + background-size: width, height; + } + } + } + `) +}) + +test('background-color values are joined when retrieved using the theme function', () => { + const { components } = processPlugins( + [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + backgroundColor: theme('backgroundColor.lol'), + }, + }) + }, + ], + makeConfig({ + theme: { + backgroundColor: { + lol: ['width', 'height'], + }, + }, + }) + ) + + expect(css(components)).toMatchCss(` + @layer components { + @variants { + .foo { + background-color: width, height; + } + } + } + `) +}) + +test('cursor values are joined when retrieved using the theme function', () => { + const { components } = processPlugins( + [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + cursor: theme('cursor.lol'), + }, + }) + }, + ], + makeConfig({ + theme: { + cursor: { + lol: ['width', 'height'], + }, + }, + }) + ) + + expect(css(components)).toMatchCss(` + @layer components { + @variants { + .foo { + cursor: width, height; + } + } + } + `) +}) + +test('animation values are joined when retrieved using the theme function', () => { + const { components } = processPlugins( + [ + function ({ addComponents, theme }) { + addComponents({ + '.foo': { + animation: theme('animation.lol'), + }, + }) + }, + ], + makeConfig({ + theme: { + animation: { + lol: ['width', 'height'], + }, + }, + }) + ) + + expect(css(components)).toMatchCss(` + @layer components { + @variants { + .foo { + animation: width, height; + } + } + } + `) +}) diff --git a/__tests__/themeFunction.test.js b/__tests__/themeFunction.test.js index 252bb73e0e80..f2027ae8e26b 100644 --- a/__tests__/themeFunction.test.js +++ b/__tests__/themeFunction.test.js @@ -70,7 +70,7 @@ test('a default value can be provided', () => { test('quotes are preserved around default values', () => { const input = ` - .heading { font-family: theme('fonts.sans', "Helvetica Neue"); } + .heading { font-family: theme('fontFamily.sans', "Helvetica Neue"); } ` const output = ` @@ -79,7 +79,7 @@ test('quotes are preserved around default values', () => { return run(input, { theme: { - fonts: { + fontFamily: { serif: 'Constantia', }, }, @@ -91,7 +91,7 @@ test('quotes are preserved around default values', () => { test('an unquoted list is valid as a default value', () => { const input = ` - .heading { font-family: theme('fonts.sans', Helvetica, Arial, sans-serif); } + .heading { font-family: theme('fontFamily.sans', Helvetica, Arial, sans-serif); } ` const output = ` @@ -100,7 +100,7 @@ test('an unquoted list is valid as a default value', () => { return run(input, { theme: { - fonts: { + fontFamily: { serif: 'Constantia', }, }, @@ -271,7 +271,112 @@ test('font sizes are retrieved without default line-heights or letter-spacing', }, }, }).then((result) => { - expect(result.css).toEqual(output) + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('outlines are retrieved without default outline-offset', () => { + const input = ` + .element { outline: theme('outline.black'); } + ` + + const output = ` + .element { outline: 2px dotted black; } + ` + + return run(input, { + theme: { + outline: { + black: ['2px dotted black', '4px'], + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('font-family values are joined when an array', () => { + const input = ` + .element { font-family: theme('fontFamily.sans'); } + ` + + const output = ` + .element { font-family: Helvetica, Arial, sans-serif; } + ` + + return run(input, { + theme: { + fontFamily: { + sans: ['Helvetica', 'Arial', 'sans-serif'], + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('box-shadow values are joined when an array', () => { + const input = ` + .element { box-shadow: theme('boxShadow.wtf'); } + ` + + const output = ` + .element { box-shadow: 0 0 2px black, 1px 2px 3px white; } + ` + + return run(input, { + theme: { + boxShadow: { + wtf: ['0 0 2px black', '1px 2px 3px white'], + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('transition-property values are joined when an array', () => { + const input = ` + .element { transition-property: theme('transitionProperty.colors'); } + ` + + const output = ` + .element { transition-property: color, fill; } + ` + + return run(input, { + theme: { + transitionProperty: { + colors: ['color', 'fill'], + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('transition-duration values are joined when an array', () => { + const input = ` + .element { transition-duration: theme('transitionDuration.lol'); } + ` + + const output = ` + .element { transition-duration: 1s, 2s; } + ` + + return run(input, { + theme: { + transitionDuration: { + lol: ['1s', '2s'], + }, + }, + }).then((result) => { + expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) }) diff --git a/src/lib/evaluateTailwindFunctions.js b/src/lib/evaluateTailwindFunctions.js index 04eb6cfbeb8d..1d742aa0df85 100644 --- a/src/lib/evaluateTailwindFunctions.js +++ b/src/lib/evaluateTailwindFunctions.js @@ -1,16 +1,7 @@ import _ from 'lodash' import functions from 'postcss-functions' import didYouMean from 'didyoumean' - -const themeTransforms = { - fontSize(value) { - return Array.isArray(value) ? value[0] : value - }, -} - -function defaultTransform(value) { - return Array.isArray(value) ? value.join(', ') : value -} +import transformThemeValue from '../util/transformThemeValue' function findClosestExistingPath(theme, path) { const parts = _.toPath(path) @@ -113,7 +104,7 @@ function validatePath(config, path, defaultValue) { return { isValid: true, - value: _.get(themeTransforms, themeSection, defaultTransform)(value), + value: transformThemeValue(themeSection)(value), } } @@ -122,13 +113,17 @@ export default function (config) { functions({ functions: { theme: (path, ...defaultValue) => { - return _.thru( - validatePath(config, path, defaultValue.length ? defaultValue : undefined), - ({ isValid, value, error }) => { - if (isValid) return value - throw root.error(error) - } + const { isValid, value, error } = validatePath( + config, + path, + defaultValue.length ? defaultValue : undefined ) + + if (!isValid) { + throw root.error(error) + } + + return value }, }, })(root) diff --git a/src/plugins/fontFamily.js b/src/plugins/fontFamily.js index a13f41cabd6d..6153989f66cf 100644 --- a/src/plugins/fontFamily.js +++ b/src/plugins/fontFamily.js @@ -1,13 +1,5 @@ import createUtilityPlugin from '../util/createUtilityPlugin' export default function () { - return createUtilityPlugin('fontFamily', [ - [ - 'font', - ['fontFamily'], - (value) => { - return Array.isArray(value) ? value.join(', ') : value - }, - ], - ]) + return createUtilityPlugin('fontFamily', [['font', ['fontFamily']]]) } diff --git a/src/util/createUtilityPlugin.js b/src/util/createUtilityPlugin.js index 4afa951e586e..d090d174c0a1 100644 --- a/src/util/createUtilityPlugin.js +++ b/src/util/createUtilityPlugin.js @@ -1,23 +1,23 @@ -import identity from 'lodash/identity' import fromPairs from 'lodash/fromPairs' import toPairs from 'lodash/toPairs' import castArray from 'lodash/castArray' import nameClass from './nameClass' +import transformThemeValue from './transformThemeValue' export default function createUtilityPlugin(themeKey, utilityVariations) { + const transformValue = transformThemeValue(themeKey) return function ({ addUtilities, variants, theme }) { - const utilities = utilityVariations.map( - ([classPrefix, properties, transformValue = identity]) => { - return fromPairs( - toPairs(theme(themeKey)).map(([key, value]) => { - return [ - nameClass(classPrefix, key), - fromPairs(castArray(properties).map((property) => [property, transformValue(value)])), - ] - }) - ) - } - ) + const pairs = toPairs(theme(themeKey)) + const utilities = utilityVariations.map(([classPrefix, properties]) => { + return fromPairs( + pairs.map(([key, value]) => { + return [ + nameClass(classPrefix, key), + fromPairs(castArray(properties).map((property) => [property, transformValue(value)])), + ] + }) + ) + }) return addUtilities(utilities, variants(themeKey)) } } diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index 53880c9f3cef..b5c8912551ea 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -8,6 +8,7 @@ import parseObjectStyles from '../util/parseObjectStyles' import prefixSelector from '../util/prefixSelector' import wrapWithVariants from '../util/wrapWithVariants' import cloneNodes from '../util/cloneNodes' +import transformThemeValue from './transformThemeValue' function parseStyles(styles) { if (!Array.isArray(styles)) { @@ -52,7 +53,12 @@ export default function (plugins, config) { handler({ postcss, config: getConfigValue, - theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue), + theme: (path, defaultValue) => { + const [pathRoot, ...subPaths] = _.toPath(path) + const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue) + + return transformThemeValue(pathRoot)(value) + }, corePlugins: (path) => { if (Array.isArray(config.corePlugins)) { return config.corePlugins.includes(path) diff --git a/src/util/transformThemeValue.js b/src/util/transformThemeValue.js new file mode 100644 index 000000000000..3e4fcdc6004f --- /dev/null +++ b/src/util/transformThemeValue.js @@ -0,0 +1,25 @@ +export default function transformThemeValue(themeSection) { + if (['fontSize', 'outline'].includes(themeSection)) { + return (value) => (Array.isArray(value) ? value[0] : value) + } + + if ( + [ + 'fontFamily', + 'boxShadow', + 'transitionProperty', + 'transitionDuration', + 'transitionDelay', + 'transitionTimingFunction', + 'backgroundImage', + 'backgroundSize', + 'backgroundColor', + 'cursor', + 'animation', + ].includes(themeSection) + ) { + return (value) => (Array.isArray(value) ? value.join(', ') : value) + } + + return (value) => value +}