From ec0b7a2be1a0ec2176a614bcec94ada6cd7aa603 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Mon, 27 Apr 2020 13:07:27 -0400 Subject: [PATCH 1/4] Integrate PurgeCSS directly into Tailwind --- __tests__/fixtures/purge-example.html | 15 + __tests__/fixtures/tailwind-output-purged.css | 628 ++++++++++++++++++ __tests__/responsiveAtRule.test.js | 20 + __tests__/sanity.test.js | 120 ++++ package.json | 1 + scripts/rebuildFixtures.js | 23 + src/lib/purgeUnusedUtilities.js | 56 ++ src/lib/substituteResponsiveAtRules.js | 22 +- src/lib/substituteTailwindAtRules.js | 20 + src/processTailwindFeatures.js | 2 + yarn.lock | 41 +- 11 files changed, 925 insertions(+), 23 deletions(-) create mode 100644 __tests__/fixtures/purge-example.html create mode 100644 __tests__/fixtures/tailwind-output-purged.css create mode 100644 src/lib/purgeUnusedUtilities.js diff --git a/__tests__/fixtures/purge-example.html b/__tests__/fixtures/purge-example.html new file mode 100644 index 000000000000..5e07ba579aeb --- /dev/null +++ b/__tests__/fixtures/purge-example.html @@ -0,0 +1,15 @@ + +
+ + + + + + + + +
diff --git a/__tests__/fixtures/tailwind-output-purged.css b/__tests__/fixtures/tailwind-output-purged.css new file mode 100644 index 000000000000..d1ee8726ab83 --- /dev/null +++ b/__tests__/fixtures/tailwind-output-purged.css @@ -0,0 +1,628 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} + +/** + * Manually forked from SUIT CSS Base: https://github.com/suitcss/base + * A thin layer on top of normalize.css that provides a starting point more + * suitable for web applications. + */ + +/** + * Removes the default spacing and border for appropriate elements. + */ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +button { + background-color: transparent; + background-image: none; + padding: 0; +} + +/** + * Work around a Firefox/IE bug where the transparent `button` background + * results in a loss of the default `button` focus styles. + */ + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +fieldset { + margin: 0; + padding: 0; +} + +ol, +ul { + list-style: none; + margin: 0; + padding: 0; +} + +/** + * Tailwind custom reset styles + */ + +/** + * 1. Use the user's configured `sans` font-family (with Tailwind's default + * sans-serif font stack as a fallback) as a sane default. + * 2. Use Tailwind's default "normal" line-height so the user isn't forced + * to override it to ensure consistency even when using the default theme. + */ + +html { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 1 */ + line-height: 1.5; /* 2 */ +} + +/** + * 1. Prevent padding and border from affecting element width. + * + * We used to set this in the html element and inherit from + * the parent element for everything else. This caused issues + * in shadow-dom-enhanced elements like
where the content + * is wrapped by a div with box-sizing set to `content-box`. + * + * https://github.com/mozdevs/cssremedy/issues/4 + * + * + * 2. Allow adding a border to an element by just adding a border-width. + * + * By default, the way the browser specifies that an element should have no + * border is by setting it's border-style to `none` in the user-agent + * stylesheet. + * + * In order to easily add borders to elements by just setting the `border-width` + * property, we change the default border-style for all elements to `solid`, and + * use border-width to hide them instead. This way our `border` utilities only + * need to set the `border-width` property instead of the entire `border` + * shorthand, making our border utilities much more straightforward to compose. + * + * https://github.com/tailwindcss/tailwindcss/pull/116 + */ + +*, +::before, +::after { + box-sizing: border-box; /* 1 */ + border-width: 0; /* 2 */ + border-style: solid; /* 2 */ + border-color: #e2e8f0; /* 2 */ +} + +/* + * Ensure horizontal rules are visible by default + */ + +hr { + border-top-width: 1px; +} + +/** + * Undo the `border-style: none` reset that Normalize applies to images so that + * our `border-{width}` utilities have the expected effect. + * + * The Normalize reset is unnecessary for us since we default the border-width + * to 0 on all elements. + * + * https://github.com/tailwindcss/tailwindcss/issues/362 + */ + +img { + border-style: solid; +} + +textarea { + resize: vertical; +} + +input::placeholder, +textarea::placeholder { + color: #a0aec0; +} + +button, +[role="button"] { + cursor: pointer; +} + +table { + border-collapse: collapse; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/** + * Reset links to optimize for opt-in styling instead of + * opt-out. + */ + +a { + color: inherit; + text-decoration: inherit; +} + +/** + * Reset form element properties that are easy to forget to + * style explicitly so you don't inadvertently introduce + * styles that deviate from your design system. These styles + * supplement a partial reset that is already applied by + * normalize.css. + */ + +button, +input, +optgroup, +select, +textarea { + padding: 0; + line-height: inherit; + color: inherit; +} + +/** + * Use the configured 'mono' font family for elements that + * are expected to be rendered with a monospace font, falling + * back to the system monospace stack if there is no configured + * 'mono' font family. + */ + +pre, +code, +kbd, +samp { + font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +/** + * Make replaced elements `display: block` by default as that's + * the behavior you want almost all of the time. Inspired by + * CSS Remedy, with `svg` added as well. + * + * https://github.com/mozdevs/cssremedy/issues/14 + */ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + vertical-align: middle; +} + +/** + * Constrain images and videos to the parent width and preserve + * their instrinsic aspect ratio. + * + * https://github.com/mozdevs/cssremedy/issues/14 + */ + +img, +video { + max-width: 100%; + height: auto; +} + +.bg-red-500 { + --bg-opacity: 1; + background-color: #f56565; + background-color: rgba(245, 101, 101, var(--bg-opacity)); +} + +.bg-black\! { + --bg-opacity: 1; + background-color: #000; + background-color: rgba(0, 0, 0, var(--bg-opacity)); +} + +.block { + display: block; +} + +.font-\%\#\$\@ { + font-family: Comic Sans; +} + +.h-screen { + height: 100vh; +} + +.w-\(1\/2\+8\) { + width: calc(50% + 2rem); +} + +.w-1\/2 { + width: 50%; +} + +.example { + font-weight: 700; + color: #f56565; +} + +@media (min-width: 768px) { + .md\:bg-blue-300 { + --bg-opacity: 1; + background-color: #90cdf4; + background-color: rgba(144, 205, 244, var(--bg-opacity)); + } + + .md\:flow-root { + display: flow-root; + } +} diff --git a/__tests__/responsiveAtRule.test.js b/__tests__/responsiveAtRule.test.js index 63c6319a4705..a59dff31ff45 100644 --- a/__tests__/responsiveAtRule.test.js +++ b/__tests__/responsiveAtRule.test.js @@ -12,6 +12,8 @@ test('it can generate responsive variants', () => { .banana { color: yellow; } .chocolate { color: brown; } } + + @tailwind screens; ` const output = ` @@ -52,6 +54,8 @@ test('it can generate responsive variants with a custom separator', () => { .banana { color: yellow; } .chocolate { color: brown; } } + + @tailwind screens; ` const output = ` @@ -92,6 +96,8 @@ test('it can generate responsive variants when classes have non-standard charact .hover\\:banana { color: yellow; } .chocolate-2\\.5 { color: brown; } } + + @tailwind screens; ` const output = ` @@ -137,6 +143,8 @@ test('responsive variants are grouped', () => { @responsive { .chocolate { color: brown; } } + + @tailwind screens; ` const output = ` @@ -181,6 +189,8 @@ test('it can generate responsive variants for nested at-rules', () => { .grid\\:banana { color: blue; } } } + + @tailwind screens; ` const output = ` @@ -244,6 +254,8 @@ test('it can generate responsive variants for deeply nested at-rules', () => { } } } + + @tailwind screens; ` const output = ` @@ -307,6 +319,8 @@ test('screen prefix is only applied to the last class in a selector', () => { @responsive { .banana li * .sandwich #foo > div { color: yellow; } } + + @tailwind screens; ` const output = ` @@ -342,6 +356,8 @@ test('responsive variants are generated for all selectors in a rule', () => { @responsive { .foo, .bar { color: yellow; } } + + @tailwind screens; ` const output = ` @@ -377,6 +393,8 @@ test('selectors with no classes cannot be made responsive', () => { @responsive { div { color: yellow; } } + + @tailwind screens; ` expect.assertions(1) return run(input, { @@ -398,6 +416,8 @@ test('all selectors in a rule must contain classes', () => { @responsive { .foo, div { color: yellow; } } + + @tailwind screens; ` expect.assertions(1) return run(input, { diff --git a/__tests__/sanity.test.js b/__tests__/sanity.test.js index b27f1aae3eeb..dd12478b6c5b 100644 --- a/__tests__/sanity.test.js +++ b/__tests__/sanity.test.js @@ -77,3 +77,123 @@ it('generates the right CSS with implicit screen utilities', () => { expect(result.css).toBe(expected) }) }) + +it('generates the right CSS when "important" is enabled', () => { + const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`) + const input = fs.readFileSync(inputPath, 'utf8') + + return postcss([tailwind({ ...config, important: true })]) + .process(input, { from: inputPath }) + .then(result => { + const expected = fs.readFileSync( + path.resolve(`${__dirname}/fixtures/tailwind-output-important.css`), + 'utf8' + ) + + expect(result.css).toBe(expected) + }) +}) + +it('purges unused classes', () => { + const OLD_NODE_ENV = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`) + const input = fs.readFileSync(inputPath, 'utf8') + + return postcss([ + tailwind({ + ...config, + theme: { + extend: { + colors: { + 'black!': '#000', + }, + spacing: { + '(1/2+8)': 'calc(50% + 2rem)', + }, + minHeight: { + '(screen-100)': 'calc(100vh - 1rem)', + }, + fontFamily: { + '%#$@': 'Comic Sans', + }, + }, + }, + purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)], + }), + ]) + .process(input, { from: inputPath }) + .then(result => { + process.env.NODE_ENV = OLD_NODE_ENV + const expected = fs.readFileSync( + path.resolve(`${__dirname}/fixtures/tailwind-output-purged.css`), + 'utf8' + ) + + expect(result.css).toBe(expected) + }) +}) + +it('does not purge except in production', () => { + const OLD_NODE_ENV = process.env.NODE_ENV + process.env.NODE_ENV = 'development' + const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`) + const input = fs.readFileSync(inputPath, 'utf8') + + return postcss([ + tailwind({ + ...config, + purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)], + }), + ]) + .process(input, { from: inputPath }) + .then(result => { + process.env.NODE_ENV = OLD_NODE_ENV + const expected = fs.readFileSync( + path.resolve(`${__dirname}/fixtures/tailwind-output.css`), + 'utf8' + ) + + expect(result.css).toBe(expected) + }) +}) + +it('purges outside of production if explicitly enabled', () => { + const OLD_NODE_ENV = process.env.NODE_ENV + process.env.NODE_ENV = 'development' + const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`) + const input = fs.readFileSync(inputPath, 'utf8') + + return postcss([ + tailwind({ + ...config, + theme: { + extend: { + colors: { + 'black!': '#000', + }, + spacing: { + '(1/2+8)': 'calc(50% + 2rem)', + }, + minHeight: { + '(screen-100)': 'calc(100vh - 1rem)', + }, + fontFamily: { + '%#$@': 'Comic Sans', + }, + }, + }, + purge: { enabled: true, paths: [path.resolve(`${__dirname}/fixtures/**/*.html`)] }, + }), + ]) + .process(input, { from: inputPath }) + .then(result => { + process.env.NODE_ENV = OLD_NODE_ENV + const expected = fs.readFileSync( + path.resolve(`${__dirname}/fixtures/tailwind-output-purged.css`), + 'utf8' + ) + + expect(result.css).toBe(expected) + }) +}) diff --git a/package.json b/package.json index f89bd7319406..b5bc5f105873 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "rimraf": "^3.0.0" }, "dependencies": { + "@fullhuman/postcss-purgecss": "^2.1.2", "autoprefixer": "^9.4.5", "bytes": "^3.0.0", "chalk": "^4.0.0", diff --git a/scripts/rebuildFixtures.js b/scripts/rebuildFixtures.js index 707b9920b9be..071c3664d23b 100644 --- a/scripts/rebuildFixtures.js +++ b/scripts/rebuildFixtures.js @@ -39,6 +39,29 @@ Promise.all([ to: '__tests__/fixtures/tailwind-output-important.css', config: { important: true }, }), + build({ + from: '__tests__/fixtures/tailwind-input.css', + to: '__tests__/fixtures/tailwind-output-purged.css', + config: { + theme: { + extend: { + colors: { + 'black!': '#000', + }, + spacing: { + '(1/2+8)': 'calc(50% + 2rem)', + }, + minHeight: { + '(screen-100)': 'calc(100vh - 1rem)', + }, + fontFamily: { + '%#$@': 'Comic Sans', + }, + }, + }, + purge: { enabled: true, paths: ['./__tests__/fixtures/**/*.html'] }, + }, + }), ]).then(() => { console.log('\nFinished rebuilding fixtures.') console.log( diff --git a/src/lib/purgeUnusedUtilities.js b/src/lib/purgeUnusedUtilities.js new file mode 100644 index 000000000000..1715d94e906a --- /dev/null +++ b/src/lib/purgeUnusedUtilities.js @@ -0,0 +1,56 @@ +import _ from 'lodash' +import postcss from 'postcss' +import purgecss from '@fullhuman/postcss-purgecss' + +export default function purgeUnusedUtilities(config) { + const purgeEnabled = + _.get(config, 'purge.enabled', false) === true || + (config.purge !== undefined && process.env.NODE_ENV === 'production') + + if (!purgeEnabled) { + return function(css) { + css.walkComments(comment => { + switch (comment.text.trim()) { + case 'tailwind start components': + case 'tailwind start utilities': + case 'tailwind start screens': + case 'tailwind end components': + case 'tailwind end utilities': + case 'tailwind end screens': + comment.remove() + break + default: + break + } + }) + } + } + + return postcss([ + function(css) { + css.prepend(postcss.comment({ text: 'purgecss start ignore' })) + css.append(postcss.comment({ text: 'purgecss end ignore' })) + + css.walkComments(comment => { + switch (comment.text.trim()) { + case 'tailwind start components': + case 'tailwind start utilities': + case 'tailwind start screens': + comment.text = 'purgecss end ignore' + break + case 'tailwind end components': + case 'tailwind end utilities': + case 'tailwind end screens': + comment.text = 'purgecss start ignore' + break + default: + break + } + }) + }, + purgecss({ + content: Array.isArray(config.purge) ? config.purge : config.purge.paths, + defaultExtractor: content => content.match(/[^<>"'`\s]+(? i.nodes.length !== 0) - if (!hasScreenRules) { - return - } - - let includesScreensExplicitly = false - css.walkAtRules('tailwind', atRule => { - if (atRule.params === 'screens') { - atRule.replaceWith(finalRules) - includesScreensExplicitly = true + if (atRule.params !== 'screens') { + return } - }) - if (!includesScreensExplicitly) { - css.append(finalRules) - return - } + if (hasScreenRules) { + atRule.before(finalRules) + } + + atRule.remove() + }) } } diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js index d82a28692ba9..1607f388b959 100644 --- a/src/lib/substituteTailwindAtRules.js +++ b/src/lib/substituteTailwindAtRules.js @@ -40,6 +40,8 @@ export default function( } }) + let includesScreensExplicitly = false + css.walkAtRules('tailwind', atRule => { if (atRule.params === 'preflight') { // prettier-ignore @@ -52,14 +54,32 @@ export default function( } if (atRule.params === 'components') { + atRule.before(postcss.comment({ text: 'tailwind start components' })) atRule.before(updateSource(pluginComponents, atRule.source)) + atRule.after(postcss.comment({ text: 'tailwind end components' })) atRule.remove() } if (atRule.params === 'utilities') { + atRule.before(postcss.comment({ text: 'tailwind start utilities' })) atRule.before(updateSource(pluginUtilities, atRule.source)) + atRule.after(postcss.comment({ text: 'tailwind end utilities' })) atRule.remove() } + + if (atRule.params === 'screens') { + includesScreensExplicitly = true + atRule.before(postcss.comment({ text: 'tailwind start screens' })) + atRule.after(postcss.comment({ text: 'tailwind end screens' })) + } }) + + if (!includesScreensExplicitly) { + css.append([ + postcss.comment({ text: 'tailwind start screens' }), + postcss.atRule({ name: 'tailwind', params: 'screens' }), + postcss.comment({ text: 'tailwind end screens' }), + ]) + } } } diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js index 61fecaf12280..2059e8c9a8b1 100644 --- a/src/processTailwindFeatures.js +++ b/src/processTailwindFeatures.js @@ -7,6 +7,7 @@ import substituteVariantsAtRules from './lib/substituteVariantsAtRules' import substituteResponsiveAtRules from './lib/substituteResponsiveAtRules' import substituteScreenAtRules from './lib/substituteScreenAtRules' import substituteClassApplyAtRules from './lib/substituteClassApplyAtRules' +import purgeUnusedUtilities from './lib/purgeUnusedUtilities' import corePlugins from './corePlugins' import processPlugins from './util/processPlugins' @@ -23,6 +24,7 @@ export default function(getConfig) { substituteResponsiveAtRules(config), substituteScreenAtRules(config), substituteClassApplyAtRules(config, processedPlugins.utilities), + purgeUnusedUtilities(config), ]).process(css, { from: _.get(css, 'source.input.file') }) } } diff --git a/yarn.lock b/yarn.lock index 238bd50b81b0..e2d50ac93093 100644 --- a/yarn.lock +++ b/yarn.lock @@ -845,6 +845,14 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@fullhuman/postcss-purgecss@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@fullhuman/postcss-purgecss/-/postcss-purgecss-2.1.2.tgz#8fe4d4ae2b58214b5452cb490a31c7146517442f" + integrity sha512-Jf34YVBK9GtXTblpu0svNUJdA7rTQoRMz+yEJe6mwTnXDIGipWLzaX/VgU/x6IPC6WvU5SY/XlawwqhxoyFPTg== + dependencies: + postcss "7.0.27" + purgecss "^2.1.2" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b" @@ -1715,6 +1723,11 @@ commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.0.1.tgz#b67622721785993182e807f4883633e6401ba53c" integrity sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA== +commander@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -4266,6 +4279,15 @@ postcss-value-parser@^4.0.3: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d" integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg== +postcss@7.0.27, postcss@^7.0.11, postcss@^7.0.18, postcss@^7.0.21, postcss@^7.0.27: + version "7.0.27" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9" + integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + postcss@^6.0.9: version "6.0.23" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" @@ -4275,15 +4297,6 @@ postcss@^6.0.9: source-map "^0.6.1" supports-color "^5.4.0" -postcss@^7.0.11, postcss@^7.0.18, postcss@^7.0.21, postcss@^7.0.27: - version "7.0.27" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9" - integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -4362,6 +4375,16 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +purgecss@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-2.1.2.tgz#96f666d04c56705208aaa1a544b5f22e13828955" + integrity sha512-5oDBxiT9VonwKmEMohPFRFZrj8fdSVKxHPwq7G5Rx/2pXicZFJu+D4m5bb3NuV0sSK3ooNxq5jFIwwHzifP5FA== + dependencies: + commander "^5.0.0" + glob "^7.0.0" + postcss "7.0.27" + postcss-selector-parser "^6.0.2" + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" From 4c3e97c9d40da394a82ab5a5fdd43d796c76992d Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Mon, 27 Apr 2020 21:43:28 -0400 Subject: [PATCH 2/4] Make PurgeCSS regex compatible with node 8 --- src/lib/purgeUnusedUtilities.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/purgeUnusedUtilities.js b/src/lib/purgeUnusedUtilities.js index 1715d94e906a..2e2c701d6931 100644 --- a/src/lib/purgeUnusedUtilities.js +++ b/src/lib/purgeUnusedUtilities.js @@ -50,7 +50,7 @@ export default function purgeUnusedUtilities(config) { }, purgecss({ content: Array.isArray(config.purge) ? config.purge : config.purge.paths, - defaultExtractor: content => content.match(/[^<>"'`\s]+(? content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [], }), ]) } From b0ac3c50cb714956c3094da899c163570e5f0c57 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Mon, 27 Apr 2020 22:18:21 -0400 Subject: [PATCH 3/4] Support purging Pug templates a bit better --- __tests__/fixtures/purge-example.html | 7 +++++++ __tests__/fixtures/tailwind-output-purged.css | 20 +++++++++++++++++++ src/lib/purgeUnusedUtilities.js | 8 +++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/__tests__/fixtures/purge-example.html b/__tests__/fixtures/purge-example.html index 5e07ba579aeb..722f5f8ef9d9 100644 --- a/__tests__/fixtures/purge-example.html +++ b/__tests__/fixtures/purge-example.html @@ -13,3 +13,10 @@
+ + +span.inline-grid.grid-cols-3 + .col-span-2 + Hello + .col-span-1.text-center + World! \ No newline at end of file diff --git a/__tests__/fixtures/tailwind-output-purged.css b/__tests__/fixtures/tailwind-output-purged.css index d1ee8726ab83..895c3629b84f 100644 --- a/__tests__/fixtures/tailwind-output-purged.css +++ b/__tests__/fixtures/tailwind-output-purged.css @@ -594,6 +594,10 @@ video { display: block; } +.inline-grid { + display: inline-grid; +} + .font-\%\#\$\@ { font-family: Comic Sans; } @@ -602,6 +606,10 @@ video { height: 100vh; } +.text-center { + text-align: center; +} + .w-\(1\/2\+8\) { width: calc(50% + 2rem); } @@ -610,6 +618,18 @@ video { width: 50%; } +.grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.col-span-1 { + grid-column: span 1 / span 1; +} + +.col-span-2 { + grid-column: span 2 / span 2; +} + .example { font-weight: 700; color: #f56565; diff --git a/src/lib/purgeUnusedUtilities.js b/src/lib/purgeUnusedUtilities.js index 2e2c701d6931..d112a2923da4 100644 --- a/src/lib/purgeUnusedUtilities.js +++ b/src/lib/purgeUnusedUtilities.js @@ -50,7 +50,13 @@ export default function purgeUnusedUtilities(config) { }, purgecss({ content: Array.isArray(config.purge) ? config.purge : config.purge.paths, - defaultExtractor: content => content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [], + defaultExtractor: content => { + return ( + content + .match(/[^<>"'`\s]*[^<>"'`\s:]/g) + .concat(content.match(/[^<>"'`\s.]*[^<>"'`\s:.]/g)) || [] + ) + }, }), ]) } From 64b6c955c7c3e2502a1cfce3f6fcc7ac96a6b854 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Tue, 28 Apr 2020 09:51:27 -0400 Subject: [PATCH 4/4] Improve PurgeCSS integration - Add better Pug support - Add "modes", with "all" and "conservative" by default - Allow passing options through to PurgeCSS - Rename `paths` to `content` to match PurgeCSS --- __tests__/fixtures/purge-example.html | 2 +- __tests__/fixtures/tailwind-output-purged.css | 648 ------------------ __tests__/purgeUnusedStyles.test.js | 274 ++++++++ __tests__/sanity.test.js | 104 --- scripts/rebuildFixtures.js | 23 - src/lib/purgeUnusedStyles.js | 73 ++ src/lib/purgeUnusedUtilities.js | 62 -- src/processTailwindFeatures.js | 4 +- 8 files changed, 350 insertions(+), 840 deletions(-) delete mode 100644 __tests__/fixtures/tailwind-output-purged.css create mode 100644 __tests__/purgeUnusedStyles.test.js create mode 100644 src/lib/purgeUnusedStyles.js delete mode 100644 src/lib/purgeUnusedUtilities.js diff --git a/__tests__/fixtures/purge-example.html b/__tests__/fixtures/purge-example.html index 722f5f8ef9d9..0a4a8e96fbf8 100644 --- a/__tests__/fixtures/purge-example.html +++ b/__tests__/fixtures/purge-example.html @@ -15,7 +15,7 @@
-span.inline-grid.grid-cols-3 +span.inline-grid.grid-cols-3(class="px-1.5") .col-span-2 Hello .col-span-1.text-center diff --git a/__tests__/fixtures/tailwind-output-purged.css b/__tests__/fixtures/tailwind-output-purged.css deleted file mode 100644 index 895c3629b84f..000000000000 --- a/__tests__/fixtures/tailwind-output-purged.css +++ /dev/null @@ -1,648 +0,0 @@ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ - -/* Document - ========================================================================== */ - -/** - * 1. Correct the line height in all browsers. - * 2. Prevent adjustments of font size after orientation changes in iOS. - */ - -html { - line-height: 1.15; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/* Sections - ========================================================================== */ - -/** - * Remove the margin in all browsers. - */ - -body { - margin: 0; -} - -/** - * Render the `main` element consistently in IE. - */ - -main { - display: block; -} - -/** - * Correct the font size and margin on `h1` elements within `section` and - * `article` contexts in Chrome, Firefox, and Safari. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/* Grouping content - ========================================================================== */ - -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ - -hr { - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -pre { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Remove the gray background on active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * 1. Remove the bottom border in Chrome 57- - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ - -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - text-decoration: underline dotted; /* 2 */ -} - -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ - -b, -strong { - font-weight: bolder; -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -code, -kbd, -samp { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/** - * Add the correct font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` elements from affecting the line height in - * all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove the border on images inside links in IE 10. - */ - -img { - border-style: none; -} - -/* Forms - ========================================================================== */ - -/** - * 1. Change the font styles in all browsers. - * 2. Remove the margin in Firefox and Safari. - */ - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; /* 1 */ - font-size: 100%; /* 1 */ - line-height: 1.15; /* 1 */ - margin: 0; /* 2 */ -} - -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ - -button, -input { /* 1 */ - overflow: visible; -} - -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ - -button, -select { /* 1 */ - text-transform: none; -} - -/** - * Correct the inability to style clickable types in iOS and Safari. - */ - -button, -[type="button"], -[type="reset"], -[type="submit"] { - -webkit-appearance: button; -} - -/** - * Remove the inner border and padding in Firefox. - */ - -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner { - border-style: none; - padding: 0; -} - -/** - * Restore the focus styles unset by the previous rule. - */ - -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -/** - * Correct the padding in Firefox. - */ - -fieldset { - padding: 0.35em 0.75em 0.625em; -} - -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. - */ - -legend { - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} - -/** - * Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ - -progress { - vertical-align: baseline; -} - -/** - * Remove the default vertical scrollbar in IE 10+. - */ - -textarea { - overflow: auto; -} - -/** - * 1. Add the correct box sizing in IE 10. - * 2. Remove the padding in IE 10. - */ - -[type="checkbox"], -[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ - -[type="number"]::-webkit-inner-spin-button, -[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ - -[type="search"] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/** - * Remove the inner padding in Chrome and Safari on macOS. - */ - -[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. - */ - -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} - -/* Interactive - ========================================================================== */ - -/* - * Add the correct display in Edge, IE 10+, and Firefox. - */ - -details { - display: block; -} - -/* - * Add the correct display in all browsers. - */ - -summary { - display: list-item; -} - -/* Misc - ========================================================================== */ - -/** - * Add the correct display in IE 10+. - */ - -template { - display: none; -} - -/** - * Add the correct display in IE 10. - */ - -[hidden] { - display: none; -} - -/** - * Manually forked from SUIT CSS Base: https://github.com/suitcss/base - * A thin layer on top of normalize.css that provides a starting point more - * suitable for web applications. - */ - -/** - * Removes the default spacing and border for appropriate elements. - */ - -blockquote, -dl, -dd, -h1, -h2, -h3, -h4, -h5, -h6, -hr, -figure, -p, -pre { - margin: 0; -} - -button { - background-color: transparent; - background-image: none; - padding: 0; -} - -/** - * Work around a Firefox/IE bug where the transparent `button` background - * results in a loss of the default `button` focus styles. - */ - -button:focus { - outline: 1px dotted; - outline: 5px auto -webkit-focus-ring-color; -} - -fieldset { - margin: 0; - padding: 0; -} - -ol, -ul { - list-style: none; - margin: 0; - padding: 0; -} - -/** - * Tailwind custom reset styles - */ - -/** - * 1. Use the user's configured `sans` font-family (with Tailwind's default - * sans-serif font stack as a fallback) as a sane default. - * 2. Use Tailwind's default "normal" line-height so the user isn't forced - * to override it to ensure consistency even when using the default theme. - */ - -html { - font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 1 */ - line-height: 1.5; /* 2 */ -} - -/** - * 1. Prevent padding and border from affecting element width. - * - * We used to set this in the html element and inherit from - * the parent element for everything else. This caused issues - * in shadow-dom-enhanced elements like
where the content - * is wrapped by a div with box-sizing set to `content-box`. - * - * https://github.com/mozdevs/cssremedy/issues/4 - * - * - * 2. Allow adding a border to an element by just adding a border-width. - * - * By default, the way the browser specifies that an element should have no - * border is by setting it's border-style to `none` in the user-agent - * stylesheet. - * - * In order to easily add borders to elements by just setting the `border-width` - * property, we change the default border-style for all elements to `solid`, and - * use border-width to hide them instead. This way our `border` utilities only - * need to set the `border-width` property instead of the entire `border` - * shorthand, making our border utilities much more straightforward to compose. - * - * https://github.com/tailwindcss/tailwindcss/pull/116 - */ - -*, -::before, -::after { - box-sizing: border-box; /* 1 */ - border-width: 0; /* 2 */ - border-style: solid; /* 2 */ - border-color: #e2e8f0; /* 2 */ -} - -/* - * Ensure horizontal rules are visible by default - */ - -hr { - border-top-width: 1px; -} - -/** - * Undo the `border-style: none` reset that Normalize applies to images so that - * our `border-{width}` utilities have the expected effect. - * - * The Normalize reset is unnecessary for us since we default the border-width - * to 0 on all elements. - * - * https://github.com/tailwindcss/tailwindcss/issues/362 - */ - -img { - border-style: solid; -} - -textarea { - resize: vertical; -} - -input::placeholder, -textarea::placeholder { - color: #a0aec0; -} - -button, -[role="button"] { - cursor: pointer; -} - -table { - border-collapse: collapse; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: inherit; - font-weight: inherit; -} - -/** - * Reset links to optimize for opt-in styling instead of - * opt-out. - */ - -a { - color: inherit; - text-decoration: inherit; -} - -/** - * Reset form element properties that are easy to forget to - * style explicitly so you don't inadvertently introduce - * styles that deviate from your design system. These styles - * supplement a partial reset that is already applied by - * normalize.css. - */ - -button, -input, -optgroup, -select, -textarea { - padding: 0; - line-height: inherit; - color: inherit; -} - -/** - * Use the configured 'mono' font family for elements that - * are expected to be rendered with a monospace font, falling - * back to the system monospace stack if there is no configured - * 'mono' font family. - */ - -pre, -code, -kbd, -samp { - font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; -} - -/** - * Make replaced elements `display: block` by default as that's - * the behavior you want almost all of the time. Inspired by - * CSS Remedy, with `svg` added as well. - * - * https://github.com/mozdevs/cssremedy/issues/14 - */ - -img, -svg, -video, -canvas, -audio, -iframe, -embed, -object { - display: block; - vertical-align: middle; -} - -/** - * Constrain images and videos to the parent width and preserve - * their instrinsic aspect ratio. - * - * https://github.com/mozdevs/cssremedy/issues/14 - */ - -img, -video { - max-width: 100%; - height: auto; -} - -.bg-red-500 { - --bg-opacity: 1; - background-color: #f56565; - background-color: rgba(245, 101, 101, var(--bg-opacity)); -} - -.bg-black\! { - --bg-opacity: 1; - background-color: #000; - background-color: rgba(0, 0, 0, var(--bg-opacity)); -} - -.block { - display: block; -} - -.inline-grid { - display: inline-grid; -} - -.font-\%\#\$\@ { - font-family: Comic Sans; -} - -.h-screen { - height: 100vh; -} - -.text-center { - text-align: center; -} - -.w-\(1\/2\+8\) { - width: calc(50% + 2rem); -} - -.w-1\/2 { - width: 50%; -} - -.grid-cols-3 { - grid-template-columns: repeat(3, minmax(0, 1fr)); -} - -.col-span-1 { - grid-column: span 1 / span 1; -} - -.col-span-2 { - grid-column: span 2 / span 2; -} - -.example { - font-weight: 700; - color: #f56565; -} - -@media (min-width: 768px) { - .md\:bg-blue-300 { - --bg-opacity: 1; - background-color: #90cdf4; - background-color: rgba(144, 205, 244, var(--bg-opacity)); - } - - .md\:flow-root { - display: flow-root; - } -} diff --git a/__tests__/purgeUnusedStyles.test.js b/__tests__/purgeUnusedStyles.test.js new file mode 100644 index 000000000000..68d71d317b7c --- /dev/null +++ b/__tests__/purgeUnusedStyles.test.js @@ -0,0 +1,274 @@ +import fs from 'fs' +import path from 'path' +import postcss from 'postcss' +import tailwind from '../src/index' +import defaultConfig from '../stubs/defaultConfig.stub.js' + +const config = { + ...defaultConfig, + theme: { + extend: { + colors: { + 'black!': '#000', + }, + spacing: { + '1.5': '0.375rem', + '(1/2+8)': 'calc(50% + 2rem)', + }, + minHeight: { + '(screen-4)': 'calc(100vh - 1rem)', + }, + fontFamily: { + '%#$@': 'Comic Sans', + }, + }, + }, +} + +test('purges unused classes', () => { + const OLD_NODE_ENV = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`) + const input = fs.readFileSync(inputPath, 'utf8') + + return postcss([ + tailwind({ + ...config, + purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)], + }), + ]) + .process(input, { from: inputPath }) + .then(result => { + process.env.NODE_ENV = OLD_NODE_ENV + + expect(result.css).not.toContain('.bg-red-600') + expect(result.css).not.toContain('.w-1\\/3') + expect(result.css).not.toContain('.flex') + expect(result.css).not.toContain('.font-sans') + expect(result.css).not.toContain('.text-right') + expect(result.css).not.toContain('.px-4') + expect(result.css).not.toContain('.h-full') + + expect(result.css).toContain('.bg-red-500') + expect(result.css).toContain('.md\\:bg-blue-300') + expect(result.css).toContain('.w-1\\/2') + expect(result.css).toContain('.block') + expect(result.css).toContain('.md\\:flow-root') + expect(result.css).toContain('.h-screen') + expect(result.css).toContain('.min-h-\\(screen-4\\)') + expect(result.css).toContain('.bg-black\\!') + expect(result.css).toContain('.font-\\%\\#\\$\\@') + expect(result.css).toContain('.w-\\(1\\/2\\+8\\)') + expect(result.css).toContain('.inline-grid') + expect(result.css).toContain('.grid-cols-3') + expect(result.css).toContain('.px-1\\.5') + expect(result.css).toContain('.col-span-2') + expect(result.css).toContain('.col-span-1') + expect(result.css).toContain('.text-center') + }) +}) + +test('does not purge except in production', () => { + const OLD_NODE_ENV = process.env.NODE_ENV + process.env.NODE_ENV = 'development' + const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`) + const input = fs.readFileSync(inputPath, 'utf8') + + return postcss([ + tailwind({ + ...defaultConfig, + purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)], + }), + ]) + .process(input, { from: inputPath }) + .then(result => { + process.env.NODE_ENV = OLD_NODE_ENV + const expected = fs.readFileSync( + path.resolve(`${__dirname}/fixtures/tailwind-output.css`), + 'utf8' + ) + + expect(result.css).toBe(expected) + }) +}) + +test('purges outside of production if explicitly enabled', () => { + const OLD_NODE_ENV = process.env.NODE_ENV + process.env.NODE_ENV = 'development' + const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`) + const input = fs.readFileSync(inputPath, 'utf8') + + return postcss([ + tailwind({ + ...config, + purge: { enabled: true, content: [path.resolve(`${__dirname}/fixtures/**/*.html`)] }, + }), + ]) + .process(input, { from: inputPath }) + .then(result => { + process.env.NODE_ENV = OLD_NODE_ENV + + expect(result.css).not.toContain('.bg-red-600') + expect(result.css).not.toContain('.w-1\\/3') + expect(result.css).not.toContain('.flex') + expect(result.css).not.toContain('.font-sans') + expect(result.css).not.toContain('.text-right') + expect(result.css).not.toContain('.px-4') + expect(result.css).not.toContain('.h-full') + + expect(result.css).toContain('.bg-red-500') + expect(result.css).toContain('.md\\:bg-blue-300') + expect(result.css).toContain('.w-1\\/2') + expect(result.css).toContain('.block') + expect(result.css).toContain('.md\\:flow-root') + expect(result.css).toContain('.h-screen') + expect(result.css).toContain('.min-h-\\(screen-4\\)') + expect(result.css).toContain('.bg-black\\!') + expect(result.css).toContain('.font-\\%\\#\\$\\@') + expect(result.css).toContain('.w-\\(1\\/2\\+8\\)') + expect(result.css).toContain('.inline-grid') + expect(result.css).toContain('.grid-cols-3') + expect(result.css).toContain('.px-1\\.5') + expect(result.css).toContain('.col-span-2') + expect(result.css).toContain('.col-span-1') + expect(result.css).toContain('.text-center') + }) +}) + +test('purgecss options can be provided', () => { + const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`) + const input = fs.readFileSync(inputPath, 'utf8') + + return postcss([ + tailwind({ + ...config, + purge: { + enabled: true, + options: { + content: [path.resolve(`${__dirname}/fixtures/**/*.html`)], + whitelist: ['md:bg-green-500'], + }, + }, + }), + ]) + .process(input, { from: inputPath }) + .then(result => { + expect(result.css).not.toContain('.bg-red-600') + expect(result.css).not.toContain('.w-1\\/3') + expect(result.css).not.toContain('.flex') + expect(result.css).not.toContain('.font-sans') + expect(result.css).not.toContain('.text-right') + expect(result.css).not.toContain('.px-4') + expect(result.css).not.toContain('.h-full') + + expect(result.css).toContain('.md\\:bg-green-500') + expect(result.css).toContain('.bg-red-500') + expect(result.css).toContain('.md\\:bg-blue-300') + expect(result.css).toContain('.w-1\\/2') + expect(result.css).toContain('.block') + expect(result.css).toContain('.md\\:flow-root') + expect(result.css).toContain('.h-screen') + expect(result.css).toContain('.min-h-\\(screen-4\\)') + expect(result.css).toContain('.bg-black\\!') + expect(result.css).toContain('.font-\\%\\#\\$\\@') + expect(result.css).toContain('.w-\\(1\\/2\\+8\\)') + expect(result.css).toContain('.inline-grid') + expect(result.css).toContain('.grid-cols-3') + expect(result.css).toContain('.px-1\\.5') + expect(result.css).toContain('.col-span-2') + expect(result.css).toContain('.col-span-1') + expect(result.css).toContain('.text-center') + }) +}) + +test('can purge all CSS, not just Tailwind classes', () => { + const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`) + const input = fs.readFileSync(inputPath, 'utf8') + + return postcss([ + tailwind({ + ...config, + purge: { + enabled: true, + mode: 'all', + content: [path.resolve(`${__dirname}/fixtures/**/*.html`)], + }, + }), + function(css) { + // Remove any comments to avoid accidentally asserting against them + // instead of against real CSS rules. + css.walkComments(c => c.remove()) + }, + ]) + .process(input, { from: inputPath }) + .then(result => { + expect(result.css).not.toContain('html') + expect(result.css).not.toContain('body') + expect(result.css).not.toContain('button') + expect(result.css).not.toContain('legend') + expect(result.css).not.toContain('progress') + + expect(result.css).toContain('.bg-red-500') + expect(result.css).toContain('.md\\:bg-blue-300') + expect(result.css).toContain('.w-1\\/2') + expect(result.css).toContain('.block') + expect(result.css).toContain('.md\\:flow-root') + expect(result.css).toContain('.h-screen') + expect(result.css).toContain('.min-h-\\(screen-4\\)') + expect(result.css).toContain('.bg-black\\!') + expect(result.css).toContain('.font-\\%\\#\\$\\@') + expect(result.css).toContain('.w-\\(1\\/2\\+8\\)') + expect(result.css).toContain('.inline-grid') + expect(result.css).toContain('.grid-cols-3') + expect(result.css).toContain('.px-1\\.5') + expect(result.css).toContain('.col-span-2') + expect(result.css).toContain('.col-span-1') + expect(result.css).toContain('.text-center') + }) +}) + +test('the `conservative` mode can be set explicitly', () => { + const OLD_NODE_ENV = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`) + const input = fs.readFileSync(inputPath, 'utf8') + + return postcss([ + tailwind({ + ...config, + purge: { + mode: 'conservative', + content: [path.resolve(`${__dirname}/fixtures/**/*.html`)], + }, + }), + ]) + .process(input, { from: inputPath }) + .then(result => { + process.env.NODE_ENV = OLD_NODE_ENV + + expect(result.css).not.toContain('.bg-red-600') + expect(result.css).not.toContain('.w-1\\/3') + expect(result.css).not.toContain('.flex') + expect(result.css).not.toContain('.font-sans') + expect(result.css).not.toContain('.text-right') + expect(result.css).not.toContain('.px-4') + expect(result.css).not.toContain('.h-full') + + expect(result.css).toContain('.bg-red-500') + expect(result.css).toContain('.md\\:bg-blue-300') + expect(result.css).toContain('.w-1\\/2') + expect(result.css).toContain('.block') + expect(result.css).toContain('.md\\:flow-root') + expect(result.css).toContain('.h-screen') + expect(result.css).toContain('.min-h-\\(screen-4\\)') + expect(result.css).toContain('.bg-black\\!') + expect(result.css).toContain('.font-\\%\\#\\$\\@') + expect(result.css).toContain('.w-\\(1\\/2\\+8\\)') + expect(result.css).toContain('.inline-grid') + expect(result.css).toContain('.grid-cols-3') + expect(result.css).toContain('.px-1\\.5') + expect(result.css).toContain('.col-span-2') + expect(result.css).toContain('.col-span-1') + expect(result.css).toContain('.text-center') + }) +}) diff --git a/__tests__/sanity.test.js b/__tests__/sanity.test.js index dd12478b6c5b..bde1251f3d42 100644 --- a/__tests__/sanity.test.js +++ b/__tests__/sanity.test.js @@ -93,107 +93,3 @@ it('generates the right CSS when "important" is enabled', () => { expect(result.css).toBe(expected) }) }) - -it('purges unused classes', () => { - const OLD_NODE_ENV = process.env.NODE_ENV - process.env.NODE_ENV = 'production' - const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`) - const input = fs.readFileSync(inputPath, 'utf8') - - return postcss([ - tailwind({ - ...config, - theme: { - extend: { - colors: { - 'black!': '#000', - }, - spacing: { - '(1/2+8)': 'calc(50% + 2rem)', - }, - minHeight: { - '(screen-100)': 'calc(100vh - 1rem)', - }, - fontFamily: { - '%#$@': 'Comic Sans', - }, - }, - }, - purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)], - }), - ]) - .process(input, { from: inputPath }) - .then(result => { - process.env.NODE_ENV = OLD_NODE_ENV - const expected = fs.readFileSync( - path.resolve(`${__dirname}/fixtures/tailwind-output-purged.css`), - 'utf8' - ) - - expect(result.css).toBe(expected) - }) -}) - -it('does not purge except in production', () => { - const OLD_NODE_ENV = process.env.NODE_ENV - process.env.NODE_ENV = 'development' - const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`) - const input = fs.readFileSync(inputPath, 'utf8') - - return postcss([ - tailwind({ - ...config, - purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)], - }), - ]) - .process(input, { from: inputPath }) - .then(result => { - process.env.NODE_ENV = OLD_NODE_ENV - const expected = fs.readFileSync( - path.resolve(`${__dirname}/fixtures/tailwind-output.css`), - 'utf8' - ) - - expect(result.css).toBe(expected) - }) -}) - -it('purges outside of production if explicitly enabled', () => { - const OLD_NODE_ENV = process.env.NODE_ENV - process.env.NODE_ENV = 'development' - const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`) - const input = fs.readFileSync(inputPath, 'utf8') - - return postcss([ - tailwind({ - ...config, - theme: { - extend: { - colors: { - 'black!': '#000', - }, - spacing: { - '(1/2+8)': 'calc(50% + 2rem)', - }, - minHeight: { - '(screen-100)': 'calc(100vh - 1rem)', - }, - fontFamily: { - '%#$@': 'Comic Sans', - }, - }, - }, - purge: { enabled: true, paths: [path.resolve(`${__dirname}/fixtures/**/*.html`)] }, - }), - ]) - .process(input, { from: inputPath }) - .then(result => { - process.env.NODE_ENV = OLD_NODE_ENV - const expected = fs.readFileSync( - path.resolve(`${__dirname}/fixtures/tailwind-output-purged.css`), - 'utf8' - ) - - expect(result.css).toBe(expected) - }) -}) diff --git a/scripts/rebuildFixtures.js b/scripts/rebuildFixtures.js index 071c3664d23b..707b9920b9be 100644 --- a/scripts/rebuildFixtures.js +++ b/scripts/rebuildFixtures.js @@ -39,29 +39,6 @@ Promise.all([ to: '__tests__/fixtures/tailwind-output-important.css', config: { important: true }, }), - build({ - from: '__tests__/fixtures/tailwind-input.css', - to: '__tests__/fixtures/tailwind-output-purged.css', - config: { - theme: { - extend: { - colors: { - 'black!': '#000', - }, - spacing: { - '(1/2+8)': 'calc(50% + 2rem)', - }, - minHeight: { - '(screen-100)': 'calc(100vh - 1rem)', - }, - fontFamily: { - '%#$@': 'Comic Sans', - }, - }, - }, - purge: { enabled: true, paths: ['./__tests__/fixtures/**/*.html'] }, - }, - }), ]).then(() => { console.log('\nFinished rebuilding fixtures.') console.log( diff --git a/src/lib/purgeUnusedStyles.js b/src/lib/purgeUnusedStyles.js new file mode 100644 index 000000000000..feab571299e8 --- /dev/null +++ b/src/lib/purgeUnusedStyles.js @@ -0,0 +1,73 @@ +import _ from 'lodash' +import postcss from 'postcss' +import purgecss from '@fullhuman/postcss-purgecss' + +function removeTailwindComments(css) { + css.walkComments(comment => { + switch (comment.text.trim()) { + case 'tailwind start components': + case 'tailwind start utilities': + case 'tailwind start screens': + case 'tailwind end components': + case 'tailwind end utilities': + case 'tailwind end screens': + comment.remove() + break + default: + break + } + }) +} + +export default function purgeUnusedUtilities(config) { + const purgeEnabled = + _.get(config, 'purge.enabled', false) === true || + (config.purge !== undefined && process.env.NODE_ENV === 'production') + + if (!purgeEnabled) { + return removeTailwindComments + } + + return postcss([ + function(css) { + const mode = _.get(config, 'purge.mode', 'conservative') + + if (mode === 'conservative') { + css.prepend(postcss.comment({ text: 'purgecss start ignore' })) + css.append(postcss.comment({ text: 'purgecss end ignore' })) + + css.walkComments(comment => { + switch (comment.text.trim()) { + case 'tailwind start components': + case 'tailwind start utilities': + case 'tailwind start screens': + comment.text = 'purgecss end ignore' + break + case 'tailwind end components': + case 'tailwind end utilities': + case 'tailwind end screens': + comment.text = 'purgecss start ignore' + break + default: + break + } + }) + } else if (mode === 'all') { + removeTailwindComments(css) + } + }, + purgecss({ + content: Array.isArray(config.purge) ? config.purge : config.purge.content, + defaultExtractor: content => { + // Capture as liberally as possible, including things like `h-(screen-1.5)` + const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [] + + // Capture classes within other delimiters like .block(class="w-1/2") in Pug + const innerMatches = content.match(/[^<>"'`\s.()]*[^<>"'`\s.():]/g) || [] + + return broadMatches.concat(innerMatches) + }, + ...config.purge.options, + }), + ]) +} diff --git a/src/lib/purgeUnusedUtilities.js b/src/lib/purgeUnusedUtilities.js deleted file mode 100644 index d112a2923da4..000000000000 --- a/src/lib/purgeUnusedUtilities.js +++ /dev/null @@ -1,62 +0,0 @@ -import _ from 'lodash' -import postcss from 'postcss' -import purgecss from '@fullhuman/postcss-purgecss' - -export default function purgeUnusedUtilities(config) { - const purgeEnabled = - _.get(config, 'purge.enabled', false) === true || - (config.purge !== undefined && process.env.NODE_ENV === 'production') - - if (!purgeEnabled) { - return function(css) { - css.walkComments(comment => { - switch (comment.text.trim()) { - case 'tailwind start components': - case 'tailwind start utilities': - case 'tailwind start screens': - case 'tailwind end components': - case 'tailwind end utilities': - case 'tailwind end screens': - comment.remove() - break - default: - break - } - }) - } - } - - return postcss([ - function(css) { - css.prepend(postcss.comment({ text: 'purgecss start ignore' })) - css.append(postcss.comment({ text: 'purgecss end ignore' })) - - css.walkComments(comment => { - switch (comment.text.trim()) { - case 'tailwind start components': - case 'tailwind start utilities': - case 'tailwind start screens': - comment.text = 'purgecss end ignore' - break - case 'tailwind end components': - case 'tailwind end utilities': - case 'tailwind end screens': - comment.text = 'purgecss start ignore' - break - default: - break - } - }) - }, - purgecss({ - content: Array.isArray(config.purge) ? config.purge : config.purge.paths, - defaultExtractor: content => { - return ( - content - .match(/[^<>"'`\s]*[^<>"'`\s:]/g) - .concat(content.match(/[^<>"'`\s.]*[^<>"'`\s:.]/g)) || [] - ) - }, - }), - ]) -} diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js index 2059e8c9a8b1..294a8c5d7e8d 100644 --- a/src/processTailwindFeatures.js +++ b/src/processTailwindFeatures.js @@ -7,7 +7,7 @@ import substituteVariantsAtRules from './lib/substituteVariantsAtRules' import substituteResponsiveAtRules from './lib/substituteResponsiveAtRules' import substituteScreenAtRules from './lib/substituteScreenAtRules' import substituteClassApplyAtRules from './lib/substituteClassApplyAtRules' -import purgeUnusedUtilities from './lib/purgeUnusedUtilities' +import purgeUnusedStyles from './lib/purgeUnusedStyles' import corePlugins from './corePlugins' import processPlugins from './util/processPlugins' @@ -24,7 +24,7 @@ export default function(getConfig) { substituteResponsiveAtRules(config), substituteScreenAtRules(config), substituteClassApplyAtRules(config, processedPlugins.utilities), - purgeUnusedUtilities(config), + purgeUnusedStyles(config), ]).process(css, { from: _.get(css, 'source.input.file') }) } }