diff --git a/CHANGELOG.md b/CHANGELOG.md index 27c08731a119..320e52d8e7e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Nothing yet! +## [3.2.4] - 2022-11-11 + +### Added + +- Add `blocklist` option to prevent generating unwanted CSS ([#9812](https://github.com/tailwindlabs/tailwindcss/pull/9812)) + +### Fixed + +- Fix watching of files on Linux when renames are involved ([#9796](https://github.com/tailwindlabs/tailwindcss/pull/9796)) +- Make sure errors are always displayed when watching for changes ([#9810](https://github.com/tailwindlabs/tailwindcss/pull/9810)) + ## [3.2.3] - 2022-11-09 ### Fixed @@ -2110,7 +2121,8 @@ No release notes - Everything! -[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v3.2.3...HEAD +[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v3.2.4...HEAD +[3.2.4]: https://github.com/tailwindlabs/tailwindcss/compare/v3.2.3...v3.2.4 [3.2.3]: https://github.com/tailwindlabs/tailwindcss/compare/v3.2.2...v3.2.3 [3.2.2]: https://github.com/tailwindlabs/tailwindcss/compare/v3.2.1...v3.2.2 [3.2.1]: https://github.com/tailwindlabs/tailwindcss/compare/v3.2.0...v3.2.1 diff --git a/package-lock.json b/package-lock.json index 2bd24e4909e4..71ae8c2e1c83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tailwindcss", - "version": "3.2.3", + "version": "3.2.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "tailwindcss", - "version": "3.2.3", + "version": "3.2.4", "license": "MIT", "dependencies": { "arg": "^5.0.2", diff --git a/package.json b/package.json index 737cdfbce574..0d36d64df136 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tailwindcss", - "version": "3.2.3", + "version": "3.2.4", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "main": "lib/index.js", diff --git a/src/cli/build/plugin.js b/src/cli/build/plugin.js index 8d21516ef3ff..6b61c723eeff 100644 --- a/src/cli/build/plugin.js +++ b/src/cli/build/plugin.js @@ -364,6 +364,23 @@ export async function createProcessor(args, cliConfigPath) { console.error() console.error('Done in', (end - start) / BigInt(1e6) + 'ms.') }) + .then( + () => {}, + (err) => { + // TODO: If an initial build fails we can't easily pick up any PostCSS dependencies + // that were collected before the error occurred + // The result is not stored on the error so we have to store it externally + // and pull the messages off of it here somehow + + // This results in a less than ideal DX because the watcher will not pick up + // changes to imported CSS if one of them caused an error during the initial build + // If you fix it and then save the main CSS file so there's no error + // The watcher will start watching the imported CSS files and will be + // resilient to future errors. + + console.error(err) + } + ) } /** diff --git a/src/cli/build/watching.js b/src/cli/build/watching.js index c5204b610e54..ffbfd1ac096a 100644 --- a/src/cli/build/watching.js +++ b/src/cli/build/watching.js @@ -97,14 +97,15 @@ export function createWatcher(args, { state, rebuild }) { * * @param {*} file * @param {(() => Promise) | null} content + * @param {boolean} skipPendingCheck * @returns {Promise} */ - function recordChangedFile(file, content = null) { + function recordChangedFile(file, content = null, skipPendingCheck = false) { file = path.resolve(file) // Applications like Vim/Neovim fire both rename and change events in succession for atomic writes // In that case rebuild has already been queued by rename, so can be skipped in change - if (pendingRebuilds.has(file)) { + if (pendingRebuilds.has(file) && !skipPendingCheck) { return Promise.resolve() } @@ -198,8 +199,10 @@ export function createWatcher(args, { state, rebuild }) { } // This will push the rebuild onto the chain + // We MUST skip the rebuild check here otherwise the rebuild will never happen on Linux + // This is because the order of events and timing is different on Linux // @ts-ignore: TypeScript isn't picking up that content is a string here - await recordChangedFile(filePath, () => content) + await recordChangedFile(filePath, () => content, true) } catch { // If reading the file fails, it's was probably a deleted temporary file // So we can ignore it and no rebuild is needed diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index a5f8d3fdfb67..7b815ffbeaa0 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -1165,7 +1165,8 @@ export function createContext(tailwindConfig, changedContent = [], root = postcs candidateRuleCache: new Map(), classCache: new Map(), applyClassCache: new Map(), - notClassCache: new Set(), + // Seed the not class cache with the blocklist (which is only strings) + notClassCache: new Set(tailwindConfig.blocklist ?? []), postCssNodeCache: new Map(), candidateRuleMap: new Map(), tailwindConfig, diff --git a/src/util/normalizeConfig.js b/src/util/normalizeConfig.js index baee48394d56..95f63a39eeee 100644 --- a/src/util/normalizeConfig.js +++ b/src/util/normalizeConfig.js @@ -150,6 +150,24 @@ export function normalizeConfig(config) { return [] })() + // Normalize the `blocklist` + config.blocklist = (() => { + let { blocklist } = config + + if (Array.isArray(blocklist)) { + if (blocklist.every((item) => typeof item === 'string')) { + return blocklist + } + + log.warn('blocklist-invalid', [ + 'The `blocklist` option must be an array of strings.', + 'https://tailwindcss.com/docs/content-configuration#discarding-classes', + ]) + } + + return [] + })() + // Normalize prefix option if (typeof config.prefix === 'function') { log.warn('prefix-function', [ diff --git a/tests/blocklist.test.js b/tests/blocklist.test.js new file mode 100644 index 000000000000..9fc319a9c908 --- /dev/null +++ b/tests/blocklist.test.js @@ -0,0 +1,116 @@ +import { run, html, css } from './util/run' + +let warn + +beforeEach(() => { + warn = jest.spyOn(require('../src/util/log').default, 'warn') +}) + +afterEach(() => warn.mockClear()) + +it('can block classes matched literally', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + blocklist: ['font', 'uppercase', 'hover:text-sm', 'bg-red-500/50', 'my-custom-class'], + } + + let input = css` + @tailwind utilities; + .my-custom-class { + color: red; + } + ` + + return run(input, config).then((result) => { + return expect(result.css).toMatchCss(css` + .font-bold { + font-weight: 700; + } + .my-custom-class { + color: red; + } + @media (min-width: 640px) { + .sm\:hover\:text-sm:hover { + font-size: 0.875rem; + line-height: 1.25rem; + } + } + `) + }) +}) + +it('can block classes inside @layer', () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + blocklist: ['my-custom-class'], + } + + let input = css` + @tailwind utilities; + @layer utilities { + .my-custom-class { + color: red; + } + } + ` + + return run(input, config).then((result) => { + return expect(result.css).toMatchCss(css` + .font-bold { + font-weight: 700; + } + `) + }) +}) + +it('blocklists do NOT support regexes', async () => { + let config = { + content: [{ raw: html`
` }], + blocklist: [/^bg-\[[^]+\]$/], + } + + let result = await run('@tailwind utilities', config) + + expect(result.css).toMatchCss(css` + .bg-\[\#f00d1e\] { + --tw-bg-opacity: 1; + background-color: rgb(240 13 30 / var(--tw-bg-opacity)); + } + .font-bold { + font-weight: 700; + } + `) + + expect(warn).toHaveBeenCalledTimes(1) + expect(warn.mock.calls.map((x) => x[0])).toEqual(['blocklist-invalid']) +}) + +it('can block classes generated by the safelist', () => { + let config = { + content: [{ raw: html`
` }], + safelist: [{ pattern: /^bg-red-(400|500)$/ }], + blocklist: ['bg-red-500'], + } + + return run('@tailwind utilities', config).then((result) => { + return expect(result.css).toMatchCss(css` + .bg-red-400 { + --tw-bg-opacity: 1; + background-color: rgb(248 113 113 / var(--tw-bg-opacity)); + } + .font-bold { + font-weight: 700; + } + `) + }) +})