@planningcenter/tapestry-migration-cli 3.1.0-rc.0 → 3.1.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) 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/removeTypeInput.test.ts +212 -0
  36. package/src/components/input/transforms/removeTypeInput.ts +22 -0
  37. package/src/components/input/transforms/removeTypeText.test.ts +160 -0
  38. package/src/components/input/transforms/removeTypeText.ts +18 -0
  39. package/src/components/input/transforms/sizeMapping.test.ts +198 -0
  40. package/src/components/input/transforms/sizeMapping.ts +17 -0
  41. package/src/components/input/transforms/skipRenderSideProps.test.ts +236 -0
  42. package/src/components/input/transforms/skipRenderSideProps.ts +27 -0
  43. package/src/components/input/transforms/stateToInvalid.test.ts +208 -0
  44. package/src/components/input/transforms/stateToInvalid.ts +59 -0
  45. package/src/components/input/transforms/stateToInvalidTernary.test.ts +159 -0
  46. package/src/components/input/transforms/stateToInvalidTernary.ts +13 -0
  47. package/src/components/input/transforms/unsupportedProps.test.ts +566 -0
  48. package/src/components/input/transforms/unsupportedProps.ts +84 -0
  49. package/src/components/link/transforms/reviewStyles.test.ts +0 -1
  50. package/src/components/select/index.ts +54 -0
  51. package/src/components/select/transformableSelect.ts +7 -0
  52. package/src/components/select/transforms/auditSpreadProps.test.ts +103 -0
  53. package/src/components/select/transforms/auditSpreadProps.ts +26 -0
  54. package/src/components/select/transforms/childrenToOptions.test.ts +329 -0
  55. package/src/components/select/transforms/childrenToOptions.ts +282 -0
  56. package/src/components/select/transforms/convertLegacyOptions.test.ts +150 -0
  57. package/src/components/select/transforms/convertLegacyOptions.ts +105 -0
  58. package/src/components/select/transforms/convertStyleProps.test.ts +73 -0
  59. package/src/components/select/transforms/convertStyleProps.ts +12 -0
  60. package/src/components/select/transforms/emptyValueToPlaceholder.test.ts +122 -0
  61. package/src/components/select/transforms/emptyValueToPlaceholder.ts +22 -0
  62. package/src/components/select/transforms/innerRefToRef.test.ts +89 -0
  63. package/src/components/select/transforms/innerRefToRef.ts +18 -0
  64. package/src/components/select/transforms/mapChildrenToOptions.test.ts +498 -0
  65. package/src/components/select/transforms/mapChildrenToOptions.ts +324 -0
  66. package/src/components/select/transforms/mergeFieldIntoSelect.test.ts +430 -0
  67. package/src/components/select/transforms/mergeFieldIntoSelect.ts +233 -0
  68. package/src/components/select/transforms/mergeSelectLabel.test.ts +458 -0
  69. package/src/components/select/transforms/mergeSelectLabel.ts +225 -0
  70. package/src/components/select/transforms/moveSelectImport.test.ts +148 -0
  71. package/src/components/select/transforms/moveSelectImport.ts +14 -0
  72. package/src/components/select/transforms/removeDefaultProps.test.ts +249 -0
  73. package/src/components/select/transforms/removeDefaultProps.ts +112 -0
  74. package/src/components/select/transforms/sizeMapping.test.ts +188 -0
  75. package/src/components/select/transforms/sizeMapping.ts +17 -0
  76. package/src/components/select/transforms/skipMultipleSelect.test.ts +148 -0
  77. package/src/components/select/transforms/skipMultipleSelect.ts +23 -0
  78. package/src/components/select/transforms/unsupportedProps.test.ts +252 -0
  79. package/src/components/select/transforms/unsupportedProps.ts +44 -0
  80. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +102 -0
  81. package/src/components/shared/transformFactories/helpers/manageImports.ts +14 -12
  82. package/src/components/shared/transformFactories/sizeMappingFactory.ts +9 -2
  83. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +56 -17
  84. package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +65 -0
  85. package/src/components/text-area/index.ts +48 -0
  86. package/src/components/text-area/transforms/auditSpreadProps.test.ts +139 -0
  87. package/src/components/text-area/transforms/auditSpreadProps.ts +10 -0
  88. package/src/components/text-area/transforms/convertStyleProps.test.ts +158 -0
  89. package/src/components/text-area/transforms/convertStyleProps.ts +10 -0
  90. package/src/components/text-area/transforms/innerRefToRef.test.ts +206 -0
  91. package/src/components/text-area/transforms/innerRefToRef.ts +14 -0
  92. package/src/components/text-area/transforms/mergeFieldIntoTextArea.test.ts +477 -0
  93. package/src/components/text-area/transforms/mergeFieldIntoTextArea.ts +227 -0
  94. package/src/components/text-area/transforms/moveTextAreaImport.test.ts +168 -0
  95. package/src/components/text-area/transforms/moveTextAreaImport.ts +13 -0
  96. package/src/components/text-area/transforms/removeDuplicateKeys.test.ts +129 -0
  97. package/src/components/text-area/transforms/removeDuplicateKeys.ts +8 -0
  98. package/src/components/text-area/transforms/removeRedundantAriaLabel.test.ts +183 -0
  99. package/src/components/text-area/transforms/removeRedundantAriaLabel.ts +59 -0
  100. package/src/components/text-area/transforms/sizeMapping.test.ts +199 -0
  101. package/src/components/text-area/transforms/sizeMapping.ts +15 -0
  102. package/src/components/text-area/transforms/stateToInvalid.test.ts +204 -0
  103. package/src/components/text-area/transforms/stateToInvalid.ts +57 -0
  104. package/src/components/text-area/transforms/stateToInvalidTernary.test.ts +133 -0
  105. package/src/components/text-area/transforms/stateToInvalidTernary.ts +11 -0
  106. package/src/components/text-area/transforms/unsupportedProps.test.ts +275 -0
  107. package/src/components/text-area/transforms/unsupportedProps.ts +35 -0
  108. package/src/index.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "3.1.0-rc.0",
