This commit is contained in:
Jordan Pittman 2024-10-04 14:33:56 -04:00
parent cae3cc7ba2
commit 0bb3e0863d
3 changed files with 126 additions and 116 deletions

View file

@ -11,11 +11,10 @@ import { help } from './commands/help'
import {
analyze as analyzeStylesheets,
migrate as migrateStylesheet,
prepare as prepareStylesheet,
split as splitStylesheets,
type MigrateOptions,
type Stylesheet,
} from './migrate'
import { Stylesheet } from './stylesheet'
import { migrate as migrateTemplate } from './template/migrate'
import { parseConfig } from './template/parseConfig'
import { args, type Arg } from './utils/args'
@ -120,19 +119,19 @@ async function run() {
files = files.filter((file) => file.endsWith('.css'))
// Analyze the stylesheets
let stylesheets: Stylesheet[] = files.map((file) => ({ file }))
let loadResults = await Promise.allSettled(files.map((filepath) => Stylesheet.load(filepath)))
// Load and parse all stylesheets
let prepareResults = await Promise.allSettled(
stylesheets.map((sheet) => prepareStylesheet(sheet)),
)
for (let result of prepareResults) {
for (let result of loadResults) {
if (result.status === 'rejected') {
error(`${result.reason}`)
}
}
let stylesheets = loadResults
.filter((result) => result.status === 'fulfilled')
.map((result) => result.value)
// Analyze the stylesheets
try {
await analyzeStylesheets(stylesheets)

View file

@ -1,6 +1,5 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import postcss, { AtRule } from 'postcss'
import postcss from 'postcss'
import type { Config } from 'tailwindcss'
import type { DesignSystem } from '../../tailwindcss/src/design-system'
import { segment } from '../../tailwindcss/src/utils/segment'
@ -8,6 +7,7 @@ import { migrateAtApply } from './codemods/migrate-at-apply'
import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities'
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives'
import { Stylesheet } from './stylesheet'
import { resolveCssId } from './utils/resolve'
import { walk, WalkAction } from './utils/walk'
@ -17,36 +17,12 @@ export interface MigrateOptions {
userConfig?: Config
}
export interface Stylesheet {
file?: string
unlink?: boolean
rootFile?: string
rootImport?: postcss.AtRule
content?: string | null
root?: postcss.Root | null
layers?: Set<string>
parents?: Set<Stylesheet>
children?: Set<Stylesheet>
importsFromParents?: Set<AtRule>
importsInSelf?: Set<AtRule>
hasUtilities?: boolean
readonly ancestors?: Set<Stylesheet>
readonly descendants?: Set<Stylesheet>
}
export async function migrateContents(
stylesheet: Stylesheet | string,
options: MigrateOptions = {},
) {
if (typeof stylesheet === 'string') {
stylesheet = {
content: stylesheet,
root: postcss.parse(stylesheet),
}
stylesheet = await Stylesheet.fromString(stylesheet)
}
return postcss()
@ -54,7 +30,7 @@ export async function migrateContents(
.use(migrateAtLayerUtilities(stylesheet))
.use(migrateMissingLayers())
.use(migrateTailwindDirectives(options))
.process(stylesheet.root!, { from: stylesheet.file })
.process(stylesheet.root, { from: stylesheet.file ?? undefined })
}
export async function migrate(stylesheet: Stylesheet, options: MigrateOptions) {
@ -67,33 +43,11 @@ export async function migrate(stylesheet: Stylesheet, options: MigrateOptions) {
export async function analyze(stylesheets: Stylesheet[]) {
let stylesheetsByFile = new Map<string, Stylesheet>()
for (let stylesheet of stylesheets) {
if (!stylesheet.file) continue
stylesheetsByFile.set(stylesheet.file, stylesheet)
stylesheet.layers ??= new Set()
stylesheet.importsFromParents ??= new Set()
stylesheet.importsInSelf ??= new Set()
stylesheet.parents ??= new Set()
stylesheet.children ??= new Set()
function* traverse(
sheet: Stylesheet,
list: (sheet: Stylesheet) => Iterable<Stylesheet>,
): Iterable<Stylesheet> {
for (let child of list(sheet)) {
yield child
yield* traverse(child, list)
}
for (let sheet of stylesheets) {
if (sheet.file) {
stylesheetsByFile.set(sheet.file, sheet)
}
Object.defineProperty(stylesheet, 'ancestors', {
get: () => new Set(traverse(stylesheet, (sheet) => sheet.parents ?? [])),
})
Object.defineProperty(stylesheet, 'descendants', {
get: () => new Set(traverse(stylesheet, (sheet) => sheet.children ?? [])),
})
}
// Step 1: Record which `@import` rules point to which stylesheets
@ -136,22 +90,22 @@ export async function analyze(stylesheets: Stylesheet[]) {
if (!parent) return
// Record the import node for this sheet so it can be modified later
stylesheet.importsFromParents!.add(node)
parent.importsInSelf!.add(node)
stylesheet.importsFromParents.add(node)
parent.importsInSelf.add(node)
// Connect all stylesheets together in a dependency graph
// The way this works is it uses the knowledge that we have a list of
// the `@import` nodes that cause a given stylesheet to be imported.
// That import has a `source` pointing to parent stylesheet's file path
// which can be used to look it up
stylesheet.parents!.add(parent)
parent.children!.add(stylesheet)
stylesheet.parents.add(parent)
parent.children.add(stylesheet)
for (let part of segment(node.params, ' ')) {
if (!part.startsWith('layer(')) continue
if (!part.endsWith(')')) continue
stylesheet.layers!.add(part.slice(6, -1).trim())
stylesheet.layers.add(part.slice(6, -1).trim())
}
},
},
@ -160,43 +114,28 @@ export async function analyze(stylesheets: Stylesheet[]) {
for (let sheet of stylesheets) {
if (!sheet.file) continue
if (!sheet.root) continue
await processor.process(sheet.root, { from: sheet.file })
}
// Step 2: Analyze the AST so each stylesheet can know what layers it is inside
for (let sheet of stylesheets) {
for (let ancestor of sheet.ancestors ?? []) {
for (let layer of ancestor.layers ?? []) {
sheet.layers!.add(layer)
for (let ancestor of sheet.ancestors) {
for (let layer of ancestor.layers) {
sheet.layers.add(layer)
}
}
}
}
export async function prepare(stylesheet: Stylesheet) {
if (stylesheet.file) {
stylesheet.file = path.resolve(process.cwd(), stylesheet.file)
stylesheet.content = await fs.readFile(stylesheet.file, 'utf-8')
}
if (stylesheet.content) {
stylesheet.root = postcss.parse(stylesheet.content, {
from: stylesheet.file,
})
}
}
export async function split(stylesheets: Stylesheet[]) {
let utilitySheets = new Map<Stylesheet, Stylesheet>()
for (let sheet of stylesheets) {
if (!sheet.root) continue
if (!sheet.file) continue
// We only care about stylesheets that were imported into a layer e.g. `layer(utilities)`
let isLayered = sheet.layers?.has('utilities') || sheet.layers?.has('components')
let isLayered = sheet.layers.has('utilities') || sheet.layers.has('components')
if (!isLayered) continue
// We only care about stylesheets that contain an `@utility`
@ -215,13 +154,12 @@ export async function split(stylesheets: Stylesheet[]) {
}
for (let sheet of stylesheets) {
if (!sheet.root) continue
if (!sheet.importsFromParents?.size) continue
if (!sheet.importsFromParents.size) continue
// Skip stylesheets that don't have utilities
// and don't have any children that have utilities
if (!sheet.hasUtilities) {
if (!Array.from(sheet.descendants ?? []).some((child) => child.hasUtilities)) {
if (!Array.from(sheet.descendants).some((child) => child.hasUtilities)) {
continue
}
}
@ -242,30 +180,22 @@ export async function split(stylesheets: Stylesheet[]) {
return WalkAction.Skip
})
let utilitySheet: Stylesheet = {
file: sheet.file!.replace(/\.css$/, '.utilities.css'),
root: utilities,
importsFromParents: new Set(),
importsInSelf: new Set(),
parents: new Set(),
children: new Set(),
layers: new Set(),
hasUtilities: true,
}
let utilitySheet = await Stylesheet.fromRoot(
utilities,
sheet.file!.replace(/\.css$/, '.utilities.css'),
)
utilitySheets.set(sheet, utilitySheet)
}
for (let sheet of stylesheets) {
if (!sheet.root) continue
let utilitySheet = utilitySheets.get(sheet)
let utilityImports: Set<postcss.AtRule> = new Set()
console.log(`---- ${sheet.file} ----`)
console.log(Array.from(sheet.importsInSelf ?? []).map((node) => node.toString()))
console.log(Array.from(sheet.importsInSelf).map((node) => node.toString()))
for (let node of sheet.importsInSelf ?? []) {
for (let node of sheet.importsInSelf) {
let id = node.params.match(/['"](.*)['"]/)?.[1]
if (!id) return
@ -279,11 +209,11 @@ export async function split(stylesheets: Stylesheet[]) {
if (utilitySheet) {
utilityImports.add(newImport)
utilitySheet.importsInSelf!.add(newImport)
utilitySheet.importsInSelf.add(newImport)
for (let child of sheet.children ?? []) {
if (child.importsFromParents?.has(node)) {
utilitySheets.get(child)!.importsFromParents!.add(newImport)
for (let child of sheet.children) {
if (child.importsFromParents.has(node)) {
utilitySheets.get(child)!.importsFromParents.add(newImport)
}
}
} else {
@ -292,22 +222,19 @@ export async function split(stylesheets: Stylesheet[]) {
}
if (utilitySheet && utilityImports.size > 0) {
utilitySheet.root!.prepend(Array.from(utilityImports))
utilitySheet.root.prepend(Array.from(utilityImports))
}
}
// Make sure the utility sheets track parents and import nodes and what not
for (let [normalSheet, utilitySheet] of utilitySheets) {
if (!utilitySheet.parents) continue
if (!utilitySheet.children) continue
for (let parent of normalSheet.parents ?? []) {
for (let parent of normalSheet.parents) {
let utilityParent = utilitySheets.get(parent)
if (!utilityParent) continue
utilitySheet.parents.add(utilityParent)
}
for (let child of normalSheet.children ?? []) {
for (let child of normalSheet.children) {
let utilityChild = utilitySheets.get(child)
if (!utilityChild) continue
utilitySheet.children.add(utilityChild)
@ -322,8 +249,6 @@ export async function split(stylesheets: Stylesheet[]) {
// can also remove those.
for (let sheet of stylesheets) {
continue
if (!sheet.root) continue
let utilitySheet = utilitySheets.get(sheet)
if (!utilitySheet) continue
@ -334,14 +259,14 @@ export async function split(stylesheets: Stylesheet[]) {
sheet.root = utilitySheet.root
// 2. Point the imports back to the original file since we don't need the utility file anymore
for (let node of utilitySheet.importsFromParents ?? []) {
for (let node of utilitySheet.importsFromParents) {
node.params = node.params.replace(/\.utilities\.css['"]/, '.css')
}
// 3. Remove the original import from the non-utility sheet
// TODO: This does not work because we're cloning trees during the migration
// we *cannot* rely on reference semantics at all for any postcss nodes
for (let node of sheet.importsFromParents ?? []) {
for (let node of sheet.importsFromParents) {
node.remove()
}

View file

@ -0,0 +1,86 @@
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import * as postcss from 'postcss'
export class Stylesheet {
/**
* A unique identifier for this stylesheet
*
* Used to track the stylesheet in PostCSS nodes.
*/
id: string
/**
* The PostCSS AST that represents this stylesheet.
*/
root: postcss.Root
/**
* The path to the file that this stylesheet was loaded from.
*
* If this stylesheet was not loaded from a file this will be `null`.
*/
file: string | null = null
/**
* Stylesheets that import this stylesheet.
*/
parents = new Set<Stylesheet>()
/**
* Stylesheets that are imported by stylesheet.
*/
children = new Set<Stylesheet>()
/**
* The layers this stylesheet is in, even transitive layers from parents.
*/
layers = new Set<string>()
importsFromParents = new Set<postcss.AtRule>()
importsInSelf = new Set<postcss.AtRule>()
hasUtilities = false
constructor(root: postcss.Root, file?: string) {
this.id = crypto.randomUUID()
this.root = root
this.file = file ?? null
}
get ancestors() {
return walk<Stylesheet>(this, (sheet) => sheet.parents ?? [])
}
get descendants() {
return walk<Stylesheet>(this, (sheet) => sheet.children ?? [])
}
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 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)
}
}
function* walk<T>(value: T, getList: (value: T) => Iterable<T>, seen = new Set<T>()): Iterable<T> {
for (let item of getList(value)) {
if (seen.has(item)) continue
seen.add(item)
yield item
yield* walk(item, getList, seen)
}
}