Skip to content

Commit c0e6536

Browse files
Include simple config objects when extracting static plugins
1 parent 4a4be27 commit c0e6536

File tree

3 files changed

+238
-18
lines changed

3 files changed

+238
-18
lines changed

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ export async function migrateJsConfig(
4545
}
4646

4747
let sources: { base: string; pattern: string }[] = []
48-
let plugins: { base: string; path: string }[] = []
48+
let plugins: { base: string; path: string; options: null | Record<string, number | string> }[] =
49+
[]
4950
let cssConfigs: string[] = []
5051

5152
if ('darkMode' in unresolvedConfig) {
@@ -63,8 +64,8 @@ export async function migrateJsConfig(
6364

6465
let simplePlugins = findStaticPlugins(source)
6566
if (simplePlugins !== null) {
66-
for (let plugin of simplePlugins) {
67-
plugins.push({ base, path: plugin })
67+
for (let [path, options] of simplePlugins) {
68+
plugins.push({ base, path, options })
6869
}
6970
}
7071

packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ describe('findStaticPlugins', () => {
1515
plugins: [plugin1, plugin2, 'plugin3', require('./plugin4')]
1616
}
1717
`),
18-
).toEqual(['./plugin1', './plugin2', 'plugin3', './plugin4'])
18+
).toEqual([
19+
['./plugin1', null],
20+
['./plugin2', null],
21+
['plugin3', null],
22+
['./plugin4', null],
23+
])
1924

2025
expect(
2126
findStaticPlugins(js`
@@ -26,7 +31,12 @@ describe('findStaticPlugins', () => {
2631
plugins: [plugin1, plugin2, 'plugin3', require('./plugin4')]
2732
} as any
2833
`),
29-
).toEqual(['./plugin1', './plugin2', 'plugin3', './plugin4'])
34+
).toEqual([
35+
['./plugin1', null],
36+
['./plugin2', null],
37+
['plugin3', null],
38+
['./plugin4', null],
39+
])
3040

3141
expect(
3242
findStaticPlugins(js`
@@ -37,7 +47,12 @@ describe('findStaticPlugins', () => {
3747
plugins: [plugin1, plugin2, 'plugin3', require('./plugin4')]
3848
} satisfies any
3949
`),
40-
).toEqual(['./plugin1', './plugin2', 'plugin3', './plugin4'])
50+
).toEqual([
51+
['./plugin1', null],
52+
['./plugin2', null],
53+
['plugin3', null],
54+
['./plugin4', null],
55+
])
4156

4257
expect(
4358
findStaticPlugins(js`
@@ -47,7 +62,11 @@ describe('findStaticPlugins', () => {
4762
plugins: [plugin1, 'plugin2', require('./plugin3')]
4863
} as any
4964
`),
50-
).toEqual(['./plugin1', 'plugin2', './plugin3'])
65+
).toEqual([
66+
['./plugin1', null],
67+
['plugin2', null],
68+
['./plugin3', null],
69+
])
5170

5271
expect(
5372
findStaticPlugins(js`
@@ -57,7 +76,11 @@ describe('findStaticPlugins', () => {
5776
plugins: [plugin1, 'plugin2', require('./plugin3')]
5877
} satisfies any
5978
`),
60-
).toEqual(['./plugin1', 'plugin2', './plugin3'])
79+
).toEqual([
80+
['./plugin1', null],
81+
['plugin2', null],
82+
['./plugin3', null],
83+
])
6184

6285
expect(
6386
findStaticPlugins(js`
@@ -67,7 +90,41 @@ describe('findStaticPlugins', () => {
6790
plugins: [plugin1, 'plugin2', require('./plugin3')]
6891
}
6992
`),
70-
).toEqual(['./plugin1', 'plugin2', './plugin3'])
93+
).toEqual([
94+
['./plugin1', null],
95+
['plugin2', null],
96+
['./plugin3', null],
97+
])
98+
})
99+
100+
test('can extract plugin options', () => {
101+
expect(
102+
findStaticPlugins(js`
103+
import plugin1 from './plugin1'
104+
import * as plugin2 from './plugin2'
105+
106+
export default {
107+
plugins: [
108+
plugin1({
109+
foo: 'bar',
110+
num: 19,
111+
}),
112+
plugin2({
113+
foo: 'bar',
114+
num: 19,
115+
}),
116+
require('./plugin3')({
117+
foo: 'bar',
118+
num: 19,
119+
}),
120+
]
121+
}
122+
`),
123+
).toEqual([
124+
['./plugin1', { foo: 'bar', num: 19 }],
125+
['./plugin2', { foo: 'bar', num: 19 }],
126+
['./plugin3', { foo: 'bar', num: 19 }],
127+
])
71128
})
72129

73130
test('bails out on inline plugins', () => {
@@ -134,6 +191,72 @@ describe('findStaticPlugins', () => {
134191
).toEqual(null)
135192
})
136193

194+
test.only('bails on invalid plugin options', () => {
195+
expect(
196+
findStaticPlugins(js`
197+
import plugin from './plugin'
198+
199+
export default {
200+
plugins: [
201+
plugin({ foo }),
202+
]
203+
}
204+
`),
205+
).toEqual(null)
206+
207+
expect(
208+
findStaticPlugins(js`
209+
import plugin from './plugin'
210+
211+
export default {
212+
plugins: [
213+
plugin(),
214+
]
215+
}
216+
`),
217+
).toEqual(null)
218+
219+
expect(
220+
findStaticPlugins(js`
221+
import plugin from './plugin'
222+
223+
export default {
224+
plugins: [
225+
plugin({ foo: { bar: 2 } }),
226+
]
227+
}
228+
`),
229+
).toEqual(null)
230+
231+
expect(
232+
findStaticPlugins(js`
233+
import plugin from './plugin'
234+
235+
const OPTIONS = { foo: 1 }
236+
237+
export default {
238+
plugins: [
239+
plugin(OPTIONS),
240+
]
241+
}
242+
`),
243+
).toEqual(null)
244+
245+
expect(
246+
findStaticPlugins(js`
247+
import plugin from './plugin'
248+
249+
let something = 1
250+
251+
export default {
252+
plugins: [
253+
plugin({ foo: something }),
254+
]
255+
}
256+
`),
257+
).toEqual(null)
258+
})
259+
137260
test('returns no plugins if none are exported', () => {
138261
expect(
139262
findStaticPlugins(js`

packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ let parser = new Parser()
55
parser.setLanguage(TS.typescript)
66
const treesitter = String.raw
77

8+
// Extract `plugins` property of the object export for both ESM and CJS files
89
const PLUGINS_QUERY = new Parser.Query(
910
TS.typescript,
1011
treesitter`
@@ -56,15 +57,55 @@ const PLUGINS_QUERY = new Parser.Query(
5657
)
5758
`,
5859
)
59-
export function findStaticPlugins(source: string): string[] | null {
60+
61+
// Extract require() calls, as well as identifiers with options or require()
62+
// with options
63+
const PLUGIN_CALL_OPTIONS_QUERY = new Parser.Query(
64+
TS.typescript,
65+
treesitter`
66+
(call_expression
67+
function: (call_expression
68+
function: (identifier) @_name (#eq? @_name "require")
69+
arguments: (arguments
70+
(string (string_fragment) @module_string)
71+
)
72+
)?
73+
function: (identifier)? @module_identifier
74+
arguments: (arguments
75+
(object
76+
(pair
77+
key: (property_identifier) @property
78+
value: (string (string_fragment) @str_value)?
79+
value: (template_string (string_fragment) @str_value)?
80+
value: (number)? @num_value
81+
82+
; The following properties only match to invalidate the plugin
83+
; options
84+
value: (object)? @invalid
85+
)
86+
)
87+
)
88+
)
89+
(call_expression
90+
function: (identifier) @_name (#eq? @_name "require")
91+
arguments: (arguments
92+
(string (string_fragment) @module_string)
93+
)
94+
)
95+
`,
96+
)
97+
98+
export function findStaticPlugins(
99+
source: string,
100+
): [string, null | Record<string, string | number>][] | null {
60101
try {
61102
let tree = parser.parse(source)
62103
let root = tree.rootNode
63104

64105
let imports = extractStaticImportMap(source)
65106
let captures = PLUGINS_QUERY.matches(root)
66107

67-
let plugins = []
108+
let plugins: [string, null | Record<string, string | number>][] = []
68109
for (let match of captures) {
69110
for (let capture of match.captures) {
70111
if (capture.name !== 'imports') continue
@@ -83,17 +124,71 @@ export function findStaticPlugins(source: string): string[] | null {
83124
if (!source || (source.export !== null && source.export !== '*')) {
84125
return null
85126
}
86-
plugins.push(source.module)
127+
plugins.push([source.module, null])
87128
break
88129
case 'string':
89-
plugins.push(pluginDefinition.children[1].text)
130+
plugins.push([pluginDefinition.children[1].text, null])
90131
break
91132
case 'call_expression':
92-
// allow require('..') calls
93-
if (pluginDefinition.children?.[0]?.text !== 'require') return null
94-
let firstArgument = pluginDefinition.children?.[1]?.children?.[1]?.children?.[1]?.text
95-
if (typeof firstArgument !== 'string') return null
96-
plugins.push(firstArgument)
133+
let matches = PLUGIN_CALL_OPTIONS_QUERY.matches(pluginDefinition)
134+
if (matches.length === 0) return null
135+
136+
let moduleName: string | null = null
137+
let moduleIdentifier: string | null = null
138+
139+
let options: Record<string, string | number> | null = null
140+
let lastProperty: string | null = null
141+
142+
for (let match of matches) {
143+
for (let capture of match.captures) {
144+
switch (capture.name) {
145+
case 'module_identifier': {
146+
moduleIdentifier = capture.node.text
147+
break
148+
}
149+
case 'module_string': {
150+
moduleName = capture.node.text
151+
break
152+
}
153+
case 'property': {
154+
if (lastProperty !== null) return null
155+
lastProperty = capture.node.text
156+
break
157+
}
158+
case 'str_value':
159+
case 'num_value': {
160+
if (lastProperty === null) return null
161+
options ??= {}
162+
options[lastProperty] =
163+
capture.name === 'num_value'
164+
? parseFloat(capture.node.text)
165+
: capture.node.text
166+
lastProperty = null
167+
break
168+
}
169+
case '_name':
170+
break
171+
default:
172+
return null
173+
}
174+
}
175+
}
176+
177+
if (lastProperty !== null) return null
178+
179+
if (moduleIdentifier !== null) {
180+
let source = imports[moduleIdentifier]
181+
if (!source || (source.export !== null && source.export !== '*')) {
182+
return null
183+
}
184+
moduleName = source.module
185+
}
186+
187+
if (moduleName === null) {
188+
return null
189+
}
190+
191+
plugins.push([moduleName, options])
97192
break
98193
default:
99194
return null
@@ -108,6 +203,7 @@ export function findStaticPlugins(source: string): string[] | null {
108203
}
109204
}
110205

206+
// Extract all top-level imports for both ESM and CJS files
111207
const IMPORT_QUERY = new Parser.Query(
112208
TS.typescript,
113209
treesitter`

0 commit comments

Comments
 (0)