import { __unstable__loadDesignSystem, compileAst } from '@tailwindcss/node'
import * as fsSync from 'node:fs'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import * as util from 'node:util'
import * as postcss from 'postcss'
import { postCssAstToCssAst } from '../../@tailwindcss-postcss/src/ast'
export type StylesheetId = string
export interface StylesheetConnection {
item: Stylesheet
meta: {
layers: string[]
}
}
export class Stylesheet {
id: StylesheetId
root: postcss.Root
isTailwindRoot = false
linkedConfigPath: string | null = null
file: string | null = null
parents = new Set<StylesheetConnection>()
children = new Set<StylesheetConnection>()
canMigrate = true
extension: string | null = null
static async load(filepath: string) {
filepath = path.resolve(process.cwd(), filepath)
let css = await fs.readFile(filepath, 'utf-8')
let root = postcss.parse(css, { from: filepath })
return new Stylesheet(root, filepath)
}
static loadSync(filepath: string) {
filepath = path.resolve(process.cwd(), filepath)
let css = fsSync.readFileSync(filepath, 'utf-8')
let root = postcss.parse(css, { from: filepath })
return new Stylesheet(root, filepath)
}
static async fromString(css: string) {
let root = postcss.parse(css)
return new Stylesheet(root)
}
static async fromRoot(root: postcss.Root, file?: string) {
return new Stylesheet(root, file)
}
constructor(root: postcss.Root, file?: string) {
this.id = Math.random().toString(36).slice(2)
this.root = root
this.file = file ?? null
if (file) {
this.extension = path.extname(file)
}
}
get importRules() {
let imports = new Set<postcss.AtRule>()
this.root.walkAtRules('import', (rule) => {
imports.add(rule)
})
return imports
}
get isEmpty() {
return this.root.toString().trim() === ''
}
*ancestors() {
for (let { item } of walkDepth(this, (sheet) => sheet.parents)) {
yield item
}
}
*descendants() {
for (let { item } of walkDepth(this, (sheet) => sheet.children)) {
yield item
}
}
layers() {
let layers = new Set<string>()
for (let { item, path } of walkDepth(this, (sheet) => sheet.parents)) {
if (item.parents.size > 0) {
continue
}
for (let { meta } of path) {
for (let layer of meta.layers) {
layers.add(layer)
}
}
}
return layers
}
*pathsToRoot(): Iterable<StylesheetConnection[]> {
for (let { item, path } of walkDepth(this, (sheet) => sheet.parents)) {
if (item.parents.size > 0) {
continue
}
yield path
}
}
analyzeImportPaths() {
let convertiblePaths: StylesheetConnection[][] = []
let nonConvertiblePaths: StylesheetConnection[][] = []
for (let path of this.pathsToRoot()) {
let isConvertible = false
for (let { meta } of path) {
for (let layer of meta.layers) {
isConvertible ||= layer === 'utilities' || layer === 'components'
}
}
if (isConvertible) {
convertiblePaths.push(path)
} else {
nonConvertiblePaths.push(path)
}
}
return { convertiblePaths, nonConvertiblePaths }
}
containsRule(cb: (rule: postcss.AnyNode) => boolean) {
let contains = false
this.root.walk((rule) => {
if (cb(rule)) {
contains = true
return false
}
})
if (contains) {
return true
}
for (let child of this.children) {
if (child.item.containsRule(cb)) {
return true
}
}
return false
}
async compiler(): Promise<Awaited<ReturnType<typeof compileAst>> | null> {
if (!this.isTailwindRoot) return null
if (!this.file) return null
return compileAst(postCssAstToCssAst(this.root), {
base: path.dirname(this.file),
onDependency() {},
})
}
async designSystem(): Promise<Awaited<ReturnType<typeof __unstable__loadDesignSystem>> | null> {
if (!this.isTailwindRoot) return null
if (!this.file) return null
return __unstable__loadDesignSystem(this.root.toString(), {
base: path.dirname(this.file),
})
}
[util.inspect.custom]() {
return {
...this,
root: this.root.toString(),
layers: Array.from(this.layers()),
parents: Array.from(this.parents, (s) => s.item.id),
children: Array.from(this.children, (s) => s.item.id),
parentsMeta: Array.from(this.parents, (s) => s.meta),
childrenMeta: Array.from(this.children, (s) => s.meta),
}
}
}
function* walkDepth(
value: Stylesheet,
connections: (value: Stylesheet) => Iterable<StylesheetConnection>,
path: StylesheetConnection[] = [],
): Iterable<{ item: Stylesheet; path: StylesheetConnection[] }> {
for (let connection of connections(value)) {
let newPath = [...path, connection]
yield* walkDepth(connection.item, connections, newPath)
yield {
item: connection.item,
path: newPath,
}
}
}