From 44adfbfcd6f0c2c44f47db14a4b520ddd57f3b15 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Tue, 27 Feb 2018 09:57:21 -0500 Subject: [PATCH 01/23] Implement basic plugin system --- __tests__/fixtures/tailwind-input.css | 2 + css/tailwind.css | 2 + defaultConfig.stub.js | 3 + src/index.js | 6 +- src/lib/substituteTailwindAtRules.js | 93 +++++++++++++++++++ src/lib/substituteTailwindPreflightAtRule.js | 15 --- src/lib/substituteTailwindUtilitiesAtRules.js | 34 ------- 7 files changed, 102 insertions(+), 53 deletions(-) create mode 100644 src/lib/substituteTailwindAtRules.js delete mode 100644 src/lib/substituteTailwindPreflightAtRule.js delete mode 100644 src/lib/substituteTailwindUtilitiesAtRules.js diff --git a/__tests__/fixtures/tailwind-input.css b/__tests__/fixtures/tailwind-input.css index ac3999689618..53204f9fd112 100644 --- a/__tests__/fixtures/tailwind-input.css +++ b/__tests__/fixtures/tailwind-input.css @@ -1,5 +1,7 @@ @tailwind preflight; +@tailwind components; + @tailwind utilities; @responsive { diff --git a/css/tailwind.css b/css/tailwind.css index f2078b182200..5881ba48f5f6 100644 --- a/css/tailwind.css +++ b/css/tailwind.css @@ -1,3 +1,5 @@ @tailwind preflight; +@tailwind components; + @tailwind utilities; diff --git a/defaultConfig.stub.js b/defaultConfig.stub.js index d2e5ab7f8277..408db7af852f 100644 --- a/defaultConfig.stub.js +++ b/defaultConfig.stub.js @@ -859,6 +859,9 @@ module.exports = { }, + plugins: [], + + /* |----------------------------------------------------------------------------- | Advanced Options https://tailwindcss.com/docs/configuration#options diff --git a/src/index.js b/src/index.js index 0b392a1aaf16..2b70f8ca524c 100644 --- a/src/index.js +++ b/src/index.js @@ -5,9 +5,8 @@ import postcss from 'postcss' import perfectionist from 'perfectionist' import registerConfigAsDependency from './lib/registerConfigAsDependency' -import substituteTailwindPreflightAtRule from './lib/substituteTailwindPreflightAtRule' +import substituteTailwindAtRules from './lib/substituteTailwindAtRules' import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions' -import substituteTailwindUtilitiesAtRules from './lib/substituteTailwindUtilitiesAtRules' import substituteVariantsAtRules from './lib/substituteVariantsAtRules' import substituteResponsiveAtRules from './lib/substituteResponsiveAtRules' import substituteScreenAtRules from './lib/substituteScreenAtRules' @@ -34,9 +33,8 @@ const plugin = postcss.plugin('tailwind', config => { return postcss( ...plugins, ...[ - substituteTailwindPreflightAtRule(lazyConfig), + substituteTailwindAtRules(lazyConfig), evaluateTailwindFunctions(lazyConfig), - substituteTailwindUtilitiesAtRules(lazyConfig), substituteVariantsAtRules(lazyConfig), substituteResponsiveAtRules(lazyConfig), substituteScreenAtRules(lazyConfig), diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js new file mode 100644 index 000000000000..efa70661c9f7 --- /dev/null +++ b/src/lib/substituteTailwindAtRules.js @@ -0,0 +1,93 @@ +import _ from 'lodash' +import applyClassPrefix from '../util/applyClassPrefix' +import container from '../generators/container' +import fs from 'fs' +import generateModules from '../util/generateModules' +import postcss from 'postcss' +import utilityModules from '../utilityModules' +import wrapWithVariants from '../util/wrapWithVariants' + +function defineSelector(selector, properties) { + const decls = _.map(properties, (value, property) => { + return postcss.decl({ + prop: `${property}`, + value: `${value}`, + }) + }) + + return postcss.rule({ selector }).append(decls) +} + +export default function(config) { + return function(css) { + const unwrappedConfig = config() + + const pluginComponents = [] + const pluginUtilities = [] + + unwrappedConfig.plugins.forEach(plugin => { + plugin({ + selector: defineSelector, + addUtilities: (utilities, variants) => { + pluginUtilities.push(wrapWithVariants(utilities, variants)) + }, + addComponents: components => { + pluginComponents.push(...components) + }, + }) + }) + + css.walkAtRules('tailwind', atRule => { + if (atRule.params === 'preflight') { + atRule.before( + postcss.parse(fs.readFileSync(`${__dirname}/../../css/preflight.css`, 'utf8')) + ) + atRule.remove() + } + + if (atRule.params === 'components') { + const tailwindComponentTree = postcss.root({ + nodes: [...container(unwrappedConfig)], + }) + + const pluginComponentTree = postcss.root({ + nodes: [...pluginComponents], + }) + + applyClassPrefix(tailwindComponentTree, unwrappedConfig.options.prefix) + + tailwindComponentTree.walk(node => (node.source = atRule.source)) + pluginComponentTree.walk(node => (node.source = atRule.source)) + + atRule.before(tailwindComponentTree) + atRule.before(pluginComponentTree) + atRule.remove() + } + + if (atRule.params === 'utilities') { + const utilities = generateModules(utilityModules, unwrappedConfig.modules, unwrappedConfig) + + if (unwrappedConfig.options.important) { + utilities.walkDecls(decl => (decl.important = true)) + } + + const tailwindUtilityTree = postcss.root({ + nodes: [...utilities.nodes], + }) + + const pluginUtilityTree = postcss.root({ + nodes: [...pluginUtilities], + }) + + applyClassPrefix(tailwindUtilityTree, unwrappedConfig.options.prefix) + + tailwindUtilityTree.walk(node => (node.source = atRule.source)) + pluginUtilityTree.walk(node => (node.source = atRule.source)) + + atRule.before(tailwindUtilityTree) + atRule.before(pluginUtilityTree) + atRule.remove() + } + }) + } +} diff --git a/src/lib/substituteTailwindPreflightAtRule.js b/src/lib/substituteTailwindPreflightAtRule.js deleted file mode 100644 index a6a1c59313f1..000000000000 --- a/src/lib/substituteTailwindPreflightAtRule.js +++ /dev/null @@ -1,15 +0,0 @@ -import fs from 'fs' -import postcss from 'postcss' - -export default function() { - return function(css) { - css.walkAtRules('tailwind', atRule => { - if (atRule.params !== 'preflight') { - return - } - - atRule.before(postcss.parse(fs.readFileSync(`${__dirname}/../../css/preflight.css`, 'utf8'))) - atRule.remove() - }) - } -} diff --git a/src/lib/substituteTailwindUtilitiesAtRules.js b/src/lib/substituteTailwindUtilitiesAtRules.js deleted file mode 100644 index d08f4457c7cf..000000000000 --- a/src/lib/substituteTailwindUtilitiesAtRules.js +++ /dev/null @@ -1,34 +0,0 @@ -import postcss from 'postcss' -import applyClassPrefix from '../util/applyClassPrefix' -import generateModules from '../util/generateModules' -import container from '../generators/container' -import utilityModules from '../utilityModules' - -export default function(config) { - return function(css) { - const unwrappedConfig = config() - - css.walkAtRules('tailwind', atRule => { - if (atRule.params !== 'utilities') { - return - } - - const utilities = generateModules(utilityModules, unwrappedConfig.modules, unwrappedConfig) - - if (unwrappedConfig.options.important) { - utilities.walkDecls(decl => (decl.important = true)) - } - - const tailwindClasses = postcss.root({ - nodes: [...container(unwrappedConfig), ...utilities.nodes], - }) - - applyClassPrefix(tailwindClasses, unwrappedConfig.options.prefix) - - tailwindClasses.walk(node => (node.source = atRule.source)) - - atRule.before(tailwindClasses) - atRule.remove() - }) - } -} From 1adba9a7966a17405381e676a2b29211165c7eb0 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 1 Mar 2018 15:24:01 -0500 Subject: [PATCH 02/23] Sort imports --- src/lib/substituteTailwindAtRules.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index efa70661c9f7..2581a4b47b7e 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -1,10 +1,11 @@ import _ from 'lodash' -import applyClassPrefix from '../util/applyClassPrefix' -import container from '../generators/container' import fs from 'fs' -import generateModules from '../util/generateModules' import postcss from 'postcss' +import container from '../generators/container' import utilityModules from '../utilityModules' +import applyClassPrefix from '../util/applyClassPrefix' +import escapeClassName from '../util/escapeClassName' +import generateModules from '../util/generateModules' import wrapWithVariants from '../util/wrapWithVariants' function defineSelector(selector, properties) { From ec364b4476e36fac60c55e24347ded1f1d359ff8 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 1 Mar 2018 15:25:03 -0500 Subject: [PATCH 03/23] Rename selector to rule, extract plugin processing --- src/lib/substituteTailwindAtRules.js | 36 ++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index 2581a4b47b7e..31809af6978f 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -8,7 +8,7 @@ import escapeClassName from '../util/escapeClassName' import generateModules from '../util/generateModules' import wrapWithVariants from '../util/wrapWithVariants' -function defineSelector(selector, properties) { +function defineRule(selector, properties) { const decls = _.map(properties, (value, property) => { return postcss.decl({ prop: `${property}`, @@ -19,24 +19,30 @@ function defineSelector(selector, properties) { return postcss.rule({ selector }).append(decls) } +function processPlugins(config) { + const pluginComponents = [] + const pluginUtilities = [] + + config.plugins.forEach(plugin => { + plugin({ + rule: defineRule, + addUtilities: (utilities, variants) => { + pluginUtilities.push(wrapWithVariants(utilities, variants)) + }, + addComponents: components => { + pluginComponents.push(...components) + }, + }) + }) + + return [pluginComponents, pluginUtilities] +} + export default function(config) { return function(css) { const unwrappedConfig = config() - const pluginComponents = [] - const pluginUtilities = [] - - unwrappedConfig.plugins.forEach(plugin => { - plugin({ - selector: defineSelector, - addUtilities: (utilities, variants) => { - pluginUtilities.push(wrapWithVariants(utilities, variants)) - }, - addComponents: components => { - pluginComponents.push(...components) - }, - }) - }) + const [pluginComponents, pluginUtilities] = processPlugins(unwrappedConfig) css.walkAtRules('tailwind', atRule => { if (atRule.params === 'preflight') { From f3fd0780c756d1269bd533afbc9f148ce3f68b7f Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 1 Mar 2018 15:25:33 -0500 Subject: [PATCH 04/23] Remove unnecessary array destructuring --- src/lib/substituteTailwindAtRules.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index 31809af6978f..0171f80f05da 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -54,11 +54,11 @@ export default function(config) { if (atRule.params === 'components') { const tailwindComponentTree = postcss.root({ - nodes: [...container(unwrappedConfig)], + nodes: container(unwrappedConfig), }) const pluginComponentTree = postcss.root({ - nodes: [...pluginComponents], + nodes: pluginComponents, }) applyClassPrefix(tailwindComponentTree, unwrappedConfig.options.prefix) @@ -79,11 +79,11 @@ export default function(config) { } const tailwindUtilityTree = postcss.root({ - nodes: [...utilities.nodes], + nodes: utilities.nodes, }) const pluginUtilityTree = postcss.root({ - nodes: [...pluginUtilities], + nodes: pluginUtilities, }) applyClassPrefix(tailwindUtilityTree, unwrappedConfig.options.prefix) From f2030d79201567807776060e7983671a67ae3b0e Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 1 Mar 2018 15:25:40 -0500 Subject: [PATCH 05/23] Pass config to plugins --- src/lib/substituteTailwindAtRules.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index 0171f80f05da..5029e09d18c8 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -25,6 +25,7 @@ function processPlugins(config) { config.plugins.forEach(plugin => { plugin({ + config, rule: defineRule, addUtilities: (utilities, variants) => { pluginUtilities.push(wrapWithVariants(utilities, variants)) From d5592e424d588a252cd863a038c107927d12ed6a Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 1 Mar 2018 15:25:47 -0500 Subject: [PATCH 06/23] Pass escape function to plugins --- src/lib/substituteTailwindAtRules.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index 5029e09d18c8..e40b5bae3694 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -27,6 +27,7 @@ function processPlugins(config) { plugin({ config, rule: defineRule, + e: escapeClassName, addUtilities: (utilities, variants) => { pluginUtilities.push(wrapWithVariants(utilities, variants)) }, From 530916b0bbfd4061ddef7ffc3e2ce7b5fe661627 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 1 Mar 2018 15:25:55 -0500 Subject: [PATCH 07/23] Add atRule helper for plugins --- src/lib/substituteTailwindAtRules.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index e40b5bae3694..d3d050be5e4d 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -19,6 +19,17 @@ function defineRule(selector, properties) { return postcss.rule({ selector }).append(decls) } +function defineAtRule(atRule, rules) { + const [name, ...params] = atRule.split(' ') + + return postcss + .atRule({ + name: name.startsWith('@') ? name.slice(1) : name, + params: params.join(' '), + }) + .append(rules) +} + function processPlugins(config) { const pluginComponents = [] const pluginUtilities = [] @@ -27,6 +38,7 @@ function processPlugins(config) { plugin({ config, rule: defineRule, + atRule: defineAtRule, e: escapeClassName, addUtilities: (utilities, variants) => { pluginUtilities.push(wrapWithVariants(utilities, variants)) From 2e361c6cac7d0f380a7b74edf82acf8be8f07b85 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 1 Mar 2018 16:34:29 -0500 Subject: [PATCH 08/23] Move processPlugins to separate module --- src/lib/substituteTailwindAtRules.js | 45 +------------------------- src/util/processPlugins.js | 48 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 44 deletions(-) create mode 100644 src/util/processPlugins.js diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index d3d050be5e4d..3f8dda8ff5f1 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -6,52 +6,9 @@ import utilityModules from '../utilityModules' import applyClassPrefix from '../util/applyClassPrefix' import escapeClassName from '../util/escapeClassName' import generateModules from '../util/generateModules' +import processPlugins from '../util/processPlugins' import wrapWithVariants from '../util/wrapWithVariants' -function defineRule(selector, properties) { - const decls = _.map(properties, (value, property) => { - return postcss.decl({ - prop: `${property}`, - value: `${value}`, - }) - }) - - return postcss.rule({ selector }).append(decls) -} - -function defineAtRule(atRule, rules) { - const [name, ...params] = atRule.split(' ') - - return postcss - .atRule({ - name: name.startsWith('@') ? name.slice(1) : name, - params: params.join(' '), - }) - .append(rules) -} - -function processPlugins(config) { - const pluginComponents = [] - const pluginUtilities = [] - - config.plugins.forEach(plugin => { - plugin({ - config, - rule: defineRule, - atRule: defineAtRule, - e: escapeClassName, - addUtilities: (utilities, variants) => { - pluginUtilities.push(wrapWithVariants(utilities, variants)) - }, - addComponents: components => { - pluginComponents.push(...components) - }, - }) - }) - - return [pluginComponents, pluginUtilities] -} - export default function(config) { return function(css) { const unwrappedConfig = config() diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js new file mode 100644 index 000000000000..2329e716876b --- /dev/null +++ b/src/util/processPlugins.js @@ -0,0 +1,48 @@ +import _ from 'lodash' +import postcss from 'postcss' +import escapeClassName from '../util/escapeClassName' +import wrapWithVariants from '../util/wrapWithVariants' + +function defineRule(selector, properties) { + const decls = _.map(properties, (value, property) => { + return postcss.decl({ + prop: `${property}`, + value: `${value}`, + }) + }) + + return postcss.rule({ selector }).append(decls) +} + +function defineAtRule(atRule, rules) { + const [name, ...params] = atRule.split(' ') + + return postcss + .atRule({ + name: name.startsWith('@') ? name.slice(1) : name, + params: params.join(' '), + }) + .append(rules) +} + +export default function (config) { + const pluginComponents = [] + const pluginUtilities = [] + + config.plugins.forEach(plugin => { + plugin({ + config, + rule: defineRule, + atRule: defineAtRule, + e: escapeClassName, + addUtilities: (utilities, variants) => { + pluginUtilities.push(wrapWithVariants(utilities, variants)) + }, + addComponents: components => { + pluginComponents.push(...components) + }, + }) + }) + + return [pluginComponents, pluginUtilities] +} From 4f9dcc50adcb07014c4e206b2fff2cad0b107ce1 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 1 Mar 2018 16:34:40 -0500 Subject: [PATCH 09/23] Add initial test suite for processPlugins --- __tests__/processPlugins.test.js | 252 +++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 __tests__/processPlugins.test.js diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js new file mode 100644 index 000000000000..75a49b16bd10 --- /dev/null +++ b/__tests__/processPlugins.test.js @@ -0,0 +1,252 @@ +import _ from 'lodash' +import postcss from 'postcss' +import processPlugins from '../src/util/processPlugins' + +function css(nodes) { + return postcss.root({ nodes }).toString() +} + +function objectFitPlugin(variants = []) { + return function ({ rule, addUtilities }) { + addUtilities([ + rule('.object-fill', { + 'object-fit': 'fill', + }), + rule('.object-contain', { + 'object-fit': 'contain', + }), + rule('.object-cover', { + 'object-fit': 'cover', + }), + ], variants) + } +} + +function buttonPlugin() { + return function ({ rule, atRule, addComponents }) { + addComponents([ + rule('.btn-blue', { + 'background-color': 'blue', + 'color': 'white', + 'padding': '.5rem 1rem', + 'border-radius': '.25rem', + }), + rule('.btn-blue:hover', { + 'background-color': 'darkblue', + }), + ]) + } +} + +function containerPlugin() { + return function ({ rule, atRule, addComponents }) { + addComponents([ + rule('.container', { + 'width': '100%', + }), + atRule('@media (min-width: 100px)', [ + rule('.container', { + 'max-width': '100px', + }), + ]), + atRule('@media (min-width: 200px)', [ + rule('.container', { + 'max-width': '200px', + }), + ]), + atRule('@media (min-width: 300px)', [ + rule('.container', { + 'max-width': '300px', + }), + ]) + ]) + } +} + +test('plugins can create utilities', () => { + const config = { + plugins: [ + objectFitPlugin() + ] + } + + const [components, utilities] = processPlugins(config) + + 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 create utilities with variants', () => { + const config = { + plugins: [ + objectFitPlugin(['responsive', 'hover', 'group-hover', 'focus']) + ] + } + + const [components, utilities] = processPlugins(config) + + expect(components.length).toBe(0) + expect(css(utilities)).toMatchCss(` + @variants responsive, hover, group-hover, focus { + .object-fill { + object-fit: fill + } + .object-contain { + object-fit: contain + } + .object-cover { + object-fit: cover + } + } + `) +}) + +test('plugins can create components', () => { + const config = { + plugins: [ + buttonPlugin() + ] + } + + const [components, utilities] = processPlugins(config) + + expect(utilities.length).toBe(0) + expect(css(components)).toMatchCss(` + .btn-blue { + background-color: blue; + color: white; + padding: .5rem 1rem; + border-radius: .25rem + } + .btn-blue:hover { + background-color: darkblue + } + `) +}) + +test('plugins can create components with media queries', () => { + const config = { + plugins: [ + containerPlugin() + ] + } + + const [components, utilities] = processPlugins(config) + + expect(utilities.length).toBe(0) + expect(css(components)).toMatchCss(` + .container { + width: 100% + } + @media (min-width: 100px) { + .container { + max-width: 100px + } + } + @media (min-width: 200px) { + .container { + max-width: 200px + } + } + @media (min-width: 300px) { + .container { + max-width: 300px + } + } + `) +}) + +test('plugins can create rules with escaped selectors', () => { + const config = { + plugins: [ + function ({ e, rule, addUtilities }) { + addUtilities([ + rule(`.${e('top-1/4')}`, { + top: '25%', + }) + ], []) + } + ] + } + + const [components, utilities] = processPlugins(config) + + expect(components.length).toBe(0) + expect(css(utilities)).toMatchCss(` + @variants { + .top-1\\/4 { + top: 25% + } + } + `) +}) + +test('plugins can access the current config', () => { + const config = { + screens: { + 'sm': '576px', + 'md': '768px', + 'lg': '992px', + 'xl': '1200px', + }, + plugins: [ + function ({ rule, atRule, addComponents, config }) { + const containerClasses = [ + rule('.container', { + 'width': '100%', + }) + ] + + _.forEach(config.screens, (breakpoint) => { + const mediaQuery = atRule(`@media (min-width: ${breakpoint})`, [ + rule('.container', { 'max-width': breakpoint }) + ]) + containerClasses.push(mediaQuery) + }) + + addComponents(containerClasses) + } + ] + } + + const [components, utilities] = processPlugins(config) + + expect(utilities.length).toBe(0) + expect(css(components)).toMatchCss(` + .container { + width: 100% + } + @media (min-width: 576px) { + .container { + max-width: 576px + } + } + @media (min-width: 768px) { + .container { + max-width: 768px + } + } + @media (min-width: 992px) { + .container { + max-width: 992px + } + } + @media (min-width: 1200px) { + .container { + max-width: 1200px + } + } + `) +}) From 1a2ea60c04c975b5e01910054a19b6274e372029 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 1 Mar 2018 19:21:19 -0500 Subject: [PATCH 10/23] Fix style --- __tests__/processPlugins.test.js | 102 +++++++++++++-------------- src/lib/substituteTailwindAtRules.js | 3 - src/util/processPlugins.js | 2 +- 3 files changed, 50 insertions(+), 57 deletions(-) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index 75a49b16bd10..74ceae9f2ab6 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -7,28 +7,31 @@ function css(nodes) { } function objectFitPlugin(variants = []) { - return function ({ rule, addUtilities }) { - addUtilities([ - rule('.object-fill', { - 'object-fit': 'fill', - }), - rule('.object-contain', { - 'object-fit': 'contain', - }), - rule('.object-cover', { - 'object-fit': 'cover', - }), - ], variants) + return function({ rule, addUtilities }) { + addUtilities( + [ + rule('.object-fill', { + 'object-fit': 'fill', + }), + rule('.object-contain', { + 'object-fit': 'contain', + }), + rule('.object-cover', { + 'object-fit': 'cover', + }), + ], + variants + ) } } function buttonPlugin() { - return function ({ rule, atRule, addComponents }) { + return function({ rule, addComponents }) { addComponents([ rule('.btn-blue', { 'background-color': 'blue', - 'color': 'white', - 'padding': '.5rem 1rem', + color: 'white', + padding: '.5rem 1rem', 'border-radius': '.25rem', }), rule('.btn-blue:hover', { @@ -39,10 +42,10 @@ function buttonPlugin() { } function containerPlugin() { - return function ({ rule, atRule, addComponents }) { + return function({ rule, atRule, addComponents }) { addComponents([ rule('.container', { - 'width': '100%', + width: '100%', }), atRule('@media (min-width: 100px)', [ rule('.container', { @@ -58,16 +61,14 @@ function containerPlugin() { rule('.container', { 'max-width': '300px', }), - ]) + ]), ]) } } test('plugins can create utilities', () => { const config = { - plugins: [ - objectFitPlugin() - ] + plugins: [objectFitPlugin()], } const [components, utilities] = processPlugins(config) @@ -90,9 +91,7 @@ test('plugins can create utilities', () => { test('plugins can create utilities with variants', () => { const config = { - plugins: [ - objectFitPlugin(['responsive', 'hover', 'group-hover', 'focus']) - ] + plugins: [objectFitPlugin(['responsive', 'hover', 'group-hover', 'focus'])], } const [components, utilities] = processPlugins(config) @@ -115,9 +114,7 @@ test('plugins can create utilities with variants', () => { test('plugins can create components', () => { const config = { - plugins: [ - buttonPlugin() - ] + plugins: [buttonPlugin()], } const [components, utilities] = processPlugins(config) @@ -138,9 +135,7 @@ test('plugins can create components', () => { test('plugins can create components with media queries', () => { const config = { - plugins: [ - containerPlugin() - ] + plugins: [containerPlugin()], } const [components, utilities] = processPlugins(config) @@ -171,14 +166,17 @@ test('plugins can create components with media queries', () => { test('plugins can create rules with escaped selectors', () => { const config = { plugins: [ - function ({ e, rule, addUtilities }) { - addUtilities([ - rule(`.${e('top-1/4')}`, { - top: '25%', - }) - ], []) - } - ] + function({ e, rule, addUtilities }) { + addUtilities( + [ + rule(`.${e('top-1/4')}`, { + top: '25%', + }), + ], + [] + ) + }, + ], } const [components, utilities] = processPlugins(config) @@ -194,34 +192,32 @@ test('plugins can create rules with escaped selectors', () => { }) test('plugins can access the current config', () => { - const config = { + const [components, utilities] = processPlugins({ screens: { - 'sm': '576px', - 'md': '768px', - 'lg': '992px', - 'xl': '1200px', + sm: '576px', + md: '768px', + lg: '992px', + xl: '1200px', }, plugins: [ - function ({ rule, atRule, addComponents, config }) { + function({ rule, atRule, addComponents, config }) { const containerClasses = [ rule('.container', { - 'width': '100%', - }) + width: '100%', + }), ] - _.forEach(config.screens, (breakpoint) => { + _.forEach(config.screens, breakpoint => { const mediaQuery = atRule(`@media (min-width: ${breakpoint})`, [ - rule('.container', { 'max-width': breakpoint }) + rule('.container', { 'max-width': breakpoint }), ]) containerClasses.push(mediaQuery) }) addComponents(containerClasses) - } - ] - } - - const [components, utilities] = processPlugins(config) + }, + ], + }) expect(utilities.length).toBe(0) expect(css(components)).toMatchCss(` diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index 3f8dda8ff5f1..a6f71e45323c 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -1,13 +1,10 @@ -import _ from 'lodash' import fs from 'fs' import postcss from 'postcss' import container from '../generators/container' import utilityModules from '../utilityModules' import applyClassPrefix from '../util/applyClassPrefix' -import escapeClassName from '../util/escapeClassName' import generateModules from '../util/generateModules' import processPlugins from '../util/processPlugins' -import wrapWithVariants from '../util/wrapWithVariants' export default function(config) { return function(css) { diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index 2329e716876b..bf68cad3425c 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -25,7 +25,7 @@ function defineAtRule(atRule, rules) { .append(rules) } -export default function (config) { +export default function(config) { const pluginComponents = [] const pluginUtilities = [] From c8153f7ed79701c519b82e6c2c4c50abf0c0167c Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 2 Mar 2018 07:45:11 -0500 Subject: [PATCH 11/23] Expose config as a function to avoid mutation and make it easy to provide defaults --- __tests__/processPlugins.test.js | 29 ++++++++++++++++++++++++++++- src/util/processPlugins.js | 2 +- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index 74ceae9f2ab6..ab4c4ff0ab00 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -207,7 +207,7 @@ test('plugins can access the current config', () => { }), ] - _.forEach(config.screens, breakpoint => { + _.forEach(config('screens'), breakpoint => { const mediaQuery = atRule(`@media (min-width: ${breakpoint})`, [ rule('.container', { 'max-width': breakpoint }), ]) @@ -246,3 +246,30 @@ test('plugins can access the current config', () => { } `) }) + +test('plugins can provide fallbacks to keys missing from the config', () => { + const [components, utilities] = processPlugins({ + borderRadius: { + '1': '1px', + '2': '2px', + '4': '4px', + '8': '8px', + }, + plugins: [ + function({ rule, atRule, addComponents, config }) { + addComponents([ + rule('.btn', { + 'border-radius': config('borderRadius.default', '.25rem') + }) + ]) + }, + ], + }) + + expect(utilities.length).toBe(0) + expect(css(components)).toMatchCss(` + .btn { + border-radius: .25rem + } + `) +}) diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index bf68cad3425c..6b04297f5940 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -31,7 +31,7 @@ export default function(config) { config.plugins.forEach(plugin => { plugin({ - config, + config: (path, defaultValue) => _.get(config, path, defaultValue), rule: defineRule, atRule: defineAtRule, e: escapeClassName, From 47178e98755d241a48afb8af9257fafc854d889c Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 2 Mar 2018 07:54:42 -0500 Subject: [PATCH 12/23] Test that "@" is optional in at-rule definitions --- __tests__/processPlugins.test.js | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index ab4c4ff0ab00..09a422d6c387 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -273,3 +273,44 @@ test('plugins can provide fallbacks to keys missing from the config', () => { } `) }) + +test('the "@" sign is optional in at-rules', () => { + const [components, utilities] = processPlugins({ + plugins: [ + function ({ rule, atRule, addComponents }) { + addComponents([ + rule('.card', { + padding: '.5rem', + }), + atRule('media (min-width: 500px)', [ + rule('.card', { + padding: '1rem', + }), + ]), + atRule('@media (min-width: 800px)', [ + rule('.card', { + padding: '1.5rem', + }), + ]) + ]) + } + ], + }) + + expect(utilities.length).toBe(0) + expect(css(components)).toMatchCss(` + .card { + padding: .5rem + } + @media (min-width: 500px) { + .card { + padding: 1rem + } + } + @media (min-width: 800px) { + .card { + padding: 1.5rem + } + } + `) +}) From 54af380fafe107ae7988eeccd1d94a3bf2e2e1c3 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 2 Mar 2018 08:11:39 -0500 Subject: [PATCH 13/23] Test utilities can be added without specifying variants --- __tests__/processPlugins.test.js | 22 ++++++++++++++++++++++ src/util/processPlugins.js | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index 09a422d6c387..b7f3db6393d7 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -314,3 +314,25 @@ test('the "@" sign is optional in at-rules', () => { } `) }) + +test('variants are optional when adding utilities', () => { + const [components, utilities] = processPlugins({ + plugins: [ + function ({ rule, addUtilities }) { + addUtilities([ + rule('.border-collapse', { + 'border-collapse': 'collapse' + }) + ]) + } + ], + }) + + expect(css(utilities)).toMatchCss(` + @variants { + .border-collapse { + border-collapse: collapse + } + } + `) +}) diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index 6b04297f5940..6e2f49238cd2 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -35,7 +35,7 @@ export default function(config) { rule: defineRule, atRule: defineAtRule, e: escapeClassName, - addUtilities: (utilities, variants) => { + addUtilities: (utilities, variants = []) => { pluginUtilities.push(wrapWithVariants(utilities, variants)) }, addComponents: components => { From c4abdfb24c4b521c56fbc91ec661ffdef788c22e Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 2 Mar 2018 08:11:52 -0500 Subject: [PATCH 14/23] Test plugins can add multiple sets of utilities and components --- __tests__/processPlugins.test.js | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index b7f3db6393d7..890ab424174d 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -336,3 +336,60 @@ test('variants are optional when adding utilities', () => { } `) }) + +test('plugins can add multiple sets of utilities and components', () => { + const [components, utilities] = processPlugins({ + plugins: [ + function ({ rule, atRule, addUtilities, addComponents }) { + addComponents([ + rule('.card', { + padding: '1rem', + 'border-radius': '.25rem', + }) + ]) + + addUtilities([ + rule('.skew-12deg', { + transform: 'skewY(-12deg)' + }) + ]) + + addComponents([ + rule('.btn', { + padding: '1rem .5rem', + display: 'inline-block', + }) + ]) + + addUtilities([ + rule('.border-collapse', { + 'border-collapse': 'collapse' + }) + ]) + } + ], + }) + + expect(css(utilities)).toMatchCss(` + @variants { + .skew-12deg { + transform: skewY(-12deg) + } + } + @variants { + .border-collapse { + border-collapse: collapse + } + } + `) + expect(css(components)).toMatchCss(` + .card { + padding: 1rem; + border-radius: .25rem + } + .btn { + padding: 1rem .5rem; + display: inline-block + } + `) +}) From 6cc448b8b76de82c5027e62b5c628ee63b614802 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 2 Mar 2018 09:08:05 -0500 Subject: [PATCH 15/23] Provide a function for prefixing utilities in plugins --- ...ClassPrefix.test.js => prefixTree.test.js} | 6 ++--- __tests__/processPlugins.test.js | 25 +++++++++++++++++++ src/lib/substituteTailwindAtRules.js | 6 ++--- src/util/applyClassPrefix.js | 8 ------ src/util/prefixSelector.js | 5 ++++ src/util/prefixTree.js | 9 +++++++ src/util/processPlugins.js | 4 +++ 7 files changed, 49 insertions(+), 14 deletions(-) rename __tests__/{applyClassPrefix.test.js => prefixTree.test.js} (81%) delete mode 100644 src/util/applyClassPrefix.js create mode 100644 src/util/prefixSelector.js create mode 100644 src/util/prefixTree.js diff --git a/__tests__/applyClassPrefix.test.js b/__tests__/prefixTree.test.js similarity index 81% rename from __tests__/applyClassPrefix.test.js rename to __tests__/prefixTree.test.js index 3c36fac31675..cd3029fb4378 100644 --- a/__tests__/applyClassPrefix.test.js +++ b/__tests__/prefixTree.test.js @@ -1,5 +1,5 @@ import postcss from 'postcss' -import applyClassPrefix from '../src/util/applyClassPrefix' +import prefixTree from '../src/util/prefixTree' test('it prefixes classes with the provided prefix', () => { const input = postcss.parse(` @@ -12,7 +12,7 @@ test('it prefixes classes with the provided prefix', () => { .tw-apple, .tw-pear { color: green; } ` - const result = applyClassPrefix(input, 'tw-').toResult() + const result = prefixTree(input, 'tw-').toResult() expect(result.css).toEqual(expected) expect(result.warnings().length).toBe(0) }) @@ -36,7 +36,7 @@ test('it handles a function as the prefix', () => { return '' } - const result = applyClassPrefix(input, prefixFunc).toResult() + const result = prefixTree(input, prefixFunc).toResult() expect(result.css).toEqual(expected) expect(result.warnings().length).toBe(0) }) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index 890ab424174d..f65fb1e99d74 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -393,3 +393,28 @@ test('plugins can add multiple sets of utilities and components', () => { } `) }) + +test("plugins can apply the user's chosen prefix", () => { + const [, utilities] = processPlugins({ + plugins: [ + function({ rule, addUtilities, prefix }) { + addUtilities([ + rule(prefix('.skew-12deg'), { + transform: 'skewY(-12deg)', + }), + ]) + }, + ], + options: { + prefix: 'tw-', + }, + }) + + expect(css(utilities)).toMatchCss(` + @variants { + .tw-skew-12deg { + transform: skewY(-12deg) + } + } + `) +}) diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index a6f71e45323c..374017d95845 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -2,7 +2,7 @@ import fs from 'fs' import postcss from 'postcss' import container from '../generators/container' import utilityModules from '../utilityModules' -import applyClassPrefix from '../util/applyClassPrefix' +import prefixTree from '../util/prefixTree' import generateModules from '../util/generateModules' import processPlugins from '../util/processPlugins' @@ -29,7 +29,7 @@ export default function(config) { nodes: pluginComponents, }) - applyClassPrefix(tailwindComponentTree, unwrappedConfig.options.prefix) + prefixTree(tailwindComponentTree, unwrappedConfig.options.prefix) tailwindComponentTree.walk(node => (node.source = atRule.source)) pluginComponentTree.walk(node => (node.source = atRule.source)) @@ -54,7 +54,7 @@ export default function(config) { nodes: pluginUtilities, }) - applyClassPrefix(tailwindUtilityTree, unwrappedConfig.options.prefix) + prefixTree(tailwindUtilityTree, unwrappedConfig.options.prefix) tailwindUtilityTree.walk(node => (node.source = atRule.source)) pluginUtilityTree.walk(node => (node.source = atRule.source)) diff --git a/src/util/applyClassPrefix.js b/src/util/applyClassPrefix.js deleted file mode 100644 index 1855fca914de..000000000000 --- a/src/util/applyClassPrefix.js +++ /dev/null @@ -1,8 +0,0 @@ -export default function(css, prefix) { - const getPrefix = typeof prefix === 'function' ? prefix : () => prefix - - css.walkRules(rule => { - rule.selectors = rule.selectors.map(selector => `.${getPrefix(selector)}${selector.slice(1)}`) - }) - return css -} diff --git a/src/util/prefixSelector.js b/src/util/prefixSelector.js new file mode 100644 index 000000000000..a5133abe794e --- /dev/null +++ b/src/util/prefixSelector.js @@ -0,0 +1,5 @@ +export default function(prefix, selector) { + const getPrefix = typeof prefix === 'function' ? prefix : () => prefix + + return `.${getPrefix(selector)}${selector.slice(1)}` +} diff --git a/src/util/prefixTree.js b/src/util/prefixTree.js new file mode 100644 index 000000000000..44499e9e6cf8 --- /dev/null +++ b/src/util/prefixTree.js @@ -0,0 +1,9 @@ +import prefixSelector from './prefixSelector' + +export default function(css, prefix) { + css.walkRules(rule => { + rule.selectors = rule.selectors.map(selector => prefixSelector(prefix, selector)) + }) + + return css +} diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index 6e2f49238cd2..d8fd1b6169b2 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -1,6 +1,7 @@ import _ from 'lodash' import postcss from 'postcss' import escapeClassName from '../util/escapeClassName' +import prefixSelector from '../util/prefixSelector' import wrapWithVariants from '../util/wrapWithVariants' function defineRule(selector, properties) { @@ -41,6 +42,9 @@ export default function(config) { addComponents: components => { pluginComponents.push(...components) }, + prefix: selector => { + return prefixSelector(config.options.prefix, selector) + }, }) }) From a19a5563824c94d3267063ac7ab198d635625f39 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 2 Mar 2018 09:08:13 -0500 Subject: [PATCH 16/23] Style fixes --- __tests__/processPlugins.test.js | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index f65fb1e99d74..73b7aff57f00 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -256,11 +256,11 @@ test('plugins can provide fallbacks to keys missing from the config', () => { '8': '8px', }, plugins: [ - function({ rule, atRule, addComponents, config }) { + function({ rule, addComponents, config }) { addComponents([ rule('.btn', { - 'border-radius': config('borderRadius.default', '.25rem') - }) + 'border-radius': config('borderRadius.default', '.25rem'), + }), ]) }, ], @@ -274,10 +274,10 @@ test('plugins can provide fallbacks to keys missing from the config', () => { `) }) -test('the "@" sign is optional in at-rules', () => { +test("the '@' sign is optional in at-rules", () => { const [components, utilities] = processPlugins({ plugins: [ - function ({ rule, atRule, addComponents }) { + function({ rule, atRule, addComponents }) { addComponents([ rule('.card', { padding: '.5rem', @@ -291,9 +291,9 @@ test('the "@" sign is optional in at-rules', () => { rule('.card', { padding: '1.5rem', }), - ]) + ]), ]) - } + }, ], }) @@ -316,15 +316,15 @@ test('the "@" sign is optional in at-rules', () => { }) test('variants are optional when adding utilities', () => { - const [components, utilities] = processPlugins({ + const [, utilities] = processPlugins({ plugins: [ - function ({ rule, addUtilities }) { + function({ rule, addUtilities }) { addUtilities([ rule('.border-collapse', { - 'border-collapse': 'collapse' - }) + 'border-collapse': 'collapse', + }), ]) - } + }, ], }) @@ -340,33 +340,33 @@ test('variants are optional when adding utilities', () => { test('plugins can add multiple sets of utilities and components', () => { const [components, utilities] = processPlugins({ plugins: [ - function ({ rule, atRule, addUtilities, addComponents }) { + function({ rule, addUtilities, addComponents }) { addComponents([ rule('.card', { padding: '1rem', 'border-radius': '.25rem', - }) + }), ]) addUtilities([ rule('.skew-12deg', { - transform: 'skewY(-12deg)' - }) + transform: 'skewY(-12deg)', + }), ]) addComponents([ rule('.btn', { padding: '1rem .5rem', display: 'inline-block', - }) + }), ]) addUtilities([ rule('.border-collapse', { - 'border-collapse': 'collapse' - }) + 'border-collapse': 'collapse', + }), ]) - } + }, ], }) From 8b5f6f14af381869686ce665484d44eb7d0f7b56 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 2 Mar 2018 10:46:19 -0500 Subject: [PATCH 17/23] Add container classes as utilities not components Just for now so that this feature can be introduced into the codebase without forcing a BC break. The container classes will eventually be moved to a built-in plugin that adds them as components. --- __tests__/fixtures/tailwind-input.css | 2 -- src/lib/substituteTailwindAtRules.js | 10 +--------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/__tests__/fixtures/tailwind-input.css b/__tests__/fixtures/tailwind-input.css index 53204f9fd112..ac3999689618 100644 --- a/__tests__/fixtures/tailwind-input.css +++ b/__tests__/fixtures/tailwind-input.css @@ -1,7 +1,5 @@ @tailwind preflight; -@tailwind components; - @tailwind utilities; @responsive { diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index 374017d95845..8fba3d8dfebc 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -21,20 +21,12 @@ export default function(config) { } if (atRule.params === 'components') { - const tailwindComponentTree = postcss.root({ - nodes: container(unwrappedConfig), - }) - const pluginComponentTree = postcss.root({ nodes: pluginComponents, }) - prefixTree(tailwindComponentTree, unwrappedConfig.options.prefix) - - tailwindComponentTree.walk(node => (node.source = atRule.source)) pluginComponentTree.walk(node => (node.source = atRule.source)) - atRule.before(tailwindComponentTree) atRule.before(pluginComponentTree) atRule.remove() } @@ -47,7 +39,7 @@ export default function(config) { } const tailwindUtilityTree = postcss.root({ - nodes: utilities.nodes, + nodes: [...container(unwrappedConfig), ...utilities.nodes], }) const pluginUtilityTree = postcss.root({ From 6aafe5a4de15031045e6233697bac8c949ce1a22 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 2 Mar 2018 10:57:20 -0500 Subject: [PATCH 18/23] Add `utility` helper for creating utility rules that are automatically escaped and respect prefix/important options --- __tests__/processPlugins.test.js | 52 ++++++++++++++++++++++++++++++++ src/util/processPlugins.js | 15 +++++++++ 2 files changed, 67 insertions(+) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index 73b7aff57f00..ff2350e550b8 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -418,3 +418,55 @@ test("plugins can apply the user's chosen prefix", () => { } `) }) + +test("utilities are escaped and automatically respect prefix and important options when created via `utility`", () => { + const [, utilities] = processPlugins({ + plugins: [ + function({ utility, addUtilities }) { + addUtilities([ + utility('.rotate-1/4', { + transform: 'rotate(90deg)', + }), + ]) + }, + ], + options: { + prefix: 'tw-', + important: true, + }, + }) + + expect(css(utilities)).toMatchCss(` + @variants { + .tw-rotate-1\\/4 { + transform: rotate(90deg) !important + } + } + `) +}) + +test("leading '.' is optional when creating utilities via `utility`", () => { + const [, utilities] = processPlugins({ + plugins: [ + function({ utility, addUtilities }) { + addUtilities([ + utility('rotate-1/4', { + transform: 'rotate(90deg)', + }), + ]) + }, + ], + options: { + prefix: 'tw-', + important: true, + }, + }) + + expect(css(utilities)).toMatchCss(` + @variants { + .tw-rotate-1\\/4 { + transform: rotate(90deg) !important + } + } + `) +}) diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index d8fd1b6169b2..9d136c04726a 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -15,6 +15,20 @@ function defineRule(selector, properties) { return postcss.rule({ selector }).append(decls) } +function defineUtility(selector, properties, options) { + if (selector.startsWith('.')) { + return defineUtility(selector.slice(1), properties, options) + } + + const rule = defineRule(prefixSelector(options.prefix, `.${escapeClassName(selector)}`), properties) + + if (options.important) { + rule.walkDecls(decl => (decl.important = true)) + } + + return rule +} + function defineAtRule(atRule, rules) { const [name, ...params] = atRule.split(' ') @@ -34,6 +48,7 @@ export default function(config) { plugin({ config: (path, defaultValue) => _.get(config, path, defaultValue), rule: defineRule, + utility: (selector, properties) => defineUtility(selector, properties, config.options), atRule: defineAtRule, e: escapeClassName, addUtilities: (utilities, variants = []) => { From 9b083eecafccb92ea1bff00fb2df9d8d8eeea2f2 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 2 Mar 2018 11:00:09 -0500 Subject: [PATCH 19/23] Inline plugin functions in tests --- __tests__/processPlugins.test.js | 159 +++++++++++++++---------------- 1 file changed, 79 insertions(+), 80 deletions(-) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index ff2350e550b8..1eadd82e572f 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -6,72 +6,25 @@ function css(nodes) { return postcss.root({ nodes }).toString() } -function objectFitPlugin(variants = []) { - return function({ rule, addUtilities }) { - addUtilities( - [ - rule('.object-fill', { - 'object-fit': 'fill', - }), - rule('.object-contain', { - 'object-fit': 'contain', - }), - rule('.object-cover', { - 'object-fit': 'cover', - }), - ], - variants - ) - } -} - -function buttonPlugin() { - return function({ rule, addComponents }) { - addComponents([ - rule('.btn-blue', { - 'background-color': 'blue', - color: 'white', - padding: '.5rem 1rem', - 'border-radius': '.25rem', - }), - rule('.btn-blue:hover', { - 'background-color': 'darkblue', - }), - ]) - } -} - -function containerPlugin() { - return function({ rule, atRule, addComponents }) { - addComponents([ - rule('.container', { - width: '100%', - }), - atRule('@media (min-width: 100px)', [ - rule('.container', { - 'max-width': '100px', - }), - ]), - atRule('@media (min-width: 200px)', [ - rule('.container', { - 'max-width': '200px', - }), - ]), - atRule('@media (min-width: 300px)', [ - rule('.container', { - 'max-width': '300px', - }), - ]), - ]) - } -} - test('plugins can create utilities', () => { - const config = { - plugins: [objectFitPlugin()], - } - - const [components, utilities] = processPlugins(config) + const [components, utilities] = processPlugins({ + plugins: [ + function({ rule, addUtilities }) { + addUtilities( + [ + rule('.object-fill', { + 'object-fit': 'fill', + }), + rule('.object-contain', { + 'object-fit': 'contain', + }), + rule('.object-cover', { + 'object-fit': 'cover', + }), + ]) + } + ], + }) expect(components.length).toBe(0) expect(css(utilities)).toMatchCss(` @@ -90,11 +43,24 @@ test('plugins can create utilities', () => { }) test('plugins can create utilities with variants', () => { - const config = { - plugins: [objectFitPlugin(['responsive', 'hover', 'group-hover', 'focus'])], - } - - const [components, utilities] = processPlugins(config) + const [components, utilities] = processPlugins({ + plugins: [ + function({ rule, addUtilities }) { + addUtilities( + [ + rule('.object-fill', { + 'object-fit': 'fill', + }), + rule('.object-contain', { + 'object-fit': 'contain', + }), + rule('.object-cover', { + 'object-fit': 'cover', + }), + ], ['responsive', 'hover', 'group-hover', 'focus']) + } + ], + }) expect(components.length).toBe(0) expect(css(utilities)).toMatchCss(` @@ -113,11 +79,23 @@ test('plugins can create utilities with variants', () => { }) test('plugins can create components', () => { - const config = { - plugins: [buttonPlugin()], - } - - const [components, utilities] = processPlugins(config) + const [components, utilities] = processPlugins({ + plugins: [ + function({ rule, addComponents }) { + addComponents([ + rule('.btn-blue', { + 'background-color': 'blue', + color: 'white', + padding: '.5rem 1rem', + 'border-radius': '.25rem', + }), + rule('.btn-blue:hover', { + 'background-color': 'darkblue', + }), + ]) + } + ], + }) expect(utilities.length).toBe(0) expect(css(components)).toMatchCss(` @@ -134,11 +112,32 @@ test('plugins can create components', () => { }) test('plugins can create components with media queries', () => { - const config = { - plugins: [containerPlugin()], - } - - const [components, utilities] = processPlugins(config) + const [components, utilities] = processPlugins({ + plugins: [ + function({ rule, atRule, addComponents }) { + addComponents([ + rule('.container', { + width: '100%', + }), + atRule('@media (min-width: 100px)', [ + rule('.container', { + 'max-width': '100px', + }), + ]), + atRule('@media (min-width: 200px)', [ + rule('.container', { + 'max-width': '200px', + }), + ]), + atRule('@media (min-width: 300px)', [ + rule('.container', { + 'max-width': '300px', + }), + ]), + ]) + } + ], + }) expect(utilities.length).toBe(0) expect(css(components)).toMatchCss(` From aa7db1281f324d5f12d650a7663c90fb31207060 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Fri, 2 Mar 2018 14:52:03 -0500 Subject: [PATCH 20/23] Style fixes --- __tests__/processPlugins.test.js | 35 ++++++++++++++++---------------- src/util/processPlugins.js | 5 ++++- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index 1eadd82e572f..12f1e64b94a2 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -10,19 +10,18 @@ test('plugins can create utilities', () => { const [components, utilities] = processPlugins({ plugins: [ function({ rule, addUtilities }) { - addUtilities( - [ - rule('.object-fill', { - 'object-fit': 'fill', - }), - rule('.object-contain', { - 'object-fit': 'contain', - }), - rule('.object-cover', { - 'object-fit': 'cover', - }), + addUtilities([ + rule('.object-fill', { + 'object-fit': 'fill', + }), + rule('.object-contain', { + 'object-fit': 'contain', + }), + rule('.object-cover', { + 'object-fit': 'cover', + }), ]) - } + }, ], }) @@ -57,8 +56,10 @@ test('plugins can create utilities with variants', () => { rule('.object-cover', { 'object-fit': 'cover', }), - ], ['responsive', 'hover', 'group-hover', 'focus']) - } + ], + ['responsive', 'hover', 'group-hover', 'focus'] + ) + }, ], }) @@ -93,7 +94,7 @@ test('plugins can create components', () => { 'background-color': 'darkblue', }), ]) - } + }, ], }) @@ -135,7 +136,7 @@ test('plugins can create components with media queries', () => { }), ]), ]) - } + }, ], }) @@ -418,7 +419,7 @@ test("plugins can apply the user's chosen prefix", () => { `) }) -test("utilities are escaped and automatically respect prefix and important options when created via `utility`", () => { +test('utilities are escaped and automatically respect prefix and important options when created via `utility`', () => { const [, utilities] = processPlugins({ plugins: [ function({ utility, addUtilities }) { diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index 9d136c04726a..b1143a7725bf 100644 --- a/src/util/processPlugins.js +++ b/src/util/processPlugins.js @@ -20,7 +20,10 @@ function defineUtility(selector, properties, options) { return defineUtility(selector.slice(1), properties, options) } - const rule = defineRule(prefixSelector(options.prefix, `.${escapeClassName(selector)}`), properties) + const rule = defineRule( + prefixSelector(options.prefix, `.${escapeClassName(selector)}`), + properties + ) if (options.important) { rule.walkDecls(decl => (decl.important = true)) From c83cbc7cc7f39aba8be651c7214383f71e4883c5 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Mon, 5 Mar 2018 09:37:17 -0500 Subject: [PATCH 21/23] Add test to document checking the user's !important preference --- __tests__/processPlugins.test.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index 12f1e64b94a2..f09c9a4e71e0 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -470,3 +470,28 @@ test("leading '.' is optional when creating utilities via `utility`", () => { } `) }) + +test("plugins can choose to make declarations !important", () => { + const [, utilities] = processPlugins({ + plugins: [ + function({ rule, addUtilities, config }) { + addUtilities([ + rule('.skew-12deg', { + transform: `skewY(-12deg)${config('options.important') ? ' !important' : ''}`, + }), + ]) + }, + ], + options: { + important: true, + }, + }) + + expect(css(utilities)).toMatchCss(` + @variants { + .skew-12deg { + transform: skewY(-12deg) !important + } + } + `) +}) From ae5b28cf9303ad86531386794341b0d7ebea8c5e Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Mon, 5 Mar 2018 09:54:43 -0500 Subject: [PATCH 22/23] Add plugin comment to default config --- defaultConfig.stub.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/defaultConfig.stub.js b/defaultConfig.stub.js index 408db7af852f..326620bdec09 100644 --- a/defaultConfig.stub.js +++ b/defaultConfig.stub.js @@ -859,6 +859,19 @@ module.exports = { }, + /* + |----------------------------------------------------------------------------- + | Plugins https://tailwindcss.com/docs/plugins + |----------------------------------------------------------------------------- + | + | Here is where you can register any additional plugins you'd like to use in + | your project. + | + | Be sure to view the complete plugin documentation to learn more about how + | the plugin system works. + | + */ + plugins: [], From c9f3a1dd403ed61bcbd1a236a880110cd0fc7df2 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Mon, 5 Mar 2018 09:55:09 -0500 Subject: [PATCH 23/23] Fix style --- __tests__/processPlugins.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index f09c9a4e71e0..e053350ec0f4 100644 --- a/__tests__/processPlugins.test.js +++ b/__tests__/processPlugins.test.js @@ -471,7 +471,7 @@ test("leading '.' is optional when creating utilities via `utility`", () => { `) }) -test("plugins can choose to make declarations !important", () => { +test('plugins can choose to make declarations !important', () => { const [, utilities] = processPlugins({ plugins: [ function({ rule, addUtilities, config }) {