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

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.
Files changed (30) hide show
  1. package/dist/_chunks-es/box.mods.js +4 -2
  2. package/dist/_chunks-es/box.mods.js.map +1 -1
  3. package/dist/_chunks-es/layout-mods.js +13 -1
  4. package/dist/_chunks-es/layout-mods.js.map +1 -1
  5. package/dist/_chunks-es/transformComponent.js +51 -22
  6. package/dist/_chunks-es/transformComponent.js.map +1 -1
  7. package/dist/transforms/latest/box/box.js +133 -18
  8. package/dist/transforms/latest/box/box.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/constants/latest/layout-mods.ts +12 -0
  11. package/src/transforms/latest/box/box.test.ts +202 -4
  12. package/src/transforms/latest/box/box.ts +36 -10
  13. package/src/utils/getElementMatchNames.ts +7 -0
  14. package/src/utils/getMappingValue.ts +18 -21
  15. package/src/utils/getNamedExportInit.ts +111 -0
  16. package/src/utils/getStaticAttributeExpression.ts +62 -9
  17. package/src/utils/getStyledComponentAliases.ts +85 -1
  18. package/src/utils/insertTodoWarning.ts +2 -14
  19. package/src/utils/parseModule.ts +29 -0
  20. package/src/utils/replaceElement.test.ts +28 -26
  21. package/src/utils/replaceElement.ts +4 -5
  22. package/src/utils/replaceStyledComponent.test.ts +40 -14
  23. package/src/utils/replaceStyledComponent.ts +2 -5
  24. package/src/utils/resolveRelativeModulePath.ts +37 -0
  25. package/src/utils/shouldTransformComponent.test.ts +14 -0
  26. package/src/utils/shouldTransformComponent.ts +5 -0
  27. package/src/utils/transformAttributes.test.ts +2 -2
  28. package/src/utils/transformStyledComponents.test.ts +80 -14
  29. package/src/utils/transformStyledComponents.ts +7 -16
  30. package/src/utils/getComponentJsxNames.ts +0 -17
@@ -0,0 +1,111 @@
1
+ import type {API, Expression, ExportSpecifier} from 'jscodeshift'
2
+
3
+ import {parseModule} from './parseModule'
4
+ import {resolveRelativeModulePath} from './resolveRelativeModulePath'
5
+
6
+ export type NamedExportInit = {
7
+ init: Expression
8
+ modulePath: string
9
+ }
10
+
11
+ function findVariableInit(
12
+ j: API['jscodeshift'],
13
+ root: ReturnType<API['jscodeshift']>,
14
+ name: string,
15
+ ): Expression | null {
16
+ let init: Expression | null = null
17
+
18
+ root.find(j.VariableDeclarator).forEach((path) => {
19
+ const {id} = path.node
20
+
21
+ if (id.type === 'Identifier' && id.name === name && path.node.init) {
22
+ init = path.node.init
23
+ }
24
+ })
25
+
26
+ return init
27
+ }
28
+
29
+ function getExportSpecifierName(exported: ExportSpecifier['exported']): string | null {
30
+ if (exported.type === 'Identifier' || exported.type === 'JSXIdentifier') {
31
+ return exported.name
32
+ }
33
+
34
+ return null
35
+ }
36
+
37
+ export function getNamedExportInit(
38
+ j: API['jscodeshift'],
39
+ exportName: string,
40
+ filePath: string,
41
+ visited: Set<string> = new Set(),
42
+ ): NamedExportInit | null {
43
+ if (visited.has(filePath)) {
44
+ return null
45
+ }
46
+
47
+ visited.add(filePath)
48
+
49
+ const root = parseModule(j, filePath)
50
+
51
+ if (!root) {
52
+ return null
53
+ }
54
+
55
+ for (const path of root.find(j.ExportNamedDeclaration).paths()) {
56
+ const {declaration, specifiers, source} = path.node
57
+ const reexportSource = typeof source?.value === 'string' ? source.value : null
58
+
59
+ if (declaration?.type === 'VariableDeclaration') {
60
+ for (const declarator of declaration.declarations) {
61
+ if (declarator.type !== 'VariableDeclarator') {
62
+ continue
63
+ }
64
+
65
+ if (
66
+ declarator.id.type === 'Identifier' &&
67
+ declarator.id.name === exportName &&
68
+ declarator.init
69
+ ) {
70
+ return {init: declarator.init, modulePath: filePath}
71
+ }
72
+ }
73
+ }
74
+
75
+ for (const spec of specifiers ?? []) {
76
+ if (spec.type !== 'ExportSpecifier') {
77
+ continue
78
+ }
79
+
80
+ const exported = getExportSpecifierName(spec.exported)
81
+
82
+ if (!exported || exported !== exportName) {
83
+ continue
84
+ }
85
+
86
+ const local = spec.local?.type === 'Identifier' ? spec.local.name : exported
87
+
88
+ if (reexportSource) {
89
+ const resolvedPath = resolveRelativeModulePath(filePath, reexportSource)
90
+
91
+ if (resolvedPath) {
92
+ const followed = getNamedExportInit(j, local, resolvedPath, visited)
93
+
94
+ if (followed) {
95
+ return followed
96
+ }
97
+ }
98
+
99
+ continue
100
+ }
101
+
102
+ const init = findVariableInit(j, root, local)
103
+
104
+ if (init) {
105
+ return {init, modulePath: filePath}
106
+ }
107
+ }
108
+ }
109
+
110
+ return null
111
+ }
@@ -1,15 +1,35 @@
1
1
  import type {API, JSXAttribute, JSXSpreadAttribute} from 'jscodeshift'
