Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for prefixes #14501

Merged
merged 7 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add support for prefixes ([#14501](https://github.com/tailwindlabs/tailwindcss/pull/14501))

### Fixed

- _Experimental_: Improve codemod output, keep CSS after last Tailwind directive unlayered ([#14512](https://github.com/tailwindlabs/tailwindcss/pull/14512))
Expand Down
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
21 changes: 21 additions & 0 deletions packages/tailwindcss/src/compat/apply-compat-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { buildPluginApi, type CssPluginOptions, type Plugin } from './plugin-api
import { registerScreensConfig } from './screens-config'
import { registerThemeVariantOverrides } from './theme-variants'

const IS_VALID_PREFIX = /^[a-z]+$/

export async function applyCompatibilityHooks({
designSystem,
base,
Expand Down Expand Up @@ -208,6 +210,25 @@ export async function applyCompatibilityHooks({
registerThemeVariantOverrides(resolvedUserConfig, designSystem)
registerScreensConfig(resolvedUserConfig, designSystem)

// If a prefix has already been set in CSS don't override it
if (!designSystem.theme.prefix && resolvedConfig.prefix) {
if (resolvedConfig.prefix.endsWith('-')) {
resolvedConfig.prefix = resolvedConfig.prefix.slice(0, -1)

console.warn(
`The prefix "${resolvedConfig.prefix}" is invalid. Prefixes must be lowercase ASCII letters (a-z) only and is written as a variant before all utilities. We have fixed up the prefix for you. Remove the trailing \`-\` to silence this warning.`,
)
}

if (!IS_VALID_PREFIX.test(resolvedConfig.prefix)) {
throw new Error(
`The prefix "${resolvedConfig.prefix}" is invalid. Prefixes must be lowercase ASCII letters (a-z) only.`,
)
}

designSystem.theme.prefix = resolvedConfig.prefix
}

// 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
154 changes: 154 additions & 0 deletions packages/tailwindcss/src/compat/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1217,3 +1217,157 @@ test('merges css breakpoints with js config screens', async () => {
"
`)
})

test('utilities must be prefixed', async () => {
let input = css`
@tailwind utilities;
@config "./config.js";

@utility custom {
color: red;
}
`

let compiler = await compile(input, {
loadModule: async (id, base) => ({
base,
module: { prefix: 'tw' },
}),
})

// 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, {
loadModule: async (id, base) => ({
base,
module: { prefix: 'tw' },
}),
})

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

test('utilities used in @apply must be prefixed', async () => {
let compiler = await compile(
css`
@config "./config.js";

.my-underline {
@apply tw:underline;
}
`,
{
loadModule: async (id, base) => ({
base,
module: { prefix: 'tw' },
}),
},
)

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

// Non-prefixed utilities cause an error
expect(() =>
compile(
css`
@config "./config.js";

.my-underline {
@apply underline;
}
`,
{
loadModule: async (id, base) => ({
base,
module: { prefix: 'tw' },
}),
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Cannot apply unknown utility class: underline]`,
)
})

test('Prefixes configured in CSS take precedence over those defined in JS configs', async () => {
let compiler = await compile(
css`
@theme prefix(wat) {
--color-red: #f00;
--color-green: #0f0;
--breakpoint-sm: 640px;
}

@config "./plugin.js";

@tailwind utilities;

@utility custom {
color: red;
}
`,
{
async loadModule(id, base) {
return {
base,
module: { prefix: 'tw' },
}
},
},
)

expect(compiler.build(['wat:custom'])).toMatchInlineSnapshot(`
":root {
--wat-color-red: #f00;
--wat-color-green: #0f0;
--wat-breakpoint-sm: 640px;
}
.wat\\:custom {
color: red;
}
"
`)
})

test('a prefix must be letters only', async () => {
await expect(() =>
compile(
css`
@config "./plugin.js";
`,
{
async loadModule(id, base) {
return {
base,
module: { prefix: '__' },
}
},
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: The prefix "__" is invalid. Prefixes must be lowercase ASCII letters (a-z) only.]`,
)
})
7 changes: 6 additions & 1 deletion 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 = {
prefix: '',
darkMode: null,
theme: {},
plugins: [],
Expand Down Expand Up @@ -54,11 +55,15 @@ export function resolveConfig(design: DesignSystem, files: ConfigFile[]): Resolv
extractConfigs(ctx, file)
}

// Merge dark mode
// Merge top level keys
for (let config of ctx.configs) {
if ('darkMode' in config && config.darkMode !== undefined) {
ctx.result.darkMode = config.darkMode ?? null
}

if ('prefix' in config && config.prefix !== undefined) {
ctx.result.prefix = config.prefix ?? ''
}
}

// 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 @@ -69,3 +69,12 @@ export interface UserConfig {
export interface ResolvedConfig {
darkMode: DarkModeStrategy | null
}

// `prefix` support
export interface UserConfig {
prefix?: string
}

export interface ResolvedConfig {
prefix: string
}
Loading