Skip to content

Commit 3873a98

Browse files
authored
<Icon />: mdi-react to @mdi/react codemod (#140)
1 parent 6f5faae commit 3873a98

File tree

4 files changed

+152
-0
lines changed

4 files changed

+152
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# `mdi-react` `ExampleIcon` to `@mdi/react` `mdiExample` path codemod
2+
3+
yarn transform --write -t ./packages/transforms/src/mdiIconToMdiPath/mdiIconToMdiPath.ts '/sourcegraph/client/!(wildcard)/src/\*_/_.{ts,tsx}'
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { testCodemod } from '@sourcegraph/codemod-toolkit-ts'
2+
3+
import { mdiIconToMdiPath } from '../mdiIconToMdiPath'
4+
5+
testCodemod('mdiIconToMdiPath', mdiIconToMdiPath, [
6+
{
7+
label: 'handles mdi icons',
8+
initialSource: `
9+
import CloseIcon from 'mdi-react/CloseIcon'
10+
11+
import { Icon } from '@sourcegraph/wildcard'
12+
13+
export const Test = <Icon as={CloseIcon} className="hello" />`,
14+
expectedSource: `
15+
import { mdiClose } from '@mdi/js'
16+
17+
import { Icon } from '@sourcegraph/wildcard'
18+
19+
export const Test = <Icon className="hello" svgPath={mdiClose} />
20+
`,
21+
},
22+
{
23+
label: 'handles custom icons',
24+
initialSource: `
25+
import { Icon } from '@sourcegraph/wildcard'
26+
27+
import CustomIcon from '../CustomIcon'
28+
29+
export const Test = <Icon as={CustomIcon} className="hello" />`,
30+
expectedSource: `
31+
import { Icon } from '@sourcegraph/wildcard'
32+
33+
import CustomIcon from '../CustomIcon'
34+
35+
export const Test = <Icon as={CustomIcon} className="hello" />
36+
`,
37+
},
38+
{
39+
label: 'ignores icons with complex logic',
40+
initialSource: `
41+
import CloseIcon from 'mdi-react/CloseIcon'
42+
import OpenIcon from 'mdi-react/OpenIcon'
43+
44+
import { Icon } from '@sourcegraph/wildcard'
45+
46+
export const Test = <Icon as={CloseIcon} className="hello" />
47+
export const Test2 = <Icon as={isOpen ? CloseIcon : OpenIcon} className="hello" />`,
48+
expectedSource: `
49+
import { mdiClose } from '@mdi/js'
50+
import CloseIcon from 'mdi-react/CloseIcon'
51+
import OpenIcon from 'mdi-react/OpenIcon'
52+
53+
import { Icon } from '@sourcegraph/wildcard'
54+
55+
export const Test = <Icon className="hello" svgPath={mdiClose} />
56+
export const Test2 = <Icon as={isOpen ? CloseIcon : OpenIcon} className="hello" />
57+
`,
58+
expectedManualChangeMessages: [
59+
`
60+
/test.tsx:7:27 - warning: Updating an expression is not supported. Please complete the transform manually
61+
>>> as={isOpen ? CloseIcon : OpenIcon}
62+
`,
63+
],
64+
},
65+
])
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './mdiIconToMdiPath'
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {
2+
runTransform,
3+
getParentUntilOrThrow,
4+
isJsxTagElement,
5+
getImportDeclarationByModuleSpecifier,
6+
} from '@sourcegraph/codemod-toolkit-ts'
7+
8+
/**
9+
* Convert `<Icon as={MdiIcon} />` element to `<Icon svgPath={mdiIconPath} />` component.
10+
*/
11+
export const mdiIconToMdiPath = runTransform(context => {
12+
const { throwManualChangeError } = context
13+
14+
const mdiIconPathsToImport = new Set<string>()
15+
16+
const isMdiReactToken = (token: string): boolean => {
17+
const importDeclaration = getImportDeclarationByModuleSpecifier(context.sourceFile, `mdi-react/${token}`)
18+
return importDeclaration !== undefined
19+
}
20+
21+
return {
22+
JsxAttribute(jsxAttribute) {
23+
const jsxTagElement = getParentUntilOrThrow(jsxAttribute, isJsxTagElement)
24+
if (jsxTagElement.getTagNameNode().getText() !== 'Icon') {
25+
// Not Icon component, so we exit
26+
return
27+
}
28+
29+
const structure = jsxAttribute.getStructure()
30+
if (structure.name !== 'as' || !structure.initializer) {
31+
// Not the 'as' prop or empty, so we exit
32+
return
33+
}
34+
35+
if (structure.initializer.includes(' ')) {
36+
// complex expression, so we exit
37+
throwManualChangeError({
38+
node: jsxAttribute,
39+
message: 'Updating an expression is not supported. Please complete the transform manually',
40+
})
41+
}
42+
43+
// like `{CloseIcon}`
44+
const token = structure.initializer
45+
46+
const iconRegex = /(\w*.)Icon/
47+
if (!token.match(iconRegex)) {
48+
// Not an icon, so we exit
49+
return
50+
}
51+
52+
if (!isMdiReactToken(token.replace('{', '').replace('}', ''))) {
53+
// its a custom icon, we don't need to do anything
54+
return
55+
}
56+
57+
const updatedValue = `mdi${token.replace(iconRegex, '$1')}`.replace('{', '').replace('}', '')
58+
59+
// Store this value so we can import it once finished with this file.
60+
mdiIconPathsToImport.add(updatedValue)
61+
62+
// Add `svgPath={...}` with updated value
63+
jsxTagElement.addAttribute({
64+
name: 'svgPath',
65+
initializer: `{${updatedValue}}`,
66+
})
67+
68+
// Remove `as={...}` with old value
69+
jsxAttribute.remove()
70+
},
71+
SourceFileExit(sourceFile) {
72+
if (mdiIconPathsToImport.size > 0) {
73+
// Add mdiIcon import
74+
sourceFile.addImportDeclaration({
75+
namedImports: [...mdiIconPathsToImport],
76+
moduleSpecifier: '@mdi/js',
77+
})
78+
}
79+
80+
sourceFile.fixUnusedIdentifiers()
81+
},
82+
}
83+
})

0 commit comments

Comments
 (0)