@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.
Files changed (48) hide show
  1. package/dist/tapestry-react-shim.cjs +7 -1
  2. package/package.json +3 -3
  3. package/src/components/input/transformableInput.ts +47 -6
  4. package/src/components/input/transforms/mergeFieldIntoInput.test.ts +78 -0
  5. package/src/components/input/transforms/mergeFieldIntoInput.ts +6 -212
  6. package/src/components/input/transforms/removeDuplicateKeys.test.ts +3 -3
  7. package/src/components/input/transforms/removeTypeInput.ts +3 -3
  8. package/src/components/input/transforms/removeTypeText.ts +2 -3
  9. package/src/components/input/transforms/unsupportedProps.test.ts +20 -20
  10. package/src/components/select/index.ts +58 -0
  11. package/src/components/select/transformableSelect.ts +7 -0
  12. package/src/components/select/transforms/auditSpreadProps.test.ts +103 -0
  13. package/src/components/select/transforms/auditSpreadProps.ts +26 -0
  14. package/src/components/select/transforms/childrenToOptions.test.ts +367 -0
  15. package/src/components/select/transforms/childrenToOptions.ts +295 -0
  16. package/src/components/select/transforms/convertLegacyOptions.test.ts +150 -0
  17. package/src/components/select/transforms/convertLegacyOptions.ts +105 -0
  18. package/src/components/select/transforms/convertStyleProps.test.ts +73 -0
  19. package/src/components/select/transforms/convertStyleProps.ts +12 -0
  20. package/src/components/select/transforms/emptyValueToPlaceholder.test.ts +122 -0
  21. package/src/components/select/transforms/emptyValueToPlaceholder.ts +22 -0
  22. package/src/components/select/transforms/innerRefToRef.test.ts +89 -0
  23. package/src/components/select/transforms/innerRefToRef.ts +18 -0
  24. package/src/components/select/transforms/mapChildrenToOptions.test.ts +521 -0
  25. package/src/components/select/transforms/mapChildrenToOptions.ts +312 -0
  26. package/src/components/select/transforms/mergeFieldIntoSelect.test.ts +506 -0
  27. package/src/components/select/transforms/mergeFieldIntoSelect.ts +7 -0
  28. package/src/components/select/transforms/mergeSelectLabel.test.ts +458 -0
  29. package/src/components/select/transforms/mergeSelectLabel.ts +225 -0
  30. package/src/components/select/transforms/moveSelectImport.test.ts +148 -0
  31. package/src/components/select/transforms/moveSelectImport.ts +14 -0
  32. package/src/components/select/transforms/removeDefaultProps.test.ts +249 -0
  33. package/src/components/select/transforms/removeDefaultProps.ts +112 -0
  34. package/src/components/select/transforms/sizeMapping.test.ts +188 -0
  35. package/src/components/select/transforms/sizeMapping.ts +17 -0
  36. package/src/components/select/transforms/skipMultipleSelect.test.ts +148 -0
  37. package/src/components/select/transforms/skipMultipleSelect.ts +23 -0
  38. package/src/components/select/transforms/stateToInvalid.test.ts +217 -0
  39. package/src/components/select/transforms/stateToInvalid.ts +59 -0
  40. package/src/components/select/transforms/stateToInvalidTernary.test.ts +146 -0
  41. package/src/components/select/transforms/stateToInvalidTernary.ts +13 -0
  42. package/src/components/select/transforms/unsupportedProps.test.ts +252 -0
  43. package/src/components/select/transforms/unsupportedProps.ts +44 -0
  44. package/src/components/shared/helpers/getAttributeExpression.ts +26 -0
  45. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +19 -2
  46. package/src/components/shared/transformFactories/mergeFieldFactory.ts +244 -0
  47. package/src/components/text-area/transforms/mergeFieldIntoTextArea.ts +4 -226
  48. 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