Skip to content

Commit b94720a

Browse files
authored
Refactor codemod structure (#17484)
This PR is an internal refactor of the codemods package structure that will make a few follow-up PRs easier. Essentially what happens is: 1. Moved `./src/template/` into `src/codemods/template/` 2. Moved `./src/codemods` into `./src/codemods/css` (because the CSS related codemods already) 3. Moved the migration files for the JS config, PostCSS config and Prettier config into `./src/codemods/config/`. 4. Made filenames with actual migrations consistent by prefixing them with `migrate-`. 5. Made sure that all the migration functions also use `migrate…` When looking at this PR, go commit by commit, it will be easier. In a lot of cases, it's just moving files around but those commits also come with changes to the code just to update the imports. [ci-all]
1 parent 9374647 commit b94720a

File tree

65 files changed

+912
-898
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+912
-898
lines changed

Diff for: packages/@tailwindcss-upgrade/src/migrate-js-config.ts renamed to packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts

+18-15
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,30 @@ import { Scanner } from '@tailwindcss/oxide'
22
import fs from 'node:fs/promises'
33
import path from 'node:path'
44
import { fileURLToPath } from 'node:url'
5-
import { loadModule } from '../../@tailwindcss-node/src/compile'
6-
import defaultTheme from '../../tailwindcss/dist/default-theme'
7-
import { atRule, toCss, type AstNode } from '../../tailwindcss/src/ast'
5+
import { loadModule } from '../../../../@tailwindcss-node/src/compile'
6+
import defaultTheme from '../../../../tailwindcss/dist/default-theme'
7+
import { atRule, toCss, type AstNode } from '../../../../tailwindcss/src/ast'
88
import {
99
keyPathToCssProperty,
1010
themeableValues,
11-
} from '../../tailwindcss/src/compat/apply-config-to-theme'
12-
import { keyframesToRules } from '../../tailwindcss/src/compat/apply-keyframes-to-theme'
13-
import { resolveConfig, type ConfigFile } from '../../tailwindcss/src/compat/config/resolve-config'
14-
import type { ResolvedConfig, ThemeConfig } from '../../tailwindcss/src/compat/config/types'
15-
import { buildCustomContainerUtilityRules } from '../../tailwindcss/src/compat/container'
16-
import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode'
17-
import type { Config } from '../../tailwindcss/src/compat/plugin-api'
18-
import type { DesignSystem } from '../../tailwindcss/src/design-system'
19-
import { escape } from '../../tailwindcss/src/utils/escape'
11+
} from '../../../../tailwindcss/src/compat/apply-config-to-theme'
12+
import { keyframesToRules } from '../../../../tailwindcss/src/compat/apply-keyframes-to-theme'
13+
import {
14+
resolveConfig,
15+
type ConfigFile,
16+
} from '../../../../tailwindcss/src/compat/config/resolve-config'
17+
import type { ResolvedConfig, ThemeConfig } from '../../../../tailwindcss/src/compat/config/types'
18+
import { buildCustomContainerUtilityRules } from '../../../../tailwindcss/src/compat/container'
19+
import { darkModePlugin } from '../../../../tailwindcss/src/compat/dark-mode'
20+
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
21+
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
22+
import { escape } from '../../../../tailwindcss/src/utils/escape'
2023
import {
2124
isValidOpacityValue,
2225
isValidSpacingMultiplier,
23-
} from '../../tailwindcss/src/utils/infer-data-type'
24-
import { findStaticPlugins, type StaticPluginOptions } from './utils/extract-static-plugins'
25-
import { highlight, info, relative } from './utils/renderer'
26+
} from '../../../../tailwindcss/src/utils/infer-data-type'
27+
import { findStaticPlugins, type StaticPluginOptions } from '../../utils/extract-static-plugins'
28+
import { highlight, info, relative } from '../../utils/renderer'
2629

2730
const __filename = fileURLToPath(import.meta.url)
2831
const __dirname = path.dirname(__filename)

