@planningcenter/tapestry-migration-cli 3.1.0-rc.2 → 3.1.0-rc.20

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 (115) hide show
  1. package/dist/tapestry-react-shim.cjs +7 -1
  2. package/package.json +3 -3
  3. package/src/components/button/transforms/convertStyleProps.test.ts +97 -0
  4. package/src/components/button/transforms/removeTypeButton.test.ts +0 -1
  5. package/src/components/checkbox/transforms/moveCheckboxImport.test.ts +3 -0
  6. package/src/components/input/index.ts +66 -0
  7. package/src/components/input/transformableInput.ts +49 -0
  8. package/src/components/input/transforms/auditSpreadProps.test.ts +192 -0
  9. package/src/components/input/transforms/auditSpreadProps.ts +26 -0
  10. package/src/components/input/transforms/autoWidthTransform.test.ts +172 -0
  11. package/src/components/input/transforms/autoWidthTransform.ts +41 -0
  12. package/src/components/input/transforms/convertStyleProps.test.ts +128 -0
  13. package/src/components/input/transforms/convertStyleProps.ts +12 -0
  14. package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.test.ts +186 -0
  15. package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.ts +27 -0
  16. package/src/components/input/transforms/inputLabelToLabelProp.test.ts +319 -0
  17. package/src/components/input/transforms/inputLabelToLabelProp.ts +203 -0
  18. package/src/components/input/transforms/mergeFieldIntoInput.test.ts +469 -0
  19. package/src/components/input/transforms/mergeFieldIntoInput.ts +7 -0
  20. package/src/components/input/transforms/mergeInputLabel.test.ts +458 -0
  21. package/src/components/input/transforms/mergeInputLabel.ts +204 -0
  22. package/src/components/input/transforms/moveInputImport.test.ts +166 -0
  23. package/src/components/input/transforms/moveInputImport.ts +14 -0
  24. package/src/components/input/transforms/numberFieldAddTypeNumber.test.ts +92 -0
  25. package/src/components/input/transforms/numberFieldAddTypeNumber.ts +14 -0
  26. package/src/components/input/transforms/numberFieldRenameToInput.test.ts +126 -0
  27. package/src/components/input/transforms/numberFieldRenameToInput.ts +9 -0
  28. package/src/components/input/transforms/removeAsInput.test.ts +139 -0
  29. package/src/components/input/transforms/removeAsInput.ts +20 -0
  30. package/src/components/input/transforms/removeDuplicateKeys.test.ts +302 -0
  31. package/src/components/input/transforms/removeDuplicateKeys.ts +10 -0
  32. package/src/components/input/transforms/removeInputBox.test.ts +352 -0
  33. package/src/components/input/transforms/removeInputBox.ts +109 -0
  34. package/src/components/input/transforms/removeRedundantAriaLabel.test.ts +128 -0
  35. package/src/components/input/transforms/removeRedundantAriaLabel.ts +21 -0
  36. package/src/components/input/transforms/removeTypeInput.test.ts +212 -0
  37. package/src/components/input/transforms/removeTypeInput.ts +22 -0
  38. package/src/components/input/transforms/removeTypeText.test.ts +160 -0
  39. package/src/components/input/transforms/removeTypeText.ts +17 -0
  40. package/src/components/input/transforms/sizeMapping.test.ts +198 -0
  41. package/src/components/input/transforms/sizeMapping.ts +17 -0
  42. package/src/components/input/transforms/skipRenderSideProps.test.ts +236 -0
  43. package/src/components/input/transforms/skipRenderSideProps.ts +27 -0
  44. package/src/components/input/transforms/stateToInvalid.test.ts +208 -0
  45. package/src/components/input/transforms/stateToInvalid.ts +59 -0
  46. package/src/components/input/transforms/stateToInvalidTernary.test.ts +159 -0
  47. package/src/components/input/transforms/stateToInvalidTernary.ts +13 -0
  48. package/src/components/input/transforms/unsupportedProps.test.ts +566 -0
  49. package/src/components/input/transforms/unsupportedProps.ts +84 -0
  50. package/src/components/link/transforms/reviewStyles.test.ts +0 -1
  51. package/src/components/select/index.ts +58 -0
  52. package/src/components/select/transformableSelect.ts +7 -0
  53. package/src/components/select/transforms/auditSpreadProps.test.ts +103 -0
  54. package/src/components/select/transforms/auditSpreadProps.ts +26 -0
  55. package/src/components/select/transforms/childrenToOptions.test.ts +367 -0
  56. package/src/components/select/transforms/childrenToOptions.ts +295 -0
  57. package/src/components/select/transforms/convertLegacyOptions.test.ts +150 -0
  58. package/src/components/select/transforms/convertLegacyOptions.ts +105 -0
  59. package/src/components/select/transforms/convertStyleProps.test.ts +73 -0
  60. package/src/components/select/transforms/convertStyleProps.ts +12 -0
  61. package/src/components/select/transforms/emptyValueToPlaceholder.test.ts +122 -0
  62. package/src/components/select/transforms/emptyValueToPlaceholder.ts +22 -0
  63. package/src/components/select/transforms/innerRefToRef.test.ts +89 -0
  64. package/src/components/select/transforms/innerRefToRef.ts +18 -0
  65. package/src/components/select/transforms/mapChildrenToOptions.test.ts +521 -0
  66. package/src/components/select/transforms/mapChildrenToOptions.ts +312 -0
  67. package/src/components/select/transforms/mergeFieldIntoSelect.test.ts +506 -0
  68. package/src/components/select/transforms/mergeFieldIntoSelect.ts +7 -0
  69. package/src/components/select/transforms/mergeSelectLabel.test.ts +458 -0
  70. package/src/components/select/transforms/mergeSelectLabel.ts +225 -0
  71. package/src/components/select/transforms/moveSelectImport.test.ts +148 -0
  72. package/src/components/select/transforms/moveSelectImport.ts +14 -0
  73. package/src/components/select/transforms/removeDefaultProps.test.ts +249 -0
  74. package/src/components/select/transforms/removeDefaultProps.ts +112 -0
  75. package/src/components/select/transforms/sizeMapping.test.ts +188 -0
  76. package/src/components/select/transforms/sizeMapping.ts +17 -0
  77. package/src/components/select/transforms/skipMultipleSelect.test.ts +148 -0
  78. package/src/components/select/transforms/skipMultipleSelect.ts +23 -0
  79. package/src/components/select/transforms/stateToInvalid.test.ts +217 -0
  80. package/src/components/select/transforms/stateToInvalid.ts +59 -0
  81. package/src/components/select/transforms/stateToInvalidTernary.test.ts +146 -0
  82. package/src/components/select/transforms/stateToInvalidTernary.ts +13 -0
  83. package/src/components/select/transforms/unsupportedProps.test.ts +252 -0
  84. package/src/components/select/transforms/unsupportedProps.ts +44 -0
  85. package/src/components/shared/helpers/getAttributeExpression.ts +26 -0
  86. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +102 -0
  87. package/src/components/shared/transformFactories/helpers/manageImports.ts +14 -12
  88. package/src/components/shared/transformFactories/mergeFieldFactory.ts +244 -0
  89. package/src/components/shared/transformFactories/sizeMappingFactory.ts +9 -2
  90. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +56 -17
  91. package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +65 -0
  92. package/src/components/text-area/index.ts +48 -0
  93. package/src/components/text-area/transforms/auditSpreadProps.test.ts +139 -0
  94. package/src/components/text-area/transforms/auditSpreadProps.ts +10 -0
  95. package/src/components/text-area/transforms/convertStyleProps.test.ts +158 -0
  96. package/src/components/text-area/transforms/convertStyleProps.ts +10 -0
  97. package/src/components/text-area/transforms/innerRefToRef.test.ts +206 -0
  98. package/src/components/text-area/transforms/innerRefToRef.ts +14 -0
  99. package/src/components/text-area/transforms/mergeFieldIntoTextArea.test.ts +477 -0
  100. package/src/components/text-area/transforms/mergeFieldIntoTextArea.ts +5 -0
  101. package/src/components/text-area/transforms/moveTextAreaImport.test.ts +168 -0
  102. package/src/components/text-area/transforms/moveTextAreaImport.ts +13 -0
  103. package/src/components/text-area/transforms/removeDuplicateKeys.test.ts +129 -0
  104. package/src/components/text-area/transforms/removeDuplicateKeys.ts +8 -0
  105. package/src/components/text-area/transforms/removeRedundantAriaLabel.test.ts +183 -0
  106. package/src/components/text-area/transforms/removeRedundantAriaLabel.ts +59 -0
  107. package/src/components/text-area/transforms/sizeMapping.test.ts +199 -0
  108. package/src/components/text-area/transforms/sizeMapping.ts +15 -0
  109. package/src/components/text-area/transforms/stateToInvalid.test.ts +204 -0
  110. package/src/components/text-area/transforms/stateToInvalid.ts +57 -0
  111. package/src/components/text-area/transforms/stateToInvalidTernary.test.ts +133 -0
  112. package/src/components/text-area/transforms/stateToInvalidTernary.ts +11 -0
  113. package/src/components/text-area/transforms/unsupportedProps.test.ts +275 -0
  114. package/src/components/text-area/transforms/unsupportedProps.ts +35 -0
  115. package/src/index.ts +2 -1
