import { Features } from '.'
import { rule, toCss, walk, WalkAction, type AstNode } from './ast'
import { compileCandidates } from './compile'
import type { DesignSystem } from './design-system'
import { DefaultMap } from './utils/default-map'
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 }) => {
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)) {
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
for (let i = 0; i < parent.nodes.length; i++) {
let node = parent.nodes[i]
if (node.kind !== 'at-rule' || node.name !== '@apply') continue
let candidates = node.params.split(/\s+/g)
{
let candidateAst = compileCandidates(candidates, designSystem, {
onInvalidCandidate: (candidate) => {
throw new Error(`Cannot apply unknown utility class: ${candidate}`)
},
}).astNodes
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)
}
}
parent.nodes.splice(i, 1, ...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
}
}
}
}