wip
This commit is contained in:
parent
0bb3e0863d
commit
f387d88e46
2 changed files with 138 additions and 71 deletions
|
|
@ -2,12 +2,13 @@ import path from 'node:path'
|
|||
import postcss from 'postcss'
|
||||
import type { Config } from 'tailwindcss'
|
||||
import type { DesignSystem } from '../../tailwindcss/src/design-system'
|
||||
import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
|
||||
import { segment } from '../../tailwindcss/src/utils/segment'
|
||||
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 { Stylesheet, type StylesheetId } from './stylesheet'
|
||||
import { resolveCssId } from './utils/resolve'
|
||||
import { walk, WalkAction } from './utils/walk'
|
||||
|
||||
|
|
@ -85,21 +86,19 @@ 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 `importsFromParents` set
|
||||
let parent = stylesheetsByFile.get(node.source?.input.file ?? '')
|
||||
if (!parent) return
|
||||
// Mark the import node with the ID of the stylesheet it points to
|
||||
// We will use these later to build lookup tables and modify the AST
|
||||
node.raws.tailwind_destination_sheet_id = stylesheet.id
|
||||
|
||||
// Record the import node for this sheet so it can be modified later
|
||||
stylesheet.importsFromParents.add(node)
|
||||
parent.importsInSelf.add(node)
|
||||
let parent = node.source?.input.file
|
||||
? stylesheetsByFile.get(node.source.input.file)
|
||||
: undefined
|
||||
|
||||
// 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)
|
||||
// Connect sheets together in a dependency graph
|
||||
if (parent) {
|
||||
stylesheet.parents.add(parent)
|
||||
parent.children.add(stylesheet)
|
||||
}
|
||||
|
||||
for (let part of segment(node.params, ' ')) {
|
||||
if (!part.startsWith('layer(')) continue
|
||||
|
|
@ -129,42 +128,46 @@ export async function analyze(stylesheets: Stylesheet[]) {
|
|||
}
|
||||
|
||||
export async function split(stylesheets: Stylesheet[]) {
|
||||
let utilitySheets = new Map<Stylesheet, Stylesheet>()
|
||||
|
||||
let stylesheetsById = new Map<StylesheetId, Stylesheet>()
|
||||
for (let sheet of stylesheets) {
|
||||
if (!sheet.file) continue
|
||||
stylesheetsById.set(sheet.id, sheet)
|
||||
}
|
||||
|
||||
// We only care about stylesheets that were imported into a layer e.g. `layer(utilities)`
|
||||
// A list of stylesheets we've created specifically to hold `@utility` rules
|
||||
let containsUtilities = new Set<Stylesheet>()
|
||||
|
||||
// Keep track of sheets that contain `@utillity` rules
|
||||
for (let sheet of stylesheets) {
|
||||
let isLayered = sheet.layers.has('utilities') || sheet.layers.has('components')
|
||||
if (!isLayered) continue
|
||||
|
||||
// We only care about stylesheets that contain an `@utility`
|
||||
let hasUtilities = false
|
||||
|
||||
walk(sheet.root, (node) => {
|
||||
if (node.type !== 'atrule') return
|
||||
if (node.name !== 'utility') return
|
||||
|
||||
hasUtilities = true
|
||||
containsUtilities.add(sheet)
|
||||
|
||||
return WalkAction.Stop
|
||||
})
|
||||
|
||||
sheet.hasUtilities = hasUtilities
|
||||
}
|
||||
|
||||
// Split every imported stylesheet into two parts
|
||||
// - one with the utilities and one without
|
||||
let utilitySheets = new Map<Stylesheet, Stylesheet>()
|
||||
|
||||
for (let sheet of stylesheets) {
|
||||
if (!sheet.importsFromParents.size) continue
|
||||
// Ignore stylesheets that were not imported
|
||||
if (!sheet.file) continue
|
||||
if (sheet.parents.size === 0) 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 (!containsUtilities.has(sheet)) {
|
||||
if (!Array.from(sheet.descendants).some((child) => containsUtilities.has(child))) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Split the stylesheet into two parts: one with the utilities and one without
|
||||
let utilities = postcss.root({
|
||||
raws: {
|
||||
tailwind_pretty: true,
|
||||
|
|
@ -175,6 +178,8 @@ export async function split(stylesheets: Stylesheet[]) {
|
|||
if (node.type !== 'atrule') return
|
||||
if (node.name !== 'utility') return
|
||||
|
||||
// `append` will move this node from the original sheet
|
||||
// to the new utilities sheet
|
||||
utilities.append(node)
|
||||
|
||||
return WalkAction.Skip
|
||||
|
|
@ -182,24 +187,37 @@ export async function split(stylesheets: Stylesheet[]) {
|
|||
|
||||
let utilitySheet = await Stylesheet.fromRoot(
|
||||
utilities,
|
||||
sheet.file!.replace(/\.css$/, '.utilities.css'),
|
||||
sheet.file.replace(/\.css$/, '.utilities.css'),
|
||||
)
|
||||
|
||||
utilitySheets.set(sheet, utilitySheet)
|
||||
stylesheetsById.set(utilitySheet.id, utilitySheet)
|
||||
}
|
||||
|
||||
for (let sheet of stylesheets) {
|
||||
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()))
|
||||
for (let node of sheet.importRules) {
|
||||
let sheetId = node.raws.tailwind_destination_sheet_id as StylesheetId | undefined
|
||||
|
||||
// This import rule does not point to a stylesheet
|
||||
// which likely means it points to `node_modules`
|
||||
if (!sheetId) continue
|
||||
|
||||
// This import points to a stylesheet that no longer exists
|
||||
// which likely means it was removed by the optimizer
|
||||
// this will be cleaned up later
|
||||
let originalDestination = stylesheetsById.get(sheetId)
|
||||
if (!originalDestination) continue
|
||||
|
||||
for (let node of sheet.importsInSelf) {
|
||||
let id = node.params.match(/['"](.*)['"]/)?.[1]
|
||||
if (!id) return
|
||||
|
||||
let newFile = id.replace(/\.css$/, '.utilities.css')
|
||||
|
||||
// The import will just point to the new file without any media queries,
|
||||
// layers, or other conditions because `@utility` MUST be top-level.
|
||||
let newImport = node.clone({
|
||||
params: `"${newFile}"`,
|
||||
raws: {
|
||||
|
|
@ -208,25 +226,29 @@ export async function split(stylesheets: Stylesheet[]) {
|
|||
})
|
||||
|
||||
if (utilitySheet) {
|
||||
// If this import is intended to go into the utility sheet
|
||||
// we'll collect it into a list to add later. If we don't'
|
||||
// we'll end up adding them in reverse order.
|
||||
utilityImports.add(newImport)
|
||||
utilitySheet.importsInSelf.add(newImport)
|
||||
|
||||
for (let child of sheet.children) {
|
||||
if (child.importsFromParents.has(node)) {
|
||||
utilitySheets.get(child)!.importsFromParents.add(newImport)
|
||||
}
|
||||
}
|
||||
let utilityDestination = utilitySheets.get(originalDestination)
|
||||
if (!utilityDestination) continue
|
||||
|
||||
newImport.raws.tailwind_destination_sheet_id = utilityDestination.id
|
||||
} else {
|
||||
// This import will go immediately after the original import
|
||||
node.after(newImport)
|
||||
newImport.raws.tailwind_destination_sheet_id = node.raws.tailwind_destination_sheet_id
|
||||
}
|
||||
}
|
||||
|
||||
// Add imports to the top of the utility sheet if necessary
|
||||
if (utilitySheet && utilityImports.size > 0) {
|
||||
utilitySheet.root.prepend(Array.from(utilityImports))
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the utility sheets track parents and import nodes and what not
|
||||
// Make sure the utility sheets are linked to one another
|
||||
for (let [normalSheet, utilitySheet] of utilitySheets) {
|
||||
for (let parent of normalSheet.parents) {
|
||||
let utilityParent = utilitySheets.get(parent)
|
||||
|
|
@ -241,32 +263,54 @@ export async function split(stylesheets: Stylesheet[]) {
|
|||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Tracks the at rules that import a given stylesheet
|
||||
let importNodes = new DefaultMap<Stylesheet, Set<postcss.AtRule>>(() => new Set())
|
||||
|
||||
for (let sheet of stylesheetsById.values()) {
|
||||
for (let node of sheet.importRules) {
|
||||
let sheetId = node.raws.tailwind_destination_sheet_id as StylesheetId | undefined
|
||||
|
||||
// This import rule does not point to a stylesheet
|
||||
if (!sheetId) continue
|
||||
|
||||
let destination = stylesheetsById.get(sheetId)
|
||||
|
||||
// This import rule does not point to a stylesheet that exists
|
||||
// We'll remove it later
|
||||
if (!destination) continue
|
||||
|
||||
importNodes.get(destination).add(node)
|
||||
}
|
||||
}
|
||||
|
||||
// At this point we've created many `{name}.utilities.css` files.
|
||||
// If the original file _becomes_ empty after splitting that means that
|
||||
// dedicated utility file is not required and we can move the utilities
|
||||
// back to the original file.
|
||||
//
|
||||
// This could be done in one step but separating them makes it easier to
|
||||
// reason about since the stylesheets are in a consistent state before we
|
||||
// perform any cleanup tasks.
|
||||
for (let sheet of stylesheets) {
|
||||
continue
|
||||
let utilitySheet = utilitySheets.get(sheet)
|
||||
|
||||
// This sheet was not split so there's nothing to do
|
||||
if (!utilitySheet) continue
|
||||
|
||||
if (sheet.root.toString().trim() !== '') continue
|
||||
// This sheet did not become empty
|
||||
if (!sheet.isEmpty) 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) {
|
||||
// 2. Rewrite imports in parent sheets to point to the original sheet
|
||||
for (let node of importNodes.get(utilitySheet)) {
|
||||
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 importNodes.get(sheet)) {
|
||||
node.remove()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import * as fs from 'node:fs/promises'
|
||||
import * as path from 'node:path'
|
||||
import * as util from 'node:util'
|
||||
import * as postcss from 'postcss'
|
||||
|
||||
export type StylesheetId = string
|
||||
|
||||
export class Stylesheet {
|
||||
/**
|
||||
* A unique identifier for this stylesheet
|
||||
*
|
||||
* Used to track the stylesheet in PostCSS nodes.
|
||||
*/
|
||||
id: string
|
||||
id: StylesheetId
|
||||
|
||||
/**
|
||||
* The PostCSS AST that represents this stylesheet.
|
||||
|
|
@ -37,24 +40,6 @@ export class Stylesheet {
|
|||
*/
|
||||
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)
|
||||
|
||||
|
|
@ -73,6 +58,44 @@ export class Stylesheet {
|
|||
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
|
||||
}
|
||||
|
||||
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() === ''
|
||||
}
|
||||
|
||||
get ancestors() {
|
||||
return walk<Stylesheet>(this, (sheet) => sheet.parents ?? [])
|
||||
}
|
||||
|
||||
get descendants() {
|
||||
return walk<Stylesheet>(this, (sheet) => sheet.children ?? [])
|
||||
}
|
||||
|
||||
[util.inspect.custom]() {
|
||||
return {
|
||||
...this,
|
||||
root: this.root.toString(),
|
||||
layers: Array.from(this.layers),
|
||||
parents: Array.from(this.parents, (s) => s.id),
|
||||
children: Array.from(this.children, (s) => s.id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* walk<T>(value: T, getList: (value: T) => Iterable<T>, seen = new Set<T>()): Iterable<T> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue