@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.
Files changed (115) hide show
  1. package/dist/tapestry-react-shim.cjs +7 -1
  2. package/package.json +3 -3
  3. package/src/components/button/transforms/convertStyleProps.test.ts +97 -0
  4. package/src/components/button/transforms/removeTypeButton.test.ts +0 -1
  5. package/src/components/checkbox/transforms/moveCheckboxImport.test.ts +3 -0
  6. package/src/components/input/index.ts +66 -0
  7. package/src/components/input/transformableInput.ts +49 -0
  8. package/src/components/input/transforms/auditSpreadProps.test.ts +192 -0
  9. package/src/components/input/transforms/auditSpreadProps.ts +26 -0
  10. package/src/components/input/transforms/autoWidthTransform.test.ts +172 -0
  11. package/src/components/input/transforms/autoWidthTransform.ts +41 -0
  12. package/src/components/input/transforms/convertStyleProps.test.ts +128 -0
  13. package/src/components/input/transforms/convertStyleProps.ts +12 -0
  14. package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.test.ts +186 -0
  15. package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.ts +27 -0
  16. package/src/components/input/transforms/inputLabelToLabelProp.test.ts +319 -0
  17. package/src/components/input/transforms/inputLabelToLabelProp.ts +203 -0
  18. package/src/components/input/transforms/mergeFieldIntoInput.test.ts +469 -0
  19. package/src/components/input/transforms/mergeFieldIntoInput.ts +7 -0
  20. package/src/components/input/transforms/mergeInputLabel.test.ts +458 -0
  21. package/src/components/input/transforms/mergeInputLabel.ts +204 -0
  22. package/src/components/input/transforms/moveInputImport.test.ts +166 -0
  23. package/src/components/input/transforms/moveInputImport.ts +14 -0
  24. package/src/components/input/transforms/numberFieldAddTypeNumber.test.ts +92 -0
  25. package/src/components/input/transforms/numberFieldAddTypeNumber.ts +14 -0
  26. package/src/components/input/transforms/numberFieldRenameToInput.test.ts +126 -0
  27. package/src/components/input/transforms/numberFieldRenameToInput.ts +9 -0
  28. package/src/components/input/transforms/removeAsInput.test.ts +139 -0
  29. package/src/components/input/transforms/removeAsInput.ts +20 -0
  30. package/src/components/input/transforms/removeDuplicateKeys.test.ts +302 -0
  31. package/src/components/input/transforms/removeDuplicateKeys.ts +10 -0
  32. package/src/components/input/transforms/removeInputBox.test.ts +352 -0
  33. package/src/components/input/transforms/removeInputBox.ts +109 -0
  34. package/src/components/input/transforms/removeRedundantAriaLabel.test.ts +128 -0
  35. package/src/components/input/transforms/removeRedundantAriaLabel.ts +21 -0
  36. package/src/components/input/transforms/removeTypeInput.test.ts +212 -0
  37. package/src/components/input/transforms/removeTypeInput.ts +22 -0
  38. package/src/components/input/transforms/removeTypeText.test.ts +160 -0
  39. package/src/components/input/transforms/removeTypeText.ts +17 -0
  40. package/src/components/input/transforms/sizeMapping.test.ts +198 -0
  41. package/src/components/input/transforms/sizeMapping.ts +17 -0
  42. package/src/components/input/transforms/skipRenderSideProps.test.ts +236 -0
  43. package/src/components/input/transforms/skipRenderSideProps.ts +27 -0
  44. package/src/components/input/transforms/stateToInvalid.test.ts +208 -0
  45. package/src/components/input/transforms/stateToInvalid.ts +59 -0
  46. package/src/components/input/transforms/stateToInvalidTernary.test.ts +159 -0
  47. package/src/components/input/transforms/stateToInvalidTernary.ts +13 -0
  48. package/src/components/input/transforms/unsupportedProps.test.ts +566 -0
  49. package/src/components/input/transforms/unsupportedProps.ts +84 -0
  50. package/src/components/link/transforms/reviewStyles.test.ts +0 -1
  51. package/src/components/select/index.ts +58 -0
  52. package/src/components/select/transformableSelect.ts +7 -0
  53. package/src/components/select/transforms/auditSpreadProps.test.ts +103 -0
  54. package/src/components/select/transforms/auditSpreadProps.ts +26 -0
  55. package/src/components/select/transforms/childrenToOptions.test.ts +367 -0
  56. package/src/components/select/transforms/childrenToOptions.ts +295 -0
  57. package/src/components/select/transforms/convertLegacyOptions.test.ts +150 -0
  58. package/src/components/select/transforms/convertLegacyOptions.ts +105 -0
  59. package/src/components/select/transforms/convertStyleProps.test.ts +73 -0
  60. package/src/components/select/transforms/convertStyleProps.ts +12 -0
  61. package/src/components/select/transforms/emptyValueToPlaceholder.test.ts +122 -0
  62. package/src/components/select/transforms/emptyValueToPlaceholder.ts +22 -0
  63. package/src/components/select/transforms/innerRefToRef.test.ts +89 -0
  64. package/src/components/select/transforms/innerRefToRef.ts +18 -0
  65. package/src/components/select/transforms/mapChildrenToOptions.test.ts +521 -0
  66. package/src/components/select/transforms/mapChildrenToOptions.ts +312 -0
  67. package/src/components/select/transforms/mergeFieldIntoSelect.test.ts +506 -0
  68. package/src/components/select/transforms/mergeFieldIntoSelect.ts +7 -0
  69. package/src/components/select/transforms/mergeSelectLabel.test.ts +458 -0
  70. package/src/components/select/transforms/mergeSelectLabel.ts +225 -0
  71. package/src/components/select/transforms/moveSelectImport.test.ts +148 -0
  72. package/src/components/select/transforms/moveSelectImport.ts +14 -0
  73. package/src/components/select/transforms/removeDefaultProps.test.ts +249 -0
  74. package/src/components/select/transforms/removeDefaultProps.ts +112 -0
  75. package/src/components/select/transforms/sizeMapping.test.ts +188 -0
  76. package/src/components/select/transforms/sizeMapping.ts +17 -0
  77. package/src/components/select/transforms/skipMultipleSelect.test.ts +148 -0
  78. package/src/components/select/transforms/skipMultipleSelect.ts +23 -0
  79. package/src/components/select/transforms/stateToInvalid.test.ts +217 -0
  80. package/src/components/select/transforms/stateToInvalid.ts +59 -0
  81. package/src/components/select/transforms/stateToInvalidTernary.test.ts +146 -0
  82. package/src/components/select/transforms/stateToInvalidTernary.ts +13 -0
  83. package/src/components/select/transforms/unsupportedProps.test.ts +252 -0
  84. package/src/components/select/transforms/unsupportedProps.ts +44 -0
  85. package/src/components/shared/helpers/getAttributeExpression.ts +26 -0
  86. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +102 -0
  87. package/src/components/shared/transformFactories/helpers/manageImports.ts +14 -12
  88. package/src/components/shared/transformFactories/mergeFieldFactory.ts +244 -0
  89. package/src/components/shared/transformFactories/sizeMappingFactory.ts +9 -2
  90. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +56 -17
  91. package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +65 -0
  92. package/src/components/text-area/index.ts +48 -0
  93. package/src/components/text-area/transforms/auditSpreadProps.test.ts +139 -0
  94. package/src/components/text-area/transforms/auditSpreadProps.ts +10 -0
  95. package/src/components/text-area/transforms/convertStyleProps.test.ts +158 -0
  96. package/src/components/text-area/transforms/convertStyleProps.ts +10 -0
  97. package/src/components/text-area/transforms/innerRefToRef.test.ts +206 -0
  98. package/src/components/text-area/transforms/innerRefToRef.ts +14 -0
  99. package/src/components/text-area/transforms/mergeFieldIntoTextArea.test.ts +477 -0
  100. package/src/components/text-area/transforms/mergeFieldIntoTextArea.ts +5 -0
  101. package/src/components/text-area/transforms/moveTextAreaImport.test.ts +168 -0
  102. package/src/components/text-area/transforms/moveTextAreaImport.ts +13 -0
  103. package/src/components/text-area/transforms/removeDuplicateKeys.test.ts +129 -0
  104. package/src/components/text-area/transforms/removeDuplicateKeys.ts +8 -0
  105. package/src/components/text-area/transforms/removeRedundantAriaLabel.test.ts +183 -0
  106. package/src/components/text-area/transforms/removeRedundantAriaLabel.ts +59 -0
  107. package/src/components/text-area/transforms/sizeMapping.test.ts +199 -0
  108. package/src/components/text-area/transforms/sizeMapping.ts +15 -0
  109. package/src/components/text-area/transforms/stateToInvalid.test.ts +204 -0
  110. package/src/components/text-area/transforms/stateToInvalid.ts +57 -0
  111. package/src/components/text-area/transforms/stateToInvalidTernary.test.ts +133 -0
  112. package/src/components/text-area/transforms/stateToInvalidTernary.ts +11 -0
  113. package/src/components/text-area/transforms/unsupportedProps.test.ts +275 -0
  114. package/src/components/text-area/transforms/unsupportedProps.ts +35 -0
  115. package/src/index.ts +2 -1
