@planningcenter/tapestry-migration-cli 3.1.0-rc.2 → 3.1.0-rc.21

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 (115) hide show
  1. package/dist/tapestry-react-shim.cjs +7 -1
  2. package/package.json +3 -3
  3. package/src/components/button/transforms/convertStyleProps.test.ts +97 -0
  4. package/src/components/button/transforms/removeTypeButton.test.ts +0 -1
  5. package/src/components/checkbox/transforms/moveCheckboxImport.test.ts +3 -0
  6. package/src/components/input/index.ts +66 -0
  7. package/src/components/input/transformableInput.ts +49 -0
  8. package/src/components/input/transforms/auditSpreadProps.test.ts +192 -0
  9. package/src/components/input/transforms/auditSpreadProps.ts +26 -0
  10. package/src/components/input/transforms/autoWidthTransform.test.ts +172 -0
  11. package/src/components/input/transforms/autoWidthTransform.ts +41 -0
  12. package/src/components/input/transforms/convertStyleProps.test.ts +128 -0
  13. package/src/components/input/transforms/convertStyleProps.ts +12 -0
  14. package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.test.ts +186 -0
  15. package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.ts +27 -0
  16. package/src/components/input/transforms/inputLabelToLabelProp.test.ts +319 -0
  17. package/src/components/input/transforms/inputLabelToLabelProp.ts +203 -0
  18. package/src/components/input/transforms/mergeFieldIntoInput.test.ts +469 -0
  19. package/src/components/input/transforms/mergeFieldIntoInput.ts +7 -0
  20. package/src/components/input/transforms/mergeInputLabel.test.ts +458 -0
  21. package/src/components/input/transforms/mergeInputLabel.ts +204 -0
  22. package/src/components/input/transforms/moveInputImport.test.ts +166 -0
  23. package/src/components/input/transforms/moveInputImport.ts +14 -0
  24. package/src/components/input/transforms/numberFieldAddTypeNumber.test.ts +92 -0
  25. package/src/components/input/transforms/numberFieldAddTypeNumber.ts +14 -0
  26. package/src/components/input/transforms/numberFieldRenameToInput.test.ts +126 -0
  27. package/src/components/input/transforms/numberFieldRenameToInput.ts +9 -0
  28. package/src/components/input/transforms/removeAsInput.test.ts +139 -0
  29. package/src/components/input/transforms/removeAsInput.ts +20 -0
  30. package/src/components/input/transforms/removeDuplicateKeys.test.ts +302 -0
  31. package/src/components/input/transforms/removeDuplicateKeys.ts +10 -0
  32. package/src/components/input/transforms/removeInputBox.test.ts +352 -0
  33. package/src/components/input/transforms/removeInputBox.ts +109 -0
  34. package/src/components/input/transforms/removeRedundantAriaLabel.test.ts +128 -0
  35. package/src/components/input/transforms/removeRedundantAriaLabel.ts +21 -0
  36. package/src/components/input/transforms/removeTypeInput.test.ts +212 -0
  37. package/src/components/input/transforms/removeTypeInput.ts +22 -0
  38. package/src/components/input/transforms/removeTypeText.test.ts +160 -0
  39. package/src/components/input/transforms/removeTypeText.ts +17 -0
  40. package/src/components/input/transforms/sizeMapping.test.ts +198 -0
  41. package/src/components/input/transforms/sizeMapping.ts +17 -0
  42. package/src/components/input/transforms/skipRenderSideProps.test.ts +236 -0
  43. package/src/components/input/transforms/skipRenderSideProps.ts +27 -0
  44. package/src/components/input/transforms/stateToInvalid.test.ts +208 -0
  45. package/src/components/input/transforms/stateToInvalid.ts +59 -0
  46. package/src/components/input/transforms/stateToInvalidTernary.test.ts +159 -0
  47. package/src/components/input/transforms/stateToInvalidTernary.ts +13 -0
  48. package/src/components/input/transforms/unsupportedProps.test.ts +566 -0
  49. package/src/components/input/transforms/unsupportedProps.ts +84 -0
  50. package/src/components/link/transforms/reviewStyles.test.ts +0 -1
  51. package/src/components/select/index.ts +58 -0
  52. package/src/components/select/transformableSelect.ts +7 -0
  53. package/src/components/select/transforms/auditSpreadProps.test.ts +103 -0
  54. package/src/components/select/transforms/auditSpreadProps.ts +26 -0
  55. package/src/components/select/transforms/childrenToOptions.test.ts +367 -0
  56. package/src/components/select/transforms/childrenToOptions.ts +295 -0
  57. package/src/components/select/transforms/convertLegacyOptions.test.ts +150 -0
  58. package/src/components/select/transforms/convertLegacyOptions.ts +105 -0
  59. package/src/components/select/transforms/convertStyleProps.test.ts +73 -0
  60. package/src/components/select/transforms/convertStyleProps.ts +12 -0
  61. package/src/components/select/transforms/emptyValueToPlaceholder.test.ts +122 -0
  62. package/src/components/select/transforms/emptyValueToPlaceholder.ts +22 -0
  63. package/src/components/select/transforms/innerRefToRef.test.ts +89 -0
  64. package/src/components/select/transforms/innerRefToRef.ts +18 -0
  65. package/src/components/select/transforms/mapChildrenToOptions.test.ts +521 -0
  66. package/src/components/select/transforms/mapChildrenToOptions.ts +312 -0
  67. package/src/components/select/transforms/mergeFieldIntoSelect.test.ts +506 -0
  68. package/src/components/select/transforms/mergeFieldIntoSelect.ts +7 -0
  69. package/src/components/select/transforms/mergeSelectLabel.test.ts +458 -0
  70. package/src/components/select/transforms/mergeSelectLabel.ts +225 -0
  71. package/src/components/select/transforms/moveSelectImport.test.ts +148 -0
  72. package/src/components/select/transforms/moveSelectImport.ts +14 -0
  73. package/src/components/select/transforms/removeDefaultProps.test.ts +249 -0
  74. package/src/components/select/transforms/removeDefaultProps.ts +112 -0
  75. package/src/components/select/transforms/sizeMapping.test.ts +188 -0
  76. package/src/components/select/transforms/sizeMapping.ts +17 -0
  77. package/src/components/select/transforms/skipMultipleSelect.test.ts +148 -0
  78. package/src/components/select/transforms/skipMultipleSelect.ts +23 -0
  79. package/src/components/select/transforms/stateToInvalid.test.ts +217 -0
  80. package/src/components/select/transforms/stateToInvalid.ts +59 -0
  81. package/src/components/select/transforms/stateToInvalidTernary.test.ts +146 -0
  82. package/src/components/select/transforms/stateToInvalidTernary.ts +13 -0
  83. package/src/components/select/transforms/unsupportedProps.test.ts +252 -0
  84. package/src/components/select/transforms/unsupportedProps.ts +44 -0
  85. package/src/components/shared/helpers/getAttributeExpression.ts +26 -0
  86. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +102 -0
  87. package/src/components/shared/transformFactories/helpers/manageImports.ts +14 -12
  88. package/src/components/shared/transformFactories/mergeFieldFactory.ts +244 -0
  89. package/src/components/shared/transformFactories/sizeMappingFactory.ts +9 -2
  90. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +56 -17
  91. package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +65 -0
  92. package/src/components/text-area/index.ts +48 -0
  93. package/src/components/text-area/transforms/auditSpreadProps.test.ts +139 -0
  94. package/src/components/text-area/transforms/auditSpreadProps.ts +10 -0
  95. package/src/components/text-area/transforms/convertStyleProps.test.ts +158 -0
  96. package/src/components/text-area/transforms/convertStyleProps.ts +10 -0
  97. package/src/components/text-area/transforms/innerRefToRef.test.ts +206 -0
  98. package/src/components/text-area/transforms/innerRefToRef.ts +14 -0
  99. package/src/components/text-area/transforms/mergeFieldIntoTextArea.test.ts +477 -0
  100. package/src/components/text-area/transforms/mergeFieldIntoTextArea.ts +5 -0
  101. package/src/components/text-area/transforms/moveTextAreaImport.test.ts +168 -0
  102. package/src/components/text-area/transforms/moveTextAreaImport.ts +13 -0
  103. package/src/components/text-area/transforms/removeDuplicateKeys.test.ts +129 -0
  104. package/src/components/text-area/transforms/removeDuplicateKeys.ts +8 -0
  105. package/src/components/text-area/transforms/removeRedundantAriaLabel.test.ts +183 -0
  106. package/src/components/text-area/transforms/removeRedundantAriaLabel.ts +59 -0
  107. package/src/components/text-area/transforms/sizeMapping.test.ts +199 -0
  108. package/src/components/text-area/transforms/sizeMapping.ts +15 -0
  109. package/src/components/text-area/transforms/stateToInvalid.test.ts +204 -0
  110. package/src/components/text-area/transforms/stateToInvalid.ts +57 -0
  111. package/src/components/text-area/transforms/stateToInvalidTernary.test.ts +133 -0
  112. package/src/components/text-area/transforms/stateToInvalidTernary.ts +11 -0
  113. package/src/components/text-area/transforms/unsupportedProps.test.ts +275 -0
  114. package/src/components/text-area/transforms/unsupportedProps.ts +35 -0
  115. package/src/index.ts +2 -1