@@ -0,0 +1,252 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./unsupportedProps"
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("unsupportedProps transform", () => {
19
+ describe("unsupported prop detection", () => {
20
+ it("should add comment for css prop with specific message", () => {
21
+ const input = `
22
+ import { Select } from "@planningcenter/tapestry-react"
23
+
24
+ function Test() {
25
+ return <Select css={{ color: 'red' }} emptyValue="Pick" />
26
+ }
27
+ `.trim()
28
+
29
+ const result = applyTransform(input)
30
+ expect(result).toContain("TODO: tapestry-migration (css)")
31
+ expect(result).toContain(
32
+ "CSS prop is not supported. Use className or style prop instead."
33
+ )
34
+ })
35
+
36
+ it("should add comment for tooltip prop with specific message", () => {
37
+ const input = `
38
+ import { Select } from "@planningcenter/tapestry-react"
39
+
40
+ function Test() {
41
+ return <Select tooltip={{ content: "Help" }} emptyValue="Pick" />
42
+ }
43
+ `.trim()
44
+
45
+ const result = applyTransform(input)
46
+ expect(result).toContain("TODO: tapestry-migration (tooltip)")
47
+ expect(result).toContain("unsupported anti-pattern")
48
+ })
49
+
50
+ it("should skip Select with multiple prop (handled by skipMultipleSelect)", () => {
51
+ const input = `
52
+ import { Select } from "@planningcenter/tapestry-react"
53
+
54
+ function Test() {
55
+ return <Select multiple emptyValue="Pick" />
56
+ }
57
+ `.trim()
58
+
59
+ const result = applyTransform(input)
60
+ expect(result).not.toContain("TODO: tapestry-migration")
61
+ expect(result).toContain("multiple")
62
+ expect(result).toContain('emptyValue="Pick"')
63
+ })
64
+
65
+ it("should add comment for renderValue prop", () => {
66
+ const input = `
67
+ import { Select } from "@planningcenter/tapestry-react"
68
+
69
+ function Test() {
70
+ return <Select renderValue={(opts) => opts.map(o => o.label)} emptyValue="Pick" />
71
+ }
72
+ `.trim()
73
+
74
+ const result = applyTransform(input)
75
+ expect(result).toContain("TODO: tapestry-migration (renderValue)")
76
+ expect(result).toContain("renderValue is not supported")
77
+ })
78
+
79
+ it("should add comments for removed props", () => {
80
+ const input = `
81
+ import { Select } from "@planningcenter/tapestry-react"
82
+
83
+ function Test() {
84
+ return (
85
+ <Select
86
+ keepInView
87
+ lockScrollWhileOpen
88
+ popoverProps={{ maxHeight: 300 }}
89
+ renderTo="body"
90
+ placeholder="Pick"
91
+ options={[]}
92
+ label="Test"
93
+ />
94
+ )
95
+ }
96
+ `.trim()
97
+
98
+ const result = applyTransform(input)
99
+ expect(result).toContain("TODO: tapestry-migration (keepInView)")
100
+ expect(result).toContain("TODO: tapestry-migration (lockScrollWhileOpen)")
101
+ expect(result).toContain("TODO: tapestry-migration (popoverProps)")
102
+ expect(result).toContain("TODO: tapestry-migration (renderTo)")
103
+ })
104
+ })
105
+
106
+ describe("supported props", () => {
107
+ it("should not add comments for supported select props", () => {
108
+ const input = `
109
+ import { Select } from "@planningcenter/tapestry-react"
110
+
111
+ function Test() {
112
+ return (
113
+ <Select
114
+ disabled
115
+ name="select"
116
+ onChange={() => {}}
117
+ value="test"
118
+ placeholder="Pick one"
119
+ options={options}
120
+ />
121
+ )
122
+ }
123
+ `.trim()
124
+
125
+ const result = applyTransform(input)
126
+ expect(result).not.toContain("TODO: tapestry-migration")
127
+ })
128
+
129
+ it("should not add comments for common props", () => {
130
+ const input = `
131
+ import { Select } from "@planningcenter/tapestry-react"
132
+
133
+ function Test() {
134
+ return (
135
+ <Select
136
+ className="test"
137
+ id="select"
138
+ style={{ color: 'red' }}
139
+ tabIndex={0}
140
+ label="Test"
141
+ placeholder="Pick"
142
+ options={[]}
143
+ />
144
+ )
145
+ }
146
+ `.trim()
147
+
148
+ const result = applyTransform(input)
149
+ expect(result).not.toContain("TODO: tapestry-migration")
150
+ })
151
+
152
+ it("should not add comments for aria props", () => {
153
+ const input = `
154
+ import { Select } from "@planningcenter/tapestry-react"
155
+
156
+ function Test() {
157
+ return (
158
+ <Select
159
+ aria-label="Test select"
160
+ aria-describedby="description"
161
+ placeholder="Pick"
162
+ options={[]}
163
+ label="Test"
164
+ />
165
+ )
166
+ }
167
+ `.trim()
168
+
169
+ const result = applyTransform(input)
170
+ expect(result).not.toContain("TODO: tapestry-migration")
171
+ })
172
+
173
+ it("should not add comments for data props", () => {
174
+ const input = `
175
+ import { Select } from "@planningcenter/tapestry-react"
176
+
177
+ function Test() {
178
+ return (
179
+ <Select
180
+ data-testid="select"
181
+ data-cy="test-select"
182
+ placeholder="Pick"
183
+ options={[]}
184
+ label="Test"
185
+ />
186
+ )
187
+ }
188
+ `.trim()
189
+
190
+ const result = applyTransform(input)
191
+ expect(result).not.toContain("TODO: tapestry-migration")
192
+ })
193
+ })
194
+
195
+ describe("edge cases", () => {
196
+ it("should not affect Select without unsupported props", () => {
197
+ const input = `
198
+ import { Select } from "@planningcenter/tapestry-react"
199
+
200
+ function Test() {
201
+ return <Select label="Test" placeholder="Pick" options={[]} />
202
+ }
203
+ `.trim()
204
+
205
+ const result = applyTransform(input)
206
+ expect(result).toBe(input)
207
+ })
208
+
209
+ it("should not affect other components", () => {
210
+ const input = `
211
+ import { Button, Select } from "@planningcenter/tapestry-react"
212
+
213
+ function Test() {
214
+ return (
215
+ <div>
216
+ <Button css={{ color: 'red' }}>Click me</Button>
217
+ <Select label="Test" placeholder="Pick" options={[]} />
218
+ </div>
219
+ )
220
+ }
221
+ `.trim()
222
+
223
+ const result = applyTransform(input)
224
+ expect(result).not.toContain("TODO: tapestry-migration")
225
+ })
226
+
227
+ it("should handle mixed supported and unsupported props", () => {
228
+ const input = `
229
+ import { Select } from "@planningcenter/tapestry-react"
230
+
231
+ function Test() {
232
+ return (
233
+ <Select
234
+ disabled
235
+ css={{ color: 'red' }}
236
+ tooltip={{ content: "help" }}
237
+ label="Test"
238
+ placeholder="Pick"
239
+ options={[]}
240
+ />
241
+ )
242
+ }
243
+ `.trim()
244
+
245
+ const result = applyTransform(input)
246
+ expect(result).toContain("TODO: tapestry-migration (css)")
247
+ expect(result).toContain("TODO: tapestry-migration (tooltip)")
248
+ expect(result).toContain("disabled")
249
+ expect(result).toContain('label="Test"')
250
+ })
251
+ })
252
+ })
@@ -0,0 +1,44 @@
1
+ import { JSXAttribute, Transform } from "jscodeshift"
2
+
3
+ import { addCommentToUnsupportedProps } from "../../shared/actions/addCommentToUnsupportedProps"
4
+ import { SELECT_SUPPORTED_PROPS } from "../../shared/helpers/unsupportedPropsHelpers"
5
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
6
+ import { transformableSelect } from "../transformableSelect"
7
+
8
+ const transform: Transform = attributeTransformFactory({
9
+ condition: transformableSelect,
10
+ targetComponent: "Select",
11
+ targetPackage: "@planningcenter/tapestry-react",
12
+ transform: (element, { j }) => {
13
+ const UNSUPPORTED_PROPS = (element.openingElement.attributes || [])
14
+ .filter(
15
+ (attr) =>
16
+ attr.type === "JSXAttribute" &&
17
+ attr.name.type === "JSXIdentifier" &&
18
+ !SELECT_SUPPORTED_PROPS.includes(attr.name.name as string) &&
19
+ !(attr.name.name as string).startsWith("aria-") &&
20
+ !(attr.name.name as string).startsWith("data-")
21
+ )
22
+ .map((attr) => (attr as JSXAttribute).name.name as string)
23
+
24
+ return addCommentToUnsupportedProps({
25
+ element,
26
+ j,
27
+ messageSuffix: (prop) => {
28
+ if (prop === "css") {
29
+ return "\n * CSS prop is not supported. Use className or style prop instead.\n"
30
+ }
31
+ if (prop === "tooltip") {
32
+ return "\n * Wrapping a select in a tooltip is an unsupported anti-pattern.\n"
33
+ }
34
+ if (prop === "renderValue") {
35
+ return "\n * renderValue is not supported. The select displays the selected option label.\n"
36
+ }
37
+ return ""
38
+ },
39
+ props: UNSUPPORTED_PROPS,
40
+ })
41
+ },
42
+ })
43
+
44
+ export default transform
@@ -0,0 +1,26 @@
1
+ import { Expression, JSXElement } from "jscodeshift"
2
+
3
+ /**
4
+ * Gets the expression AST node for a JSX attribute value.
5
+ * Returns the expression node for dynamic values, or a StringLiteral for static ones.
6
+ */
7
+ export function getAttributeExpression(
8
+ element: JSXElement,
9
+ attrName: string
10
+ ): Expression | null {
11
+ const attrs = element.openingElement.attributes || []
12
+ for (const attr of attrs) {
13
+ if (attr.type !== "JSXAttribute") continue
14
+ if (attr.name.name !== attrName) continue
15
+
16
+ if (!attr.value) return null // boolean shorthand has no expression
17
+ if (attr.value.type === "StringLiteral") return attr.value
18
+ if (attr.value.type === "JSXExpressionContainer") {
19
+ const expr = attr.value.expression
20
+ if (expr.type === "JSXEmptyExpression") return null
21
+ return expr as Expression
22
+ }
23
+ return null
24
+ }
25
+ return null
26
+ }
@@ -49,3 +49,105 @@ export const CHECKBOX_RADIO_SUPPORTED_PROPS = [
49
49
  ...CHECKBOX_RADIO_SHARED_PROPS,
50
50
  ...STYLE_PROP_NAMES_WITHOUT_CSS,
51
51
  ]
