diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 7be71d1e0bd2..6b64cb4dc0e6 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -37,7 +37,7 @@ jobs: env: CI: true - run: npm run prepublishOnly - - run: npm test + - run: npm test -- --coverage env: CI: true - run: bash <(curl -s --retry 5 --retry-delay 2 --connect-timeout 2 https://codecov.io/bash) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3d518d231e2..bbf871a4ba91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Nothing yet! + +## [2.0.2] - 2020-12-11 + +### Fixed + +- Fix issue with `@apply` not working as expected with `!important` inside an atrule ([#2824](https://github.com/tailwindlabs/tailwindcss/pull/2824)) +- Fix issue with `@apply` not working as expected with defined classes ([#2832](https://github.com/tailwindlabs/tailwindcss/pull/2832)) +- Fix memory leak, and broken `@apply` when splitting up files ([#3032](https://github.com/tailwindlabs/tailwindcss/pull/3032)) + +### Added + +- Add default values for the `ring` utility ([#2951](https://github.com/tailwindlabs/tailwindcss/pull/2951)) + +## [2.0.1] - 2020-11-18 + +- Nothing, just the only thing I could do when I found out npm won't let me publish the same version under two tags. + +## [2.0.0] - 2020-11-18 + ### Added - Add redesigned color palette ([#2623](https://github.com/tailwindlabs/tailwindcss/pull/2623), [700866c](https://github.com/tailwindlabs/tailwindcss/commit/700866ce5e0c0b8d140be161c4d07fc6f31242bc), [#2633](https://github.com/tailwindlabs/tailwindcss/pull/2633)) @@ -1295,7 +1315,10 @@ No release notes - Everything! -[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v2.0.0-alpha.25...HEAD +[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v2.0.2...HEAD +[2.0.2]: https://github.com/tailwindlabs/tailwindcss/compare/v2.0.1...v2.0.2 +[2.0.1]: https://github.com/tailwindlabs/tailwindcss/compare/v2.0.0...v2.0.1 +[2.0.0]: https://github.com/tailwindlabs/tailwindcss/compare/v1.9.6...v2.0.0 [2.0.0-alpha.25]: https://github.com/tailwindlabs/tailwindcss/compare/v2.0.0-alpha.24...v2.0.0-alpha.25 [2.0.0-alpha.24]: https://github.com/tailwindlabs/tailwindcss/compare/v2.0.0-alpha.23...v2.0.0-alpha.24 [2.0.0-alpha.23]: https://github.com/tailwindlabs/tailwindcss/compare/v2.0.0-alpha.22...v2.0.0-alpha.23 diff --git a/__tests__/applyAtRule.test.js b/__tests__/applyAtRule.test.js index e9fe54998be5..3cf53394c26d 100644 --- a/__tests__/applyAtRule.test.js +++ b/__tests__/applyAtRule.test.js @@ -337,16 +337,17 @@ test('you can apply utility classes that do not actually exist as long as they w }) }) -test('the shadow lookup is only used if no @tailwind rules were in the source tree', () => { +test('shadow lookup will be constructed when we have missing @tailwind atrules', () => { const input = ` @tailwind base; + .foo { @apply mt-4; } ` expect.assertions(1) - return run(input).catch((e) => { - expect(e).toMatchObject({ name: 'CssSyntaxError' }) + return run(input).then((result) => { + expect(result.css).toContain(`.foo { margin-top: 1rem;\n}`) }) }) @@ -558,6 +559,104 @@ test('you can apply utilities with multi-class selectors like group-hover varian }) test('you can apply classes recursively', () => { + const input = ` + .baz { + color: blue; + } + .bar { + @apply baz px-4; + } + .foo { + @apply bar; + } + ` + const expected = ` + .baz { + color: blue; + } + .bar { + padding-left: 1rem; + padding-right: 1rem; + color: blue; + } + .foo { + padding-left: 1rem; + padding-right: 1rem; + color: blue; + } + ` + + expect.assertions(2) + + return run(input).then((result) => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('you can apply complex classes recursively', () => { + const input = ` + .button { + @apply rounded-xl px-6 py-2 hover:text-white focus:border-opacity-100; + } + + .button-yellow { + @apply button bg-yellow-600 text-gray-200; + } + ` + + const expected = ` + .button:focus { + --tw-border-opacity: 1; + } + + .button { + border-radius: 0.75rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1.5rem; + padding-right: 1.5rem; + } + + .button:hover { + --tw-text-opacity: 1; + color: rgba(255, 255, 255, var(--tw-text-opacity)); + } + + .button-yellow { + --tw-bg-opacity: 1; + background-color: rgba(217, 119, 6, var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgba(229, 231, 235, var(--tw-text-opacity)); + } + + .button-yellow:focus { + --tw-border-opacity: 1; + } + + .button-yellow { + border-radius: 0.75rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1.5rem; + padding-right: 1.5rem; + } + + .button-yellow:hover { + --tw-text-opacity: 1; + color: rgba(255, 255, 255, var(--tw-text-opacity)); + } + ` + + expect.assertions(2) + + return run(input).then((result) => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('you can apply classes recursively out of order', () => { const input = ` .foo { @apply bar; @@ -911,14 +1010,10 @@ describe('using apply with the prefix option', () => { test('a "Did You Mean" suggestion is omitted if a similar class cannot be identified.', () => { const input = ` - .foo { @apply anteater; } - ` + .foo { @apply anteater; } + ` - const config = resolveConfig([ - { - ...defaultConfig, - }, - ]) + const config = resolveConfig([{ ...defaultConfig }]) expect.assertions(1) @@ -1018,7 +1113,7 @@ test('you can apply classes to a rule with multiple selectors', () => { @apply float-left opacity-50 hover:opacity-100 md:float-right; } } - ` + ` const expected = ` @supports (display: grid) { @@ -1043,6 +1138,85 @@ test('you can apply classes to a rule with multiple selectors', () => { }) }) +test('you can apply classes to a rule with multiple selectors with important and a prefix enabled', () => { + const input = ` + @supports (display: grid) { + .foo, h1 > .bar * { + @apply tw-float-left tw-opacity-50 hover:tw-opacity-100 md:tw-float-right; + } + } + ` + + const expected = ` + @supports (display: grid) { + .foo, h1 > .bar * { + float: left; + opacity: 0.5; + } + + .foo:hover, h1 > .bar *:hover { + opacity: 1; + } + + @media (min-width: 768px) { + .foo, h1 > .bar * { + float: right; + } + } + } + ` + + const config = resolveConfig([ + { + ...defaultConfig, + prefix: 'tw-', + important: true, + }, + ]) + + return run(input, config).then((result) => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + +test('you can apply classes to multiple selectors at the same time, removing important', () => { + const input = ` + .multiple p, + .multiple ul, + .multiple ol { + @apply mt-5; + } + + .multiple h2, + .multiple h3, + .multiple h4 { + @apply mt-8; + } + ` + + const expected = ` + .multiple p, + .multiple ul, + .multiple ol { + margin-top: 1.25rem; + } + + .multiple h2, + .multiple h3, + .multiple h4 { + margin-top: 2rem; + } + ` + + const config = resolveConfig([{ ...defaultConfig, important: true }]) + + return run(input, config).then((result) => { + expect(result.css).toMatchCss(expected) + expect(result.warnings().length).toBe(0) + }) +}) + test('you can apply classes in a nested rule', () => { const input = ` .selector { @@ -1150,11 +1324,11 @@ test('declarations within a rule that uses @apply can be !important', () => { ` const expected = ` - .foo { - text-align: center; - float: left; - display: block !important; - } + .foo { + text-align: center; + float: left; + display: block !important; + } ` expect.assertions(2) @@ -1175,11 +1349,11 @@ test('declarations within a rule that uses @apply with !important remain not !im ` const expected = ` - .foo { - text-align: center !important; - float: left; - display: block !important; - } + .foo { + text-align: center !important; + float: left; + display: block !important; + } ` expect.assertions(2) @@ -1189,3 +1363,40 @@ test('declarations within a rule that uses @apply with !important remain not !im expect(result.warnings().length).toBe(0) }) }) + +test('lookup tree is correctly cached based on used tailwind atrules', async () => { + const input1 = ` + @tailwind utilities; + + .foo { @apply mt-4; } + ` + + const input2 = ` + @tailwind components; + + .foo { @apply mt-4; } + ` + + let config = { + corePlugins: [], + plugins: [ + function ({ addUtilities, addComponents }) { + addUtilities({ '.mt-4': { marginTop: '1rem' } }, []) + addComponents({ '.container': { maxWidth: '500px' } }, []) + }, + ], + } + + let output1 = await run(input1, config) + let output2 = await run(input2, config) + + expect(output1.css).toMatchCss(` + .mt-4 { margin-top: 1rem; } + .foo { margin-top: 1rem; } + `) + + expect(output2.css).toMatchCss(` + .container { max-width: 500px; } + .foo { margin-top: 1rem; } + `) +}) diff --git a/__tests__/plugins/ringWidth.test.js b/__tests__/plugins/ringWidth.test.js new file mode 100644 index 000000000000..6f929229fbfb --- /dev/null +++ b/__tests__/plugins/ringWidth.test.js @@ -0,0 +1,104 @@ +import invokePlugin from '../util/invokePlugin' +import plugin from '../../src/plugins/ringWidth' + +test('ring widths', () => { + const config = { + theme: { + ringWidth: { + 4: '4px', + }, + ringOffsetWidth: { + 4: '4px', + }, + ringColor: { + black: '#000', + }, + ringOffsetColor: { + white: '#fff', + }, + ringOpacity: { + 50: '.5', + }, + }, + variants: { + ringColor: [], + }, + } + + const { utilities } = invokePlugin(plugin(), config) + expect(utilities).toEqual([ + [ + { + '*': { + '--tw-ring-color': 'rgba(147, 197, 253, 0.5)', + '--tw-ring-inset': 'var(--tw-empty,/*!*/ /*!*/)', + '--tw-ring-offset-color': '#fff', + '--tw-ring-offset-shadow': '0 0 #0000', + '--tw-ring-offset-width': '0px', + '--tw-ring-shadow': '0 0 #0000', + }, + }, + { + respectImportant: false, + }, + ], + [ + { + '.ring-4': { + '--tw-ring-offset-shadow': + 'var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)', + '--tw-ring-shadow': + 'var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color)', + 'box-shadow': + 'var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)', + }, + '.ring-inset': { + '--tw-ring-inset': 'inset', + }, + }, + undefined, + ], + ]) +}) + +test('ring widths with defaults', () => { + const config = { + theme: { + ringWidth: {}, + ringOffsetWidth: { + DEFAULT: '2px', + }, + ringOffsetColor: { + DEFAULT: 'pink', + }, + }, + variants: { + ringColor: [], + }, + } + + const { utilities } = invokePlugin(plugin(), config) + expect(utilities).toEqual([ + [ + { + '*': { + '--tw-ring-color': 'rgba(147, 197, 253, 0.5)', + '--tw-ring-inset': 'var(--tw-empty,/*!*/ /*!*/)', + '--tw-ring-offset-color': 'pink', + '--tw-ring-offset-shadow': '0 0 #0000', + '--tw-ring-offset-width': '2px', + '--tw-ring-shadow': '0 0 #0000', + }, + }, + { respectImportant: false }, + ], + [ + { + '.ring-inset': { + '--tw-ring-inset': 'inset', + }, + }, + undefined, + ], + ]) +}) diff --git a/jest/customMatchers.js b/jest/customMatchers.js index 32fbd588674f..e5e5b4348566 100644 --- a/jest/customMatchers.js +++ b/jest/customMatchers.js @@ -2,7 +2,6 @@ import prettier from 'prettier' import diff from 'jest-diff' function format(input) { - return input return prettier.format(input, { parser: 'css', printWidth: 100, diff --git a/package.json b/package.json index bfd5f3b4a691..8e4e6d4c63f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tailwindcss", - "version": "2.0.1", + "version": "2.0.2", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "main": "lib/index.js", @@ -23,7 +23,8 @@ "rebuild-fixtures": "npm run babelify && babel-node scripts/rebuildFixtures.js", "prepublishOnly": "npm run babelify && babel-node scripts/build.js", "style": "eslint .", - "test": "jest && eslint .", + "test": "jest", + "posttest": "npm run style", "compat": "node scripts/compat.js --prepare", "compat:restore": "node scripts/compat.js --restore" }, @@ -37,18 +38,18 @@ ], "devDependencies": { "@babel/cli": "^7.11.6", - "@babel/core": "^7.12.3", + "@babel/core": "^7.12.9", "@babel/node": "^7.0.0", "@babel/preset-env": "^7.0.0", "autoprefixer": "^10.0.2", "babel-jest": "^26.6.3", "clean-css": "^4.1.9", - "eslint": "^7.12.1", + "eslint": "^7.14.0", "eslint-config-prettier": "^6.15.0", - "eslint-plugin-prettier": "^3.1.4", + "eslint-plugin-prettier": "^3.2.0", "jest": "^26.6.3", - "postcss": "^8.0.9", - "prettier": "^2.1.2", + "postcss": "^8.1.10", + "prettier": "^2.2.0", "rimraf": "^3.0.0" }, "peerDependencies": { @@ -96,7 +97,6 @@ ] }, "jest": { - "collectCoverage": true, "testTimeout": 30000, "setupFilesAfterEnv": [ "/jest/customMatchers.js" diff --git a/perf/tailwind.config.js b/perf/tailwind.config.js index 7fb2125aae9e..5211607402b6 100644 --- a/perf/tailwind.config.js +++ b/perf/tailwind.config.js @@ -1,14 +1,12 @@ +let colors = require('../colors') module.exports = { - future: 'all', - experimental: 'all', purge: [], + darkMode: 'class', theme: { - extend: {}, + extend: { colors }, }, variants: [ 'responsive', - 'motion-safe', - 'motion-reduce', 'group-hover', 'group-focus', 'hover', @@ -19,10 +17,6 @@ module.exports = { 'visited', 'disabled', 'checked', - 'first', - 'last', - 'odd', - 'even', ], plugins: [], } diff --git a/scripts/compat.js b/scripts/compat.js index 4a8e8dfb4202..b4fef57894ba 100644 --- a/scripts/compat.js +++ b/scripts/compat.js @@ -37,10 +37,16 @@ if (process.argv.includes('--prepare')) { // 5. Remove peerDependencies delete packageJson.peerDependencies - // 6. Write package.json with the new contents + // 6. Use new name + packageJson.name = '@tailwindcss/postcss7-compat' + + // 7. Make sure you can publish + packageJson.publishConfig = { access: 'public' } + + // 8. Write package.json with the new contents fs.writeFileSync(fromRootPath('package.json'), JSON.stringify(packageJson, null, 2), 'utf8') - // 7. Print some useful information to make publishing easy + // 9. Print some useful information to make publishing easy console.log() console.log('You can safely publish `tailwindcss` in PostCSS 7 compatibility mode:\n') console.log( diff --git a/src/lib/substituteClassApplyAtRules.js b/src/lib/substituteClassApplyAtRules.js index 9763118c4fa7..90a3e4544206 100644 --- a/src/lib/substituteClassApplyAtRules.js +++ b/src/lib/substituteClassApplyAtRules.js @@ -11,15 +11,25 @@ import substituteScreenAtRules from './substituteScreenAtRules' import prefixSelector from '../util/prefixSelector' import { useMemo } from '../util/useMemo' -function hasAtRule(css, atRule) { - let foundAtRule = false - - css.walkAtRules(atRule, () => { - foundAtRule = true - return false - }) +function hasAtRule(css, atRule, condition) { + let found = false + + css.walkAtRules( + atRule, + condition === undefined + ? () => { + found = true + return false + } + : (node) => { + if (condition(node)) { + found = true + return false + } + } + ) - return foundAtRule + return found } function cloneWithoutChildren(node) { @@ -96,7 +106,7 @@ function buildUtilityMap(css, lookupTree) { let index = 0 const utilityMap = {} - function handle(rule) { + function handle(getRule, rule) { const utilityNames = extractUtilityNames(rule.selector) utilityNames.forEach((utilityName, i) => { @@ -108,16 +118,29 @@ function buildUtilityMap(css, lookupTree) { index, utilityName, classPosition: i, - get rule() { - return cloneRuleWithParent(rule) - }, + ...getRule(rule), }) index++ }) } - lookupTree.walkRules(handle) - css.walkRules(handle) + // Lookup tree is the big lookup tree, making the rule lazy allows us to save + // some memory because we don't need everything. + lookupTree.walkRules( + handle.bind(null, (rule) => ({ + get rule() { + return cloneRuleWithParent(rule) + }, + })) + ) + + // This is the end user's css. This might contain rules that we want to + // apply. We want immediate copies of everything in case that we have user + // defined classes that are recursively applied. Down below we are modifying + // the rules directly. We could do a better solution where we keep track of a + // dependency tree, but that is a bit more complex. Might revisit later, + // we'll see how this turns out! + css.walkRules(handle.bind(null, (rule) => ({ rule: cloneRuleWithParent(rule) }))) return utilityMap } @@ -242,11 +265,21 @@ function processApplyAtRules(css, lookupTree, config) { : (util) => util.rule.nodes.forEach((n) => afterRule.append(n.clone())) ) - const { nodes } = _.tap(postcss.root({ nodes: rulesToInsert }), (root) => + rulesToInsert.forEach((rule) => { + if (rule.type === 'atrule') { + rule.walkRules((rule) => { + rule.__tailwind = { ...rule.__tailwind, important } + }) + } else { + rule.__tailwind = { ...rule.__tailwind, important } + } + }) + + const { nodes } = _.tap(postcss.root({ nodes: rulesToInsert }), (root) => { root.walkDecls((d) => { d.important = important }) - ) + }) const mergedRules = mergeAdjacentRules(nearestParentRule, [...nodes, afterRule]) @@ -275,7 +308,7 @@ function processApplyAtRules(css, lookupTree, config) { return css } -let defaultTailwindTree = null +let defaultTailwindTree = new Map() export default function substituteClassApplyAtRules(config, getProcessedPlugins, configChanged) { return function (css) { @@ -284,15 +317,29 @@ export default function substituteClassApplyAtRules(config, getProcessedPlugins, return css } - // Tree already contains @tailwind rules, don't prepend default Tailwind tree - if (hasAtRule(css, 'tailwind')) { + let requiredTailwindAtRules = ['base', 'components', 'utilities'] + if ( + hasAtRule(css, 'tailwind', (node) => { + let idx = requiredTailwindAtRules.indexOf(node.params) + if (idx !== -1) requiredTailwindAtRules.splice(idx, 1) + if (requiredTailwindAtRules.length <= 0) return true + return false + }) + ) { + // Tree already contains all the at rules (requiredTailwindAtRules) return processApplyAtRules(css, postcss.root(), config) } - // Tree contains no @tailwind rules, so generate all of Tailwind's styles and - // prepend them to the user's CSS. Important for