Skip to content

Commit ea0dc30

Browse files
Detect static plugins in the JS config and migrate it to CSS
1 parent 51fd7af commit ea0dc30

File tree

8 files changed

+400
-6
lines changed

8 files changed

+400
-6
lines changed

integrations/upgrade/js-config.test.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ test(
88
'package.json': json`
99
{
1010
"dependencies": {
11+
"@tailwindcss/typography": "^0.5.15",
1112
"@tailwindcss/upgrade": "workspace:^"
1213
}
1314
}
1415
`,
1516
'tailwind.config.ts': ts`
1617
import { type Config } from 'tailwindcss'
1718
import defaultTheme from 'tailwindcss/defaultTheme'
19+
import typography from '@tailwindcss/typography'
20+
import customPlugin from './custom-plugin'
1821
19-
module.exports = {
22+
export default {
2023
darkMode: 'selector',
2124
content: ['./src/**/*.{html,js}', './my-app/**/*.{html,js}'],
2225
theme: {
@@ -50,9 +53,15 @@ test(
5053
},
5154
},
5255
},
53-
plugins: [],
56+
plugins: [typography, customPlugin],
5457
} satisfies Config
5558
`,
59+
'custom-plugin.js': ts`
60+
export default function ({ addVariant }) {
61+
addVariant('inverted', '@media (inverted-colors: inverted)')
62+
addVariant('hocus', ['&:focus', '&:hover'])
63+
}
64+
`,
5665
'src/input.css': css`
5766
@tailwind base;
5867
@tailwind components;
@@ -71,6 +80,9 @@ test(
7180
@source './**/*.{html,js}';
7281
@source '../my-app/**/*.{html,js}';
7382
83+
@plugin '@tailwindcss/typography';
84+
@plugin '../custom-plugin';
85+
7486
@variant dark (&:where(.dark, .dark *));
7587
7688
@theme {
@@ -101,3 +113,52 @@ test(
101113
expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('')
102114
},
103115
)
116+
117+
test(
118+
`does not upgrade a complex JS config file to CSS`,
119+
{
120+
fs: {
121+
'package.json': json`
122+
{
123+
"dependencies": {
124+
"@tailwindcss/upgrade": "workspace:^"
125+
}
126+
}
127+
`,
128+
'tailwind.config.ts': ts`
129+
import { type Config } from 'tailwindcss'
130+
131+
export default {
132+
plugins: [function complexConfig() {}],
133+
} satisfies Config
134+
`,
135+
'src/input.css': css`
136+
@tailwind base;
137+
@tailwind components;
138+
@tailwind utilities;
139+
`,
140+
},
141+
},
142+
async ({ exec, fs }) => {
143+
await exec('npx @tailwindcss/upgrade')
144+
145+
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
146+
"
147+
--- src/input.css ---
148+
@import 'tailwindcss';
149+
@config '../tailwind.config.ts';
150+
"
151+
`)
152+
153+
expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(`
154+
"
155+
--- tailwind.config.ts ---
156+
import { type Config } from 'tailwindcss'
157+
158+
export default {
159+
plugins: [function complexConfig() {}],
160+
} satisfies Config
161+
"
162+
`)
163+
},
164+
)

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: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { deepMerge } from '../../tailwindcss/src/compat/config/deep-merge'
1111
import { mergeThemeExtension } from '../../tailwindcss/src/compat/config/resolve-config'
1212
import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode'
13+
import { findSimplePlugins } from './utils/extract-static-imports'
1314
import { info } from './utils/renderer'
1415

1516
const __filename = fileURLToPath(import.meta.url)
@@ -19,6 +20,7 @@ export type JSConfigMigration =
1920
// Could not convert the config file, need to inject it as-is in a @config directive
2021
null | {
2122
sources: { base: string; pattern: string }[]
23+
plugins: { base: string; path: string }[]
2224
css: string
2325
}
2426

@@ -39,6 +41,7 @@ export async function migrateJsConfig(
3941
}
4042

4143
let sources: { base: string; pattern: string }[] = []
44+
let plugins: { base: string; path: string }[] = []
4245
let cssConfigs: string[] = []
4346