3
+ "version": "3.1.0-rc.10",
4
4
  "description": "CLI tool for Tapestry migrations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@emotion/react": "^11.14.0",
35
- "@planningcenter/tapestry": "^3.1.0-rc.0",
35
+ "@planningcenter/tapestry": "^3.1.0-rc.10",
36
36
  "@planningcenter/tapestry-react": "^4.11.5",
37
37
  "@types/jscodeshift": "^17.3.0",
38
38
  "@types/node": "^20.0.0",
@@ -52,5 +52,5 @@
52
52
  "publishConfig": {
53
53
  "access": "public"
54
54
  },
55
- "gitHead": "40a91a7f151dcdc14176e0db2761062b31e51086"
55
+ "gitHead": "f2b292d99911669942785d4c0d49327caf0fa663"
56
56
  }
@@ -252,6 +252,103 @@ describe("convertStyleProps transform", () => {
252
252
  expect(result).toContain("paddingTop:")
253
253
  expect(result).not.toContain("marginLeft={16}")
254
254
  expect(result).not.toContain('paddingTop="8px"')
255
+ expect(result).toContain('color: "red"')
256
+ expect(result).toContain("fontSize: 14")
257
+ })
258
+
259
+ it("should preserve non-conflicting existing props when merging", () => {
260
+ const source = `
261
+ import { Button } from "@planningcenter/tapestry-react"
262
+
263
+ export function TestComponent() {
264
+ return (
265
+ <Button
266
+ style={{ color: "red" }}
267
+ marginLeft={16}
268
+ >
269
+ Test
270
+ </Button>
271
+ )
272
+ }
273
+ `
274
+
275
+ const result = applyTransform(source)
276
+
277
+ expect(result).not.toBeNull()
278
+ expect(result).toContain('color: "red"')
279
+ expect(result).toContain("marginLeft:")
280
+ expect(result).not.toContain("marginLeft={16}")
281
+ })
282
+
283
+ it("should let new props win over conflicting existing props", () => {
284
+ const source = `
285
+ import { Button } from "@planningcenter/tapestry-react"
286
+
287
+ export function TestComponent() {
288
+ return (
289
+ <Button
290
+ style={{ marginLeft: "999px" }}
291
+ marginLeft={16}
292
+ >
293
+ Test
294
+ </Button>
295
+ )
296
+ }
297
+ `
298
+
299
+ const result = applyTransform(source)
300
+
301
+ expect(result).not.toBeNull()
302
+ expect(result).not.toContain('"999px"')
303
+ expect(result).toContain("marginLeft:")
304
+ })
305
+
306
+ it("should preserve SpreadElement when merging", () => {
307
+ const source = `
308
+ import { Button } from "@planningcenter/tapestry-react"
309
+
310
+ export function TestComponent() {
311
+ return (
312
+ <Button
313
+ style={{ ...base, color: "red" }}
314
+ paddingTop="8px"
315
+ >
316
+ Test
317
+ </Button>
318
+ )
319
+ }
320
+ `
321
+
322
+ const result = applyTransform(source)
323
+
324
+ expect(result).not.toBeNull()
325
+ expect(result).toContain("...base")
326
+ expect(result).toContain('color: "red"')
327
+ expect(result).toContain("paddingTop:")
328
+ })
329
+
330
+ it("should merge computed styles into existing dynamic style prop via spread", () => {
331
+ const source = `
332
+ import { Button } from "@planningcenter/tapestry-react"
333
+
334
+ export function TestComponent() {
335
+ return (
336
+ <Button
337
+ style={getStyles()}
338
+ marginLeft={16}
339
+ >
340
+ Test
341
+ </Button>
342
+ )
343
+ }
344
+ `
345
+
346
+ const result = applyTransform(source)
347
+
348
+ expect(result).not.toBeNull()
349
+ expect(result).toContain("...getStyles()")
350
+ expect(result).toContain("marginLeft:")
351
+ expect(result).not.toContain("Could not merge")
255
352
  })
