From 9d05910507dda8d7477dfe0c2d503b4ddb580125 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 22 Jun 2018 10:35:47 -0400 Subject: [PATCH 1/7] Generate variants based on the order specified in the modules config --- __tests__/variantsAtRule.test.js | 27 ++++++++++++++++++++++++++- src/lib/substituteVariantsAtRules.js | 8 +++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/__tests__/variantsAtRule.test.js b/__tests__/variantsAtRule.test.js index 51a462a72..43f9899e6 100644 --- a/__tests__/variantsAtRule.test.js +++ b/__tests__/variantsAtRule.test.js @@ -92,7 +92,7 @@ test('it can generate group-hover variants', () => { test('it can generate hover, active and focus variants', () => { const input = ` - @variants hover, active, group-hover, focus { + @variants group-hover, hover, focus, active { .banana { color: yellow; } .chocolate { color: brown; } } @@ -141,3 +141,28 @@ test('it wraps the output in a responsive at-rule if responsive is included as a expect(result.warnings().length).toBe(0) }) }) + +test('variants are generated in the order specified', () => { + const input = ` + @variants focus, active, hover { + .banana { color: yellow; } + .chocolate { color: brown; } + } + ` + + const output = ` + .banana { color: yellow; } + .chocolate { color: brown; } + .focus\\:banana:focus { color: yellow; } + .focus\\:chocolate:focus { color: brown; } + .active\\:banana:active { color: yellow; } + .active\\:chocolate:active { color: brown; } + .hover\\:banana:hover { color: yellow; } + .hover\\:chocolate:hover { color: brown; } + ` + + return run(input).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) diff --git a/src/lib/substituteVariantsAtRules.js b/src/lib/substituteVariantsAtRules.js index 5ddaa8ea5..67e1e4595 100644 --- a/src/lib/substituteVariantsAtRules.js +++ b/src/lib/substituteVariantsAtRules.js @@ -42,7 +42,7 @@ export default function(config) { const unwrappedConfig = config() css.walkAtRules('variants', atRule => { - const variants = postcss.list.comma(atRule.params) + const variants = postcss.list.comma(atRule.params).filter(variant => variant !== '') if (variants.includes('responsive')) { const responsiveParent = postcss.atRule({ name: 'responsive' }) @@ -52,10 +52,8 @@ export default function(config) { atRule.before(atRule.clone().nodes) - _.forEach(['group-hover', 'hover', 'focus', 'active'], variant => { - if (variants.includes(variant)) { - variantGenerators[variant](atRule, unwrappedConfig) - } + _.forEach(_.without(variants, 'responsive'), variant => { + variantGenerators[variant](atRule, unwrappedConfig) }) atRule.remove() From 9eca69ad831ac154c2ebb9c7bdfe9341118ecaeb Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 22 Jun 2018 12:08:54 -0400 Subject: [PATCH 2/7] Refactor process plugins to return an object --- __tests__/containerPlugin.test.js | 12 +++---- __tests__/processPlugins.test.js | 52 ++++++++++++++-------------- src/lib/substituteTailwindAtRules.js | 4 ++- src/util/processPlugins.js | 7 +++- 4 files changed, 41 insertions(+), 34 deletions(-) diff --git a/__tests__/containerPlugin.test.js b/__tests__/containerPlugin.test.js index a82ccf18d..04cb3dba9 100644 --- a/__tests__/containerPlugin.test.js +++ b/__tests__/containerPlugin.test.js @@ -26,7 +26,7 @@ function processPluginsWithValidConfig(config) { } test('options are not required', () => { - const [components] = processPluginsWithValidConfig({ + const { components } = processPluginsWithValidConfig({ plugins: [container()], }) @@ -48,7 +48,7 @@ test('options are not required', () => { }) test('screens can be specified explicitly', () => { - const [components] = processPluginsWithValidConfig({ + const { components } = processPluginsWithValidConfig({ plugins: [ container({ screens: { @@ -71,7 +71,7 @@ test('screens can be specified explicitly', () => { }) test('screens can be an array', () => { - const [components] = processPluginsWithValidConfig({ + const { components } = processPluginsWithValidConfig({ plugins: [ container({ screens: ['400px', '500px'], @@ -91,7 +91,7 @@ test('screens can be an array', () => { }) test('the container can be centered by default', () => { - const [components] = processPluginsWithValidConfig({ + const { components } = processPluginsWithValidConfig({ plugins: [ container({ center: true, @@ -121,7 +121,7 @@ test('the container can be centered by default', () => { }) test('horizontal padding can be included by default', () => { - const [components] = processPluginsWithValidConfig({ + const { components } = processPluginsWithValidConfig({ plugins: [ container({ padding: '2rem', @@ -151,7 +151,7 @@ test('horizontal padding can be included by default', () => { }) test('setting all options at once', () => { - const [components] = processPluginsWithValidConfig({ + const { components } = processPluginsWithValidConfig({ plugins: [ container({ screens: { diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index d5958c476..3115ad088 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -19,7 +19,7 @@ function processPluginsWithValidConfig(config) { } test('plugins can create utilities with object syntax', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ plugins: [ function({ addUtilities }) { addUtilities({ @@ -54,7 +54,7 @@ test('plugins can create utilities with object syntax', () => { }) test('plugins can create utilities with arrays of objects', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ plugins: [ function({ addUtilities }) { addUtilities([ @@ -95,7 +95,7 @@ test('plugins can create utilities with arrays of objects', () => { }) test('plugins can create utilities with raw PostCSS nodes', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ plugins: [ function({ addUtilities }) { addUtilities([ @@ -139,7 +139,7 @@ test('plugins can create utilities with raw PostCSS nodes', () => { }) test('plugins can create utilities with mixed object styles and PostCSS nodes', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ plugins: [ function({ addUtilities }) { addUtilities([ @@ -182,7 +182,7 @@ test('plugins can create utilities with mixed object styles and PostCSS nodes', }) test('plugins can create utilities with variants', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ plugins: [ function({ addUtilities }) { addUtilities( @@ -220,7 +220,7 @@ test('plugins can create utilities with variants', () => { }) test('plugins can create components with object syntax', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ plugins: [ function({ addComponents }) { addComponents({ @@ -253,7 +253,7 @@ test('plugins can create components with object syntax', () => { }) test('plugins can create components with raw PostCSS nodes', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ plugins: [ function({ addComponents }) { addComponents([ @@ -301,7 +301,7 @@ test('plugins can create components with raw PostCSS nodes', () => { }) test('plugins can create components with mixed object styles and raw PostCSS nodes', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ plugins: [ function({ addComponents }) { addComponents([ @@ -348,7 +348,7 @@ test('plugins can create components with mixed object styles and raw PostCSS nod }) test('plugins can create components with media queries with object syntax', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ plugins: [ function({ addComponents }) { addComponents({ @@ -399,7 +399,7 @@ test('plugins can create components with media queries with object syntax', () = }) test('media queries can be defined multiple times using objects-in-array syntax', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ plugins: [ function({ addComponents }) { addComponents([ @@ -452,7 +452,7 @@ test('media queries can be defined multiple times using objects-in-array syntax' }) test('plugins can create nested rules', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ plugins: [ function({ addComponents }) { addComponents({ @@ -519,7 +519,7 @@ test('plugins can create rules with escaped selectors', () => { ], } - const [components, utilities] = processPluginsWithValidConfig(config) + const { components, utilities } = processPluginsWithValidConfig(config) expect(components.length).toBe(0) expect(css(utilities)).toMatchCss(` @@ -532,7 +532,7 @@ test('plugins can create rules with escaped selectors', () => { }) test('plugins can access the current config', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ screens: { sm: '576px', md: '768px', @@ -593,7 +593,7 @@ test('plugins can access the current config', () => { }) test('plugins can provide fallbacks to keys missing from the config', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ borderRadius: { '1': '1px', '2': '2px', @@ -620,7 +620,7 @@ test('plugins can provide fallbacks to keys missing from the config', () => { }) test('variants are optional when adding utilities', () => { - const [, utilities] = processPluginsWithValidConfig({ + const { utilities } = processPluginsWithValidConfig({ plugins: [ function({ addUtilities }) { addUtilities({ @@ -642,7 +642,7 @@ test('variants are optional when adding utilities', () => { }) test('plugins can add multiple sets of utilities and components', () => { - const [components, utilities] = processPluginsWithValidConfig({ + const { components, utilities } = processPluginsWithValidConfig({ plugins: [ function({ addUtilities, addComponents }) { addComponents({ @@ -699,7 +699,7 @@ test('plugins can add multiple sets of utilities and components', () => { }) test('plugins respect prefix and important options by default when adding utilities', () => { - const [, utilities] = processPluginsWithValidConfig({ + const { utilities } = processPluginsWithValidConfig({ plugins: [ function({ addUtilities }) { addUtilities({ @@ -725,7 +725,7 @@ test('plugins respect prefix and important options by default when adding utilit }) test("component declarations respect the 'prefix' option by default", () => { - const [components] = processPluginsWithValidConfig({ + const { components } = processPluginsWithValidConfig({ plugins: [ function({ addComponents }) { addComponents({ @@ -748,7 +748,7 @@ test("component declarations respect the 'prefix' option by default", () => { }) test("component declarations can optionally ignore 'prefix' option", () => { - const [components] = processPluginsWithValidConfig({ + const { components } = processPluginsWithValidConfig({ plugins: [ function({ addComponents }) { addComponents( @@ -774,7 +774,7 @@ test("component declarations can optionally ignore 'prefix' option", () => { }) test("component declarations are not affected by the 'important' option", () => { - const [components] = processPluginsWithValidConfig({ + const { components } = processPluginsWithValidConfig({ plugins: [ function({ addComponents }) { addComponents({ @@ -797,7 +797,7 @@ test("component declarations are not affected by the 'important' option", () => }) test("plugins can apply the user's chosen prefix to components manually", () => { - const [components] = processPluginsWithValidConfig({ + const { components } = processPluginsWithValidConfig({ plugins: [ function({ addComponents, prefix }) { addComponents( @@ -823,7 +823,7 @@ test("plugins can apply the user's chosen prefix to components manually", () => }) test('prefix can optionally be ignored for utilities', () => { - const [, utilities] = processPluginsWithValidConfig({ + const { utilities } = processPluginsWithValidConfig({ plugins: [ function({ addUtilities }) { addUtilities( @@ -854,7 +854,7 @@ test('prefix can optionally be ignored for utilities', () => { }) test('important can optionally be ignored for utilities', () => { - const [, utilities] = processPluginsWithValidConfig({ + const { utilities } = processPluginsWithValidConfig({ plugins: [ function({ addUtilities }) { addUtilities( @@ -885,7 +885,7 @@ test('important can optionally be ignored for utilities', () => { }) test('variants can still be specified when ignoring prefix and important options', () => { - const [, utilities] = processPluginsWithValidConfig({ + const { utilities } = processPluginsWithValidConfig({ plugins: [ function({ addUtilities }) { addUtilities( @@ -909,7 +909,7 @@ test('variants can still be specified when ignoring prefix and important options }) expect(css(utilities)).toMatchCss(` - @variants responsive, hover, focus{ + @variants responsive, hover, focus { .rotate-90 { transform: rotate(90deg) } @@ -918,7 +918,7 @@ test('variants can still be specified when ignoring prefix and important options }) test('prefix will prefix all classes in a selector', () => { - const [components] = processPluginsWithValidConfig({ + const { components } = processPluginsWithValidConfig({ plugins: [ function({ addComponents, prefix }) { addComponents( diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index c03dbbace..56bf1ed3b 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -9,7 +9,9 @@ export default function(config) { return function(css) { const unwrappedConfig = config() - const [pluginComponents, pluginUtilities] = processPlugins(unwrappedConfig) + const { components: pluginComponents, utilities: pluginUtilities } = processPlugins( + unwrappedConfig + ) css.walkAtRules('tailwind', atRule => { if (atRule.params === 'preflight') { diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index 0701751f0..320dc5c8e 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -17,6 +17,7 @@ function parseStyles(styles) { export default function(config) { const pluginComponents = [] const pluginUtilities = [] + const pluginVariantGenerators = {} config.plugins.forEach(plugin => { plugin({ @@ -62,5 +63,9 @@ export default function(config) { }) }) - return [pluginComponents, pluginUtilities] + return { + components: pluginComponents, + utilities: pluginUtilities, + variantGenerators: pluginVariantGenerators, + } } From d77bc055eeeaa90b65f45e996c92857580d9a17a Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 22 Jun 2018 12:30:30 -0400 Subject: [PATCH 3/7] Support for basic variant generator plugins Allows you to write a plugin that registers a new variant but only allows you to modify the selector (like what our built-in generators do.) Next steps are to support variants that wrap rules with at-rules (like @supports for example), variants that can modify properties (as opposed to just selectors), and to give variant plugin authors control over how responsive variants interact with their own variants. --- .eslintrc | 5 ++++- __tests__/variantsAtRule.test.js | 31 ++++++++++++++++++++++++++++ src/lib/substituteVariantsAtRules.js | 7 ++++++- src/util/processPlugins.js | 18 ++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/.eslintrc b/.eslintrc index ff4ccca48..87712fea0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,7 +4,10 @@ }, "parserOptions": { "ecmaVersion": 6, - "sourceType": "module" + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true + } }, "extends": ["eslint-config-postcss", "prettier"], "plugins": ["prettier"], diff --git a/__tests__/variantsAtRule.test.js b/__tests__/variantsAtRule.test.js index 43f9899e6..3ef28a210 100644 --- a/__tests__/variantsAtRule.test.js +++ b/__tests__/variantsAtRule.test.js @@ -166,3 +166,34 @@ test('variants are generated in the order specified', () => { expect(result.warnings().length).toBe(0) }) }) + +test('plugin variants work', () => { + const input = ` + @variants first-child { + .banana { color: yellow; } + .chocolate { color: brown; } + } + ` + + const output = ` + .banana { color: yellow; } + .chocolate { color: brown; } + .first-child\\:banana:first-child { color: yellow; } + .first-child\\:chocolate:first-child { color: brown; } + ` + + return run(input, () => ({ + ...config, + plugins: [ + ...config.plugins, + function({ addVariant }) { + addVariant('first-child', ({ className, separator }) => { + return `.first-child${separator}${className}:first-child` + }) + }, + ], + })).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) diff --git a/src/lib/substituteVariantsAtRules.js b/src/lib/substituteVariantsAtRules.js index 67e1e4595..33f1f944b 100644 --- a/src/lib/substituteVariantsAtRules.js +++ b/src/lib/substituteVariantsAtRules.js @@ -1,6 +1,7 @@ import _ from 'lodash' import postcss from 'postcss' import buildSelectorVariant from '../util/buildSelectorVariant' +import processPlugins from '../util/processPlugins' function buildPseudoClassVariant(selector, pseudoClass, separator) { return `${buildSelectorVariant(selector, pseudoClass, separator)}:${pseudoClass}` @@ -18,7 +19,7 @@ function generatePseudoClassVariant(pseudoClass) { } } -const variantGenerators = { +const defaultVariantGenerators = { 'group-hover': (container, { options: { separator } }) => { const cloned = container.clone() @@ -40,6 +41,10 @@ const variantGenerators = { export default function(config) { return function(css) { const unwrappedConfig = config() + const variantGenerators = { + ...defaultVariantGenerators, + ...processPlugins(unwrappedConfig).variantGenerators, + } css.walkAtRules('variants', atRule => { const variants = postcss.list.comma(atRule.params).filter(variant => variant !== '') diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index 320dc5c8e..c8665f8c9 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -14,6 +14,21 @@ function parseStyles(styles) { return _.flatMap(styles, style => (style instanceof Node ? style : parseObjectStyles(style))) } +function generateVariantFunction(generator) { + return (container, config) => { + const cloned = container.clone() + + cloned.walkRules(rule => { + rule.selector = generator({ + className: rule.selector.slice(1), + separator: escapeClassName(config.options.separator), + }) + }) + + container.before(cloned.nodes) + } +} + export default function(config) { const pluginComponents = [] const pluginUtilities = [] @@ -60,6 +75,9 @@ export default function(config) { pluginComponents.push(...styles.nodes) }, + addVariant: (name, generator) => { + pluginVariantGenerators[name] = generateVariantFunction(generator) + }, }) }) From 64cda2f44b7360f4460086820c15cf0828894388 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 22 Jun 2018 12:43:24 -0400 Subject: [PATCH 4/7] Refactor duplication to use generateVariantFunction --- src/lib/substituteVariantsAtRules.js | 17 ++++------------- src/util/generateVariantFunction.js | 16 ++++++++++++++++ src/util/processPlugins.js | 16 +--------------- 3 files changed, 21 insertions(+), 28 deletions(-) create mode 100644 src/util/generateVariantFunction.js diff --git a/src/lib/substituteVariantsAtRules.js b/src/lib/substituteVariantsAtRules.js index 33f1f944b..2d12d1b86 100644 --- a/src/lib/substituteVariantsAtRules.js +++ b/src/lib/substituteVariantsAtRules.js @@ -1,22 +1,13 @@ import _ from 'lodash' import postcss from 'postcss' import buildSelectorVariant from '../util/buildSelectorVariant' +import generateVariantFunction from '../util/generateVariantFunction' import processPlugins from '../util/processPlugins' -function buildPseudoClassVariant(selector, pseudoClass, separator) { - return `${buildSelectorVariant(selector, pseudoClass, separator)}:${pseudoClass}` -} - function generatePseudoClassVariant(pseudoClass) { - return (container, config) => { - const cloned = container.clone() - - cloned.walkRules(rule => { - rule.selector = buildPseudoClassVariant(rule.selector, pseudoClass, config.options.separator) - }) - - container.before(cloned.nodes) - } + return generateVariantFunction(({ className, separator }) => { + return `.${pseudoClass}${separator}${className}:${pseudoClass}` + }) } const defaultVariantGenerators = { diff --git a/src/util/generateVariantFunction.js b/src/util/generateVariantFunction.js new file mode 100644 index 000000000..dff63acfe --- /dev/null +++ b/src/util/generateVariantFunction.js @@ -0,0 +1,16 @@ +import escapeClassName from './escapeClassName' + +export default function generateVariantFunction(generator) { + return (container, config) => { + const cloned = container.clone() + + cloned.walkRules(rule => { + rule.selector = generator({ + className: rule.selector.slice(1), + separator: escapeClassName(config.options.separator), + }) + }) + + container.before(cloned.nodes) + } +} diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index c8665f8c9..bc7dd6bdb 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -2,6 +2,7 @@ import _ from 'lodash' import postcss from 'postcss' import Node from 'postcss/lib/node' import escapeClassName from '../util/escapeClassName' +import generateVariantFunction from '../util/generateVariantFunction' import parseObjectStyles from '../util/parseObjectStyles' import prefixSelector from '../util/prefixSelector' import wrapWithVariants from '../util/wrapWithVariants' @@ -14,21 +15,6 @@ function parseStyles(styles) { return _.flatMap(styles, style => (style instanceof Node ? style : parseObjectStyles(style))) } -function generateVariantFunction(generator) { - return (container, config) => { - const cloned = container.clone() - - cloned.walkRules(rule => { - rule.selector = generator({ - className: rule.selector.slice(1), - separator: escapeClassName(config.options.separator), - }) - }) - - container.before(cloned.nodes) - } -} - export default function(config) { const pluginComponents = [] const pluginUtilities = [] From b21d258f636538ff2eb522d66135e384e09aae66 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 22 Jun 2018 20:52:59 -0400 Subject: [PATCH 5/7] Wrap Tailwind plugins in new plugin to only unwrap config once --- __tests__/applyAtRule.test.js | 2 +- __tests__/configFunction.test.js | 2 +- __tests__/responsiveAtRule.test.js | 30 +++++++++---------- __tests__/variantsAtRule.test.js | 6 ++-- src/index.js | 40 +++++++++----------------- src/lib/evaluateTailwindFunctions.js | 2 +- src/lib/substituteResponsiveAtRules.js | 4 +-- src/lib/substituteScreenAtRules.js | 6 ++-- src/lib/substituteTailwindAtRules.js | 10 +++---- src/lib/substituteVariantsAtRules.js | 5 ++-- src/processTailwindFeatures.js | 25 ++++++++++++++++ 11 files changed, 70 insertions(+), 62 deletions(-) create mode 100644 src/processTailwindFeatures.js diff --git a/__tests__/applyAtRule.test.js b/__tests__/applyAtRule.test.js index 386925afa..4de99ffdb 100644 --- a/__tests__/applyAtRule.test.js +++ b/__tests__/applyAtRule.test.js @@ -1,7 +1,7 @@ import postcss from 'postcss' import plugin from '../src/lib/substituteClassApplyAtRules' -function run(input, opts = () => {}) { +function run(input, opts = {}) { return postcss([plugin(opts)]).process(input, { from: undefined }) } diff --git a/__tests__/configFunction.test.js b/__tests__/configFunction.test.js index 0b58e53e0..5f6037294 100644 --- a/__tests__/configFunction.test.js +++ b/__tests__/configFunction.test.js @@ -2,7 +2,7 @@ import postcss from 'postcss' import plugin from '../src/lib/evaluateTailwindFunctions' function run(input, opts = {}) { - return postcss([plugin(() => opts)]).process(input, { from: undefined }) + return postcss([plugin(opts)]).process(input, { from: undefined }) } test('it looks up values in the config using dot notation', () => { diff --git a/__tests__/responsiveAtRule.test.js b/__tests__/responsiveAtRule.test.js index 783fbf79b..49d64551a 100644 --- a/__tests__/responsiveAtRule.test.js +++ b/__tests__/responsiveAtRule.test.js @@ -2,7 +2,7 @@ import postcss from 'postcss' import plugin from '../src/lib/substituteResponsiveAtRules' import config from '../defaultConfig.stub.js' -function run(input, opts = () => config) { +function run(input, opts = config) { return postcss([plugin(opts)]).process(input, { from: undefined }) } @@ -31,7 +31,7 @@ test('it can generate responsive variants', () => { } ` - return run(input, () => ({ + return run(input, { screens: { sm: '500px', md: '750px', @@ -40,7 +40,7 @@ test('it can generate responsive variants', () => { options: { separator: ':', }, - })).then(result => { + }).then(result => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) @@ -71,7 +71,7 @@ test('it can generate responsive variants with a custom separator', () => { } ` - return run(input, () => ({ + return run(input, { screens: { sm: '500px', md: '750px', @@ -80,7 +80,7 @@ test('it can generate responsive variants with a custom separator', () => { options: { separator: '__', }, - })).then(result => { + }).then(result => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) @@ -117,7 +117,7 @@ test('responsive variants are grouped', () => { } ` - return run(input, () => ({ + return run(input, { screens: { sm: '500px', md: '750px', @@ -126,7 +126,7 @@ test('responsive variants are grouped', () => { options: { separator: ':', }, - })).then(result => { + }).then(result => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) @@ -152,7 +152,7 @@ test('screen prefix is only applied to the last class in a selector', () => { } ` - return run(input, () => ({ + return run(input, { screens: { sm: '500px', md: '750px', @@ -161,7 +161,7 @@ test('screen prefix is only applied to the last class in a selector', () => { options: { separator: ':', }, - })).then(result => { + }).then(result => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) @@ -187,7 +187,7 @@ test('responsive variants are generated for all selectors in a rule', () => { } ` - return run(input, () => ({ + return run(input, { screens: { sm: '500px', md: '750px', @@ -196,7 +196,7 @@ test('responsive variants are generated for all selectors in a rule', () => { options: { separator: ':', }, - })).then(result => { + }).then(result => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) @@ -209,7 +209,7 @@ test('selectors with no classes cannot be made responsive', () => { } ` expect.assertions(1) - return run(input, () => ({ + return run(input, { screens: { sm: '500px', md: '750px', @@ -218,7 +218,7 @@ test('selectors with no classes cannot be made responsive', () => { options: { separator: ':', }, - })).catch(e => { + }).catch(e => { expect(e).toMatchObject({ name: 'CssSyntaxError' }) }) }) @@ -230,7 +230,7 @@ test('all selectors in a rule must contain classes', () => { } ` expect.assertions(1) - return run(input, () => ({ + return run(input, { screens: { sm: '500px', md: '750px', @@ -239,7 +239,7 @@ test('all selectors in a rule must contain classes', () => { options: { separator: ':', }, - })).catch(e => { + }).catch(e => { expect(e).toMatchObject({ name: 'CssSyntaxError' }) }) }) diff --git a/__tests__/variantsAtRule.test.js b/__tests__/variantsAtRule.test.js index 3ef28a210..2e7627a8e 100644 --- a/__tests__/variantsAtRule.test.js +++ b/__tests__/variantsAtRule.test.js @@ -2,7 +2,7 @@ import postcss from 'postcss' import plugin from '../src/lib/substituteVariantsAtRules' import config from '../defaultConfig.stub.js' -function run(input, opts = () => config) { +function run(input, opts = config) { return postcss([plugin(opts)]).process(input, { from: undefined }) } @@ -182,7 +182,7 @@ test('plugin variants work', () => { .first-child\\:chocolate:first-child { color: brown; } ` - return run(input, () => ({ + return run(input, { ...config, plugins: [ ...config.plugins, @@ -192,7 +192,7 @@ test('plugin variants work', () => { }) }, ], - })).then(result => { + }).then(result => { expect(result.css).toMatchCss(output) expect(result.warnings().length).toBe(0) }) diff --git a/src/index.js b/src/index.js index 3f3650050..22863659c 100644 --- a/src/index.js +++ b/src/index.js @@ -5,15 +5,10 @@ import postcss from 'postcss' import perfectionist from 'perfectionist' import registerConfigAsDependency from './lib/registerConfigAsDependency' -import substituteTailwindAtRules from './lib/substituteTailwindAtRules' -import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions' -import substituteVariantsAtRules from './lib/substituteVariantsAtRules' -import substituteResponsiveAtRules from './lib/substituteResponsiveAtRules' -import substituteScreenAtRules from './lib/substituteScreenAtRules' -import substituteClassApplyAtRules from './lib/substituteClassApplyAtRules' - +import processTailwindFeatures from './processTailwindFeatures' import mergeConfigWithDefaults from './util/mergeConfigWithDefaults' + const plugin = postcss.plugin('tailwind', config => { const plugins = [] @@ -36,26 +31,19 @@ const plugin = postcss.plugin('tailwind', config => { ) } - return postcss( + return postcss([ ...plugins, - ...[ - substituteTailwindAtRules(lazyConfig), - evaluateTailwindFunctions(lazyConfig), - substituteVariantsAtRules(lazyConfig), - substituteResponsiveAtRules(lazyConfig), - substituteScreenAtRules(lazyConfig), - substituteClassApplyAtRules(lazyConfig), - perfectionist({ - cascade: true, - colorShorthand: true, - indentSize: 2, - maxSelectorLength: 1, - maxValueLength: false, - trimLeadingZero: true, - trimTrailingZeros: true, - }), - ] - ) + processTailwindFeatures(lazyConfig), + perfectionist({ + cascade: true, + colorShorthand: true, + indentSize: 2, + maxSelectorLength: 1, + maxValueLength: false, + trimLeadingZero: true, + trimTrailingZeros: true, + }), + ]) }) plugin.defaultConfig = function() { diff --git a/src/lib/evaluateTailwindFunctions.js b/src/lib/evaluateTailwindFunctions.js index 40ce28a54..c071fd800 100644 --- a/src/lib/evaluateTailwindFunctions.js +++ b/src/lib/evaluateTailwindFunctions.js @@ -5,7 +5,7 @@ export default function(config) { return functions({ functions: { config: (path, defaultValue) => { - return _.get(config(), _.trim(path, `'"`), defaultValue) + return _.get(config, _.trim(path, `'"`), defaultValue) }, }, }) diff --git a/src/lib/substituteResponsiveAtRules.js b/src/lib/substituteResponsiveAtRules.js index bc0b911b5..95762c310 100644 --- a/src/lib/substituteResponsiveAtRules.js +++ b/src/lib/substituteResponsiveAtRules.js @@ -6,8 +6,8 @@ import buildSelectorVariant from '../util/buildSelectorVariant' export default function(config) { return function(css) { - const screens = config().screens - const separator = config().options.separator + const screens = config.screens + const separator = config.options.separator const responsiveRules = [] let finalRules = [] diff --git a/src/lib/substituteScreenAtRules.js b/src/lib/substituteScreenAtRules.js index bc8cc7da4..78f6afe4d 100644 --- a/src/lib/substituteScreenAtRules.js +++ b/src/lib/substituteScreenAtRules.js @@ -3,17 +3,15 @@ import buildMediaQuery from '../util/buildMediaQuery' export default function(config) { return function(css) { - const options = config() - css.walkAtRules('screen', atRule => { const screen = atRule.params - if (!_.has(options.screens, screen)) { + if (!_.has(config.screens, screen)) { throw atRule.error(`No \`${screen}\` screen found.`) } atRule.name = 'media' - atRule.params = buildMediaQuery(options.screens[screen]) + atRule.params = buildMediaQuery(config.screens[screen]) }) } } diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index 56bf1ed3b..74badd406 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -7,10 +7,8 @@ import processPlugins from '../util/processPlugins' export default function(config) { return function(css) { - const unwrappedConfig = config() - const { components: pluginComponents, utilities: pluginUtilities } = processPlugins( - unwrappedConfig + config ) css.walkAtRules('tailwind', atRule => { @@ -37,9 +35,9 @@ export default function(config) { } if (atRule.params === 'utilities') { - const utilities = generateModules(utilityModules, unwrappedConfig.modules, unwrappedConfig) + const utilities = generateModules(utilityModules, config.modules, config) - if (unwrappedConfig.options.important) { + if (config.options.important) { utilities.walkDecls(decl => (decl.important = true)) } @@ -51,7 +49,7 @@ export default function(config) { nodes: pluginUtilities, }) - prefixTree(tailwindUtilityTree, unwrappedConfig.options.prefix) + prefixTree(tailwindUtilityTree, config.options.prefix) tailwindUtilityTree.walk(node => (node.source = atRule.source)) pluginUtilityTree.walk(node => (node.source = atRule.source)) diff --git a/src/lib/substituteVariantsAtRules.js b/src/lib/substituteVariantsAtRules.js index 2d12d1b86..0ba5ab4f3 100644 --- a/src/lib/substituteVariantsAtRules.js +++ b/src/lib/substituteVariantsAtRules.js @@ -31,10 +31,9 @@ const defaultVariantGenerators = { export default function(config) { return function(css) { - const unwrappedConfig = config() const variantGenerators = { ...defaultVariantGenerators, - ...processPlugins(unwrappedConfig).variantGenerators, + ...processPlugins(config).variantGenerators, } css.walkAtRules('variants', atRule => { @@ -49,7 +48,7 @@ export default function(config) { atRule.before(atRule.clone().nodes) _.forEach(_.without(variants, 'responsive'), variant => { - variantGenerators[variant](atRule, unwrappedConfig) + variantGenerators[variant](atRule, config) }) atRule.remove() diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js new file mode 100644 index 000000000..f65007c89 --- /dev/null +++ b/src/processTailwindFeatures.js @@ -0,0 +1,25 @@ +import path from 'path' + +import _ from 'lodash' +import postcss from 'postcss' + +import registerConfigAsDependency from './lib/registerConfigAsDependency' +import substituteTailwindAtRules from './lib/substituteTailwindAtRules' +import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions' +import substituteVariantsAtRules from './lib/substituteVariantsAtRules' +import substituteResponsiveAtRules from './lib/substituteResponsiveAtRules' +import substituteScreenAtRules from './lib/substituteScreenAtRules' +import substituteClassApplyAtRules from './lib/substituteClassApplyAtRules' + +export default function(lazyConfig) { + const config = lazyConfig() + + return postcss([ + substituteTailwindAtRules(config), + evaluateTailwindFunctions(config), + substituteVariantsAtRules(config), + substituteResponsiveAtRules(config), + substituteScreenAtRules(config), + substituteClassApplyAtRules(config), + ]) +} From 9b22ff351345d35b13b14e06e5836f66e559e874 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 22 Jun 2018 20:57:40 -0400 Subject: [PATCH 6/7] Only process plugins once --- __tests__/variantsAtRule.test.js | 3 ++- src/index.js | 1 - src/lib/substituteTailwindAtRules.js | 7 +------ src/lib/substituteVariantsAtRules.js | 5 ++--- src/processTailwindFeatures.js | 10 ++++------ 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/__tests__/variantsAtRule.test.js b/__tests__/variantsAtRule.test.js index 2e7627a8e..edb1a8083 100644 --- a/__tests__/variantsAtRule.test.js +++ b/__tests__/variantsAtRule.test.js @@ -1,9 +1,10 @@ import postcss from 'postcss' import plugin from '../src/lib/substituteVariantsAtRules' import config from '../defaultConfig.stub.js' +import processPlugins from '../src/util/processPlugins' function run(input, opts = config) { - return postcss([plugin(opts)]).process(input, { from: undefined }) + return postcss([plugin(opts, processPlugins(opts))]).process(input, { from: undefined }) } test('it can generate hover variants', () => { diff --git a/src/index.js b/src/index.js index 22863659c..7a67368e5 100644 --- a/src/index.js +++ b/src/index.js @@ -8,7 +8,6 @@ import registerConfigAsDependency from './lib/registerConfigAsDependency' import processTailwindFeatures from './processTailwindFeatures' import mergeConfigWithDefaults from './util/mergeConfigWithDefaults' - const plugin = postcss.plugin('tailwind', config => { const plugins = [] diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index 74badd406..0d7365426 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -3,14 +3,9 @@ import postcss from 'postcss' import utilityModules from '../utilityModules' import prefixTree from '../util/prefixTree' import generateModules from '../util/generateModules' -import processPlugins from '../util/processPlugins' -export default function(config) { +export default function(config, { components: pluginComponents, utilities: pluginUtilities }) { return function(css) { - const { components: pluginComponents, utilities: pluginUtilities } = processPlugins( - config - ) - css.walkAtRules('tailwind', atRule => { if (atRule.params === 'preflight') { const preflightTree = postcss.parse( diff --git a/src/lib/substituteVariantsAtRules.js b/src/lib/substituteVariantsAtRules.js index 0ba5ab4f3..9e3f5fc60 100644 --- a/src/lib/substituteVariantsAtRules.js +++ b/src/lib/substituteVariantsAtRules.js @@ -2,7 +2,6 @@ import _ from 'lodash' import postcss from 'postcss' import buildSelectorVariant from '../util/buildSelectorVariant' import generateVariantFunction from '../util/generateVariantFunction' -import processPlugins from '../util/processPlugins' function generatePseudoClassVariant(pseudoClass) { return generateVariantFunction(({ className, separator }) => { @@ -29,11 +28,11 @@ const defaultVariantGenerators = { active: generatePseudoClassVariant('active'), } -export default function(config) { +export default function(config, { variantGenerators: pluginVariantGenerators }) { return function(css) { const variantGenerators = { ...defaultVariantGenerators, - ...processPlugins(config).variantGenerators, + ...pluginVariantGenerators, } css.walkAtRules('variants', atRule => { diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js index f65007c89..49f097a7b 100644 --- a/src/processTailwindFeatures.js +++ b/src/processTailwindFeatures.js @@ -1,23 +1,21 @@ -import path from 'path' - -import _ from 'lodash' import postcss from 'postcss' -import registerConfigAsDependency from './lib/registerConfigAsDependency' import substituteTailwindAtRules from './lib/substituteTailwindAtRules' import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions' import substituteVariantsAtRules from './lib/substituteVariantsAtRules' import substituteResponsiveAtRules from './lib/substituteResponsiveAtRules' import substituteScreenAtRules from './lib/substituteScreenAtRules' import substituteClassApplyAtRules from './lib/substituteClassApplyAtRules' +import processPlugins from './util/processPlugins' export default function(lazyConfig) { const config = lazyConfig() + const plugins = processPlugins(config) return postcss([ - substituteTailwindAtRules(config), + substituteTailwindAtRules(config, plugins), evaluateTailwindFunctions(config), - substituteVariantsAtRules(config), + substituteVariantsAtRules(config, plugins), substituteResponsiveAtRules(config), substituteScreenAtRules(config), substituteClassApplyAtRules(config), From f974b8df03c97ddaae0163d8e920df64867522b7 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Tue, 26 Jun 2018 10:31:06 -0400 Subject: [PATCH 7/7] Add support for writing variant plugins with raw PostCSS API --- __tests__/variantsAtRule.test.js | 82 +++++++++++++++++++++++++++- src/lib/substituteVariantsAtRules.js | 23 +++----- src/util/generateVariantFunction.js | 30 +++++++--- 3 files changed, 108 insertions(+), 27 deletions(-) diff --git a/__tests__/variantsAtRule.test.js b/__tests__/variantsAtRule.test.js index edb1a8083..cdb7d394c 100644 --- a/__tests__/variantsAtRule.test.js +++ b/__tests__/variantsAtRule.test.js @@ -168,7 +168,43 @@ test('variants are generated in the order specified', () => { }) }) -test('plugin variants work', () => { +test('plugin variants can modify rules using the raw PostCSS API', () => { + const input = ` + @variants important { + .banana { color: yellow; } + .chocolate { color: brown; } + } + ` + + const output = ` + .banana { color: yellow; } + .chocolate { color: brown; } + .\\!banana { color: yellow !important; } + .\\!chocolate { color: brown !important; } + ` + + return run(input, { + ...config, + plugins: [ + ...config.plugins, + function({ addVariant }) { + addVariant('important', ({ container }) => { + container.walkRules(rule => { + rule.selector = `.\\!${rule.selector.slice(1)}` + rule.walkDecls(decl => { + decl.important = true + }) + }) + }) + }, + ], + }).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('plugin variants can modify selectors with a simplified API', () => { const input = ` @variants first-child { .banana { color: yellow; } @@ -188,8 +224,48 @@ test('plugin variants work', () => { plugins: [ ...config.plugins, function({ addVariant }) { - addVariant('first-child', ({ className, separator }) => { - return `.first-child${separator}${className}:first-child` + addVariant('first-child', ({ modifySelectors, separator }) => { + modifySelectors(({ className }) => { + return `.first-child${separator}${className}:first-child` + }) + }) + }, + ], + }).then(result => { + expect(result.css).toMatchCss(output) + expect(result.warnings().length).toBe(0) + }) +}) + +test('plugin variants can wrap rules in another at-rule using the raw PostCSS API', () => { + const input = ` + @variants supports-grid { + .banana { color: yellow; } + .chocolate { color: brown; } + } + ` + + const output = ` + .banana { color: yellow; } + .chocolate { color: brown; } + @supports (display: grid) { + .supports-grid\\:banana { color: yellow; } + .supports-grid\\:chocolate { color: brown; } + } + ` + + return run(input, { + ...config, + plugins: [ + ...config.plugins, + function({ addVariant }) { + addVariant('supports-grid', ({ container, separator }) => { + const supportsRule = postcss.atRule({ name: 'supports', params: '(display: grid)' }) + supportsRule.nodes = container.nodes + container.nodes = [supportsRule] + supportsRule.walkRules(rule => { + rule.selector = `.supports-grid${separator}${rule.selector.slice(1)}` + }) }) }, ], diff --git a/src/lib/substituteVariantsAtRules.js b/src/lib/substituteVariantsAtRules.js index 9e3f5fc60..adae4cbc5 100644 --- a/src/lib/substituteVariantsAtRules.js +++ b/src/lib/substituteVariantsAtRules.js @@ -1,28 +1,21 @@ import _ from 'lodash' import postcss from 'postcss' -import buildSelectorVariant from '../util/buildSelectorVariant' import generateVariantFunction from '../util/generateVariantFunction' function generatePseudoClassVariant(pseudoClass) { - return generateVariantFunction(({ className, separator }) => { - return `.${pseudoClass}${separator}${className}:${pseudoClass}` + return generateVariantFunction(({ modifySelectors, separator }) => { + return modifySelectors(({ className }) => { + return `.${pseudoClass}${separator}${className}:${pseudoClass}` + }) }) } const defaultVariantGenerators = { - 'group-hover': (container, { options: { separator } }) => { - const cloned = container.clone() - - cloned.walkRules(rule => { - rule.selector = `.group:hover ${buildSelectorVariant( - rule.selector, - 'group-hover', - separator - )}` + 'group-hover': generateVariantFunction(({ modifySelectors, separator }) => { + return modifySelectors(({ className }) => { + return `.group:hover .group-hover${separator}${className}` }) - - container.before(cloned.nodes) - }, + }), hover: generatePseudoClassVariant('hover'), focus: generatePseudoClassVariant('focus'), active: generatePseudoClassVariant('active'), diff --git a/src/util/generateVariantFunction.js b/src/util/generateVariantFunction.js index dff63acfe..4046ca737 100644 --- a/src/util/generateVariantFunction.js +++ b/src/util/generateVariantFunction.js @@ -1,16 +1,28 @@ +import _ from 'lodash' +import postcss from 'postcss' import escapeClassName from './escapeClassName' export default function generateVariantFunction(generator) { return (container, config) => { - const cloned = container.clone() + const cloned = postcss.root({ nodes: container.clone().nodes }) - cloned.walkRules(rule => { - rule.selector = generator({ - className: rule.selector.slice(1), - separator: escapeClassName(config.options.separator), - }) - }) - - container.before(cloned.nodes) + container.before( + _.defaultTo( + generator({ + container: cloned, + separator: escapeClassName(config.options.separator), + modifySelectors: modifierFunction => { + cloned.walkRules(rule => { + rule.selector = modifierFunction({ + className: rule.selector.slice(1), + selector: rule.selector, + }) + }) + return cloned + }, + }), + cloned + ).nodes + ) } }