import postcss, { type AtRule, type ChildNode, type Comment, type Plugin, type Root } from 'postcss'
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
import { walk, WalkAction } from '../../utils/walk'
const BUCKET_ORDER = [
'import',
'config',
'plugin',
'source',
'custom-variant',
'theme',
'compatibility',
'utility',
'user',
]
export function sortBuckets(): Plugin {
async function migrate(root: Root) {
{
let comments: Comment[] = []
let buckets = new DefaultMap<string, AtRule>((name) => {
let bucket = postcss.atRule({ name: 'tw-bucket', params: name, nodes: [] })
root.append(bucket)
return bucket
})
root.walkAtRules('tw-bucket', (node) => {
buckets.set(node.params, node)
})
let lastLayer = 'user'
function injectInto(name: string, ...nodes: ChildNode[]) {
lastLayer = name
buckets.get(name).nodes?.push(...comments.splice(0), ...nodes)
}
walk(root, (node) => {
if (node.type === 'atrule' && node.name === 'tw-bucket') {
return WalkAction.Skip
}
if (node.type === 'comment') {
if (comments.length > 0) {
comments.push(node)
return
}
let prevDistance = distance(node.prev(), node) ?? Infinity
let nextDistance = distance(node, node.next()) ?? Infinity
if (prevDistance < nextDistance) {
buckets.get(lastLayer).nodes?.push(node)
} else {
comments.push(node)
}
}
else if (
node.type === 'atrule' &&
['config', 'plugin', 'source', 'theme', 'utility', 'custom-variant'].includes(node.name)
) {
injectInto(node.name, node)
}
else if (
(node.type === 'atrule' && node.name === 'layer' && !node.nodes) ||
(node.type === 'atrule' && node.name === 'import') ||
(node.type === 'atrule' && node.name === 'charset') ||
(node.type === 'atrule' && node.name === 'tailwind')
) {
injectInto('import', node)
}
else if (node.type === 'rule' || node.type === 'atrule') {
injectInto('user', node)
}
else {
injectInto('user', node)
}
return WalkAction.Skip
})
if (comments.length > 0) {
injectInto(lastLayer)
}
}
let firstBuckets = new Map<string, AtRule>()
root.walkAtRules('tw-bucket', (node) => {
let firstBucket = firstBuckets.get(node.params)
if (!firstBucket) {
firstBuckets.set(node.params, node)
return
}
if (node.nodes) {
firstBucket.append(...node.nodes)
}
})
root.walkAtRules('tw-bucket', (node) => {
if (!node.nodes?.length) {
node.remove()
}
})
{
let sorted = Array.from(firstBuckets.values()).sort((a, z) => {
let aIndex = BUCKET_ORDER.indexOf(a.params)
let zIndex = BUCKET_ORDER.indexOf(z.params)
return aIndex - zIndex
})
root.removeAll()
root.append(sorted)
}
}
return {
postcssPlugin: '@tailwindcss/upgrade/sort-buckets',
OnceExit: migrate,
}
}
function distance(before?: ChildNode, after?: ChildNode): number | null {
if (!before || !after) return null
if (!before.source || !after.source) return null
if (!before.source.start || !after.source.start) return null
if (!before.source.end || !after.source.end) return null
let d = Math.abs(before.source.end.line - after.source.start.line)
return d
}