diff --git a/CHANGELOG.md b/CHANGELOG.md index 116052da884b..57b594865855 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add experimental `2xl` breakpoint ([#2468](https://github.com/tailwindlabs/tailwindcss/pull/2468)) - Add `col-span-full` and `row-span-full` ([#2471](https://github.com/tailwindlabs/tailwindcss/pull/2471)) - Promote `defaultLineHeights` and `standardFontWeights` from experimental to future +- Add new `presets` config option ([#2474](https://github.com/tailwindlabs/tailwindcss/pull/2474)) ## [1.8.12] - 2020-10-07 diff --git a/__tests__/configurePlugins.test.js b/__tests__/configurePlugins.test.js index 854686ef63b3..03d9852a02d5 100644 --- a/__tests__/configurePlugins.test.js +++ b/__tests__/configurePlugins.test.js @@ -1,11 +1,7 @@ import configurePlugins from '../src/util/configurePlugins' test('setting a plugin to false removes it', () => { - const plugins = { - fontSize: () => 'fontSize', - display: () => 'display', - backgroundPosition: () => 'backgroundPosition', - } + const plugins = ['fontSize', 'display', 'backgroundPosition'] const configuredPlugins = configurePlugins( { @@ -18,11 +14,7 @@ test('setting a plugin to false removes it', () => { }) test('passing only false removes all plugins', () => { - const plugins = { - fontSize: () => 'fontSize', - display: () => 'display', - backgroundPosition: () => 'backgroundPosition', - } + const plugins = ['fontSize', 'display', 'backgroundPosition'] const configuredPlugins = configurePlugins(false, plugins) @@ -30,11 +22,7 @@ test('passing only false removes all plugins', () => { }) test('passing an array whitelists plugins', () => { - const plugins = { - fontSize: () => 'fontSize', - display: () => 'display', - backgroundPosition: () => 'backgroundPosition', - } + const plugins = ['fontSize', 'display', 'backgroundPosition'] const configuredPlugins = configurePlugins(['display'], plugins) diff --git a/__tests__/customConfig.test.js b/__tests__/customConfig.test.js index 5c3f3e7d2404..ae00c0ba61e2 100644 --- a/__tests__/customConfig.test.js +++ b/__tests__/customConfig.test.js @@ -210,3 +210,107 @@ test('tailwind.config.js is picked up by default when passing an empty object', }) }) }) + +test('the default config can be overridden using the presets key', () => { + return postcss([ + tailwind({ + presets: [ + { + theme: { + extend: { + colors: { + black: 'black', + }, + backgroundColor: theme => theme('colors'), + }, + }, + corePlugins: ['backgroundColor'], + }, + ], + theme: { + extend: { colors: { white: 'white' } }, + }, + }), + ]) + .process( + ` + @tailwind utilities + `, + { from: undefined } + ) + .then(result => { + const expected = ` + .bg-black { + background-color: black; + } + .bg-white { + background-color: white; + } + ` + + expect(result.css).toMatchCss(expected) + }) +}) + +test('presets can have their own presets', () => { + return postcss([ + tailwind({ + presets: [ + { + theme: { + colors: { red: '#dd0000' }, + }, + }, + { + presets: [ + { + theme: { + colors: { + transparent: 'transparent', + red: '#ff0000', + }, + }, + }, + ], + theme: { + extend: { + colors: { + black: 'black', + red: '#ee0000', + }, + backgroundColor: theme => theme('colors'), + }, + }, + corePlugins: ['backgroundColor'], + }, + ], + theme: { + extend: { colors: { white: 'white' } }, + }, + }), + ]) + .process( + ` + @tailwind utilities + `, + { from: undefined } + ) + .then(result => { + const expected = ` + .bg-transparent { + background-color: transparent; + } + .bg-red { + background-color: #ee0000; + } + .bg-black { + background-color: black; + } + .bg-white { + background-color: white; + } + ` + + expect(result.css).toMatchCss(expected) + }) +}) diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index 50e6b9877c1d..f2db50ea55c5 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1,3 +1,4 @@ +import { corePluginList } from '../src/corePlugins' import resolveConfig from '../src/util/resolveConfig' test('prefix key overrides default prefix', () => { @@ -23,7 +24,7 @@ test('prefix key overrides default prefix', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: 'tw-', important: false, separator: ':', @@ -63,7 +64,7 @@ test('important key overrides default important', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '', important: true, separator: ':', @@ -103,7 +104,7 @@ test('important (selector) key overrides default important', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '', important: '#app', separator: ':', @@ -143,7 +144,7 @@ test('separator key overrides default separator', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '', important: false, separator: '__', @@ -200,7 +201,7 @@ test('theme key is merged instead of replaced', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -269,7 +270,7 @@ test('variants key is merged instead of replaced', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -339,7 +340,7 @@ test('a global variants list replaces the default', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -387,7 +388,7 @@ test('missing top level keys are pulled from the default config', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -437,7 +438,7 @@ test('functions in the default theme section are lazily evaluated', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -505,7 +506,7 @@ test('functions in the user theme section are lazily evaluated', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -575,7 +576,7 @@ test('theme values in the extend section extend the existing theme', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -659,7 +660,7 @@ test('theme values in the extend section extend the user theme', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -731,7 +732,7 @@ test('theme values in the extend section can extend values that are depended on const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -789,7 +790,7 @@ test('theme values in the extend section are not deeply merged', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -839,7 +840,7 @@ test('the theme function can use a default value if the key is missing', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -896,7 +897,7 @@ test('the theme function can resolve function values', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -964,7 +965,7 @@ test('the theme function can resolve deep function values', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -1029,7 +1030,7 @@ test('theme values in the extend section are lazily evaluated', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -1094,7 +1095,7 @@ test('lazily evaluated values have access to the config utils', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -1225,7 +1226,7 @@ test('custom properties are multiplied by -1 for negative values', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -1343,7 +1344,7 @@ test('more than two config objects can be resolved', () => { const result = resolveConfig([firstConfig, secondConfig, thirdConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '-', important: false, separator: ':', @@ -1414,7 +1415,7 @@ test('plugin config modifications are applied', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: 'tw-', important: false, separator: ':', @@ -1462,7 +1463,7 @@ test('user config takes precedence over plugin config modifications', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: 'user-', important: false, separator: ':', @@ -1522,7 +1523,7 @@ test('plugin config can register plugins that also have config', () => { const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: 'tw-', important: true, separator: '__', @@ -1577,7 +1578,7 @@ test('plugin configs take precedence over plugin configs registered by that plug const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: 'outer-', important: false, separator: ':', @@ -1642,7 +1643,7 @@ test('plugin theme extensions are added even if user overrides top-level theme c const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '', important: false, separator: ':', @@ -1713,7 +1714,7 @@ test('user theme extensions take precedence over plugin theme extensions with th const result = resolveConfig([userConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '', important: false, separator: ':', @@ -1779,7 +1780,7 @@ test('variants can be defined as a function', () => { const result = resolveConfig([userConfig, otherConfig, defaultConfig]) - expect(result).toEqual({ + expect(result).toMatchObject({ prefix: '', important: false, separator: ':', @@ -1795,6 +1796,63 @@ test('variants can be defined as a function', () => { rotate: ['responsive', 'focus'], cursor: ['focus', 'checked', 'hover'], }, - plugins: userConfig.plugins, + }) +}) + +test('core plugin configuration builds on the default list when starting with an empty object', () => { + const userConfig = { + corePlugins: { display: false }, + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: {}, + variants: {}, + corePlugins: {}, + } + + const result = resolveConfig([userConfig, defaultConfig]) + + expect(result).toMatchObject({ + prefix: '', + important: false, + separator: ':', + theme: {}, + variants: {}, + corePlugins: Object.keys(corePluginList).filter(c => c !== 'display'), + }) +}) + +test('core plugin configurations stack', () => { + const userConfig = { + corePlugins: { display: false }, + } + + const otherConfig = { + corePlugins: ({ corePlugins }) => { + return [...corePlugins, 'margin'] + }, + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: {}, + variants: {}, + corePlugins: ['float', 'display', 'padding'], + } + + const result = resolveConfig([userConfig, otherConfig, defaultConfig]) + + expect(result).toMatchObject({ + prefix: '', + important: false, + separator: ':', + theme: {}, + variants: {}, + corePlugins: ['float', 'padding', 'margin'], }) }) diff --git a/jest/runInTempDirectory.js b/jest/runInTempDirectory.js index 025361045d58..c0928e219141 100644 --- a/jest/runInTempDirectory.js +++ b/jest/runInTempDirectory.js @@ -3,9 +3,11 @@ import path from 'path' import rimraf from 'rimraf' +let id = 0 + export default function(callback) { return new Promise(resolve => { - const workerId = process.env.JEST_WORKER_ID + const workerId = `${process.env.JEST_WORKER_ID}-${id++}` const tmpPath = path.resolve(__dirname, `../__tmp_${workerId}`) const currentPath = process.cwd() diff --git a/resolveConfig.js b/resolveConfig.js index a9db71f4641e..6fb5c15fb243 100644 --- a/resolveConfig.js +++ b/resolveConfig.js @@ -3,6 +3,5 @@ const getAllConfigs = require('./lib/util/getAllConfigs').default module.exports = function resolveConfig(...configs) { const [, ...defaultConfigs] = getAllConfigs(configs[0]) - return resolveConfigObjects([...configs, ...defaultConfigs]) } diff --git a/src/corePlugins.js b/src/corePlugins.js index 61e7b2170250..63e177508fd8 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -112,118 +112,122 @@ import animation from './plugins/animation' import configurePlugins from './util/configurePlugins' +export const corePluginList = { + preflight, + container, + space, + divideWidth, + divideColor, + divideStyle, + divideOpacity, + accessibility, + appearance, + backgroundAttachment, + backgroundClip, + backgroundColor, + backgroundImage, + gradientColorStops, + backgroundOpacity, + backgroundPosition, + backgroundRepeat, + backgroundSize, + borderCollapse, + borderColor, + borderOpacity, + borderRadius, + borderStyle, + borderWidth, + boxSizing, + cursor, + display, + flexDirection, + flexWrap, + placeItems, + placeContent, + placeSelf, + alignItems, + alignContent, + alignSelf, + justifyItems, + justifyContent, + justifySelf, + flex, + flexGrow, + flexShrink, + order, + float, + clear, + fontFamily, + fontWeight, + height, + fontSize, + lineHeight, + listStylePosition, + listStyleType, + margin, + maxHeight, + maxWidth, + minHeight, + minWidth, + objectFit, + objectPosition, + opacity, + outline, + overflow, + overscrollBehavior, + padding, + placeholderColor, + placeholderOpacity, + pointerEvents, + position, + inset, + resize, + boxShadow, + fill, + stroke, + strokeWidth, + tableLayout, + textAlign, + textColor, + textOpacity, + fontStyle, + textTransform, + textDecoration, + fontSmoothing, + fontVariantNumeric, + letterSpacing, + userSelect, + verticalAlign, + visibility, + whitespace, + wordBreak, + width, + zIndex, + gap, + gridAutoFlow, + gridTemplateColumns, + gridColumn, + gridColumnStart, + gridColumnEnd, + gridTemplateRows, + gridRow, + gridRowStart, + gridRowEnd, + transform, + transformOrigin, + scale, + rotate, + translate, + skew, + transitionProperty, + transitionTimingFunction, + transitionDuration, + transitionDelay, + animation, +} + export default function({ corePlugins: corePluginConfig }) { - return configurePlugins(corePluginConfig, { - preflight, - container, - space, - divideWidth, - divideColor, - divideStyle, - divideOpacity, - accessibility, - appearance, - backgroundAttachment, - backgroundClip, - backgroundColor, - backgroundImage, - gradientColorStops, - backgroundOpacity, - backgroundPosition, - backgroundRepeat, - backgroundSize, - borderCollapse, - borderColor, - borderOpacity, - borderRadius, - borderStyle, - borderWidth, - boxSizing, - cursor, - display, - flexDirection, - flexWrap, - placeItems, - placeContent, - placeSelf, - alignItems, - alignContent, - alignSelf, - justifyItems, - justifyContent, - justifySelf, - flex, - flexGrow, - flexShrink, - order, - float, - clear, - fontFamily, - fontWeight, - height, - fontSize, - lineHeight, - listStylePosition, - listStyleType, - margin, - maxHeight, - maxWidth, - minHeight, - minWidth, - objectFit, - objectPosition, - opacity, - outline, - overflow, - overscrollBehavior, - padding, - placeholderColor, - placeholderOpacity, - pointerEvents, - position, - inset, - resize, - boxShadow, - fill, - stroke, - strokeWidth, - tableLayout, - textAlign, - textColor, - textOpacity, - fontStyle, - textTransform, - textDecoration, - fontSmoothing, - fontVariantNumeric, - letterSpacing, - userSelect, - verticalAlign, - visibility, - whitespace, - wordBreak, - width, - zIndex, - gap, - gridAutoFlow, - gridTemplateColumns, - gridColumn, - gridColumnStart, - gridColumnEnd, - gridTemplateRows, - gridRow, - gridRowStart, - gridRowEnd, - transform, - transformOrigin, - scale, - rotate, - translate, - skew, - transitionProperty, - transitionTimingFunction, - transitionDuration, - transitionDelay, - animation, + return configurePlugins(corePluginConfig, Object.keys(corePluginList)).map(pluginName => { + return corePluginList[pluginName]() }) } diff --git a/src/index.js b/src/index.js index 4b34ee27bc02..d2def911941f 100644 --- a/src/index.js +++ b/src/index.js @@ -45,7 +45,7 @@ function resolveConfigPath(filePath) { } const getConfigFunction = config => () => { - if (_.isUndefined(config) && !_.isObject(config)) { + if (_.isUndefined(config)) { return resolveConfig([...getAllConfigs(defaultConfig)]) } diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js index a93ad611db0a..4d94b47da835 100644 --- a/src/processTailwindFeatures.js +++ b/src/processTailwindFeatures.js @@ -37,7 +37,7 @@ export default function(getConfig) { [ ...corePlugins(config), ...[flagEnabled(config, 'darkModeVariant') ? darkModeVariantPlugin : () => {}], - ...config.plugins, + ..._.get(config, 'plugins', []), ], config ) diff --git a/src/util/configurePlugins.js b/src/util/configurePlugins.js index 295ae5bf709f..b44b04bdabb8 100644 --- a/src/util/configurePlugins.js +++ b/src/util/configurePlugins.js @@ -1,9 +1,13 @@ export default function(pluginConfig, plugins) { + if (pluginConfig === undefined) { + return plugins + } + const pluginNames = Array.isArray(pluginConfig) ? pluginConfig - : Object.keys(plugins).filter(pluginName => { + : plugins.filter(pluginName => { return pluginConfig !== false && pluginConfig[pluginName] !== false }) - return pluginNames.map(pluginName => plugins[pluginName]()) + return pluginNames } diff --git a/src/util/getAllConfigs.js b/src/util/getAllConfigs.js index b48c003a6128..ef67e06ce8c8 100644 --- a/src/util/getAllConfigs.js +++ b/src/util/getAllConfigs.js @@ -7,9 +7,13 @@ import extendedFontSizeScale from '../flagged/extendedFontSizeScale.js' import darkModeVariant from '../flagged/darkModeVariant.js' import standardFontWeights from '../flagged/standardFontWeights' import additionalBreakpoint from '../flagged/additionalBreakpoint' +import { flatMap, get } from 'lodash' + +export default function getAllConfigs(config, defaultPresets = [defaultConfig]) { + const configs = flatMap([...get(config, 'presets', defaultPresets)].reverse(), preset => { + return getAllConfigs(preset, []) + }) -export default function getAllConfigs(config) { - const configs = [defaultConfig] const features = { uniformColorPalette, extendedSpacingScale, diff --git a/src/util/prefixSelector.js b/src/util/prefixSelector.js index c4448e8af906..7f9c9a95d09f 100644 --- a/src/util/prefixSelector.js +++ b/src/util/prefixSelector.js @@ -2,7 +2,8 @@ import parser from 'postcss-selector-parser' import tap from 'lodash/tap' export default function(prefix, selector) { - const getPrefix = typeof prefix === 'function' ? prefix : () => prefix + const getPrefix = + typeof prefix === 'function' ? prefix : () => (prefix === undefined ? '' : prefix) return parser(selectors => { selectors.walkClasses(classSelector => { diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index d381e4c931cf..1b33d5f343e5 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -74,7 +74,7 @@ export default function(plugins, config) { return config.target === 'browserslist' ? browserslistTarget : config.target } - const [defaultTarget, targetOverrides] = getConfigValue('target') + const [defaultTarget, targetOverrides] = getConfigValue('target', 'relaxed') const target = _.get(targetOverrides, path, defaultTarget) diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index 0af2b1769159..961cc93a1037 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -7,6 +7,8 @@ import map from 'lodash/map' import get from 'lodash/get' import toPath from 'lodash/toPath' import negateValue from './negateValue' +import { corePluginList } from '../corePlugins' +import configurePlugins from './configurePlugins' const configUtils = { negative(scale) { @@ -187,6 +189,17 @@ function resolveVariants([firstConfig, ...variantConfigs]) { }, {}) } +function resolveCorePlugins(corePluginConfigs) { + const result = [...corePluginConfigs].reverse().reduce((resolved, corePluginConfig) => { + if (isFunction(corePluginConfig)) { + return corePluginConfig({ corePlugins: resolved }) + } + return configurePlugins(corePluginConfig, resolved) + }, Object.keys(corePluginList)) + + return result +} + export default function resolveConfig(configs) { const allConfigs = extractPluginConfigs(configs) @@ -196,6 +209,7 @@ export default function resolveConfig(configs) { mergeExtensions(mergeThemes(map(allConfigs, t => get(t, 'theme', {})))) ), variants: resolveVariants(allConfigs.map(c => c.variants)), + corePlugins: resolveCorePlugins(allConfigs.map(c => c.corePlugins)), }, ...allConfigs )