@@ -0,0 +1,319 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./inputLabelToLabelProp"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string | null {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ return transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ {}
14
+ ) as string | null
15
+ }
16
+
17
+ describe("inputLabelToLabelProp transform", () => {
18
+ describe("successful conversions", () => {
19
+ it("InputLabel before Input → label prop added, InputLabel removed", () => {
20
+ const input = `
21
+ import { Input, StackView } from "@planningcenter/tapestry-react"
22
+
23
+ function Test() {
24
+ return (
25
+ <StackView>
26
+ <Input.InputLabel htmlFor="f1">Name</Input.InputLabel>
27
+ <Input id="f1" />
28
+ </StackView>
29
+ )
30
+ }
31
+ `.trim()
32
+
33
+ const result = applyTransform(input)
34
+ expect(result).not.toBeNull()
35
+ expect(result).toContain('label="Name"')
36
+ expect(result).not.toContain("<Input.InputLabel")
37
+ })
38
+
39
+ it("Input before InputLabel → label prop added, InputLabel removed", () => {
40
+ const input = `
41
+ import { Input, StackView } from "@planningcenter/tapestry-react"
42
+
43
+ function Test() {
44
+ return (
45
+ <StackView>
46
+ <Input id="f1" />
47
+ <Input.InputLabel htmlFor="f1">Email</Input.InputLabel>
48
+ </StackView>
49
+ )
50
+ }
51
+ `.trim()
52
+
53
+ const result = applyTransform(input)
54
+ expect(result).not.toBeNull()
55
+ expect(result).toContain('label="Email"')
56
+ expect(result).not.toContain("<Input.InputLabel")
57
+ })
58
+
59
+ it("no axis prop (default vertical) → transforms", () => {
60
+ const input = `
61
+ import { Input, StackView } from "@planningcenter/tapestry-react"
62
+
63
+ function Test() {
64
+ return (
65
+ <StackView>
66
+ <Input.InputLabel>Address</Input.InputLabel>
67
+ <Input id="addr" />
68
+ </StackView>
69
+ )
70
+ }
71
+ `.trim()
72
+
73
+ const result = applyTransform(input)
74
+ expect(result).not.toBeNull()
75
+ expect(result).toContain('label="Address"')
76
+ })
77
+
78
+ it('axis="vertical" explicitly → transforms', () => {
79
+ const input = `
80
+ import { Input, StackView } from "@planningcenter/tapestry-react"
81
+
82
+ function Test() {
83
+ return (
84
+ <StackView axis="vertical">
85
+ <Input.InputLabel>City</Input.InputLabel>
86
+ <Input id="city" />
87
+ </StackView>
88
+ )
89
+ }
90
+ `.trim()
91
+
92
+ const result = applyTransform(input)
93
+ expect(result).not.toBeNull()
94
+ expect(result).toContain('label="City"')
95
+ })
96
+
97
+ it("axis={someVar} (dynamic, non-string-literal) → transforms (best-effort)", () => {
98
+ const input = `
99
+ import { Input, StackView } from "@planningcenter/tapestry-react"
100
+
101
+ function Test() {
102
+ return (
103
+ <StackView axis={someVar}>
104
+ <Input.InputLabel>State</Input.InputLabel>
105
+ <Input id="state" />
106
+ </StackView>
107
+ )
108
+ }
109
+ `.trim()
110
+
111
+ const result = applyTransform(input)
112
+ expect(result).not.toBeNull()
113
+ expect(result).toContain('label="State"')
114
+ })
115
+
116
+ it("multiple label+input pairs in one StackView → both transformed", () => {
117
+ const input = `
118
+ import { Input, StackView } from "@planningcenter/tapestry-react"
119
+
120
+ function Test() {
121
+ return (
122
+ <StackView>
123
+ <Input.InputLabel>First Name</Input.InputLabel>
124
+ <Input id="first" />
125
+ <Input.InputLabel>Last Name</Input.InputLabel>
126
+ <Input id="last" />
127
+ </StackView>
128
+ )
129
+ }
130
+ `.trim()
131
+
132
+ const result = applyTransform(input)
133
+ expect(result).not.toBeNull()
134
+ expect(result).toContain('label="First Name"')
135
+ expect(result).toContain('label="Last Name"')
136
+ expect(result).not.toContain("<Input.InputLabel")
137
+ })
138
+ })
139
+
140
+ describe("no-op / comment cases", () => {
141
+ it('axis="horizontal" → no transform, returns null', () => {
142
+ const input = `
143
+ import { Input, StackView } from "@planningcenter/tapestry-react"
144
+
145
+ function Test() {
146
+ return (
147
+ <StackView axis="horizontal">
148
+ <Input.InputLabel>Name</Input.InputLabel>
149
+ <Input id="f1" />
150
+ </StackView>
151
+ )
152
+ }
153
+ `.trim()
154
+
155
+ const result = applyTransform(input)
156
+ expect(result).toBeNull()
157
+ })
158
+
159
+ it("Input has renderLeft → no transform for that pair, returns null", () => {
160
+ const input = `
161
+ import { Input, StackView } from "@planningcenter/tapestry-react"
162
+
163
+ function Test() {
164
+ return (
165
+ <StackView>
166
+ <Input.InputLabel>Name</Input.InputLabel>
167
+ <Input id="f1" renderLeft={() => <span />} />
168
+ </StackView>
169
+ )
170
+ }
171
+ `.trim()
172
+
173
+ const result = applyTransform(input)
174
+ expect(result).toBeNull()
175
+ })
176
+
177
+ it("Input has renderRight → no transform for that pair, returns null", () => {
178
+ const input = `
179
+ import { Input, StackView } from "@planningcenter/tapestry-react"
180
+
181
+ function Test() {
182
+ return (
183
+ <StackView>
184
+ <Input.InputLabel>Name</Input.InputLabel>
185
+ <Input id="f1" renderRight={() => <span />} />
186
+ </StackView>
187
+ )
188
+ }
189
+ `.trim()
190
+
191
+ const result = applyTransform(input)
192
+ expect(result).toBeNull()
193
+ })
194
+
195
+ it("Input already has label prop → TODO comment added, InputLabel not removed", () => {
196
+ const input = `
197
+ import { Input, StackView } from "@planningcenter/tapestry-react"
198
+
199
+ function Test() {
200
+ return (
201
+ <StackView>
202
+ <Input.InputLabel>New Label</Input.InputLabel>
203
+ <Input id="f1" label="Existing" />
204
+ </StackView>
205
+ )
206
+ }
207
+ `.trim()
208
+
209
+ const result = applyTransform(input)
210
+ expect(result).not.toBeNull()
211
+ expect(result).toContain(
212
+ "TODO: tapestry-migration (inputLabelToLabelProp)"
213
+ )
214
+ expect(result).toContain("already has a label prop")
215
+ expect(result).toContain("New Label")
216
+ expect(result).toContain('label="Existing"')
217
+ // Should not add a second label attribute
218
+ expect(result?.match(/label=/g)?.length).toBe(1)
219
+ })
220
+
221
+ it("InputLabel has complex children (JSX nodes) → TODO comment added, InputLabel not removed", () => {
222
+ const input = `
223
+ import { Input, StackView } from "@planningcenter/tapestry-react"
224
+
225
+ function Test() {
226
+ return (
227
+ <StackView>
228
+ <Input.InputLabel><span>Name <em>*</em></span></Input.InputLabel>
229
+ <Input id="f1" />
230
+ </StackView>
231
+ )
232
+ }
233
+ `.trim()
234
+
235
+ const result = applyTransform(input)
236
+ expect(result).not.toBeNull()
237
+ expect(result).toContain(
238
+ "TODO: tapestry-migration (inputLabelToLabelProp)"
239
+ )
240
+ expect(result).toContain("complex and cannot be auto-converted")
241
+ expect(result).toContain("<Input.InputLabel")
242
+ expect(result).not.toContain('label="')
243
+ })
244
+
245
+ it("Input and InputLabel not adjacent (other element between them) → no transform", () => {
246
+ const input = `
247
+ import { Input, StackView } from "@planningcenter/tapestry-react"
248
+
249
+ function Test() {
250
+ return (
251
+ <StackView>
252
+ <Input.InputLabel>Name</Input.InputLabel>
253
+ <span>hint</span>
254
+ <Input id="f1" />
255
+ </StackView>
256
+ )
257
+ }
258
+ `.trim()
259
+
260
+ const result = applyTransform(input)
261
+ expect(result).toBeNull()
262
+ })
263
+
264
+ it("no StackView wrapper → no transform, returns null", () => {
265
+ const input = `
266
+ import { Input, StackView } from "@planningcenter/tapestry-react"
267
+
268
+ function Test() {
269
+ return (
270
+ <div>
271
+ <Input.InputLabel>Name</Input.InputLabel>
272
+ <Input id="f1" />
273
+ </div>
274
+ )
275
+ }
276
+ `.trim()
277
+
278
+ const result = applyTransform(input)
279
+ expect(result).toBeNull()
280
+ })
281
+
282
+ it("StackView from different package → no transform, returns null", () => {
283
+ const input = `
284
+ import { Input } from "@planningcenter/tapestry-react"
285
+ import { StackView } from "other-library"
286
+
287
+ function Test() {
288
+ return (
289
+ <StackView>
290
+ <Input.InputLabel>Name</Input.InputLabel>
291
+ <Input id="f1" />
292
+ </StackView>
293
+ )
294
+ }
295
+ `.trim()
296
+
297
+ const result = applyTransform(input)
298
+ expect(result).toBeNull()
299
+ })
300
+
301
+ it("StackView not imported at all → no transform, returns null", () => {
302
+ const input = `
303
+ import { Input } from "@planningcenter/tapestry-react"
304
+
305
+ function Test() {
306
+ return (
307
+ <StackView>
308
+ <Input.InputLabel>Name</Input.InputLabel>
309
+ <Input id="f1" />
310
+ </StackView>
311
+ )
312
+ }
313
+ `.trim()
314
+
315
+ const result = applyTransform(input)
316
+ expect(result).toBeNull()
317
+ })
318
+ })
319
+ })
@@ -0,0 +1,203 @@
1
+ import { JSXElement, JSXText, Transform } from "jscodeshift"
2
+
3
+ type JSXChild = NonNullable<JSXElement["children"]>[number]
4
+
5
+ import { addAttribute } from "../../shared/actions/addAttribute"
6
+ import { addComment } from "../../shared/actions/addComment"
7
+ import { getAttribute } from "../../shared/actions/getAttribute"
8
+ import { extractTextContent } from "../../shared/helpers/childrenToLabelHelpers"
9
+ import { getImportName } from "../../shared/transformFactories/helpers/manageImports"
10
+
11
+ const SCOPE = "inputLabelToLabelProp"
12
+
13
+ function isInputLabelElement(node: JSXChild, localName: string): boolean {
14
+ return (
15
+ node.type === "JSXElement" &&
16
+ node.openingElement.name.type === "JSXMemberExpression" &&
17
+ node.openingElement.name.object.type === "JSXIdentifier" &&
18
+ node.openingElement.name.object.name === localName &&
19
+ node.openingElement.name.property.name === "InputLabel"
20
+ )
21
+ }
22
+
23
+ function isInputElement(node: JSXChild, localName: string): boolean {
24
+ return (
25
+ node.type === "JSXElement" &&
26
+ node.openingElement.name.type === "JSXIdentifier" &&
27
+ node.openingElement.name.name === localName
28
+ )
29
+ }
30
+
31
+ function isHorizontalAxis(stackViewElement: JSXElement): boolean {
32
+ const axisAttr = getAttribute({ element: stackViewElement, name: "axis" })
33
+ if (!axisAttr) return false
34
+ const value = axisAttr.value
35
+ if (!value) return false
36
+ if (value.type === "StringLiteral" && value.value === "horizontal")
37
+ return true
38
+ if (
39
+ value.type === "JSXExpressionContainer" &&
40
+ value.expression.type === "StringLiteral" &&
41
+ value.expression.value === "horizontal"
42
+ )
43
+ return true
44
+ return false
45
+ }
46
+
47
+ function hasRenderSideProp(inputElement: JSXElement): boolean {
48
+ const attrs = inputElement.openingElement.attributes || []
49
+ return attrs.some(
50
+ (attr) =>
51
+ attr.type === "JSXAttribute" &&
52
+ attr.name.type === "JSXIdentifier" &&
53
+ (attr.name.name === "renderLeft" || attr.name.name === "renderRight")
54
+ )
55
+ }
56
+
57
+ const transform: Transform = (fileInfo, api) => {
58
+ const j = api.jscodeshift
59
+ const source = j(fileInfo.source)
60
+
61
+ const inputLocalName = getImportName(
62
+ "Input",
63
+ "@planningcenter/tapestry-react",
64
+ {
65
+ j,
66
+ source,
67
+ }
68
+ )
69
+ if (!inputLocalName) return null
70
+
71
+ const stackViewLocalName = getImportName(
72
+ "StackView",
73
+ "@planningcenter/tapestry-react",
74
+ {
75
+ j,
76
+ source,
77
+ }
78
+ )
79
+ if (!stackViewLocalName) return null
80
+
81
+ let hasChanges = false
82
+
83
+ source.find(j.JSXElement).forEach((path) => {
84
+ const el = path.value
85
+ const opening = el.openingElement
86
+
87
+ if (opening.name.type !== "JSXIdentifier") return
88
+ if (opening.name.name !== stackViewLocalName) return
89
+
90
+ if (isHorizontalAxis(el)) return
91
+
92
+ const children = el.children || []
93
+ let i = 0
94
+
95
+ while (i < children.length) {
96
+ const child = children[i]
97
+
98
+ // Skip whitespace JSXText nodes
99
+ if (child.type === "JSXText" && child.value.trim() === "") {
100
+ i++
101
+ continue
102
+ }
103
+
104
+ // Find next non-whitespace sibling
105
+ let nextIdx = i + 1
106
+ while (
107
+ nextIdx < children.length &&
108
+ children[nextIdx].type === "JSXText" &&
109
+ (children[nextIdx] as JSXText).value.trim() === ""
110
+ ) {
111
+ nextIdx++
112
+ }
113
+
114
+ if (nextIdx >= children.length) {
115
+ i++
116
+ continue
117
+ }
118
+
119
+ const nextChild = children[nextIdx]
120
+
121
+ let inputLabelEl: JSXElement | null = null
122
+ let inputEl: JSXElement | null = null
123
+ let labelFirst = false
124
+
125
+ if (
126
+ isInputLabelElement(child, inputLocalName) &&
127
+ isInputElement(nextChild, inputLocalName)
128
+ ) {
129
+ inputLabelEl = child as JSXElement
130
+ inputEl = nextChild as JSXElement
131
+ labelFirst = true
132
+ } else if (
133
+ isInputElement(child, inputLocalName) &&
134
+ isInputLabelElement(nextChild, inputLocalName)
135
+ ) {
136
+ inputEl = child as JSXElement
137
+ inputLabelEl = nextChild as JSXElement
138
+ labelFirst = false
139
+ }
140
+
141
+ if (!inputLabelEl || !inputEl) {
142
+ i++
143
+ continue
144
+ }
145
+
146
+ // Skip if Input has renderLeft or renderRight
147
+ if (hasRenderSideProp(inputEl)) {
148
+ i++
149
+ continue
150
+ }
151
+
152
+ const { isSimpleText, textContent } = extractTextContent(
153
+ inputLabelEl.children || []
154
+ )
155
+
156
+ if (!isSimpleText) {
157
+ addComment({
158
+ element: inputEl,
159
+ j,
160
+ scope: SCOPE,
161
+ source,
162
+ text: "InputLabel children are complex and cannot be auto-converted to a label prop. Please migrate manually.",
163
+ })
164
+ hasChanges = true
165
+ i = nextIdx + 1
166
+ continue
167
+ }
168
+
169
+ const existingLabel = getAttribute({ element: inputEl, name: "label" })
170
+ if (existingLabel) {
171
+ addComment({
172
+ element: inputEl,
173
+ j,
174
+ scope: SCOPE,
175
+ source,
176
+ text: `Input already has a label prop. The InputLabel had text: "${textContent}". Please review manually.`,
177
+ })
178
+ hasChanges = true
179
+ i = nextIdx + 1
180
+ continue
181
+ }
182
+
183
+ addAttribute({ element: inputEl, j, name: "label", value: textContent })
184
+
185
+ if (labelFirst) {
186
+ // InputLabel is at i, Input is at nextIdx — remove InputLabel + whitespace between
187
+ children.splice(i, nextIdx - i)
188
+ // Input is now at i; advance past it
189
+ } else {
190
+ // Input is at i, InputLabel is at nextIdx — remove whitespace + InputLabel after Input
191
+ children.splice(i + 1, nextIdx - i)
192
+ // Input is still at i; advance past it
193
+ }
194
+
195
+ hasChanges = true
196
+ i++
197
+ }
198
+ })
199
+
200
+ return hasChanges ? source.toSource() : null
201
+ }
202
+
203
+ export default transform