Skip to content

Commit

Permalink
Add blocklist support from v3 config files (#14556)
Browse files Browse the repository at this point in the history
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);
  }
  ```
  • Loading branch information
thecrypticace authored Sep 30, 2024
1 parent ab82efa commit 6a50e6e
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
4 changes: 4 additions & 0 deletions packages/tailwindcss/src/compat/apply-compat-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions packages/tailwindcss/src/compat/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]`,
)
})
5 changes: 5 additions & 0 deletions packages/tailwindcss/src/compat/config/resolve-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface ResolutionContext {
}

let minimal: ResolvedConfig = {
blocklist: [],
prefix: '',
darkMode: null,
theme: {},
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions packages/tailwindcss/src/compat/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,12 @@ export interface UserConfig {
export interface ResolvedConfig {
prefix: string
}

// `blocklist` support
export interface UserConfig {
blocklist?: string[]
}

export interface ResolvedConfig {
blocklist: string[]
}
5 changes: 5 additions & 0 deletions packages/tailwindcss/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 13 additions & 2 deletions packages/tailwindcss/src/design-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type DesignSystem = {
utilities: Utilities
variants: Variants

invalidCandidates: Set<string>

getClassOrder(classes: string[]): [string, bigint | null][]
getClassList(): ClassEntry[]
getVariants(): VariantEntry[]
Expand Down Expand Up @@ -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))
Expand Down
5 changes: 2 additions & 3 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down
21 changes: 21 additions & 0 deletions packages/tailwindcss/src/intellisense.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
",
]
`)
})

0 comments on commit 6a50e6e

Please sign in to comment.