@sanity/ui-codemod 1.0.0-alpha.1

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 (157) hide show
  1. package/bin/run.js +5 -0
  2. package/dist/_chunks-es/addAttribute.js +11 -0
  3. package/dist/_chunks-es/addAttribute.js.map +1 -0
  4. package/dist/_chunks-es/box.mods.js +186 -0
  5. package/dist/_chunks-es/box.mods.js.map +1 -0
  6. package/dist/_chunks-es/flex.mods.js +74 -0
  7. package/dist/_chunks-es/flex.mods.js.map +1 -0
  8. package/dist/_chunks-es/getTransformPath.js +75 -0
  9. package/dist/_chunks-es/getTransformPath.js.map +1 -0
  10. package/dist/_chunks-es/grid.mods.js +110 -0
  11. package/dist/_chunks-es/grid.mods.js.map +1 -0
  12. package/dist/_chunks-es/layout-mods.js +424 -0
  13. package/dist/_chunks-es/layout-mods.js.map +1 -0
  14. package/dist/_chunks-es/transformComponent.js +359 -0
  15. package/dist/_chunks-es/transformComponent.js.map +1 -0
  16. package/dist/commands/latest/box.d.ts +47 -0
  17. package/dist/commands/latest/box.js +17 -0
  18. package/dist/commands/latest/box.js.map +1 -0
  19. package/dist/commands/latest/card.d.ts +47 -0
  20. package/dist/commands/latest/card.js +17 -0
  21. package/dist/commands/latest/card.js.map +1 -0
  22. package/dist/commands/latest/code.d.ts +47 -0
  23. package/dist/commands/latest/code.js +17 -0
  24. package/dist/commands/latest/code.js.map +1 -0
  25. package/dist/commands/latest/container.d.ts +47 -0
  26. package/dist/commands/latest/container.js +17 -0
  27. package/dist/commands/latest/container.js.map +1 -0
  28. package/dist/commands/latest/flex.d.ts +47 -0
  29. package/dist/commands/latest/flex.js +17 -0
  30. package/dist/commands/latest/flex.js.map +1 -0
  31. package/dist/commands/latest/grid.d.ts +47 -0
  32. package/dist/commands/latest/grid.js +17 -0
  33. package/dist/commands/latest/grid.js.map +1 -0
  34. package/dist/commands/latest/heading.d.ts +47 -0
  35. package/dist/commands/latest/heading.js +17 -0
  36. package/dist/commands/latest/heading.js.map +1 -0
  37. package/dist/commands/latest/text.d.ts +47 -0
  38. package/dist/commands/latest/text.js +17 -0
  39. package/dist/commands/latest/text.js.map +1 -0
  40. package/dist/transforms/latest/box/box.d.ts +18 -0
  41. package/dist/transforms/latest/box/box.js +101 -0
  42. package/dist/transforms/latest/box/box.js.map +1 -0
  43. package/dist/transforms/latest/card/card.d.ts +18 -0
  44. package/dist/transforms/latest/card/card.js +618 -0
  45. package/dist/transforms/latest/card/card.js.map +1 -0
  46. package/dist/transforms/latest/code/code.d.ts +18 -0
  47. package/dist/transforms/latest/code/code.js +73 -0
  48. package/dist/transforms/latest/code/code.js.map +1 -0
  49. package/dist/transforms/latest/container/container.d.ts +18 -0
  50. package/dist/transforms/latest/container/container.js +91 -0
  51. package/dist/transforms/latest/container/container.js.map +1 -0
  52. package/dist/transforms/latest/flex/flex.d.ts +18 -0
  53. package/dist/transforms/latest/flex/flex.js +19 -0
  54. package/dist/transforms/latest/flex/flex.js.map +1 -0
  55. package/dist/transforms/latest/grid/grid.d.ts +18 -0
  56. package/dist/transforms/latest/grid/grid.js +19 -0
  57. package/dist/transforms/latest/grid/grid.js.map +1 -0
  58. package/dist/transforms/latest/heading/heading.d.ts +18 -0
  59. package/dist/transforms/latest/heading/heading.js +85 -0
  60. package/dist/transforms/latest/heading/heading.js.map +1 -0
  61. package/dist/transforms/latest/text/text.d.ts +18 -0
  62. package/dist/transforms/latest/text/text.js +88 -0
  63. package/dist/transforms/latest/text/text.js.map +1 -0
  64. package/package.json +68 -0
  65. package/src/baseCommand.ts +74 -0
  66. package/src/commands/latest/box.ts +21 -0
  67. package/src/commands/latest/card.ts +21 -0
  68. package/src/commands/latest/code.ts +21 -0
  69. package/src/commands/latest/container.ts +21 -0
  70. package/src/commands/latest/flex.ts +21 -0
  71. package/src/commands/latest/grid.ts +21 -0
  72. package/src/commands/latest/heading.ts +21 -0
  73. package/src/commands/latest/inline.ts +21 -0
  74. package/src/commands/latest/label.ts +21 -0
  75. package/src/commands/latest/stack.ts +21 -0
  76. package/src/commands/latest/text.ts +21 -0
  77. package/src/constants/latest/layout-mods.ts +422 -0
  78. package/src/transforms/latest/box/box.mods.ts +88 -0
  79. package/src/transforms/latest/box/box.test.ts +204 -0
  80. package/src/transforms/latest/box/box.ts +104 -0
  81. package/src/transforms/latest/card/card.mods.ts +592 -0
  82. package/src/transforms/latest/card/card.test.ts +207 -0
  83. package/src/transforms/latest/card/card.ts +71 -0
  84. package/src/transforms/latest/code/code.mods.ts +57 -0
  85. package/src/transforms/latest/code/code.test.ts +61 -0
  86. package/src/transforms/latest/code/code.ts +53 -0
  87. package/src/transforms/latest/container/container.mods.ts +77 -0
  88. package/src/transforms/latest/container/container.test.ts +105 -0
  89. package/src/transforms/latest/container/container.ts +42 -0
  90. package/src/transforms/latest/flex/flex.mods.ts +73 -0
  91. package/src/transforms/latest/flex/flex.test.ts +80 -0
  92. package/src/transforms/latest/flex/flex.ts +42 -0
  93. package/src/transforms/latest/grid/grid.mods.ts +109 -0
  94. package/src/transforms/latest/grid/grid.test.ts +97 -0
  95. package/src/transforms/latest/grid/grid.ts +42 -0
  96. package/src/transforms/latest/heading/heading.mods.ts +70 -0
  97. package/src/transforms/latest/heading/heading.test.ts +87 -0
  98. package/src/transforms/latest/heading/heading.ts +53 -0
  99. package/src/transforms/latest/inline/inline.mods.ts +65 -0
  100. package/src/transforms/latest/inline/inline.test.ts +57 -0
  101. package/src/transforms/latest/inline/inline.ts +42 -0
  102. package/src/transforms/latest/label/label.mods.ts +72 -0
  103. package/src/transforms/latest/label/label.test.ts +113 -0
  104. package/src/transforms/latest/label/label.ts +67 -0
  105. package/src/transforms/latest/stack/stack.mods.ts +12 -0
  106. package/src/transforms/latest/stack/stack.test.ts +160 -0
  107. package/src/transforms/latest/stack/stack.ts +102 -0
  108. package/src/transforms/latest/text/text.mods.ts +72 -0
  109. package/src/transforms/latest/text/text.test.ts +113 -0
  110. package/src/transforms/latest/text/text.ts +57 -0
  111. package/src/types/AnyExpression.ts +9 -0
  112. package/src/types/AttributeMods.ts +16 -0
  113. package/src/types/BaseOptions.ts +6 -0
  114. package/src/utils/addAttribute.test.ts +43 -0
  115. package/src/utils/addAttribute.ts +26 -0
  116. package/src/utils/addImportSpecifier.test.ts +36 -0
  117. package/src/utils/addImportSpecifier.ts +42 -0
  118. package/src/utils/getAttribute.ts +22 -0
  119. package/src/utils/getAttributeExpression.ts +22 -0
  120. package/src/utils/getAttributeValue.ts +20 -0
  121. package/src/utils/getClonedImportSpecifiers.ts +29 -0
  122. package/src/utils/getComponentJsxNames.ts +17 -0
  123. package/src/utils/getComponentLocalNames.ts +69 -0
  124. package/src/utils/getCompositeAttrValue.ts +39 -0
  125. package/src/utils/getMappingArray.ts +36 -0
  126. package/src/utils/getMappingExpression.ts +22 -0
  127. package/src/utils/getMappingValue.ts +52 -0
  128. package/src/utils/getShorthandAttributes.ts +54 -0
  129. package/src/utils/getSplitImportSpecifiers.ts +40 -0
  130. package/src/utils/getStaticAttributeExpression.ts +35 -0
  131. package/src/utils/getStyleExpression.ts +37 -0
  132. package/src/utils/getStyledComponentAliases.ts +28 -0
  133. package/src/utils/getStyledComponentName.ts +52 -0
  134. package/src/utils/getTransformPath.ts +17 -0
  135. package/src/utils/insertTodoWarning.ts +48 -0
  136. package/src/utils/isImportBindingUsed.ts +53 -0
  137. package/src/utils/isValidStyleType.ts +21 -0
  138. package/src/utils/mergeStyle.ts +64 -0
  139. package/src/utils/removeUnusedImport.test.ts +77 -0
  140. package/src/utils/removeUnusedImport.ts +57 -0
  141. package/src/utils/replaceElement.test.ts +181 -0
  142. package/src/utils/replaceElement.ts +106 -0
  143. package/src/utils/replaceStyledComponent.test.ts +57 -0
  144. package/src/utils/replaceStyledComponent.ts +60 -0
  145. package/src/utils/shouldTransformComponent.test.ts +61 -0
  146. package/src/utils/shouldTransformComponent.ts +40 -0
  147. package/src/utils/testUtils.ts +54 -0
  148. package/src/utils/transformAttributes.test.ts +544 -0
  149. package/src/utils/transformAttributes.ts +248 -0
  150. package/src/utils/transformComponent.test.ts +45 -0
  151. package/src/utils/transformComponent.ts +31 -0
  152. package/src/utils/transformImport.test.ts +175 -0
  153. package/src/utils/transformImport.ts +55 -0
  154. package/src/utils/transformImportAlias.test.ts +40 -0
  155. package/src/utils/transformImportAlias.ts +49 -0
  156. package/src/utils/transformStyledComponents.test.ts +172 -0
  157. package/src/utils/transformStyledComponents.ts +58 -0