Diff for: packages/@tailwindcss-upgrade/src/migrate-postcss.ts renamed to packages/@tailwindcss-upgrade/src/codemods/config/migrate-postcss.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'node:fs/promises'
22
import path from 'node:path'
3-
import { pkg } from './utils/packages'
4-
import { highlight, info, relative, success, warn } from './utils/renderer'
3+
import { pkg } from '../../utils/packages'
4+
import { highlight, info, relative, success, warn } from '../../utils/renderer'
55

66
// Migrates simple PostCSS setups. This is to cover non-dynamic config files
77
// similar to the ones we have all over our docs:

Diff for: packages/@tailwindcss-upgrade/src/migrate-prettier.ts renamed to packages/@tailwindcss-upgrade/src/codemods/config/migrate-prettier.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'node:fs/promises'
22
import path from 'node:path'
3-
import { pkg } from './utils/packages'
4-
import { highlight, success } from './utils/renderer'
3+
import { pkg } from '../../utils/packages'
4+
import { highlight, success } from '../../utils/renderer'
55

66
export async function migratePrettierPlugin(base: string) {
77
let packageJsonPath = path.resolve(base, 'package.json')
+293
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import { isGitIgnored } from 'globby'
2+
import path from 'node:path'
3+
import postcss, { type Result } from 'postcss'
4+
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
5+
import { segment } from '../../../../tailwindcss/src/utils/segment'
6+
import { Stylesheet, type StylesheetConnection } from '../../stylesheet'
7+
import { error, highlight, relative } from '../../utils/renderer'
8+
import { resolveCssId } from '../../utils/resolve'
9+
10+
export async function analyze(stylesheets: Stylesheet[]) {
11+
let isIgnored = await isGitIgnored()
12+
let processingQueue: (() => Promise<Result>)[] = []
13+
let stylesheetsByFile = new DefaultMap<string, Stylesheet | null>((file) => {
14+
// We don't want to process ignored files (like node_modules)
15+
if (isIgnored(file)) {
16+
return null
17+
}
18+
19+
try {
20+
let sheet = Stylesheet.loadSync(file)
21+
22+
// Mutate incoming stylesheets to include the newly discovered sheet
23+
stylesheets.push(sheet)
24+
25+
// Queue up the processing of this stylesheet
26+
processingQueue.push(() => processor.process(sheet.root, { from: sheet.file! }))
27+
28+
return sheet
29+
} catch {
30+
return null
31+
}
32+
})
33+
34+
// Step 1: Record which `@import` rules point to which stylesheets
35+
// and which stylesheets are parents/children of each other
36+
let processor = postcss([
37+
{
38+
postcssPlugin: 'mark-import-nodes',
39+
AtRule: {
40+
import(node) {
41+
// Find what the import points to
42+
let id = node.params.match(/['"](.*)['"]/)?.[1]
43+
if (!id) return
44+
45+
let basePath = node.source?.input.file
46+
? path.dirname(node.source.input.file)
47+
: process.cwd()
48+
49+
// Resolve the import to a file path
50+
let resolvedPath: string | false = false
51+
try {
52+
// We first try to resolve the file as relative to the current file
53+
// to mimic the behavior of `postcss-import` since that's what was
54+
// used to resolve imports in Tailwind CSS v3.
55+
if (id[0] !== '.') {
56+
try {
57+
resolvedPath = resolveCssId(`./${id}`, basePath)
58+
} catch {}
59+
}
60+
61+
if (!resolvedPath) {
62+
resolvedPath = resolveCssId(id, basePath)
63+
}
64+
} catch (err) {
65+
// Import is a URL, we don't want to process these, but also don't
66+
// want to show an error message for them.
67+
if (id.startsWith('http://') || id.startsWith('https://') || id.startsWith('//')) {
68+
return
69+
}
70+
71+
// Something went wrong, we can't resolve the import.
72+
error(
73+
`Failed to resolve import: ${highlight(id)} in ${highlight(relative(node.source?.input.file!, basePath))}. Skipping.`,
74+
{ prefix: '↳ ' },
75+
)
76+
return
77+
}
78+
79+
if (!resolvedPath) return
80+
81+
// Find the stylesheet pointing to the resolved path
82+
let stylesheet = stylesheetsByFile.get(resolvedPath)
83+
84+
// If it _does not_ exist in stylesheets we don't care and skip it
85+
// this is likely because its in node_modules or a workspace package
86+
// that we don't want to modify
87+
if (!stylesheet) return
88+
89+
// Mark the import node with the ID of the stylesheet it points to
90+
// We will use these later to build lookup tables and modify the AST
91+
node.raws.tailwind_destination_sheet_id = stylesheet.id
92+
93+
let parent = node.source?.input.file
94+
? stylesheetsByFile.get(node.source.input.file)
95+
: undefined
96+
97+
let layers: string[] = []
98+
99+
for (let part of segment(node.params, ' ')) {
100+
if (!part.startsWith('layer(')) continue
101+
if (!part.endsWith(')')) continue
102+
103+
layers.push(part.slice(6, -1).trim())
104+
}
105+
106+
// Connect sheets together in a dependency graph
107+
if (parent) {
108+
let meta = { layers }
109+
stylesheet.parents.add({ item: parent, meta })
110+
parent.children.add({ item: stylesheet, meta })
111+
}
112+
},
113+
},
114+
},
115+
])
116+
117+
// Seed the map with all the known stylesheets, and queue up the processing of
118+
// each incoming stylesheet.
119+
for (let sheet of stylesheets) {
120+
if (sheet.file) {
121+
stylesheetsByFile.set(sheet.file, sheet)
122+
processingQueue.push(() => processor.process(sheet.root, { from: sheet.file ?? undefined }))
123+
}
124+
}
125+
126+
// Process all the stylesheets from step 1
127+
while (processingQueue.length > 0) {
128+
let task = processingQueue.shift()!
129+
await task()
130+
}
131+
132+
// ---
133+
134+
let commonPath = process.cwd()
135+
136+
function pathToString(path: StylesheetConnection[]) {
137+
let parts: string[] = []
138+
139+
for (let connection of path) {
140+
if (!connection.item.file) continue
141+
142+
let filePath = connection.item.file.replace(commonPath, '')
143+
let layers = connection.meta.layers.join(', ')
144+
145+
if (layers.length > 0) {
146+
parts.push(`${filePath} (layers: ${layers})`)
147+
} else {
148+
parts.push(filePath)
149+
}
150+
}
151+
152+
return parts.join(' <- ')
153+
}
154+
155+
let lines: string[] = []
156+
157+
for (let sheet of stylesheets) {
158+
if (!sheet.file) continue
159+
160+
let { convertiblePaths, nonConvertiblePaths } = sheet.analyzeImportPaths()
161+
let isAmbiguous = convertiblePaths.length > 0 && nonConvertiblePaths.length > 0
162+
163+
if (!isAmbiguous) continue
164+
165+
sheet.canMigrate = false
166+
167+
let filePath = sheet.file.replace(commonPath, '')
168+
169+
for (let path of convertiblePaths) {
170+
lines.push(`- ${filePath} <- ${pathToString(path)}`)
171+
}
172+
173+
for (let path of nonConvertiblePaths) {
174+
lines.push(`- ${filePath} <- ${pathToString(path)}`)
175+
}
176+
}
177+
178+
if (lines.length === 0) {
179+
let tailwindRootLeafs = new Set<Stylesheet>()
180+
181+
for (let sheet of stylesheets) {
182+
// If the current file already contains `@config`, then we can assume it's
183+
// a Tailwind CSS root file.
184+
sheet.root.walkAtRules('config', () => {
185+
sheet.isTailwindRoot = true
186+
return false
187+
})
188+
if (sheet.isTailwindRoot) continue
189+
190+
// If an `@tailwind` at-rule, or `@import "tailwindcss"` is present,
191+
// then we can assume it's a file where Tailwind CSS might be configured.
192+
//
193+
// However, if 2 or more stylesheets exist with these rules that share a
194+
// common parent, then we want to mark the common parent as the root
195+
// stylesheet instead.
196+
sheet.root.walkAtRules((node) => {
197+
if (
198+
node.name === 'tailwind' ||
199+
(node.name === 'import' && node.params.match(/^["']tailwindcss["']/)) ||
200+
(node.name === 'import' && node.params.match(/^["']tailwindcss\/.*?["']$/))
201+
) {
202+
sheet.isTailwindRoot = true
203+
tailwindRootLeafs.add(sheet)
204+
}
205+
})
206+
}
207+
208+
// Only a single Tailwind CSS root file exists, no need to do anything else.
209+
if (tailwindRootLeafs.size <= 1) {
210+
return
211+
}
212+
213+
// Mark the common parent as the root file
214+
{
215+
// Group each sheet from tailwindRootLeafs by their common parent
216+
let commonParents = new DefaultMap<Stylesheet, Set<Stylesheet>>(() => new Set<Stylesheet>())
217+
218+
// Seed common parents with leafs
219+
for (let sheet of tailwindRootLeafs) {
220+
commonParents.get(sheet).add(sheet)
221+
}
222+
223+
// If any 2 common parents come from the same tree, then all children of
224+
// parent A and parent B will be moved to the parent of parent A and
225+
// parent B. Parent A and parent B will be removed.
226+
let repeat = true
227+
repeat: while (repeat) {
228+
repeat = false
229+
230+
for (let [sheetA, childrenA] of commonParents) {
231+
for (let [sheetB, childrenB] of commonParents) {
232+
if (sheetA === sheetB) continue
233+
234+
// Ancestors from self to root. Reversed order so we find the
235+
// nearest common parent first
236+
//
237+
// Including self because if you compare a sheet with its parent,
238+
// then the parent is still the common sheet between the two. In
239+
// this case, the parent is the root file.
240+
let ancestorsA = [sheetA].concat(Array.from(sheetA.ancestors()).reverse())
241+
let ancestorsB = [sheetB].concat(Array.from(sheetB.ancestors()).reverse())
242+
243+
for (let parentA of ancestorsA) {
244+
for (let parentB of ancestorsB) {
245+
if (parentA !== parentB) continue
246+
247+
// Found the parent
248+
let parent = parentA
249+
250+
commonParents.delete(sheetA)
251+
commonParents.delete(sheetB)
252+
253+
for (let child of childrenA) {
254+
commonParents.get(parent).add(child)
255+
}
256+
257+
for (let child of childrenB) {
258+
commonParents.get(parent).add(child)
259+
}
260+
261+
// Found a common parent between sheet A and sheet B. We can
262+
// stop looking for more common parents between A and B, and
263+
// continue with the next sheet.
264+
repeat = true
265+
continue repeat
266+
}
267+
}
268+
}
269+
}
270+
}
271+
272+
// Mark the common parent as the Tailwind CSS root file, and remove the
273+
// flag from each leaf.
274+
for (let [parent, children] of commonParents) {
275+
parent.isTailwindRoot = true
276+
277+
for (let child of children) {
278+
if (parent === child) continue
279+
280+
child.isTailwindRoot = false
281+
}
282+
}
283+
return
284+
}
285+
}
286+
287+
{
288+
let error = `You have one or more stylesheets that are imported into a utility layer and non-utility layer.\n`
289+
error += `We cannot convert stylesheets under these conditions. Please look at the following stylesheets:\n`
290+
291+
throw new Error(error + lines.join('\n'))
292+
}
293+
}

Diff for: packages/@tailwindcss-upgrade/src/codemods/format-nodes.ts renamed to packages/@tailwindcss-upgrade/src/codemods/css/format-nodes.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import postcss, { type ChildNode, type Plugin, type Root } from 'postcss'
22
import { format, type Options } from 'prettier'
3-
import { walk } from '../utils/walk'
3+
import { walk } from '../../utils/walk'
44

55
const FORMAT_OPTIONS: Options = {
66
parser: 'css',

0 commit comments

Comments
 (0)