@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.
- package/dist/tapestry-react-shim.cjs +5065 -0
- package/package.json +9 -5
- package/src/components/button/index.ts +53 -3
- package/src/components/button/transforms/auditSpreadProps.test.ts +352 -0
- package/src/components/button/transforms/auditSpreadProps.ts +24 -0
- package/src/components/button/transforms/childrenToLabel.test.ts +363 -0
- package/src/components/button/transforms/childrenToLabel.ts +84 -0
- package/src/components/button/transforms/commentOnVisualKindDifference.ts +24 -0
- package/src/components/button/transforms/convertStyleProps.test.ts +464 -0
- package/src/components/button/transforms/convertStyleProps.ts +16 -0
- package/src/components/button/transforms/iconLeftToPrefix.test.ts +432 -0
- package/src/components/button/transforms/iconLeftToPrefix.ts +33 -0
- package/src/components/button/transforms/iconRightToSuffix.test.ts +407 -0
- package/src/components/button/transforms/iconRightToSuffix.ts +33 -0
- package/src/components/button/transforms/iconToIconButton.test.ts +377 -0
- package/src/components/button/transforms/iconToIconButton.ts +53 -0
- package/src/components/button/transforms/removeAsButton.ts +15 -0
- package/src/components/button/transforms/removeDuplicateKeys.test.ts +302 -0
- package/src/components/button/transforms/removeDuplicateKeys.ts +8 -0
- package/src/components/button/transforms/reviewStyles.ts +17 -0
- package/src/components/button/transforms/spinnerToLoadingButton.test.ts +165 -0
- package/src/components/button/transforms/spinnerToLoadingButton.ts +14 -0
- package/src/components/button/transforms/themeVariantToKind.test.ts +401 -0
- package/src/components/button/transforms/themeVariantToKind.ts +90 -0
- package/src/components/button/transforms/tooltipToWrapper.test.ts +392 -0
- package/src/components/button/transforms/tooltipToWrapper.ts +35 -0
- package/src/components/button/transforms/unsupportedProps.ts +73 -0
- package/src/components/shared/actions/addAttribute.test.ts +300 -0
- package/src/components/shared/actions/addAttribute.ts +65 -0
- package/src/components/shared/actions/addComment.test.ts +1 -1
- package/src/components/shared/actions/addCommentToAttribute.test.ts +45 -0
- package/src/components/shared/actions/addCommentToAttribute.ts +28 -0
- package/src/components/shared/actions/addCommentToUnsupportedProps.ts +29 -0
- package/src/components/shared/actions/convertAttributeFromObjectToJSXElement.test.ts +139 -0
- package/src/components/shared/actions/convertAttributeFromObjectToJSXElement.ts +81 -0
- package/src/components/shared/actions/createWrapper.test.ts +642 -0
- package/src/components/shared/actions/createWrapper.ts +70 -0
- package/src/components/shared/actions/getAttribute.ts +18 -0
- package/src/components/shared/actions/getAttributeValue.test.ts +261 -0
- package/src/components/shared/actions/getAttributeValue.ts +15 -0
- package/src/components/shared/actions/getAttributeValueAsProps.ts +57 -0
- package/src/components/shared/actions/getSpreadProps.ts +7 -0
- package/src/components/shared/actions/hasSpreadProps.ts +7 -0
- package/src/components/shared/actions/removeChildren.ts +7 -0
- package/src/components/shared/actions/removeDuplicateKeys.test.ts +280 -0
- package/src/components/shared/actions/removeDuplicateKeys.ts +45 -0
- package/src/components/shared/actions/removeUnusedImport.test.ts +302 -0
- package/src/components/shared/actions/removeUnusedImport.ts +81 -0
- package/src/components/shared/actions/transformElementName.test.ts +9 -9
- package/src/components/shared/actions/transformElementName.ts +13 -16
- package/src/components/shared/conditions/hasChildren.ts +5 -0
- package/src/components/shared/getJavaScriptTheme.ts +68 -0
- package/src/components/shared/jsThemeLoader.ts +85 -0
- package/src/components/shared/transformFactories/attributeCombineFactory.test.ts +374 -0
- package/src/components/shared/transformFactories/attributeCombineFactory.ts +300 -0
- package/src/components/shared/transformFactories/attributeTransformFactory.ts +14 -6
- package/src/components/shared/transformFactories/componentTransformFactory.ts +1 -1
- package/src/components/shared/transformFactories/helpers/addImport.test.ts +278 -0
- package/src/components/shared/transformFactories/helpers/manageImports.ts +53 -20
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +362 -0
- package/src/index.ts +4 -0
- package/src/stubs/stackViewPlugin.ts +33 -0
- package/src/stubs/tapestry-stub.ts +16 -0
- package/src/tapestry-react-shim.ts +7 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { JSCodeshift, JSXAttribute, JSXElement } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
export function getAttribute({
|
|
4
|
+
element,
|
|
5
|
+
|
|
6
|
+
name,
|
|
7
|
+
}: {
|
|
8
|
+
element: JSXElement
|
|
9
|
+
name: string
|
|
10
|
+
}) {
|
|
11
|
+
const attributes = element.openingElement.attributes || []
|
|
12
|
+
const attribute = attributes.find(
|
|
13
|
+
(attr) => attr.type === "JSXAttribute" && attr.name.name === name
|
|
14
|
+
)
|
|
15
|
+
if (!attribute) return
|
|
16
|
+
|
|
17
|
+
return attribute as JSXAttribute
|
|
18
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import jscodeshift, {
|
|
2
|
+
JSXAttribute,
|
|
3
|
+
JSXElement,
|
|
4
|
+
JSXIdentifier,
|
|
5
|
+
} from "jscodeshift"
|
|
6
|
+
import { describe, expect, it } from "vitest"
|
|
7
|
+
|
|
8
|
+
import { getAttributeValue } from "./getAttributeValue"
|
|
9
|
+
|
|
10
|
+
const j = jscodeshift.withParser("tsx")
|
|
11
|
+
|
|
12
|
+
function createElementFromCode(code: string): JSXElement {
|
|
13
|
+
const source = j(`<div>${code}</div>`)
|
|
14
|
+
return source.find(j.JSXElement).at(0).get().value.children?.[0] as JSXElement
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getAttributeFromElement(
|
|
18
|
+
element: JSXElement,
|
|
19
|
+
name: string
|
|
20
|
+
): JSXAttribute | null {
|
|
21
|
+
const attributes = element.openingElement.attributes || []
|
|
22
|
+
return (
|
|
23
|
+
(attributes.find(
|
|
24
|
+
(attr) =>
|
|
25
|
+
attr.type === "JSXAttribute" &&
|
|
26
|
+
(attr.name as JSXIdentifier)?.name === name
|
|
27
|
+
) as JSXAttribute) || null
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("getAttributeValue", () => {
|
|
32
|
+
describe("string literal values", () => {
|
|
33
|
+
it("should extract string literal value", () => {
|
|
34
|
+
const element = createElementFromCode(
|
|
35
|
+
'<Button kind="primary">Save</Button>'
|
|
36
|
+
)
|
|
37
|
+
const kindAttribute = getAttributeFromElement(element, "kind")
|
|
38
|
+
|
|
39
|
+
const value = getAttributeValue({ attribute: kindAttribute, j })
|
|
40
|
+
|
|
41
|
+
expect(value).toBe("primary")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("should handle empty string literal", () => {
|
|
45
|
+
const element = createElementFromCode('<Button title="">Save</Button>')
|
|
46
|
+
const titleAttribute = getAttributeFromElement(element, "title")
|
|
47
|
+
|
|
48
|
+
const value = getAttributeValue({ attribute: titleAttribute, j })
|
|
49
|
+
|
|
50
|
+
expect(value).toBe("")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("should handle strings with special characters", () => {
|
|
54
|
+
const element = createElementFromCode(
|
|
55
|
+
'<Button aria-label="Save & Continue">Save</Button>'
|
|
56
|
+
)
|
|
57
|
+
const ariaAttribute = getAttributeFromElement(element, "aria-label")
|
|
58
|
+
|
|
59
|
+
const value = getAttributeValue({ attribute: ariaAttribute, j })
|
|
60
|
+
|
|
61
|
+
expect(value).toBe("Save & Continue")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("should handle strings with quotes", () => {
|
|
65
|
+
const element = createElementFromCode(
|
|
66
|
+
"<Button title='Say \"Hello\"'>Save</Button>"
|
|
67
|
+
)
|
|
68
|
+
const titleAttribute = getAttributeFromElement(element, "title")
|
|
69
|
+
|
|
70
|
+
const value = getAttributeValue({ attribute: titleAttribute, j })
|
|
71
|
+
|
|
72
|
+
expect(value).toBe('Say "Hello"')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe("expression container values", () => {
|
|
77
|
+
it("should convert boolean expression to source code", () => {
|
|
78
|
+
const element = createElementFromCode(
|
|
79
|
+
"<Button disabled={true}>Save</Button>"
|
|
80
|
+
)
|
|
81
|
+
const disabledAttribute = getAttributeFromElement(element, "disabled")
|
|
82
|
+
|
|
83
|
+
const value = getAttributeValue({ attribute: disabledAttribute, j })
|
|
84
|
+
|
|
85
|
+
expect(value).toBe("{true}")
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("should convert variable expression to source code", () => {
|
|
89
|
+
const element = createElementFromCode(
|
|
90
|
+
"<Button onClick={handleClick}>Save</Button>"
|
|
91
|
+
)
|
|
92
|
+
const onClickAttribute = getAttributeFromElement(element, "onClick")
|
|
93
|
+
|
|
94
|
+
const value = getAttributeValue({ attribute: onClickAttribute, j })
|
|
95
|
+
|
|
96
|
+
expect(value).toBe("{handleClick}")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("should convert complex expression to source code", () => {
|
|
100
|
+
const element = createElementFromCode(
|
|
101
|
+
'<Button style={{ color: "red", fontSize: 16 }}>Save</Button>'
|
|
102
|
+
)
|
|
103
|
+
const styleAttribute = getAttributeFromElement(element, "style")
|
|
104
|
+
|
|
105
|
+
const value = getAttributeValue({ attribute: styleAttribute, j })
|
|
106
|
+
|
|
107
|
+
expect(value).toContain("color")
|
|
108
|
+
expect(value).toContain("red")
|
|
109
|
+
expect(value).toContain("fontSize")
|
|
110
|
+
expect(value).toContain("16")
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("should handle function call expressions", () => {
|
|
114
|
+
const element = createElementFromCode(
|
|
115
|
+
'<Button className={getButtonClass("primary")}>Save</Button>'
|
|
116
|
+
)
|
|
117
|
+
const classAttribute = getAttributeFromElement(element, "className")
|
|
118
|
+
|
|
119
|
+
const value = getAttributeValue({ attribute: classAttribute, j })
|
|
120
|
+
|
|
121
|
+
expect(value).toBe('{getButtonClass("primary")}')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("should handle template literal expressions", () => {
|
|
125
|
+
const element = createElementFromCode(
|
|
126
|
+
"<Button className={`btn-${variant}`}>Save</Button>"
|
|
127
|
+
)
|
|
128
|
+
const classAttribute = getAttributeFromElement(element, "className")
|
|
129
|
+
|
|
130
|
+
const value = getAttributeValue({ attribute: classAttribute, j })
|
|
131
|
+
|
|
132
|
+
expect(value).toBe("{`btn-${variant}`}")
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it("should handle ternary expressions", () => {
|
|
136
|
+
const element = createElementFromCode(
|
|
137
|
+
"<Button disabled={loading ? true : false}>Save</Button>"
|
|
138
|
+
)
|
|
139
|
+
const disabledAttribute = getAttributeFromElement(element, "disabled")
|
|
140
|
+
|
|
141
|
+
const value = getAttributeValue({ attribute: disabledAttribute, j })
|
|
142
|
+
|
|
143
|
+
expect(value).toBe("{loading ? true : false}")
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe("null and undefined attributes", () => {
|
|
148
|
+
it("should return null for null attribute", () => {
|
|
149
|
+
const value = getAttributeValue({ attribute: null, j })
|
|
150
|
+
|
|
151
|
+
expect(value).toBeNull()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it("should return null for undefined attribute", () => {
|
|
155
|
+
const value = getAttributeValue({ attribute: undefined, j })
|
|
156
|
+
|
|
157
|
+
expect(value).toBeNull()
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe("attributes without values", () => {
|
|
162
|
+
it("should return null for boolean attribute without value", () => {
|
|
163
|
+
const element = createElementFromCode("<Button disabled>Save</Button>")
|
|
164
|
+
const disabledAttribute = getAttributeFromElement(element, "disabled")
|
|
165
|
+
|
|
166
|
+
const value = getAttributeValue({ attribute: disabledAttribute, j })
|
|
167
|
+
|
|
168
|
+
expect(value).toBeNull()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("should handle attribute with null value", () => {
|
|
172
|
+
const mockAttribute = {
|
|
173
|
+
name: { name: "test", type: "JSXIdentifier" },
|
|
174
|
+
type: "JSXAttribute",
|
|
175
|
+
value: null,
|
|
176
|
+
} as JSXAttribute
|
|
177
|
+
|
|
178
|
+
const value = getAttributeValue({ attribute: mockAttribute, j })
|
|
179
|
+
|
|
180
|
+
expect(value).toBeNull()
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe("edge cases", () => {
|
|
185
|
+
it("should handle numeric expressions", () => {
|
|
186
|
+
const element = createElementFromCode(
|
|
187
|
+
"<Button tabIndex={0}>Save</Button>"
|
|
188
|
+
)
|
|
189
|
+
const tabIndexAttribute = getAttributeFromElement(element, "tabIndex")
|
|
190
|
+
|
|
191
|
+
const value = getAttributeValue({ attribute: tabIndexAttribute, j })
|
|
192
|
+
|
|
193
|
+
expect(value).toBe("{0}")
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("should handle array expressions", () => {
|
|
197
|
+
const element = createElementFromCode(
|
|
198
|
+
"<Button data={[1, 2, 3]}>Save</Button>"
|
|
199
|
+
)
|
|
200
|
+
const dataAttribute = getAttributeFromElement(element, "data")
|
|
201
|
+
|
|
202
|
+
const value = getAttributeValue({ attribute: dataAttribute, j })
|
|
203
|
+
|
|
204
|
+
expect(value).toBe("{[1, 2, 3]}")
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it("should handle nested object expressions", () => {
|
|
208
|
+
const element = createElementFromCode(
|
|
209
|
+
'<Button config={{ theme: { primary: "blue" } }}>Save</Button>'
|
|
210
|
+
)
|
|
211
|
+
const configAttribute = getAttributeFromElement(element, "config")
|
|
212
|
+
|
|
213
|
+
const value = getAttributeValue({ attribute: configAttribute, j })
|
|
214
|
+
|
|
215
|
+
expect(value).toContain("theme")
|
|
216
|
+
expect(value).toContain("primary")
|
|
217
|
+
expect(value).toContain("blue")
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it("should handle JSX element expressions", () => {
|
|
221
|
+
const element = createElementFromCode(
|
|
222
|
+
'<Button icon={<Icon name="save" />}>Save</Button>'
|
|
223
|
+
)
|
|
224
|
+
const iconAttribute = getAttributeFromElement(element, "icon")
|
|
225
|
+
|
|
226
|
+
const value = getAttributeValue({ attribute: iconAttribute, j })
|
|
227
|
+
|
|
228
|
+
expect(value).toBe('{<Icon name="save" />}')
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe("integration scenarios", () => {
|
|
233
|
+
it("should handle mixed attribute types on same element", () => {
|
|
234
|
+
const element = createElementFromCode(
|
|
235
|
+
'<Button kind="primary" disabled={true} onClick={handleClick} data-testid="save-btn">Save</Button>'
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
const kindValue = getAttributeValue({
|
|
239
|
+
attribute: getAttributeFromElement(element, "kind"),
|
|
240
|
+
j,
|
|
241
|
+
})
|
|
242
|
+
const disabledValue = getAttributeValue({
|
|
243
|
+
attribute: getAttributeFromElement(element, "disabled"),
|
|
244
|
+
j,
|
|
245
|
+
})
|
|
246
|
+
const onClickValue = getAttributeValue({
|
|
247
|
+
attribute: getAttributeFromElement(element, "onClick"),
|
|
248
|
+
j,
|
|
249
|
+
})
|
|
250
|
+
const testIdValue = getAttributeValue({
|
|
251
|
+
attribute: getAttributeFromElement(element, "data-testid"),
|
|
252
|
+
j,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
expect(kindValue).toBe("primary")
|
|
256
|
+
expect(disabledValue).toBe("{true}")
|
|
257
|
+
expect(onClickValue).toBe("{handleClick}")
|
|
258
|
+
expect(testIdValue).toBe("save-btn")
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { JSCodeshift, JSXAttribute } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
export function getAttributeValue({
|
|
4
|
+
attribute,
|
|
5
|
+
j,
|
|
6
|
+
}: {
|
|
7
|
+
attribute: JSXAttribute | null | undefined
|
|
8
|
+
j: JSCodeshift
|
|
9
|
+
}) {
|
|
10
|
+
return attribute && attribute.value
|
|
11
|
+
? attribute?.value?.type === "StringLiteral"
|
|
12
|
+
? attribute.value.value
|
|
13
|
+
: j(attribute.value).toSource()
|
|
14
|
+
: null
|
|
15
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JSCodeshift,
|
|
3
|
+
JSXAttribute,
|
|
4
|
+
JSXElement,
|
|
5
|
+
JSXSpreadAttribute,
|
|
6
|
+
} from "jscodeshift"
|
|
7
|
+
|
|
8
|
+
import { getAttribute } from "./getAttribute"
|
|
9
|
+
|
|
10
|
+
export function getAttributeValueAsProps({
|
|
11
|
+
j,
|
|
12
|
+
element,
|
|
13
|
+
name,
|
|
14
|
+
stringValueKey,
|
|
15
|
+
}: {
|
|
16
|
+
element: JSXElement
|
|
17
|
+
j: JSCodeshift
|
|
18
|
+
name: string
|
|
19
|
+
stringValueKey?: string
|
|
20
|
+
}): JSXSpreadAttribute[] {
|
|
21
|
+
const attribute = getAttribute({ element, name })
|
|
22
|
+
if (attribute && attribute.value) {
|
|
23
|
+
if (attribute.value.type === "StringLiteral") {
|
|
24
|
+
return stringToProps({ attribute, j, stringValueKey })
|
|
25
|
+
}
|
|
26
|
+
if (attribute.value.type === "JSXExpressionContainer") {
|
|
27
|
+
if (attribute.value.expression.type !== "JSXEmptyExpression") {
|
|
28
|
+
return [j.jsxSpreadAttribute(attribute.value.expression)]
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
return [j.jsxSpreadAttribute(attribute.value)]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stringToProps({
|
|
39
|
+
attribute,
|
|
40
|
+
j,
|
|
41
|
+
stringValueKey,
|
|
42
|
+
}: {
|
|
43
|
+
attribute: JSXAttribute
|
|
44
|
+
j: JSCodeshift
|
|
45
|
+
stringValueKey?: string
|
|
46
|
+
}): JSXSpreadAttribute[] {
|
|
47
|
+
if (!attribute.value) return []
|
|
48
|
+
if (stringValueKey === undefined) return []
|
|
49
|
+
|
|
50
|
+
return [
|
|
51
|
+
j.jsxSpreadAttribute(
|
|
52
|
+
j.objectExpression([
|
|
53
|
+
j.objectProperty(j.identifier(stringValueKey), attribute.value),
|
|
54
|
+
])
|
|
55
|
+
),
|
|
56
|
+
]
|
|
57
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import jscodeshift, {
|
|
2
|
+
JSXAttribute,
|
|
3
|
+
JSXElement,
|
|
4
|
+
JSXIdentifier,
|
|
5
|
+
StringLiteral,
|
|
6
|
+
} from "jscodeshift"
|
|
7
|
+
import { describe, expect, it } from "vitest"
|
|
8
|
+
|
|
9
|
+
import { removeDuplicateKeys } from "./removeDuplicateKeys"
|
|
10
|
+
|
|
11
|
+
const j = jscodeshift.withParser("tsx")
|
|
12
|
+
|
|
13
|
+
function buildSourceAndElement(code: string) {
|
|
14
|
+
const source = j(`<div>${code}</div>`)
|
|
15
|
+
const element = source.find(j.JSXElement).at(0).get().value
|
|
16
|
+
.children?.[0] as JSXElement
|
|
17
|
+
return { element, source }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getAttributeFromElement(
|
|
21
|
+
element: JSXElement,
|
|
22
|
+
name: string
|
|
23
|
+
): JSXAttribute | null {
|
|
24
|
+
const attributes = element.openingElement.attributes || []
|
|
25
|
+
return (
|
|
26
|
+
(attributes.find(
|
|
27
|
+
(attr) =>
|
|
28
|
+
attr.type === "JSXAttribute" &&
|
|
29
|
+
(attr.name as JSXIdentifier)?.name === name
|
|
30
|
+
) as JSXAttribute) || null
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getAttributeNames(element: JSXElement): string[] {
|
|
35
|
+
const attributes = element.openingElement.attributes || []
|
|
36
|
+
return attributes
|
|
37
|
+
.filter((attr) => attr.type === "JSXAttribute")
|
|
38
|
+
.map((attr) => (attr as JSXAttribute).name?.name || "")
|
|
39
|
+
.filter(Boolean) as string[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("removeDuplicateKeys", () => {
|
|
43
|
+
describe("elements with duplicate attributes", () => {
|
|
44
|
+
it("should remove duplicate string attributes, keeping the first occurrence", () => {
|
|
45
|
+
const { source, element } = buildSourceAndElement(
|
|
46
|
+
'<Button kind="primary" disabled kind="secondary">Save</Button>'
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const result = removeDuplicateKeys(element, { j, source })
|
|
50
|
+
|
|
51
|
+
expect(result).toBe(true)
|
|
52
|
+
|
|
53
|
+
const attributes = getAttributeNames(element)
|
|
54
|
+
expect(attributes).toEqual(["kind", "disabled"])
|
|
55
|
+
|
|
56
|
+
const kindAttr = getAttributeFromElement(element, "kind")
|
|
57
|
+
expect(kindAttr?.value?.type).toBe("StringLiteral")
|
|
58
|
+
expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("should handle multiple sets of duplicate attributes", () => {
|
|
62
|
+
const { source, element } = buildSourceAndElement(
|
|
63
|
+
'<Button kind="primary" size="large" kind="secondary" disabled size="small">Save</Button>'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const result = removeDuplicateKeys(element, { j, source })
|
|
67
|
+
|
|
68
|
+
expect(result).toBe(true)
|
|
69
|
+
|
|
70
|
+
const attributes = getAttributeNames(element)
|
|
71
|
+
expect(attributes).toEqual(["kind", "size", "disabled"])
|
|
72
|
+
|
|
73
|
+
const kindAttr = getAttributeFromElement(element, "kind")
|
|
74
|
+
const sizeAttr = getAttributeFromElement(element, "size")
|
|
75
|
+
expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
|
|
76
|
+
expect((sizeAttr?.value as StringLiteral)?.value).toBe("large")
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("should handle duplicate boolean attributes", () => {
|
|
80
|
+
const { source, element } = buildSourceAndElement(
|
|
81
|
+
"<Button disabled loading disabled>Save</Button>"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const result = removeDuplicateKeys(element, { j, source })
|
|
85
|
+
|
|
86
|
+
expect(result).toBe(true)
|
|
87
|
+
|
|
88
|
+
const attributes = getAttributeNames(element)
|
|
89
|
+
expect(attributes).toEqual(["disabled", "loading"])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("should handle duplicate expression attributes", () => {
|
|
93
|
+
const { source, element } = buildSourceAndElement(
|
|
94
|
+
"<Button onClick={handleClick} disabled={true} onClick={handleOtherClick}>Save</Button>"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const result = removeDuplicateKeys(element, { j, source })
|
|
98
|
+
|
|
99
|
+
expect(result).toBe(true)
|
|
100
|
+
|
|
101
|
+
const attributes = getAttributeNames(element)
|
|
102
|
+
expect(attributes).toEqual(["onClick", "disabled"])
|
|
103
|
+
|
|
104
|
+
const onClickAttr = getAttributeFromElement(element, "onClick")
|
|
105
|
+
expect(onClickAttr?.value?.type).toBe("JSXExpressionContainer")
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("should preserve attribute order for non-duplicates", () => {
|
|
109
|
+
const { source, element } = buildSourceAndElement(
|
|
110
|
+
'<Button aria-label="Save" kind="primary" data-testid="btn" kind="secondary" className="btn">Save</Button>'
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const result = removeDuplicateKeys(element, { j, source })
|
|
114
|
+
|
|
115
|
+
expect(result).toBe(true)
|
|
116
|
+
|
|
117
|
+
const attributes = getAttributeNames(element)
|
|
118
|
+
expect(attributes).toEqual([
|
|
119
|
+
"aria-label",
|
|
120
|
+
"kind",
|
|
121
|
+
"data-testid",
|
|
122
|
+
"className",
|
|
123
|
+
])
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe("elements without duplicate attributes", () => {
|
|
128
|
+
it("should return false when no duplicates exist", () => {
|
|
129
|
+
const { source, element } = buildSourceAndElement(
|
|
130
|
+
'<Button kind="primary" size="large" disabled>Save</Button>'
|
|
131
|
+
)
|
|
132
|
+
const initialAttributeCount =
|
|
133
|
+
element.openingElement.attributes?.length || 0
|
|
134
|
+
|
|
135
|
+
const result = removeDuplicateKeys(element, { j, source })
|
|
136
|
+
|
|
137
|
+
expect(result).toBe(false)
|
|
138
|
+
expect(element.openingElement.attributes).toHaveLength(
|
|
139
|
+
initialAttributeCount
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const attributes = getAttributeNames(element)
|
|
143
|
+
expect(attributes).toEqual(["kind", "size", "disabled"])
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it("should handle single attribute elements", () => {
|
|
147
|
+
const { source, element } = buildSourceAndElement(
|
|
148
|
+
'<Button kind="primary">Save</Button>'
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const result = removeDuplicateKeys(element, { j, source })
|
|
152
|
+
|
|
153
|
+
expect(result).toBe(false)
|
|
154
|
+
|
|
155
|
+
const attributes = getAttributeNames(element)
|
|
156
|
+
expect(attributes).toEqual(["kind"])
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("should handle elements with no attributes", () => {
|
|
160
|
+
const { source, element } = buildSourceAndElement("<Button>Save</Button>")
|
|
161
|
+
|
|
162
|
+
const result = removeDuplicateKeys(element, { j, source })
|
|
163
|
+
|
|
164
|
+
expect(result).toBe(false)
|
|
165
|
+
|
|
166
|
+
const attributes = getAttributeNames(element)
|
|
167
|
+
expect(attributes).toEqual([])
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
describe("edge cases", () => {
|
|
172
|
+
it("should handle JSX spread attributes (ignore them)", () => {
|
|
173
|
+
const { source, element } = buildSourceAndElement(
|
|
174
|
+
'<Button kind="primary" kind="secondary">Save</Button>'
|
|
175
|
+
)
|
|
176
|
+
const spreadAttr = j.jsxSpreadAttribute(j.identifier("props"))
|
|
177
|
+
element.openingElement.attributes?.push(spreadAttr)
|
|
178
|
+
|
|
179
|
+
const result = removeDuplicateKeys(element, { j, source })
|
|
180
|
+
|
|
181
|
+
expect(result).toBe(true)
|
|
182
|
+
expect(element.openingElement.attributes).toHaveLength(2)
|
|
183
|
+
const attributes = getAttributeNames(element)
|
|
184
|
+
expect(attributes).toEqual(["kind"])
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it("should handle elements with only spread attributes", () => {
|
|
188
|
+
const { source, element } = buildSourceAndElement("<Button>Save</Button>")
|
|
189
|
+
const spreadAttr1 = j.jsxSpreadAttribute(j.identifier("props1"))
|
|
190
|
+
const spreadAttr2 = j.jsxSpreadAttribute(j.identifier("props2"))
|
|
191
|
+
element.openingElement.attributes = [spreadAttr1, spreadAttr2]
|
|
192
|
+
|
|
193
|
+
const result = removeDuplicateKeys(element, { j, source })
|
|
194
|
+
|
|
195
|
+
expect(result).toBe(false)
|
|
196
|
+
expect(element.openingElement.attributes).toHaveLength(2)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it("should handle elements with mixed attribute types", () => {
|
|
200
|
+
const { source, element } = buildSourceAndElement(
|
|
201
|
+
'<Button kind="primary">Save</Button>'
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const spreadAttr = j.jsxSpreadAttribute(j.identifier("otherProps"))
|
|
205
|
+
element.openingElement.attributes?.push(spreadAttr)
|
|
206
|
+
|
|
207
|
+
const result = removeDuplicateKeys(element, { j, source })
|
|
208
|
+
|
|
209
|
+
expect(result).toBe(false)
|
|
210
|
+
expect(element.openingElement.attributes).toHaveLength(2)
|
|
211
|
+
|
|
212
|
+
const attributes = getAttributeNames(element)
|
|
213
|
+
expect(attributes).toEqual(["kind"])
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it("should handle many duplicate attributes efficiently", () => {
|
|
217
|
+
const { source, element } = buildSourceAndElement(
|
|
218
|
+
'<Button kind="1" kind="2" kind="3" kind="4" kind="5" size="1" size="2" size="3">Save</Button>'
|
|
219
|
+
)
|
|
220
|
+
const initialCount = element.openingElement.attributes?.length || 0
|
|
221
|
+
|
|
222
|
+
const result = removeDuplicateKeys(element, { j, source })
|
|
223
|
+
|
|
224
|
+
expect(result).toBe(true)
|
|
225
|
+
|
|
226
|
+
const attributes = getAttributeNames(element)
|
|
227
|
+
expect(attributes).toEqual(["kind", "size"])
|
|
228
|
+
expect(element.openingElement.attributes?.length).toBe(2)
|
|
229
|
+
expect(initialCount).toBeGreaterThan(2)
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe("integration with JSCodeshift", () => {
|
|
234
|
+
it("should work correctly when element is part of a larger AST", () => {
|
|
235
|
+
const source = j(`
|
|
236
|
+
<div>
|
|
237
|
+
<Button kind="primary" onClick={handleClick} kind="secondary">Save</Button>
|
|
238
|
+
<Button size="large">Cancel</Button>
|
|
239
|
+
</div>
|
|
240
|
+
`)
|
|
241
|
+
|
|
242
|
+
const buttonElements = source
|
|
243
|
+
.find(j.JSXElement)
|
|
244
|
+
.filter(
|
|
245
|
+
(path) =>
|
|
246
|
+
(path.value.openingElement.name as JSXIdentifier)?.name === "Button"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
const firstButton = buttonElements.at(0).get().value as JSXElement
|
|
250
|
+
const result = removeDuplicateKeys(firstButton, { j, source })
|
|
251
|
+
|
|
252
|
+
expect(result).toBe(true)
|
|
253
|
+
|
|
254
|
+
const attributes = getAttributeNames(firstButton)
|
|
255
|
+
expect(attributes).toEqual(["kind", "onClick"])
|
|
256
|
+
|
|
257
|
+
const rendered = source.toSource()
|
|
258
|
+
expect(rendered).toContain('kind="primary"')
|
|
259
|
+
expect(rendered).not.toContain('kind="secondary"')
|
|
260
|
+
expect(rendered).toContain("onClick={handleClick}")
|
|
261
|
+
expect(rendered).toContain('size="large"')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it("should maintain proper JSX structure after removing duplicates", () => {
|
|
265
|
+
const { source, element } = buildSourceAndElement(
|
|
266
|
+
'<Button kind="primary" disabled kind="secondary" loading>Text</Button>'
|
|
267
|
+
)
|
|
268
|
+
removeDuplicateKeys(element, { j, source })
|
|
269
|
+
|
|
270
|
+
const result = source.toSource()
|
|
271
|
+
|
|
272
|
+
expect(result).toContain("<Button")
|
|
273
|
+
expect(result).toContain('kind="primary"')
|
|
274
|
+
expect(result).toContain("disabled")
|
|
275
|
+
expect(result).toContain("loading")
|
|
276
|
+
expect(result).toContain(">Text</Button>")
|
|
277
|
+
expect(result).not.toContain('kind="secondary"')
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
})
|