This commit is contained in:
Jordan Pittman 2024-10-04 13:10:41 -04:00
parent ca55a77c20
commit 6a0da6fa56
2 changed files with 53 additions and 66 deletions

View file

@ -134,7 +134,11 @@ async function run() {
}
// Analyze the stylesheets
await analyzeStylesheets(stylesheets)
try {
await analyzeStylesheets(stylesheets)
} catch (e: unknown) {
error(`${e}`)
}
// Migrate each file
let options: MigrateOptions = {
@ -154,7 +158,11 @@ async function run() {
}
// Split up stylesheets (as needed)
await splitStylesheets(stylesheets)
try {
await splitStylesheets(stylesheets)
} catch (e: unknown) {
error(`${e}`)
}
// Format nodes
for (let sheet of stylesheets) {

View file

@ -30,7 +30,8 @@ export interface Stylesheet {
parents?: Set<Stylesheet>
children?: Set<Stylesheet>
importRules?: Set<AtRule>
importsFromParents?: Set<AtRule>
importsInSelf?: Set<AtRule>
hasUtilities?: boolean
readonly ancestors?: Set<Stylesheet>
@ -71,7 +72,8 @@ export async function analyze(stylesheets: Stylesheet[]) {
stylesheetsByFile.set(stylesheet.file, stylesheet)
stylesheet.layers ??= new Set()
stylesheet.importRules ??= new Set()
stylesheet.importsFromParents ??= new Set()
stylesheet.importsInSelf ??= new Set()
stylesheet.parents ??= new Set()
stylesheet.children ??= new Set()
@ -129,16 +131,13 @@ export async function analyze(stylesheets: Stylesheet[]) {
// that we don't want to modify
if (!stylesheet) return
// If it does then this import node get added to that sylesheets `importRules` set
// If it does then this import node get added to that sylesheets `importsFromParents` set
let parent = stylesheetsByFile.get(node.source?.input.file ?? '')
if (!parent) return
// Record the import node for this sheet so it can be modified later
stylesheet.importRules!.add(node)
node.raws.sheets ??= new Set()
// @ts-ignore
node.raws.sheets!.add(stylesheet)
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
@ -191,7 +190,6 @@ export async function prepare(stylesheet: Stylesheet) {
export async function split(stylesheets: Stylesheet[]) {
let utilitySheets = new Map<Stylesheet, Stylesheet>()
let newRules: postcss.AtRule[] = []
for (let sheet of stylesheets) {
if (!sheet.root) continue
@ -218,7 +216,7 @@ export async function split(stylesheets: Stylesheet[]) {
for (let sheet of stylesheets) {
if (!sheet.root) continue
if (!sheet.importRules?.size) continue
if (!sheet.importsFromParents?.size) continue
// Skip stylesheets that don't have utilities
// and don't have any children that have utilities
@ -247,7 +245,8 @@ export async function split(stylesheets: Stylesheet[]) {
let utilitySheet: Stylesheet = {
file: sheet.file!.replace(/\.css$/, '.utilities.css'),
root: utilities,
importRules: new Set(),
importsFromParents: new Set(),
importsInSelf: new Set(),
parents: new Set(),
children: new Set(),
layers: new Set(),
@ -261,25 +260,15 @@ export async function split(stylesheets: Stylesheet[]) {
if (!sheet.root) continue
let utilitySheet = utilitySheets.get(sheet)
let importNodes = new Set<postcss.AtRule>()
sheet.root.walkAtRules('import', (node) => {
importNodes.add(node)
})
let utilityImports: Set<postcss.AtRule> = new Set()
for (let node of importNodes) {
if (!node.raws.sheets) continue
console.log(`---- ${sheet.file} ----`)
console.log(Array.from(sheet.importsInSelf ?? []).map((node) => node.toString()))
for (let node of sheet.importsInSelf ?? []) {
let id = node.params.match(/['"](.*)['"]/)?.[1]
if (!id) return
let normalSheetForImport = Array.from(sheet.children ?? [])?.find((child) => {
return child.importRules?.has(node)
})
let utilitySheetForImport = utilitySheets.get(normalSheetForImport!)
let newFile = id.replace(/\.css$/, '.utilities.css')
let newImport = node.clone({
params: `"${newFile}"`,
@ -290,7 +279,13 @@ export async function split(stylesheets: Stylesheet[]) {
if (utilitySheet) {
utilityImports.add(newImport)
utilitySheetForImport?.importRules?.add(newImport)
utilitySheet.importsInSelf!.add(newImport)
for (let child of sheet.children ?? []) {
if (child.importsFromParents?.has(node)) {
utilitySheets.get(child)!.importsFromParents!.add(newImport)
}
}
} else {
node.after(newImport)
}
@ -319,57 +314,41 @@ export async function split(stylesheets: Stylesheet[]) {
}
}
stylesheets.push(...utilitySheets.values())
return
// At this point, we probably created `{name}.utilities.css` files. If the
// original `{name}.css` is empty, then we can optimize the output a bit more
// by re-using the original file but just getting rid of the `layer
// (utilities)` marker.
// If removing files means that some `@import` at-rules are now unnecessary, we
// can also remove those.
{
// 1. Get rid of empty files (and their imports)
let repeat = true
while (repeat) {
repeat = false
for (let stylesheet of stylesheets) {
// Was already marked to be removed, skip
if (stylesheet.unlink) continue
for (let sheet of stylesheets) {
if (!sheet.root) continue
// Original content was not empty, but the new content is. Therefore we
// can mark the file for removal.
// TODO: Make sure that empty files are not even part of `stylesheets`
// in the first place. Then we can get rid of this check.
if (stylesheet.content?.trim() !== '' && stylesheet?.root?.toString().trim() === '') {
repeat = true
stylesheet.unlink = true
let utilitySheet = utilitySheets.get(sheet)
if (!utilitySheet) continue
// Cleanup imports that are now unnecessary
for (let parent of stylesheet.importRules ?? []) {
parent.remove()
}
}
}
if (sheet.root.toString().trim() !== '') continue
// We have a sheet that became empty after splitting
// 1. Replace the sheet with it's utility sheet content
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 ?? []) {
node.params = node.params.replace(/\.utilities\.css['"]/, '.css')
}
// 2. Use `{name}.css` instead of `{name}.utilities.css` if the `{name}.css`
// was marked for removal.
for (let [originalSheet, utilitySheet] of utilitySheets) {
// Original sheet was marked for removal, use the original file instead.
if (!originalSheet.unlink) continue
// Fixup the import rule
for (let parent of originalSheet.importRules ?? []) {
parent.params = parent.params.replace(/\.utilities\.css(['"])/, '.css$1')
}
// Fixup the file path
// utilitySheet.file = utilitySheet.file?.replace(/\.utilities\.css$/, '.css')
console.log('Cleanup', utilitySheet.file)
// 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 ?? []) {
node.remove()
}
// 3. Mark the utility sheet for removal
utilitySheets.delete(sheet)
}
stylesheets.push(...utilitySheets.values())
}
// @import './a.css' layer(utilities) ;