Skip to content

Commit 468cb5e

Browse files
Detect and migrate static plugin usages (#14648)
This PR builds on top of the new [JS config to CSS migration](#14651) and extends it to support migrating _static_ plugins. What are _static_ plugins you might ask? Static plugins are plugins where we can statically determine that these are coming from a different file (so there is nothing inside the JS config that creates them). An example for this is this config file: ```js import typographyPlugin from '@tailwindcss/typography' import { type Config } from 'tailwindcss' export default { content: ['./src/**/*.{js,jsx,ts,tsx}'], darkMode: 'selector', plugins: [typographyPlugin], } satisfies Config ``` Here, the `plugins` array only has one element and it is a static import from the `@tailwindcss/typography` module. In this PR we attempt to parse the config file via Tree-sitter to extract the following information from this file: - What are the contents of the `plugins` array - What are statically imported resources from the file We then check if _all_ entries in the `plugins` array are either static resources or _strings_ (something I saw working in some tests but I’m not sure it still does). We migrate the JS config file to CSS if all plugins are static and we can migrate them to CSS `@plugin` calls. ## Todo This will need to be rebased after the updated tests in #14648
1 parent 4b19de3 commit 468cb5e

File tree

9 files changed

+521
-57
lines changed

9 files changed

+521
-57
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- _Upgrade (experimental)_: Migrate v3 PostCSS setups to v4 in some cases ([#14612](https://github.com/tailwindlabs/tailwindcss/pull/14612))
1717
- _Upgrade (experimental)_: Automatically discover JavaScript config files ([#14597](https://github.com/tailwindlabs/tailwindcss/pull/14597))
1818
- _Upgrade (experimental)_: Migrate legacy classes to the v4 alternative ([#14643](https://github.com/tailwindlabs/tailwindcss/pull/14643))
19-
- _Upgrade (experimental)_: Migrate static JS configurations to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639), [#14650](https://github.com/tailwindlabs/tailwindcss/pull/14650))
19+
- _Upgrade (experimental)_: Migrate static JS configurations to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639), [#14650](https://github.com/tailwindlabs/tailwindcss/pull/14650), [#14648](https://github.com/tailwindlabs/tailwindcss/pull/14648))
2020
- _Upgrade (experimental)_: Migrate `@media screen(…)` when running codemods ([#14603](https://github.com/tailwindlabs/tailwindcss/pull/14603))
2121
- _Upgrade (experimental)_: Inject `@config "…"` when a `tailwind.config.{js,ts,…}` is detected ([#14635](https://github.com/tailwindlabs/tailwindcss/pull/14635))
2222
- _Upgrade (experimental)_: Migrate `aria-*`, `data-*`, and `supports-*` variants from arbitrary values to bare values ([#14644](https://github.com/tailwindlabs/tailwindcss/pull/14644))

integrations/upgrade/js-config.test.ts

Lines changed: 47 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -103,29 +103,32 @@ test(
103103
)
104104

105105
test(
106-
'does not upgrade JS config files with functions in the theme config',
106+
'upgrades JS config files with plugins',
107107
{
108108
fs: {
109109
'package.json': json`
110110
{
111111
"dependencies": {
112+
"@tailwindcss/typography": "^0.5.15",
112113
"@tailwindcss/upgrade": "workspace:^"
113114
}
114115
}
115116
`,
116117
'tailwind.config.ts': ts`
117118
import { type Config } from 'tailwindcss'
119+
import typography from '@tailwindcss/typography'
120+
import customPlugin from './custom-plugin'
118121
119122
export default {
120-
theme: {
121-
extend: {
122-
colors: ({ colors }) => ({
123-
gray: colors.neutral,
124-
}),
125-
},
126-
},
123+
plugins: [typography, customPlugin],
127124
} satisfies Config
128125
`,
126+
'custom-plugin.js': ts`
127+
export default function ({ addVariant }) {
128+
addVariant('inverted', '@media (inverted-colors: inverted)')
129+
addVariant('hocus', ['&:focus', '&:hover'])
130+
}
131+
`,
129132
'src/input.css': css`
130133
@tailwind base;
131134
@tailwind components;
@@ -136,35 +139,26 @@ test(
136139
async ({ exec, fs }) => {
137140
await exec('npx @tailwindcss/upgrade')
138141

139-
expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(`
142+
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
140143
"
141144
--- src/input.css ---
142145
@import 'tailwindcss';
143-
@config '../tailwind.config.ts';
146+
147+
@plugin '@tailwindcss/typography';
148+
@plugin '../custom-plugin';
144149
"
145150
`)
146151

147152
expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(`
148153
"
149-
--- tailwind.config.ts ---
150-
import { type Config } from 'tailwindcss'
151154
152-
export default {
153-
theme: {
154-
extend: {
155-
colors: ({ colors }) => ({
156-
gray: colors.neutral,
157-
}),
158-
},
159-
},
160-
} satisfies Config
161155
"
162156
`)
163157
},
164158
)
165159

166160
test(
167-
'does not upgrade JS config files with theme keys contributed to by plugins in the theme config',
161+
'does not upgrade JS config files with functions in the theme config',
168162
{
169163
fs: {
170164
'package.json': json`
@@ -179,13 +173,10 @@ test(
179173
180174
export default {
181175
theme: {
182-
typography: {
183-
DEFAULT: {
184-
css: {
185-
'--tw-prose-body': 'red',
186-
color: 'var(--tw-prose-body)',
187-
},
188-
},
176+
extend: {
177+
colors: ({ colors }) => ({
178+
gray: colors.neutral,
179+
}),
189180
},
190181
},
191182
} satisfies Config
@@ -194,14 +185,13 @@ test(
194185
@tailwind base;
195186
@tailwind components;
196187
@tailwind utilities;
197-
@config '../tailwind.config.ts';
198188
`,
199189
},
200190
},
201191
async ({ exec, fs }) => {
202192
await exec('npx @tailwindcss/upgrade')
203193

204-
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
194+
expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(`
205195
"
206196
--- src/input.css ---
207197
@import 'tailwindcss';
@@ -216,13 +206,10 @@ test(
216206
217207
export default {
218208
theme: {
219-
typography: {
220-
DEFAULT: {
221-
css: {
222-
'--tw-prose-body': 'red',
223-
color: 'var(--tw-prose-body)',
224-
},
225-
},
209+
extend: {
210+
colors: ({ colors }) => ({
211+
gray: colors.neutral,
212+
}),
226213
},
227214
},
228215
} satisfies Config
@@ -232,36 +219,37 @@ test(
232219
)
233220

234221
test(
235-
'does not upgrade JS config files with plugins',
222+
'does not upgrade JS config files with theme keys contributed to by plugins in the theme config',
236223
{
237224
fs: {
238225
'package.json': json`
239226
{
240227
"dependencies": {
241-
"@tailwindcss/typography": "^0.5.15",
242228
"@tailwindcss/upgrade": "workspace:^"
243229
}
244230
}
245231
`,
246232
'tailwind.config.ts': ts`
247233
import { type Config } from 'tailwindcss'
248-
import typography from '@tailwindcss/typography'
249-
import customPlugin from './custom-plugin'
250234
251235
export default {
252-
plugins: [typography, customPlugin],
236+
theme: {
237+
typography: {
238+
DEFAULT: {
239+
css: {
240+
'--tw-prose-body': 'red',
241+
color: 'var(--tw-prose-body)',
242+
},
243+
},
244+
},
245+
},
253246
} satisfies Config
254247
`,
255-
'custom-plugin.js': ts`
256-
export default function ({ addVariant }) {
257-
addVariant('inverted', '@media (inverted-colors: inverted)')
258-
addVariant('hocus', ['&:focus', '&:hover'])
259-
}
260-
`,
261248
'src/input.css': css`
262249
@tailwind base;
263250
@tailwind components;
264251
@tailwind utilities;
252+
@config '../tailwind.config.ts';
265253
`,
266254
},
267255
},
@@ -280,11 +268,18 @@ test(
280268
"
281269
--- tailwind.config.ts ---
282270
import { type Config } from 'tailwindcss'
283-
import typography from '@tailwindcss/typography'
284-
import customPlugin from './custom-plugin'
285271
286272
export default {
287-
plugins: [typography, customPlugin],
273+
theme: {
274+
typography: {
275+
DEFAULT: {
276+
css: {
277+
'--tw-prose-body': 'red',
278+
color: 'var(--tw-prose-body)',
279+
},
280+
},
281+
},
282+
},
288283
} satisfies Config
289284
"
290285
`)

packages/@tailwindcss-upgrade/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
"postcss-selector-parser": "^6.1.2",
4040
"prettier": "^3.3.3",
4141
"string-byte-slice": "^3.0.0",
42-
"tailwindcss": "workspace:^"
42+
"tailwindcss": "workspace:^",
43+
"tree-sitter": "^0.21.1",
44+
"tree-sitter-typescript": "^0.23.0"
4345
},
4446
"devDependencies": {
4547
"@types/node": "catalog:",

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,21 @@ export function migrateConfig(
5555
let absolute = path.resolve(source.base, source.pattern)
5656
css += `@source '${relativeToStylesheet(sheet, absolute)}';\n`
5757
}
58-
5958
if (jsConfigMigration.sources.length > 0) {
6059
css = css + '\n'
6160
}
6261

62+
for (let plugin of jsConfigMigration.plugins) {
63+
let relative =
64+
plugin.path[0] === '.'
65+
? relativeToStylesheet(sheet, path.resolve(plugin.base, plugin.path))
66+
: plugin.path
67+
css += `@plugin '${relative}';\n`
68+
}
69+
if (jsConfigMigration.plugins.length > 0) {
70+
css = css + '\n'
71+
}
72+
6373
cssConfig.append(postcss.parse(css + jsConfigMigration.css))
6474
}
6575

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { deepMerge } from '../../tailwindcss/src/compat/config/deep-merge'
1212
import { mergeThemeExtension } from '../../tailwindcss/src/compat/config/resolve-config'
1313
import type { ThemeConfig } from '../../tailwindcss/src/compat/config/types'
1414
import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode'
15+
import { findStaticPlugins } from './utils/extract-static-plugins'
1516
import { info } from './utils/renderer'
1617

1718
const __filename = fileURLToPath(import.meta.url)
@@ -21,6 +22,7 @@ export type JSConfigMigration =
2122
// Could not convert the config file, need to inject it as-is in a @config directive
2223
null | {
2324
sources: { base: string; pattern: string }[]
25+
plugins: { base: string; path: string }[]
2426
css: string
2527
}
2628

@@ -41,6 +43,7 @@ export async function migrateJsConfig(
4143
}
4244

4345
let sources: { base: string; pattern: string }[] = []
46+
let plugins: { base: string; path: string }[] = []
4447
let cssConfigs: string[] = []
4548

4649
if ('darkMode' in unresolvedConfig) {
@@ -56,8 +59,16 @@ export async function migrateJsConfig(
5659
if (themeConfig) cssConfigs.push(themeConfig)
5760
}
5861

62+
let simplePlugins = findStaticPlugins(source)
63+
if (simplePlugins !== null) {
64+
for (let plugin of simplePlugins) {
65+
plugins.push({ base, path: plugin })
66+
}
67+
}
68+
5969
return {
6070
sources,
71+
plugins,
6172
css: cssConfigs.join('\n'),
6273
}
6374
}
@@ -168,7 +179,9 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean {
168179
return ['string', 'number', 'boolean', 'undefined'].includes(typeof value)
169180
}
170181

171-
if (!isSimpleValue(unresolvedConfig)) {
182+
// Plugins are more complex, so we have a special heuristics for them.
183+
let { plugins, ...remainder } = unresolvedConfig
184+
if (!isSimpleValue(remainder)) {
172185
return false
173186
}
174187

@@ -186,7 +199,7 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean {
186199
return false
187200
}
188201

189-
if (unresolvedConfig.plugins && unresolvedConfig.plugins.length > 0) {
202+
if (findStaticPlugins(source) === null) {
190203
return false
191204
}
192205

0 commit comments

Comments
 (0)