@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,312 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrowFunctionExpression,
|
|
3
|
+
CallExpression,
|
|
4
|
+
Expression,
|
|
5
|
+
FunctionExpression,
|
|
6
|
+
JSCodeshift,
|
|
7
|
+
JSXElement,
|
|
8
|
+
JSXExpressionContainer,
|
|
9
|
+
Transform,
|
|
10
|
+
} from "jscodeshift"
|
|
11
|
+
|
|
12
|
+
import { removeChildren } from "../../shared/actions/removeChildren"
|
|
13
|
+
import { andConditions } from "../../shared/conditions/andConditions"
|
|
14
|
+
import { hasChildren } from "../../shared/conditions/hasChildren"
|
|
15
|
+
import { getAttributeExpression } from "../../shared/helpers/getAttributeExpression"
|
|
16
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
17
|
+
import { getImportName } from "../../shared/transformFactories/helpers/manageImports"
|
|
18
|
+
import { transformableSelect } from "../transformableSelect"
|
|
19
|
+
|
|
20
|
+
type ChildNode = JSXElement["children"][number]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Checks if a JSX element is `Select.Option` (or aliased equivalent)
|
|
24
|
+
*/
|
|
25
|
+
function isSelectOption(node: JSXElement, localSelectName: string): boolean {
|
|
26
|
+
const opening = node.openingElement
|
|
27
|
+
return (
|
|
28
|
+
opening.name.type === "JSXMemberExpression" &&
|
|
29
|
+
opening.name.object.type === "JSXIdentifier" &&
|
|
30
|
+
opening.name.object.name === localSelectName &&
|
|
31
|
+
opening.name.property.name === "Option"
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Gets the disabled expression if present and not a simple boolean shorthand true.
|
|
37
|
+
* Returns the expression node, or true for shorthand, or null if not present.
|
|
38
|
+
*/
|
|
39
|
+
function getDisabledExpression(element: JSXElement): Expression | true | null {
|
|
40
|
+
const attrs = element.openingElement.attributes || []
|
|
41
|
+
for (const attr of attrs) {
|
|
42
|
+
if (attr.type !== "JSXAttribute") continue
|
|
43
|
+
if (attr.name.name !== "disabled") continue
|
|
44
|
+
|
|
45
|
+
// Boolean shorthand: <Option disabled />
|
|
46
|
+
if (!attr.value) return true
|
|
47
|
+
if (attr.value.type === "JSXExpressionContainer") {
|
|
48
|
+
const expr = attr.value.expression
|
|
49
|
+
if (expr.type === "JSXEmptyExpression") return null
|
|
50
|
+
return expr as Expression
|
|
51
|
+
}
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extracts a single simple expression from JSX children.
|
|
59
|
+
* Handles: `{item.name}`, plain text.
|
|
60
|
+
* Returns null for complex multi-element children or JSX elements.
|
|
61
|
+
*/
|
|
62
|
+
function extractChildExpression(
|
|
63
|
+
children: ChildNode[],
|
|
64
|
+
j: JSCodeshift
|
|
65
|
+
): Expression | null {
|
|
66
|
+
const meaningful = getMeaningfulChildren(children)
|
|
67
|
+
|
|
68
|
+
if (meaningful.length === 0) return null
|
|
69
|
+
|
|
70
|
+
// Single text child
|
|
71
|
+
if (meaningful.length === 1 && meaningful[0].type === "JSXText") {
|
|
72
|
+
return j.stringLiteral(meaningful[0].value.trim())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Single expression child like {item.name}
|
|
76
|
+
if (
|
|
77
|
+
meaningful.length === 1 &&
|
|
78
|
+
meaningful[0].type === "JSXExpressionContainer"
|
|
79
|
+
) {
|
|
80
|
+
const expr = (meaningful[0] as JSXExpressionContainer).expression
|
|
81
|
+
if (expr.type === "JSXEmptyExpression") return null
|
|
82
|
+
return expr as Expression
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Extracts JSX children as a ReactNode label expression.
|
|
90
|
+
* For a single JSXElement child, returns it directly.
|
|
91
|
+
* For multiple meaningful children, wraps them in a JSX fragment.
|
|
92
|
+
* Returns null if children cannot be extracted.
|
|
93
|
+
*/
|
|
94
|
+
function extractChildrenAsJSXLabel(
|
|
95
|
+
children: ChildNode[],
|
|
96
|
+
j: JSCodeshift
|
|
97
|
+
): Expression | null {
|
|
98
|
+
const meaningful = getMeaningfulChildren(children)
|
|
99
|
+
if (meaningful.length === 0) return null
|
|
100
|
+
|
|
101
|
+
// Single JSXElement child — use directly
|
|
102
|
+
if (meaningful.length === 1 && meaningful[0].type === "JSXElement") {
|
|
103
|
+
return meaningful[0] as unknown as Expression
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Multiple children — wrap in a JSX fragment <>...</>
|
|
107
|
+
const fragment = j.jsxFragment(
|
|
108
|
+
j.jsxOpeningFragment(),
|
|
109
|
+
j.jsxClosingFragment(),
|
|
110
|
+
children.filter((child) => {
|
|
111
|
+
// Keep everything except pure-whitespace text nodes
|
|
112
|
+
if (child.type === "JSXText") return child.value.trim().length > 0
|
|
113
|
+
return true
|
|
114
|
+
})
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return fragment as unknown as Expression
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getMeaningfulChildren(children: ChildNode[]): ChildNode[] {
|
|
121
|
+
return children.filter((child) => {
|
|
122
|
+
if (child.type === "JSXText") return child.value.trim().length > 0
|
|
123
|
+
return true
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Extracts the JSXElement from an arrow/function callback body.
|
|
129
|
+
* Handles: `(item) => <JSX />`, `(item) => (<JSX />)`, and
|
|
130
|
+
* `(item) => { return <JSX /> }`
|
|
131
|
+
*/
|
|
132
|
+
function getReturnedJSXElement(
|
|
133
|
+
callback: ArrowFunctionExpression | FunctionExpression
|
|
134
|
+
): JSXElement | null {
|
|
135
|
+
const { body } = callback
|
|
136
|
+
|
|
137
|
+
// Arrow with expression body: (item) => <Select.Option ...>
|
|
138
|
+
if (body.type === "JSXElement") {
|
|
139
|
+
return body
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Arrow with block body: (item) => { return <Select.Option ...> }
|
|
143
|
+
if (body.type === "BlockStatement") {
|
|
144
|
+
const returnStmt = body.body.find((s) => s.type === "ReturnStatement")
|
|
145
|
+
if (
|
|
146
|
+
returnStmt &&
|
|
147
|
+
returnStmt.type === "ReturnStatement" &&
|
|
148
|
+
returnStmt.argument?.type === "JSXElement"
|
|
149
|
+
) {
|
|
150
|
+
return returnStmt.argument
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Checks if a CallExpression is a `.map()` call.
|
|
159
|
+
*/
|
|
160
|
+
function isMapCall(expr: Expression): expr is CallExpression {
|
|
161
|
+
return (
|
|
162
|
+
expr.type === "CallExpression" &&
|
|
163
|
+
expr.callee.type === "MemberExpression" &&
|
|
164
|
+
expr.callee.property.type === "Identifier" &&
|
|
165
|
+
expr.callee.property.name === "map"
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const transform: Transform = attributeTransformFactory({
|
|
170
|
+
condition: andConditions(transformableSelect, hasChildren),
|
|
171
|
+
targetComponent: "Select",
|
|
172
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
173
|
+
transform: (element, { j, source }) => {
|
|
174
|
+
const children = element.children || []
|
|
175
|
+
|
|
176
|
+
const localSelectName =
|
|
177
|
+
getImportName("Select", "@planningcenter/tapestry-react", {
|
|
178
|
+
j,
|
|
179
|
+
source,
|
|
180
|
+
}) || "Select"
|
|
181
|
+
|
|
182
|
+
// Find meaningful children (skip whitespace)
|
|
183
|
+
const meaningful = children.filter((child: ChildNode) => {
|
|
184
|
+
if (child.type === "JSXText") return child.value.trim().length > 0
|
|
185
|
+
return true
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// Must be exactly one expression container with a .map() call
|
|
189
|
+
if (meaningful.length !== 1) return false
|
|
190
|
+
if (meaningful[0].type !== "JSXExpressionContainer") return false
|
|
191
|
+
|
|
192
|
+
const container = meaningful[0] as JSXExpressionContainer
|
|
193
|
+
const expr = container.expression
|
|
194
|
+
if (expr.type === "JSXEmptyExpression") return false
|
|
195
|
+
if (!isMapCall(expr as Expression)) return false
|
|
196
|
+
|
|
197
|
+
const mapCall = expr as CallExpression
|
|
198
|
+
const callback = mapCall.arguments[0]
|
|
199
|
+
if (
|
|
200
|
+
!callback ||
|
|
201
|
+
(callback.type !== "ArrowFunctionExpression" &&
|
|
202
|
+
callback.type !== "FunctionExpression")
|
|
203
|
+
) {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const callbackFn = callback as ArrowFunctionExpression | FunctionExpression
|
|
208
|
+
|
|
209
|
+
// Get the returned JSX element from the callback
|
|
210
|
+
const optionElement = getReturnedJSXElement(callbackFn)
|
|
211
|
+
if (!optionElement) return false
|
|
212
|
+
if (!isSelectOption(optionElement, localSelectName)) return false
|
|
213
|
+
|
|
214
|
+
// Extract value expression from Select.Option
|
|
215
|
+
const valueExpr = getAttributeExpression(optionElement, "value")
|
|
216
|
+
if (!valueExpr) return false
|
|
217
|
+
|
|
218
|
+
// Extract label from children — try simple expression first, then JSX
|
|
219
|
+
const optionChildren = optionElement.children || []
|
|
220
|
+
const simpleLabelExpr = extractChildExpression(optionChildren, j)
|
|
221
|
+
const jsxLabelExpr = simpleLabelExpr
|
|
222
|
+
? null
|
|
223
|
+
: extractChildrenAsJSXLabel(optionChildren, j)
|
|
224
|
+
const labelExpr = simpleLabelExpr || jsxLabelExpr
|
|
225
|
+
|
|
226
|
+
if (!labelExpr) return false
|
|
227
|
+
|
|
228
|
+
const isComplexLabel = !simpleLabelExpr && !!jsxLabelExpr
|
|
229
|
+
|
|
230
|
+
// Build the object properties: { label: ..., value: ... }
|
|
231
|
+
const properties = [j.objectProperty(j.identifier("label"), labelExpr)]
|
|
232
|
+
|
|
233
|
+
// Complex labels require textValue for type-ahead matching
|
|
234
|
+
if (isComplexLabel) {
|
|
235
|
+
const textValueProp = j.objectProperty(
|
|
236
|
+
j.identifier("textValue"),
|
|
237
|
+
j.stringLiteral("")
|
|
238
|
+
)
|
|
239
|
+
// Add a TODO comment to the textValue property
|
|
240
|
+
const comment = j.commentBlock(
|
|
241
|
+
" TODO: tapestry-migration (textValue): Provide a plain-text value for type-ahead matching ",
|
|
242
|
+
true,
|
|
243
|
+
false
|
|
244
|
+
)
|
|
245
|
+
textValueProp.comments = [comment]
|
|
246
|
+
properties.push(textValueProp)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
properties.push(j.objectProperty(j.identifier("value"), valueExpr))
|
|
250
|
+
|
|
251
|
+
// Handle disabled prop if present
|
|
252
|
+
const disabledExpr = getDisabledExpression(optionElement)
|
|
253
|
+
if (disabledExpr === true) {
|
|
254
|
+
properties.push(
|
|
255
|
+
j.objectProperty(j.identifier("disabled"), j.booleanLiteral(true))
|
|
256
|
+
)
|
|
257
|
+
} else if (disabledExpr !== null) {
|
|
258
|
+
properties.push(j.objectProperty(j.identifier("disabled"), disabledExpr))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Preserve data-* attributes on the option object
|
|
262
|
+
const attrs = optionElement.openingElement.attributes || []
|
|
263
|
+
for (const attr of attrs) {
|
|
264
|
+
if (attr.type !== "JSXAttribute") continue
|
|
265
|
+
const name = attr.name.name as string
|
|
266
|
+
if (!name.startsWith("data-")) continue
|
|
267
|
+
const expr = getAttributeExpression(optionElement, name)
|
|
268
|
+
if (expr) {
|
|
269
|
+
properties.push(j.objectProperty(j.stringLiteral(name), expr))
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Build new .map() callback that returns an object
|
|
274
|
+
const objectReturn = j.objectExpression(properties)
|
|
275
|
+
const newCallback = j.arrowFunctionExpression(
|
|
276
|
+
callbackFn.params,
|
|
277
|
+
j.parenthesizedExpression(objectReturn)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
// Build new .map() call: array.map(item => ({ ... }))
|
|
281
|
+
const newMapCall = j.callExpression(mapCall.callee, [newCallback])
|
|
282
|
+
|
|
283
|
+
// Add as options prop
|
|
284
|
+
element.openingElement.attributes = element.openingElement.attributes || []
|
|
285
|
+
|
|
286
|
+
element.openingElement.attributes.push(
|
|
287
|
+
j.jsxAttribute(
|
|
288
|
+
j.jsxIdentifier("options"),
|
|
289
|
+
j.jsxExpressionContainer(newMapCall)
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
// Complex labels require popover mode via the complex prop
|
|
294
|
+
if (isComplexLabel) {
|
|
295
|
+
const hasComplexProp = (element.openingElement.attributes || []).some(
|
|
296
|
+
(attr) => attr.type === "JSXAttribute" && attr.name.name === "complex"
|
|
297
|
+
)
|
|
298
|
+
if (!hasComplexProp) {
|
|
299
|
+
element.openingElement.attributes.push(
|
|
300
|
+
j.jsxAttribute(j.jsxIdentifier("complex"))
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Remove children
|
|
306
|
+
removeChildren(element)
|
|
307
|
+
|
|
308
|
+
return true
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
export default transform
|