Merge pull request #1162 from tailwindcss/allow-plugins-to-modify-config
Allow plugins to add their own config file to be resolved with the user's custom config
This commit is contained in:
commit
7a7303b3a9
8 changed files with 633 additions and 16 deletions
|
|
@ -9,6 +9,7 @@
|
|||
"extends": ["eslint-config-postcss", "prettier"],
|
||||
"plugins": ["prettier"],
|
||||
"rules": {
|
||||
"no-unused-vars": [2, {"args": "all", "argsIgnorePattern": "^_"}],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,84 @@ 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({ 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
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1234,3 +1234,480 @@ 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'],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('plugin config modifications are applied', () => {
|
||||
const userConfig = {
|
||||
plugins: [
|
||||
{
|
||||
config: {
|
||||
prefix: 'tw-',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
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: [
|
||||
{
|
||||
config: {
|
||||
prefix: 'tw-',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
|
||||
test('plugin config can register plugins that also have config', () => {
|
||||
const userConfig = {
|
||||
plugins: [
|
||||
{
|
||||
config: {
|
||||
prefix: 'tw-',
|
||||
plugins: [
|
||||
{
|
||||
config: {
|
||||
important: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
config: {
|
||||
separator: '__',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
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() {},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
|
||||
test('plugin theme extensions are added even if user overrides top-level theme config', () => {
|
||||
const userConfig = {
|
||||
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,
|
||||
})
|
||||
})
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,10 +55,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([configObject, defaultConfig])
|
||||
}
|
||||
|
||||
const plugin = postcss.plugin('tailwind', config => {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function updateSource(nodes, source) {
|
|||
}
|
||||
|
||||
export default function(
|
||||
config,
|
||||
_config,
|
||||
{ base: pluginBase, components: pluginComponents, utilities: pluginUtilities }
|
||||
) {
|
||||
return function(css) {
|
||||
|
|
|
|||
|
|
@ -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 : _.get(plugin, 'handler', () => {})
|
||||
|
||||
handler({
|
||||
postcss,
|
||||
config: getConfigValue,
|
||||
theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import some from 'lodash/some'
|
||||
import mergeWith from 'lodash/mergeWith'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import isUndefined from 'lodash/isUndefined'
|
||||
import defaults from 'lodash/defaults'
|
||||
import map from 'lodash/map'
|
||||
import get from 'lodash/get'
|
||||
import toPath from 'lodash/toPath'
|
||||
import negateValue from './negateValue'
|
||||
|
||||
|
|
@ -23,18 +26,47 @@ 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)
|
||||
}, {})
|
||||
)
|
||||
|
||||
return {
|
||||
...theme,
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
if (Array.isArray(mergedValue)) {
|
||||
return [extendValue, ...mergedValue]
|
||||
}
|
||||
|
||||
return [extendValue, mergedValue]
|
||||
})
|
||||
}, {}),
|
||||
}
|
||||
}
|
||||
|
||||
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))),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -62,16 +94,40 @@ function resolveFunctionKeys(object) {
|
|||
}, {})
|
||||
}
|
||||
|
||||
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(
|
||||
{
|
||||
theme: resolveFunctionKeys(mergeExtensions(defaults({}, ...map(configs, 'theme')))),
|
||||
theme: resolveFunctionKeys(
|
||||
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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue