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