Skip to content

Commit 5191a76

Browse files
committed
Track parent/child relationships across stylesheets
CSS files may be imported and we’ll need to know what files import other files in order to unlock more advanced transformations
1 parent 9e4b989 commit 5191a76

File tree

3 files changed

+127
-1
lines changed

3 files changed

+127
-1
lines changed

packages/@tailwindcss-upgrade/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import path from 'node:path'
66
import postcss from 'postcss'
77
import { formatNodes } from './codemods/format-nodes'
88
import { help } from './commands/help'
9-
import { migrate as migrateStylesheet } from './migrate'
9+
import { analyze as analyzeStylesheets, migrate as migrateStylesheet } from './migrate'
1010
import { migratePostCSSConfig } from './migrate-postcss'
1111
import { Stylesheet } from './stylesheet'
1212
import { migrate as migrateTemplate } from './template/migrate'
@@ -112,6 +112,13 @@ async function run() {
112112
.filter((result) => result.status === 'fulfilled')
113113
.map((result) => result.value)
114114

115+
// Analyze the stylesheets
116+
try {
117+
await analyzeStylesheets(stylesheets)
118+
} catch (e: unknown) {
119+
error(`${e}`)
120+
}
121+
115122
// Migrate each file
116123
let migrateResults = await Promise.allSettled(
117124
stylesheets.map((sheet) => migrateStylesheet(sheet, config)),

packages/@tailwindcss-upgrade/src/migrate.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import path from 'node:path'
12
import postcss from 'postcss'
23
import type { Config } from 'tailwindcss'
34
import type { DesignSystem } from '../../tailwindcss/src/design-system'
@@ -7,6 +8,7 @@ import { migrateMediaScreen } from './codemods/migrate-media-screen'
78
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
89
import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives'
910
import { Stylesheet } from './stylesheet'
11+
import { resolveCssId } from './utils/resolve'
1012

1113
export interface MigrateOptions {
1214
newPrefix: string | null
@@ -40,3 +42,68 @@ export async function migrate(stylesheet: Stylesheet, options: MigrateOptions) {
4042

4143
await migrateContents(stylesheet, options)
4244
}
45+
46+
export async function analyze(stylesheets: Stylesheet[]) {
47+
let stylesheetsByFile = new Map<string, Stylesheet>()
48+
49+
for (let sheet of stylesheets) {
50+
if (sheet.file) {
51+
stylesheetsByFile.set(sheet.file, sheet)
52+
}
53+
}
54+
55+
// Step 1: Record which `@import` rules point to which stylesheets
56+
// and which stylesheets are parents/children of each other
57+
let processor = postcss([
58+
{
59+
postcssPlugin: 'mark-import-nodes',
60+
AtRule: {
61+
import(node) {
62+
// Find what the import points to
63+
let id = node.params.match(/['"](.*)['"]/)?.[1]
64+
if (!id) return
65+
66+
let basePath = node.source?.input.file
67+
? path.dirname(node.source.input.file)
68+
: process.cwd()
69+
70+
// Resolve the import to a file path
71+
let resolvedPath: string | false
72+
try {
73+
resolvedPath = resolveCssId(id, basePath)
74+
} catch (err) {
75+
console.warn(`Failed to resolve import: ${id}. Skipping.`)
76+
console.error(err)
77+
return
78+
}
79+
80+
if (!resolvedPath) return
81+
82+
// Find the stylesheet pointing to the resolved path
83+
let stylesheet = stylesheetsByFile.get(resolvedPath)
84+
85+
// If it _does not_ exist in stylesheets we don't care and skip it
86+
// this is likely because its in node_modules or a workspace package
87+
// that we don't want to modify
88+
if (!stylesheet) return
89+
90+
let parent = node.source?.input.file
91+
? stylesheetsByFile.get(node.source.input.file)
92+
: undefined
93+
94+
// Connect sheets together in a dependency graph
95+
if (parent) {
96+
stylesheet.parents.add(parent)
97+
parent.children.add(stylesheet)
98+
}
99+
},
100+
},
101+
},
102+
])
103+
104+
for (let sheet of stylesheets) {
105+
if (!sheet.file) continue
106+
107+
await processor.process(sheet.root, { from: sheet.file })
108+
}
109+
}

packages/@tailwindcss-upgrade/src/stylesheet.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,18 @@ import * as postcss from 'postcss'
55

66
export type StylesheetId = string
77

8+
export interface StylesheetConnection {
9+
item: Stylesheet
10+
}
11+
812
export class Stylesheet {
13+
/**
14+
* A unique identifier for this stylesheet
15+
*
16+
* Used to track the stylesheet in PostCSS nodes.
17+
*/
18+
id: StylesheetId
19+
920
/**
1021
* The PostCSS AST that represents this stylesheet.
1122
*/
@@ -18,6 +29,16 @@ export class Stylesheet {
1829
*/
1930
file: string | null = null
2031

32+
/**
33+
* Stylesheets that import this stylesheet.
34+
*/
35+
parents = new Set<StylesheetConnection>()
36+
37+
/**
38+
* Stylesheets that are imported by stylesheet.
39+
*/
40+
children = new Set<StylesheetConnection>()
41+
2142
static async load(filepath: string) {
2243
filepath = path.resolve(process.cwd(), filepath)
2344

@@ -38,14 +59,45 @@ export class Stylesheet {
3859
}
3960

4061
constructor(root: postcss.Root, file?: string) {
62+
this.id = Math.random().toString(36).slice(2)
4163
this.root = root
4264
this.file = file ?? null
4365
}
4466

67+
*ancestors() {
68+
for (let { item } of walkDepth(this, (sheet) => sheet.parents)) {
69+
yield item
70+
}
71+
}
72+
73+
*descendants() {
74+
for (let { item } of walkDepth(this, (sheet) => sheet.children)) {
75+
yield item
76+
}
77+
}
78+
4579
[util.inspect.custom]() {
4680
return {
4781
...this,
4882
root: this.root.toString(),
83+
parents: Array.from(this.parents, (s) => s.item.id),
84+
children: Array.from(this.children, (s) => s.item.id),
85+
}
86+
}
87+
}
88+
89+
function* walkDepth(
90+
value: Stylesheet,
91+
connections: (value: Stylesheet) => Iterable<StylesheetConnection>,
92+
path: StylesheetConnection[] = [],
93+
): Iterable<{ item: Stylesheet; path: StylesheetConnection[] }> {
94+
for (let connection of connections(value)) {
95+
let newPath = [...path, connection]
96+
97+
yield* walkDepth(connection.item, connections, newPath)
98+
yield {
99+
item: connection.item,
100+
path: newPath,
49101
}
50102
}
51103
}

0 commit comments

Comments
 (0)