diff --git a/CHANGELOG.md b/CHANGELOG.md index e40421238cc4..2b7eab50d4f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add support for prefixes ([#14501](https://github.com/tailwindlabs/tailwindcss/pull/14501)) - Expose timing information in debug mode ([#14553](https://github.com/tailwindlabs/tailwindcss/pull/14553)) +- Add support for `blocklist` in config files ([#14556](https://github.com/tailwindlabs/tailwindcss/pull/14556)) - _Experimental_: Add template codemods for migrating `bg-gradient-*` utilities to `bg-linear-*` ([#14537](https://github.com/tailwindlabs/tailwindcss/pull/14537])) - _Experimental_: Migrate `@import "tailwindcss/tailwind.css"` to `@import "tailwindcss"` ([#14514](https://github.com/tailwindlabs/tailwindcss/pull/14514)) diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 1cf3cc8102ff..915cfea427db 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -229,6 +229,10 @@ export async function applyCompatibilityHooks({ designSystem.theme.prefix = resolvedConfig.prefix } + for (let candidate of resolvedConfig.blocklist) { + designSystem.invalidCandidates.add(candidate) + } + // Replace `resolveThemeValue` with a version that is backwards compatible // with dot-notation but also aware of any JS theme configurations registered // by plugins or JS config files. This is significantly slower than just diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index 4725a8f2dd4b..bb25a02317d5 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1371,3 +1371,72 @@ test('a prefix must be letters only', async () => { `[Error: The prefix "__" is invalid. Prefixes must be lowercase ASCII letters (a-z) only.]`, ) }) + +test('blocklisted canddiates are not generated', async () => { + let compiler = await compile( + css` + @theme reference { + --color-white: #fff; + --breakpoint-md: 48rem; + } + @tailwind utilities; + @config "./config.js"; + `, + { + async loadModule(id, base) { + return { + base, + module: { + blocklist: ['bg-white'], + }, + } + }, + }, + ) + + // bg-white will not get generated + expect(compiler.build(['bg-white'])).toEqual('') + + // underline will as will md:bg-white + expect(compiler.build(['underline', 'bg-white', 'md:bg-white'])).toMatchInlineSnapshot(` + ".underline { + text-decoration-line: underline; + } + .md\\:bg-white { + @media (width >= 48rem) { + background-color: var(--color-white, #fff); + } + } + " + `) +}) + +test('blocklisted canddiates cannot be used with `@apply`', async () => { + await expect(() => + compile( + css` + @theme reference { + --color-white: #fff; + --breakpoint-md: 48rem; + } + @tailwind utilities; + @config "./config.js"; + .foo { + @apply bg-white; + } + `, + { + async loadModule(id, base) { + return { + base, + module: { + blocklist: ['bg-white'], + }, + } + }, + }, + ), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Cannot apply unknown utility class: bg-white]`, + ) +}) diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index 0c20bf7c335c..acf89c3cf024 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -27,6 +27,7 @@ interface ResolutionContext { } let minimal: ResolvedConfig = { + blocklist: [], prefix: '', darkMode: null, theme: {}, @@ -64,6 +65,10 @@ export function resolveConfig(design: DesignSystem, files: ConfigFile[]): Resolv if ('prefix' in config && config.prefix !== undefined) { ctx.result.prefix = config.prefix ?? '' } + + if ('blocklist' in config && config.blocklist !== undefined) { + ctx.result.blocklist = config.blocklist ?? [] + } } // Merge themes diff --git a/packages/tailwindcss/src/compat/config/types.ts b/packages/tailwindcss/src/compat/config/types.ts index 4b6e072b58d2..289e1bae0cfd 100644 --- a/packages/tailwindcss/src/compat/config/types.ts +++ b/packages/tailwindcss/src/compat/config/types.ts @@ -78,3 +78,12 @@ export interface UserConfig { export interface ResolvedConfig { prefix: string } + +// `blocklist` support +export interface UserConfig { + blocklist?: string[] +} + +export interface ResolvedConfig { + blocklist: string[] +} diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 9b7b3ce468b0..c5de56c4c64a 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -22,6 +22,11 @@ export function compileCandidates( // Parse candidates and variants for (let rawCandidate of rawCandidates) { + if (designSystem.invalidCandidates.has(rawCandidate)) { + onInvalidCandidate?.(rawCandidate) + continue // Bail, invalid candidate + } + let candidates = designSystem.parseCandidate(rawCandidate) if (candidates.length === 0) { onInvalidCandidate?.(rawCandidate) diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index 0bac8102d7de..e2b07a24b117 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -13,6 +13,8 @@ export type DesignSystem = { utilities: Utilities variants: Variants + invalidCandidates: Set + getClassOrder(classes: string[]): [string, bigint | null][] getClassList(): ClassEntry[] getVariants(): VariantEntry[] @@ -45,12 +47,21 @@ export function buildDesignSystem(theme: Theme): DesignSystem { utilities, variants, + invalidCandidates: new Set(), + candidatesToCss(classes: string[]) { let result: (string | null)[] = [] for (let className of classes) { - let { astNodes } = compileCandidates([className], this) - if (astNodes.length === 0) { + let wasInvalid = false + + let { astNodes } = compileCandidates([className], this, { + onInvalidCandidate(candidate) { + wasInvalid = true + }, + }) + + if (astNodes.length === 0 || wasInvalid) { result.push(null) } else { result.push(toCss(astNodes)) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index ff74f1881aa1..82c8203a5522 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -399,9 +399,8 @@ export async function compile( } // Track all invalid candidates - let invalidCandidates = new Set() function onInvalidCandidate(candidate: string) { - invalidCandidates.add(candidate) + designSystem.invalidCandidates.add(candidate) } // Track all valid candidates, these are the incoming `rawCandidate` that @@ -419,7 +418,7 @@ export async function compile( // Add all new candidates unless we know that they are invalid. let prevSize = allValidCandidates.size for (let candidate of newRawCandidates) { - if (!invalidCandidates.has(candidate)) { + if (!designSystem.invalidCandidates.has(candidate)) { allValidCandidates.add(candidate) didChange ||= allValidCandidates.size !== prevSize } diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index 0eb4d16cc94f..f6a906c28e96 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -83,3 +83,24 @@ test('The variant `has-force` does not crash', () => { expect(has.selectors({ value: 'force' })).toMatchInlineSnapshot(`[]`) }) + +test('Can produce CSS per candidate using `candidatesToCss`', () => { + let design = loadDesignSystem() + design.invalidCandidates = new Set(['bg-[#fff]']) + + expect(design.candidatesToCss(['underline', 'i-dont-exist', 'bg-[#fff]', 'bg-[#000]'])) + .toMatchInlineSnapshot(` + [ + ".underline { + text-decoration-line: underline; + } + ", + null, + null, + ".bg-\\[\\#000\\] { + background-color: #000; + } + ", + ] + `) +})