Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Improve test harness
  • Loading branch information
philipp-spiess committed Oct 14, 2024
commit 5f12fa5fabbb57f6cb0458e5b54c9dc099a225d9
143 changes: 104 additions & 39 deletions packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,147 @@
import dedent from 'dedent'
import { expect, test } from 'vitest'
import { describe, expect, test } from 'vitest'
import { extractStaticImportMap, findSimplePlugins } from './extract-static-imports'

const js = dedent

test('extracts different kind of imports from an ESM file', () => {
let extracted = extractStaticImportMap(js`
import plugin1 from './plugin1'
import * as plugin2 from './plugin2'
import plugin6, { plugin3, plugin4, default as plugin5 } from './plugin3'
import plugin8, * as plugin7 from './plugin7'
`)
describe('findSimplePlugins', () => {
test('parses all export styles', () => {
expect(
findSimplePlugins(js`
import plugin1 from './plugin1'
import * as plugin2 from './plugin2'

expect(extracted).toEqual({
plugin1: { module: './plugin1', export: null },
plugin2: { module: './plugin2', export: '*' },
plugin3: { module: './plugin3', export: 'plugin3' },
plugin4: { module: './plugin3', export: 'plugin4' },
plugin5: { module: './plugin3', export: 'default' },
plugin6: { module: './plugin3', export: null },
plugin7: { module: './plugin7', export: '*' },
plugin8: { module: './plugin7', export: null },
})
})
export default {
plugins: [plugin1, plugin2, 'plugin3']
}
`),
).toEqual(['./plugin1', './plugin2', 'plugin3'])

expect(
findSimplePlugins(js`
import plugin1 from './plugin1'
import * as plugin2 from './plugin2'

export default {
plugins: [plugin1, plugin2, 'plugin3']
} as any
`),
).toEqual(['./plugin1', './plugin2', 'plugin3'])

expect(
findSimplePlugins(js`
import plugin1 from './plugin1'
import * as plugin2 from './plugin2'

export default {
plugins: [plugin1, plugin2, 'plugin3']
} satisfies any
`),
).toEqual(['./plugin1', './plugin2', 'plugin3'])

expect(
findSimplePlugins(js`
import plugin1 from './plugin1'
import * as plugin2 from './plugin2'

module.exports = {
plugins: [plugin1, plugin2, 'plugin3']
} as any
`),
).toEqual(['./plugin1', './plugin2', 'plugin3'])

test('find simple plugins', () => {
expect(
findSimplePlugins(js`
expect(
findSimplePlugins(js`
import plugin1 from './plugin1'
import * as plugin2 from './plugin2'

module.exports = {
plugins: [plugin1, plugin2, 'plugin3']
} satisfies any
`),
).toEqual(['./plugin1', './plugin2', 'plugin3'])

expect(
findSimplePlugins(js`
import plugin1 from './plugin1'
import * as plugin2 from './plugin2'

export default {
module.exports = {
plugins: [plugin1, plugin2, 'plugin3']
}
`),
).toEqual(['./plugin1', './plugin2', 'plugin3'])
).toEqual(['./plugin1', './plugin2', 'plugin3'])
})

expect(
findSimplePlugins(js`
test('bails out on inline plugins', () => {
expect(
findSimplePlugins(js`
import plugin1 from './plugin1'

export default {
plugins: [plugin1, () => {} ]
}
`),
).toEqual(null)
).toEqual(null)

expect(
findSimplePlugins(js`
expect(
findSimplePlugins(js`
let plugin1 = () => {}

export default {
plugins: [plugin1]
}
`),
).toEqual(null)
).toEqual(null)
})

expect(
findSimplePlugins(js`
test('bails out on named imports for plugins', () => {
expect(
findSimplePlugins(js`
import {plugin1} from './plugin1'

export default {
plugins: [plugin1]
}
`),
).toEqual(null)
).toEqual(null)
})

expect(
findSimplePlugins(js`
test('returns no plugins if none are exported', () => {
expect(
findSimplePlugins(js`
export default {
plugins: []
}
`),
).toEqual([])
).toEqual([])

expect(
findSimplePlugins(js`
expect(
findSimplePlugins(js`
export default {}
`),
).toEqual([])
).toEqual([])
})
})

describe('extractStaticImportMap', () => {
test('extracts different kind of imports from an ESM file', () => {
let extracted = extractStaticImportMap(js`
import plugin1 from './plugin1'
import * as plugin2 from './plugin2'
import plugin6, { plugin3, plugin4, default as plugin5 } from './plugin3'
import plugin8, * as plugin7 from './plugin7'
`)

expect(extracted).toEqual({
plugin1: { module: './plugin1', export: null },
plugin2: { module: './plugin2', export: '*' },
plugin3: { module: './plugin3', export: 'plugin3' },
plugin4: { module: './plugin3', export: 'plugin4' },
plugin5: { module: './plugin3', export: 'default' },
plugin6: { module: './plugin3', export: null },
plugin7: { module: './plugin7', export: '*' },
plugin8: { module: './plugin7', export: null },
})
})
})
167 changes: 97 additions & 70 deletions packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,103 @@ let parser = new Parser()
parser.setLanguage(TS.typescript)
const treesitter = String.raw

const PLUGINS_QUERY = new Parser.Query(
TS.typescript,
treesitter`
; export default = {}
(export_statement
value: (satisfies_expression (object
(pair
key: (property_identifier) @name (#eq? @name "plugins")
value: (array) @imports
)
))?
value: (as_expression (object
(pair
key: (property_identifier) @name (#eq? @name "plugins")
value: (array) @imports
)
))?
value: (object
(pair
key: (property_identifier) @name (#eq? @name "plugins")
value: (array) @imports
)
)?
)

; module.exports = {}
(expression_statement
(assignment_expression
left: (member_expression) @left (#eq? @left "module.exports")
right: (satisfies_expression (object
(pair
key: (property_identifier) @name (#eq? @name "plugins")
value: (array) @imports
)
))?
right: (as_expression (object
(pair
key: (property_identifier) @name (#eq? @name "plugins")
value: (array) @imports
)
))?
right: (object
(pair
key: (property_identifier) @name (#eq? @name "plugins")
value: (array) @imports
)
)?
)
)

`,
)
export function findSimplePlugins(source: string): string[] | null {
try {
let tree = parser.parse(source)
let root = tree.rootNode

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

let plugins = []
for (let match of captures) {
for (let capture of match.captures) {
if (capture.name !== 'imports') continue

for (let pluginDefinition of capture.node.children) {
if (
pluginDefinition.type === '[' ||
pluginDefinition.type === ']' ||
pluginDefinition.type === ','
)
continue

switch (pluginDefinition.type) {
case 'identifier':
let source = imports[pluginDefinition.text]
if (!source || (source.export !== null && source.export !== '*')) {
return null
}
plugins.push(source.module)
break
case 'string':
plugins.push(pluginDefinition.children[1].text)
break
default:
return null
}
}
}
}
return plugins
} catch (error) {
console.error(error)
return null
}
}

const ESM_IMPORT_QUERY = new Parser.Query(
TS.typescript,
treesitter`
Expand Down Expand Up @@ -69,73 +166,3 @@ export function extractStaticImportMap(source: string) {

return imports
}

const PLUGINS_QUERY = new Parser.Query(
TS.typescript,
treesitter`
(export_statement
value: (satisfies_expression (object
(pair
key: (property_identifier) @name (#eq? @name "plugins")
value: (array) @imports
)
))?
value: (as_expression (object
(pair
key: (property_identifier) @name (#eq? @name "plugins")
value: (array) @imports
)
))?
value: (object
(pair
key: (property_identifier) @name (#eq? @name "plugins")
value: (array) @imports
)
)?
)
`,
)
export function findSimplePlugins(source: string): string[] | null {
try {
let tree = parser.parse(source)
let root = tree.rootNode

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

let plugins = []
for (let match of captures) {
for (let capture of match.captures) {
if (capture.name !== 'imports') continue

for (let pluginDefinition of capture.node.children) {
if (
pluginDefinition.type === '[' ||
pluginDefinition.type === ']' ||
pluginDefinition.type === ','
)
continue

switch (pluginDefinition.type) {
case 'identifier':
let source = imports[pluginDefinition.text]
if (!source || (source.export !== null && source.export !== '*')) {
return null
}
plugins.push(source.module)
break
case 'string':
plugins.push(pluginDefinition.children[1].text)
break
default:
return null
}
}
}
}
return plugins
} catch (error) {
console.error(error)
return null
}
}