@sanity/ui-codemod 1.0.0-alpha.2 → 1.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,9 +21,21 @@ const TO_MOD: AttributeMods = {
21
21
  },
22
22
  }
23
23
 
24
- function transform(fileInfo: FileInfo, api: API): string {
24
+ function transform(
25
+ fileInfo: FileInfo,
26
+ api: API,
27
+ options?: {
28
+ setLocalNames?: boolean
29
+ unsetLocalNames?: boolean
30
+ },
31
+ ): string {
25
32
  const j = api.jscodeshift
26
33
  const root = j(fileInfo.source)
34
+ const localNames = options?.unsetLocalNames
35
+ ? (new Set() as Set<string>)
36
+ : options?.setLocalNames
37
+ ? getComponentLocalNames(j, root, 'Card')
38
+ : undefined
27
39
 
28
40
  replaceElement(
29
41
  j,
@@ -31,6 +43,7 @@ function transform(fileInfo: FileInfo, api: API): string {
31
43
  (attrs) => !getAttribute(attrs, 'margin'),
32
44
  {
33
45
  element: 'Card',
46
+ ...(localNames !== undefined ? {localNames} : {}),
34
47
  callback: (path) => transformAttributes(j, path, FROM_MOD, 'Warning'),
35
48
  },
36
49
  {
@@ -140,32 +153,9 @@ defineInlineTest(
140
153
  'replaces element and combines imports',
141
154
  )
142
155
 
143
- function transformWithLocalNames(fileInfo: FileInfo, api: API): string {
144
- const j = api.jscodeshift
145
- const root = j(fileInfo.source)
146
- const localNames = getComponentLocalNames(j, root, 'Card')
147
-
148
- replaceElement(
149
- j,
150
- root,
151
- (attrs) => !getAttribute(attrs, 'margin'),
152
- {
153
- element: 'Card',
154
- localNames,
155
- callback: (path) => transformAttributes(j, path, FROM_MOD, 'Warning'),
156
- },
157
- {
158
- element: 'Box',
159
- callback: (path) => transformAttributes(j, path, TO_MOD, 'Warning'),
160
- },
161
- )
162
-
163
- return root.toSource()
164
- }
165
-
166
156
  defineInlineTest(
167
- transformWithLocalNames,
168
- {},
157
+ transform,
158
+ {setLocalNames: true},
169
159
  `
170
160
  import {Card as LegacyCard} from '@sanity/ui'
171
161
 
@@ -179,3 +169,15 @@ defineInlineTest(
179
169
  `,
180
170
  'preserves aliased jsx name and rewrites import',
181
171
  )
172
+
173
+ defineInlineTest(
174
+ transform,
175
+ {unsetLocalNames: true},
176
+ `
177
+ <Card padding={1} />
178
+ `,
179
+ `
180
+ <Card padding={1} />
181
+ `,
182
+ 'does not match bare element when localNames is empty',
183
+ )
@@ -1,11 +1,14 @@
1
1
  import type {API, ASTPath, JSXAttribute, JSXOpeningElement, JSXSpreadAttribute} from 'jscodeshift'
2
2
 
3
3
  import {addImportSpecifier} from './addImportSpecifier'
4
+ import {getElementMatchNames} from './getElementMatchNames'
4
5
  import {removeUnusedImport} from './removeUnusedImport'
5
6
  import {transformImportAlias} from './transformImportAlias'
6
7
 
7
8
  /**
8
9
  * Replaces JSX component if `filter` passes. Runs callbacks and updates imports.
10
+ * If `localNames` is omitted, matches `from.element`. `localNames` may be an empty set,
11
+ * to avoid rewriting unrelated JSX when only cross-file styled aliases are present.
9
12
  * Returns whether the AST was updated.
10
13
  */
11
14
  export function replaceElement(
@@ -22,11 +25,7 @@ export function replaceElement(
22
25
  callback?: (path: ASTPath<JSXOpeningElement>) => boolean | void
23
26
  },
24
27
  ): boolean {
25
- const names = new Set(from.localNames)
26
-
27
- if (names.size === 0) {
28
- names.add(from.element)
29
- }
28
+ const names = getElementMatchNames(from.element, from.localNames)
30
29
 
31
30
  let hasChanges = false
32
31
  let needsDefaultImportUpdate = false
@@ -4,21 +4,31 @@ import {getComponentLocalNames} from './getComponentLocalNames'
4
4
  import {replaceStyledComponent} from './replaceStyledComponent'
5
5
  import {defineInlineTest} from './testUtils'
6
6
 
7
- function transform(fileInfo: FileInfo, api: API): string {
7
+ function transform(
8
+ fileInfo: FileInfo,
9
+ api: API,
10
+ options?: {
11
+ setLocalNames?: boolean
12
+ unsetLocalNames?: boolean
13
+ },
14
+ ): string {
8
15
  const j = api.jscodeshift
9
16
  const root = j(fileInfo.source)
17
+ const localNames = options?.unsetLocalNames
18
+ ? (new Set() as Set<string>)
19
+ : options?.setLocalNames
20
+ ? getComponentLocalNames(j, root, 'Card')
21
+ : undefined
10
22
 
11
- replaceStyledComponent(j, root, {element: 'Card'}, 'Box')
12
-
13
- return root.toSource()
14
- }
15
-
16
- function transformWithLocalNames(fileInfo: FileInfo, api: API): string {
17
- const j = api.jscodeshift
18
- const root = j(fileInfo.source)
19
- const localNames = getComponentLocalNames(j, root, 'Card')
20
-
21
- replaceStyledComponent(j, root, {element: 'Card', localNames}, 'Box')
23
+ replaceStyledComponent(
24
+ j,
25
+ root,
26
+ {
27
+ element: 'Card',
28
+ ...(localNames !== undefined ? {localNames} : {}),
29
+ },
30
+ 'Box',
31
+ )
22
32
 
23
33
  return root.toSource()
24
34
  }
@@ -40,8 +50,8 @@ defineInlineTest(
40
50
  )
41
51
 
42
52
  defineInlineTest(
43
- transformWithLocalNames,
44
- {},
53
+ transform,
54
+ {setLocalNames: true},
45
55
  `
46
56
  import {Card as LegacyCard} from '@sanity/ui'
47
57
 
@@ -55,3 +65,19 @@ defineInlineTest(
55
65
  `,
56
66
  'preserves aliased styled import and rewrites import',
57
67
  )
68
+
69
+ defineInlineTest(
70
+ transform,
71
+ {unsetLocalNames: true},
72
+ `
73
+ import {Card} from '@sanity/ui'
74
+
75
+ const RootCard = styled(Card)(({theme}) => {})
76
+ `,
77
+ `
78
+ import {Card} from '@sanity/ui'
79
+
80
+ const RootCard = styled(Card)(({theme}) => {})
81
+ `,
82
+ 'does not match styled component when localNames is empty',
83
+ )
@@ -1,6 +1,7 @@
1
1
  import type {API} from 'jscodeshift'
2
2
 
3
3
  import {addImportSpecifier} from './addImportSpecifier'
4
+ import {getElementMatchNames} from './getElementMatchNames'
4
5
  import {removeUnusedImport} from './removeUnusedImport'
5
6
  import {transformImportAlias} from './transformImportAlias'
6
7
 
@@ -13,11 +14,7 @@ export function replaceStyledComponent(
13
14
  },
14
15
  to: string,
15
16
  ): void {
16
- const names = new Set(from.localNames)
17
-
18
- if (names.size === 0) {
19
- names.add(from.element)
20
- }
17
+ const names = getElementMatchNames(from.element, from.localNames)
21
18
 
22
19
  let needsDefaultImportUpdate = false
23
20
  const aliases = new Set<string>()
@@ -0,0 +1,37 @@
1
+ import {existsSync} from 'node:fs'
2
+ import {dirname, join, resolve} from 'node:path'
3
+
4
+ const EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js'] as const
5
+
6
+ export function resolveRelativeModulePath(
7
+ fromFilePath: string,
8
+ moduleSpecifier: string,
9
+ ): string | null {
10
+ if (!moduleSpecifier.startsWith('.')) {
11
+ return null
12
+ }
13
+
14
+ const base = resolve(dirname(fromFilePath), moduleSpecifier)
15
+
16
+ for (const ext of EXTENSIONS) {
17
+ const filePath = `${base}${ext}`
18
+
19
+ if (existsSync(filePath)) {
20
+ return filePath
21
+ }
22
+ }
23
+
24
+ for (const ext of EXTENSIONS) {
25
+ const indexPath = join(base, `index${ext}`)
26
+
27
+ if (existsSync(indexPath)) {
28
+ return indexPath
29
+ }
30
+ }
31
+
32
+ if (existsSync(base)) {
33
+ return base
34
+ }
35
+
36
+ return null
37
+ }
@@ -18,6 +18,20 @@ describe('shouldTransformComponent', () => {
18
18
  expect(shouldTransformComponent(j, root, 'Box', new Set())).toBe(false)
19
19
  })
20
20
 
21
+ it('returns true when styled aliases are passed in', () => {
22
+ const root = j(`
23
+ import {RootBox} from './Component.styled'
24
+
25
+ export function Component() {
26
+ return <RootBox />
27
+ }
28
+ `)
29
+
30
+ expect(
31
+ shouldTransformComponent(j, root, 'Box', new Set(), undefined, new Set(['RootBox'])),
32
+ ).toBe(true)
33
+ })
34
+
21
35
  it('returns false when fromPackage and toPackage are the same', () => {
22
36
  const root = j(`
23
37
  import {Box} from '@legacy/ui'
@@ -14,11 +14,16 @@ export function shouldTransformComponent(
14
14
  componentName: string,
15
15
  localNames: Set<string>,
16
16
  options?: BaseOptions,
17
+ styledAliases?: Set<string>,
17
18
  ): boolean {
18
19
  if (localNames.size > 0) {
19
20
  return true
20
21
  }
21
22
 
23
+ if (styledAliases && styledAliases.size > 0) {
24
+ return true
25
+ }
26
+
22
27
  const fromPackage = options?.fromPackage ?? DEFAULT_UI_PACKAGE
23
28
  const toPackage = options?.toPackage ?? DEFAULT_UI_PACKAGE
24
29
 
@@ -1,13 +1,20 @@
1
+ import {mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs'
2
+ import {tmpdir} from 'node:os'
3
+ import {join} from 'node:path'
4
+
1
5
  import {type API, type FileInfo} from 'jscodeshift'
6
+ import jscodeshift from 'jscodeshift'
7
+ import {afterEach, describe, expect, it} from 'vitest'
2
8
 
3
9
  import {getAttribute} from './getAttribute'
4
10
  import {getComponentLocalNames} from './getComponentLocalNames'
5
11
  import {getStyledComponentAliases} from './getStyledComponentAliases'
12
+ import {clearModuleParseCache} from './parseModule'
6
13
  import {defineInlineTest} from './testUtils'
7
14
  import {transformAttributes} from './transformAttributes'
8
15
  import {transformStyledComponents} from './transformStyledComponents'
9
16
 
10
- const DEFAULT_WARNING = 'Please double check styled-component migration(s) below'
17
+ const DEFAULT_WARNING = 'Please double check styled-component migration below'
11
18
  const CUSTOM_WARNING =
12
19
  'Please double check styled(Card) migration — update to styled(Box) manually if needed'
13
20
 
@@ -22,7 +29,7 @@ function transform(
22
29
  const j = api.jscodeshift
23
30
  const root = j(fileInfo.source)
24
31
  const localNames = getComponentLocalNames(j, root, 'Card')
25
- const styledAliases = getStyledComponentAliases(j, root, localNames)
32
+ const styledAliases = getStyledComponentAliases(j, root, 'Card', fileInfo.path, localNames)
26
33
 
27
34
  transformStyledComponents(j, root, styledAliases, (attrs) => !!getAttribute(attrs, 'margin'), {
28
35
  warning: options?.warning,
@@ -111,13 +118,15 @@ defineInlineTest(
111
118
  import {Card} from '@sanity/ui'
112
119
 
113
120
  function Example() {
114
- // UI-CODEMOD TODO: ${DEFAULT_WARNING}
115
- const RootCard = styled(Card)(({theme}) => ({}));
121
+ const RootCard = styled(Card)(({theme}) => ({}))
116
122
 
117
- return <RootCard padding={1} />
123
+ return (
124
+ // UI-CODEMOD TODO: ${DEFAULT_WARNING}
125
+ <RootCard padding={1} />
126
+ );
118
127
  }
119
128
  `,
120
- 'adds todo warning on styled definition when filter does not match',
129
+ 'adds todo warning on styled component jsx instances when filter does not match',
121
130
  )
122
131
 
123
132
  defineInlineTest(
@@ -136,13 +145,15 @@ defineInlineTest(
136
145
  import {Card} from '@sanity/ui'
137
146
 
138
147
  function Example() {
139
- // UI-CODEMOD TODO: ${CUSTOM_WARNING}
140
- const RootCard = styled(Card)(({theme}) => ({}));
148
+ const RootCard = styled(Card)(({theme}) => ({}))
141
149
 
142
- return <RootCard padding={1} />
150
+ return (
151
+ // UI-CODEMOD TODO: ${CUSTOM_WARNING}
152
+ <RootCard padding={1} />
153
+ );
143
154
  }
144
155
  `,
145
- 'adds custom todo warning on styled definition when filter does not match',
156
+ 'adds custom todo warning on styled component jsx instances when filter does not match',
146
157
  )
147
158
 
148
159
  defineInlineTest(
@@ -152,21 +163,76 @@ defineInlineTest(
152
163
  import {Card} from '@sanity/ui'
153
164
 
154
165
  function Example() {
155
- // UI-CODEMOD TODO: ${DEFAULT_WARNING}
156
166
  const RootCard = styled(Card)(({theme}) => ({}))
157
167
 
158
- return <RootCard padding={1} />
168
+ return (
169
+ // UI-CODEMOD TODO: ${DEFAULT_WARNING}
170
+ <RootCard padding={1} />
171
+ );
159
172
  }
160
173
  `,
161
174
  `
162
175
  import {Card} from '@sanity/ui'
163
176
 
164
177
  function Example() {
165
- // UI-CODEMOD TODO: ${DEFAULT_WARNING}
166
178
  const RootCard = styled(Card)(({theme}) => ({}))
167
179
 
168
- return <RootCard padding={1} />
180
+ return (
181
+ // UI-CODEMOD TODO: ${DEFAULT_WARNING}
182
+ <RootCard padding={1} />
183
+ );
169
184
  }
170
185
  `,
171
186
  'does not duplicate todo warning',
172
187
  )
188
+
189
+ describe('imported styled aliases', () => {
190
+ const j = jscodeshift
191
+ const tempDirs: string[] = []
192
+
193
+ afterEach(() => {
194
+ clearModuleParseCache()
195
+
196
+ for (const dir of tempDirs.splice(0)) {
197
+ rmSync(dir, {recursive: true, force: true})
198
+ }
199
+ })
200
+
201
+ it('adds todo warning on styled component jsx instances when filter does not match', () => {
202
+ const dir = mkdtempSync(join(tmpdir(), 'ui-codemod-styled-crossfile-'))
203
+
204
+ tempDirs.push(dir)
205
+
206
+ writeFileSync(
207
+ join(dir, 'Component.styled.tsx'),
208
+ `
209
+ import {Card} from '@sanity/ui'
210
+
211
+ export const RootCard = styled(Card)(({theme}) => ({}))
212
+ `,
213
+ )
214
+
215
+ writeFileSync(
216
+ join(dir, 'Component.tsx'),
217
+ `
218
+ import {RootCard} from './Component.styled'
219
+
220
+ export function Component() {
221
+ return <RootCard padding={1} />
222
+ }
223
+ `,
224
+ )
225
+
226
+ const importerPath = join(dir, 'Component.tsx')
227
+ const root = j(readFileSync(importerPath, 'utf8'))
228
+ const localNames = getComponentLocalNames(j, root, 'Card')
229
+ const styledAliases = getStyledComponentAliases(j, root, 'Card', importerPath, localNames)
230
+
231
+ transformStyledComponents(j, root, styledAliases, (attrs) => !!getAttribute(attrs, 'margin'), {
232
+ warning: DEFAULT_WARNING,
233
+ })
234
+
235
+ expect(root.toSource()).toContain(`UI-CODEMOD TODO: ${DEFAULT_WARNING}`)
236
+ expect(root.toSource()).not.toContain('const RootCard = styled(Card)')
237
+ })
238
+ })
@@ -2,11 +2,11 @@ import type {API, ASTPath, JSXAttribute, JSXOpeningElement, JSXSpreadAttribute}
2
2
 
3
3
  import {insertTodoWarning} from './insertTodoWarning'
4
4
 
5
- const DEFAULT_WARNING = 'Please double check styled-component migration(s) below'
5
+ const DEFAULT_WARNING = 'Please double check styled-component migration below'
6
6
 
7
7
  /**
8
8
  * Runs callback on JSX styled-component if `filter` passes. Otherwise, adds
9
- * a TODO on the styled definition if manual review is needed.
9
+ * a TODO on the component JSX instance when manual review is needed.
10
10
  * Returns whether the AST was updated.
11
11
  */
12
12
  export function transformStyledComponents(
@@ -21,7 +21,6 @@ export function transformStyledComponents(
21
21
  ): boolean {
22
22
  const names = new Set(aliases)
23
23
  const {callback, warning = DEFAULT_WARNING} = options
24
- const aliasesToWarn = new Set<string>()
25
24
  let hasChanges = false
26
25
 
27
26
  root.find(j.JSXOpeningElement).forEach((path) => {
@@ -34,23 +33,15 @@ export function transformStyledComponents(
34
33
  const attrs = path.node.attributes ?? []
35
34
 
36
35
  if (!filter(attrs)) {
37
- aliasesToWarn.add(name.name)
38
- }
39
-
40
- if (filter(attrs)) {
41
- if (callback?.(path)) {
36
+ if (insertTodoWarning(j, path, warning)) {
42
37
  hasChanges = true
43
38
  }
44
- }
45
- })
46
39
 
47
- root.find(j.VariableDeclarator).forEach((path) => {
48
- const {id} = path.node
40
+ return
41
+ }
49
42
 
50
- if (id.type === 'Identifier' && aliasesToWarn.has(id.name)) {
51
- if (insertTodoWarning(j, path, warning)) {
52
- hasChanges = true
53
- }
43
+ if (callback?.(path)) {
44
+ hasChanges = true
54
45
  }
55
46
  })
56
47
 
@@ -1,17 +0,0 @@
1
- import type {API, Collection} from 'jscodeshift'
2
-
3
- import type {BaseOptions} from '../types/BaseOptions'
4
- import {getComponentLocalNames} from './getComponentLocalNames'
5
- import {getStyledComponentAliases} from './getStyledComponentAliases'
6
-
7
- export function getComponentJsxNames(
8
- j: API['jscodeshift'],
9
- root: Collection,
10
- componentName: string,
11
- options?: BaseOptions,
12
- ): Set<string> {
13
- const localNames = getComponentLocalNames(j, root, componentName, options)
14
- const styledAliases = getStyledComponentAliases(j, root, localNames)
15
-
16
- return new Set([...localNames, ...styledAliases])
17
- }