@planningcenter/tapestry-migration-cli 3.1.0-rc.6 → 3.1.0-rc.7

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 (52) hide show
  1. package/package.json +3 -3
  2. package/src/components/button/transforms/convertStyleProps.test.ts +97 -0
  3. package/src/components/button/transforms/removeTypeButton.test.ts +0 -1
  4. package/src/components/checkbox/transforms/moveCheckboxImport.test.ts +3 -0
  5. package/src/components/input/index.ts +66 -0
  6. package/src/components/input/transformableInput.ts +8 -0
  7. package/src/components/input/transforms/auditSpreadProps.test.ts +192 -0
  8. package/src/components/input/transforms/auditSpreadProps.ts +26 -0
  9. package/src/components/input/transforms/autoWidthTransform.test.ts +172 -0
  10. package/src/components/input/transforms/autoWidthTransform.ts +41 -0
  11. package/src/components/input/transforms/convertStyleProps.test.ts +128 -0
  12. package/src/components/input/transforms/convertStyleProps.ts +12 -0
  13. package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.test.ts +186 -0
  14. package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.ts +27 -0
  15. package/src/components/input/transforms/inputLabelToLabelProp.test.ts +319 -0
  16. package/src/components/input/transforms/inputLabelToLabelProp.ts +203 -0
  17. package/src/components/input/transforms/mergeFieldIntoInput.test.ts +391 -0
  18. package/src/components/input/transforms/mergeFieldIntoInput.ts +213 -0
  19. package/src/components/input/transforms/mergeInputLabel.test.ts +458 -0
  20. package/src/components/input/transforms/mergeInputLabel.ts +204 -0
  21. package/src/components/input/transforms/moveInputImport.test.ts +166 -0
  22. package/src/components/input/transforms/moveInputImport.ts +14 -0
  23. package/src/components/input/transforms/numberFieldAddTypeNumber.test.ts +92 -0
  24. package/src/components/input/transforms/numberFieldAddTypeNumber.ts +14 -0
  25. package/src/components/input/transforms/numberFieldRenameToInput.test.ts +126 -0
  26. package/src/components/input/transforms/numberFieldRenameToInput.ts +9 -0
  27. package/src/components/input/transforms/removeAsInput.test.ts +139 -0
  28. package/src/components/input/transforms/removeAsInput.ts +20 -0
  29. package/src/components/input/transforms/removeDuplicateKeys.test.ts +302 -0
  30. package/src/components/input/transforms/removeDuplicateKeys.ts +10 -0
  31. package/src/components/input/transforms/removeInputBox.test.ts +352 -0
  32. package/src/components/input/transforms/removeInputBox.ts +109 -0
  33. package/src/components/input/transforms/removeRedundantAriaLabel.test.ts +128 -0
  34. package/src/components/input/transforms/removeRedundantAriaLabel.ts +21 -0
  35. package/src/components/input/transforms/removeTypeText.test.ts +160 -0
  36. package/src/components/input/transforms/removeTypeText.ts +18 -0
  37. package/src/components/input/transforms/sizeMapping.test.ts +198 -0
  38. package/src/components/input/transforms/sizeMapping.ts +17 -0
  39. package/src/components/input/transforms/skipRenderSideProps.test.ts +236 -0
  40. package/src/components/input/transforms/skipRenderSideProps.ts +27 -0
  41. package/src/components/input/transforms/stateToInvalid.test.ts +208 -0
  42. package/src/components/input/transforms/stateToInvalid.ts +59 -0
  43. package/src/components/input/transforms/stateToInvalidTernary.test.ts +159 -0
  44. package/src/components/input/transforms/stateToInvalidTernary.ts +13 -0
  45. package/src/components/input/transforms/unsupportedProps.test.ts +566 -0
  46. package/src/components/input/transforms/unsupportedProps.ts +84 -0
  47. package/src/components/link/transforms/reviewStyles.test.ts +0 -1
  48. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +52 -0
  49. package/src/components/shared/transformFactories/helpers/manageImports.ts +14 -12
  50. package/src/components/shared/transformFactories/sizeMappingFactory.ts +9 -2
  51. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +54 -16
  52. package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +65 -0
