import { AtRule, type ChildNode, type Plugin, type Root } from 'postcss'
const DEFAULT_LAYER_ORDER = ['theme', 'base', 'components', 'utilities']
export function migrateTailwindDirectives(options: { newPrefix: string | null }): Plugin {
let prefixParams = options.newPrefix ? ` prefix(${options.newPrefix})` : ''
function migrate(root: Root) {
let baseNode = null as AtRule | null
let utilitiesNode = null as AtRule | null
let orderedNodes: AtRule[] = []
let defaultImportNode = null as AtRule | null
let utilitiesImportNode = null as AtRule | null
let preflightImportNode = null as AtRule | null
let themeImportNode = null as AtRule | null
let layerOrder: string[] = []
root.walkAtRules((node) => {
if (node.name === 'import' && node.params.match(/^["']tailwindcss\/tailwind\.css["']$/)) {
node.params = node.params.replace('tailwindcss/tailwind.css', 'tailwindcss')
}
if (node.name === 'import' && node.params.match(/^["']tailwindcss["']/)) {
node.params += prefixParams
}
else if (
(node.name === 'tailwind' && node.params === 'base') ||
(node.name === 'import' && node.params.match(/^["']tailwindcss\/base["']$/))
) {
layerOrder.push('base')
orderedNodes.push(node)
baseNode = node
} else if (
(node.name === 'tailwind' && node.params === 'utilities') ||
(node.name === 'import' && node.params.match(/^["']tailwindcss\/utilities["']$/))
) {
layerOrder.push('utilities')
orderedNodes.push(node)
utilitiesNode = node
}
else if (
(node.name === 'tailwind' && node.params === 'components') ||
(node.name === 'tailwind' && node.params === 'screens') ||
(node.name === 'tailwind' && node.params === 'variants') ||
(node.name === 'import' && node.params.match(/^["']tailwindcss\/components["']$/))
) {
node.remove()
}
else if (node.name === 'responsive') {
if (node.nodes) {
for (let child of node.nodes) {
child.raws.tailwind_pretty = true
}
node.replaceWith(node.nodes)
} else {
node.remove()
}
}
})
if (baseNode !== null && utilitiesNode !== null) {
if (!defaultImportNode) {
findTargetNode(orderedNodes).before(
new AtRule({ name: 'import', params: `'tailwindcss'${prefixParams}` }),
)
}
baseNode?.remove()
utilitiesNode?.remove()
}
else if (utilitiesNode !== null) {
if (!utilitiesImportNode) {
findTargetNode(orderedNodes).before(
new AtRule({ name: 'import', params: "'tailwindcss/utilities' layer(utilities)" }),
)
}
utilitiesNode?.remove()
} else if (baseNode !== null) {
if (!themeImportNode) {
findTargetNode(orderedNodes).before(
new AtRule({ name: 'import', params: `'tailwindcss/theme' layer(theme)${prefixParams}` }),
)
}
if (!preflightImportNode) {
findTargetNode(orderedNodes).before(
new AtRule({ name: 'import', params: "'tailwindcss/preflight' layer(base)" }),
)
}
baseNode?.remove()
}
{
let sortedLayerOrder = layerOrder.toSorted((a, z) => {
return DEFAULT_LAYER_ORDER.indexOf(a) - DEFAULT_LAYER_ORDER.indexOf(z)
})
if (layerOrder.some((layer, index) => layer !== sortedLayerOrder[index])) {
let newLayerOrder = DEFAULT_LAYER_ORDER.toSorted((a, z) => {
return layerOrder.indexOf(a) - layerOrder.indexOf(z)
})
root.prepend({ name: 'layer', params: newLayerOrder.join(', ') })
}
}
}
return {
postcssPlugin: '@tailwindcss/upgrade/migrate-tailwind-directives',
OnceExit: migrate,
}
}
function findTargetNode(nodes: AtRule[]) {
let target: ChildNode = nodes.at(0)!
let previous = target.prev()
while (previous) {
if (previous.type === 'rule') {
target = previous
}
if (previous.type === 'atrule') {
if (previous.name === 'charset' || previous.name === 'import') {
previous = previous.prev()
continue
}
else if (previous.name === 'layer' && !previous.nodes) {
previous = previous.prev()
continue
}
else {
target = previous
}
}
previous = previous.prev()
}
return target
}