@planningcenter/tapestry-migration-cli 3.1.0-rc.9 → 3.1.0
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/input/transformableInput.ts +47 -6
- package/src/components/input/transforms/mergeFieldIntoInput.test.ts +78 -0
- package/src/components/input/transforms/mergeFieldIntoInput.ts +6 -212
- package/src/components/input/transforms/removeDuplicateKeys.test.ts +3 -3
- package/src/components/input/transforms/removeTypeInput.ts +3 -3
- package/src/components/input/transforms/removeTypeText.ts +2 -3
- package/src/components/input/transforms/unsupportedProps.test.ts +20 -20
- 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 +19 -2
- package/src/components/shared/transformFactories/mergeFieldFactory.ts +244 -0
- package/src/components/text-area/transforms/mergeFieldIntoTextArea.ts +4 -226
- package/src/index.ts +2 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./unsupportedProps"
|
|
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("unsupportedProps transform", () => {
|
|
19
|
+
describe("unsupported prop detection", () => {
|
|
20
|
+
it("should add comment for css prop with specific message", () => {
|
|
21
|
+
const input = `
|
|
22
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
23
|
+
|
|
24
|
+
function Test() {
|
|
25
|
+
return <Select css={{ color: 'red' }} emptyValue="Pick" />
|
|
26
|
+
}
|
|
27
|
+
`.trim()
|
|
28
|
+
|
|
29
|
+
const result = applyTransform(input)
|
|
30
|
+
expect(result).toContain("TODO: tapestry-migration (css)")
|
|
31
|
+
expect(result).toContain(
|
|
32
|
+
"CSS prop is not supported. Use className or style prop instead."
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("should add comment for tooltip prop with specific message", () => {
|
|
37
|
+
const input = `
|
|
38
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
39
|
+
|
|
40
|
+
function Test() {
|
|
41
|
+
return <Select tooltip={{ content: "Help" }} emptyValue="Pick" />
|
|
42
|
+
}
|
|
43
|
+
`.trim()
|
|
44
|
+
|
|
45
|
+
const result = applyTransform(input)
|
|
46
|
+
expect(result).toContain("TODO: tapestry-migration (tooltip)")
|
|
47
|
+
expect(result).toContain("unsupported anti-pattern")
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("should skip Select with multiple prop (handled by skipMultipleSelect)", () => {
|
|
51
|
+
const input = `
|
|
52
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
53
|
+
|
|
54
|
+
function Test() {
|
|
55
|
+
return <Select multiple emptyValue="Pick" />
|
|
56
|
+
}
|
|
57
|
+
`.trim()
|
|
58
|
+
|
|
59
|
+
const result = applyTransform(input)
|
|
60
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
61
|
+
expect(result).toContain("multiple")
|
|
62
|
+
expect(result).toContain('emptyValue="Pick"')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("should add comment for renderValue prop", () => {
|
|
66
|
+
const input = `
|
|
67
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
68
|
+
|
|
69
|
+
function Test() {
|
|
70
|
+
return <Select renderValue={(opts) => opts.map(o => o.label)} emptyValue="Pick" />
|
|
71
|
+
}
|
|
72
|
+
`.trim()
|
|
73
|
+
|
|
74
|
+
const result = applyTransform(input)
|
|
75
|
+
expect(result).toContain("TODO: tapestry-migration (renderValue)")
|
|
76
|
+
expect(result).toContain("renderValue is not supported")
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("should add comments for removed props", () => {
|
|
80
|
+
const input = `
|
|
81
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
82
|
+
|
|
83
|
+
function Test() {
|
|
84
|
+
return (
|
|
85
|
+
<Select
|
|
86
|
+
keepInView
|
|
87
|
+
lockScrollWhileOpen
|
|
88
|
+
popoverProps={{ maxHeight: 300 }}
|
|
89
|
+
renderTo="body"
|
|
90
|
+
placeholder="Pick"
|
|
91
|
+
options={[]}
|
|
92
|
+
label="Test"
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
`.trim()
|
|
97
|
+
|
|
98
|
+
const result = applyTransform(input)
|
|
99
|
+
expect(result).toContain("TODO: tapestry-migration (keepInView)")
|
|
100
|
+
expect(result).toContain("TODO: tapestry-migration (lockScrollWhileOpen)")
|
|
101
|
+
expect(result).toContain("TODO: tapestry-migration (popoverProps)")
|
|
102
|
+
expect(result).toContain("TODO: tapestry-migration (renderTo)")
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe("supported props", () => {
|
|
107
|
+
it("should not add comments for supported select props", () => {
|
|
108
|
+
const input = `
|
|
109
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
110
|
+
|
|
111
|
+
function Test() {
|
|
112
|
+
return (
|
|
113
|
+
<Select
|
|
114
|
+
disabled
|
|
115
|
+
name="select"
|
|
116
|
+
onChange={() => {}}
|
|
117
|
+
value="test"
|
|
118
|
+
placeholder="Pick one"
|
|
119
|
+
options={options}
|
|
120
|
+
/>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
`.trim()
|
|
124
|
+
|
|
125
|
+
const result = applyTransform(input)
|
|
126
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("should not add comments for common props", () => {
|
|
130
|
+
const input = `
|
|
131
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
132
|
+
|
|
133
|
+
function Test() {
|
|
134
|
+
return (
|
|
135
|
+
<Select
|
|
136
|
+
className="test"
|
|
137
|
+
id="select"
|
|
138
|
+
style={{ color: 'red' }}
|
|
139
|
+
tabIndex={0}
|
|
140
|
+
label="Test"
|
|
141
|
+
placeholder="Pick"
|
|
142
|
+
options={[]}
|
|
143
|
+
/>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
`.trim()
|
|
147
|
+
|
|
148
|
+
const result = applyTransform(input)
|
|
149
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it("should not add comments for aria props", () => {
|
|
153
|
+
const input = `
|
|
154
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
155
|
+
|
|
156
|
+
function Test() {
|
|
157
|
+
return (
|
|
158
|
+
<Select
|
|
159
|
+
aria-label="Test select"
|
|
160
|
+
aria-describedby="description"
|
|
161
|
+
placeholder="Pick"
|
|
162
|
+
options={[]}
|
|
163
|
+
label="Test"
|
|
164
|
+
/>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
`.trim()
|
|
168
|
+
|
|
169
|
+
const result = applyTransform(input)
|
|
170
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it("should not add comments for data props", () => {
|
|
174
|
+
const input = `
|
|
175
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
176
|
+
|
|
177
|
+
function Test() {
|
|
178
|
+
return (
|
|
179
|
+
<Select
|
|
180
|
+
data-testid="select"
|
|
181
|
+
data-cy="test-select"
|
|
182
|
+
placeholder="Pick"
|
|
183
|
+
options={[]}
|
|
184
|
+
label="Test"
|
|
185
|
+
/>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
`.trim()
|
|
189
|
+
|
|
190
|
+
const result = applyTransform(input)
|
|
191
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe("edge cases", () => {
|
|
196
|
+
it("should not affect Select without unsupported props", () => {
|
|
197
|
+
const input = `
|
|
198
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
199
|
+
|
|
200
|
+
function Test() {
|
|
201
|
+
return <Select label="Test" placeholder="Pick" options={[]} />
|
|
202
|
+
}
|
|
203
|
+
`.trim()
|
|
204
|
+
|
|
205
|
+
const result = applyTransform(input)
|
|
206
|
+
expect(result).toBe(input)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it("should not affect other components", () => {
|
|
210
|
+
const input = `
|
|
211
|
+
import { Button, Select } from "@planningcenter/tapestry-react"
|
|
212
|
+
|
|
213
|
+
function Test() {
|
|
214
|
+
return (
|
|
215
|
+
<div>
|
|
216
|
+
<Button css={{ color: 'red' }}>Click me</Button>
|
|
217
|
+
<Select label="Test" placeholder="Pick" options={[]} />
|
|
218
|
+
</div>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
`.trim()
|
|
222
|
+
|
|
223
|
+
const result = applyTransform(input)
|
|
224
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it("should handle mixed supported and unsupported props", () => {
|
|
228
|
+
const input = `
|
|
229
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
230
|
+
|
|
231
|
+
function Test() {
|
|
232
|
+
return (
|
|
233
|
+
<Select
|
|
234
|
+
disabled
|
|
235
|
+
css={{ color: 'red' }}
|
|
236
|
+
tooltip={{ content: "help" }}
|
|
237
|
+
label="Test"
|
|
238
|
+
placeholder="Pick"
|
|
239
|
+
options={[]}
|
|
240
|
+
/>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
`.trim()
|
|
244
|
+
|
|
245
|
+
const result = applyTransform(input)
|
|
246
|
+
expect(result).toContain("TODO: tapestry-migration (css)")
|
|
247
|
+
expect(result).toContain("TODO: tapestry-migration (tooltip)")
|
|
248
|
+
expect(result).toContain("disabled")
|
|
249
|
+
expect(result).toContain('label="Test"')
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { JSXAttribute, Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addCommentToUnsupportedProps } from "../../shared/actions/addCommentToUnsupportedProps"
|
|
4
|
+
import { SELECT_SUPPORTED_PROPS } from "../../shared/helpers/unsupportedPropsHelpers"
|
|
5
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
6
|
+
import { transformableSelect } from "../transformableSelect"
|
|
7
|
+
|
|
8
|
+
const transform: Transform = attributeTransformFactory({
|
|
9
|
+
condition: transformableSelect,
|
|
10
|
+
targetComponent: "Select",
|
|
11
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
12
|
+
transform: (element, { j }) => {
|
|
13
|
+
const UNSUPPORTED_PROPS = (element.openingElement.attributes || [])
|
|
14
|
+
.filter(
|
|
15
|
+
(attr) =>
|
|
16
|
+
attr.type === "JSXAttribute" &&
|
|
17
|
+
attr.name.type === "JSXIdentifier" &&
|
|
18
|
+
!SELECT_SUPPORTED_PROPS.includes(attr.name.name as string) &&
|
|
19
|
+
!(attr.name.name as string).startsWith("aria-") &&
|
|
20
|
+
!(attr.name.name as string).startsWith("data-")
|
|
21
|
+
)
|
|
22
|
+
.map((attr) => (attr as JSXAttribute).name.name as string)
|
|
23
|
+
|
|
24
|
+
return addCommentToUnsupportedProps({
|
|
25
|
+
element,
|
|
26
|
+
j,
|
|
27
|
+
messageSuffix: (prop) => {
|
|
28
|
+
if (prop === "css") {
|
|
29
|
+
return "\n * CSS prop is not supported. Use className or style prop instead.\n"
|
|
30
|
+
}
|
|
31
|
+
if (prop === "tooltip") {
|
|
32
|
+
return "\n * Wrapping a select in a tooltip is an unsupported anti-pattern.\n"
|
|
33
|
+
}
|
|
34
|
+
if (prop === "renderValue") {
|
|
35
|
+
return "\n * renderValue is not supported. The select displays the selected option label.\n"
|
|
36
|
+
}
|
|
37
|
+
return ""
|
|
38
|
+
},
|
|
39
|
+
props: UNSUPPORTED_PROPS,
|
|
40
|
+
})
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export default transform
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Expression, JSXElement } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gets the expression AST node for a JSX attribute value.
|
|
5
|
+
* Returns the expression node for dynamic values, or a StringLiteral for static ones.
|
|
6
|
+
*/
|
|
7
|
+
export function getAttributeExpression(
|
|
8
|
+
element: JSXElement,
|
|
9
|
+
attrName: string
|
|
10
|
+
): Expression | null {
|
|
11
|
+
const attrs = element.openingElement.attributes || []
|
|
12
|
+
for (const attr of attrs) {
|
|
13
|
+
if (attr.type !== "JSXAttribute") continue
|
|
14
|
+
if (attr.name.name !== attrName) continue
|
|
15
|
+
|
|
16
|
+
if (!attr.value) return null // boolean shorthand has no expression
|
|
17
|
+
if (attr.value.type === "StringLiteral") return attr.value
|
|
18
|
+
if (attr.value.type === "JSXExpressionContainer") {
|
|
19
|
+
const expr = attr.value.expression
|
|
20
|
+
if (expr.type === "JSXEmptyExpression") return null
|
|
21
|
+
return expr as Expression
|
|
22
|
+
}
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
@@ -96,9 +96,26 @@ export const TYPE_SPECIFIC_PROPS: Record<string, string[]> = {
|
|
|
96
96
|
url: ["dirname", "inputMode", "list", "pattern"],
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
export const INPUT_SUPPORTED_PROPS = [
|
|
99
|
+
export const INPUT_SUPPORTED_PROPS = [...COMMON_PROPS, ...INPUT_SPECIFIC_PROPS]
|
|
100
|
+
|
|
101
|
+
export const SELECT_SPECIFIC_PROPS = [
|
|
102
|
+
"complex",
|
|
103
|
+
"defaultValue",
|
|
104
|
+
"description",
|
|
105
|
+
"disabled",
|
|
106
|
+
"form",
|
|
107
|
+
"invalid",
|
|
108
|
+
"name",
|
|
109
|
+
"onChange",
|
|
110
|
+
"options",
|
|
111
|
+
"placeholder",
|
|
112
|
+
"required",
|
|
113
|
+
"value",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
export const SELECT_SUPPORTED_PROPS = [
|
|
100
117
|
...COMMON_PROPS,
|
|
101
|
-
...
|
|
118
|
+
...SELECT_SPECIFIC_PROPS,
|
|
102
119
|
...STYLE_PROP_NAMES_WITHOUT_CSS,
|
|
103
120
|
]
|
|
104
121
|
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { JSXElement, JSXText, Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addComment } from "../actions/addComment"
|
|
4
|
+
import { TransformCondition } from "../types"
|
|
5
|
+
import {
|
|
6
|
+
getImportName,
|
|
7
|
+
removeImportFromDeclaration,
|
|
8
|
+
} from "./helpers/manageImports"
|
|
9
|
+
|
|
10
|
+
interface MergeFieldFactoryOptions {
|
|
11
|
+
condition?: TransformCondition
|
|
12
|
+
targetComponent: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function mergeFieldFactory({
|
|
16
|
+
condition,
|
|
17
|
+
targetComponent,
|
|
18
|
+
}: MergeFieldFactoryOptions): Transform {
|
|
19
|
+
const SCOPE = `mergeFieldInto${targetComponent}`
|
|
20
|
+
|
|
21
|
+
const transform: Transform = (fileInfo, api) => {
|
|
22
|
+
const j = api.jscodeshift
|
|
23
|
+
const source = j(fileInfo.source)
|
|
24
|
+
|
|
25
|
+
const fieldLocalName = getImportName(
|
|
26
|
+
"Field",
|
|
27
|
+
"@planningcenter/tapestry-react",
|
|
28
|
+
{ j, source }
|
|
29
|
+
)
|
|
30
|
+
if (!fieldLocalName) return null
|
|
31
|
+
|
|
32
|
+
const targetLocalName = getImportName(
|
|
33
|
+
targetComponent,
|
|
34
|
+
"@planningcenter/tapestry-react",
|
|
35
|
+
{ j, source }
|
|
36
|
+
)
|
|
37
|
+
if (!targetLocalName) return null
|
|
38
|
+
|
|
39
|
+
let hasChanges = false
|
|
40
|
+
let anyFieldRemoved = false
|
|
41
|
+
|
|
42
|
+
source.find(j.JSXElement).forEach((path) => {
|
|
43
|
+
const el = path.value
|
|
44
|
+
const opening = el.openingElement
|
|
45
|
+
|
|
46
|
+
if (opening.name.type !== "JSXIdentifier") return
|
|
47
|
+
if (opening.name.name !== fieldLocalName) return
|
|
48
|
+
|
|
49
|
+
const elementChildren = (el.children || []).filter(
|
|
50
|
+
(child) =>
|
|
51
|
+
child.type !== "JSXText" || (child as JSXText).value.trim() !== ""
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const targetChildren = elementChildren.filter((child) => {
|
|
55
|
+
if (child.type !== "JSXElement") return false
|
|
56
|
+
const childOpening = (child as JSXElement).openingElement
|
|
57
|
+
return (
|
|
58
|
+
childOpening.name.type === "JSXIdentifier" &&
|
|
59
|
+
childOpening.name.name === targetLocalName
|
|
60
|
+
)
|
|
61
|
+
}) as JSXElement[]
|
|
62
|
+
|
|
63
|
+
// Case: exactly 1 child and it is the target — merge props and unwrap
|
|
64
|
+
if (elementChildren.length === 1 && targetChildren.length === 1) {
|
|
65
|
+
const targetEl = targetChildren[0]
|
|
66
|
+
|
|
67
|
+
// Skip if condition fails (e.g. Input with renderLeft/renderRight, Select with multiple)
|
|
68
|
+
if (condition && !condition(targetEl)) return
|
|
69
|
+
|
|
70
|
+
const fieldAttrs = opening.attributes || []
|
|
71
|
+
|
|
72
|
+
// Bail out if Field has spread props — we can't know what they contain
|
|
73
|
+
const hasFieldSpreads = fieldAttrs.some(
|
|
74
|
+
(attr) => attr.type === "JSXSpreadAttribute"
|
|
75
|
+
)
|
|
76
|
+
if (hasFieldSpreads) {
|
|
77
|
+
addComment({
|
|
78
|
+
element: targetEl,
|
|
79
|
+
j,
|
|
80
|
+
scope: SCOPE,
|
|
81
|
+
source,
|
|
82
|
+
text: `Field has spread props that cannot be auto-merged into ${targetComponent}. Please migrate manually.`,
|
|
83
|
+
})
|
|
84
|
+
hasChanges = true
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const attr of fieldAttrs) {
|
|
89
|
+
if (attr.type !== "JSXAttribute") continue
|
|
90
|
+
if (attr.name.type !== "JSXIdentifier") continue
|
|
91
|
+
|
|
92
|
+
const attrName = attr.name.name
|
|
93
|
+
const targetAttrs = targetEl.openingElement.attributes || []
|
|
94
|
+
|
|
95
|
+
if (attrName === "label") {
|
|
96
|
+
const hasLabel = targetAttrs.some(
|
|
97
|
+
(a) =>
|
|
98
|
+
a.type === "JSXAttribute" &&
|
|
99
|
+
a.name?.type === "JSXIdentifier" &&
|
|
100
|
+
a.name.name === "label"
|
|
101
|
+
)
|
|
102
|
+
if (hasLabel) {
|
|
103
|
+
addComment({
|
|
104
|
+
element: targetEl,
|
|
105
|
+
j,
|
|
106
|
+
scope: SCOPE,
|
|
107
|
+
source,
|
|
108
|
+
text: `Field had label prop but ${targetComponent} already has label. Please migrate manually.`,
|
|
109
|
+
})
|
|
110
|
+
} else {
|
|
111
|
+
targetEl.openingElement.attributes.push(attr)
|
|
112
|
+
}
|
|
113
|
+
} else if (attrName === "feedbackText") {
|
|
114
|
+
const hasDescription = targetAttrs.some(
|
|
115
|
+
(a) =>
|
|
116
|
+
a.type === "JSXAttribute" &&
|
|
117
|
+
a.name?.type === "JSXIdentifier" &&
|
|
118
|
+
a.name.name === "description"
|
|
119
|
+
)
|
|
120
|
+
if (hasDescription) {
|
|
121
|
+
addComment({
|
|
122
|
+
element: targetEl,
|
|
123
|
+
j,
|
|
124
|
+
scope: SCOPE,
|
|
125
|
+
source,
|
|
126
|
+
text: `Field had feedbackText prop but ${targetComponent} already has description. Please migrate manually.`,
|
|
127
|
+
})
|
|
128
|
+
} else {
|
|
129
|
+
const newAttr = j.jsxAttribute(
|
|
130
|
+
j.jsxIdentifier("description"),
|
|
131
|
+
attr.value
|
|
132
|
+
)
|
|
133
|
+
targetEl.openingElement.attributes.push(newAttr)
|
|
134
|
+
}
|
|
135
|
+
} else if (attrName === "state") {
|
|
136
|
+
const hasState = targetAttrs.some(
|
|
137
|
+
(a) =>
|
|
138
|
+
a.type === "JSXAttribute" &&
|
|
139
|
+
a.name?.type === "JSXIdentifier" &&
|
|
140
|
+
a.name.name === "state"
|
|
141
|
+
)
|
|
142
|
+
if (hasState) {
|
|
143
|
+
addComment({
|
|
144
|
+
element: targetEl,
|
|
145
|
+
j,
|
|
146
|
+
scope: SCOPE,
|
|
147
|
+
source,
|
|
148
|
+
text: `Field had state prop but ${targetComponent} already has state. Please migrate manually.`,
|
|
149
|
+
})
|
|
150
|
+
} else {
|
|
151
|
+
targetEl.openingElement.attributes.push(attr)
|
|
152
|
+
}
|
|
153
|
+
} else if (attrName === "key") {
|
|
154
|
+
const hasKey = targetAttrs.some(
|
|
155
|
+
(a) =>
|
|
156
|
+
a.type === "JSXAttribute" &&
|
|
157
|
+
a.name?.type === "JSXIdentifier" &&
|
|
158
|
+
a.name.name === "key"
|
|
159
|
+
)
|
|
160
|
+
if (!hasKey) {
|
|
161
|
+
targetEl.openingElement.attributes.push(attr)
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
// Unsupported prop — add comment to target
|
|
165
|
+
addComment({
|
|
166
|
+
element: targetEl,
|
|
167
|
+
j,
|
|
168
|
+
scope: SCOPE,
|
|
169
|
+
source,
|
|
170
|
+
text: `Field prop '${attrName}' is not supported by ${targetComponent}. Please migrate manually.`,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const parent = path.parent?.value
|
|
176
|
+
if (parent?.children) {
|
|
177
|
+
const idx = parent.children.indexOf(el)
|
|
178
|
+
if (idx === -1) return
|
|
179
|
+
parent.children.splice(idx, 1, ...(el.children || []))
|
|
180
|
+
} else {
|
|
181
|
+
// Root JSX (e.g. directly inside return parens) — use path.replace
|
|
182
|
+
const nonWsChildren = (el.children || []).filter(
|
|
183
|
+
(child) =>
|
|
184
|
+
child.type !== "JSXText" || (child as JSXText).value.trim() !== ""
|
|
185
|
+
)
|
|
186
|
+
if (nonWsChildren.length === 1) {
|
|
187
|
+
path.replace(nonWsChildren[0])
|
|
188
|
+
} else {
|
|
189
|
+
path.replace(
|
|
190
|
+
j.jsxFragment(
|
|
191
|
+
j.jsxOpeningFragment(),
|
|
192
|
+
j.jsxClosingFragment(),
|
|
193
|
+
el.children || []
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
hasChanges = true
|
|
199
|
+
anyFieldRemoved = true
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Case: more than 1 non-whitespace child — comment each target child, leave Field
|
|
204
|
+
if (elementChildren.length > 1) {
|
|
205
|
+
for (const child of targetChildren) {
|
|
206
|
+
// Skip if condition fails
|
|
207
|
+
if (condition && !condition(child)) continue
|
|
208
|
+
addComment({
|
|
209
|
+
element: child,
|
|
210
|
+
j,
|
|
211
|
+
scope: SCOPE,
|
|
212
|
+
source,
|
|
213
|
+
text: `Field has multiple children and cannot be auto-merged into ${targetComponent}. Please migrate manually.`,
|
|
214
|
+
})
|
|
215
|
+
hasChanges = true
|
|
216
|
+
}
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Case: exactly 1 child but not the target — skip without comment
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// Remove Field from imports only if all Field usages were converted
|
|
224
|
+
if (anyFieldRemoved) {
|
|
225
|
+
const stillUsesField =
|
|
226
|
+
source.find(j.JSXOpeningElement, {
|
|
227
|
+
name: { name: fieldLocalName },
|
|
228
|
+
}).length > 0
|
|
229
|
+
|
|
230
|
+
if (!stillUsesField) {
|
|
231
|
+
const fieldImports = source.find(j.ImportDeclaration, {
|
|
232
|
+
source: { value: "@planningcenter/tapestry-react" },
|
|
233
|
+
})
|
|
234
|
+
for (let i = 0; i < fieldImports.length; i++) {
|
|
235
|
+
removeImportFromDeclaration(fieldImports.at(i), "Field")
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return hasChanges ? source.toSource() : null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return transform
|
|
244
|
+
}
|