@@ -0,0 +1,352 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./removeInputBox"
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("removeInputBox transform", () => {
18
+ describe("basic unwrap", () => {
19
+ it("removes InputBox wrapper and promotes Input child", () => {
20
+ const input = `
21
+ import { Input } from "@planningcenter/tapestry-react"
22
+
23
+ function Test() {
24
+ return (
25
+ <Box>
26
+ <Input.InputBox>
27
+ <Input id="name" value={name} />
28
+ </Input.InputBox>
29
+ </Box>
30
+ )
31
+ }
32
+ `.trim()
33
+
34
+ const result = applyTransform(input)
35
+ expect(result).not.toBeNull()
36
+ expect(result).not.toContain("InputBox")
37
+ expect(result).toContain("<Input")
38
+ })
39
+ })
40
+
41
+ describe("aliased import", () => {
42
+ it("handles aliased Input imports", () => {
43
+ const input = `
44
+ import { Input as TapInput } from "@planningcenter/tapestry-react"
45
+
46
+ function Test() {
47
+ return (
48
+ <Box>
49
+ <TapInput.InputBox>
50
+ <TapInput id="name" value={name} />
51
+ </TapInput.InputBox>
52
+ </Box>
53
+ )
54
+ }
55
+ `.trim()
56
+
57
+ const result = applyTransform(input)
58
+ expect(result).not.toBeNull()
59
+ expect(result).not.toContain("InputBox")
60
+ expect(result).toContain("<TapInput")
61
+ })
62
+ })
63
+
64
+ describe("InputBox has props", () => {
65
+ it("adds TODO to Input child and does not unwrap when InputBox has a single prop", () => {
66
+ const input = `
67
+ import { Input } from "@planningcenter/tapestry-react"
68
+
69
+ function Test() {
70
+ return (
71
+ <Box>
72
+ <Input.InputBox size="sm">
73
+ <Input id="name" value={name} />
74
+ </Input.InputBox>
75
+ </Box>
76
+ )
77
+ }
78
+ `.trim()
79
+
80
+ const result = applyTransform(input)
81
+ expect(result).not.toBeNull()
82
+ expect(result).toContain("InputBox")
83
+ expect(result).toContain("TODO: tapestry-migration (removeInputBox)")
84
+ expect(result).toContain(
85
+ "Tapestry doesn't support InputBox passing props to children (size)"
86
+ )
87
+ })
88
+
89
+ it("lists all props in the TODO comment when InputBox has multiple props", () => {
90
+ const input = `
91
+ import { Input } from "@planningcenter/tapestry-react"
92
+
93
+ function Test() {
94
+ return (
95
+ <Box>
96
+ <Input.InputBox size="sm" spacing={1}>
97
+ <Input id="name" value={name} />
98
+ </Input.InputBox>
99
+ </Box>
100
+ )
101
+ }
102
+ `.trim()
103
+
104
+ const result = applyTransform(input)
105
+ expect(result).not.toBeNull()
106
+ expect(result).toContain("InputBox")
107
+ expect(result).toContain("size, spacing")
108
+ })
109
+
110
+ it("does not add TODO when Input has renderLeft/renderRight (not a convertible child)", () => {
111
+ const input = `
112
+ import { Input } from "@planningcenter/tapestry-react"
113
+
114
+ function Test() {
115
+ return (
116
+ <Box>
117
+ <Input.InputBox size="sm">
118
+ <Input id="name" renderLeft={<Icon />} value={name} />
119
+ </Input.InputBox>
120
+ </Box>
121
+ )
122
+ }
123
+ `.trim()
124
+
125
+ const result = applyTransform(input)
126
+ expect(result).toBeNull()
127
+ })
128
+ })
129
+
130
+ describe("InputBox with multiple children", () => {
131
+ it("adds TODO to Input child and does not unwrap when InputBox has multiple non-whitespace children", () => {
132
+ const input = `
133
+ import { Input } from "@planningcenter/tapestry-react"
134
+
135
+ function Test() {
136
+ return (
137
+ <Box>
138
+ <Input.InputBox>
139
+ <Input id="name" value={name} />
140
+ <Text>helper</Text>
141
+ </Input.InputBox>
142
+ </Box>
143
+ )
144
+ }
145
+ `.trim()
146
+
147
+ const result = applyTransform(input)
148
+ expect(result).not.toBeNull()
149
+ expect(result).toContain("InputBox")
150
+ expect(result).toContain("TODO: tapestry-migration (removeInputBox)")
151
+ expect(result).toContain("multiple children")
152
+ })
153
+
154
+ it("returns null when InputBox has multiple children but none are convertible Input", () => {
155
+ const input = `
156
+ import { Input } from "@planningcenter/tapestry-react"
157
+
158
+ function Test() {
159
+ return (
160
+ <Box>
161
+ <Input.InputBox>
162
+ <Select value={val} />
163
+ <Text>helper</Text>
164
+ </Input.InputBox>
165
+ </Box>
166
+ )
167
+ }
168
+ `.trim()
169
+
170
+ const result = applyTransform(input)
171
+ expect(result).toBeNull()
172
+ })
173
+ })
174
+
175
+ describe("renderLeft/renderRight child with no props → no change", () => {
176
+ it("returns null when the single Input child has renderLeft", () => {
177
+ const input = `
178
+ import { Input } from "@planningcenter/tapestry-react"
179
+
180
+ function Test() {
181
+ return (
182
+ <Box>
183
+ <Input.InputBox>
184
+ <Input id="name" renderLeft={<Icon />} value={name} />
185
+ </Input.InputBox>
186
+ </Box>
187
+ )
188
+ }
189
+ `.trim()
190
+
191
+ const result = applyTransform(input)
192
+ expect(result).toBeNull()
193
+ })
194
+
195
+ it("returns null when the single Input child has renderRight", () => {
196
+ const input = `
197
+ import { Input } from "@planningcenter/tapestry-react"
198
+
199
+ function Test() {
200
+ return (
201
+ <Box>
202
+ <Input.InputBox>
203
+ <Input id="name" renderRight={<Icon />} value={name} />
204
+ </Input.InputBox>
205
+ </Box>
206
+ )
207
+ }
208
+ `.trim()
209
+
210
+ const result = applyTransform(input)
211
+ expect(result).toBeNull()
212
+ })
213
+ })
214
+
215
+ describe("non-Input child only → no change", () => {
216
+ it("returns null when InputBox contains only a non-Input child", () => {
217
+ const input = `
218
+ import { Input } from "@planningcenter/tapestry-react"
219
+
220
+ function Test() {
221
+ return (
222
+ <Box>
223
+ <Input.InputBox>
224
+ <Select value={val} />
225
+ </Input.InputBox>
226
+ </Box>
227
+ )
228
+ }
229
+ `.trim()
230
+
231
+ const result = applyTransform(input)
232
+ expect(result).toBeNull()
233
+ })
234
+
235
+ it("returns null when InputBox contains a JSX expression as its only child", () => {
236
+ const input = `
237
+ import { Input } from "@planningcenter/tapestry-react"
238
+
239
+ function Test() {
240
+ return (
241
+ <Box>
242
+ <Input.InputBox>
243
+ {condition && <Input id="name" value={name} />}
244
+ </Input.InputBox>
245
+ </Box>
246
+ )
247
+ }
248
+ `.trim()
249
+
250
+ const result = applyTransform(input)
251
+ expect(result).toBeNull()
252
+ })
253
+
254
+ it("returns null when InputBox has no non-whitespace children", () => {
255
+ const input = `
256
+ import { Input } from "@planningcenter/tapestry-react"
257
+
258
+ function Test() {
259
+ return (
260
+ <Box>
261
+ <Input.InputBox>
262
+ </Input.InputBox>
263
+ </Box>
264
+ )
265
+ }
266
+ `.trim()
267
+
268
+ const result = applyTransform(input)
269
+ expect(result).toBeNull()
270
+ })
271
+ })
272
+
273
+ describe("root-level InputBox", () => {
274
+ it("unwraps root-level InputBox with single convertible Input child", () => {
275
+ const input = `
276
+ import { Input } from "@planningcenter/tapestry-react"
277
+
278
+ function NameInput() {
279
+ return (
280
+ <Input.InputBox>
281
+ <Input id="name" value={name} />
282
+ </Input.InputBox>
283
+ )
284
+ }
285
+ `.trim()
286
+
287
+ const result = applyTransform(input)
288
+ expect(result).not.toBeNull()
289
+ expect(result).not.toContain("Input.InputBox")
290
+ expect(result).toContain('<Input id="name"')
291
+ })
292
+ })
293
+
294
+ describe("not from tapestry-react → no change", () => {
295
+ it("returns null when Input is not imported from tapestry-react", () => {
296
+ const input = `
297
+ import { Input } from "@planningcenter/tapestry"
298
+
299
+ function Test() {
300
+ return (
301
+ <Box>
302
+ <Input.InputBox>
303
+ <Input id="name" value={name} />
304
+ </Input.InputBox>
305
+ </Box>
306
+ )
307
+ }
308
+ `.trim()
309
+
310
+ const result = applyTransform(input)
311
+ expect(result).toBeNull()
312
+ })
313
+
314
+ it("returns null when no Input import exists at all", () => {
315
+ const input = `
316
+ import { Button } from "@planningcenter/tapestry-react"
317
+
318
+ function Test() {
319
+ return <Button label="Click" />
320
+ }
321
+ `.trim()
322
+
323
+ const result = applyTransform(input)
324
+ expect(result).toBeNull()
325
+ })
326
+ })
327
+
328
+ describe("integration with mergeInputLabel pipeline", () => {
329
+ it("after removeInputBox runs, Input is a direct sibling of InputLabel so mergeInputLabel can merge them", () => {
330
+ const input = `
331
+ import { Input } from "@planningcenter/tapestry-react"
332
+
333
+ function Test() {
334
+ return (
335
+ <Box>
336
+ <Input.InputLabel controls="input-name">Name</Input.InputLabel>
337
+ <Input.InputBox>
338
+ <Input id="input-name" value={name} />
339
+ </Input.InputBox>
340
+ </Box>
341
+ )
342
+ }
343
+ `.trim()
344
+
345
+ const result = applyTransform(input)
346
+ expect(result).not.toBeNull()
347
+ expect(result).not.toContain("InputBox")
348
+ expect(result).toContain("<Input.InputLabel")
349
+ expect(result).toContain("<Input")
350
+ })
351
+ })
352
+ })
@@ -0,0 +1,109 @@
1
+ import { JSXAttribute, JSXElement, JSXText, Transform } from "jscodeshift"
2
+
3
+ type JSXChild = NonNullable<JSXElement["children"]>[number]
4
+
5
+ import { addComment } from "../../shared/actions/addComment"
6
+ import { getImportName } from "../../shared/transformFactories/helpers/manageImports"
7
+
8
+ const SCOPE = "removeInputBox"
9
+
10
+ const transform: Transform = (fileInfo, api) => {
11
+ const j = api.jscodeshift
12
+ const source = j(fileInfo.source)
13
+
14
+ const inputLocalName = getImportName(
15
+ "Input",
16
+ "@planningcenter/tapestry-react",
17
+ { j, source }
18
+ )
19
+ if (!inputLocalName) return null
20
+
21
+ let hasChanges = false
22
+
23
+ const isConvertibleInputChild = (child: JSXChild): child is JSXElement => {
24
+ if (child.type !== "JSXElement") return false
25
+ if (child.openingElement.name.type !== "JSXIdentifier") return false
26
+ if (child.openingElement.name.name !== inputLocalName) return false
27
+ const attrs = child.openingElement.attributes || []
28
+ return !attrs.some(
29
+ (a) =>
30
+ a.type === "JSXAttribute" &&
31
+ a.name.type === "JSXIdentifier" &&
32
+ (a.name.name === "renderLeft" || a.name.name === "renderRight")
33
+ )
34
+ }
35
+
36
+ source.find(j.JSXElement).forEach((path) => {
37
+ const el = path.value
38
+ const opening = el.openingElement
39
+
40
+ if (opening.name.type !== "JSXMemberExpression") return
41
+ const { object, property } = opening.name
42
+ if (object.type !== "JSXIdentifier" || object.name !== inputLocalName)
43
+ return
44
+ if (property.name !== "InputBox") return
45
+
46
+ const elementChildren = (el.children || []).filter(
47
+ (child) =>
48
+ child.type !== "JSXText" || (child as JSXText).value.trim() !== ""
49
+ )
50
+ const inputChildren = elementChildren.filter(isConvertibleInputChild)
51
+
52
+ // Case: InputBox has props — add TODO to each Input child, don't unwrap
53
+ const attrs = opening.attributes || []
54
+ if (attrs.length > 0) {
55
+ const propNames = attrs
56
+ .filter(
57
+ (a): a is JSXAttribute =>
58
+ a.type === "JSXAttribute" && a.name?.type === "JSXIdentifier"
59
+ )
60
+ .map((a) => a.name.name)
61
+ .join(", ")
62
+
63
+ for (const child of inputChildren) {
64
+ addComment({
65
+ element: child,
66
+ j,
67
+ scope: SCOPE,
68
+ source,
69
+ text: `Tapestry doesn't support InputBox passing props to children (${propNames}). Please migrate manually.`,
70
+ })
71
+ hasChanges = true
72
+ }
73
+ return
74
+ }
75
+
76
+ // Case: InputBox has more than one non-whitespace child — add TODO to each Input child, don't unwrap
77
+ if (elementChildren.length > 1) {
78
+ for (const child of inputChildren) {
79
+ addComment({
80
+ element: child,
81
+ j,
82
+ scope: SCOPE,
83
+ source,
84
+ text: "InputBox has multiple children and cannot be auto-unwrapped. Please migrate manually.",
85
+ })
86
+ hasChanges = true
87
+ }
88
+ return
89
+ }
90
+
91
+ // Only unwrap when there is exactly one non-whitespace child and it is a convertible Input
92
+ if (elementChildren.length !== 1 || inputChildren.length !== 1) return
93
+
94
+ const parent = path.parent?.value
95
+ if (parent?.children) {
96
+ const idx = parent.children.indexOf(el)
97
+ if (idx === -1) return
98
+ parent.children.splice(idx, 1, ...(el.children || []))
99
+ } else {
100
+ // Root-level JSX — use path.replace
101
+ path.replace(inputChildren[0])
102
+ }
103
+ hasChanges = true
104
+ })
105
+
106
+ return hasChanges ? source.toSource() : null
107
+ }
108
+
109
+ export default transform
@@ -0,0 +1,128 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./removeRedundantAriaLabel"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string | null {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ const api = {
11
+ j,
12
+ jscodeshift: j,
13
+ report: () => {},
14
+ stats: () => {},
15
+ }
16
+ const result = transform(fileInfo, api, {})
17
+ return result as string | null
18
+ }
19
+
20
+ describe("removeRedundantAriaLabel transform", () => {
21
+ describe("basic transformation", () => {
22
+ it("should remove aria-label when both aria-label and label are present", () => {
23
+ const input = `
24
+ import { Input } from "@planningcenter/tapestry-react"
25
+
26
+ function Component() {
27
+ return <Input aria-label="Name" label="Name" />
28
+ }
29
+ `
30
+
31
+ const result = applyTransform(input)
32
+
33
+ expect(result).toContain('<Input label="Name" />')
34
+ expect(result).not.toContain("aria-label")
35
+ })
36
+
37
+ it("should preserve aria-label when no label prop exists", () => {
38
+ const input = `
39
+ import { Input } from "@planningcenter/tapestry-react"
40
+
41
+ function Component() {
42
+ return <Input aria-label="Name" />
43
+ }
44
+ `
45
+
46
+ const result = applyTransform(input)
47
+
48
+ expect(result).toBeNull()
49
+ })
50
+
51
+ it("should return null when Input has neither aria-label nor label", () => {
52
+ const input = `
53
+ import { Input } from "@planningcenter/tapestry-react"
54
+
55
+ function Component() {
56
+ return <Input onChange={handleChange} />
57
+ }
58
+ `
59
+
60
+ const result = applyTransform(input)
61
+
62
+ expect(result).toBeNull()
63
+ })
64
+ })
65
+
66
+ describe("edge cases", () => {
67
+ it("should not affect other components with the same props", () => {
68
+ const input = `
69
+ import { Input } from "@planningcenter/tapestry-react"
70
+
71
+ function Component() {
72
+ return (
73
+ <div>
74
+ <Input aria-label="Name" label="Name" />
75
+ <input aria-label="Name" label="Name" />
76
+ </div>
77
+ )
78
+ }
79
+ `
80
+
81
+ const result = applyTransform(input)
82
+
83
+ expect(result).toContain('<Input label="Name" />')
84
+ expect(result).toContain('<input aria-label="Name" label="Name" />')
85
+ })
86
+
87
+ it("should handle expression syntax aria-label={'text'}", () => {
88
+ const input = `
89
+ import { Input } from "@planningcenter/tapestry-react"
90
+
91
+ function Component() {
92
+ return <Input aria-label={"Name"} label="Name" />
93
+ }
94
+ `
95
+
96
+ const result = applyTransform(input)
97
+
98
+ if (result) {
99
+ expect(result).toContain('<Input label="Name" />')
100
+ expect(result).not.toContain("aria-label")
101
+ } else {
102
+ expect(input).toContain('aria-label={"Name"}')
103
+ }
104
+ })
105
+
106
+ it("should handle multiple Inputs in one file with mixed cases", () => {
107
+ const input = `
108
+ import { Input } from "@planningcenter/tapestry-react"
109
+
110
+ function Component() {
111
+ return (
112
+ <div>
113
+ <Input aria-label="Name" label="Name" />
114
+ <Input aria-label="Email" />
115
+ <Input label="Phone" />
116
+ </div>
117
+ )
118
+ }
119
+ `
120
+
121
+ const result = applyTransform(input)
122
+
123
+ expect(result).toContain('<Input label="Name" />')
124
+ expect(result).toContain('aria-label="Email"')
125
+ expect(result).toContain('<Input label="Phone" />')
126
+ })
127
+ })
128
+ })
@@ -0,0 +1,21 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { removeAttribute } from "../../shared/actions/removeAttribute"
4
+ import { andConditions } from "../../shared/conditions/andConditions"
5
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
6
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
7
+ import { transformableInput } from "../transformableInput"
8
+
9
+ const transform: Transform = attributeTransformFactory({
10
+ condition: andConditions(
11
+ transformableInput,
12
+ hasAttribute("aria-label"),
13
+ hasAttribute("label")
14
+ ),
15
+ targetComponent: "Input",
16
+ targetPackage: "@planningcenter/tapestry-react",
17
+ transform: (element, { j, source }) =>
18
+ removeAttribute("aria-label", { element, j, source }),
19
+ })
20
+
21
+ export default transform