@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.
- package/dist/tapestry-react-shim.cjs +7 -1
- package/package.json +3 -3
- package/src/components/button/transforms/convertStyleProps.test.ts +97 -0
- package/src/components/button/transforms/removeTypeButton.test.ts +0 -1
- package/src/components/checkbox/transforms/moveCheckboxImport.test.ts +3 -0
- package/src/components/input/index.ts +66 -0
- package/src/components/input/transformableInput.ts +49 -0
- package/src/components/input/transforms/auditSpreadProps.test.ts +192 -0
- package/src/components/input/transforms/auditSpreadProps.ts +26 -0
- package/src/components/input/transforms/autoWidthTransform.test.ts +172 -0
- package/src/components/input/transforms/autoWidthTransform.ts +41 -0
- package/src/components/input/transforms/convertStyleProps.test.ts +128 -0
- package/src/components/input/transforms/convertStyleProps.ts +12 -0
- package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.test.ts +186 -0
- package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.ts +27 -0
- package/src/components/input/transforms/inputLabelToLabelProp.test.ts +319 -0
- package/src/components/input/transforms/inputLabelToLabelProp.ts +203 -0
- package/src/components/input/transforms/mergeFieldIntoInput.test.ts +469 -0
- package/src/components/input/transforms/mergeFieldIntoInput.ts +7 -0
- package/src/components/input/transforms/mergeInputLabel.test.ts +458 -0
- package/src/components/input/transforms/mergeInputLabel.ts +204 -0
- package/src/components/input/transforms/moveInputImport.test.ts +166 -0
- package/src/components/input/transforms/moveInputImport.ts +14 -0
- package/src/components/input/transforms/numberFieldAddTypeNumber.test.ts +92 -0
- package/src/components/input/transforms/numberFieldAddTypeNumber.ts +14 -0
- package/src/components/input/transforms/numberFieldRenameToInput.test.ts +126 -0
- package/src/components/input/transforms/numberFieldRenameToInput.ts +9 -0
- package/src/components/input/transforms/removeAsInput.test.ts +139 -0
- package/src/components/input/transforms/removeAsInput.ts +20 -0
- package/src/components/input/transforms/removeDuplicateKeys.test.ts +302 -0
- package/src/components/input/transforms/removeDuplicateKeys.ts +10 -0
- package/src/components/input/transforms/removeInputBox.test.ts +352 -0
- package/src/components/input/transforms/removeInputBox.ts +109 -0
- package/src/components/input/transforms/removeRedundantAriaLabel.test.ts +128 -0
- package/src/components/input/transforms/removeRedundantAriaLabel.ts +21 -0
- package/src/components/input/transforms/removeTypeInput.test.ts +212 -0
- package/src/components/input/transforms/removeTypeInput.ts +22 -0
- package/src/components/input/transforms/removeTypeText.test.ts +160 -0
- package/src/components/input/transforms/removeTypeText.ts +17 -0
- package/src/components/input/transforms/sizeMapping.test.ts +198 -0
- package/src/components/input/transforms/sizeMapping.ts +17 -0
- package/src/components/input/transforms/skipRenderSideProps.test.ts +236 -0
- package/src/components/input/transforms/skipRenderSideProps.ts +27 -0
- package/src/components/input/transforms/stateToInvalid.test.ts +208 -0
- package/src/components/input/transforms/stateToInvalid.ts +59 -0
- package/src/components/input/transforms/stateToInvalidTernary.test.ts +159 -0
- package/src/components/input/transforms/stateToInvalidTernary.ts +13 -0
- package/src/components/input/transforms/unsupportedProps.test.ts +566 -0
- package/src/components/input/transforms/unsupportedProps.ts +84 -0
- package/src/components/link/transforms/reviewStyles.test.ts +0 -1
- package/src/components/select/index.ts +58 -0
- package/src/components/select/transformableSelect.ts +7 -0
- package/src/components/select/transforms/auditSpreadProps.test.ts +103 -0
- package/src/components/select/transforms/auditSpreadProps.ts +26 -0
- package/src/components/select/transforms/childrenToOptions.test.ts +367 -0
- package/src/components/select/transforms/childrenToOptions.ts +295 -0
- package/src/components/select/transforms/convertLegacyOptions.test.ts +150 -0
- package/src/components/select/transforms/convertLegacyOptions.ts +105 -0
- package/src/components/select/transforms/convertStyleProps.test.ts +73 -0
- package/src/components/select/transforms/convertStyleProps.ts +12 -0
- package/src/components/select/transforms/emptyValueToPlaceholder.test.ts +122 -0
- package/src/components/select/transforms/emptyValueToPlaceholder.ts +22 -0
- package/src/components/select/transforms/innerRefToRef.test.ts +89 -0
- package/src/components/select/transforms/innerRefToRef.ts +18 -0
- package/src/components/select/transforms/mapChildrenToOptions.test.ts +521 -0
- package/src/components/select/transforms/mapChildrenToOptions.ts +312 -0
- package/src/components/select/transforms/mergeFieldIntoSelect.test.ts +506 -0
- package/src/components/select/transforms/mergeFieldIntoSelect.ts +7 -0
- package/src/components/select/transforms/mergeSelectLabel.test.ts +458 -0
- package/src/components/select/transforms/mergeSelectLabel.ts +225 -0
- package/src/components/select/transforms/moveSelectImport.test.ts +148 -0
- package/src/components/select/transforms/moveSelectImport.ts +14 -0
- package/src/components/select/transforms/removeDefaultProps.test.ts +249 -0
- package/src/components/select/transforms/removeDefaultProps.ts +112 -0
- package/src/components/select/transforms/sizeMapping.test.ts +188 -0
- package/src/components/select/transforms/sizeMapping.ts +17 -0
- package/src/components/select/transforms/skipMultipleSelect.test.ts +148 -0
- package/src/components/select/transforms/skipMultipleSelect.ts +23 -0
- package/src/components/select/transforms/stateToInvalid.test.ts +217 -0
- package/src/components/select/transforms/stateToInvalid.ts +59 -0
- package/src/components/select/transforms/stateToInvalidTernary.test.ts +146 -0
- package/src/components/select/transforms/stateToInvalidTernary.ts +13 -0
- package/src/components/select/transforms/unsupportedProps.test.ts +252 -0
- package/src/components/select/transforms/unsupportedProps.ts +44 -0
- package/src/components/shared/helpers/getAttributeExpression.ts +26 -0
- package/src/components/shared/helpers/unsupportedPropsHelpers.ts +102 -0
- package/src/components/shared/transformFactories/helpers/manageImports.ts +14 -12
- package/src/components/shared/transformFactories/mergeFieldFactory.ts +244 -0
- package/src/components/shared/transformFactories/sizeMappingFactory.ts +9 -2
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +56 -17
- package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +65 -0
- package/src/components/text-area/index.ts +48 -0
- package/src/components/text-area/transforms/auditSpreadProps.test.ts +139 -0
- package/src/components/text-area/transforms/auditSpreadProps.ts +10 -0
- package/src/components/text-area/transforms/convertStyleProps.test.ts +158 -0
- package/src/components/text-area/transforms/convertStyleProps.ts +10 -0
- package/src/components/text-area/transforms/innerRefToRef.test.ts +206 -0
- package/src/components/text-area/transforms/innerRefToRef.ts +14 -0
- package/src/components/text-area/transforms/mergeFieldIntoTextArea.test.ts +477 -0
- package/src/components/text-area/transforms/mergeFieldIntoTextArea.ts +5 -0
- package/src/components/text-area/transforms/moveTextAreaImport.test.ts +168 -0
- package/src/components/text-area/transforms/moveTextAreaImport.ts +13 -0
- package/src/components/text-area/transforms/removeDuplicateKeys.test.ts +129 -0
- package/src/components/text-area/transforms/removeDuplicateKeys.ts +8 -0
- package/src/components/text-area/transforms/removeRedundantAriaLabel.test.ts +183 -0
- package/src/components/text-area/transforms/removeRedundantAriaLabel.ts +59 -0
- package/src/components/text-area/transforms/sizeMapping.test.ts +199 -0
- package/src/components/text-area/transforms/sizeMapping.ts +15 -0
- package/src/components/text-area/transforms/stateToInvalid.test.ts +204 -0
- package/src/components/text-area/transforms/stateToInvalid.ts +57 -0
- package/src/components/text-area/transforms/stateToInvalidTernary.test.ts +133 -0
- package/src/components/text-area/transforms/stateToInvalidTernary.ts +11 -0
- package/src/components/text-area/transforms/unsupportedProps.test.ts +275 -0
- package/src/components/text-area/transforms/unsupportedProps.ts +35 -0
- package/src/index.ts +2 -1
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./mergeSelectLabel"
|
|
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("mergeSelectLabel transform", () => {
|
|
18
|
+
describe("basic merge", () => {
|
|
19
|
+
it("merges InputLabel with simple text into Select label prop inside Box", () => {
|
|
20
|
+
const input = `
|
|
21
|
+
import { Box, Input, Select } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
function Test() {
|
|
24
|
+
return (
|
|
25
|
+
<Box grow={1}>
|
|
26
|
+
<Input.InputLabel controls="select-product">Product</Input.InputLabel>
|
|
27
|
+
<Select id="select-product" emptyValue="Pick" />
|
|
28
|
+
</Box>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
`.trim()
|
|
32
|
+
|
|
33
|
+
const result = applyTransform(input)
|
|
34
|
+
expect(result).not.toBeNull()
|
|
35
|
+
expect(result).toContain('label="Product"')
|
|
36
|
+
expect(result).not.toContain("<Input.InputLabel")
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("merges InputLabel with simple text into Select label prop inside StackView", () => {
|
|
40
|
+
const input = `
|
|
41
|
+
import { Input, Select, StackView } from "@planningcenter/tapestry-react"
|
|
42
|
+
|
|
43
|
+
function Test() {
|
|
44
|
+
return (
|
|
45
|
+
<StackView>
|
|
46
|
+
<Input.InputLabel controls="select-name">Name</Input.InputLabel>
|
|
47
|
+
<Select id="select-name" emptyValue="Pick" />
|
|
48
|
+
</StackView>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
`.trim()
|
|
52
|
+
|
|
53
|
+
const result = applyTransform(input)
|
|
54
|
+
expect(result).not.toBeNull()
|
|
55
|
+
expect(result).toContain('label="Name"')
|
|
56
|
+
expect(result).not.toContain("<Input.InputLabel")
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe("StackView axis", () => {
|
|
61
|
+
it("skips horizontal StackView and returns null", () => {
|
|
62
|
+
const input = `
|
|
63
|
+
import { Input, Select, StackView } from "@planningcenter/tapestry-react"
|
|
64
|
+
|
|
65
|
+
function Test() {
|
|
66
|
+
return (
|
|
67
|
+
<StackView axis="horizontal">
|
|
68
|
+
<Input.InputLabel controls="select-id">Name</Input.InputLabel>
|
|
69
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
70
|
+
</StackView>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
`.trim()
|
|
74
|
+
|
|
75
|
+
const result = applyTransform(input)
|
|
76
|
+
expect(result).toBeNull()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("processes vertical StackView (explicit axis)", () => {
|
|
80
|
+
const input = `
|
|
81
|
+
import { Input, Select, StackView } from "@planningcenter/tapestry-react"
|
|
82
|
+
|
|
83
|
+
function Test() {
|
|
84
|
+
return (
|
|
85
|
+
<StackView axis="vertical">
|
|
86
|
+
<Input.InputLabel controls="select-id">Name</Input.InputLabel>
|
|
87
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
88
|
+
</StackView>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
`.trim()
|
|
92
|
+
|
|
93
|
+
const result = applyTransform(input)
|
|
94
|
+
expect(result).not.toBeNull()
|
|
95
|
+
expect(result).toContain('label="Name"')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe("complex InputLabel children", () => {
|
|
100
|
+
it("adds comment when children are complex JSX", () => {
|
|
101
|
+
const input = `
|
|
102
|
+
import { Box, Input, Select } from "@planningcenter/tapestry-react"
|
|
103
|
+
|
|
104
|
+
function Test() {
|
|
105
|
+
return (
|
|
106
|
+
<Box>
|
|
107
|
+
<Input.InputLabel controls="select-id">
|
|
108
|
+
<span>Product <em>*</em></span>
|
|
109
|
+
</Input.InputLabel>
|
|
110
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
111
|
+
</Box>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
`.trim()
|
|
115
|
+
|
|
116
|
+
const result = applyTransform(input)
|
|
117
|
+
expect(result).not.toBeNull()
|
|
118
|
+
expect(result).not.toContain("<Input.InputLabel")
|
|
119
|
+
expect(result).toContain("TODO: tapestry-migration (mergeSelectLabel)")
|
|
120
|
+
expect(result).toContain("complex and cannot be auto-converted")
|
|
121
|
+
expect(result).not.toContain('label="')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("adds comment when children are dynamic expressions", () => {
|
|
125
|
+
const input = `
|
|
126
|
+
import { Box, Input, Select } from "@planningcenter/tapestry-react"
|
|
127
|
+
|
|
128
|
+
function Test() {
|
|
129
|
+
return (
|
|
130
|
+
<Box>
|
|
131
|
+
<Input.InputLabel controls="select-id">{labelText}</Input.InputLabel>
|
|
132
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
133
|
+
</Box>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
`.trim()
|
|
137
|
+
|
|
138
|
+
const result = applyTransform(input)
|
|
139
|
+
expect(result).not.toBeNull()
|
|
140
|
+
expect(result).not.toContain("<Input.InputLabel")
|
|
141
|
+
expect(result).toContain("complex and cannot be auto-converted")
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe("InputLabel extra props", () => {
|
|
146
|
+
it("adds comment listing removed props when InputLabel has non-controls props", () => {
|
|
147
|
+
const input = `
|
|
148
|
+
import { Box, Input, Select } from "@planningcenter/tapestry-react"
|
|
149
|
+
|
|
150
|
+
function Test() {
|
|
151
|
+
return (
|
|
152
|
+
<Box>
|
|
153
|
+
<Input.InputLabel controls="select-id" color={token("--t-text-color")}>Product</Input.InputLabel>
|
|
154
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
155
|
+
</Box>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
`.trim()
|
|
159
|
+
|
|
160
|
+
const result = applyTransform(input)
|
|
161
|
+
expect(result).not.toBeNull()
|
|
162
|
+
expect(result).toContain('label="Product"')
|
|
163
|
+
expect(result).toContain(
|
|
164
|
+
"The following props from Input.InputLabel were removed: color"
|
|
165
|
+
)
|
|
166
|
+
expect(result).not.toContain("<Input.InputLabel")
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it("does not add removed props comment when only controls prop is present", () => {
|
|
170
|
+
const input = `
|
|
171
|
+
import { Box, Input, Select } from "@planningcenter/tapestry-react"
|
|
172
|
+
|
|
173
|
+
function Test() {
|
|
174
|
+
return (
|
|
175
|
+
<Box>
|
|
176
|
+
<Input.InputLabel controls="select-id">Name</Input.InputLabel>
|
|
177
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
178
|
+
</Box>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
`.trim()
|
|
182
|
+
|
|
183
|
+
const result = applyTransform(input)
|
|
184
|
+
expect(result).not.toBeNull()
|
|
185
|
+
expect(result).toContain('label="Name"')
|
|
186
|
+
expect(result).not.toContain("were removed")
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe("whitespace handling", () => {
|
|
191
|
+
it("handles whitespace text nodes between InputLabel and Select", () => {
|
|
192
|
+
const input = `
|
|
193
|
+
import { Box, Input, Select } from "@planningcenter/tapestry-react"
|
|
194
|
+
|
|
195
|
+
function Test() {
|
|
196
|
+
return (
|
|
197
|
+
<Box>
|
|
198
|
+
<Input.InputLabel controls="select-id">Phone</Input.InputLabel>
|
|
199
|
+
|
|
200
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
201
|
+
</Box>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
`.trim()
|
|
205
|
+
|
|
206
|
+
const result = applyTransform(input)
|
|
207
|
+
expect(result).not.toBeNull()
|
|
208
|
+
expect(result).toContain('label="Phone"')
|
|
209
|
+
expect(result).not.toContain("<Input.InputLabel")
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe("existing label prop on Select", () => {
|
|
214
|
+
it("adds comment and does not duplicate label when Select already has label prop", () => {
|
|
215
|
+
const input = `
|
|
216
|
+
import { Box, Input, Select } from "@planningcenter/tapestry-react"
|
|
217
|
+
|
|
218
|
+
function Test() {
|
|
219
|
+
return (
|
|
220
|
+
<Box>
|
|
221
|
+
<Input.InputLabel controls="select-id">New Label</Input.InputLabel>
|
|
222
|
+
<Select id="select-id" label="Existing Label" emptyValue="Pick" />
|
|
223
|
+
</Box>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
`.trim()
|
|
227
|
+
|
|
228
|
+
const result = applyTransform(input)
|
|
229
|
+
expect(result).not.toBeNull()
|
|
230
|
+
expect(result).not.toContain("<Input.InputLabel")
|
|
231
|
+
expect(result).toContain('label="Existing Label"')
|
|
232
|
+
expect(result).toContain("TODO: tapestry-migration (mergeSelectLabel)")
|
|
233
|
+
expect(result).toContain("already has a label prop")
|
|
234
|
+
expect(result).toContain("New Label")
|
|
235
|
+
expect(result?.match(/label=/g)?.length).toBe(1)
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe("multiple pairs", () => {
|
|
240
|
+
it("merges multiple InputLabel+Select pairs in the same parent", () => {
|
|
241
|
+
const input = `
|
|
242
|
+
import { Box, Input, Select } from "@planningcenter/tapestry-react"
|
|
243
|
+
|
|
244
|
+
function Test() {
|
|
245
|
+
return (
|
|
246
|
+
<Box>
|
|
247
|
+
<Input.InputLabel controls="select-first">First</Input.InputLabel>
|
|
248
|
+
<Select id="select-first" emptyValue="Pick" />
|
|
249
|
+
<Input.InputLabel controls="select-last">Last</Input.InputLabel>
|
|
250
|
+
<Select id="select-last" emptyValue="Pick" />
|
|
251
|
+
</Box>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
`.trim()
|
|
255
|
+
|
|
256
|
+
const result = applyTransform(input)
|
|
257
|
+
expect(result).not.toBeNull()
|
|
258
|
+
expect(result).toContain('label="First"')
|
|
259
|
+
expect(result).toContain('label="Last"')
|
|
260
|
+
expect(result).not.toContain("<Input.InputLabel")
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
describe("no required imports", () => {
|
|
265
|
+
it("returns null when Input is not imported from tapestry-react", () => {
|
|
266
|
+
const input = `
|
|
267
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
268
|
+
|
|
269
|
+
function Test() {
|
|
270
|
+
return (
|
|
271
|
+
<Box>
|
|
272
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
273
|
+
</Box>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
`.trim()
|
|
277
|
+
|
|
278
|
+
const result = applyTransform(input)
|
|
279
|
+
expect(result).toBeNull()
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it("returns null when Select is not imported from tapestry-react", () => {
|
|
283
|
+
const input = `
|
|
284
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
285
|
+
|
|
286
|
+
function Test() {
|
|
287
|
+
return (
|
|
288
|
+
<Box>
|
|
289
|
+
<Input.InputLabel controls="input-id">Name</Input.InputLabel>
|
|
290
|
+
<Input id="input-id" value={name} />
|
|
291
|
+
</Box>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
`.trim()
|
|
295
|
+
|
|
296
|
+
const result = applyTransform(input)
|
|
297
|
+
expect(result).toBeNull()
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
describe("aliased imports", () => {
|
|
302
|
+
it("handles aliased Input import for InputLabel resolution", () => {
|
|
303
|
+
const input = `
|
|
304
|
+
import { Box, Input as TapestryInput, Select } from "@planningcenter/tapestry-react"
|
|
305
|
+
|
|
306
|
+
function Test() {
|
|
307
|
+
return (
|
|
308
|
+
<Box>
|
|
309
|
+
<TapestryInput.InputLabel controls="select-id">City</TapestryInput.InputLabel>
|
|
310
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
311
|
+
</Box>
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
`.trim()
|
|
315
|
+
|
|
316
|
+
const result = applyTransform(input)
|
|
317
|
+
expect(result).not.toBeNull()
|
|
318
|
+
expect(result).toContain('label="City"')
|
|
319
|
+
expect(result).not.toContain("<TapestryInput.InputLabel")
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it("handles aliased Select import", () => {
|
|
323
|
+
const input = `
|
|
324
|
+
import { Box, Input, Select as TapSelect } from "@planningcenter/tapestry-react"
|
|
325
|
+
|
|
326
|
+
function Test() {
|
|
327
|
+
return (
|
|
328
|
+
<Box>
|
|
329
|
+
<Input.InputLabel controls="select-id">City</Input.InputLabel>
|
|
330
|
+
<TapSelect id="select-id" emptyValue="Pick" />
|
|
331
|
+
</Box>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
`.trim()
|
|
335
|
+
|
|
336
|
+
const result = applyTransform(input)
|
|
337
|
+
expect(result).not.toBeNull()
|
|
338
|
+
expect(result).toContain('label="City"')
|
|
339
|
+
expect(result).not.toContain("<Input.InputLabel")
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
describe("parent layout props", () => {
|
|
344
|
+
it("skips Box with inline prop", () => {
|
|
345
|
+
const input = `
|
|
346
|
+
import { Box, Input, Select } from "@planningcenter/tapestry-react"
|
|
347
|
+
|
|
348
|
+
function Test() {
|
|
349
|
+
return (
|
|
350
|
+
<Box inline>
|
|
351
|
+
<Input.InputLabel controls="select-id">Name</Input.InputLabel>
|
|
352
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
353
|
+
</Box>
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
`.trim()
|
|
357
|
+
|
|
358
|
+
const result = applyTransform(input)
|
|
359
|
+
expect(result).toBeNull()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it("skips Box with compact prop", () => {
|
|
363
|
+
const input = `
|
|
364
|
+
import { Box, Input, Select } from "@planningcenter/tapestry-react"
|
|
365
|
+
|
|
366
|
+
function Test() {
|
|
367
|
+
return (
|
|
368
|
+
<Box compact>
|
|
369
|
+
<Input.InputLabel controls="select-id">Name</Input.InputLabel>
|
|
370
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
371
|
+
</Box>
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
`.trim()
|
|
375
|
+
|
|
376
|
+
const result = applyTransform(input)
|
|
377
|
+
expect(result).toBeNull()
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it("skips StackView with labelBasis prop", () => {
|
|
381
|
+
const input = `
|
|
382
|
+
import { Input, Select, StackView } from "@planningcenter/tapestry-react"
|
|
383
|
+
|
|
384
|
+
function Test() {
|
|
385
|
+
return (
|
|
386
|
+
<StackView labelBasis="200px">
|
|
387
|
+
<Input.InputLabel controls="select-id">Name</Input.InputLabel>
|
|
388
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
389
|
+
</StackView>
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
`.trim()
|
|
393
|
+
|
|
394
|
+
const result = applyTransform(input)
|
|
395
|
+
expect(result).toBeNull()
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it("still merges Box without layout props", () => {
|
|
399
|
+
const input = `
|
|
400
|
+
import { Box, Input, Select } from "@planningcenter/tapestry-react"
|
|
401
|
+
|
|
402
|
+
function Test() {
|
|
403
|
+
return (
|
|
404
|
+
<Box grow={1}>
|
|
405
|
+
<Input.InputLabel controls="select-id">Name</Input.InputLabel>
|
|
406
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
407
|
+
</Box>
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
`.trim()
|
|
411
|
+
|
|
412
|
+
const result = applyTransform(input)
|
|
413
|
+
expect(result).not.toBeNull()
|
|
414
|
+
expect(result).toContain('label="Name"')
|
|
415
|
+
})
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
describe("non-Box/StackView parents", () => {
|
|
419
|
+
it("skips InputLabel+Select pairs inside non-Box/StackView parents", () => {
|
|
420
|
+
const input = `
|
|
421
|
+
import { Box, Input, Select } from "@planningcenter/tapestry-react"
|
|
422
|
+
|
|
423
|
+
function Test() {
|
|
424
|
+
return (
|
|
425
|
+
<div>
|
|
426
|
+
<Input.InputLabel controls="select-id">Name</Input.InputLabel>
|
|
427
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
428
|
+
</div>
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
`.trim()
|
|
432
|
+
|
|
433
|
+
const result = applyTransform(input)
|
|
434
|
+
expect(result).toBeNull()
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
describe("InputLabel not followed by Select", () => {
|
|
439
|
+
it("skips InputLabel when the next element is not a Select", () => {
|
|
440
|
+
const input = `
|
|
441
|
+
import { Box, Input, Select } from "@planningcenter/tapestry-react"
|
|
442
|
+
|
|
443
|
+
function Test() {
|
|
444
|
+
return (
|
|
445
|
+
<Box>
|
|
446
|
+
<Input.InputLabel controls="input-id">Name</Input.InputLabel>
|
|
447
|
+
<Input id="input-id" value={name} />
|
|
448
|
+
<Select id="select-id" emptyValue="Pick" />
|
|
449
|
+
</Box>
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
`.trim()
|
|
453
|
+
|
|
454
|
+
const result = applyTransform(input)
|
|
455
|
+
expect(result).toBeNull()
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
})
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { JSXElement, Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addAttribute } from "../../shared/actions/addAttribute"
|
|
4
|
+
import { addComment } from "../../shared/actions/addComment"
|
|
5
|
+
import { getAttribute } from "../../shared/actions/getAttribute"
|
|
6
|
+
import { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
7
|
+
import { extractTextContent } from "../../shared/helpers/childrenToLabelHelpers"
|
|
8
|
+
import { getImportName } from "../../shared/transformFactories/helpers/manageImports"
|
|
9
|
+
|
|
10
|
+
const SCOPE = "mergeSelectLabel"
|
|
11
|
+
|
|
12
|
+
const transform: Transform = (fileInfo, api) => {
|
|
13
|
+
const j = api.jscodeshift
|
|
14
|
+
const source = j(fileInfo.source)
|
|
15
|
+
|
|
16
|
+
const inputLocalName = getImportName(
|
|
17
|
+
"Input",
|
|
18
|
+
"@planningcenter/tapestry-react",
|
|
19
|
+
{
|
|
20
|
+
j,
|
|
21
|
+
source,
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
if (!inputLocalName) return null
|
|
25
|
+
|
|
26
|
+
const selectLocalName = getImportName(
|
|
27
|
+
"Select",
|
|
28
|
+
"@planningcenter/tapestry-react",
|
|
29
|
+
{
|
|
30
|
+
j,
|
|
31
|
+
source,
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
if (!selectLocalName) return null
|
|
35
|
+
|
|
36
|
+
const boxLocalName = getImportName("Box", "@planningcenter/tapestry-react", {
|
|
37
|
+
j,
|
|
38
|
+
source,
|
|
39
|
+
})
|
|
40
|
+
const stackViewLocalName = getImportName(
|
|
41
|
+
"StackView",
|
|
42
|
+
"@planningcenter/tapestry-react",
|
|
43
|
+
{
|
|
44
|
+
j,
|
|
45
|
+
source,
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if (!boxLocalName && !stackViewLocalName) return null
|
|
50
|
+
|
|
51
|
+
let hasChanges = false
|
|
52
|
+
|
|
53
|
+
source.find(j.JSXElement).forEach((path) => {
|
|
54
|
+
const parentEl = path.value
|
|
55
|
+
const openingElement = parentEl.openingElement
|
|
56
|
+
|
|
57
|
+
if (openingElement.name.type !== "JSXIdentifier") return
|
|
58
|
+
const parentName = openingElement.name.name
|
|
59
|
+
const isBox = boxLocalName && parentName === boxLocalName
|
|
60
|
+
const isStackView = stackViewLocalName && parentName === stackViewLocalName
|
|
61
|
+
if (!isBox && !isStackView) return
|
|
62
|
+
|
|
63
|
+
// Skip parents with layout props that indicate a complex layout
|
|
64
|
+
// where label merging would be inappropriate
|
|
65
|
+
const SKIP_PARENT_PROPS = ["compact", "inline", "labelBasis"]
|
|
66
|
+
const parentAttrs = openingElement.attributes || []
|
|
67
|
+
const hasLayoutProp = parentAttrs.some(
|
|
68
|
+
(attr) =>
|
|
69
|
+
attr.type === "JSXAttribute" &&
|
|
70
|
+
attr.name.type === "JSXIdentifier" &&
|
|
71
|
+
SKIP_PARENT_PROPS.includes(attr.name.name)
|
|
72
|
+
)
|
|
73
|
+
if (hasLayoutProp) return
|
|
74
|
+
|
|
75
|
+
if (isStackView) {
|
|
76
|
+
const axisAttr = getAttribute({ element: parentEl, name: "axis" })
|
|
77
|
+
if (axisAttr) {
|
|
78
|
+
const axisValue = axisAttr.value
|
|
79
|
+
if (
|
|
80
|
+
axisValue &&
|
|
81
|
+
((axisValue.type === "StringLiteral" &&
|
|
82
|
+
axisValue.value === "horizontal") ||
|
|
83
|
+
(axisValue.type === "JSXExpressionContainer" &&
|
|
84
|
+
axisValue.expression.type === "StringLiteral" &&
|
|
85
|
+
axisValue.expression.value === "horizontal"))
|
|
86
|
+
) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const children = parentEl.children || []
|
|
93
|
+
let i = 0
|
|
94
|
+
|
|
95
|
+
while (i < children.length) {
|
|
96
|
+
const child = children[i]
|
|
97
|
+
|
|
98
|
+
if (child.type !== "JSXElement") {
|
|
99
|
+
i++
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const childOpening = child.openingElement
|
|
104
|
+
const childName = childOpening.name
|
|
105
|
+
|
|
106
|
+
if (childName.type !== "JSXMemberExpression") {
|
|
107
|
+
i++
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
const objectPart = childName.object
|
|
111
|
+
if (
|
|
112
|
+
objectPart.type !== "JSXIdentifier" ||
|
|
113
|
+
objectPart.name !== inputLocalName
|
|
114
|
+
) {
|
|
115
|
+
i++
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
if (childName.property.name !== "InputLabel") {
|
|
119
|
+
i++
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Find the next JSXElement sibling (skip JSXText whitespace nodes)
|
|
124
|
+
let nextIdx = i + 1
|
|
125
|
+
while (
|
|
126
|
+
nextIdx < children.length &&
|
|
127
|
+
children[nextIdx].type === "JSXText"
|
|
128
|
+
) {
|
|
129
|
+
nextIdx++
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
nextIdx >= children.length ||
|
|
134
|
+
children[nextIdx].type !== "JSXElement"
|
|
135
|
+
) {
|
|
136
|
+
i++
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const selectEl = children[nextIdx] as JSXElement
|
|
141
|
+
const selectOpening = selectEl.openingElement
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
selectOpening.name.type !== "JSXIdentifier" ||
|
|
145
|
+
selectOpening.name.name !== selectLocalName
|
|
146
|
+
) {
|
|
147
|
+
i++
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Skip multiple selects — they are not transformable
|
|
152
|
+
if (hasAttribute("multiple")(selectEl)) {
|
|
153
|
+
i++
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const { isSimpleText, textContent } = extractTextContent(
|
|
158
|
+
child.children || []
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
// Collect non-controls attrs from InputLabel to warn about
|
|
162
|
+
const inputLabelAttrs = childOpening.attributes || []
|
|
163
|
+
const removedProps: string[] = []
|
|
164
|
+
for (const attr of inputLabelAttrs) {
|
|
165
|
+
if (
|
|
166
|
+
attr.type === "JSXAttribute" &&
|
|
167
|
+
attr.name.type === "JSXIdentifier" &&
|
|
168
|
+
attr.name.name !== "controls"
|
|
169
|
+
) {
|
|
170
|
+
removedProps.push(attr.name.name)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Remove InputLabel and any whitespace nodes before Select
|
|
175
|
+
children.splice(i, nextIdx - i)
|
|
176
|
+
// Select is now at index i
|
|
177
|
+
|
|
178
|
+
hasChanges = true
|
|
179
|
+
|
|
180
|
+
if (!isSimpleText) {
|
|
181
|
+
addComment({
|
|
182
|
+
element: selectEl,
|
|
183
|
+
j,
|
|
184
|
+
scope: SCOPE,
|
|
185
|
+
source,
|
|
186
|
+
text: "InputLabel children are complex and cannot be auto-converted to a label prop. Please migrate manually.",
|
|
187
|
+
})
|
|
188
|
+
} else {
|
|
189
|
+
const existingLabel = getAttribute({ element: selectEl, name: "label" })
|
|
190
|
+
if (existingLabel) {
|
|
191
|
+
addComment({
|
|
192
|
+
element: selectEl,
|
|
193
|
+
j,
|
|
194
|
+
scope: SCOPE,
|
|
195
|
+
source,
|
|
196
|
+
text: `Select already has a label prop. The InputLabel had text: "${textContent}". Please review manually.`,
|
|
197
|
+
})
|
|
198
|
+
} else {
|
|
199
|
+
addAttribute({
|
|
200
|
+
element: selectEl,
|
|
201
|
+
j,
|
|
202
|
+
name: "label",
|
|
203
|
+
value: textContent,
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (removedProps.length > 0) {
|
|
209
|
+
addComment({
|
|
210
|
+
element: selectEl,
|
|
211
|
+
j,
|
|
212
|
+
scope: SCOPE,
|
|
213
|
+
source,
|
|
214
|
+
text: `The following props from Input.InputLabel were removed: ${removedProps.join(", ")}. Please review manually.`,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
i++
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
return hasChanges ? source.toSource() : null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export default transform
|