256
353
  })
257
354
 
@@ -5,7 +5,6 @@ import transform from "./removeTypeButton"
5
5
 
6
6
  const j = jscodeshift.withParser("tsx")
7
7
 
8
- // Helper to run transform and get result
9
8
  function applyTransform(source: string): string | null {
10
9
  const fileInfo = { path: "test.tsx", source }
11
10
  const api = {
@@ -91,6 +91,9 @@ function Test() {
91
91
  'import { Button } from "@planningcenter/tapestry-react"'
92
92
  )
93
93
  expect(result).toContain(
94
+ 'import { Checkbox } from "@planningcenter/tapestry"'
95
+ )
96
+ expect(result).not.toContain(
94
97
  'import { Checkbox } from "@planningcenter/tapestry-react"'
95
98
  )
96
99
  })
@@ -0,0 +1,66 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import auditSpreadProps from "./transforms/auditSpreadProps"
4
+ import autoWidthTransform from "./transforms/autoWidthTransform"
5
+ import convertStyleProps from "./transforms/convertStyleProps"
6
+ import highlightOnInteractionToSelectTextOnFocus from "./transforms/highlightOnInteractionToSelectTextOnFocus"
7
+ import inputLabelToLabelProp from "./transforms/inputLabelToLabelProp"
8
+ import mergeFieldIntoInput from "./transforms/mergeFieldIntoInput"
9
+ import mergeInputLabel from "./transforms/mergeInputLabel"
10
+ import moveInputImport from "./transforms/moveInputImport"
11
+ import numberFieldAddTypeNumber from "./transforms/numberFieldAddTypeNumber"
12
+ import numberFieldRenameToInput from "./transforms/numberFieldRenameToInput"
13
+ import removeAsInput from "./transforms/removeAsInput"
14
+ import removeDuplicateKeys from "./transforms/removeDuplicateKeys"
15
+ import removeInputBox from "./transforms/removeInputBox"
16
+ import removeRedundantAriaLabel from "./transforms/removeRedundantAriaLabel"
17
+ import removeTypeText from "./transforms/removeTypeText"
18
+ import sizeMapping from "./transforms/sizeMapping"
19
+ import skipRenderSideProps from "./transforms/skipRenderSideProps"
20
+ import stateToInvalid from "./transforms/stateToInvalid"
21
+ import stateToInvalidTernary from "./transforms/stateToInvalidTernary"
22
+ import unsupportedProps from "./transforms/unsupportedProps"
23
+
24
+ const transform: Transform = (fileInfo, api, options) => {
25
+ let currentSource = fileInfo.source
26
+ let hasAnyChanges = false
27
+
28
+ const transforms = [
29
+ numberFieldAddTypeNumber,
30
+ numberFieldRenameToInput,
31
+ removeInputBox,
32
+ skipRenderSideProps,
33
+ mergeInputLabel,
34
+ mergeFieldIntoInput,
35
+ auditSpreadProps,
36
+ highlightOnInteractionToSelectTextOnFocus,
37
+ sizeMapping,
38
+ autoWidthTransform,
39
+ stateToInvalidTernary,
40
+ stateToInvalid,
41
+ convertStyleProps,
42
+ inputLabelToLabelProp,
43
+ removeDuplicateKeys,
44
+ removeRedundantAriaLabel,
45
+ removeTypeText,
46
+ removeAsInput,
47
+ unsupportedProps,
48
+ moveInputImport,
49
+ ]
50
+
51
+ for (const individualTransform of transforms) {
52
+ const result = individualTransform(
53
+ { ...fileInfo, source: currentSource },
54
+ api,
55
+ options
56
+ )
57
+ if (result && result !== currentSource) {
58
+ currentSource = result as string
59
+ hasAnyChanges = true
60
+ }
61
+ }
62
+
63
+ return hasAnyChanges ? currentSource : null
64
+ }
65
+
66
+ export default transform
@@ -0,0 +1,8 @@
1
+ import { hasAttribute } from "../shared/conditions/hasAttribute"
2
+ import { notCondition } from "../shared/conditions/notCondition"
3
+ import { orConditions } from "../shared/conditions/orConditions"
4
+ import { TransformCondition } from "../shared/types"
5
+
6
+ export const transformableInput: TransformCondition = notCondition(
7
+ orConditions(hasAttribute("renderLeft"), hasAttribute("renderRight"))
8
+ )
@@ -0,0 +1,192 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./auditSpreadProps"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ const AUDIT_COMMENT =
9
+ "TODO: tapestry-migration (spreadAttribute): Spread props can contain unsupported props, please explore usages and migrate as needed."
10
+
11
+ function applyTransform(source: string) {
12
+ const fileInfo = { path: "test.tsx", source }
13
+ return transform(
14
+ fileInfo,
15
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
16
+ {}
17
+ ) as string | null
18
+ }
19
+
20
+ describe("auditSpreadProps transform", () => {
21
+ describe("basic transformations", () => {
22
+ it("should add comment to Input with single spread prop", () => {
23
+ const input = `
24
+ import { Input } from "@planningcenter/tapestry-react"
25
+
26
+ export default function Test() {
27
+ const props = { onChange: handleChange }
28
+ return <Input {...props} label="Name" />
29
+ }
30
+ `.trim()
31
+
32
+ const result = applyTransform(input)
33
+ expect(result).toContain(AUDIT_COMMENT)
34
+ expect(result).toContain("{...props}")
35
+ })
36
+
37
+ it("should add comment to Input with multiple spread props", () => {
38
+ const input = `
39
+ import { Input } from "@planningcenter/tapestry-react"
40
+
41
+ export default function Test() {
42
+ const baseProps = { onChange: handleChange }
43
+ const styleProps = { className: "input" }
44
+ return <Input {...baseProps} {...styleProps} label="Name" />
45
+ }
46
+ `.trim()
47
+
48
+ const result = applyTransform(input)
49
+ expect(result).toContain(AUDIT_COMMENT)
50
+ expect(result).toContain("{...baseProps}")
51
+ expect(result).toContain("{...styleProps}")
52
+ })
53
+
54
+ it("should handle Input with spread props and regular attributes", () => {
55
+ const input = `
56
+ import { Input } from "@planningcenter/tapestry-react"
57
+
58
+ export default function Test() {
59
+ const props = { onChange: handleChange }
60
+ return <Input label="Name" {...props} disabled />
61
+ }
62
+ `.trim()
63
+
64
+ const result = applyTransform(input)
65
+ expect(result).toContain(AUDIT_COMMENT)
66
+ expect(result).toContain('label="Name"')
67
+ expect(result).toContain("{...props}")
68
+ expect(result).toContain("disabled")
69
+ })
70
+
71
+ it("should handle multiple Input components with spread props", () => {
72
+ const input = `
73
+ import { Input } from "@planningcenter/tapestry-react"
74
+
75
+ export default function Test() {
76
+ const props1 = { onChange: handleChange1 }
77
+ const props2 = { onChange: handleChange2 }
78
+ return (
79
+ <div>
80
+ <Input {...props1} label="First" />
81
+ <Input {...props2} label="Second" />
82
+ </div>
83
+ )
84
+ }
85
+ `.trim()
86
+
87
+ const result = applyTransform(input)
88
+ expect(result).toContain(AUDIT_COMMENT)
89
+ expect(result).toContain("{...props1}")
90
+ expect(result).toContain("{...props2}")
91
+ })
92
+ })
93
+
94
+ describe("edge cases", () => {
95
+ it("should not transform Input without spread props", () => {
96
+ const input = `
97
+ import { Input } from "@planningcenter/tapestry-react"
98
+
99
+ export default function Test() {
100
+ return <Input onChange={handleChange} label="Name" />
101
+ }
102
+ `.trim()
103
+
104
+ const result = applyTransform(input)
105
+ expect(result).toBe(null)
106
+ })
107
+
108
+ it("should not transform if Input is not imported from @planningcenter/tapestry-react", () => {
109
+ const input = `
110
+ import { Input } from "other-library"
111
+
112
+ export default function Test() {
113
+ const props = { onChange: handleChange }
114
+ return <Input {...props} label="Name" />
115
+ }
116
+ `.trim()
117
+
118
+ const result = applyTransform(input)
119
+ expect(result).toBe(null)
120
+ })
121
+
122
+ it("should handle Input with alias import", () => {
123
+ const input = `
124
+ import { Input as TapestryInput } from "@planningcenter/tapestry-react"
125
+
126
+ export default function Test() {
127
+ const props = { onChange: handleChange }
128
+ return <TapestryInput {...props} label="Name" />
129
+ }
130
+ `.trim()
131
+
132
+ const result = applyTransform(input)
133
+ expect(result).toContain(AUDIT_COMMENT)
134
+ expect(result).toContain("{...props}")
135
+ })
136
+
137
+ it("should handle mixed Input components (with and without spread props)", () => {
138
+ const input = `
139
+ import { Input } from "@planningcenter/tapestry-react"
140
+
141
+ export default function Test() {
142
+ const props = { onChange: handleSave }
143
+ return (
144
+ <div>
145
+ <Input {...props} label="First" />
146
+ <Input label="Second" />
147
+ <Input onChange={handleChange} label="Third" />
148
+ </div>
149
+ )
150
+ }
151
+ `.trim()
152
+
153
+ const result = applyTransform(input)
154
+ expect(result).toContain(AUDIT_COMMENT)
155
+ expect(result).toContain("{...props}")
156
+ expect(result).toContain('<Input label="Second" />')
157
+ expect(result).toContain("onChange={handleChange}")
158
+ })
159
+
160
+ it("should return null when no Input components have spread props", () => {
161
+ const input = `
162
+ import { Input } from "@planningcenter/tapestry-react"
163
+
164
+ export default function Test() {
165
+ return (
166
+ <div>
167
+ <Input label="First" />
168
+ <Input label="Second" />
169
+ </div>
170
+ )
171
+ }
172
+ `.trim()
173
+
174
+ const result = applyTransform(input)
175
+ expect(result).toBe(null)
176
+ })
177
+
178
+ it("should return null when no Input imports exist", () => {
179
+ const input = `
180
+ import { Button } from "@planningcenter/tapestry-react"
181
+
182
+ export default function Test() {
183
+ const props = { onClick: handleClick }
184
+ return <Button {...props}>Save</Button>
185
+ }
186
+ `.trim()
187
+
188
+ const result = applyTransform(input)
189
+ expect(result).toBe(null)
190
+ })
191
+ })
192
+ })
@@ -0,0 +1,26 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
4
+ import { getSpreadProps } from "../../shared/actions/getSpreadProps"
5
+ import { hasSpreadProps } from "../../shared/actions/hasSpreadProps"
6
+ import { andConditions } from "../../shared/conditions/andConditions"
7
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
8
+ import { transformableInput } from "../transformableInput"
9
+
10
+ const COMMENT =
11
+ "Spread props can contain unsupported props, please explore usages and migrate as needed."
12
+
13
+ const transform: Transform = attributeTransformFactory({
14
+ condition: andConditions(hasSpreadProps, transformableInput),
15
+ targetComponent: "Input",
16
+ targetPackage: "@planningcenter/tapestry-react",
17
+ transform: (element, { j }) => {
18
+ const spreadProps = getSpreadProps(element)
19
+ spreadProps.forEach((prop) =>
20
+ addCommentToAttribute({ attribute: prop, j, text: COMMENT })
21
+ )
22
+ return spreadProps.length > 0
23
+ },
24
+ })
25
+
26
+ export default transform
@@ -0,0 +1,172 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./autoWidthTransform"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ const result = transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ {}
14
+ ) as string | null
15
+ return result || source
16
+ }
17
+
18
+ describe("autoWidthTransform transform", () => {
19
+ describe("boolean values — pass through unchanged", () => {
20
+ it("should not add comment for shorthand autoWidth", () => {
21
+ const input = `
22
+ import { Input } from "@planningcenter/tapestry-react"
23
+
24
+ function Test() {
25
+ return <Input autoWidth label="Name" />
26
+ }
27
+ `.trim()
28
+
29
+ const result = applyTransform(input)
30
+ expect(result).not.toContain("TODO: tapestry-migration")
31
+ expect(result).toBe(input)
32
+ })
33
+
34
+ it("should not add comment for autoWidth={true}", () => {
35
+ const input = `
36
+ import { Input } from "@planningcenter/tapestry-react"
37
+
38
+ function Test() {
39
+ return <Input autoWidth={true} label="Name" />
40
+ }
41
+ `.trim()
42
+
43
+ const result = applyTransform(input)
44
+ expect(result).not.toContain("TODO: tapestry-migration")
45
+ expect(result).toBe(input)
46
+ })
47
+
48
+ it("should not add comment for autoWidth={false}", () => {
49
+ const input = `
50
+ import { Input } from "@planningcenter/tapestry-react"
51
+
52
+ function Test() {
53
+ return <Input autoWidth={false} label="Name" />
54
+ }
55
+ `.trim()
56
+
57
+ const result = applyTransform(input)
58
+ expect(result).not.toContain("TODO: tapestry-migration")
59
+ expect(result).toBe(input)
60
+ })
61
+ })
62
+
63
+ describe("non-boolean values — add comment", () => {
64
+ it("should add comment for numeric autoWidth", () => {
65
+ const input = `
66
+ import { Input } from "@planningcenter/tapestry-react"
67
+
68
+ function Test() {
69
+ return <Input autoWidth={120} label="Name" />
70
+ }
71
+ `.trim()
72
+
73
+ const result = applyTransform(input)
74
+ expect(result).toContain("TODO: tapestry-migration (autoWidth)")
75
+ expect(result).toContain(
76
+ "'autoWidth' no longer accepts measurement values"
77
+ )
78
+ expect(result).toContain("autoWidth={120}")
79
+ })
80
+
81
+ it("should add comment for string autoWidth", () => {
82
+ const input = `
83
+ import { Input } from "@planningcenter/tapestry-react"
84
+
85
+ function Test() {
86
+ return <Input autoWidth="fit-content" label="Name" />
87
+ }
88
+ `.trim()
89
+
90
+ const result = applyTransform(input)
91
+ expect(result).toContain("TODO: tapestry-migration (autoWidth)")
92
+ expect(result).toContain(
93
+ "'autoWidth' no longer accepts measurement values"
94
+ )
95
+ })
96
+
97
+ it("should add comment for variable autoWidth", () => {
98
+ const input = `
99
+ import { Input } from "@planningcenter/tapestry-react"
100
+
101
+ function Test() {
102
+ const width = 120
103
+ return <Input autoWidth={width} label="Name" />
104
+ }
105
+ `.trim()
106
+
107
+ const result = applyTransform(input)
108
+ expect(result).toContain("TODO: tapestry-migration (autoWidth)")
109
+ })
110
+
111
+ it("should add comment for expression autoWidth", () => {
112
+ const input = `
113
+ import { Input } from "@planningcenter/tapestry-react"
114
+
115
+ function Test() {
116
+ return <Input autoWidth={isWide ? 200 : 100} label="Name" />
117
+ }
118
+ `.trim()
119
+
120
+ const result = applyTransform(input)
121
+ expect(result).toContain("TODO: tapestry-migration (autoWidth)")
122
+ })
123
+ })
124
+
125
+ describe("edge cases", () => {
126
+ it("should not affect Input without autoWidth prop", () => {
127
+ const input = `
128
+ import { Input } from "@planningcenter/tapestry-react"
129
+
130
+ function Test() {
131
+ return <Input label="Name" />
132
+ }
133
+ `.trim()
134
+
135
+ const result = applyTransform(input)
136
+ expect(result).toBe(input)
137
+ })
138
+
139
+ it("should not affect other components", () => {
140
+ const input = `
141
+ import { Input, Button } from "@planningcenter/tapestry-react"
142
+
143
+ function Test() {
144
+ return (
145
+ <div>
146
+ <Button autoWidth={120}>Click</Button>
147
+ <Input autoWidth={120} label="Name" />
148
+ </div>
149
+ )
150
+ }
151
+ `.trim()
152
+
153
+ const result = applyTransform(input)
154
+ expect(result).toContain("TODO: tapestry-migration (autoWidth)")
155
+ // Verify the Button is unchanged
156
+ expect(result).toContain("<Button autoWidth={120}>Click</Button>")
157
+ })
158
+
159
+ it("should not transform if not imported from @planningcenter/tapestry-react", () => {
160
+ const input = `
161
+ import { Input } from "other-library"
162
+
163
+ function Test() {
164
+ return <Input autoWidth={120} label="Name" />
165
+ }
166
+ `.trim()
167
+
168
+ const result = applyTransform(input)
169
+ expect(result).toBe(input)
170
+ })
171
+ })
172
+ })
@@ -0,0 +1,41 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { addComment } from "../../shared/actions/addComment"
4
+ import { getAttribute } from "../../shared/actions/getAttribute"
5
+ import { andConditions } from "../../shared/conditions/andConditions"
6
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
7
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
8
+ import { transformableInput } from "../transformableInput"
9
+
10
+ const transform: Transform = attributeTransformFactory({
11
+ condition: andConditions(hasAttribute("autoWidth"), transformableInput),
12
+ targetComponent: "Input",
13
+ targetPackage: "@planningcenter/tapestry-react",
14
+ transform: (element, { j, source }) => {
15
+ const attr = getAttribute({ element, name: "autoWidth" })
16
+ if (!attr) return false
17
+
18
+ // Shorthand boolean (<Input autoWidth />) — pass through
19
+ if (attr.value === null) return false
20
+
21
+ // Explicit boolean ({true} or {false}) — pass through
22
+ if (
23
+ attr.value.type === "JSXExpressionContainer" &&
24
+ attr.value.expression.type === "BooleanLiteral"
25
+ ) {
26
+ return false
27
+ }
28
+
29
+ // Non-boolean value — add comment
30
+ addComment({
31
+ element,
32
+ j,
33
+ scope: "autoWidth",
34
+ source,
35
+ text: "'autoWidth' no longer accepts measurement values. Use autoWidth={true} to grow to content width, or remove the prop.",
36
+ })
37
+ return true
38
+ },
39
+ })
40
+
41
+ export default transform