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:
Jordan Pittman 2024-09-30 15:12:03 -04:00 committed by GitHub
parent ab82efab7d
commit 6a50e6e160
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 129 additions and 5 deletions

View File

@ -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))

View File

@ -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

View File

@ -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]`,
)
})

View File

@ -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

View File

@ -78,3 +78,12 @@ export interface UserConfig {
export interface ResolvedConfig {
prefix: string
}
// `blocklist` support
export interface UserConfig {
blocklist?: string[]
}
export interface ResolvedConfig {
blocklist: string[]
}

View File

@ -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)

View File

@ -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))

View File

@ -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
}

View File

@ -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;
}
",
]
`)
})