import { version } from '../package.json'
import { substituteAtApply } from './apply'
import {
atRoot,
atRule,
comment,
context as contextNode,
decl,
rule,
styleRule,
toCss,
walk,
WalkAction,
type AstNode,
type AtRule,
type StyleRule,
} from './ast'
import { substituteAtImports } from './at-import'
import { applyCompatibilityHooks } from './compat/apply-compat-hooks'
import type { UserConfig } from './compat/config/types'
import { type Plugin } from './compat/plugin-api'
import { compileCandidates } from './compile'
import { substituteFunctions } from './css-functions'
import * as CSS from './css-parser'
import { buildDesignSystem, type DesignSystem } from './design-system'
import { Theme, ThemeOptions } from './theme'
import { segment } from './utils/segment'
import { compoundsForSelectors } from './variants'
export type Config = UserConfig
const IS_VALID_PREFIX = /^[a-z]+$/
const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/
type CompileOptions = {
base?: string
loadModule?: (
id: string,
base: string,
resourceHint: 'plugin' | 'config',
) => Promise<{ module: Plugin | Config; base: string }>
loadStylesheet?: (id: string, base: string) => Promise<{ content: string; base: string }>
}
function throwOnLoadModule(): never {
throw new Error('No `loadModule` function provided to `compile`')
}
function throwOnLoadStylesheet(): never {
throw new Error('No `loadStylesheet` function provided to `compile`')
}
function parseThemeOptions(params: string) {
let options = ThemeOptions.NONE
let prefix = null
for (let option of segment(params, ' ')) {
if (option === 'reference') {
options |= ThemeOptions.REFERENCE
} else if (option === 'inline') {
options |= ThemeOptions.INLINE
} else if (option === 'default') {
options |= ThemeOptions.DEFAULT
} else if (option.startsWith('prefix(') && option.endsWith(')')) {
prefix = option.slice(7, -1)
}
}
return [options, prefix] as const
}
async function parseCss(
css: string,
{
base = '',
loadModule = throwOnLoadModule,
loadStylesheet = throwOnLoadStylesheet,
}: CompileOptions = {},
) {
let ast = [contextNode({ base }, CSS.parse(css))] as AstNode[]
await substituteAtImports(ast, base, loadStylesheet)
let important = null as boolean | null
let theme = new Theme()
let customVariants: ((designSystem: DesignSystem) => void)[] = []
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
let firstThemeRule = null as StyleRule | null
let utilitiesNode = null as AtRule | null
let globs: { base: string; pattern: string }[] = []
let root:
| null
| 'none'
| { base: string; pattern: string } = null
walk(ast, (node, { parent, replaceWith, context }) => {
if (node.kind !== 'at-rule') return
if (
utilitiesNode === null &&
node.name === '@tailwind' &&
(node.params === 'utilities' || node.params.startsWith('utilities'))
) {
let params = segment(node.params, ' ')
for (let param of params) {
if (param.startsWith('source(')) {
let path = param.slice(7, -1)
if (path === 'none') {
root = path
continue
}
if (
(path[0] === '"' && path[path.length - 1] !== '"') ||
(path[0] === "'" && path[path.length - 1] !== "'") ||
(path[0] !== "'" && path[0] !== '"')
) {
throw new Error('`source(…)` paths must be quoted.')
}
root = {
base: context.sourceBase ?? context.base,
pattern: path.slice(1, -1),
}
}
}
utilitiesNode = node
}
if (node.name === '@utility') {
if (parent !== null) {
throw new Error('`@utility` cannot be nested.')
}
let name = node.params
if (!IS_VALID_UTILITY_NAME.test(name)) {
throw new Error(
`\`@utility ${name}\` defines an invalid utility name. Utilities should be alphanumeric and start with a lowercase letter.`,
)
}
if (node.nodes.length === 0) {
throw new Error(
`\`@utility ${name}\` is empty. Utilities should include at least one property.`,
)
}
customUtilities.push((designSystem) => {
designSystem.utilities.static(name, (candidate) => {
if (candidate.negative) return
return structuredClone(node.nodes)
})
})
return
}
if (node.name === '@source') {
if (node.nodes.length > 0) {
throw new Error('`@source` cannot have a body.')
}
if (parent !== null) {
throw new Error('`@source` cannot be nested.')
}
let path = node.params
if (
(path[0] === '"' && path[path.length - 1] !== '"') ||
(path[0] === "'" && path[path.length - 1] !== "'") ||
(path[0] !== "'" && path[0] !== '"')
) {
throw new Error('`@source` paths must be quoted.')
}
globs.push({ base: context.base, pattern: path.slice(1, -1) })
replaceWith([])
return
}
if (node.name === '@variant') {
if (parent !== null) {
throw new Error('`@variant` cannot be nested.')
}
replaceWith([])
let [name, selector] = segment(node.params, ' ')
if (node.nodes.length > 0 && selector) {
throw new Error(`\`@variant ${name}\` cannot have both a selector and a body.`)
}
if (node.nodes.length === 0) {
if (!selector) {
throw new Error(`\`@variant ${name}\` has no selector or body.`)
}
let selectors = segment(selector.slice(1, -1), ',')
let atRuleParams: string[] = []
let styleRuleSelectors: string[] = []
for (let selector of selectors) {
selector = selector.trim()
if (selector[0] === '@') {
atRuleParams.push(selector)
} else {
styleRuleSelectors.push(selector)
}
}
customVariants.push((designSystem) => {
designSystem.variants.static(
name,
(r) => {
let nodes: AstNode[] = []
if (styleRuleSelectors.length > 0) {
nodes.push(styleRule(styleRuleSelectors.join(', '), r.nodes))
}
for (let selector of atRuleParams) {
nodes.push(rule(selector, r.nodes))
}
r.nodes = nodes
},
{
compounds: compoundsForSelectors([...styleRuleSelectors, ...atRuleParams]),
},
)
})
return
}
else {
customVariants.push((designSystem) => {
designSystem.variants.fromAst(name, node.nodes)
})
return
}
}
if (node.name === '@media') {
let params = segment(node.params, ' ')
let unknownParams: string[] = []
for (let param of params) {
if (param.startsWith('source(')) {
let path = param.slice(7, -1)
walk(node.nodes, (child, { replaceWith }) => {
if (child.kind !== 'at-rule') return
if (child.name === '@tailwind' && child.params === 'utilities') {
child.params += ` source(${path})`
replaceWith([contextNode({ sourceBase: context.base }, [child])])
return WalkAction.Stop
}
})
}
else if (param.startsWith('theme(')) {
let themeParams = param.slice(6, -1)
walk(node.nodes, (child) => {
if (child.kind !== 'at-rule') {
throw new Error(
'Files imported with `@import "…" theme(…)` must only contain `@theme` blocks.',
)
}
if (child.name === '@theme') {
child.params += ' ' + themeParams
return WalkAction.Skip
}
})
}
else if (param.startsWith('prefix(')) {
let prefix = param.slice(7, -1)
walk(node.nodes, (child) => {
if (child.kind !== 'at-rule') return
if (child.name === '@theme') {
child.params += ` prefix(${prefix})`
return WalkAction.Skip
}
})
}
else if (param === 'important') {
important = true
}
else {
unknownParams.push(param)
}
}
if (unknownParams.length > 0) {
node.params = unknownParams.join(' ')
} else if (params.length > 0) {
replaceWith(node.nodes)
}
return WalkAction.Skip
}
if (node.name === '@theme') {
let [themeOptions, themePrefix] = parseThemeOptions(node.params)
if (themePrefix) {
if (!IS_VALID_PREFIX.test(themePrefix)) {
throw new Error(
`The prefix "${themePrefix}" is invalid. Prefixes must be lowercase ASCII letters (a-z) only.`,
)
}
theme.prefix = themePrefix
}
walk(node.nodes, (child, { replaceWith }) => {
if (child.kind === 'at-rule' && child.name === '@keyframes') {
theme.addKeyframes(child)
replaceWith([])
return WalkAction.Skip
}
if (child.kind === 'comment') return
if (child.kind === 'declaration' && child.property.startsWith('--')) {
theme.add(child.property, child.value ?? '', themeOptions)
return
}
let snippet = toCss([atRule(node.name, node.params, [child])])
.split('\n')
.map((line, idx, all) => `${idx === 0 || idx >= all.length - 2 ? ' ' : '>'} ${line}`)
.join('\n')
throw new Error(
`\`@theme\` blocks must only contain custom properties or \`@keyframes\`.\n\n${snippet}`,
)
})
if (!firstThemeRule && !(themeOptions & ThemeOptions.REFERENCE)) {
firstThemeRule = styleRule(':root', node.nodes)
replaceWith([firstThemeRule])
} else {
replaceWith([])
}
return WalkAction.Skip
}
})
let designSystem = buildDesignSystem(theme)
if (important) {
designSystem.important = important
}
await applyCompatibilityHooks({ designSystem, base, ast, loadModule, globs })
for (let customVariant of customVariants) {
customVariant(designSystem)
}
for (let customUtility of customUtilities) {
customUtility(designSystem)
}
if (firstThemeRule) {
let nodes = []
for (let [key, value] of theme.entries()) {
if (value.options & ThemeOptions.REFERENCE) continue
nodes.push(decl(key, value.value))
}
let keyframesRules = theme.getKeyframes()
if (keyframesRules.length > 0) {
let animationParts = [...theme.namespace('--animate').values()].flatMap((animation) =>
animation.split(' '),
)
for (let keyframesRule of keyframesRules) {
let keyframesName = keyframesRule.params
if (!animationParts.includes(keyframesName)) {
continue
}
nodes.push(atRoot([keyframesRule]))
}
}
firstThemeRule.nodes = nodes
}
substituteAtApply(ast, designSystem)
substituteFunctions(ast, designSystem.resolveThemeValue)
walk(ast, (node, { replaceWith }) => {
if (node.kind !== 'at-rule') return
if (node.name === '@utility') {
replaceWith([])
}
return WalkAction.Skip
})
return {
designSystem,
ast,
globs,
root,
utilitiesNode,
}
}
export async function compile(
css: string,
opts: CompileOptions = {},
): Promise<{
globs: { base: string; pattern: string }[]
root:
| null
| 'none'
| { base: string; pattern: string }
build(candidates: string[]): string
}> {
let { designSystem, ast, globs, root, utilitiesNode } = await parseCss(css, opts)
if (process.env.NODE_ENV !== 'test') {
ast.unshift(comment(`! tailwindcss v${version} | MIT License | https://tailwindcss.com `))
}
function onInvalidCandidate(candidate: string) {
designSystem.invalidCandidates.add(candidate)
}
let allValidCandidates = new Set<string>()
let compiledCss = toCss(ast)
let previousAstNodeCount = 0
return {
globs,
root,
build(newRawCandidates: string[]) {
let didChange = false
let prevSize = allValidCandidates.size
for (let candidate of newRawCandidates) {
if (!designSystem.invalidCandidates.has(candidate)) {
allValidCandidates.add(candidate)
didChange ||= allValidCandidates.size !== prevSize
}
}
if (!didChange) {
return compiledCss
}
if (utilitiesNode) {
let newNodes = compileCandidates(allValidCandidates, designSystem, {
onInvalidCandidate,
}).astNodes
if (previousAstNodeCount === newNodes.length) {
return compiledCss
}
previousAstNodeCount = newNodes.length
utilitiesNode.nodes = newNodes
compiledCss = toCss(ast)
}
return compiledCss
},
}
}
export async function __unstable__loadDesignSystem(css: string, opts: CompileOptions = {}) {
let result = await parseCss(css, opts)
return result.designSystem
}
export default function postcssPluginWarning() {
throw new Error(
`It looks like you're trying to use \`tailwindcss\` directly as a PostCSS plugin. The PostCSS plugin has moved to a separate package, so to continue using Tailwind CSS with PostCSS you'll need to install \`@tailwindcss/postcss\` and update your PostCSS configuration.`,
)
}