@planningcenter/tapestry-migration-cli 2.2.1-qa-362.0 → 2.3.0-rc.10

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 (64) hide show
  1. package/dist/tapestry-react-shim.cjs +5065 -0
  2. package/package.json +9 -5
  3. package/src/components/button/index.ts +53 -3
  4. package/src/components/button/transforms/auditSpreadProps.test.ts +352 -0
  5. package/src/components/button/transforms/auditSpreadProps.ts +24 -0
  6. package/src/components/button/transforms/childrenToLabel.test.ts +363 -0
  7. package/src/components/button/transforms/childrenToLabel.ts +84 -0
  8. package/src/components/button/transforms/commentOnVisualKindDifference.ts +24 -0
  9. package/src/components/button/transforms/convertStyleProps.test.ts +464 -0
  10. package/src/components/button/transforms/convertStyleProps.ts +16 -0
  11. package/src/components/button/transforms/iconLeftToPrefix.test.ts +432 -0
  12. package/src/components/button/transforms/iconLeftToPrefix.ts +33 -0
  13. package/src/components/button/transforms/iconRightToSuffix.test.ts +407 -0
  14. package/src/components/button/transforms/iconRightToSuffix.ts +33 -0
  15. package/src/components/button/transforms/iconToIconButton.test.ts +377 -0
  16. package/src/components/button/transforms/iconToIconButton.ts +53 -0
  17. package/src/components/button/transforms/removeAsButton.ts +15 -0
  18. package/src/components/button/transforms/removeDuplicateKeys.test.ts +302 -0
  19. package/src/components/button/transforms/removeDuplicateKeys.ts +8 -0
  20. package/src/components/button/transforms/reviewStyles.ts +17 -0
  21. package/src/components/button/transforms/spinnerToLoadingButton.test.ts +165 -0
  22. package/src/components/button/transforms/spinnerToLoadingButton.ts +14 -0
  23. package/src/components/button/transforms/themeVariantToKind.test.ts +401 -0
  24. package/src/components/button/transforms/themeVariantToKind.ts +90 -0
  25. package/src/components/button/transforms/tooltipToWrapper.test.ts +392 -0
  26. package/src/components/button/transforms/tooltipToWrapper.ts +35 -0
  27. package/src/components/button/transforms/unsupportedProps.ts +73 -0
  28. package/src/components/shared/actions/addAttribute.test.ts +300 -0
  29. package/src/components/shared/actions/addAttribute.ts +65 -0
  30. package/src/components/shared/actions/addComment.test.ts +1 -1
  31. package/src/components/shared/actions/addCommentToAttribute.test.ts +45 -0
  32. package/src/components/shared/actions/addCommentToAttribute.ts +28 -0
  33. package/src/components/shared/actions/addCommentToUnsupportedProps.ts +29 -0
  34. package/src/components/shared/actions/convertAttributeFromObjectToJSXElement.test.ts +139 -0
  35. package/src/components/shared/actions/convertAttributeFromObjectToJSXElement.ts +81 -0
  36. package/src/components/shared/actions/createWrapper.test.ts +642 -0
  37. package/src/components/shared/actions/createWrapper.ts +70 -0
  38. package/src/components/shared/actions/getAttribute.ts +18 -0
  39. package/src/components/shared/actions/getAttributeValue.test.ts +261 -0
  40. package/src/components/shared/actions/getAttributeValue.ts +15 -0
  41. package/src/components/shared/actions/getAttributeValueAsProps.ts +57 -0
  42. package/src/components/shared/actions/getSpreadProps.ts +7 -0
  43. package/src/components/shared/actions/hasSpreadProps.ts +7 -0
  44. package/src/components/shared/actions/removeChildren.ts +7 -0
  45. package/src/components/shared/actions/removeDuplicateKeys.test.ts +280 -0
  46. package/src/components/shared/actions/removeDuplicateKeys.ts +45 -0
  47. package/src/components/shared/actions/removeUnusedImport.test.ts +302 -0
  48. package/src/components/shared/actions/removeUnusedImport.ts +81 -0
  49. package/src/components/shared/actions/transformElementName.test.ts +9 -9
  50. package/src/components/shared/actions/transformElementName.ts +13 -16
  51. package/src/components/shared/conditions/hasChildren.ts +5 -0
  52. package/src/components/shared/getJavaScriptTheme.ts +68 -0
  53. package/src/components/shared/jsThemeLoader.ts +85 -0
  54. package/src/components/shared/transformFactories/attributeCombineFactory.test.ts +374 -0
  55. package/src/components/shared/transformFactories/attributeCombineFactory.ts +300 -0
  56. package/src/components/shared/transformFactories/attributeTransformFactory.ts +14 -6
  57. package/src/components/shared/transformFactories/componentTransformFactory.ts +1 -1
  58. package/src/components/shared/transformFactories/helpers/addImport.test.ts +278 -0
  59. package/src/components/shared/transformFactories/helpers/manageImports.ts +53 -20
  60. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +362 -0
  61. package/src/index.ts +4 -0
  62. package/src/stubs/stackViewPlugin.ts +33 -0
  63. package/src/stubs/tapestry-stub.ts +16 -0
  64. package/src/tapestry-react-shim.ts +7 -0