52
+
53
+ export const INPUT_SPECIFIC_PROPS = [
54
+ "autoComplete",
55
+ "autoWidth",
56
+ "defaultValue",
57
+ "description",
58
+ "disabled",
59
+ "form",
60
+ "hideLabel",
61
+ "invalid",
62
+ "max",
63
+ "maxLength",
64
+ "min",
65
+ "minLength",
66
+ "name",
67
+ "onChange",
68
+ "placeholder",
69
+ "readOnly",
70
+ "required",
71
+ "selectTextOnFocus",
72
+ "spellCheck",
73
+ "type",
74
+ "value",
75
+ ]
76
+
77
+ export const ACCEPTED_INPUT_TYPES = [
78
+ "email",
79
+ "number",
80
+ "password",
81
+ "search",
82
+ "tel",
83
+ "text",
84
+ "url",
85
+ ] as const
86
+
87
+ // Per https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#attributes
88
+ // Only covers the types accepted by the Tapestry Input component.
89
+ export const TYPE_SPECIFIC_PROPS: Record<string, string[]> = {
90
+ email: ["dirname", "list", "multiple", "pattern"],
91
+ number: ["inputMode", "list", "step"],
92
+ password: ["pattern"],
93
+ search: ["dirname", "inputMode", "list", "pattern"],
94
+ tel: ["dirname", "inputMode", "list", "pattern"],
95
+ text: ["dirname", "inputMode", "list", "pattern"],
96
+ url: ["dirname", "inputMode", "list", "pattern"],
97
+ }
98
+
99
+ export const INPUT_SUPPORTED_PROPS = [...COMMON_PROPS, ...INPUT_SPECIFIC_PROPS]
100
+
101
+ export const SELECT_SPECIFIC_PROPS = [
102
+ "complex",
103
+ "defaultValue",
104
+ "description",
105
+ "disabled",
106
+ "form",
107
+ "invalid",
108
+ "name",
109
+ "onChange",
110
+ "options",
111
+ "placeholder",
112
+ "required",
113
+ "value",
114
+ ]
115
+
116
+ export const SELECT_SUPPORTED_PROPS = [
117
+ ...COMMON_PROPS,
118
+ ...SELECT_SPECIFIC_PROPS,
119
+ ...STYLE_PROP_NAMES_WITHOUT_CSS,
120
+ ]
121
+
122
+ export const TEXTAREA_SPECIFIC_PROPS = [
123
+ "autoComplete",
124
+ "autoFocus",
125
+ "cols",
126
+ "defaultValue",
127
+ "description",
128
+ "disabled",
129
+ "dirname",
130
+ "form",
131
+ "hideLabel",
132
+ "invalid",
133
+ "maxLength",
134
+ "minLength",
135
+ "name",
136
+ "onChange",
137
+ "onInput",
138
+ "onSelect",
139
+ "placeholder",
140
+ "readOnly",
141
+ "required",
142
+ "resize",
143
+ "rows",
144
+ "spellCheck",
145
+ "value",
146
+ "wrap",
147
+ ]
148
+
149
+ export const TEXTAREA_SUPPORTED_PROPS = [
150
+ ...COMMON_PROPS,
151
+ ...TEXTAREA_SPECIFIC_PROPS,
152
+ ...STYLE_PROP_NAMES_WITHOUT_CSS,
153
+ ]
@@ -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,