4447
if ('darkMode' in unresolvedConfig) {
@@ -53,8 +56,17 @@ export async function migrateJsConfig(
5356
cssConfigs.push(await migrateTheme(unresolvedConfig as any))
5457
}
5558

59+
let simplePlugins = findSimplePlugins(source)
60+
console.log(simplePlugins)
61+
if (simplePlugins !== null) {
62+
for (let plugin of simplePlugins) {
63+
plugins.push({ base, path: plugin })
64+
}
65+
}
66+
5667
return {
5768
sources,
69+
plugins,
5870
css: cssConfigs.join('\n'),
5971
}
6072
}
@@ -158,7 +170,9 @@ function isSimpleConfig(unresolvedConfig: Config, source: string): boolean {
158170
}
159171
return ['string', 'number', 'boolean', 'undefined'].includes(typeof value)
160172
}
161-
if (!isSimpleValue(unresolvedConfig)) {
173+
// Plugins can be complex, we have a special heuristics for them!
174+
let { plugins, ...remainder } = unresolvedConfig
175+
if (!isSimpleValue(remainder)) {
162176
return false
163177
}
164178

@@ -174,7 +188,7 @@ function isSimpleConfig(unresolvedConfig: Config, source: string): boolean {
174188
if (Object.keys(unresolvedConfig).some((key) => !knownProperties.includes(key))) {
175189
return false
176190
}
177-
if (unresolvedConfig.plugins && unresolvedConfig.plugins.length > 0) {
191+
if (findSimplePlugins(source) === null) {
178192
return false
179193
}
180194
if (unresolvedConfig.presets && unresolvedConfig.presets.length > 0) {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import dedent from 'dedent'
2+
import { expect, test } from 'vitest'
3+
import { extractStaticImportMap, findSimplePlugins } from './extract-static-imports'
4+
5+
const js = dedent
6+
7+
test('extracts different kind of imports from a source file', () => {
8+
let extracted = extractStaticImportMap(js`
9+
import plugin1 from './plugin1'
10+
// import * as plugin2 from './plugin2'
11+
import plugin6, { plugin3, plugin4, default as plugin5 } from './plugin3'
12+
// import * as plugin7, { plugin8, foo as plugin9 } from './plugin7'
13+
14+
// const plugin6 = require('plugin6')
15+
// const {plugin7} = require('plugin7')
16+
// const {foo: plugin8} = require('plugin8')
17+
// let plugin9 = require('plugin9')
18+
// let {plugin10} = require('plugin10')
19+
// let {foo: plugin11} = require('plugin11')
20+
// let plugin12 = require('plugin12')
21+
// let {plugin13} = require('plugin13')
22+
// let {foo: plugin14} = require('plugin14')
23+
`)
24+
25+
expect(extracted).toEqual({
26+
plugin1: { module: './plugin1', export: null },
27+
// plugin2: { module: './plugin2', export: 'plugin2' },
28+
plugin3: { module: './plugin3', export: 'plugin3' },
29+
plugin4: { module: './plugin3', export: 'plugin4' },
30+
plugin5: { module: './plugin3', export: 'default' },
31+
plugin6: { module: './plugin3', export: null },
32+
33+
// plugin6: { module: 'plugin6', export: null },
34+
// plugin7: { module: 'plugin7', export: 'plugin7' },
35+
// plugin8: { module: 'plugin8', export: 'foo' },
36+
// plugin9: { module: 'plugin9', export: null },
37+
// plugin10: { module: 'plugin10', export: 'plugin10' },
38+
// plugin11: { module: 'plugin11', export: 'foo' },
39+
// plugin12: { module: 'plugin12', export: null },
40+
// plugin13: { module: 'plugin13', export: 'plugin13' },
41+
// plugin14: { module: 'plugin14', export: 'foo' },
42+
})
43+
})
44+
45+
test('find simple plugins', () => {
46+
expect(
47+
findSimplePlugins(js`
48+
import plugin1 from './plugin1'
49+
50+
export default {
51+
plugins: [plugin1, 'plugin2']
52+
}
53+
`),
54+
).toEqual(['./plugin1', 'plugin2'])
55+
56+
expect(
57+
findSimplePlugins(js`
58+
import plugin1 from './plugin1'
59+
60+
export default {
61+
plugins: [plugin1, () => {} ]
62+
}
63+
`),
64+
).toEqual(null)
65+
66+
expect(
67+
findSimplePlugins(js`
68+
import {plugin1} from './plugin1'
69+
70+
export default {
71+
plugins: [plugin1]
72+
}
73+
`),
74+
).toEqual(null)
75+
76+
expect(
77+
findSimplePlugins(js`
78+
export default {
79+
plugins: []
80+
}
81+
`),
82+
).toEqual([])
83+
84+
expect(
85+
findSimplePlugins(js`
86+
export default {}
87+
`),
88+
).toEqual([])
89+
})

0 commit comments

Comments
 (0)