@planningcenter/tapestry-migration-cli 3.1.0-rc.2 → 3.1.0-rc.21
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/button/transforms/convertStyleProps.test.ts +97 -0
- package/src/components/button/transforms/removeTypeButton.test.ts +0 -1
- package/src/components/checkbox/transforms/moveCheckboxImport.test.ts +3 -0
- package/src/components/input/index.ts +66 -0
- package/src/components/input/transformableInput.ts +49 -0
- package/src/components/input/transforms/auditSpreadProps.test.ts +192 -0
- package/src/components/input/transforms/auditSpreadProps.ts +26 -0
- package/src/components/input/transforms/autoWidthTransform.test.ts +172 -0
- package/src/components/input/transforms/autoWidthTransform.ts +41 -0
- package/src/components/input/transforms/convertStyleProps.test.ts +128 -0
- package/src/components/input/transforms/convertStyleProps.ts +12 -0
- package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.test.ts +186 -0
- package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.ts +27 -0
- package/src/components/input/transforms/inputLabelToLabelProp.test.ts +319 -0
- package/src/components/input/transforms/inputLabelToLabelProp.ts +203 -0
- package/src/components/input/transforms/mergeFieldIntoInput.test.ts +469 -0
- package/src/components/input/transforms/mergeFieldIntoInput.ts +7 -0
- package/src/components/input/transforms/mergeInputLabel.test.ts +458 -0
- package/src/components/input/transforms/mergeInputLabel.ts +204 -0
- package/src/components/input/transforms/moveInputImport.test.ts +166 -0
- package/src/components/input/transforms/moveInputImport.ts +14 -0
- package/src/components/input/transforms/numberFieldAddTypeNumber.test.ts +92 -0
- package/src/components/input/transforms/numberFieldAddTypeNumber.ts +14 -0
- package/src/components/input/transforms/numberFieldRenameToInput.test.ts +126 -0
- package/src/components/input/transforms/numberFieldRenameToInput.ts +9 -0
- package/src/components/input/transforms/removeAsInput.test.ts +139 -0
- package/src/components/input/transforms/removeAsInput.ts +20 -0
- package/src/components/input/transforms/removeDuplicateKeys.test.ts +302 -0
- package/src/components/input/transforms/removeDuplicateKeys.ts +10 -0
- package/src/components/input/transforms/removeInputBox.test.ts +352 -0
- package/src/components/input/transforms/removeInputBox.ts +109 -0
- package/src/components/input/transforms/removeRedundantAriaLabel.test.ts +128 -0
- package/src/components/input/transforms/removeRedundantAriaLabel.ts +21 -0
- 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.test.ts +160 -0
- package/src/components/input/transforms/removeTypeText.ts +17 -0
- package/src/components/input/transforms/sizeMapping.test.ts +198 -0
- package/src/components/input/transforms/sizeMapping.ts +17 -0
- package/src/components/input/transforms/skipRenderSideProps.test.ts +236 -0
- package/src/components/input/transforms/skipRenderSideProps.ts +27 -0
- package/src/components/input/transforms/stateToInvalid.test.ts +208 -0
- package/src/components/input/transforms/stateToInvalid.ts +59 -0
- package/src/components/input/transforms/stateToInvalidTernary.test.ts +159 -0
- package/src/components/input/transforms/stateToInvalidTernary.ts +13 -0
- package/src/components/input/transforms/unsupportedProps.test.ts +566 -0
- package/src/components/input/transforms/unsupportedProps.ts +84 -0
- package/src/components/link/transforms/reviewStyles.test.ts +0 -1
- 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 +102 -0
- package/src/components/shared/transformFactories/helpers/manageImports.ts +14 -12
- package/src/components/shared/transformFactories/mergeFieldFactory.ts +244 -0
- package/src/components/shared/transformFactories/sizeMappingFactory.ts +9 -2
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +56 -17
- package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +65 -0
- 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 +2 -1
|
@@ -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
|
+
}
|
|
@@ -2,7 +2,9 @@ import { Transform } from "jscodeshift"
|
|
|
2
2
|
|
|
3
3
|
import { addCommentToAttribute } from "../actions/addCommentToAttribute"
|
|
4
4
|
import { getAttributeValue } from "../actions/getAttributeValue"
|
|
5
|
+
import { andConditions } from "../conditions/andConditions"
|
|
5
6
|
import { hasAttribute } from "../conditions/hasAttribute"
|
|
7
|
+
import { TransformCondition } from "../types"
|
|
6
8
|
import { attributeTransformFactory } from "./attributeTransformFactory"
|
|
7
9
|
|
|
8
10
|
/**
|
|
@@ -13,16 +15,21 @@ import { attributeTransformFactory } from "./attributeTransformFactory"
|
|
|
13
15
|
* @param options.targetComponent - The component name to target (e.g., "ToggleSwitch", "Radio")
|
|
14
16
|
* @param options.targetPackage - The package the component is imported from
|
|
15
17
|
* @param options.sizeMapping - Object mapping old size values to new size values (e.g., { xs: "sm", lg: "md" })
|
|
18
|
+
* @param options.condition - Optional additional condition beyond hasAttribute("size")
|
|
16
19
|
* @returns A Transform function that maps size attributes
|
|
17
20
|
*/
|
|
18
21
|
export function sizeMappingFactory(options: {
|
|
22
|
+
condition?: TransformCondition
|
|
19
23
|
sizeMapping: Record<string, string>
|
|
20
24
|
targetComponent: string
|
|
21
25
|
targetPackage: string
|
|
22
26
|
}): Transform {
|
|
23
|
-
const { sizeMapping, ...restOptions } = options
|
|
27
|
+
const { condition, sizeMapping, ...restOptions } = options
|
|
28
|
+
const baseCondition = hasAttribute("size")
|
|
24
29
|
return attributeTransformFactory({
|
|
25
|
-
condition:
|
|
30
|
+
condition: condition
|
|
31
|
+
? andConditions(baseCondition, condition)
|
|
32
|
+
: baseCondition,
|
|
26
33
|
...restOptions,
|
|
27
34
|
transform: (element, { j }) => {
|
|
28
35
|
let hasChanges = false
|
|
@@ -16,6 +16,7 @@ import { addComment } from "../../shared/actions/addComment"
|
|
|
16
16
|
import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
|
|
17
17
|
import { getAttribute } from "../../shared/actions/getAttribute"
|
|
18
18
|
import { removeAttribute } from "../../shared/actions/removeAttribute"
|
|
19
|
+
import { TransformCondition } from "../types"
|
|
19
20
|
import { attributeTransformFactory } from "./attributeTransformFactory"
|
|
20
21
|
|
|
21
22
|
type StylePropMapping = Record<
|
|
@@ -26,16 +27,12 @@ type StylePropMapping = Record<
|
|
|
26
27
|
}
|
|
27
28
|
>
|
|
28
29
|
|
|
29
|
-
// Helper function to extract prop value from JSX attribute
|
|
30
30
|
function extractPropValue(attr: JSXAttribute, j: JSCodeshift) {
|
|
31
31
|
if (!attr.value) {
|
|
32
|
-
// Boolean prop like <Button disabled />
|
|
33
32
|
return true
|
|
34
33
|
} else if (attr.value.type === "StringLiteral") {
|
|
35
|
-
// String literal like color="blue"
|
|
36
34
|
return attr.value.value
|
|
37
35
|
} else if (attr.value.type === "JSXExpressionContainer") {
|
|
38
|
-
// Expression like color={someVar} or color={5}
|
|
39
36
|
const expression = attr.value.expression
|
|
40
37
|
if (expression.type === "StringLiteral") {
|
|
41
38
|
return expression.value
|
|
@@ -44,14 +41,11 @@ function extractPropValue(attr: JSXAttribute, j: JSCodeshift) {
|
|
|
44
41
|
} else if (expression.type === "BooleanLiteral") {
|
|
45
42
|
return expression.value
|
|
46
43
|
} else if (expression.type === "Identifier") {
|
|
47
|
-
// Variable reference - we'll use the variable name as a placeholder
|
|
48
44
|
return `{${expression.name}}`
|
|
49
45
|
} else {
|
|
50
|
-
// Complex expression - convert back to string representation
|
|
51
46
|
return `{${j(expression).toSource()}}`
|
|
52
47
|
}
|
|
53
48
|
} else {
|
|
54
|
-
// Fallback for other types
|
|
55
49
|
return j(attr.value).toSource()
|
|
56
50
|
}
|
|
57
51
|
}
|
|
@@ -77,7 +71,6 @@ function processKeepStyleProps({
|
|
|
77
71
|
const propName = attr.name.name as string
|
|
78
72
|
const propValue = extractPropValue(attr, j)
|
|
79
73
|
|
|
80
|
-
// If it's a complex expression (starts and ends with braces), handle it directly
|
|
81
74
|
if (
|
|
82
75
|
typeof propValue === "string" &&
|
|
83
76
|
propValue.startsWith("{") &&
|
|
@@ -118,7 +111,6 @@ function processRemoveStyleProps({
|
|
|
118
111
|
})
|
|
119
112
|
}
|
|
120
113
|
|
|
121
|
-
// Process props that need style property mappings
|
|
122
114
|
function processStylePropMappings({
|
|
123
115
|
attributes,
|
|
124
116
|
j,
|
|
@@ -175,6 +167,7 @@ function applyStylesToComponent({
|
|
|
175
167
|
}: {
|
|
176
168
|
element: JSXElement
|
|
177
169
|
j: JSCodeshift
|
|
170
|
+
source: Collection
|
|
178
171
|
styles: Record<string, unknown>
|
|
179
172
|
}) {
|
|
180
173
|
const styleAttr = getAttribute({ element, name: "style" })
|
|
@@ -187,11 +180,9 @@ function applyStylesToComponent({
|
|
|
187
180
|
value.startsWith("{") &&
|
|
188
181
|
value.endsWith("}")
|
|
189
182
|
) {
|
|
190
|
-
|
|
191
|
-
const expressionCode = value.slice(1, -1) // Remove surrounding braces
|
|
183
|
+
const expressionCode = value.slice(1, -1)
|
|
192
184
|
try {
|
|
193
185
|
const parsed = j(expressionCode)
|
|
194
|
-
// Get the first expression from the program body
|
|
195
186
|
const firstStatement = parsed.find(j.Program).get("body", 0).value
|
|
196
187
|
if (firstStatement?.type === "ExpressionStatement") {
|
|
197
188
|
valueNode = firstStatement.expression
|
|
@@ -199,7 +190,6 @@ function applyStylesToComponent({
|
|
|
199
190
|
valueNode = j.stringLiteral(value)
|
|
200
191
|
}
|
|
201
192
|
} catch {
|
|
202
|
-
// If parsing fails, fall back to string literal
|
|
203
193
|
valueNode = j.stringLiteral(value)
|
|
204
194
|
}
|
|
205
195
|
} else if (typeof value === "string") {
|
|
@@ -214,8 +204,56 @@ function applyStylesToComponent({
|
|
|
214
204
|
)
|
|
215
205
|
|
|
216
206
|
if (styleAttr && styleAttr.type === "JSXAttribute") {
|
|
217
|
-
|
|
218
|
-
|
|
207
|
+
const existingValue = styleAttr.value
|
|
208
|
+
if (
|
|
209
|
+
existingValue?.type === "JSXExpressionContainer" &&
|
|
210
|
+
existingValue.expression.type === "ObjectExpression"
|
|
211
|
+
) {
|
|
212
|
+
// Case 1: Existing style is a static object literal — merge, new props win
|
|
213
|
+
const newProps = (
|
|
214
|
+
styleValue.expression as ReturnType<typeof j.objectExpression>
|
|
215
|
+
).properties
|
|
216
|
+
const newKeys = new Set(
|
|
217
|
+
newProps
|
|
218
|
+
.filter((p) => p.type === "ObjectProperty")
|
|
219
|
+
.map((p) => {
|
|
220
|
+
const prop = p as ReturnType<typeof j.objectProperty>
|
|
221
|
+
if (prop.key.type === "Identifier") return prop.key.name
|
|
222
|
+
if (prop.key.type === "StringLiteral") return prop.key.value
|
|
223
|
+
return null
|
|
224
|
+
})
|
|
225
|
+
.filter(Boolean)
|
|
226
|
+
)
|
|
227
|
+
const survivingExisting = existingValue.expression.properties.filter(
|
|
228
|
+
(p) => {
|
|
229
|
+
if (p.type === "SpreadElement" || p.type === "RestElement")
|
|
230
|
+
return true
|
|
231
|
+
const prop = p as ReturnType<typeof j.objectProperty>
|
|
232
|
+
if (prop.key.type === "Identifier") return !newKeys.has(prop.key.name)
|
|
233
|
+
if (prop.key.type === "StringLiteral")
|
|
234
|
+
return !newKeys.has(prop.key.value)
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
const mergedObject = j.objectExpression([
|
|
239
|
+
...survivingExisting,
|
|
240
|
+
...newProps,
|
|
241
|
+
])
|
|
242
|
+
styleAttr.value = j.jsxExpressionContainer(mergedObject)
|
|
243
|
+
} else if (
|
|
244
|
+
existingValue?.type === "JSXExpressionContainer" &&
|
|
245
|
+
existingValue.expression.type !== "JSXEmptyExpression"
|
|
246
|
+
) {
|
|
247
|
+
// Case 2: Dynamic expression — merge via spread: { ...existing, ...computed }
|
|
248
|
+
const newProps = (
|
|
249
|
+
styleValue.expression as ReturnType<typeof j.objectExpression>
|
|
250
|
+
).properties
|
|
251
|
+
const mergedObject = j.objectExpression([
|
|
252
|
+
j.spreadElement(existingValue.expression),
|
|
253
|
+
...newProps,
|
|
254
|
+
])
|
|
255
|
+
styleAttr.value = j.jsxExpressionContainer(mergedObject)
|
|
256
|
+
}
|
|
219
257
|
} else {
|
|
220
258
|
const styleAttr = j.jsxAttribute(j.jsxIdentifier("style"), styleValue)
|
|
221
259
|
|
|
@@ -225,6 +263,7 @@ function applyStylesToComponent({
|
|
|
225
263
|
}
|
|
226
264
|
|
|
227
265
|
export function stylePropTransformFactory(config: {
|
|
266
|
+
condition?: TransformCondition
|
|
228
267
|
plugin?: {
|
|
229
268
|
getStyles: (props: Record<string, unknown>) => Record<string, unknown>
|
|
230
269
|
styleProps: string[]
|
|
@@ -321,9 +360,8 @@ export function stylePropTransformFactory(config: {
|
|
|
321
360
|
styles = { ...styles, ...directStyleProps }
|
|
322
361
|
if (options.verbose) console.log("Final generated styles:", styles)
|
|
323
362
|
|
|
324
|
-
// Only apply styles if there are actual CSS properties to add
|
|
325
363
|
if (Object.keys(styles).length > 0) {
|
|
326
|
-
applyStylesToComponent({ element, j, styles })
|
|
364
|
+
applyStylesToComponent({ element, j, source, styles })
|
|
327
365
|
|
|
328
366
|
if (options.verbose) {
|
|
329
367
|
const styleAttr = getAttribute({ element, name: "style" })
|
|
@@ -354,6 +392,7 @@ export function stylePropTransformFactory(config: {
|
|
|
354
392
|
}
|
|
355
393
|
|
|
356
394
|
return attributeTransformFactory({
|
|
395
|
+
condition: config.condition,
|
|
357
396
|
targetComponent: config.targetComponent,
|
|
358
397
|
targetPackage: config.targetPackage,
|
|
359
398
|
transform,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Expression, Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { getAttribute } from "../actions/getAttribute"
|
|
4
|
+
import { removeAttribute } from "../actions/removeAttribute"
|
|
5
|
+
import { TransformCondition } from "../types"
|
|
6
|
+
import { attributeTransformFactory } from "./attributeTransformFactory"
|
|
7
|
+
|
|
8
|
+
interface TernaryConditionalToPropConfig {
|
|
9
|
+
condition?: TransformCondition
|
|
10
|
+
fromProp: string
|
|
11
|
+
matchValue: string
|
|
12
|
+
targetComponent: string
|
|
13
|
+
targetPackage: string
|
|
14
|
+
toProp: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ternaryConditionalToPropFactory({
|
|
18
|
+
condition,
|
|
19
|
+
fromProp,
|
|
20
|
+
matchValue,
|
|
21
|
+
targetComponent,
|
|
22
|
+
targetPackage,
|
|
23
|
+
toProp,
|
|
24
|
+
}: TernaryConditionalToPropConfig): Transform {
|
|
25
|
+
return attributeTransformFactory({
|
|
26
|
+
condition,
|
|
27
|
+
targetComponent,
|
|
28
|
+
targetPackage,
|
|
29
|
+
transform: (element, { j, source }) => {
|
|
30
|
+
const attr = getAttribute({ element, name: fromProp })
|
|
31
|
+
if (!attr) return false
|
|
32
|
+
|
|
33
|
+
if (attr.value?.type !== "JSXExpressionContainer") return false
|
|
34
|
+
|
|
35
|
+
const expr = attr.value.expression
|
|
36
|
+
if (expr.type !== "ConditionalExpression") return false
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
expr.consequent.type !== "StringLiteral" ||
|
|
40
|
+
expr.consequent.value !== matchValue
|
|
41
|
+
) {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const isNullAlternate = expr.alternate.type === "NullLiteral"
|
|
46
|
+
const isUndefinedAlternate =
|
|
47
|
+
expr.alternate.type === "Identifier" &&
|
|
48
|
+
expr.alternate.name === "undefined"
|
|
49
|
+
|
|
50
|
+
if (!isNullAlternate && !isUndefinedAlternate) return false
|
|
51
|
+
|
|
52
|
+
const testExpr = expr.test as Expression
|
|
53
|
+
|
|
54
|
+
removeAttribute(fromProp, { element, j, source })
|
|
55
|
+
element.openingElement.attributes.push(
|
|
56
|
+
j.jsxAttribute(
|
|
57
|
+
j.jsxIdentifier(toProp),
|
|
58
|
+
j.jsxExpressionContainer(testExpr)
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return true
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import auditSpreadProps from "./transforms/auditSpreadProps"
|
|
4
|
+
import convertStyleProps from "./transforms/convertStyleProps"
|
|
5
|
+
import innerRefToRef from "./transforms/innerRefToRef"
|
|
6
|
+
import mergeFieldIntoTextArea from "./transforms/mergeFieldIntoTextArea"
|
|
7
|
+
import moveTextAreaImport from "./transforms/moveTextAreaImport"
|
|
8
|
+
import removeDuplicateKeys from "./transforms/removeDuplicateKeys"
|
|
9
|
+
import removeRedundantAriaLabel from "./transforms/removeRedundantAriaLabel"
|
|
10
|
+
import sizeMapping from "./transforms/sizeMapping"
|
|
11
|
+
import stateToInvalid from "./transforms/stateToInvalid"
|
|
12
|
+
import stateToInvalidTernary from "./transforms/stateToInvalidTernary"
|
|
13
|
+
import unsupportedProps from "./transforms/unsupportedProps"
|
|
14
|
+
|
|
15
|
+
const transform: Transform = (fileInfo, api, options) => {
|
|
16
|
+
let currentSource = fileInfo.source
|
|
17
|
+
let hasAnyChanges = false
|
|
18
|
+
|
|
19
|
+
const transforms = [
|
|
20
|
+
mergeFieldIntoTextArea,
|
|
21
|
+
auditSpreadProps,
|
|
22
|
+
innerRefToRef,
|
|
23
|
+
stateToInvalidTernary,
|
|
24
|
+
stateToInvalid,
|
|
25
|
+
sizeMapping,
|
|
26
|
+
convertStyleProps,
|
|
27
|
+
removeDuplicateKeys,
|
|
28
|
+
removeRedundantAriaLabel,
|
|
29
|
+
unsupportedProps,
|
|
30
|
+
moveTextAreaImport,
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
for (const individualTransform of transforms) {
|
|
34
|
+
const result = individualTransform(
|
|
35
|
+
{ ...fileInfo, source: currentSource },
|
|
36
|
+
api,
|
|
37
|
+
options
|
|
38
|
+
)
|
|
39
|
+
if (result && result !== currentSource) {
|
|
40
|
+
currentSource = result as string
|
|
41
|
+
hasAnyChanges = true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return hasAnyChanges ? currentSource : null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default transform
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./auditSpreadProps"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
const AUDIT_COMMENT =
|
|
9
|
+
"TODO: tapestry-migration (spreadAttribute): Spread props can contain unsupported props, please explore usages and migrate as needed."
|
|
10
|
+
|
|
11
|
+
function applyTransform(source: string) {
|
|
12
|
+
const fileInfo = { path: "test.tsx", source }
|
|
13
|
+
return transform(
|
|
14
|
+
fileInfo,
|
|
15
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
16
|
+
{}
|
|
17
|
+
) as string | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("auditSpreadProps transform", () => {
|
|
21
|
+
describe("basic transformations", () => {
|
|
22
|
+
it("should add comment to TextArea with single spread prop", () => {
|
|
23
|
+
const input = `
|
|
24
|
+
import { TextArea } from "@planningcenter/tapestry-react"
|
|
25
|
+
|
|
26
|
+
export default function Test() {
|
|
27
|
+
const props = { onChange: handleChange }
|
|
28
|
+
return <TextArea {...props} label="Notes" />
|
|
29
|
+
}
|
|
30
|
+
`.trim()
|
|
31
|
+
|
|
32
|
+
const result = applyTransform(input)
|
|
33
|
+
expect(result).toContain(AUDIT_COMMENT)
|
|
34
|
+
expect(result).toContain("{...props}")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("should add comment to TextArea with multiple spread props", () => {
|
|
38
|
+
const input = `
|
|
39
|
+
import { TextArea } from "@planningcenter/tapestry-react"
|
|
40
|
+
|
|
41
|
+
export default function Test() {
|
|
42
|
+
const baseProps = { onChange: handleChange }
|
|
43
|
+
const styleProps = { className: "textarea" }
|
|
44
|
+
return <TextArea {...baseProps} {...styleProps} label="Notes" />
|
|
45
|
+
}
|
|
46
|
+
`.trim()
|
|
47
|
+
|
|
48
|
+
const result = applyTransform(input)
|
|
49
|
+
expect(result).toContain(AUDIT_COMMENT)
|
|
50
|
+
expect(result).toContain("{...baseProps}")
|
|
51
|
+
expect(result).toContain("{...styleProps}")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("should handle multiple TextArea components with spread props", () => {
|
|
55
|
+
const input = `
|
|
56
|
+
import { TextArea } from "@planningcenter/tapestry-react"
|
|
57
|
+
|
|
58
|
+
export default function Test() {
|
|
59
|
+
const props1 = { onChange: handleChange1 }
|
|
60
|
+
const props2 = { onChange: handleChange2 }
|
|
61
|
+
return (
|
|
62
|
+
<div>
|
|
63
|
+
<TextArea {...props1} label="Notes" />
|
|
64
|
+
<TextArea {...props2} label="Comments" />
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
`.trim()
|
|
69
|
+
|
|
70
|
+
const result = applyTransform(input)
|
|
71
|
+
expect(result).toContain(AUDIT_COMMENT)
|
|
72
|
+
expect(result).toContain("{...props1}")
|
|
73
|
+
expect(result).toContain("{...props2}")
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe("edge cases", () => {
|
|
78
|
+
it("should not transform TextArea without spread props", () => {
|
|
79
|
+
const input = `
|
|
80
|
+
import { TextArea } from "@planningcenter/tapestry-react"
|
|
81
|
+
|
|
82
|
+
export default function Test() {
|
|
83
|
+
return <TextArea onChange={handleChange} label="Notes" />
|
|
84
|
+
}
|
|
85
|
+
`.trim()
|
|
86
|
+
|
|
87
|
+
const result = applyTransform(input)
|
|
88
|
+
expect(result).toBe(null)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("should not transform if TextArea is not imported from @planningcenter/tapestry-react", () => {
|
|
92
|
+
const input = `
|
|
93
|
+
import { TextArea } from "other-library"
|
|
94
|
+
|
|
95
|
+
export default function Test() {
|
|
96
|
+
const props = { onChange: handleChange }
|
|
97
|
+
return <TextArea {...props} label="Notes" />
|
|
98
|
+
}
|
|
99
|
+
`.trim()
|
|
100
|
+
|
|
101
|
+
const result = applyTransform(input)
|
|
102
|
+
expect(result).toBe(null)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("should handle TextArea with alias import", () => {
|
|
106
|
+
const input = `
|
|
107
|
+
import { TextArea as MyTextArea } from "@planningcenter/tapestry-react"
|
|
108
|
+
|
|
109
|
+
export default function Test() {
|
|
110
|
+
const props = { onChange: handleChange }
|
|
111
|
+
return <MyTextArea {...props} label="Notes" />
|
|
112
|
+
}
|
|
113
|
+
`.trim()
|
|
114
|
+
|
|
115
|
+
const result = applyTransform(input)
|
|
116
|
+
expect(result).toContain(AUDIT_COMMENT)
|
|
117
|
+
expect(result).toContain("{...props}")
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("should return null when no TextArea imports exist", () => {
|
|
121
|
+
const input = `
|
|
122
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
123
|
+
|
|
124
|
+
export default function Test() {
|
|
125
|
+
const props = { onClick: handleClick }
|
|
126
|
+
return <Button {...props}>Save</Button>
|
|
127
|
+
}
|
|
128
|
+
`.trim()
|
|
129
|
+
|
|
130
|
+
const result = applyTransform(input)
|
|
131
|
+
expect(result).toBe(null)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("should return null for empty file", () => {
|
|
135
|
+
const result = applyTransform("")
|
|
136
|
+
expect(result).toBe(null)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
})
|