@planningcenter/tapestry-migration-cli 3.1.0-rc.5 → 3.1.0-rc.7

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 (52) hide show
  1. package/package.json +3 -3
  2. package/src/components/button/transforms/convertStyleProps.test.ts +97 -0
  3. package/src/components/button/transforms/removeTypeButton.test.ts +0 -1
  4. package/src/components/checkbox/transforms/moveCheckboxImport.test.ts +3 -0
  5. package/src/components/input/index.ts +66 -0
  6. package/src/components/input/transformableInput.ts +8 -0
  7. package/src/components/input/transforms/auditSpreadProps.test.ts +192 -0
  8. package/src/components/input/transforms/auditSpreadProps.ts +26 -0
  9. package/src/components/input/transforms/autoWidthTransform.test.ts +172 -0
  10. package/src/components/input/transforms/autoWidthTransform.ts +41 -0
  11. package/src/components/input/transforms/convertStyleProps.test.ts +128 -0
  12. package/src/components/input/transforms/convertStyleProps.ts +12 -0
  13. package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.test.ts +186 -0
  14. package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.ts +27 -0
  15. package/src/components/input/transforms/inputLabelToLabelProp.test.ts +319 -0
  16. package/src/components/input/transforms/inputLabelToLabelProp.ts +203 -0
  17. package/src/components/input/transforms/mergeFieldIntoInput.test.ts +391 -0
  18. package/src/components/input/transforms/mergeFieldIntoInput.ts +213 -0
  19. package/src/components/input/transforms/mergeInputLabel.test.ts +458 -0
  20. package/src/components/input/transforms/mergeInputLabel.ts +204 -0
  21. package/src/components/input/transforms/moveInputImport.test.ts +166 -0
  22. package/src/components/input/transforms/moveInputImport.ts +14 -0
  23. package/src/components/input/transforms/numberFieldAddTypeNumber.test.ts +92 -0
  24. package/src/components/input/transforms/numberFieldAddTypeNumber.ts +14 -0
  25. package/src/components/input/transforms/numberFieldRenameToInput.test.ts +126 -0
  26. package/src/components/input/transforms/numberFieldRenameToInput.ts +9 -0
  27. package/src/components/input/transforms/removeAsInput.test.ts +139 -0
  28. package/src/components/input/transforms/removeAsInput.ts +20 -0
  29. package/src/components/input/transforms/removeDuplicateKeys.test.ts +302 -0
  30. package/src/components/input/transforms/removeDuplicateKeys.ts +10 -0
  31. package/src/components/input/transforms/removeInputBox.test.ts +352 -0
  32. package/src/components/input/transforms/removeInputBox.ts +109 -0
  33. package/src/components/input/transforms/removeRedundantAriaLabel.test.ts +128 -0
  34. package/src/components/input/transforms/removeRedundantAriaLabel.ts +21 -0
  35. package/src/components/input/transforms/removeTypeText.test.ts +160 -0
  36. package/src/components/input/transforms/removeTypeText.ts +18 -0
  37. package/src/components/input/transforms/sizeMapping.test.ts +198 -0
  38. package/src/components/input/transforms/sizeMapping.ts +17 -0
  39. package/src/components/input/transforms/skipRenderSideProps.test.ts +236 -0
  40. package/src/components/input/transforms/skipRenderSideProps.ts +27 -0
  41. package/src/components/input/transforms/stateToInvalid.test.ts +208 -0
  42. package/src/components/input/transforms/stateToInvalid.ts +59 -0
  43. package/src/components/input/transforms/stateToInvalidTernary.test.ts +159 -0
  44. package/src/components/input/transforms/stateToInvalidTernary.ts +13 -0
  45. package/src/components/input/transforms/unsupportedProps.test.ts +566 -0
  46. package/src/components/input/transforms/unsupportedProps.ts +84 -0
  47. package/src/components/link/transforms/reviewStyles.test.ts +0 -1
  48. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +52 -0
  49. package/src/components/shared/transformFactories/helpers/manageImports.ts +14 -12
  50. package/src/components/shared/transformFactories/sizeMappingFactory.ts +9 -2
  51. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +54 -16
  52. package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +65 -0
