import { isGitIgnored } from 'globby'
import path from 'node:path'
import postcss, { type Result } from 'postcss'
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
import { segment } from '../../../../tailwindcss/src/utils/segment'
import { Stylesheet, type StylesheetConnection } from '../../stylesheet'
import { error, highlight, relative } from '../../utils/renderer'
import { resolveCssId } from '../../utils/resolve'
export async function analyze(stylesheets: Stylesheet[]) {
let isIgnored = await isGitIgnored()
let processingQueue: (() => Promise<Result>)[] = []
let stylesheetsByFile = new DefaultMap<string, Stylesheet | null>((file) => {
try {
if (isIgnored(file)) {
return null
}
} catch {
return null
}
try {
let sheet = Stylesheet.loadSync(file)
stylesheets.push(sheet)
processingQueue.push(() => processor.process(sheet.root, { from: sheet.file! }))
return sheet
} catch {
return null
}
})
let processor = postcss([
{
postcssPlugin: 'mark-import-nodes',
AtRule: {
import(node) {
let id = node.params.match(/['"](.*?)['"]/)?.[1]
if (!id) return
let basePath = node.source?.input.file
? path.dirname(node.source.input.file)
: process.cwd()
let resolvedPath: string | false = false
try {
if (id[0] !== '.') {
try {
resolvedPath = resolveCssId(`./${id}`, basePath)
} catch {}
}
if (!resolvedPath) {
resolvedPath = resolveCssId(id, basePath)
}
} catch (err) {
if (id.startsWith('http://') || id.startsWith('https://') || id.startsWith('//')) {
return
}
error(
`Failed to resolve import: ${highlight(id)} in ${highlight(relative(node.source?.input.file!, basePath))}. Skipping.`,
{ prefix: '↳ ' },
)
return
}
if (!resolvedPath) return
let stylesheet = stylesheetsByFile.get(resolvedPath)
if (!stylesheet) return
node.raws.tailwind_destination_sheet_id = stylesheet.id
let parent = node.source?.input.file
? stylesheetsByFile.get(node.source.input.file)
: undefined
let layers: string[] = []
for (let part of segment(node.params, ' ')) {
if (!part.startsWith('layer(')) continue
if (!part.endsWith(')')) continue
layers.push(part.slice(6, -1).trim())
}
if (parent) {
let meta = { layers }
stylesheet.parents.add({ item: parent, meta })
parent.children.add({ item: stylesheet, meta })
}
},
},
},
])
for (let sheet of stylesheets) {
if (sheet.file) {
stylesheetsByFile.set(sheet.file, sheet)
processingQueue.push(() => processor.process(sheet.root, { from: sheet.file ?? undefined }))
}
}
while (processingQueue.length > 0) {
let task = processingQueue.shift()!
await task()
}
let commonPath = process.cwd()
function pathToString(path: StylesheetConnection[]) {
let parts: string[] = []
for (let connection of path) {
if (!connection.item.file) continue
let filePath = connection.item.file.replace(commonPath, '')
let layers = connection.meta.layers.join(', ')
if (layers.length > 0) {
parts.push(`${filePath} (layers: ${layers})`)
} else {
parts.push(filePath)
}
}
return parts.join(' <- ')
}
let lines: string[] = []
for (let sheet of stylesheets) {
if (!sheet.file) continue
let { convertiblePaths, nonConvertiblePaths } = sheet.analyzeImportPaths()
let isAmbiguous = convertiblePaths.length > 0 && nonConvertiblePaths.length > 0
if (!isAmbiguous) continue
sheet.canMigrate = false
let filePath = sheet.file.replace(commonPath, '')
for (let path of convertiblePaths) {
lines.push(`- ${filePath} <- ${pathToString(path)}`)
}
for (let path of nonConvertiblePaths) {
lines.push(`- ${filePath} <- ${pathToString(path)}`)
}
}
if (lines.length === 0) {
let tailwindRootLeafs = new Set<Stylesheet>()
for (let sheet of stylesheets) {
sheet.root.walkAtRules('config', () => {
sheet.isTailwindRoot = true
return false
})
if (sheet.isTailwindRoot) continue
sheet.root.walkAtRules((node) => {
if (
node.name === 'tailwind' ||
(node.name === 'import' && node.params.match(/^["']tailwindcss["']/)) ||
(node.name === 'import' && node.params.match(/^["']tailwindcss\/.*?["']$/))
) {
sheet.isTailwindRoot = true
tailwindRootLeafs.add(sheet)
}
})
}
if (tailwindRootLeafs.size <= 1) {
return
}
{
let commonParents = new DefaultMap<Stylesheet, Set<Stylesheet>>(() => new Set<Stylesheet>())
for (let sheet of tailwindRootLeafs) {
commonParents.get(sheet).add(sheet)
}
let repeat = true
repeat: while (repeat) {
repeat = false
for (let [sheetA, childrenA] of commonParents) {
for (let [sheetB, childrenB] of commonParents) {
if (sheetA === sheetB) continue
let ancestorsA = [sheetA].concat(Array.from(sheetA.ancestors()).reverse())
let ancestorsB = [sheetB].concat(Array.from(sheetB.ancestors()).reverse())
for (let parentA of ancestorsA) {
for (let parentB of ancestorsB) {
if (parentA !== parentB) continue
let parent = parentA
commonParents.delete(sheetA)
commonParents.delete(sheetB)
for (let child of childrenA) {
commonParents.get(parent).add(child)
}
for (let child of childrenB) {
commonParents.get(parent).add(child)
}
repeat = true
continue repeat
}
}
}
}
}
for (let [parent, children] of commonParents) {
parent.isTailwindRoot = true
for (let child of children) {
if (parent === child) continue
child.isTailwindRoot = false
}
}
return
}
}
{
let error = `You have one or more stylesheets that are imported into a utility layer and non-utility layer.\n`
error += `We cannot convert stylesheets under these conditions. Please look at the following stylesheets:\n`
throw new Error(error + lines.join('\n'))
}
}