mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Add blocklist support from v3 config files (#14556)
This PR adds support for the `blocklist` config option when using a JS
config file in v4. You can now block certain classes from being
generated at all. This is useful in cases where scanning files sees
things that look like classes but are actually not used. For example, in
paragraphs in a markdown file:
```js
// tailwind.config.js
export default {
blocklist: ['bg-red-500'],
}
```
```html
<!-- index.html -->
<div class="bg-red-500 text-black/75"></div>
```
Output:
```css
.text-black/75 {
color: rgba(0, 0, 0, 0.75);
}
```
This commit is contained in:
parent
ab82efab7d
commit
6a50e6e160
@ -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))
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]`,
|
||||
)
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -78,3 +78,12 @@ export interface UserConfig {
|
||||
export interface ResolvedConfig {
|
||||
prefix: string
|
||||
}
|
||||
|
||||
// `blocklist` support
|
||||
export interface UserConfig {
|
||||
blocklist?: string[]
|
||||
}
|
||||
|
||||
export interface ResolvedConfig {
|
||||
blocklist: string[]
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -13,6 +13,8 @@ export type DesignSystem = {
|
||||
utilities: Utilities
|
||||
variants: Variants
|
||||
|
||||
invalidCandidates: Set<string>
|
||||
|
||||
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))
|
||||
|
||||
@ -399,9 +399,8 @@ export async function compile(
|
||||
}
|
||||
|
||||
// Track all invalid candidates
|
||||
let invalidCandidates = new Set<string>()
|
||||
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
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
",
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user