@@ -0,0 +1,22 @@
1
+ import {type API} from 'jscodeshift'
2
+
3
+ import type {AnyExpression} from '../types/AnyExpression'
4
+
5
+ export function getMappingExpression(
6
+ j: API['jscodeshift'],
7
+ val: string | boolean | number | undefined,
8
+ ): AnyExpression {
9
+ if (val === undefined) {
10
+ return j.identifier('undefined')
11
+ }
12
+
13
+ if (typeof val === 'boolean') {
14
+ return j.booleanLiteral(val)
15
+ }
16
+
17
+ if (typeof val === 'number') {
18
+ return j.numericLiteral(val)
19
+ }
20
+
21
+ return j.stringLiteral(val)
22
+ }
@@ -0,0 +1,52 @@
1
+ import type {AnyExpression} from '../types/AnyExpression'
2
+ import type {AttributeMapping} from '../types/AttributeMods'
3
+
4
+ export function getMappingValue(mapping: AttributeMapping, expr: AnyExpression) {
5
+ let matchedVal
6
+
7
+ for (const val in mapping) {
8
+ if (expr.value === mapping[val]) {
9
+ matchedVal = mapping[val]
10
+ }
11
+ }
12
+
13
+ if (matchedVal) {
14
+ return matchedVal
15
+ }
16
+
17
+ let key
18
+
19
+ if (
20
+ expr.type === 'NumericLiteral' ||
21
+ expr.type === 'BooleanLiteral' ||
22
+ expr.type === 'StringLiteral' ||
23
+ expr.type === 'Literal'
24
+ ) {
25
+ key = String(expr.value)
26
+ }
27
+
28
+ if (expr.type === 'TemplateLiteral') {
29
+ const expressions = expr.expressions as unknown[]
30
+ const quasis = expr.quasis as {value: {cooked: string | null}}[]
31
+
32
+ if (expressions.length === 0 && quasis.length === 1) {
33
+ key = quasis[0]?.value.cooked ?? null
34
+ }
35
+ }
36
+
37
+ if (!key) {
38
+ return
39
+ }
40
+
41
+ if (Object.prototype.hasOwnProperty.call(mapping, key)) {
42
+ return mapping[key]
43
+ }
44
+
45
+ const number = Number(key)
46
+
47
+ if (!Number.isNaN(number) && Object.prototype.hasOwnProperty.call(mapping, number)) {
48
+ return mapping[number]
49
+ }
50
+
51
+ return
52
+ }
@@ -0,0 +1,54 @@
1
+ import type {API, JSXAttribute} from 'jscodeshift'
2
+
3
+ import type {AnyExpression} from '../types/AnyExpression'
4
+ import type {AttributeMod} from '../types/AttributeMods'
5
+ import {getMappingArray} from './getMappingArray'
6
+ import {getMappingExpression} from './getMappingExpression'
7
+ import {getMappingValue} from './getMappingValue'
8
+
9
+ export function getShorthandAttributes(
10
+ j: API['jscodeshift'],
11
+ expr: AnyExpression,
12
+ mod: AttributeMod,
13
+ ) {
14
+ const attributes: JSXAttribute[] = []
15
+
16
+ if (!('props' in mod)) {
17
+ return []
18
+ }
19
+
20
+ if (expr.type === 'ArrayExpression') {
21
+ for (const prop of mod.props) {
22
+ const styleArray = getMappingArray(j, expr, prop.mapping, true)
23
+
24
+ if (!styleArray) {
25
+ continue
26
+ }
27
+
28
+ attributes.push(
29
+ j.jsxAttribute(j.jsxIdentifier(prop.name), j.jsxExpressionContainer(styleArray)),
30
+ )
31
+ }
32
+ }
33
+
34
+ for (const prop of mod.props) {
35
+ const styleValue = getMappingValue(prop.mapping, expr)
36
+
37
+ if (styleValue === undefined) {
38
+ continue
39
+ }
40
+
41
+ if (typeof styleValue === 'string') {
42
+ attributes.push(j.jsxAttribute(j.jsxIdentifier(prop.name), j.stringLiteral(styleValue)))
43
+ } else {
44
+ attributes.push(
45
+ j.jsxAttribute(
46
+ j.jsxIdentifier(prop.name),
47
+ j.jsxExpressionContainer(getMappingExpression(j, styleValue) as never),
48
+ ),
49
+ )
50
+ }
51
+ }
52
+
53
+ return attributes
54
+ }
@@ -0,0 +1,40 @@
1
+ import type {ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier} from 'jscodeshift'
2
+
3
+ export function getSplitImportSpecifiers(
4
+ specifiers: (ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier)[] = [],
5
+ componentName: string,
6
+ ) {
7
+ const componentNames = new Set([componentName, `${componentName}Props`])
8
+ const specs = {
9
+ componentSpecs: [] as (ImportSpecifier | ImportDefaultSpecifier)[],
10
+ restSpecs: [] as (ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier)[],
11
+ }
12
+
13
+ for (const spec of specifiers) {
14
+ if (spec.type === 'ImportNamespaceSpecifier') {
15
+ specs.restSpecs.push(spec)
16
+ continue
17
+ }
18
+
19
+ if (spec.type === 'ImportSpecifier' || spec.type === 'ImportDefaultSpecifier') {
20
+ let local
21
+
22
+ if (spec.type === 'ImportSpecifier') {
23
+ const loc = spec.local ?? spec.imported
24
+ local = loc.type === 'Identifier' ? loc.name : null
25
+ }
26
+
27
+ if (spec.type === 'ImportDefaultSpecifier') {
28
+ local = spec.local?.type === 'Identifier' ? spec.local.name : null
29
+ }
30
+
31
+ if (local && componentNames.has(local)) {
32
+ specs.componentSpecs.push(spec)
33
+ } else {
34
+ specs.restSpecs.push(spec)
35
+ }
36
+ }
37
+ }
38
+
39
+ return specs
40
+ }
@@ -0,0 +1,35 @@
1
+ import type {API, JSXAttribute, JSXSpreadAttribute} from 'jscodeshift'
2
+
3
+ import {getAttribute} from './getAttribute'
4
+ import {getAttributeExpression} from './getAttributeExpression'
5
+
6
+ export type CompositePrimitive = string | number | boolean
7
+
8
+ export function getStaticAttributeExpression(
9
+ j: API['jscodeshift'],
10
+ attrs: (JSXAttribute | JSXSpreadAttribute)[],
11
+ name: string,
12
+ ): CompositePrimitive | null {
13
+ const attr = getAttribute(attrs, name)
14
+
15
+ if (!attr) {
16
+ return null
17
+ }
18
+
19
+ const expr = getAttributeExpression(attr, j)
20
+
21
+ if (!expr) {
22
+ return null
23
+ }
24
+
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
32
+ }
33
+
34
+ return null
35
+ }
@@ -0,0 +1,37 @@
1
+ import {type API} from 'jscodeshift'
2
+
3
+ import {type AnyExpression} from '../types/AnyExpression'
4
+
5
+ export function getStyleExpression(j: API['jscodeshift'], expr: AnyExpression): AnyExpression {
6
+ if (expr.type === 'Literal') {
7
+ if (expr.value === null) {
8
+ return j.literal(null)
9
+ }
10
+
11
+ if (typeof expr.value === 'boolean') {
12
+ return j.booleanLiteral(expr.value)
13
+ }
14
+
15
+ if (typeof expr.value === 'number') {
16
+ return j.numericLiteral(expr.value)
17
+ }
18
+
19
+ if (typeof expr.value === 'string') {
20
+ return j.stringLiteral(expr.value)
21
+ }
22
+ }
23
+
24
+ if (expr.type === 'BooleanLiteral') {
25
+ return j.booleanLiteral(expr.value as boolean)
26
+ }
27
+
28
+ if (expr.type === 'NumericLiteral') {
29
+ return j.numericLiteral(expr.value as number)
30
+ }
31
+
32
+ if (expr.type === 'StringLiteral') {
33
+ return j.stringLiteral(expr.value as string)
34
+ }
35
+
36
+ return expr
37
+ }
@@ -0,0 +1,28 @@
1
+ import type {API, Collection} from 'jscodeshift'
2
+
3
+ import {getStyledComponentName} from './getStyledComponentName'
4
+
5
+ export function getStyledComponentAliases(
6
+ j: API['jscodeshift'],
7
+ root: Collection,
8
+ localNames: Iterable<string>,
9
+ ): Set<string> {
10
+ const aliases = new Set<string>()
11
+ const names = new Set(localNames)
12
+
13
+ root.find(j.VariableDeclarator).forEach((path) => {
14
+ const {id, init} = path.node
15
+
16
+ if (!init || id.type !== 'Identifier') {
17
+ return
18
+ }
19
+
20
+ const baseComponent = getStyledComponentName(init)
21
+
22
+ if (baseComponent && names.has(baseComponent)) {
23
+ aliases.add(id.name)
24
+ }
25
+ })
26
+
27
+ return aliases
28
+ }
@@ -0,0 +1,52 @@
1
+ export function getStyledComponentName(node: unknown): string | null {
2
+ if (!node || typeof node !== 'object' || !('type' in node)) {
3
+ return null
4
+ }
5
+
6
+ const current = node as {
7
+ type: string
8
+ callee?: unknown
9
+ tag?: unknown
10
+ object?: unknown
11
+ arguments?: unknown[]
12
+ }
13
+
14
+ if (current.type === 'TaggedTemplateExpression') {
15
+ return getStyledComponentName(current.tag)
16
+ }
17
+
18
+ if (current.type === 'CallExpression') {
19
+ const callee = current.callee
20
+
21
+ if (
22
+ callee &&
23
+ typeof callee === 'object' &&
24
+ 'type' in callee &&
25
+ callee.type === 'Identifier' &&
26
+ 'name' in callee &&
27
+ callee.name === 'styled'
28
+ ) {
29
+ const arg = current.arguments?.[0]
30
+
31
+ if (
32
+ arg &&
33
+ typeof arg === 'object' &&
34
+ 'type' in arg &&
35
+ arg.type === 'Identifier' &&
36
+ 'name' in arg
37
+ ) {
38
+ return arg.name as string
39
+ }
40
+
41
+ return null
42
+ }
43
+
44
+ return getStyledComponentName(callee)
45
+ }
46
+
47
+ if (current.type === 'MemberExpression' && current.object) {
48
+ return getStyledComponentName(current.object)
49
+ }
50
+
51
+ return null
52
+ }
@@ -0,0 +1,17 @@
1
+ import {existsSync} from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ export function getTransformPath(root: string, version: string, name: string) {
5
+ const jsPath = path.join(root, `dist/transforms/${version}/${name}/${name}.js`)
6
+ const tsPath = path.join(root, `src/transforms/${version}/${name}/${name}.ts`)
7
+
8
+ if (existsSync(jsPath)) {
9
+ return jsPath
10
+ }
11
+
12
+ if (existsSync(tsPath)) {
13
+ return tsPath
14
+ }
15
+
16
+ throw new Error(`Could not find ${name} codemod`)
17
+ }
@@ -0,0 +1,48 @@
1
+ import {
2
+ type API,
3
+ type ASTPath,
4
+ type ImportDeclaration,
5
+ type JSXOpeningElement,
6
+ type VariableDeclarator,
7
+ } from 'jscodeshift'
8
+
9
+ type CommentableNode = {
10
+ comments?: {type: string; value: string; leading?: boolean}[] | null
11
+ }
12
+
13
+ /**
14
+ * Inserts a `UI-CODEMOD TODO` comment.
15
+ * Returns whether the AST was updated.
16
+ */
17
+ export function insertTodoWarning(
18
+ j: API['jscodeshift'],
19
+ path: ASTPath<JSXOpeningElement | ImportDeclaration | VariableDeclarator>,
20
+ warning: string,
21
+ ): boolean {
22
+ let target: CommentableNode | null = null
23
+
24
+ if (path.node.type === 'ImportDeclaration') {
25
+ 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
+ } else if (path.parent?.node.type === 'JSXElement') {
33
+ target = path.parent.node
34
+ }
35
+
36
+ if (!target) {
37
+ return false
38
+ }
39
+
40
+ target.comments ??= []
41
+
42
+ if (target.comments.some((comment) => comment.value.includes(warning))) {
43
+ return false
44
+ }
45
+
46
+ target.comments.unshift(j.commentLine(` UI-CODEMOD TODO: ${warning}`, true))
47
+ return true
48
+ }
@@ -0,0 +1,53 @@
1
+ import type {API, ASTPath, ImportSpecifier} from 'jscodeshift'
2
+
3
+ export function isImportBindingUsed(
4
+ j: API['jscodeshift'],
5
+ root: ReturnType<API['jscodeshift']>,
6
+ name: string,
7
+ specifierPath: ASTPath<ImportSpecifier>,
8
+ ): boolean {
9
+ const jsxOpening = root.find(j.JSXOpeningElement).some(({node}) => {
10
+ return node.name.type === 'JSXIdentifier' && node.name.name === name
11
+ })
12
+
13
+ if (jsxOpening) {
14
+ return true
15
+ }
16
+
17
+ const jsxClosing = root.find(j.JSXClosingElement).some(({node}) => {
18
+ return node.name.type === 'JSXIdentifier' && node.name.name === name
19
+ })
20
+
21
+ if (jsxClosing) {
22
+ return true
23
+ }
24
+
25
+ const importedPath = specifierPath.get('imported')
26
+ const localPath = specifierPath.get('local')
27
+
28
+ return root.find(j.Identifier).some((idPath) => {
29
+ if (idPath.node.name !== name) {
30
+ return false
31
+ }
32
+
33
+ if (idPath === importedPath || idPath === localPath) {
34
+ return false
35
+ }
36
+
37
+ const parent = idPath.parent?.node
38
+
39
+ if (!parent) {
40
+ return true
41
+ }
42
+
43
+ if (parent.type === 'MemberExpression' && !parent.computed && parent.property === idPath.node) {
44
+ return false
45
+ }
46
+
47
+ if (parent.type === 'TSQualifiedName' && parent.right === idPath.node) {
48
+ return false
49
+ }
50
+
51
+ return true
52
+ })
53
+ }
@@ -0,0 +1,21 @@
1
+ import {type AnyExpression} from '../types/AnyExpression'
2
+
3
+ export function isValidStyleType(expr: AnyExpression) {
4
+ if (expr.type === 'TemplateLiteral') {
5
+ const expressions = expr.expressions as unknown[]
6
+ const quasis = expr.quasis as {value: {cooked: string | null}}[]
7
+ return expressions.length === 0 && quasis.length === 1
8
+ }
9
+
10
+ if (
11
+ expr.type === 'StringLiteral' ||
12
+ expr.type === 'Literal' ||
13
+ expr.type === 'NumericLiteral' ||
14
+ expr.type === 'BooleanLiteral' ||
15
+ expr.type === 'NullLiteral'
16
+ ) {
17
+ return true
18
+ }
19
+
20
+ return false
21
+ }
@@ -0,0 +1,64 @@
1
+ import {type API, type JSXAttribute, type JSXSpreadAttribute} from 'jscodeshift'
2
+
3
+ import {type AnyExpression} from '../types/AnyExpression'
4
+
5
+ export function mergeStyle(
6
+ j: API['jscodeshift'],
7
+ attrs: (JSXAttribute | JSXSpreadAttribute)[],
8
+ styleKey: string,
9
+ styleExpression: AnyExpression,
10
+ ) {
11
+ const styleProp = j.objectProperty(j.identifier(styleKey), styleExpression as never)
12
+
13
+ const styleIndex = attrs.findIndex(
14
+ (a): a is JSXAttribute =>
15
+ a.type === 'JSXAttribute' && a.name.type === 'JSXIdentifier' && a.name.name === 'style',
16
+ )
17
+
18
+ if (styleIndex === -1) {
19
+ attrs.push(
20
+ j.jsxAttribute(
21
+ j.jsxIdentifier('style'),
22
+ j.jsxExpressionContainer(j.objectExpression([styleProp])),
23
+ ),
24
+ )
25
+
26
+ return true
27
+ }
28
+
29
+ const styleAttr = attrs[styleIndex] as JSXAttribute
30
+
31
+ if (styleAttr.value?.type !== 'JSXExpressionContainer') {
32
+ return false
33
+ }
34
+
35
+ const expr = styleAttr.value.expression
36
+
37
+ if (expr.type === 'ObjectExpression') {
38
+ expr.properties = expr.properties.filter((prop) => {
39
+ if (prop.type !== 'ObjectProperty' && prop.type !== 'Property') {
40
+ return true
41
+ }
42
+
43
+ const key = prop.key
44
+
45
+ const name =
46
+ key.type === 'Identifier'
47
+ ? key.name
48
+ : key.type === 'StringLiteral' || key.type === 'Literal'
49
+ ? String(key.value)
50
+ : null
51
+
52
+ return name !== styleKey
53
+ })
54
+
55
+ expr.properties.push(styleProp)
56
+ return true
57
+ }
58
+
59
+ styleAttr.value = j.jsxExpressionContainer(
60
+ j.objectExpression([j.spreadElement(expr as never), styleProp]),
61
+ )
62
+
63
+ return true
64
+ }
@@ -0,0 +1,77 @@
1
+ import {type API, type FileInfo} from 'jscodeshift'
2
+
3
+ import {removeUnusedImport} from './removeUnusedImport'
4
+ import {defineInlineTest} from './testUtils'
5
+
6
+ function transform(fileInfo: FileInfo, api: API): string {
7
+ const j = api.jscodeshift
8
+ const root = j(fileInfo.source)
9
+
10
+ removeUnusedImport(j, root, 'Card')
11
+ return root.toSource()
12
+ }
13
+
14
+ defineInlineTest(
15
+ transform,
16
+ {},
17
+ `
18
+ import {Card} from '@sanity/ui'
19
+
20
+ <Card density="regular" />
21
+ `,
22
+ `
23
+ import {Card} from '@sanity/ui'
24
+
25
+ <Card density="regular" />
26
+ `,
27
+ 'preserves import if element is in jsx',
28
+ )
29
+
30
+ defineInlineTest(
31
+ transform,
32
+ {},
33
+ `
34
+ import {Card} from '@sanity/ui'
35
+ import {Box} from '@sanity/ui'
36
+
37
+ <Box />
38
+ `,
39
+ `
40
+ import {Box} from '@sanity/ui'
41
+
42
+ <Box />
43
+ `,
44
+ 'removes import if element is not in jsx',
45
+ )
46
+
47
+ defineInlineTest(
48
+ transform,
49
+ {},
50
+ `
51
+ import {Box, Card} from '@sanity/ui'
52
+
53
+ <Box />
54
+ `,
55
+ `
56
+ import { Box } from '@sanity/ui';
57
+
58
+ <Box />
59
+ `,
60
+ 'updates import with multiple specifiers if element is not in jsx',
61
+ )
62
+
63
+ defineInlineTest(
64
+ transform,
65
+ {},
66
+ `
67
+ import {Card} from '@sanity/ui'
68
+
69
+ const RootCard = styled(Card)(({theme}) => {})
70
+ `,
71
+ `
72
+ import {Card} from '@sanity/ui'
73
+
74
+ const RootCard = styled(Card)(({theme}) => {})
75
+ `,
76
+ 'preserves import when component is used with styled()',
77
+ )
@@ -0,0 +1,57 @@
1
+ import type {API, ASTPath, ImportSpecifier} from 'jscodeshift'
2
+
3
+ import {isImportBindingUsed} from './isImportBindingUsed'
4
+
5
+ /**
6
+ * Removes unused import specifier or the whole import if no specifiers remain.
7
+ * Returns whether the AST was updated.
8
+ */
9
+ export function removeUnusedImport(
10
+ j: API['jscodeshift'],
11
+ root: ReturnType<API['jscodeshift']>,
12
+ name: string,
13
+ ): boolean {
14
+ let hasChanges = false
15
+
16
+ root.find(j.ImportDeclaration).forEach((path) => {
17
+ const specs = path.node.specifiers
18
+
19
+ if (!specs?.length) {
20
+ return
21
+ }
22
+
23
+ const next = specs.filter((spec, i) => {
24
+ if (spec.type !== 'ImportSpecifier' || spec.imported.type !== 'Identifier') {
25
+ return true
26
+ }
27
+
28
+ if (spec.imported.name !== name) {
29
+ return true
30
+ }
31
+
32
+ if ('importKind' in spec && spec.importKind === 'type') {
33
+ return true
34
+ }
35
+
36
+ const localName = spec.local?.type === 'Identifier' ? spec.local.name : spec.imported.name
37
+ const specifierPath = path.get('specifiers', i) as ASTPath<ImportSpecifier>
38
+
39
+ return isImportBindingUsed(j, root, localName, specifierPath)
40
+ })
41
+
42
+ if (next.length === specs.length) {
43
+ return
44
+ }
45
+
46
+ if (next.length === 0) {
47
+ j(path).remove()
48
+ hasChanges = true
49
+ return
50
+ }
51
+
52
+ path.node.specifiers = next
53
+ hasChanges = true
54
+ })
55
+
56
+ return hasChanges
57
+ }