@@ -0,0 +1,300 @@
1
+ import jscodeshift, {
2
+ BooleanLiteral,
3
+ ConditionalExpression,
4
+ Identifier,
5
+ JSXAttribute,
6
+ JSXElement,
7
+ JSXExpressionContainer,
8
+ JSXIdentifier,
9
+ StringLiteral,
10
+ } from "jscodeshift"
11
+ import { describe, expect, it } from "vitest"
12
+
13
+ import { addAttribute, Conditional } from "./addAttribute"
14
+
15
+ const j = jscodeshift.withParser("tsx")
16
+
17
+ function createElementFromCode(code: string): JSXElement {
18
+ const source = j(`<div>${code}</div>`)
19
+ return source.find(j.JSXElement).at(0).get().value.children?.[0] as JSXElement
20
+ }
21
+
22
+ function getAttributeFromElement(
23
+ element: JSXElement,
24
+ name: string
25
+ ): JSXAttribute | null {
26
+ const attributes = element.openingElement.attributes || []
27
+ return (
28
+ (attributes.find(
29
+ (attr) =>
30
+ attr.type === "JSXAttribute" &&
31
+ (attr.name as JSXIdentifier)?.name === name
32
+ ) as JSXAttribute) || null
33
+ )
34
+ }
35
+
36
+ describe("addAttribute", () => {
37
+ describe("string values", () => {
38
+ it("should add string attribute to element", () => {
39
+ const element = createElementFromCode("<Button>Save</Button>")
40
+
41
+ addAttribute({ element, j, name: "kind", value: "primary" })
42
+
43
+ const kindAttr = getAttributeFromElement(element, "kind")
44
+ expect(kindAttr).not.toBeNull()
45
+ expect(kindAttr?.value?.type).toBe("StringLiteral")
46
+ expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
47
+ })
48
+
49
+ it("should add multiple string attributes", () => {
50
+ const element = createElementFromCode("<Button>Save</Button>")
51
+
52
+ addAttribute({ element, j, name: "kind", value: "primary" })
53
+ addAttribute({ element, j, name: "size", value: "large" })
54
+
55
+ const kindAttr = getAttributeFromElement(element, "kind")
56
+ const sizeAttr = getAttributeFromElement(element, "size")
57
+
58
+ expect(kindAttr).not.toBeNull()
59
+ expect(sizeAttr).not.toBeNull()
60
+ expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
61
+ expect((sizeAttr?.value as StringLiteral)?.value).toBe("large")
62
+ })
63
+
64
+ it("should handle empty string values", () => {
65
+ const element = createElementFromCode("<Button>Save</Button>")
66
+
67
+ addAttribute({ element, j, name: "title", value: "" })
68
+
69
+ const titleAttr = getAttributeFromElement(element, "title")
70
+ expect(titleAttr).not.toBeNull()
71
+ expect(titleAttr?.value?.type).toBe("StringLiteral")
72
+ expect((titleAttr?.value as StringLiteral)?.value).toBe("")
73
+ })
74
+
75
+ it("should handle special characters in string values", () => {
76
+ const element = createElementFromCode("<Button>Save</Button>")
77
+
78
+ addAttribute({ element, j, name: "aria-label", value: "Save & Continue" })
79
+
80
+ const ariaAttr = getAttributeFromElement(element, "aria-label")
81
+ expect(ariaAttr).not.toBeNull()
82
+ expect((ariaAttr?.value as StringLiteral)?.value).toBe("Save & Continue")
83
+ })
84
+ })
85
+
86
+ describe("boolean values", () => {
87
+ it("should add boolean true attribute", () => {
88
+ const element = createElementFromCode("<Button>Save</Button>")
89
+
90
+ addAttribute({ element, j, name: "disabled", value: true })
91
+
92
+ const disabledAttr = getAttributeFromElement(element, "disabled")
93
+ expect(disabledAttr).not.toBeNull()
94
+ expect(disabledAttr?.value?.type).toBe("JSXExpressionContainer")
95
+
96
+ const container = disabledAttr?.value as JSXExpressionContainer
97
+ expect(container.expression.type).toBe("BooleanLiteral")
98
+ expect((container.expression as BooleanLiteral).value).toBe(true)
99
+ })
100
+
101
+ it("should add boolean false attribute", () => {
102
+ const element = createElementFromCode("<Button>Save</Button>")
103
+
104
+ addAttribute({ element, j, name: "loading", value: false })
105
+
106
+ const loadingAttr = getAttributeFromElement(element, "loading")
107
+ expect(loadingAttr).not.toBeNull()
108
+ expect(loadingAttr?.value?.type).toBe("JSXExpressionContainer")
109
+
110
+ const container = loadingAttr?.value as JSXExpressionContainer
111
+ expect(container.expression.type).toBe("BooleanLiteral")
112
+ expect((container.expression as BooleanLiteral).value).toBe(false)
113
+ })
114
+ })
115
+
116
+ describe("null values", () => {
117
+ it("should not add attribute when value is null", () => {
118
+ const element = createElementFromCode("<Button>Save</Button>")
119
+ const initialAttrCount = (element.openingElement.attributes || []).length
120
+
121
+ addAttribute({ element, j, name: "kind", value: null })
122
+
123
+ const finalAttrCount = (element.openingElement.attributes || []).length
124
+ expect(finalAttrCount).toBe(initialAttrCount)
125
+
126
+ const kindAttr = getAttributeFromElement(element, "kind")
127
+ expect(kindAttr).toBeNull()
128
+ })
129
+ })
130
+
131
+ describe("conditional values", () => {
132
+ it("should add conditional attribute with test, consequent, and alternate", () => {
133
+ const element = createElementFromCode("<Button>Save</Button>")
134
+ const conditionalValue: Conditional = {
135
+ alternate: "secondary",
136
+ consequent: "primary",
137
+ test: "isPrimary",
138
+ }
139
+
140
+ addAttribute({ element, j, name: "kind", value: conditionalValue })
141
+
142
+ const kindAttr = getAttributeFromElement(element, "kind")
143
+ expect(kindAttr).not.toBeNull()
144
+ expect(kindAttr?.value?.type).toBe("JSXExpressionContainer")
145
+
146
+ const container = kindAttr?.value as JSXExpressionContainer
147
+ expect(container.expression.type).toBe("ConditionalExpression")
148
+
149
+ const conditional = container.expression as ConditionalExpression
150
+ expect(conditional.test.type).toBe("Identifier")
151
+ expect((conditional.test as Identifier).name).toBe("isPrimary")
152
+ expect(conditional.consequent.type).toBe("StringLiteral")
153
+ expect((conditional.consequent as StringLiteral).value).toBe("primary")
154
+ expect(conditional.alternate.type).toBe("StringLiteral")
155
+ expect((conditional.alternate as StringLiteral).value).toBe("secondary")
156
+ })
157
+
158
+ it("should handle conditional with empty string values", () => {
159
+ const element = createElementFromCode("<Button>Save</Button>")
160
+ const conditionalValue: Conditional = {
161
+ alternate: "",
162
+ consequent: "primary",
163
+ test: "hasTheme",
164
+ }
165
+
166
+ addAttribute({ element, j, name: "kind", value: conditionalValue })
167
+
168
+ const kindAttr = getAttributeFromElement(element, "kind")
169
+ expect(kindAttr).not.toBeNull()
170
+
171
+ const container = kindAttr?.value as JSXExpressionContainer
172
+ const conditional = container.expression as ConditionalExpression
173
+ expect((conditional.consequent as StringLiteral).value).toBe("primary")
174
+ expect((conditional.alternate as StringLiteral).value).toBe("")
175
+ })
176
+
177
+ it("should handle conditional with same consequent and alternate", () => {
178
+ const element = createElementFromCode("<Button>Save</Button>")
179
+ const conditionalValue: Conditional = {
180
+ alternate: "neutral",
181
+ consequent: "neutral",
182
+ test: "condition",
183
+ }
184
+
185
+ addAttribute({ element, j, name: "kind", value: conditionalValue })
186
+
187
+ const kindAttr = getAttributeFromElement(element, "kind")
188
+ expect(kindAttr).not.toBeNull()
189
+
190
+ const container = kindAttr?.value as JSXExpressionContainer
191
+ const conditional = container.expression as ConditionalExpression
192
+ expect((conditional.consequent as StringLiteral).value).toBe("neutral")
193
+ expect((conditional.alternate as StringLiteral).value).toBe("neutral")
194
+ })
195
+
196
+ it("should render conditional attribute correctly in source", () => {
197
+ const source = j("<Button>Save</Button>")
198
+ const element = source.find(j.JSXElement).at(0).get().value as JSXElement
199
+ const conditionalValue: Conditional = {
200
+ alternate: "secondary",
201
+ consequent: "primary",
202
+ test: "isPrimary",
203
+ }
204
+
205
+ addAttribute({ element, j, name: "kind", value: conditionalValue })
206
+
207
+ const result = source.toSource()
208
+ expect(result).toContain('kind={isPrimary ? "primary" : "secondary"}')
209
+ })
210
+
211
+ it("should handle multiple conditional attributes", () => {
212
+ const element = createElementFromCode("<Button>Save</Button>")
213
+ const kindConditional: Conditional = {
214
+ alternate: "secondary",
215
+ consequent: "primary",
216
+ test: "isPrimary",
217
+ }
218
+ const sizeConditional: Conditional = {
219
+ alternate: "small",
220
+ consequent: "large",
221
+ test: "isLarge",
222
+ }
223
+
224
+ addAttribute({ element, j, name: "kind", value: kindConditional })
225
+ addAttribute({ element, j, name: "size", value: sizeConditional })
226
+
227
+ const kindAttr = getAttributeFromElement(element, "kind")
228
+ const sizeAttr = getAttributeFromElement(element, "size")
229
+
230
+ expect(kindAttr).not.toBeNull()
231
+ expect(sizeAttr).not.toBeNull()
232
+ expect(element.openingElement.attributes).toHaveLength(2)
233
+ })
234
+ })
235
+
236
+ describe("adding to elements with existing attributes", () => {
237
+ it("should add attribute to element that already has attributes", () => {
238
+ const element = createElementFromCode(
239
+ '<Button onClick={handleClick} className="btn">Save</Button>'
240
+ )
241
+
242
+ addAttribute({ element, j, name: "kind", value: "primary" })
243
+
244
+ expect(element.openingElement.attributes).toHaveLength(3)
245
+
246
+ const kindAttr = getAttributeFromElement(element, "kind")
247
+ expect(kindAttr).not.toBeNull()
248
+ expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
249
+
250
+ const onClickAttr = getAttributeFromElement(element, "onClick")
251
+ const classAttr = getAttributeFromElement(element, "className")
252
+ expect(onClickAttr).not.toBeNull()
253
+ expect(classAttr).not.toBeNull()
254
+ })
255
+ })
256
+
257
+ describe("adding to self-closing elements", () => {
258
+ it("should add attribute to self-closing element", () => {
259
+ const element = createElementFromCode("<Button />")
260
+
261
+ addAttribute({ element, j, name: "kind", value: "primary" })
262
+
263
+ const kindAttr = getAttributeFromElement(element, "kind")
264
+ expect(kindAttr).not.toBeNull()
265
+ expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
266
+ })
267
+ })
268
+
269
+ describe("integration with JSCodeshift", () => {
270
+ it("should render correctly in transformed source", () => {
271
+ const source = j("<Button>Save</Button>")
272
+ const element = source.find(j.JSXElement).at(0).get().value as JSXElement
273
+
274
+ addAttribute({ element, j, name: "kind", value: "primary" })
275
+ addAttribute({ element, j, name: "disabled", value: true })
276
+
277
+ const result = source.toSource()
278
+
279
+ expect(result).toContain('kind="primary"')
280
+ expect(result).toContain("disabled={true}")
281
+ expect(result).toContain("<Button")
282
+ expect(result).toContain(">Save</Button>")
283
+ })
284
+
285
+ it("should handle complex attribute names", () => {
286
+ const element = createElementFromCode("<Button>Save</Button>")
287
+
288
+ addAttribute({ element, j, name: "data-testid", value: "save-button" })
289
+ addAttribute({ element, j, name: "aria-describedby", value: "help-text" })
290
+
291
+ const testIdAttr = getAttributeFromElement(element, "data-testid")
292
+ const ariaAttr = getAttributeFromElement(element, "aria-describedby")
293
+
294
+ expect(testIdAttr).not.toBeNull()
295
+ expect(ariaAttr).not.toBeNull()
296
+ expect((testIdAttr?.value as StringLiteral)?.value).toBe("save-button")
297
+ expect((ariaAttr?.value as StringLiteral)?.value).toBe("help-text")
298
+ })
299
+ })
300
+ })
@@ -0,0 +1,65 @@
1
+ import { JSCodeshift, JSXElement } from "jscodeshift"
2
+
3
+ export interface Conditional {
4
+ alternate: string
5
+ consequent: string
6
+ test: string
7
+ }
8
+
9
+ function formatValue(value: string | boolean, j: JSCodeshift) {
10
+ if (typeof value === "string") {
11
+ return j.stringLiteral(value)
12
+ } else if (typeof value === "boolean") {
13
+ return j.jsxExpressionContainer(j.booleanLiteral(value))
14
+ } else {
15
+ throw new Error(`Unsupported attribute value type: ${typeof value}`)
16
+ }
17
+ }
18
+
19
+ export function addAttribute({
20
+ element,
21
+ name,
22
+ j,
23
+ value,
24
+ }: {
25
+ element: JSXElement
26
+ j: JSCodeshift
27
+ name: string
28
+ value: string | boolean | Conditional | null
29
+ }) {
30
+ if (value === null) return
31
+ const attributes = element.openingElement.attributes || []
32
+
33
+ if (
34
+ typeof value === "object" &&
35
+ "test" in value &&
36
+ "consequent" in value &&
37
+ "alternate" in value
38
+ ) {
39
+ addConditionalAttribute(value, element, j, name)
40
+ return
41
+ }
42
+
43
+ const formattedValue = formatValue(value, j)
44
+ attributes.push(j.jsxAttribute(j.jsxIdentifier(name), formattedValue))
45
+ }
46
+
47
+ function addConditionalAttribute(
48
+ { test, consequent, alternate }: Conditional,
49
+ element: JSXElement,
50
+ j: JSCodeshift,
51
+ targetAttribute: string
52
+ ) {
53
+ const conditional = j.jsxExpressionContainer(
54
+ j.conditionalExpression(
55
+ j.identifier(test),
56
+ j.stringLiteral(consequent),
57
+ j.stringLiteral(alternate)
58
+ )
59
+ )
60
+ const attribute = j.jsxAttribute(
61
+ j.jsxIdentifier(targetAttribute),
62
+ conditional
63
+ )
64
+ element.openingElement.attributes?.push(attribute)
65
+ }
@@ -84,7 +84,7 @@ describe("addComment", () => {
84
84
  .at(0)
85
85
  .get().value as JSXElement
86
86
 
87
- const commentText = "TODO: Update this nested button"
87
+ const commentText = "Update this nested button"
88
88
  addComment({
89
89
  element: buttonElement,
90
90
  j,
@@ -0,0 +1,45 @@
1
+ import jscodeshift, {
2
+ JSXAttribute,
3
+ JSXElement,
4
+ JSXIdentifier,
5
+ } from "jscodeshift"
6
+ import { describe, expect, it } from "vitest"
7
+
8
+ import { addCommentToAttribute } from "./addCommentToAttribute"
9
+
10
+ const j = jscodeshift.withParser("tsx")
11
+
12
+ describe("addCommentToAttribute", () => {
13
+ it("adds comments before attribute if provided", () => {
14
+ const outerCode = `
15
+ <div>
16
+ <Button
17
+ onClick={() => { console.log('hi')}}
18
+ kind='secondary'
19
+ fullWidth
20
+ {...props}>Save</Button>
21
+ </div>
22
+ `
23
+ const source = j(outerCode)
24
+ const buttonElement = source
25
+ .find(j.JSXElement)
26
+ .filter(
27
+ (path) =>
28
+ (path.value.openingElement.name as JSXIdentifier)?.name === "Button"
29
+ )
30
+ .at(0)
31
+ .get().value as JSXElement
32
+ const attribute = buttonElement.openingElement.attributes?.find(
33
+ (attr) => attr.type === "JSXAttribute" && attr.name.name === "kind"
34
+ ) as JSXAttribute
35
+
36
+ const commentText = "This needs to be updated"
37
+ addCommentToAttribute({ attribute, j, text: commentText })
38
+
39
+ const result = source.toSource()
40
+
41
+ expect(result)
42
+ .toContain(`/* TODO: tapestry-migration (kind): ${commentText} */
43
+ kind='secondary'`)
44
+ })
45
+ })
@@ -0,0 +1,28 @@
1
+ import { JSCodeshift, JSXAttribute, JSXSpreadAttribute } from "jscodeshift"
2
+
3
+ import { formatComment } from "./addComment"
4
+
5
+ export function addCommentToAttribute({
6
+ text,
7
+ attribute,
8
+ j,
9
+ }: {
10
+ attribute: JSXAttribute | JSXSpreadAttribute
11
+ j: JSCodeshift
12
+ text: string
13
+ }) {
14
+ const attributeName =
15
+ ((attribute.type === "JSXAttribute" && attribute.name.name) as string) ||
16
+ "spreadAttribute"
17
+ const comment = j.commentBlock(
18
+ formatComment(text, attributeName),
19
+ true,
20
+ false
21
+ )
22
+
23
+ if (attribute.comments) {
24
+ attribute.comments.unshift(comment)
25
+ } else {
26
+ attribute.comments = [comment]
27
+ }
28
+ }
@@ -0,0 +1,29 @@
1
+ import { JSCodeshift, JSXAttribute, JSXElement } from "jscodeshift"
2
+
3
+ import { addCommentToAttribute } from "./addCommentToAttribute"
4
+
5
+ export function addCommentToUnsupportedProps({
6
+ element,
7
+ j,
8
+ props,
9
+ messageSuffix = () => "",
10
+ }: {
11
+ element: JSXElement
12
+ j: JSCodeshift
13
+ messageSuffix?: (prop: string) => string
14
+ props: string[]
15
+ }): boolean {
16
+ const unsupportedAttributes = (
17
+ element.openingElement.attributes || []
18
+ ).filter(
19
+ (attr) =>
20
+ attr.type === "JSXAttribute" && props.includes(attr.name.name as string)
21
+ ) as JSXAttribute[]
22
+
23
+ unsupportedAttributes.forEach((attribute) => {
24
+ const propName = attribute.name.name as string
25
+ const text = `'${propName}' is not supported, please migrate as needed.${messageSuffix(propName)}`
26
+ addCommentToAttribute({ attribute, j, text })
27
+ })
28
+ return unsupportedAttributes.length > 0
29
+ }
@@ -0,0 +1,139 @@
1
+ import jscodeshift, {
2
+ JSXAttribute,
3
+ JSXElement,
4
+ JSXExpressionContainer,
5
+ JSXIdentifier,
6
+ } from "jscodeshift"
7
+ import { describe, expect, it } from "vitest"
8
+
9
+ import { convertAttributeFromObjectToJSXElement } from "./convertAttributeFromObjectToJSXElement"
10
+
11
+ const j = jscodeshift.withParser("tsx")
12
+
13
+ function createSource(code: string) {
14
+ return j(code)
15
+ }
16
+
17
+ function createElementFromCode(code: string): JSXElement {
18
+ const source = createSource(`<div>${code}</div>`)
19
+ return source.find(j.JSXElement).at(0).get().value.children?.[0] as JSXElement
20
+ }
21
+
22
+ describe("convertAttributeFromObjectToJSXElement", () => {
23
+ it("should convert object attribute to JSX element", () => {
24
+ const element = createElementFromCode(
25
+ '<Button iconLeft={{ name: "star", size: 16 }} />'
26
+ )
27
+
28
+ const result = convertAttributeFromObjectToJSXElement({
29
+ attributeName: "iconLeft",
30
+ element,
31
+ elementName: "Icon",
32
+ j,
33
+ })
34
+
35
+ expect(result).not.toBeNull()
36
+ expect(result?.type).toBe("JSXElement")
37
+ expect((result?.openingElement.name as JSXIdentifier).name).toBe("Icon")
38
+ expect(result?.openingElement.selfClosing).toBe(true)
39
+ expect(result?.openingElement.attributes).toHaveLength(1)
40
+ expect(result?.openingElement.attributes?.[0].type).toBe(
41
+ "JSXSpreadAttribute"
42
+ )
43
+
44
+ // Check that the attribute still exists but now has JSX element value
45
+ const iconLeftAttr = element.openingElement.attributes?.find(
46
+ (attr) => attr.type === "JSXAttribute" && attr.name?.name === "iconLeft"
47
+ ) as JSXAttribute
48
+ expect(iconLeftAttr).toBeDefined()
49
+ expect(iconLeftAttr?.value?.type).toBe("JSXExpressionContainer")
50
+ expect(
51
+ (iconLeftAttr?.value as JSXExpressionContainer)?.expression?.type
52
+ ).toBe("JSXElement")
53
+ })
54
+
55
+ it("should handle expression container attributes", () => {
56
+ const element = createElementFromCode("<Button iconLeft={iconProps} />")
57
+
58
+ const result = convertAttributeFromObjectToJSXElement({
59
+ attributeName: "iconLeft",
60
+ element,
61
+ elementName: "Icon",
62
+ j,
63
+ })
64
+
65
+ expect(result).not.toBeNull()
66
+ expect(result?.openingElement.attributes).toHaveLength(1)
67
+ expect(result?.openingElement.attributes?.[0].type).toBe(
68
+ "JSXSpreadAttribute"
69
+ )
70
+
71
+ // Check that the attribute still exists but now has JSX element value
72
+ const iconLeftAttr = element.openingElement.attributes?.find(
73
+ (attr) => attr.type === "JSXAttribute" && attr.name?.name === "iconLeft"
74
+ ) as JSXAttribute
75
+ expect(iconLeftAttr).toBeDefined()
76
+ expect(iconLeftAttr?.value?.type).toBe("JSXExpressionContainer")
77
+ expect(
78
+ (iconLeftAttr?.value as JSXExpressionContainer)?.expression?.type
79
+ ).toBe("JSXElement")
80
+ })
81
+
82
+ it("should return null for empty expression container", () => {
83
+ const element = createElementFromCode("<Button iconLeft={{}} />")
84
+
85
+ const result = convertAttributeFromObjectToJSXElement({
86
+ attributeName: "iconLeft",
87
+ element,
88
+ elementName: "Icon",
89
+ j,
90
+ })
91
+
92
+ // Should return null since empty object has no props to spread
93
+ expect(result).toBeNull()
94
+
95
+ // Check that the attribute was not modified since no conversion occurred
96
+ const iconLeftAttr = element.openingElement.attributes?.find(
97
+ (attr) => attr.type === "JSXAttribute" && attr.name?.name === "iconLeft"
98
+ ) as JSXAttribute
99
+ expect(iconLeftAttr).toBeDefined()
100
+ expect(iconLeftAttr?.value?.type).toBe("JSXExpressionContainer")
101
+ expect(
102
+ (iconLeftAttr?.value as JSXExpressionContainer)?.expression?.type
103
+ ).toBe("ObjectExpression")
104
+ })
105
+
106
+ it("should return null for missing attribute", () => {
107
+ const element = createElementFromCode("<Button>Save</Button>")
108
+
109
+ const result = convertAttributeFromObjectToJSXElement({
110
+ attributeName: "iconLeft",
111
+ element,
112
+ elementName: "Icon",
113
+ j,
114
+ })
115
+
116
+ expect(result).toBeNull()
117
+ })
118
+
119
+ it("should return null for attribute with no value", () => {
120
+ const element = j.jsxElement(
121
+ j.jsxOpeningElement(
122
+ j.jsxIdentifier("Button"),
123
+ [j.jsxAttribute(j.jsxIdentifier("iconLeft"), null)],
124
+ false
125
+ ),
126
+ j.jsxClosingElement(j.jsxIdentifier("Button")),
127
+ [j.jsxText("Save")]
128
+ )
129
+
130
+ const result = convertAttributeFromObjectToJSXElement({
131
+ attributeName: "iconLeft",
132
+ element,
133
+ elementName: "Icon",
134
+ j,
135
+ })
136
+
137
+ expect(result).toBeNull()
138
+ })
139
+ })
@@ -0,0 +1,81 @@
1
+ import {
2
+ JSCodeshift,
3
+ JSXAttribute,
4
+ JSXElement,
5
+ JSXSpreadAttribute,
6
+ } from "jscodeshift"
7
+
8
+ import { getAttribute } from "./getAttribute"
9
+
10
+ export function convertAttributeFromObjectToJSXElement({
11
+ attributeName,
12
+ element,
13
+ elementName,
14
+ j,
15
+ stringValueKey,
16
+ }: {
17
+ attributeName: string
18
+ element: JSXElement
19
+ elementName: string
20
+ j: JSCodeshift
21
+ stringValueKey?: string
22
+ }): JSXElement | null {
23
+ const attribute = getAttribute({ element, name: attributeName })
24
+ if (!attribute || !attribute.value) return null
25
+
26
+ const elementProps = buildProps({ attribute, j, stringValueKey })
27
+ if (!elementProps) return null
28
+
29
+ const newElement = j.jsxElement(
30
+ j.jsxOpeningElement(j.jsxIdentifier(elementName), elementProps, true),
31
+ null,
32
+ []
33
+ )
34
+
35
+ attribute.value = j.jsxExpressionContainer(newElement)
36
+
37
+ return newElement
38
+ }
39
+
40
+ function buildProps({
41
+ attribute,
42
+ j,
43
+ stringValueKey,
44
+ }: {
45
+ attribute: JSXAttribute
46
+ j: JSCodeshift
47
+ stringValueKey?: string
48
+ }): JSXSpreadAttribute[] | null {
49
+ if (!attribute.value) return null
50
+ if (attribute.value.type == "StringLiteral")
51
+ return stringToProps({ attribute, j, stringValueKey })
52
+ if (attribute.value.type !== "JSXExpressionContainer") return null
53
+ if (attribute.value.expression.type === "JSXEmptyExpression") return null
54
+ if (
55
+ attribute.value.expression.type === "ObjectExpression" &&
56
+ attribute.value.expression.properties.length === 0
57
+ )
58
+ return null
59
+ return [j.jsxSpreadAttribute(attribute.value.expression)]
60
+ }
61
+
62
+ function stringToProps({
63
+ attribute,
64
+ j,
65
+ stringValueKey,
66
+ }: {
67
+ attribute: JSXAttribute
68
+ j: JSCodeshift
69
+ stringValueKey?: string
70
+ }): JSXSpreadAttribute[] | null {
71
+ if (!attribute.value) return null
72
+ if (stringValueKey === undefined) return null
73
+
74
+ return [
75
+ j.jsxSpreadAttribute(
76
+ j.objectExpression([
77
+ j.objectProperty(j.identifier(stringValueKey), attribute.value),
78
+ ])
79
+ ),
80
+ ]
81
+ }