From 03ef8978d9dacc2ff84ee32e8be25a3fdf60b12f Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 22 Oct 2020 14:36:12 -0400 Subject: [PATCH 1/3] WIP --- __tests__/resolveConfig.test.js | 41 +++++++ src/util/resolveConfig.js | 208 +++++++++++++++++++++++--------- 2 files changed, 194 insertions(+), 55 deletions(-) diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index b6c37b1f2995..1eb010822734 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1738,6 +1738,47 @@ test('user theme extensions take precedence over plugin theme extensions with th }) }) +test('variants can be extended', () => { + const userConfig = { + variants: { + borderColor: ({ after }) => after(['group-focus'], 'hover'), + extend: { + backgroundColor: ['active', 'disabled', 'group-hover'], + }, + }, + } + + const otherConfig = { + variants: { + extend: { + textColor: ['hover', 'focus-within'], + }, + }, + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: {}, + variants: { + borderColor: ['hover', 'focus'], + backgroundColor: ['responsive', 'hover', 'focus'], + textColor: ['responsive', 'focus'], + }, + } + + const result = resolveConfig([userConfig, otherConfig, defaultConfig]) + + expect(result).toMatchObject({ + variants: { + borderColor: ['hover', 'group-focus', 'focus'], + backgroundColor: ['responsive', 'group-hover', 'hover', 'focus', 'active', 'disabled'], + textColor: ['responsive', 'focus-within', 'hover', 'focus'], + }, + }) +}) + test('variants can be defined as a function', () => { const userConfig = { variants: { diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index b2adea7c1ddb..fc47be4ffc74 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -5,6 +5,7 @@ import isUndefined from 'lodash/isUndefined' import defaults from 'lodash/defaults' import map from 'lodash/map' import get from 'lodash/get' +import uniq from 'lodash/uniq' import toPath from 'lodash/toPath' import negateValue from './negateValue' import { corePluginList } from '../corePluginList' @@ -39,6 +40,34 @@ function value(valueToResolve, ...args) { return isFunction(valueToResolve) ? valueToResolve(...args) : valueToResolve } +function collectExtends(obj) { + const withoutExtend = (({ extend: _, ...rest }) => rest)( + obj.reduce((merged, o) => { + return defaults(merged, o) + }, {}) + ) + + return { + ...withoutExtend, + + // 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: obj.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 mergeThemes(themes) { const theme = (({ extend: _, ...t }) => t)( themes.reduce((merged, t) => { @@ -130,67 +159,136 @@ function extractPluginConfigs(configs) { return allConfigs } +function poop(resolved, plugin, pluginVariants) { + return pluginVariants({ + variants(path) { + return get(resolved, path, []) + }, + before(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) { + if (variant === undefined) { + return [...toInsert, ...existingPluginVariants] + } + + const index = existingPluginVariants.indexOf(variant) + + if (index === -1) { + return [...existingPluginVariants, ...toInsert] + } + + return [ + ...existingPluginVariants.slice(0, index), + ...toInsert, + ...existingPluginVariants.slice(index), + ] + }, + after(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) { + if (variant === undefined) { + return [...existingPluginVariants, ...toInsert] + } + + const index = existingPluginVariants.indexOf(variant) + + if (index === -1) { + return [...toInsert, ...existingPluginVariants] + } + + return [ + ...existingPluginVariants.slice(0, index + 1), + ...toInsert, + ...existingPluginVariants.slice(index + 1), + ] + }, + without(toRemove, existingPluginVariants = get(resolved, plugin, [])) { + return existingPluginVariants.filter((v) => !toRemove.includes(v)) + }, + }) +} + +function mergeVariants(variants) { + const variantsWithoutExtend = (({ extend: _, ...rest }) => rest)( + variants.reduce((resolved, variants) => { + Object.entries(variants || {}).forEach(([plugin, pluginVariants]) => { + if (isFunction(pluginVariants)) { + resolved[plugin] = poop(resolved, plugin, pluginVariants) + } else { + resolved[plugin] = pluginVariants + } + }) + + return resolved + }, {}) + ) + + return { + ...variantsWithoutExtend, + + // 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: variants.reduce((merged, { extend }) => { + return mergeWith(merged, extend, (mergedValue, extendValue) => { + if (isUndefined(mergedValue)) { + return [extendValue] + } + + if (Array.isArray(mergedValue)) { + return [extendValue, ...mergedValue] + } + + return [extendValue, mergedValue] + }) + }, {}), + } +} + +const defaultVariantSortOrder = [ + 'DEFAULT', + 'dark', + 'motion-safe', + 'motion-reduce', + 'first', + 'last', + 'odd', + 'even', + 'visited', + 'checked', + 'group-hover', + 'group-focus', + 'focus-within', + 'hover', + 'focus', + 'focus-visible', + 'active', + 'disabled', +] + +function mergeVariantExtensions({ extend, ...variants }) { + return mergeWith(variants, extend, (variantsValue, extensions) => { + const merged = uniq([...variantsValue, ...extensions].flat()) + + if (extensions.flat().length === 0) { + return merged + } + + return merged.sort( + (a, z) => defaultVariantSortOrder.indexOf(a) - defaultVariantSortOrder.indexOf(z) + ) + }) +} + function resolveVariants([firstConfig, ...variantConfigs]) { + // Global variants configuration like `variants: ['hover', 'focus']` if (Array.isArray(firstConfig)) { return firstConfig } - return [firstConfig, ...variantConfigs].reverse().reduce((resolved, variants) => { - Object.entries(variants || {}).forEach(([plugin, pluginVariants]) => { - if (isFunction(pluginVariants)) { - resolved[plugin] = pluginVariants({ - variants(path) { - return get(resolved, path, []) - }, - before(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) { - if (variant === undefined) { - return [...toInsert, ...existingPluginVariants] - } - - const index = existingPluginVariants.indexOf(variant) - - if (index === -1) { - return [...existingPluginVariants, ...toInsert] - } - - return [ - ...existingPluginVariants.slice(0, index), - ...toInsert, - ...existingPluginVariants.slice(index), - ] - }, - after(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) { - if (variant === undefined) { - return [...existingPluginVariants, ...toInsert] - } - - const index = existingPluginVariants.indexOf(variant) - - if (index === -1) { - return [...toInsert, ...existingPluginVariants] - } - - return [ - ...existingPluginVariants.slice(0, index + 1), - ...toInsert, - ...existingPluginVariants.slice(index + 1), - ] - }, - without(toRemove, existingPluginVariants = get(resolved, plugin, [])) { - return existingPluginVariants.filter((v) => !toRemove.includes(v)) - }, - }) - } else { - resolved[plugin] = pluginVariants - } - }) - - return resolved - }, {}) + const allVariants = [firstConfig, ...variantConfigs].reverse() + const mergedVariants = mergeVariants(allVariants) + const extensionsMerged = mergeVariantExtensions(mergedVariants) + return extensionsMerged } function resolveCorePlugins(corePluginConfigs) { - const result = [...corePluginConfigs].reverse().reduce((resolved, corePluginConfig) => { + const result = [...corePluginConfigs].reduceRight((resolved, corePluginConfig) => { if (isFunction(corePluginConfig)) { return corePluginConfig({ corePlugins: resolved }) } @@ -201,7 +299,7 @@ function resolveCorePlugins(corePluginConfigs) { } function resolvePluginLists(pluginLists) { - const result = [...pluginLists].reverse().reduce((resolved, pluginList) => { + const result = [...pluginLists].reduceRight((resolved, pluginList) => { return [...resolved, ...pluginList] }, []) @@ -216,7 +314,7 @@ export default function resolveConfig(configs) { theme: resolveFunctionKeys( mergeExtensions(mergeThemes(map(allConfigs, (t) => get(t, 'theme', {})))) ), - variants: resolveVariants(allConfigs.map((c) => c.variants)), + variants: resolveVariants(allConfigs.map((c) => get(c, 'variants', {}))), corePlugins: resolveCorePlugins(allConfigs.map((c) => c.corePlugins)), plugins: resolvePluginLists(configs.map((c) => get(c, 'plugins', []))), }, From 8fba8f46935ee4ec481f86ce8df37562f3e76f6c Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 22 Oct 2020 15:01:41 -0400 Subject: [PATCH 2/3] It's alive --- src/util/resolveConfig.js | 245 +++++++++++++++++--------------------- 1 file changed, 109 insertions(+), 136 deletions(-) diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index fc47be4ffc74..9d20e81def15 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -40,59 +40,29 @@ function value(valueToResolve, ...args) { return isFunction(valueToResolve) ? valueToResolve(...args) : valueToResolve } -function collectExtends(obj) { - const withoutExtend = (({ extend: _, ...rest }) => rest)( - obj.reduce((merged, o) => { - return defaults(merged, o) - }, {}) - ) +function collectExtends(items) { + return items.reduce((merged, { extend }) => { + return mergeWith(merged, extend, (mergedValue, extendValue) => { + if (isUndefined(mergedValue)) { + return [extendValue] + } - return { - ...withoutExtend, + if (Array.isArray(mergedValue)) { + return [extendValue, ...mergedValue] + } - // 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: obj.reduce((merged, { extend }) => { - return mergeWith(merged, extend, (mergedValue, extendValue) => { - if (isUndefined(mergedValue)) { - return [extendValue] - } - - if (Array.isArray(mergedValue)) { - return [extendValue, ...mergedValue] - } - - return [extendValue, mergedValue] - }) - }, {}), - } + return [extendValue, mergedValue] + }) + }, {}) } function mergeThemes(themes) { - const theme = (({ extend: _, ...t }) => t)( - themes.reduce((merged, t) => { - return defaults(merged, t) - }, {}) - ) - return { - ...theme, + ...themes.reduce((merged, theme) => defaults(merged, 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] - }) - }, {}), + extend: collectExtends(themes), } } @@ -159,84 +129,63 @@ function extractPluginConfigs(configs) { return allConfigs } -function poop(resolved, plugin, pluginVariants) { - return pluginVariants({ - variants(path) { - return get(resolved, path, []) - }, - before(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) { - if (variant === undefined) { - return [...toInsert, ...existingPluginVariants] - } - - const index = existingPluginVariants.indexOf(variant) - - if (index === -1) { - return [...existingPluginVariants, ...toInsert] - } - - return [ - ...existingPluginVariants.slice(0, index), - ...toInsert, - ...existingPluginVariants.slice(index), - ] - }, - after(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) { - if (variant === undefined) { - return [...existingPluginVariants, ...toInsert] - } - - const index = existingPluginVariants.indexOf(variant) - - if (index === -1) { - return [...toInsert, ...existingPluginVariants] +function mergeVariants(variants) { + const mergedVariants = variants.reduce((resolved, variants) => { + Object.entries(variants || {}).forEach(([plugin, pluginVariants]) => { + if (isFunction(pluginVariants)) { + resolved[plugin] = pluginVariants({ + variants(path) { + return get(resolved, path, []) + }, + before(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) { + if (variant === undefined) { + return [...toInsert, ...existingPluginVariants] + } + + const index = existingPluginVariants.indexOf(variant) + + if (index === -1) { + return [...existingPluginVariants, ...toInsert] + } + + return [ + ...existingPluginVariants.slice(0, index), + ...toInsert, + ...existingPluginVariants.slice(index), + ] + }, + after(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) { + if (variant === undefined) { + return [...existingPluginVariants, ...toInsert] + } + + const index = existingPluginVariants.indexOf(variant) + + if (index === -1) { + return [...toInsert, ...existingPluginVariants] + } + + return [ + ...existingPluginVariants.slice(0, index + 1), + ...toInsert, + ...existingPluginVariants.slice(index + 1), + ] + }, + without(toRemove, existingPluginVariants = get(resolved, plugin, [])) { + return existingPluginVariants.filter((v) => !toRemove.includes(v)) + }, + }) + } else { + resolved[plugin] = pluginVariants } + }) - return [ - ...existingPluginVariants.slice(0, index + 1), - ...toInsert, - ...existingPluginVariants.slice(index + 1), - ] - }, - without(toRemove, existingPluginVariants = get(resolved, plugin, [])) { - return existingPluginVariants.filter((v) => !toRemove.includes(v)) - }, - }) -} - -function mergeVariants(variants) { - const variantsWithoutExtend = (({ extend: _, ...rest }) => rest)( - variants.reduce((resolved, variants) => { - Object.entries(variants || {}).forEach(([plugin, pluginVariants]) => { - if (isFunction(pluginVariants)) { - resolved[plugin] = poop(resolved, plugin, pluginVariants) - } else { - resolved[plugin] = pluginVariants - } - }) - - return resolved - }, {}) - ) + return resolved + }, {}) return { - ...variantsWithoutExtend, - - // 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: variants.reduce((merged, { extend }) => { - return mergeWith(merged, extend, (mergedValue, extendValue) => { - if (isUndefined(mergedValue)) { - return [extendValue] - } - - if (Array.isArray(mergedValue)) { - return [extendValue, ...mergedValue] - } - - return [extendValue, mergedValue] - }) - }, {}), + ...mergedVariants, + extend: collectExtends(variants), } } @@ -261,7 +210,7 @@ const defaultVariantSortOrder = [ 'disabled', ] -function mergeVariantExtensions({ extend, ...variants }) { +function mergeVariantExtensions({ extend, ...variants }, variantOrder) { return mergeWith(variants, extend, (variantsValue, extensions) => { const merged = uniq([...variantsValue, ...extensions].flat()) @@ -269,22 +218,20 @@ function mergeVariantExtensions({ extend, ...variants }) { return merged } - return merged.sort( - (a, z) => defaultVariantSortOrder.indexOf(a) - defaultVariantSortOrder.indexOf(z) - ) + return merged.sort((a, z) => variantOrder.indexOf(a) - variantOrder.indexOf(z)) }) } -function resolveVariants([firstConfig, ...variantConfigs]) { +function resolveVariants([firstConfig, ...variantConfigs], variantOrder) { // Global variants configuration like `variants: ['hover', 'focus']` if (Array.isArray(firstConfig)) { return firstConfig } - const allVariants = [firstConfig, ...variantConfigs].reverse() - const mergedVariants = mergeVariants(allVariants) - const extensionsMerged = mergeVariantExtensions(mergedVariants) - return extensionsMerged + return mergeVariantExtensions( + mergeVariants([firstConfig, ...variantConfigs].reverse()), + variantOrder + ) } function resolveCorePlugins(corePluginConfigs) { @@ -307,23 +254,49 @@ function resolvePluginLists(pluginLists) { } export default function resolveConfig(configs) { - const allConfigs = extractPluginConfigs(configs) + const allConfigs = [ + ...extractPluginConfigs(configs), + { + darkMode: false, + prefix: '', + important: false, + separator: ':', + variantOrder: [ + 'DEFAULT', + 'dark', + 'motion-safe', + 'motion-reduce', + 'first', + 'last', + 'odd', + 'even', + 'visited', + 'checked', + 'group-hover', + 'group-focus', + 'focus-within', + 'hover', + 'focus', + 'focus-visible', + 'active', + 'disabled', + ], + }, + ] + const { variantOrder } = allConfigs.find((c) => c.variantOrder) return defaults( { theme: resolveFunctionKeys( mergeExtensions(mergeThemes(map(allConfigs, (t) => get(t, 'theme', {})))) ), - variants: resolveVariants(allConfigs.map((c) => get(c, 'variants', {}))), + variants: resolveVariants( + allConfigs.map((c) => get(c, 'variants', {})), + variantOrder + ), corePlugins: resolveCorePlugins(allConfigs.map((c) => c.corePlugins)), plugins: resolvePluginLists(configs.map((c) => get(c, 'plugins', []))), }, - ...allConfigs, - { - darkMode: false, - prefix: '', - important: false, - separator: ':', - } + ...allConfigs ) } From f35b46ae6242b0890daddeb26aea13c3305e6eb0 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 22 Oct 2020 16:22:23 -0400 Subject: [PATCH 3/3] Pull default variant order from config --- __tests__/resolveConfig.test.js | 86 +++++++++++++++++++++++++++++++++ src/util/resolveConfig.js | 43 +---------------- stubs/defaultConfig.stub.js | 16 ++++++ stubs/simpleConfig.stub.js | 4 +- 4 files changed, 107 insertions(+), 42 deletions(-) diff --git a/__tests__/resolveConfig.test.js b/__tests__/resolveConfig.test.js index 1eb010822734..7295717a65b7 100644 --- a/__tests__/resolveConfig.test.js +++ b/__tests__/resolveConfig.test.js @@ -1779,6 +1779,92 @@ test('variants can be extended', () => { }) }) +test('variant sort order can be customized', () => { + const userConfig = { + variantOrder: [ + 'disabled', + 'focus', + 'group-hover', + 'focus-within', + 'active', + 'hover', + 'responsive', + ], + variants: { + borderColor: ({ after }) => after(['group-focus'], 'hover'), + extend: { + backgroundColor: ['active', 'disabled', 'group-hover'], + }, + }, + } + + const otherConfig = { + variants: { + extend: { + textColor: ['hover', 'focus-within'], + }, + }, + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: {}, + variants: { + borderColor: ['hover', 'focus'], + backgroundColor: ['responsive', 'hover', 'focus'], + textColor: ['responsive', 'focus'], + }, + } + + const result = resolveConfig([userConfig, otherConfig, defaultConfig]) + + expect(result).toMatchObject({ + variants: { + borderColor: ['hover', 'group-focus', 'focus'], + backgroundColor: ['disabled', 'focus', 'group-hover', 'active', 'hover', 'responsive'], + textColor: ['focus', 'focus-within', 'hover', 'responsive'], + }, + }) +}) + +test('custom variants go to the beginning by default when sort is applied', () => { + const userConfig = { + variants: { + extend: { + backgroundColor: ['active', 'custom-variant-1', 'group-hover', 'custom-variant-2'], + }, + }, + } + + const defaultConfig = { + prefix: '', + important: false, + separator: ':', + theme: {}, + variants: { + backgroundColor: ['responsive', 'hover', 'focus'], + }, + } + + const result = resolveConfig([userConfig, defaultConfig]) + + expect(result).toMatchObject({ + variants: { + backgroundColor: [ + 'responsive', + 'custom-variant-1', + 'custom-variant-2', + 'group-hover', + 'hover', + 'focus', + 'active', + ], + }, + }) +}) + test('variants can be defined as a function', () => { const userConfig = { variants: { diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js index 9d20e81def15..f682214b175f 100644 --- a/src/util/resolveConfig.js +++ b/src/util/resolveConfig.js @@ -10,6 +10,7 @@ import toPath from 'lodash/toPath' import negateValue from './negateValue' import { corePluginList } from '../corePluginList' import configurePlugins from './configurePlugins' +import defaultConfig from '../../stubs/defaultConfig.stub' const configUtils = { negative(scale) { @@ -189,27 +190,6 @@ function mergeVariants(variants) { } } -const defaultVariantSortOrder = [ - 'DEFAULT', - 'dark', - 'motion-safe', - 'motion-reduce', - 'first', - 'last', - 'odd', - 'even', - 'visited', - 'checked', - 'group-hover', - 'group-focus', - 'focus-within', - 'hover', - 'focus', - 'focus-visible', - 'active', - 'disabled', -] - function mergeVariantExtensions({ extend, ...variants }, variantOrder) { return mergeWith(variants, extend, (variantsValue, extensions) => { const merged = uniq([...variantsValue, ...extensions].flat()) @@ -261,26 +241,7 @@ export default function resolveConfig(configs) { prefix: '', important: false, separator: ':', - variantOrder: [ - 'DEFAULT', - 'dark', - 'motion-safe', - 'motion-reduce', - 'first', - 'last', - 'odd', - 'even', - 'visited', - 'checked', - 'group-hover', - 'group-focus', - 'focus-within', - 'hover', - 'focus', - 'focus-visible', - 'active', - 'disabled', - ], + variantOrder: defaultConfig.variantOrder, }, ] const { variantOrder } = allConfigs.find((c) => c.variantOrder) diff --git a/stubs/defaultConfig.stub.js b/stubs/defaultConfig.stub.js index f93022f79e79..b4928ed052bf 100644 --- a/stubs/defaultConfig.stub.js +++ b/stubs/defaultConfig.stub.js @@ -668,6 +668,22 @@ module.exports = { }, }, }, + variantOrder: [ + 'first', + 'last', + 'odd', + 'even', + 'visited', + 'checked', + 'group-hover', + 'group-focus', + 'focus-within', + 'hover', + 'focus', + 'focus-visible', + 'active', + 'disabled', + ], variants: { accessibility: ['responsive', 'focus'], alignContent: ['responsive'], diff --git a/stubs/simpleConfig.stub.js b/stubs/simpleConfig.stub.js index 4e246b6ba5d4..db401eb7e7d8 100644 --- a/stubs/simpleConfig.stub.js +++ b/stubs/simpleConfig.stub.js @@ -4,6 +4,8 @@ module.exports = { theme: { extend: {}, }, - variants: {}, + variants: { + extend: {}, + }, plugins: [], }