Skip to content

Commit 9e4b989

Browse files
committed
Extract Stylesheet class
The purpose of the `Stylesheet` class is to collect and hold relevant information about CSS files that can be used for more advanced transformations. Additionally this unlocks two things: - Formatting only happens after all transformations have completed - Files are only written once at the end — which will simplify the introduction of a `--dry-run` option in the future
1 parent adf24e2 commit 9e4b989

File tree

5 files changed

+127
-20
lines changed

5 files changed

+127
-20
lines changed

packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import dedent from 'dedent'
22
import postcss from 'postcss'
33
import { describe, expect, it } from 'vitest'
4+
import { Stylesheet } from '../stylesheet'
45
import { formatNodes } from './format-nodes'
56
import { migrateAtLayerUtilities } from './migrate-at-layer-utilities'
67

78
const css = dedent
89

9-
function migrate(input: string) {
10+
async function migrate(data: string) {
11+
let stylesheet: Stylesheet
12+
13+
stylesheet = await Stylesheet.fromString(data)
14+
1015
return postcss()
1116
.use(migrateAtLayerUtilities())
1217
.use(formatNodes())
13-
.process(input, { from: expect.getState().testPath })
18+
.process(stylesheet.root!, { from: expect.getState().testPath })
1419
.then((result) => result.css)
1520
}
1621

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
22
import dedent from 'dedent'
3+
import postcss from 'postcss'
34
import { expect, it } from 'vitest'
5+
import { formatNodes } from './codemods/format-nodes'
46
import { migrateContents } from './migrate'
57

68
const css = dedent
@@ -13,9 +15,15 @@ let designSystem = await __unstable__loadDesignSystem(
1315
)
1416
let config = { designSystem, userConfig: {}, newPrefix: null }
1517

18+
function migrate(input: string, config: any) {
19+
return migrateContents(input, config, expect.getState().testPath)
20+
.then((result) => postcss([formatNodes()]).process(result.root, result.opts))
21+
.then((result) => result.css)
22+
}
23+
1624
it('should print the input as-is', async () => {
1725
expect(
18-
await migrateContents(
26+
await migrate(
1927
css`
2028
/* above */
2129
.foo/* after */ {
@@ -25,7 +33,6 @@ it('should print the input as-is', async () => {
2533
}
2634
`,
2735
config,
28-
expect.getState().testPath,
2936
),
3037
).toMatchInlineSnapshot(`
3138
"/* above */
@@ -39,7 +46,7 @@ it('should print the input as-is', async () => {
3946

4047
it('should migrate a stylesheet', async () => {
4148
expect(
42-
await migrateContents(
49+
await migrate(
4350
css`
4451
@tailwind base;
4552
@@ -116,7 +123,7 @@ it('should migrate a stylesheet', async () => {
116123

117124
it('should migrate a stylesheet (with imports)', async () => {
118125
expect(
119-
await migrateContents(
126+
await migrate(
120127
css`
121128
@import 'tailwindcss/base';
122129
@import './my-base.css';
@@ -137,7 +144,7 @@ it('should migrate a stylesheet (with imports)', async () => {
137144

138145
it('should migrate a stylesheet (with preceding rules that should be wrapped in an `@layer`)', async () => {
139146
expect(
140-
await migrateContents(
147+
await migrate(
141148
css`
142149
@charset "UTF-8";
143150
@layer foo, bar, baz;
@@ -166,7 +173,7 @@ it('should migrate a stylesheet (with preceding rules that should be wrapped in
166173

167174
it('should keep CSS as-is before existing `@layer` at-rules', async () => {
168175
expect(
169-
await migrateContents(
176+
await migrate(
170177
css`
171178
.foo {
172179
color: blue;

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
#!/usr/bin/env node
22

33
import { globby } from 'globby'
4+
import fs from 'node:fs/promises'
45
import path from 'node:path'
6+
import postcss from 'postcss'
7+
import { formatNodes } from './codemods/format-nodes'
58
import { help } from './commands/help'
69
import { migrate as migrateStylesheet } from './migrate'
710
import { migratePostCSSConfig } from './migrate-postcss'
11+
import { Stylesheet } from './stylesheet'
812
import { migrate as migrateTemplate } from './template/migrate'
913
import { prepareConfig } from './template/prepare-config'
1014
import { args, type Arg } from './utils/args'
@@ -94,8 +98,42 @@ async function run() {
9498
// Ensure we are only dealing with CSS files
9599
files = files.filter((file) => file.endsWith('.css'))
96100

101+
// Analyze the stylesheets
102+
let loadResults = await Promise.allSettled(files.map((filepath) => Stylesheet.load(filepath)))
103+
104+
// Load and parse all stylesheets
105+
for (let result of loadResults) {
106+
if (result.status === 'rejected') {
107+
error(`${result.reason}`)
108+
}
109+
}
110+
111+
let stylesheets = loadResults
112+
.filter((result) => result.status === 'fulfilled')
113+
.map((result) => result.value)
114+
97115
// Migrate each file
98-
await Promise.allSettled(files.map((file) => migrateStylesheet(file, config)))
116+
let migrateResults = await Promise.allSettled(
117+
stylesheets.map((sheet) => migrateStylesheet(sheet, config)),
118+
)
119+
120+
for (let result of migrateResults) {
121+
if (result.status === 'rejected') {
122+
error(`${result.reason}`)
123+
}
124+
}
125+
126+
// Format nodes
127+
for (let sheet of stylesheets) {
128+
await postcss([formatNodes()]).process(sheet.root!, { from: sheet.file! })
129+
}
130+
131+
// Write all files to disk
132+
for (let sheet of stylesheets) {
133+
if (!sheet.file) continue
134+
135+
await fs.writeFile(sheet.file, sheet.root.toString())
136+
}
99137

100138
success('Stylesheet migration complete.')
101139
}
Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,42 @@
1-
import fs from 'node:fs/promises'
2-
import path from 'node:path'
31
import postcss from 'postcss'
42
import type { Config } from 'tailwindcss'
53
import type { DesignSystem } from '../../tailwindcss/src/design-system'
6-
import { formatNodes } from './codemods/format-nodes'
74
import { migrateAtApply } from './codemods/migrate-at-apply'
85
import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities'
96
import { migrateMediaScreen } from './codemods/migrate-media-screen'
107
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
118
import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives'
9+
import { Stylesheet } from './stylesheet'
1210

1311
export interface MigrateOptions {
1412
newPrefix: string | null
1513
designSystem: DesignSystem
1614
userConfig: Config
1715
}
1816

19-
export async function migrateContents(contents: string, options: MigrateOptions, file?: string) {
17+
export async function migrateContents(
18+
stylesheet: Stylesheet | string,
19+
options: MigrateOptions,
20+
file?: string,
21+
) {
22+
if (typeof stylesheet === 'string') {
23+
stylesheet = await Stylesheet.fromString(stylesheet)
24+
stylesheet.file = file ?? null
25+
}
26+
2027
return postcss()
2128
.use(migrateAtApply(options))
2229
.use(migrateMediaScreen(options))
2330
.use(migrateAtLayerUtilities())
2431
.use(migrateMissingLayers())
2532
.use(migrateTailwindDirectives(options))
26-
.use(formatNodes())
27-
.process(contents, { from: file })
28-
.then((result) => result.css)
33+
.process(stylesheet.root, { from: stylesheet.file ?? undefined })
2934
}
3035

31-
export async function migrate(file: string, options: MigrateOptions) {
32-
let fullPath = path.resolve(process.cwd(), file)
33-
let contents = await fs.readFile(fullPath, 'utf-8')
36+
export async function migrate(stylesheet: Stylesheet, options: MigrateOptions) {
37+
if (!stylesheet.file) {
38+
throw new Error('Cannot migrate a stylesheet without a file path')
39+
}
3440

35-
await fs.writeFile(fullPath, await migrateContents(contents, options, fullPath))
41+
await migrateContents(stylesheet, options)
3642
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as fs from 'node:fs/promises'
2+
import * as path from 'node:path'
3+
import * as util from 'node:util'
4+
import * as postcss from 'postcss'
5+
6+
export type StylesheetId = string
7+
8+
export class Stylesheet {
9+
/**
10+
* The PostCSS AST that represents this stylesheet.
11+
*/
12+
root: postcss.Root
13+
14+
/**
15+
* The path to the file that this stylesheet was loaded from.
16+
*
17+
* If this stylesheet was not loaded from a file this will be `null`.
18+
*/
19+
file: string | null = null
20+
21+
static async load(filepath: string) {
22+
filepath = path.resolve(process.cwd(), filepath)
23+
24+
let css = await fs.readFile(filepath, 'utf-8')
25+
let root = postcss.parse(css, { from: filepath })
26+
27+
return new Stylesheet(root, filepath)
28+
}
29+
30+
static async fromString(css: string) {
31+
let root = postcss.parse(css)
32+
33+
return new Stylesheet(root)
34+
}
35+
36+
static async fromRoot(root: postcss.Root, file?: string) {
37+
return new Stylesheet(root, file)
38+
}
39+
40+
constructor(root: postcss.Root, file?: string) {
41+
this.root = root
42+
this.file = file ?? null
43+
}
44+
45+
[util.inspect.custom]() {
46+
return {
47+
...this,
48+
root: this.root.toString(),
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)