import path from 'node:path'
import postcss, { AtRule, type Plugin, Root } from 'postcss'
import { normalizePath } from '../../../@tailwindcss-node/src/normalize-path'
import type { JSConfigMigration } from '../migrate-js-config'
import type { Stylesheet } from '../stylesheet'
import { walk, WalkAction } from '../utils/walk'
const ALREADY_INJECTED = new WeakMap<Stylesheet, string[]>()
export function migrateConfig(
sheet: Stylesheet,
{
configFilePath,
jsConfigMigration,
}: { configFilePath: string; jsConfigMigration: JSConfigMigration },
): Plugin {
function injectInto(sheet: Stylesheet) {
let alreadyInjected = ALREADY_INJECTED.get(sheet)
if (alreadyInjected && alreadyInjected.includes(configFilePath)) {
return
} else if (alreadyInjected) {
alreadyInjected.push(configFilePath)
} else {
ALREADY_INJECTED.set(sheet, [configFilePath])
}
let root = sheet.root
if (!sheet.file) return
let cssConfig = new AtRule()
if (jsConfigMigration === null) {
{
let hasConfig = false
root.walkAtRules('config', () => {
hasConfig = true
return false
})
if (hasConfig) return
}
cssConfig.append(
new AtRule({
name: 'config',
params: `'${relativeToStylesheet(sheet, configFilePath)}'`,
}),
)
} else {
let css = '\n\n'
for (let source of jsConfigMigration.sources) {
let absolute = path.resolve(source.base, source.pattern)
css += `@source '${relativeToStylesheet(sheet, absolute)}';\n`
}
if (jsConfigMigration.sources.length > 0) {
css = css + '\n'
}
for (let plugin of jsConfigMigration.plugins) {
let relative =
plugin.path[0] === '.'
? relativeToStylesheet(sheet, path.resolve(plugin.base, plugin.path))
: plugin.path
if (plugin.options === null) {
css += `@plugin '${relative}';\n`
} else {
css += `@plugin '${relative}' {\n`
for (let [property, value] of Object.entries(plugin.options)) {
let cssValue = ''
if (typeof value === 'string') {
cssValue = quoteString(value)
} else if (Array.isArray(value)) {
cssValue = value
.map((v) => (typeof v === 'string' ? quoteString(v) : '' + v))
.join(', ')
} else {
cssValue = '' + value
}
css += ` ${property}: ${cssValue};\n`
}
css += '}\n'
}
}
if (jsConfigMigration.plugins.length > 0) {
css = css + '\n'
}
cssConfig.append(postcss.parse(css + jsConfigMigration.css))
}
let locationNode = null as AtRule | null
walk(root, (node) => {
if (node.type === 'atrule' && node.name === 'import') {
locationNode = node
}
return WalkAction.Skip
})
for (let node of cssConfig?.nodes ?? []) {
node.raws.tailwind_pretty = true
}
if (!locationNode) {
root.prepend(cssConfig.nodes)
} else if (locationNode.name === 'import') {
locationNode.after(cssConfig.nodes)
}
}
function migrate(root: Root) {
let hasTailwindImport = false
let hasFullTailwindImport = false
root.walkAtRules('import', (node) => {
if (node.params.match(/['"]tailwindcss['"]/)) {
hasTailwindImport = true
hasFullTailwindImport = true
return false
} else if (node.params.match(/['"]tailwindcss\/.*?['"]/)) {
hasTailwindImport = true
}
})
if (!hasTailwindImport) return
if (hasFullTailwindImport || sheet.parents.size <= 0) {
injectInto(sheet)
return
}
if (sheet.parents.size > 0) {
for (let parent of sheet.ancestors()) {
if (parent.parents.size === 0) {
injectInto(parent)
}
}
}
}
return {
postcssPlugin: '@tailwindcss/upgrade/migrate-config',
OnceExit: migrate,
}
}
function relativeToStylesheet(sheet: Stylesheet, absolute: string) {
if (!sheet.file) throw new Error('Can not find a path for the stylesheet')
let sheetPath = sheet.file
let relative = path.relative(path.dirname(sheetPath), absolute)
if (relative[0] !== '.') {
relative = `./${relative}`
}
return normalizePath(relative)
}
function quoteString(value: string): string {
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
}