Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Require candidates to be prefixed when configured
  • Loading branch information
thecrypticace committed Sep 25, 2024
commit 3cecbdf13231b2cc01f40bee76930e632f75d893
50 changes: 49 additions & 1 deletion packages/tailwindcss/src/candidate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ import { Variants } from './variants'

function run(
candidate: string,
{ utilities, variants }: { utilities?: Utilities; variants?: Variants } = {},
{
utilities,
variants,
prefix,
}: { utilities?: Utilities; variants?: Variants; prefix?: string } = {},
) {
utilities ??= new Utilities()
variants ??= new Variants()

let designSystem = buildDesignSystem(new Theme())
designSystem.theme.prefix = prefix ?? null

designSystem.utilities = utilities
designSystem.variants = variants
Expand Down Expand Up @@ -1259,3 +1264,46 @@ it('should parse a variant containing an arbitrary string with unbalanced parens
]
`)
})

it('should parse candidates with a prefix', () => {
let utilities = new Utilities()
utilities.static('flex', () => [])

let variants = new Variants()
variants.static('hover', () => {})

// A prefix is required
expect(run(`flex`, { utilities, variants, prefix: 'tw' })).toEqual([])

// The prefix always comes first — even before variants
expect(run(`tw:flex`, { utilities, variants, prefix: 'tw' })).toMatchInlineSnapshot(`
[
{
"important": false,
"kind": "static",
"negative": false,
"raw": "tw:flex",
"root": "flex",
"variants": [],
},
]
`)
expect(run(`tw:hover:flex`, { utilities, variants, prefix: 'tw' })).toMatchInlineSnapshot(`
[
{
"important": false,
"kind": "static",
"negative": false,
"raw": "tw:hover:flex",
"root": "flex",
"variants": [
{
"compounds": true,
"kind": "static",
"root": "hover",
},
],
},
]
`)
})
9 changes: 9 additions & 0 deletions packages/tailwindcss/src/candidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,15 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter
// ^^^^^^^^^ -> Base
let rawVariants = segment(input, ':')

// A prefix is a special variant used to prefix all utilities. When present,
// all utilities must start with that variant which we will then remove from
// the variant list so no other part of the codebase has to know about it.
if (designSystem.theme.prefix) {
if (rawVariants[0] !== designSystem.theme.prefix) return null

rawVariants.shift()
}

// Safety: At this point it is safe to use TypeScript's non-null assertion
// operator because even if the `input` was an empty string, splitting an
// empty string by `:` will always result in an array with at least one
Expand Down
175 changes: 175 additions & 0 deletions packages/tailwindcss/src/compat/prefix.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { expect, test } from 'vitest'
import { compile } from '..'

const css = String.raw

test('utilities must be prefixed', async () => {
let input = css`
@theme reference prefix(tw);
@tailwind utilities;

@utility custom {
color: red;
}
`

let compiler = await compile(input)

// Prefixed utilities are generated
expect(
compiler.build(['tw:underline', 'tw:hover:line-through', 'tw:custom']),
).toMatchInlineSnapshot(`
".tw\\:custom {
color: red;
}
.tw\\:underline {
text-decoration-line: underline;
}
.tw\\:hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through;
}
}
}
"
`)

// Non-prefixed utilities are ignored
compiler = await compile(input)

expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toEqual('')
})

test('utilities used in @apply must be prefixed', async () => {
let compiler = await compile(css`
@theme reference prefix(tw);

.my-underline {
@apply tw:underline;
}
`)

// Prefixed utilities are generated
expect(compiler.build([])).toMatchInlineSnapshot(`
".my-underline {
text-decoration-line: underline;
}
"
`)

// Non-prefixed utilities cause an error
expect(() =>
compile(css`
@theme reference prefix(tw);

.my-underline {
@apply underline;
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Cannot apply unknown utility class: underline]`,
)
})

test('a prefix can be configured via @import theme(…)', async () => {
let input = css`
@import 'tailwindcss/theme' theme(reference prefix(tw));
@tailwind utilities;

@utility custom {
color: red;
}
`

let compiler = await compile(input, {
async loadStylesheet(id, base) {
return {
base,
content: '@theme {}',
}
},
})

// Prefixed utilities are generated
expect(compiler.build(['tw:underline', 'tw:hover:line-through', 'tw:custom']))
.toMatchInlineSnapshot(`
".tw\\:custom {
color: red;
}
.tw\\:underline {
text-decoration-line: underline;
}
.tw\\:hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through;
}
}
}
"
`)

// Non-prefixed utilities are ignored
compiler = await compile(input, {
async loadStylesheet(id, base) {
return {
base,
content: '@theme {}',
}
},
})

expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toEqual('')
})

test('a prefix can be configured via @import prefix(…)', async () => {
let input = css`
@import 'tailwindcss/theme' prefix(tw);
@tailwind utilities;

@utility custom {
color: red;
}
`

let compiler = await compile(input, {
async loadStylesheet(id, base) {
return {
base,
content: '@theme reference {}',
}
},
})

// Prefixed utilities are generated
expect(compiler.build(['tw:underline', 'tw:hover:line-through', 'tw:custom']))
.toMatchInlineSnapshot(`
".tw\\:custom {
color: red;
}
.tw\\:underline {
text-decoration-line: underline;
}
.tw\\:hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through;
}
}
}
"
`)

// Non-prefixed utilities are ignored
compiler = await compile(input, {
async loadStylesheet(id, base) {
return {
base,
content: '@theme reference {}',
}
},
})

expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toEqual('')
})