From 782a828c81c877a8ec45cde214a015b53df07746 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Sat, 17 Oct 2020 21:00:42 -0400 Subject: [PATCH] Switch to new apply implementation --- __tests__/applyAtRule.test.js | 252 ------------------- __tests__/fixtures/tailwind-input-import.css | 6 +- src/featureFlags.js | 1 - src/lib/substituteClassApplyAtRules.js | 155 +----------- 4 files changed, 4 insertions(+), 410 deletions(-) delete mode 100644 __tests__/applyAtRule.test.js diff --git a/__tests__/applyAtRule.test.js b/__tests__/applyAtRule.test.js deleted file mode 100644 index 36dce22c6..000000000 --- a/__tests__/applyAtRule.test.js +++ /dev/null @@ -1,252 +0,0 @@ -import postcss from 'postcss' -import tailwind from '../src/index' - -function run(input, config = {}) { - return postcss([tailwind({ ...config })]).process(input, { from: undefined }) -} - -test('it copies the declarations from a class into itself', () => { - const output = '.a { color: red; } .b { color: red; }' - - return run('.a { color: red; } .b { @apply .a; }').then((result) => { - expect(result.css).toMatchCss(output) - expect(result.warnings().length).toBe(0) - }) -}) - -test('selectors with invalid characters do not need to be manually escaped', () => { - const input = ` - .a\\:1\\/2 { color: red; } - .b { @apply .a:1/2; } - ` - - const expected = ` - .a\\:1\\/2 { color: red; } - .b { color: red; } - ` - - return run(input).then((result) => { - expect(result.css).toMatchCss(expected) - expect(result.warnings().length).toBe(0) - }) -}) - -test('it removes important from applied classes by default', () => { - const input = ` - .a { color: red !important; } - .b { @apply .a; } - ` - - const expected = ` - .a { color: red !important; } - .b { color: red; } - ` - - return run(input).then((result) => { - expect(result.css).toMatchCss(expected) - expect(result.warnings().length).toBe(0) - }) -}) - -test('applied rules can be made !important', () => { - const input = ` - .a { color: red; } - .b { @apply .a !important; } - ` - - const expected = ` - .a { color: red; } - .b { color: red !important; } - ` - - return run(input).then((result) => { - expect(result.css).toMatchCss(expected) - expect(result.warnings().length).toBe(0) - }) -}) - -test('cssnext custom property sets are preserved', () => { - const input = ` - .a { - color: red; - } - .b { - @apply .a --custom-property-set; - } - ` - - const expected = ` - .a { - color: red; - } - .b { - color: red; - @apply --custom-property-set; - } - ` - - return run(input).then((result) => { - expect(result.css).toMatchCss(expected) - expect(result.warnings().length).toBe(0) - }) -}) - -test('it fails if the class does not exist', () => { - return run('.b { @apply .a; }').catch((e) => { - expect(e).toMatchObject({ name: 'CssSyntaxError' }) - }) -}) - -test('applying classes that are defined in a media query is not supported', () => { - const input = ` - @media (min-width: 300px) { - .a { color: blue; } - } - - .b { - @apply .a; - } - ` - expect.assertions(1) - return run(input).catch((e) => { - expect(e).toMatchObject({ name: 'CssSyntaxError' }) - }) -}) - -test('applying classes that are ever used in a media query is not supported', () => { - const input = ` - .a { - color: red; - } - - @media (min-width: 300px) { - .a { color: blue; } - } - - .b { - @apply .a; - } - ` - expect.assertions(1) - return run(input).catch((e) => { - expect(e).toMatchObject({ name: 'CssSyntaxError' }) - }) -}) - -test('it does not match classes that include pseudo-selectors', () => { - const input = ` - .a:hover { - color: red; - } - - .b { - @apply .a; - } - ` - expect.assertions(1) - return run(input).catch((e) => { - expect(e).toMatchObject({ name: 'CssSyntaxError' }) - }) -}) - -test('it does not match classes that have multiple rules', () => { - const input = ` - .a { - color: red; - } - - .b { - @apply .a; - } - - .a { - color: blue; - } - ` - expect.assertions(1) - return run(input).catch((e) => { - expect(e).toMatchObject({ name: 'CssSyntaxError' }) - }) -}) - -test('you can apply utility classes that do not actually exist as long as they would exist if utilities were being generated', () => { - const input = ` - .foo { @apply .mt-4; } - ` - - const expected = ` - .foo { margin-top: 1rem; } - ` - - return run(input).then((result) => { - expect(result.css).toMatchCss(expected) - expect(result.warnings().length).toBe(0) - }) -}) - -test('you can apply utility classes without using the given prefix', () => { - const input = ` - .foo { @apply .tw-mt-4 .mb-4; } - ` - - const expected = ` - .foo { margin-top: 1rem; margin-bottom: 1rem; } - ` - - return run(input, { prefix: 'tw-' }).then((result) => { - expect(result.css).toMatchCss(expected) - expect(result.warnings().length).toBe(0) - }) -}) - -test('you can apply utility classes without using the given prefix when using a function for the prefix', () => { - const input = ` - .foo { @apply .tw-mt-4 .mb-4; } - ` - - const expected = ` - .foo { margin-top: 1rem; margin-bottom: 1rem; } - ` - - return run(input, { - prefix: () => { - return 'tw-' - }, - }).then((result) => { - expect(result.css).toMatchCss(expected) - expect(result.warnings().length).toBe(0) - }) -}) - -test('you can apply utility classes without specificity prefix even if important (selector) is used', () => { - const input = ` - .foo { @apply .mt-8 .mb-8; } - ` - - const expected = ` - .foo { margin-top: 2rem; margin-bottom: 2rem; } - ` - - return run(input, { important: '#app' }).then((result) => { - expect(result.css).toMatchCss(expected) - expect(result.warnings().length).toBe(0) - }) -}) - -test('you can apply utility classes without using the given prefix even if important (selector) is used', () => { - const input = ` - .foo { @apply .tw-mt-4 .mb-4; } - ` - - const expected = ` - .foo { margin-top: 1rem; margin-bottom: 1rem; } - ` - - return run(input, { - prefix: 'tw-', - important: '#app', - }).then((result) => { - expect(result.css).toMatchCss(expected) - expect(result.warnings().length).toBe(0) - }) -}) diff --git a/__tests__/fixtures/tailwind-input-import.css b/__tests__/fixtures/tailwind-input-import.css index 344489980..f5e5f08bd 100644 --- a/__tests__/fixtures/tailwind-input-import.css +++ b/__tests__/fixtures/tailwind-input-import.css @@ -1,12 +1,12 @@ -@import "tailwindcss/base"; +@import 'tailwindcss/base'; @import 'tailwindcss/components'; -@import "tailwindcss/utilities"; +@import 'tailwindcss/utilities'; @responsive { .example { - @apply .font-bold; + @apply font-bold; color: theme('colors.red.500'); } } diff --git a/src/featureFlags.js b/src/featureFlags.js index 76374c427..56c88381c 100644 --- a/src/featureFlags.js +++ b/src/featureFlags.js @@ -8,7 +8,6 @@ const featureFlags = { 'uniformColorPalette', 'extendedSpacingScale', 'extendedFontSizeScale', - 'applyComplexClasses', 'darkModeVariant', 'additionalBreakpoint', ], diff --git a/src/lib/substituteClassApplyAtRules.js b/src/lib/substituteClassApplyAtRules.js index fc46c325e..f439ca819 100644 --- a/src/lib/substituteClassApplyAtRules.js +++ b/src/lib/substituteClassApplyAtRules.js @@ -1,158 +1,5 @@ -import _ from 'lodash' -import postcss from 'postcss' -import escapeClassName from '../util/escapeClassName' -import prefixSelector from '../util/prefixSelector' -import increaseSpecificity from '../util/increaseSpecificity' - -import { flagEnabled } from '../featureFlags' import applyComplexClasses from '../flagged/applyComplexClasses' -function buildClassTable(css) { - const classTable = {} - - css.walkRules((rule) => { - if (!_.has(classTable, rule.selector)) { - classTable[rule.selector] = [] - } - classTable[rule.selector].push(rule) - }) - - return classTable -} - -function buildShadowTable(generatedUtilities) { - const utilities = postcss.root() - - postcss.root({ nodes: generatedUtilities }).walkAtRules('variants', (atRule) => { - utilities.append(atRule.clone().nodes) - }) - - return buildClassTable(utilities) -} - -function normalizeClassName(className) { - return `.${escapeClassName(_.trimStart(className, '.'))}` -} - -function findClass(classToApply, classTable, onError) { - const matches = _.get(classTable, classToApply, []) - - if (_.isEmpty(matches)) { - return [] - } - - if (matches.length > 1) { - // prettier-ignore - throw onError(`\`@apply\` cannot be used with ${classToApply} because ${classToApply} is included in multiple rulesets.`) - } - - const [match] = matches - - if (match.parent.type !== 'root') { - // prettier-ignore - throw onError(`\`@apply\` cannot be used with ${classToApply} because ${classToApply} is nested inside of an at-rule (@${match.parent.name}).`) - } - - return match.clone().nodes -} - -let shadowLookup = null - export default function (config, getProcessedPlugins, configChanged) { - if (flagEnabled(config, 'applyComplexClasses')) { - return applyComplexClasses(config, getProcessedPlugins, configChanged) - } - - return function (css) { - const classLookup = buildClassTable(css) - shadowLookup = - configChanged || !shadowLookup - ? buildShadowTable(getProcessedPlugins().utilities) - : shadowLookup - - css.walkRules((rule) => { - rule.walkAtRules('apply', (atRule) => { - const classesAndProperties = postcss.list.space(atRule.params) - - /* - * Don't wreck CSSNext-style @apply rules: - * http://cssnext.io/features/#custom-properties-set-apply - * - * These are deprecated in CSSNext but still playing it safe for now. - * We might consider renaming this at-rule. - */ - const [customProperties, classes] = _.partition(classesAndProperties, (classOrProperty) => { - return _.startsWith(classOrProperty, '--') - }) - - const decls = _(classes) - .reject((cssClass) => cssClass === '!important') - .flatMap((cssClass) => { - const classToApply = normalizeClassName(cssClass) - const onError = (message) => { - return atRule.error(message) - } - - return _.reduce( - [ - // Find exact class match in user's CSS - () => { - return findClass(classToApply, classLookup, onError) - }, - // Find exact class match in shadow lookup - () => { - return findClass(classToApply, shadowLookup, onError) - }, - // Find prefixed version of class in shadow lookup - () => { - return findClass( - prefixSelector(config.prefix, classToApply), - shadowLookup, - onError - ) - }, - // Find important-scoped version of class in shadow lookup - () => { - return findClass( - increaseSpecificity(config.important, classToApply), - shadowLookup, - onError - ) - }, - // Find important-scoped and prefixed version of class in shadow lookup - () => { - return findClass( - increaseSpecificity( - config.important, - prefixSelector(config.prefix, classToApply) - ), - shadowLookup, - onError - ) - }, - () => { - // prettier-ignore - throw onError(`\`@apply\` cannot be used with \`${classToApply}\` because \`${classToApply}\` either cannot be found, or its actual definition includes a pseudo-selector like :hover, :active, etc. If you're sure that \`${classToApply}\` exists, make sure that any \`@import\` statements are being properly processed *before* Tailwind CSS sees your CSS, as \`@apply\` can only be used for classes in the same CSS tree.`) - }, - ], - (classDecls, candidate) => (!_.isEmpty(classDecls) ? classDecls : candidate()), - [] - ) - }) - .value() - - _.tap(_.last(classesAndProperties) === '!important', (important) => { - decls.forEach((decl) => (decl.important = important)) - }) - - atRule.before(decls) - - atRule.params = customProperties.join(' ') - - if (_.isEmpty(customProperties)) { - atRule.remove() - } - }) - }) - } + return applyComplexClasses(config, getProcessedPlugins, configChanged) }