@@ -136,7 +136,6 @@ export function removeImportFromDeclaration(
136
136
  if (importIndex >= 0) {
137
137
  specifiers.splice(importIndex, 1)
138
138
 
139
- // Remove entire import if no specifiers left
140
139
  if (specifiers.length === 0) {
141
140
  importPath.get().prune()
142
141
  }
@@ -162,19 +161,23 @@ export function addImport({
162
161
  pkg: string
163
162
  source: Collection
164
163
  }): string {
165
- // Check if component is already imported from the target package
166
164
  const existingImportName = getImportName(component, pkg, { j, source })
167
165
  if (existingImportName) {
168
166
  return existingImportName
169
167
  }
170
168
 
171
- // Check for conflicts with imports from other packages
172
169
  const hasConflict = hasConflictingImport(component, pkg, { j, source })
173
170
  const finalComponentName = hasConflict ? conflictAlias : component
174
171
  const alias =
175
172
  finalComponentName !== component ? finalComponentName : undefined
176
173
 
177
- // Handle target package import addition
174
+ // If there's a conflict but no meaningful alias (conflictAlias === component), skip
175
+ // adding the import to avoid duplicate identifiers. The conflicting import from another
176
+ // package will be migrated to the target package by another transform in the pipeline.
177
+ if (hasConflict && !alias) {
178
+ return finalComponentName
179
+ }
180
+
178
181
  const targetImport = source
179
182
  .find(j.ImportDeclaration, {
180
183
  source: { value: pkg },
@@ -223,18 +226,17 @@ export function manageImports(
223
226
  name: { name: sourceComponentName },
224
227
  }).length > 0
225
228
 
226
- // Handle source package import cleanup
227
- const sourceImport = source
228
- .find(j.ImportDeclaration, {
229
+ // Handle source package import cleanup — search all declarations, not just the first,
230
+ // since a component may be in a separate import declaration from the same package.
231
+ if (!stillUsesSource) {
232
+ const sourceImports = source.find(j.ImportDeclaration, {
229
233
  source: { value: config.fromPackage },
230
234
  })
231
- .at(0)
232
-
233
- if (sourceImport.length > 0 && !stillUsesSource) {
234
- removeImportFromDeclaration(sourceImport, config.fromComponent)
235
+ for (let i = 0; i < sourceImports.length; i++) {
236
+ removeImportFromDeclaration(sourceImports.at(i), config.fromComponent)
237
+ }
235
238
  }
236
239
 
237
- // Handle target package import addition using addImport
238
240
  addImport({
239
241
  component: config.toComponent,
240
242
  conflictAlias: config.conflictAlias || targetComponentName,
@@ -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: hasAttribute("size"),
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,
@@ -187,11 +179,9 @@ function applyStylesToComponent({
187
179
  value.startsWith("{") &&
188
180
  value.endsWith("}")
189
181
  ) {
190
- // This is a complex expression wrapped in braces - parse it as JS
191
- const expressionCode = value.slice(1, -1) // Remove surrounding braces
182
+ const expressionCode = value.slice(1, -1)
192
183
  try {
193
184
  const parsed = j(expressionCode)
194
- // Get the first expression from the program body
195
185
  const firstStatement = parsed.find(j.Program).get("body", 0).value
196
186
  if (firstStatement?.type === "ExpressionStatement") {
197
187
  valueNode = firstStatement.expression
@@ -199,7 +189,6 @@ function applyStylesToComponent({
199
189
  valueNode = j.stringLiteral(value)
200
190
  }
201
191
  } catch {
202
- // If parsing fails, fall back to string literal
203
192
  valueNode = j.stringLiteral(value)
204
193
  }
205
194
  } else if (typeof value === "string") {
@@ -214,8 +203,56 @@ function applyStylesToComponent({
214
203
  )
215
204
 
216
205
  if (styleAttr && styleAttr.type === "JSXAttribute") {
217
- // TODO: ensure this adds to exisiting style object rather than replacing
218
- styleAttr.value = styleValue
206
+ const existingValue = styleAttr.value
207
+ if (
208
+ existingValue?.type === "JSXExpressionContainer" &&
209
+ existingValue.expression.type === "ObjectExpression"
210
+ ) {
211
+ // Case 1: Existing style is a static object literal — merge, new props win
212
+ const newProps = (
213
+ styleValue.expression as ReturnType<typeof j.objectExpression>
214
+ ).properties
215
+ const newKeys = new Set(
216
+ newProps
217
+ .filter((p) => p.type === "ObjectProperty")
218
+ .map((p) => {
219
+ const prop = p as ReturnType<typeof j.objectProperty>
220
+ if (prop.key.type === "Identifier") return prop.key.name
221
+ if (prop.key.type === "StringLiteral") return prop.key.value
222
+ return null
223
+ })
224
+ .filter(Boolean)
225
+ )
226
+ const survivingExisting = existingValue.expression.properties.filter(
227
+ (p) => {
228
+ if (p.type === "SpreadElement" || p.type === "RestElement")
229
+ return true
230
+ const prop = p as ReturnType<typeof j.objectProperty>
231
+ if (prop.key.type === "Identifier") return !newKeys.has(prop.key.name)
232
+ if (prop.key.type === "StringLiteral")
233
+ return !newKeys.has(prop.key.value)
234
+ return true
235
+ }
236
+ )
237
+ const mergedObject = j.objectExpression([
238
+ ...survivingExisting,
239
+ ...newProps,
240
+ ])
241
+ styleAttr.value = j.jsxExpressionContainer(mergedObject)
242
+ } else if (
243
+ existingValue?.type === "JSXExpressionContainer" &&
244
+ existingValue.expression.type !== "JSXEmptyExpression"
245
+ ) {
246
+ // Case 2: Dynamic expression — merge via spread: { ...existing, ...computed }
247
+ const newProps = (
248
+ styleValue.expression as ReturnType<typeof j.objectExpression>
249
+ ).properties
250
+ const mergedObject = j.objectExpression([
251
+ j.spreadElement(existingValue.expression),
252
+ ...newProps,
253
+ ])
254
+ styleAttr.value = j.jsxExpressionContainer(mergedObject)
255
+ }
219
256
  } else {
220
257
  const styleAttr = j.jsxAttribute(j.jsxIdentifier("style"), styleValue)
221
258
 
@@ -225,6 +262,7 @@ function applyStylesToComponent({
225
262
  }
226
263
 
227
264
  export function stylePropTransformFactory(config: {
265
+ condition?: TransformCondition
228
266
  plugin?: {
229
267
  getStyles: (props: Record<string, unknown>) => Record<string, unknown>
230
268
  styleProps: string[]
@@ -321,7 +359,6 @@ export function stylePropTransformFactory(config: {
321
359
  styles = { ...styles, ...directStyleProps }
322
360
  if (options.verbose) console.log("Final generated styles:", styles)
323
361
 
324
- // Only apply styles if there are actual CSS properties to add
325
362
  if (Object.keys(styles).length > 0) {
326
363
  applyStylesToComponent({ element, j, styles })
327
364
 
@@ -354,6 +391,7 @@ export function stylePropTransformFactory(config: {
354
391
  }
355
392
 
356
393
  return attributeTransformFactory({
394
+ condition: config.condition,
357
395
  targetComponent: config.targetComponent,
358
396
  targetPackage: config.targetPackage,
359
397
  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
+ }