import { Features } from '.'
import { rule, toCss, walk, WalkAction, type AstNode } from './ast'
import { compileCandidates } from './compile'
import type { DesignSystem } from './design-system'
import type { SourceLocation } from './source-maps/source'
import { DefaultMap } from './utils/default-map'
import { segment } from './utils/segment'
export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
let features = Features.None
let root = rule('&', ast)
let parents = new Set<AstNode>()
let dependencies = new DefaultMap<AstNode, Set<string>>(() => new Set<string>())
let definitions = new DefaultMap(() => new Set<AstNode>())
walk([root], (node, { parent, path }) => {
if (node.kind !== 'at-rule') return
if (node.name === '@keyframes') {
walk(node.nodes, (child) => {
if (child.kind === 'at-rule' && child.name === '@apply') {
throw new Error(`You cannot use \`@apply\` inside \`@keyframes\`.`)
}
})
return WalkAction.Skip
}
if (node.name === '@utility') {
let name = node.params.replace(/-\*$/, '')
definitions.get(name).add(node)
walk(node.nodes, (child) => {
if (child.kind !== 'at-rule' || child.name !== '@apply') return
parents.add(node)
for (let dependency of resolveApplyDependencies(child, designSystem)) {
dependencies.get(node).add(dependency)
}
})
return
}
if (node.name === '@apply') {
if (parent === null) return
features |= Features.AtApply
parents.add(parent)
for (let dependency of resolveApplyDependencies(node, designSystem)) {
for (let parent of path) {
if (parent === node) continue
if (!parents.has(parent)) continue
dependencies.get(parent).add(dependency)
}
}
}
})
let seen = new Set<AstNode>()
let sorted: AstNode[] = []
let wip = new Set<AstNode>()
function visit(node: AstNode, path: AstNode[] = []) {
if (seen.has(node)) {
return
}
if (wip.has(node)) {
let next = path[(path.indexOf(node) + 1) % path.length]
if (
node.kind === 'at-rule' &&
node.name === '@utility' &&
next.kind === 'at-rule' &&
next.name === '@utility'
) {
walk(node.nodes, (child) => {
if (child.kind !== 'at-rule' || child.name !== '@apply') return
let candidates = child.params.split(/\s+/g)
for (let candidate of candidates) {
for (let candidateAstNode of designSystem.parseCandidate(candidate)) {
switch (candidateAstNode.kind) {
case 'arbitrary':
break
case 'static':
case 'functional':
if (next.params.replace(/-\*$/, '') === candidateAstNode.root) {
throw new Error(
`You cannot \`@apply\` the \`${candidate}\` utility here because it creates a circular dependency.`,
)
}
break
default:
candidateAstNode satisfies never
}
}
}
})
}
throw new Error(
`Circular dependency detected:\n\n${toCss([node])}\nRelies on:\n\n${toCss([next])}`,
)
}
wip.add(node)
for (let dependencyId of dependencies.get(node)) {
for (let dependency of definitions.get(dependencyId)) {
path.push(node)
visit(dependency, path)
path.pop()
}
}
seen.add(node)
wip.delete(node)
sorted.push(node)
}
for (let node of parents) {
visit(node)
}
for (let parent of sorted) {
if (!('nodes' in parent)) continue
walk(parent.nodes, (child, { replaceWith }) => {
if (child.kind !== 'at-rule' || child.name !== '@apply') return
let parts = child.params.split(/(\s+)/g)
let candidateOffsets: Record<string, number> = {}
let offset = 0
for (let [idx, part] of parts.entries()) {
if (idx % 2 === 0) candidateOffsets[part] = offset
offset += part.length
}
{
let candidates = Object.keys(candidateOffsets)
let compiled = compileCandidates(candidates, designSystem, {
respectImportant: false,
onInvalidCandidate: (candidate) => {
if (designSystem.theme.prefix && !candidate.startsWith(designSystem.theme.prefix)) {
throw new Error(
`Cannot apply unprefixed utility class \`${candidate}\`. Did you mean \`${designSystem.theme.prefix}:${candidate}\`?`,
)
}
if (designSystem.invalidCandidates.has(candidate)) {
throw new Error(
`Cannot apply utility class \`${candidate}\` because it has been explicitly disabled: https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-excluding-classes`,
)
}
let parts = segment(candidate, ':')
if (parts.length > 1) {
let utility = parts.pop()!
if (designSystem.candidatesToCss([utility])[0]) {
let compiledVariants = designSystem.candidatesToCss(
parts.map((variant) => `${variant}:[--tw-variant-check:1]`),
)
let unknownVariants = parts.filter((_, idx) => compiledVariants[idx] === null)
if (unknownVariants.length > 0) {
if (unknownVariants.length === 1) {
throw new Error(
`Cannot apply utility class \`${candidate}\` because the ${unknownVariants.map((variant) => `\`${variant}\``)} variant does not exist.`,
)
} else {
let formatter = new Intl.ListFormat('en', {
style: 'long',
type: 'conjunction',
})
throw new Error(
`Cannot apply utility class \`${candidate}\` because the ${formatter.format(unknownVariants.map((variant) => `\`${variant}\``))} variants do not exist.`,
)
}
}
}
}
if (designSystem.theme.size === 0) {
throw new Error(
`Cannot apply unknown utility class \`${candidate}\`. Are you using CSS modules or similar and missing \`@reference\`? https://tailwindcss.com/docs/functions-and-directives#reference-directive`,
)
}
throw new Error(`Cannot apply unknown utility class \`${candidate}\``)
},
})
let src = child.src
let candidateAst = compiled.astNodes.map((node) => {
let candidate = compiled.nodeSorting.get(node)?.candidate
let candidateOffset = candidate ? candidateOffsets[candidate] : undefined
node = structuredClone(node)
if (!src || !candidate || candidateOffset === undefined) {
walk([node], (node) => {
node.src = src
})
return node
}
let candidateSrc: SourceLocation = [src[0], src[1], src[2]]
candidateSrc[1] += 7 + candidateOffset
candidateSrc[2] = candidateSrc[1] + candidate.length
walk([node], (node) => {
node.src = candidateSrc
})
return node
})
let newNodes: AstNode[] = []
for (let candidateNode of candidateAst) {
if (candidateNode.kind === 'rule') {
for (let child of candidateNode.nodes) {
newNodes.push(child)
}
} else {
newNodes.push(candidateNode)
}
}
replaceWith(newNodes)
}
})
}
return features
}
function* resolveApplyDependencies(
node: Extract<AstNode, { kind: 'at-rule' }>,
designSystem: DesignSystem,
) {
for (let candidate of node.params.split(/\s+/g)) {
for (let node of designSystem.parseCandidate(candidate)) {
switch (node.kind) {
case 'arbitrary':
break
case 'static':
case 'functional':
yield node.root
break
default:
node satisfies never
}
}
}
}