@@ -0,0 +1,17 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { sizeMappingFactory } from "../../shared/transformFactories/sizeMappingFactory"
4
+ import { transformableSelect } from "../transformableSelect"
5
+
6
+ const transform: Transform = sizeMappingFactory({
7
+ condition: transformableSelect,
8
+ sizeMapping: {
9
+ sm: "md",
10
+ xl: "lg",
11
+ xs: "md",
12
+ },
13
+ targetComponent: "Select",
14
+ targetPackage: "@planningcenter/tapestry-react",
15
+ })
16
+
17
+ export default transform
@@ -0,0 +1,148 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import pipeline from "../index"
5
+ import transform from "./skipMultipleSelect"
6
+
7
+ const j = jscodeshift.withParser("tsx")
8
+
9
+ const SKIP_COMMENT =
10
+ "TODO: tapestry-migration (migration): Automatic migration skipped: Select uses the multiple prop which has no equivalent in the new API. Migrate this Select manually."
11
+
12
+ function applyTransform(source: string): string | null {
13
+ const fileInfo = { path: "test.tsx", source }
14
+ return transform(
15
+ fileInfo,
16
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
17
+ {}
18
+ ) as string | null
19
+ }
20
+
21
+ describe("skipMultipleSelect transform", () => {
22
+ describe("selects that should be skipped", () => {
23
+ it("should add skip comment to Select with multiple", () => {
24
+ const input = `
25
+ import { Select } from "@planningcenter/tapestry-react"
26
+
27
+ function Test() {
28
+ return <Select multiple label="Tags" />
29
+ }
30
+ `.trim()
31
+
32
+ const result = applyTransform(input)
33
+ expect(result).not.toBeNull()
34
+ expect(result).toContain(SKIP_COMMENT)
35
+ })
36
+
37
+ it("should add skip comment to Select with multiple={true}", () => {
38
+ const input = `
39
+ import { Select } from "@planningcenter/tapestry-react"
40
+
41
+ function Test() {
42
+ return <Select multiple={true} label="Tags" />
43
+ }
44
+ `.trim()
45
+
46
+ const result = applyTransform(input)
47
+ expect(result).not.toBeNull()
48
+ expect(result).toContain(SKIP_COMMENT)
49
+ })
50
+
51
+ it("should preserve all props on skipped Select", () => {
52
+ const input = `
53
+ import { Select } from "@planningcenter/tapestry-react"
54
+
55
+ function Test() {
56
+ return (
57
+ <Select
58
+ multiple
59
+ label="Tags"
60
+ size="lg"
61
+ onChange={handleChange}
62
+ />
63
+ )
64
+ }
65
+ `.trim()
66
+
67
+ const result = applyTransform(input)
68
+ expect(result).toContain(SKIP_COMMENT)
69
+ expect(result).toContain('label="Tags"')
70
+ expect(result).toContain('size="lg"')
71
+ expect(result).toContain("onChange={handleChange}")
72
+ })
73
+ })
74
+
75
+ describe("selects that should NOT be skipped", () => {
76
+ it("should return null for Select without multiple", () => {
77
+ const input = `
78
+ import { Select } from "@planningcenter/tapestry-react"
79
+
80
+ function Test() {
81
+ return <Select label="Country" />
82
+ }
83
+ `.trim()
84
+
85
+ const result = applyTransform(input)
86
+ expect(result).toBeNull()
87
+ })
88
+ })
89
+
90
+ describe("pipeline integration", () => {
91
+ it("should not migrate the import when all Selects have multiple", () => {
92
+ const input = `
93
+ import { Select } from "@planningcenter/tapestry-react"
94
+
95
+ function Test() {
96
+ return <Select multiple label="Tags" size="sm" />
97
+ }
98
+ `.trim()
99
+
100
+ const fileInfo = { path: "test.tsx", source: input }
101
+ const result = pipeline(
102
+ fileInfo,
103
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
104
+ {}
105
+ ) as string | null
106
+
107
+ // Import stays on tapestry-react since Select was skipped
108
+ expect(result).toContain("@planningcenter/tapestry-react")
109
+ expect(result).not.toContain('@planningcenter/tapestry"')
110
+ // Size is NOT migrated
111
+ expect(result).toContain('size="sm"')
112
+ // Skip comment is present
113
+ expect(result).toContain("Automatic migration skipped")
114
+ })
115
+
116
+ it("should migrate non-multiple Selects while skipping multiple ones", () => {
117
+ const input = `
118
+ import { Select } from "@planningcenter/tapestry-react"
119
+
120
+ function Test() {
121
+ return (
122
+ <div>
123
+ <Select multiple label="Skip me" size="sm" />
124
+ <Select label="Migrate me" size="sm" />
125
+ </div>
126
+ )
127
+ }
128
+ `.trim()
129
+
130
+ const fileInfo = { path: "test.tsx", source: input }
131
+ const result = pipeline(
132
+ fileInfo,
133
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
134
+ {}
135
+ ) as string | null
136
+
137
+ expect(result).not.toBeNull()
138
+ // The skipped Select keeps size="sm"
139
+ expect(result).toContain('size="sm"')
140
+ // The migrated Select has size="md"
141
+ expect(result).toContain('size="md"')
142
+ // Import moves to tapestry (because at least one Select was migrated)
143
+ expect(result).toContain('@planningcenter/tapestry"')
144
+ // Skip comment is present
145
+ expect(result).toContain("Automatic migration skipped")
146
+ })
147
+ })
148
+ })
@@ -0,0 +1,23 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { addComment } from "../../shared/actions/addComment"
4
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
5
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
6
+
7
+ const transform: Transform = attributeTransformFactory({
8
+ condition: hasAttribute("multiple"),
9
+ targetComponent: "Select",
10
+ targetPackage: "@planningcenter/tapestry-react",
11
+ transform: (element, { j, source }) => {
12
+ addComment({
13
+ element,
14
+ j,
15
+ scope: "migration",
16
+ source,
17
+ text: "Automatic migration skipped: Select uses the multiple prop which has no equivalent in the new API. Migrate this Select manually.",
18
+ })
19
+ return true
20
+ },
21
+ })
22
+
23
+ export default transform
@@ -0,0 +1,217 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./stateToInvalid"
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("stateToInvalid transform", () => {
19
+ describe("state='error' conversion", () => {
20
+ it("should convert state='error' to invalid", () => {
21
+ const input = `
22
+ import { Select } from "@planningcenter/tapestry-react"
23
+
24
+ function Test() {
25
+ return <Select state="error" label="Name" />
26
+ }
27
+ `.trim()
28
+
29
+ const result = applyTransform(input)
30
+ expect(result).toContain("invalid")
31
+ expect(result).not.toContain('state="error"')
32
+ })
33
+
34
+ it('should convert state={"error"} expression to invalid', () => {
35
+ const input = `
36
+ import { Select } from "@planningcenter/tapestry-react"
37
+
38
+ function Test() {
39
+ return <Select state={"error"} label="Name" />
40
+ }
41
+ `.trim()
42
+
43
+ const result = applyTransform(input)
44
+ expect(result).toContain("invalid")
45
+ expect(result).not.toContain("state=")
46
+ })
47
+
48
+ it("should preserve other props when converting state='error'", () => {
49
+ const input = `
50
+ import { Select } from "@planningcenter/tapestry-react"
51
+
52
+ function Test() {
53
+ return (
54
+ <Select
55
+ state="error"
56
+ label="Name"
57
+ emptyValue="Pick"
58
+ onChange={handleChange}
59
+ />
60
+ )
61
+ }
62
+ `.trim()
63
+
64
+ const result = applyTransform(input)
65
+ expect(result).toContain("invalid")
66
+ expect(result).not.toContain("state=")
67
+ expect(result).toContain('label="Name"')
68
+ expect(result).toContain('emptyValue="Pick"')
69
+ expect(result).toContain("onChange={handleChange}")
70
+ })
71
+ })
72
+
73
+ describe("state='success' conversion", () => {
74
+ it("should remove state='success' and add comment", () => {
75
+ const input = `
76
+ import { Select } from "@planningcenter/tapestry-react"
77
+
78
+ function Test() {
79
+ return <Select state="success" label="Name" />
80
+ }
81
+ `.trim()
82
+
83
+ const result = applyTransform(input)
84
+ expect(result).not.toContain('<Select state="success"')
85
+ expect(result).toContain("TODO: tapestry-migration (state)")
86
+ expect(result).toContain("no equivalent")
87
+ })
88
+
89
+ it('should remove state={"success"} and add comment', () => {
90
+ const input = `
91
+ import { Select } from "@planningcenter/tapestry-react"
92
+
93
+ function Test() {
94
+ return <Select state={"success"} label="Name" />
95
+ }
96
+ `.trim()
97
+
98
+ const result = applyTransform(input)
99
+ expect(result).not.toContain("state={")
100
+ expect(result).toContain("TODO: tapestry-migration (state)")
101
+ })
102
+ })
103
+
104
+ describe("dynamic state value", () => {
105
+ it("should add comment for variable state value", () => {
106
+ const input = `
107
+ import { Select } from "@planningcenter/tapestry-react"
108
+
109
+ function Test() {
110
+ const selectState = isError ? "error" : undefined
111
+ return <Select state={selectState} label="Name" />
112
+ }
113
+ `.trim()
114
+
115
+ const result = applyTransform(input)
116
+ expect(result).toContain("TODO: tapestry-migration (state)")
117
+ expect(result).toContain("'state' has been replaced by 'invalid'")
118
+ expect(result).toContain("state={selectState}")
119
+ })
120
+
121
+ it("should add comment for string variable", () => {
122
+ const input = `
123
+ import { Select } from "@planningcenter/tapestry-react"
124
+
125
+ function Test() {
126
+ return <Select state={currentState} label="Name" />
127
+ }
128
+ `.trim()
129
+
130
+ const result = applyTransform(input)
131
+ expect(result).toContain("TODO: tapestry-migration (state)")
132
+ })
133
+ })
134
+
135
+ describe("edge cases", () => {
136
+ it("should not affect Select without state prop", () => {
137
+ const input = `
138
+ import { Select } from "@planningcenter/tapestry-react"
139
+
140
+ function Test() {
141
+ return <Select label="Name" />
142
+ }
143
+ `.trim()
144
+
145
+ const result = applyTransform(input)
146
+ expect(result).toBe(input)
147
+ })
148
+
149
+ it("should not affect other components", () => {
150
+ const input = `
151
+ import { Select, Button } from "@planningcenter/tapestry-react"
152
+
153
+ function Test() {
154
+ return (
155
+ <div>
156
+ <Button state="error">Error</Button>
157
+ <Select state="error" label="Name" />
158
+ </div>
159
+ )
160
+ }
161
+ `.trim()
162
+
163
+ const result = applyTransform(input)
164
+ expect(result).toContain('<Button state="error">Error</Button>')
165
+ expect(result).not.toContain('<Select state="error"')
166
+ expect(result).toContain("invalid")
167
+ })
168
+
169
+ it("should not transform if not imported from @planningcenter/tapestry-react", () => {
170
+ const input = `
171
+ import { Select } from "other-library"
172
+
173
+ function Test() {
174
+ return <Select state="error" label="Name" />
175
+ }
176
+ `.trim()
177
+
178
+ const result = applyTransform(input)
179
+ expect(result).toBe(input)
180
+ })
181
+
182
+ it("should not transform Select with multiple attribute", () => {
183
+ const input = `
184
+ import { Select } from "@planningcenter/tapestry-react"
185
+
186
+ function Test() {
187
+ return <Select multiple state="error" label="Name" />
188
+ }
189
+ `.trim()
190
+
191
+ const result = applyTransform(input)
192
+ expect(result).toBe(input)
193
+ })
194
+
195
+ it("should handle multiple selects with different states", () => {
196
+ const input = `
197
+ import { Select } from "@planningcenter/tapestry-react"
198
+
199
+ function Test() {
200
+ return (
201
+ <div>
202
+ <Select state="error" label="Error field" />
203
+ <Select state="success" label="Success field" />
204
+ <Select label="Normal field" />
205
+ </div>
206
+ )
207
+ }
208
+ `.trim()
209
+
210
+ const result = applyTransform(input)
211
+ expect(result).toContain("invalid")
212
+ expect(result).toContain("TODO: tapestry-migration (state)")
213
+ expect(result).not.toContain('<Select state="error"')
214
+ expect(result).not.toContain('<Select state="success"')
215
+ })
216
+ })
217
+ })
@@ -0,0 +1,59 @@
1
+ import { 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 { getAttributeValue } from "../../shared/actions/getAttributeValue"
7
+ import { removeAttribute } from "../../shared/actions/removeAttribute"
8
+ import { andConditions } from "../../shared/conditions/andConditions"
9
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
10
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
11
+ import { transformableSelect } from "../transformableSelect"
12
+
13
+ const transform: Transform = attributeTransformFactory({
14
+ condition: andConditions(hasAttribute("state"), transformableSelect),
15
+ targetComponent: "Select",
16
+ targetPackage: "@planningcenter/tapestry-react",
17
+ transform: (element, { j, source }) => {
18
+ const attr = getAttribute({ element, name: "state" })
19
+ if (!attr) return false
20
+
21
+ const value = getAttributeValue({ attribute: attr, j })
22
+
23
+ if (value === "error") {
24
+ removeAttribute("state", { element, j, source })
25
+ addAttribute({
26
+ booleanAsShorthand: true,
27
+ element,
28
+ j,
29
+ name: "invalid",
30
+ value: true,
31
+ })
32
+ return true
33
+ }
34
+
35
+ if (value === "success") {
36
+ removeAttribute("state", { element, j, source })
37
+ addComment({
38
+ element,
39
+ j,
40
+ scope: "state",
41
+ source,
42
+ text: "'state=\"success\"' has no equivalent in the new Select API. Remove this prop and implement success feedback separately.",
43
+ })
44
+ return true
45
+ }
46
+
47
+ // Dynamic value — can't determine at compile time
48
+ addComment({
49
+ element,
50
+ j,
51
+ scope: "state",
52
+ source,
53
+ text: "'state' has been replaced by 'invalid' (boolean). For error state, use invalid={true}. 'success' has no equivalent. Migrate this prop manually.",
54
+ })
55
+ return true
56
+ },
57
+ })
58
+
59
+ export default transform
@@ -0,0 +1,146 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./stateToInvalidTernary"
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("stateToInvalidTernary transform", () => {
18
+ describe("successful conversions", () => {
19
+ it("converts state={hasError ? 'error' : null} → invalid={hasError}", () => {
20
+ const input = `
21
+ import { Select } from "@planningcenter/tapestry-react"
22
+
23
+ function Test() {
24
+ return <Select state={hasError ? "error" : null} label="Name" />
25
+ }
26
+ `.trim()
27
+
28
+ const result = applyTransform(input)
29
+ expect(result).not.toBeNull()
30
+ expect(result).toContain("invalid={hasError}")
31
+ expect(result).not.toContain("state=")
32
+ })
33
+
34
+ it("converts member expression: state={form.hasError ? 'error' : null} → invalid={form.hasError}", () => {
35
+ const input = `
36
+ import { Select } from "@planningcenter/tapestry-react"
37
+
38
+ function Test() {
39
+ return <Select state={form.hasError ? "error" : null} label="Name" />
40
+ }
41
+ `.trim()
42
+
43
+ const result = applyTransform(input)
44
+ expect(result).not.toBeNull()
45
+ expect(result).toContain("invalid={form.hasError}")
46
+ expect(result).not.toContain("state=")
47
+ })
48
+
49
+ it("converts binary expression: state={errors.length > 0 ? 'error' : null} → invalid={errors.length > 0}", () => {
50
+ const input = `
51
+ import { Select } from "@planningcenter/tapestry-react"
52
+
53
+ function Test() {
54
+ return <Select state={errors.length > 0 ? "error" : null} label="Name" />
55
+ }
56
+ `.trim()
57
+
58
+ const result = applyTransform(input)
59
+ expect(result).not.toBeNull()
60
+ expect(result).toContain("invalid={errors.length > 0}")
61
+ expect(result).not.toContain("state=")
62
+ })
63
+
64
+ it("converts state={hasError ? 'error' : undefined} → invalid={hasError}", () => {
65
+ const input = `
66
+ import { Select } from "@planningcenter/tapestry-react"
67
+
68
+ function Test() {
69
+ return <Select state={hasError ? "error" : undefined} label="Name" />
70
+ }
71
+ `.trim()
72
+
73
+ const result = applyTransform(input)
74
+ expect(result).not.toBeNull()
75
+ expect(result).toContain("invalid={hasError}")
76
+ expect(result).not.toContain("state=")
77
+ })
78
+ })
79
+
80
+ describe("no-op cases", () => {
81
+ it("returns null when consequent is not 'error'", () => {
82
+ const input = `
83
+ import { Select } from "@planningcenter/tapestry-react"
84
+
85
+ function Test() {
86
+ return <Select state={hasWarning ? "warning" : null} label="Name" />
87
+ }
88
+ `.trim()
89
+
90
+ const result = applyTransform(input)
91
+ expect(result).toBeNull()
92
+ })
93
+
94
+ it("returns null when alternate is not null/undefined", () => {
95
+ const input = `
96
+ import { Select } from "@planningcenter/tapestry-react"
97
+
98
+ function Test() {
99
+ return <Select state={hasError ? "error" : "success"} label="Name" />
100
+ }
101
+ `.trim()
102
+
103
+ const result = applyTransform(input)
104
+ expect(result).toBeNull()
105
+ })
106
+
107
+ it("returns null when Select is from a different package", () => {
108
+ const input = `
109
+ import { Select } from "other-library"
110
+
111
+ function Test() {
112
+ return <Select state={hasError ? "error" : null} label="Name" />
113
+ }
114
+ `.trim()
115
+
116
+ const result = applyTransform(input)
117
+ expect(result).toBeNull()
118
+ })
119
+
120
+ it("returns null when Select has multiple attribute", () => {
121
+ const input = `
122
+ import { Select } from "@planningcenter/tapestry-react"
123
+
124
+ function Test() {
125
+ return <Select multiple state={hasError ? "error" : null} label="Name" />
126
+ }
127
+ `.trim()
128
+
129
+ const result = applyTransform(input)
130
+ expect(result).toBeNull()
131
+ })
132
+
133
+ it("returns null when Select has no state prop", () => {
134
+ const input = `
135
+ import { Select } from "@planningcenter/tapestry-react"
136
+
137
+ function Test() {
138
+ return <Select label="Name" />
139
+ }
140
+ `.trim()
141
+
142
+ const result = applyTransform(input)
143
+ expect(result).toBeNull()
144
+ })
145
+ })
146
+ })
@@ -0,0 +1,13 @@
1
+ import { andConditions } from "../../shared/conditions/andConditions"
2
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
3
+ import { ternaryConditionalToPropFactory } from "../../shared/transformFactories/ternaryConditionalToPropFactory"
4
+ import { transformableSelect } from "../transformableSelect"
5
+
6
+ export default ternaryConditionalToPropFactory({
7
+ condition: andConditions(hasAttribute("state"), transformableSelect),
8
+ fromProp: "state",
9
+ matchValue: "error",
10
+ targetComponent: "Select",
11
+ targetPackage: "@planningcenter/tapestry-react",
12
+ toProp: "invalid",
13
+ })