@planningcenter/tapestry-migration-cli 3.1.0-rc.8 → 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.test.ts +212 -0
- package/src/components/input/transforms/removeTypeInput.ts +22 -0
- 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 +52 -2
- package/src/components/shared/transformFactories/mergeFieldFactory.ts +244 -0
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +2 -1
- package/src/components/text-area/index.ts +48 -0
- package/src/components/text-area/transforms/auditSpreadProps.test.ts +139 -0
- package/src/components/text-area/transforms/auditSpreadProps.ts +10 -0
- package/src/components/text-area/transforms/convertStyleProps.test.ts +158 -0
- package/src/components/text-area/transforms/convertStyleProps.ts +10 -0
- package/src/components/text-area/transforms/innerRefToRef.test.ts +206 -0
- package/src/components/text-area/transforms/innerRefToRef.ts +14 -0
- package/src/components/text-area/transforms/mergeFieldIntoTextArea.test.ts +477 -0
- package/src/components/text-area/transforms/mergeFieldIntoTextArea.ts +5 -0
- package/src/components/text-area/transforms/moveTextAreaImport.test.ts +168 -0
- package/src/components/text-area/transforms/moveTextAreaImport.ts +13 -0
- package/src/components/text-area/transforms/removeDuplicateKeys.test.ts +129 -0
- package/src/components/text-area/transforms/removeDuplicateKeys.ts +8 -0
- package/src/components/text-area/transforms/removeRedundantAriaLabel.test.ts +183 -0
- package/src/components/text-area/transforms/removeRedundantAriaLabel.ts +59 -0
- package/src/components/text-area/transforms/sizeMapping.test.ts +199 -0
- package/src/components/text-area/transforms/sizeMapping.ts +15 -0
- package/src/components/text-area/transforms/stateToInvalid.test.ts +204 -0
- package/src/components/text-area/transforms/stateToInvalid.ts +57 -0
- package/src/components/text-area/transforms/stateToInvalidTernary.test.ts +133 -0
- package/src/components/text-area/transforms/stateToInvalidTernary.ts +11 -0
- package/src/components/text-area/transforms/unsupportedProps.test.ts +275 -0
- package/src/components/text-area/transforms/unsupportedProps.ts +35 -0
- package/src/index.ts +4 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JSCodeshift,
|
|
3
|
+
JSXElement,
|
|
4
|
+
JSXExpressionContainer,
|
|
5
|
+
JSXText,
|
|
6
|
+
Transform,
|
|
7
|
+
} from "jscodeshift"
|
|
8
|
+
|
|
9
|
+
import { addComment } from "../../shared/actions/addComment"
|
|
10
|
+
import { removeChildren } from "../../shared/actions/removeChildren"
|
|
11
|
+
import { andConditions } from "../../shared/conditions/andConditions"
|
|
12
|
+
import { hasChildren } from "../../shared/conditions/hasChildren"
|
|
13
|
+
import { getAttributeExpression } from "../../shared/helpers/getAttributeExpression"
|
|
14
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
15
|
+
import { getImportName } from "../../shared/transformFactories/helpers/manageImports"
|
|
16
|
+
import { transformableSelect } from "../transformableSelect"
|
|
17
|
+
|
|
18
|
+
type ChildNode = JSXElement["children"][number]
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Checks if a JSX element is a specific member expression like `Select.Option`
|
|
22
|
+
*/
|
|
23
|
+
function isMemberExpression(
|
|
24
|
+
node: ChildNode,
|
|
25
|
+
objectName: string,
|
|
26
|
+
propertyName: string
|
|
27
|
+
): node is JSXElement {
|
|
28
|
+
if (!node || node.type !== "JSXElement") return false
|
|
29
|
+
const opening = node.openingElement
|
|
30
|
+
return (
|
|
31
|
+
opening.name.type === "JSXMemberExpression" &&
|
|
32
|
+
opening.name.object.type === "JSXIdentifier" &&
|
|
33
|
+
opening.name.object.name === objectName &&
|
|
34
|
+
opening.name.property.name === propertyName
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extracts a string value from a JSX attribute.
|
|
40
|
+
* Returns null if the value is dynamic/complex.
|
|
41
|
+
*/
|
|
42
|
+
function getStaticAttributeValue(
|
|
43
|
+
element: JSXElement,
|
|
44
|
+
attrName: string
|
|
45
|
+
): string | null {
|
|
46
|
+
const attrs = element.openingElement.attributes || []
|
|
47
|
+
for (const attr of attrs) {
|
|
48
|
+
if (attr.type !== "JSXAttribute") continue
|
|
49
|
+
if (attr.name.name !== attrName) continue
|
|
50
|
+
|
|
51
|
+
if (!attr.value) return null // boolean shorthand
|
|
52
|
+
if (attr.value.type === "StringLiteral") return attr.value.value
|
|
53
|
+
if (attr.value.type === "JSXExpressionContainer") {
|
|
54
|
+
const expr = attr.value.expression
|
|
55
|
+
if (expr.type === "StringLiteral") return expr.value
|
|
56
|
+
}
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Checks if a JSX attribute is a boolean shorthand (e.g. `disabled`)
|
|
64
|
+
*/
|
|
65
|
+
function hasBooleanAttribute(element: JSXElement, attrName: string): boolean {
|
|
66
|
+
const attrs = element.openingElement.attributes || []
|
|
67
|
+
for (const attr of attrs) {
|
|
68
|
+
if (attr.type !== "JSXAttribute") continue
|
|
69
|
+
if (attr.name.name !== attrName) continue
|
|
70
|
+
// Boolean shorthand: <Option disabled />
|
|
71
|
+
if (!attr.value) return true
|
|
72
|
+
// Explicit true: <Option disabled={true} />
|
|
73
|
+
if (
|
|
74
|
+
attr.value.type === "JSXExpressionContainer" &&
|
|
75
|
+
attr.value.expression.type === "BooleanLiteral" &&
|
|
76
|
+
attr.value.expression.value === true
|
|
77
|
+
)
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Extracts plain text from JSX children.
|
|
85
|
+
* Returns null if children are complex (JSX expressions, elements, etc.)
|
|
86
|
+
*/
|
|
87
|
+
function extractSimpleTextChildren(children: ChildNode[]): string | null {
|
|
88
|
+
const meaningful = children.filter((child) => {
|
|
89
|
+
if (child.type === "JSXText") {
|
|
90
|
+
return child.value.trim().length > 0
|
|
91
|
+
}
|
|
92
|
+
return true
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
if (meaningful.length === 0) return null
|
|
96
|
+
|
|
97
|
+
// All meaningful children must be text
|
|
98
|
+
const allText = meaningful.every((child) => child.type === "JSXText")
|
|
99
|
+
if (!allText) return null
|
|
100
|
+
|
|
101
|
+
return meaningful.map((child) => (child as JSXText).value.trim()).join(" ")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Attempts to convert a Select.Option element into an options array entry AST node.
|
|
106
|
+
* Returns null if the option is too complex for automated conversion.
|
|
107
|
+
*/
|
|
108
|
+
function convertOptionToObjectExpression(
|
|
109
|
+
optionElement: JSXElement,
|
|
110
|
+
j: JSCodeshift
|
|
111
|
+
) {
|
|
112
|
+
const value = getStaticAttributeValue(optionElement, "value")
|
|
113
|
+
if (value === null) return null
|
|
114
|
+
|
|
115
|
+
const children = optionElement.children || []
|
|
116
|
+
const labelText = extractSimpleTextChildren(children)
|
|
117
|
+
if (labelText === null) return null
|
|
118
|
+
|
|
119
|
+
const properties = [
|
|
120
|
+
j.objectProperty(j.identifier("label"), j.stringLiteral(labelText)),
|
|
121
|
+
j.objectProperty(j.identifier("value"), j.stringLiteral(value)),
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
if (hasBooleanAttribute(optionElement, "disabled")) {
|
|
125
|
+
properties.push(
|
|
126
|
+
j.objectProperty(j.identifier("disabled"), j.booleanLiteral(true))
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Preserve data-* attributes on the option object
|
|
131
|
+
const attrs = optionElement.openingElement.attributes || []
|
|
132
|
+
for (const attr of attrs) {
|
|
133
|
+
if (attr.type !== "JSXAttribute") continue
|
|
134
|
+
const name = attr.name.name as string
|
|
135
|
+
if (!name.startsWith("data-")) continue
|
|
136
|
+
const expr = getAttributeExpression(optionElement, name)
|
|
137
|
+
if (expr) {
|
|
138
|
+
properties.push(j.objectProperty(j.stringLiteral(name), expr))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return j.objectExpression(properties)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Attempts to convert a Select.OptionGroup element into a group object AST node.
|
|
147
|
+
*/
|
|
148
|
+
function convertOptionGroupToObjectExpression(
|
|
149
|
+
groupElement: JSXElement,
|
|
150
|
+
localSelectName: string,
|
|
151
|
+
j: JSCodeshift
|
|
152
|
+
) {
|
|
153
|
+
const title = getStaticAttributeValue(groupElement, "title")
|
|
154
|
+
if (title === null) return null
|
|
155
|
+
|
|
156
|
+
const childOptions: ReturnType<typeof convertOptionToObjectExpression>[] = []
|
|
157
|
+
const children = groupElement.children || []
|
|
158
|
+
|
|
159
|
+
for (const child of children) {
|
|
160
|
+
if (child.type === "JSXText" && child.value.trim() === "") continue
|
|
161
|
+
if (!isMemberExpression(child, localSelectName, "Option")) return null
|
|
162
|
+
|
|
163
|
+
const optObj = convertOptionToObjectExpression(child, j)
|
|
164
|
+
if (optObj === null) return null
|
|
165
|
+
childOptions.push(optObj)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (childOptions.length === 0) return null
|
|
169
|
+
|
|
170
|
+
return j.objectExpression([
|
|
171
|
+
j.objectProperty(j.identifier("label"), j.stringLiteral(title)),
|
|
172
|
+
j.objectProperty(j.identifier("options"), j.arrayExpression(childOptions)),
|
|
173
|
+
])
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const transform: Transform = attributeTransformFactory({
|
|
177
|
+
condition: andConditions(transformableSelect, hasChildren),
|
|
178
|
+
targetComponent: "Select",
|
|
179
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
180
|
+
transform: (element, { j, source }) => {
|
|
181
|
+
const children = element.children || []
|
|
182
|
+
|
|
183
|
+
// Get the local name of Select for member expression matching
|
|
184
|
+
const localSelectName =
|
|
185
|
+
getImportName("Select", "@planningcenter/tapestry-react", {
|
|
186
|
+
j,
|
|
187
|
+
source,
|
|
188
|
+
}) || "Select"
|
|
189
|
+
|
|
190
|
+
// Check if any children are dynamic (.map expressions, ternaries, etc.)
|
|
191
|
+
const hasDynamicChildren = children.some(
|
|
192
|
+
(child: ChildNode) =>
|
|
193
|
+
child.type === "JSXExpressionContainer" &&
|
|
194
|
+
(child as JSXExpressionContainer).expression.type !==
|
|
195
|
+
"JSXEmptyExpression"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if (hasDynamicChildren) {
|
|
199
|
+
addComment({
|
|
200
|
+
element,
|
|
201
|
+
j,
|
|
202
|
+
scope: "children",
|
|
203
|
+
source,
|
|
204
|
+
text: 'Select children contain dynamic expressions. Manually convert to the options prop format: options={[{ value: "...", label: "..." }]}',
|
|
205
|
+
})
|
|
206
|
+
return true
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check for Select.Value or Select.Inline children
|
|
210
|
+
const hasUnsupportedSubcomponents = children.some(
|
|
211
|
+
(child: ChildNode) =>
|
|
212
|
+
isMemberExpression(child, localSelectName, "Value") ||
|
|
213
|
+
isMemberExpression(child, localSelectName, "Inline")
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if (hasUnsupportedSubcomponents) {
|
|
217
|
+
addComment({
|
|
218
|
+
element,
|
|
219
|
+
j,
|
|
220
|
+
scope: "children",
|
|
221
|
+
source,
|
|
222
|
+
text: "Select.Value and Select.Inline are not supported in the new Select. Manually convert children to the options prop format.",
|
|
223
|
+
})
|
|
224
|
+
return true
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Filter to meaningful children (skip whitespace-only JSXText)
|
|
228
|
+
const meaningfulChildren = children.filter((child: ChildNode) => {
|
|
229
|
+
if (child.type === "JSXText") return child.value.trim().length > 0
|
|
230
|
+
return true
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// Attempt to convert all children to option objects
|
|
234
|
+
const optionObjects: ReturnType<typeof convertOptionToObjectExpression>[] =
|
|
235
|
+
[]
|
|
236
|
+
let allConvertible = true
|
|
237
|
+
|
|
238
|
+
for (const child of meaningfulChildren) {
|
|
239
|
+
if (isMemberExpression(child, localSelectName, "Option")) {
|
|
240
|
+
const obj = convertOptionToObjectExpression(child, j)
|
|
241
|
+
if (obj === null) {
|
|
242
|
+
allConvertible = false
|
|
243
|
+
break
|
|
244
|
+
}
|
|
245
|
+
optionObjects.push(obj)
|
|
246
|
+
} else if (isMemberExpression(child, localSelectName, "OptionGroup")) {
|
|
247
|
+
const obj = convertOptionGroupToObjectExpression(
|
|
248
|
+
child,
|
|
249
|
+
localSelectName,
|
|
250
|
+
j
|
|
251
|
+
)
|
|
252
|
+
if (obj === null) {
|
|
253
|
+
allConvertible = false
|
|
254
|
+
break
|
|
255
|
+
}
|
|
256
|
+
optionObjects.push(obj)
|
|
257
|
+
} else {
|
|
258
|
+
// Unknown child type
|
|
259
|
+
allConvertible = false
|
|
260
|
+
break
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!allConvertible || optionObjects.length === 0) {
|
|
265
|
+
addComment({
|
|
266
|
+
element,
|
|
267
|
+
j,
|
|
268
|
+
scope: "children",
|
|
269
|
+
source,
|
|
270
|
+
text: 'Select children could not be automatically converted. Manually convert to the options prop format: options={[{ value: "...", label: "..." }]}',
|
|
271
|
+
})
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Build the options prop
|
|
276
|
+
const optionsAttr = j.jsxAttribute(
|
|
277
|
+
j.jsxIdentifier("options"),
|
|
278
|
+
j.jsxExpressionContainer(j.arrayExpression(optionObjects))
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
element.openingElement.attributes = element.openingElement.attributes || []
|
|
282
|
+
element.openingElement.attributes.push(optionsAttr)
|
|
283
|
+
|
|
284
|
+
// Remove children
|
|
285
|
+
removeChildren(element)
|
|
286
|
+
|
|
287
|
+
// Clean up unused Select subcomponent usage
|
|
288
|
+
// (the import cleanup happens in moveSelectImport or will be handled
|
|
289
|
+
// by removeUnusedImport if Select.Option/etc. are no longer used)
|
|
290
|
+
|
|
291
|
+
return true
|
|
292
|
+
},
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
export default transform
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./convertLegacyOptions"
|
|
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("convertLegacyOptions transform", () => {
|
|
19
|
+
describe("optgroup to options key rename", () => {
|
|
20
|
+
it("should rename optgroup key to options in static array", () => {
|
|
21
|
+
const input = `
|
|
22
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
23
|
+
|
|
24
|
+
function Test() {
|
|
25
|
+
return (
|
|
26
|
+
<Select
|
|
27
|
+
emptyValue="Pick"
|
|
28
|
+
options={[
|
|
29
|
+
{ label: "Group", optgroup: [{ label: "A", value: "a" }] }
|
|
30
|
+
]}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
`.trim()
|
|
35
|
+
|
|
36
|
+
const result = applyTransform(input)
|
|
37
|
+
expect(result).toContain("options: [")
|
|
38
|
+
expect(result).not.toContain("optgroup")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("should handle mixed flat options and groups", () => {
|
|
42
|
+
const input = `
|
|
43
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
44
|
+
|
|
45
|
+
function Test() {
|
|
46
|
+
return (
|
|
47
|
+
<Select
|
|
48
|
+
emptyValue="Pick"
|
|
49
|
+
options={[
|
|
50
|
+
{ label: "Standalone", value: "s" },
|
|
51
|
+
{ label: "Group", optgroup: [{ label: "A", value: "a" }] }
|
|
52
|
+
]}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
`.trim()
|
|
57
|
+
|
|
58
|
+
const result = applyTransform(input)
|
|
59
|
+
expect(result).toContain("options: [")
|
|
60
|
+
expect(result).not.toContain("optgroup")
|
|
61
|
+
expect(result).toContain('value: "s"')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("should handle multiple groups", () => {
|
|
65
|
+
const input = `
|
|
66
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
67
|
+
|
|
68
|
+
function Test() {
|
|
69
|
+
return (
|
|
70
|
+
<Select
|
|
71
|
+
emptyValue="Pick"
|
|
72
|
+
options={[
|
|
73
|
+
{ label: "Group 1", optgroup: [{ label: "A", value: "a" }] },
|
|
74
|
+
{ label: "Group 2", optgroup: [{ label: "B", value: "b" }] }
|
|
75
|
+
]}
|
|
76
|
+
/>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
`.trim()
|
|
80
|
+
|
|
81
|
+
const result = applyTransform(input)
|
|
82
|
+
expect(result).not.toContain("optgroup")
|
|
83
|
+
const optionsMatches = result.match(/options: \[/g)
|
|
84
|
+
expect(optionsMatches).toHaveLength(2)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe("no-op cases", () => {
|
|
89
|
+
it("should not transform options without optgroup key", () => {
|
|
90
|
+
const input = `
|
|
91
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
92
|
+
|
|
93
|
+
function Test() {
|
|
94
|
+
return (
|
|
95
|
+
<Select
|
|
96
|
+
emptyValue="Pick"
|
|
97
|
+
options={[
|
|
98
|
+
{ label: "Apple", value: "apple" },
|
|
99
|
+
{ label: "Orange", value: "orange" }
|
|
100
|
+
]}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
`.trim()
|
|
105
|
+
|
|
106
|
+
const result = applyTransform(input)
|
|
107
|
+
expect(result).toBe(input)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("should not transform Select without options prop", () => {
|
|
111
|
+
const input = `
|
|
112
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
113
|
+
|
|
114
|
+
function Test() {
|
|
115
|
+
return <Select emptyValue="Pick" />
|
|
116
|
+
}
|
|
117
|
+
`.trim()
|
|
118
|
+
|
|
119
|
+
const result = applyTransform(input)
|
|
120
|
+
expect(result).toBe(input)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("should not transform other components", () => {
|
|
124
|
+
const input = `
|
|
125
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
126
|
+
|
|
127
|
+
function Test() {
|
|
128
|
+
return <Button options={[{ optgroup: [] }]}>Click</Button>
|
|
129
|
+
}
|
|
130
|
+
`.trim()
|
|
131
|
+
|
|
132
|
+
const result = applyTransform(input)
|
|
133
|
+
expect(result).toBe(input)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("should add comment for dynamic options variable", () => {
|
|
137
|
+
const input = `
|
|
138
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
139
|
+
|
|
140
|
+
function Test() {
|
|
141
|
+
return <Select emptyValue="Pick" options={dynamicOptions} />
|
|
142
|
+
}
|
|
143
|
+
`.trim()
|
|
144
|
+
|
|
145
|
+
const result = applyTransform(input)
|
|
146
|
+
expect(result).toContain("TODO: tapestry-migration")
|
|
147
|
+
expect(result).toContain("options format has changed")
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { JSXAttribute, ObjectExpression, Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
|
|
4
|
+
import { andConditions } from "../../shared/conditions/andConditions"
|
|
5
|
+
import { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
6
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
7
|
+
import { transformableSelect } from "../transformableSelect"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Checks if an ObjectExpression has an `optgroup` property (legacy grouped options format).
|
|
11
|
+
*/
|
|
12
|
+
function hasOptgroupProperty(obj: ObjectExpression): boolean {
|
|
13
|
+
return obj.properties.some(
|
|
14
|
+
(prop) =>
|
|
15
|
+
prop.type === "ObjectProperty" &&
|
|
16
|
+
prop.key.type === "Identifier" &&
|
|
17
|
+
prop.key.name === "optgroup"
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Renames the `optgroup` key to `options` in a legacy option group object.
|
|
23
|
+
* Legacy: { label: "Group", optgroup: [...] }
|
|
24
|
+
* New: { label: "Group", options: [...] }
|
|
25
|
+
*/
|
|
26
|
+
function renameOptgroupToOptions(obj: ObjectExpression): boolean {
|
|
27
|
+
let changed = false
|
|
28
|
+
for (const prop of obj.properties) {
|
|
29
|
+
if (
|
|
30
|
+
prop.type === "ObjectProperty" &&
|
|
31
|
+
prop.key.type === "Identifier" &&
|
|
32
|
+
prop.key.name === "optgroup"
|
|
33
|
+
) {
|
|
34
|
+
prop.key.name = "options"
|
|
35
|
+
changed = true
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return changed
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const transform: Transform = attributeTransformFactory({
|
|
42
|
+
condition: andConditions(transformableSelect, hasAttribute("options")),
|
|
43
|
+
targetComponent: "Select",
|
|
44
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
45
|
+
transform: (element, { j }) => {
|
|
46
|
+
const attrs = element.openingElement.attributes || []
|
|
47
|
+
const optionsAttr = attrs.find(
|
|
48
|
+
(attr): attr is JSXAttribute =>
|
|
49
|
+
attr.type === "JSXAttribute" && (attr.name.name as string) === "options"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if (!optionsAttr || !optionsAttr.value) return false
|
|
53
|
+
|
|
54
|
+
// Only handle static array literals
|
|
55
|
+
let arrayExpr
|
|
56
|
+
if (
|
|
57
|
+
optionsAttr.value.type === "JSXExpressionContainer" &&
|
|
58
|
+
optionsAttr.value.expression.type === "ArrayExpression"
|
|
59
|
+
) {
|
|
60
|
+
arrayExpr = optionsAttr.value.expression
|
|
61
|
+
} else if (optionsAttr.value.type === "JSXExpressionContainer") {
|
|
62
|
+
// Dynamic value — flag for manual review if it's not an array literal
|
|
63
|
+
const expr = optionsAttr.value.expression
|
|
64
|
+
if (
|
|
65
|
+
expr.type !== "ArrayExpression" &&
|
|
66
|
+
expr.type !== "JSXEmptyExpression"
|
|
67
|
+
) {
|
|
68
|
+
addCommentToAttribute({
|
|
69
|
+
attribute: optionsAttr,
|
|
70
|
+
j,
|
|
71
|
+
text: "The options format has changed. Legacy optgroup arrays should use the 'options' key instead of 'optgroup'. Review and update manually.",
|
|
72
|
+
})
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
return false
|
|
76
|
+
} else {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check if any element in the array uses the legacy `optgroup` key
|
|
81
|
+
let hasLegacyFormat = false
|
|
82
|
+
for (const el of arrayExpr.elements) {
|
|
83
|
+
if (el && el.type === "ObjectExpression" && hasOptgroupProperty(el)) {
|
|
84
|
+
hasLegacyFormat = true
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!hasLegacyFormat) return false
|
|
90
|
+
|
|
91
|
+
// Rename `optgroup` → `options` in each group object
|
|
92
|
+
let changed = false
|
|
93
|
+
for (const el of arrayExpr.elements) {
|
|
94
|
+
if (el && el.type === "ObjectExpression") {
|
|
95
|
+
if (renameOptgroupToOptions(el)) {
|
|
96
|
+
changed = true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return changed
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
export default transform
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./convertStyleProps"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function applyTransform(source: string, options = {}) {
|
|
9
|
+
const fileInfo = { path: "test.tsx", source }
|
|
10
|
+
const result = transform(
|
|
11
|
+
fileInfo,
|
|
12
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
13
|
+
options
|
|
14
|
+
) as string | null
|
|
15
|
+
return result || source
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("convertStyleProps transform", () => {
|
|
19
|
+
it("should convert visible={false} to display: none", () => {
|
|
20
|
+
const source = `
|
|
21
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
export function TestComponent() {
|
|
24
|
+
return <Select visible={false} label="Test" placeholder="Pick" options={[]} />
|
|
25
|
+
}
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
const result = applyTransform(source)
|
|
29
|
+
expect(result).toContain("style={{")
|
|
30
|
+
expect(result).toContain('display: "none"')
|
|
31
|
+
expect(result).not.toContain("visible={false}")
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("should remove visible={true} with no style changes", () => {
|
|
35
|
+
const source = `
|
|
36
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
37
|
+
|
|
38
|
+
export function TestComponent() {
|
|
39
|
+
return <Select visible={true} label="Test" placeholder="Pick" options={[]} />
|
|
40
|
+
}
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
const result = applyTransform(source)
|
|
44
|
+
expect(result).not.toContain("visible={true}")
|
|
45
|
+
expect(result).not.toContain("style={{")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("should remove style props like marginTop", () => {
|
|
49
|
+
const source = `
|
|
50
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
51
|
+
|
|
52
|
+
export function TestComponent() {
|
|
53
|
+
return <Select marginTop={16} label="Test" placeholder="Pick" options={[]} />
|
|
54
|
+
}
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
const result = applyTransform(source)
|
|
58
|
+
expect(result).not.toContain("marginTop=")
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("should not affect non-tapestry-react imports", () => {
|
|
62
|
+
const source = `
|
|
63
|
+
import { Select } from "other-library"
|
|
64
|
+
|
|
65
|
+
export function TestComponent() {
|
|
66
|
+
return <Select marginTop={16} label="Test" />
|
|
67
|
+
}
|
|
68
|
+
`
|
|
69
|
+
|
|
70
|
+
const result = applyTransform(source)
|
|
71
|
+
expect(result).toContain("marginTop={16}")
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { stackViewPlugin } from "../../../stubs/stackViewPlugin"
|
|
2
|
+
import { stylePropTransformFactory } from "../../shared/transformFactories/stylePropTransformFactory"
|
|
3
|
+
import { transformableSelect } from "../transformableSelect"
|
|
4
|
+
|
|
5
|
+
export default stylePropTransformFactory({
|
|
6
|
+
condition: transformableSelect,
|
|
7
|
+
plugin: stackViewPlugin,
|
|
8
|
+
stylesToKeep: ["visible"],
|
|
9
|
+
stylesToRemove: [],
|
|
10
|
+
targetComponent: "Select",
|
|
11
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
12
|
+
})
|