2
2
 
3
+ import type {AnyExpression} from '../types/AnyExpression'
3
4
  import {getAttribute} from './getAttribute'
4
5
  import {getAttributeExpression} from './getAttributeExpression'
5
6
 
6
7
  export type CompositePrimitive = string | number | boolean
7
8
 
9
+ export type StaticAttributeValue = CompositePrimitive | (CompositePrimitive | null | undefined)[]
10
+
11
+ function getStaticPrimitive(expr: AnyExpression): CompositePrimitive | undefined {
12
+ if (
13
+ expr.type === 'StringLiteral' ||
14
+ expr.type === 'NumericLiteral' ||
15
+ expr.type === 'BooleanLiteral' ||
16
+ expr.type === 'Literal'
17
+ ) {
18
+ const {value} = expr
19
+
20
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
21
+ return value
22
+ }
23
+ }
24
+
25
+ return undefined
26
+ }
27
+
8
28
  export function getStaticAttributeExpression(
9
29
  j: API['jscodeshift'],
10
30
  attrs: (JSXAttribute | JSXSpreadAttribute)[],
11
31
  name: string,
12
- ): CompositePrimitive | null {
32
+ ): StaticAttributeValue | null {
13
33
  const attr = getAttribute(attrs, name)
14
34
 
15
35
  if (!attr) {
@@ -22,14 +42,47 @@ export function getStaticAttributeExpression(
22
42
  return null
23
43
  }
24
44
 
25
- if (
26
- expr.type === 'StringLiteral' ||
27
- expr.type === 'NumericLiteral' ||
28
- expr.type === 'BooleanLiteral' ||
29
- expr.type === 'Literal'
30
- ) {
31
- return expr.value as string | number | boolean
45
+ const primitive = getStaticPrimitive(expr as AnyExpression)
46
+
47
+ if (primitive !== undefined) {
48
+ return primitive
49
+ }
50
+
51
+ if (expr.type !== 'ArrayExpression') {
52
+ return null
53
+ }
54
+
55
+ const elements = expr.elements as ((AnyExpression & {name?: string}) | null)[]
56
+ const values: (CompositePrimitive | null | undefined)[] = []
57
+
58
+ for (const element of elements) {
59
+ if (element === null || element.type === 'NullLiteral') {
60
+ values.push(null)
61
+ continue
62
+ }
63
+
64
+ if (element.type === 'SpreadElement') {
65
+ return null
66
+ }
67
+
68
+ if (element.type === 'Literal' && element.value === null) {
69
+ values.push(null)
70
+ continue
71
+ }
72
+
73
+ if (element.type === 'Identifier' && element.name === 'undefined') {
74
+ values.push(undefined)
75
+ continue
76
+ }
77
+
78
+ const elementValue = getStaticPrimitive(element)
79
+
80
+ if (elementValue === undefined) {
81
+ return null
82
+ }
83
+
84
+ values.push(elementValue)
32
85
  }
33
86
 
34
- return null
87
+ return values
35
88
  }
@@ -1,8 +1,13 @@
1
1
  import type {API, Collection} from 'jscodeshift'
2
2
 
3
+ import type {BaseOptions} from '../types/BaseOptions'
4
+ import {getComponentLocalNames} from './getComponentLocalNames'
5
+ import {getNamedExportInit} from './getNamedExportInit'
3
6
  import {getStyledComponentName} from './getStyledComponentName'
7
+ import {parseModule} from './parseModule'
8
+ import {resolveRelativeModulePath} from './resolveRelativeModulePath'
4
9
 
5
- export function getStyledComponentAliases(
10
+ export function getSameFileStyledComponentAliases(
6
11
  j: API['jscodeshift'],
7
12
  root: Collection,
8
13
  localNames: Iterable<string>,
@@ -26,3 +31,82 @@ export function getStyledComponentAliases(
26
31
 
27
32
  return aliases
28
33
  }
34
+
35
+ export function getImportedStyledComponentAliases(
36
+ j: API['jscodeshift'],
37
+ root: Collection,
38
+ componentName: string,
39
+ filePath: string | undefined,
40
+ options?: BaseOptions,
41
+ ): Set<string> {
42
+ const aliases = new Set<string>()
43
+
44
+ if (!filePath) {
45
+ return aliases
46
+ }
47
+
48
+ root.find(j.ImportDeclaration).forEach((path) => {
49
+ const source = path.node.source.value
50
+
51
+ if (typeof source !== 'string') {
52
+ return
53
+ }
54
+
55
+ const resolvedPath = resolveRelativeModulePath(filePath, source)
56
+
57
+ if (!resolvedPath) {
58
+ return
59
+ }
60
+
61
+ for (const spec of path.node.specifiers ?? []) {
62
+ if (spec.type !== 'ImportSpecifier') {
63
+ continue
64
+ }
65
+
66
+ if ('importKind' in spec && spec.importKind === 'type') {
67
+ continue
68
+ }
69
+
70
+ if (spec.imported.type !== 'Identifier') {
71
+ continue
72
+ }
73
+
74
+ const exportName = spec.imported.name
75
+ const localName = spec.local?.type === 'Identifier' ? spec.local.name : exportName
76
+ const namedExport = getNamedExportInit(j, exportName, resolvedPath)
77
+
78
+ if (!namedExport) {
79
+ continue
80
+ }
81
+
82
+ const sourceRoot = parseModule(j, namedExport.modulePath)
83
+
84
+ if (!sourceRoot) {
85
+ continue
86
+ }
87
+
88
+ const exportLocalNames = getComponentLocalNames(j, sourceRoot, componentName, options)
89
+ const baseComponent = getStyledComponentName(namedExport.init)
90
+
91
+ if (baseComponent && exportLocalNames.has(baseComponent)) {
92
+ aliases.add(localName)
93
+ }
94
+ }
95
+ })
96
+
97
+ return aliases
98
+ }
99
+
100
+ export function getStyledComponentAliases(
101
+ j: API['jscodeshift'],
102
+ root: Collection,
103
+ componentName: string,
104
+ filePath: string | undefined,
105
+ localNames: Set<string>,
106
+ options?: BaseOptions,
107
+ ): Set<string> {
108
+ const sameFile = getSameFileStyledComponentAliases(j, root, localNames)
109
+ const imported = getImportedStyledComponentAliases(j, root, componentName, filePath, options)
110
+
111
+ return new Set([...sameFile, ...imported])
112
+ }
@@ -1,10 +1,4 @@
1
- import {
2
- type API,
3
- type ASTPath,
4
- type ImportDeclaration,
5
- type JSXOpeningElement,
6
- type VariableDeclarator,
7
- } from 'jscodeshift'
1
+ import {type API, type ASTPath, type ImportDeclaration, type JSXOpeningElement} from 'jscodeshift'
8
2
 
9
3
  type CommentableNode = {
10
4
  comments?: {type: string; value: string; leading?: boolean}[] | null
@@ -16,19 +10,13 @@ type CommentableNode = {
16
10
  */
17
11
  export function insertTodoWarning(
18
12
  j: API['jscodeshift'],
19
- path: ASTPath<JSXOpeningElement | ImportDeclaration | VariableDeclarator>,
13
+ path: ASTPath<JSXOpeningElement | ImportDeclaration>,
20
14
  warning: string,
21
15
  ): boolean {
22
16
  let target: CommentableNode | null = null
23
17
 
24
18
  if (path.node.type === 'ImportDeclaration') {
25
19
  target = path.node
26
- } else if (path.node.type === 'VariableDeclarator') {
27
- const parent = path.parent
28
-
29
- if (parent?.node.type === 'VariableDeclaration') {
30
- target = parent.node
31
- }
32
20
  } else if (path.parent?.node.type === 'JSXElement') {
33
21
  target = path.parent.node
34
22
  }
@@ -0,0 +1,29 @@
1
+ import {readFileSync} from 'node:fs'
2
+
3
+ import type {API, Collection} from 'jscodeshift'
4
+
5
+ const parseCache = new Map<string, Collection>()
6
+
7
+ export function parseModule(j: API['jscodeshift'], filePath: string): Collection | null {
8
+ const cached = parseCache.get(filePath)
9
+
10
+ if (cached) {
11
+ return cached
12
+ }
13
+
14
+ try {
15
+ const source = readFileSync(filePath, 'utf8')
16
+ const root = j(source)
17
+
18
+ parseCache.set(filePath, root)
19
+
20
+ return root
21
+ } catch {
22
+ return null
23
+ }
24
+ }
25
+
26
+ /** @internal */
27
+ export function clearModuleParseCache(): void {
28
+ parseCache.clear()
29
+ }
@@ -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
 
@@ -290,7 +290,7 @@ defineInlineTest(
290
290
  transform,
291
291
  {},
292
292
  `
293
- <div flex="1" />
293
+ <div flex={1} />
294
294
  `,
295
295
  `
296
296
  <div flexGrow={1} />
@@ -314,7 +314,7 @@ defineInlineTest(
314
314
  transform,
315
315
  {},
316
316
  `
317
- <div flex={["2", "none"]} />
317
+ <div flex={[2, "none"]} />
318
318
  `,
319
319
  `
320
320
  <div