import { type AtRule, type Comment, type Plugin, type Rule } from 'postcss'
import SelectorParser from 'postcss-selector-parser'
import { segment } from '../../../../tailwindcss/src/utils/segment'
import { Stylesheet } from '../../stylesheet'
import * as version from '../../utils/version'
import { walk, WalkAction, walkDepth } from '../../utils/walk'
export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin {
function migrate(atRule: AtRule) {
if (!version.isMajor(3)) return
if (atRule.params !== 'utilities' && atRule.params !== 'components') return
let defaultsAtRule = atRule.clone()
walk(atRule, (node) => {
if (node.type !== 'rule') return
let selectors = segment(node.selector, ',')
if (selectors.length > 1) {
let clonedNodes: Rule[] = []
for (let selector of selectors) {
let clone = node.clone({ selector })
clonedNodes.push(clone)
}
node.replaceWith(clonedNodes)
}
return WalkAction.Skip
})
let classes = new Set<string>()
walk(atRule, (node) => {
if (node.type !== 'rule') return
SelectorParser((selectors) => {
selectors.each((selector) => {
walk(selector, (selectorNode) => {
if (selectorNode.type === 'pseudo' && selectorNode.value === ':not') {
return WalkAction.Skip
}
if (selectorNode.type === 'class') {
classes.add(selectorNode.value)
}
})
})
}).processSync(node.selector, { updateSelector: false })
return WalkAction.Skip
})
walk(defaultsAtRule, (node) => {
if (node.type !== 'rule') return
SelectorParser((selectors) => {
selectors.each((selector) => {
walk(selector, (selectorNode) => {
if (selectorNode.type === 'pseudo' && selectorNode.value === ':not') {
return WalkAction.Skip
}
if (selectorNode.type === 'class' && classes.has(selectorNode.value)) {
node.remove()
return WalkAction.Stop
}
})
})
}).processSync(node, { updateSelector: true })
})
let clones: AtRule[] = [defaultsAtRule]
for (let cls of classes) {
let clone = atRule.clone()
clones.push(clone)
walk(clone, (node) => {
if (node.type === 'atrule') {
if (!node.nodes || node.nodes?.length === 0) {
node.remove()
}
}
if (node.type !== 'rule') return
let containsClass = false
SelectorParser((selectors) => {
selectors.each((selector) => {
walk(selector, (selectorNode) => {
if (selectorNode.type === 'pseudo' && selectorNode.value === ':not') {
return WalkAction.Skip
}
if (selectorNode.type === 'class' && selectorNode.value === cls) {
containsClass = true
let target = selector.atPosition(
selectorNode.source!.start!.line,
selectorNode.source!.start!.column,
)
let parent = target.parent!
let idx = (target.parent?.index(target) ?? 0) - 1
while (idx >= 0 && parent.at(idx)?.type !== 'combinator') {
let current = parent.at(idx + 1)
let previous = parent.at(idx)
parent.at(idx + 1).replaceWith(previous)
parent.at(idx).replaceWith(current)
idx--
}
target.replaceWith(SelectorParser.nesting())
}
})
})
}).processSync(node, { updateSelector: true })
if (!containsClass) {
let toRemove: (Comment | Rule)[] = [node]
let idx = node.parent?.index(node) ?? null
if (idx !== null) {
for (let i = idx - 1; i >= 0; i--) {
if (node.parent?.nodes.at(i)?.type === 'rule') {
break
}
if (node.parent?.nodes.at(i)?.type === 'comment') {
toRemove.push(node.parent?.nodes.at(i) as Comment)
}
}
}
for (let node of toRemove) {
node.remove()
}
}
return WalkAction.Skip
})
clone.name = 'utility'
clone.params = cls
clone.raws.before = `${clone.raws.before ?? ''}\n\n`
}
for (let idx = clones.length - 1; idx >= 0; idx--) {
let clone = clones[idx]
walkDepth(clone, (node) => {
if (clone === defaultsAtRule) {
if (node.type === 'comment') {
let found = false
for (let other of clones) {
if (other === defaultsAtRule) continue
walk(other, (child) => {
if (
child.type === 'comment' &&
child.source?.start?.offset === node.source?.start?.offset
) {
node.remove()
found = true
return WalkAction.Stop
}
})
if (found) {
return WalkAction.Skip
}
}
}
}
if ((node.type === 'rule' || node.type === 'atrule') && node.nodes?.length === 0) {
node.remove()
}
else if (node.type === 'rule' && node.selector === '&') {
interface PostCSSNode {
type: string
parent?: PostCSSNode
}
let parent: PostCSSNode | undefined = node.parent
let skip = false
while (parent) {
if (parent.type === 'rule') {
skip = true
break
}
parent = parent.parent
}
if (!skip) node.replaceWith(node.nodes)
}
})
if (clone.nodes?.length === 0) {
clones.splice(idx, 1)
}
}
atRule.replaceWith(clones)
}
return {
postcssPlugin: '@tailwindcss/upgrade/migrate-at-layer-utilities',
OnceExit: (root, { atRule }) => {
let layers = stylesheet.layers()
let isUtilityStylesheet = layers.has('utilities') || layers.has('components')
if (isUtilityStylesheet) {
let rule = atRule({ name: 'layer', params: 'utilities' })
rule.append(root.nodes)
root.append(rule)
}
root.walkAtRules('layer', migrate)
{
let utilities = new Map<string, AtRule>()
walk(root, (child) => {
if (child.type === 'atrule' && child.name === 'utility') {
let existing = utilities.get(child.params)
if (existing) {
existing.append(child.nodes!)
child.remove()
} else {
utilities.set(child.params, child)
}
}
})
}
if (isUtilityStylesheet) {
root.each((node) => {
if (node.type !== 'atrule') return
if (node.name !== 'layer') return
if (node.params !== 'utilities') return
node.replaceWith(node.nodes ?? [])
})
}
},
}
}