@planningcenter/tapestry-migration-cli 3.3.2-qa-891.0 → 3.4.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 (25) hide show
  1. package/dist/tapestry-react-shim.cjs +17 -8
  2. package/package.json +3 -3
  3. package/src/components/button/transforms/childrenToLabel.ts +10 -3
  4. package/src/components/button/transforms/iconToIconButton.ts +0 -1
  5. package/src/components/button/transforms/moveButtonImport.ts +0 -1
  6. package/src/components/checkbox/transforms/childrenToLabel.ts +10 -3
  7. package/src/components/checkbox/transforms/moveCheckboxImport.ts +0 -1
  8. package/src/components/input/transforms/moveInputImport.ts +0 -1
  9. package/src/components/input/transforms/numberFieldRenameToInput.ts +0 -1
  10. package/src/components/link/index.ts +0 -2
  11. package/src/components/link/transforms/childrenToLabel.test.ts +36 -3
  12. package/src/components/link/transforms/childrenToLabel.ts +10 -3
  13. package/src/components/link/transforms/moveLinkImport.ts +0 -1
  14. package/src/components/link/transforms/unsupportedProps.ts +2 -1
  15. package/src/components/radio/transforms/moveRadioImport.ts +0 -1
  16. package/src/components/select/transforms/moveSelectImport.ts +0 -1
  17. package/src/components/shared/actions/addAttribute.ts +18 -2
  18. package/src/components/shared/actions/createWrapper.ts +2 -1
  19. package/src/components/shared/helpers/childrenToLabelHelpers.ts +24 -1
  20. package/src/components/shared/transformFactories/helpers/addImport.test.ts +45 -0
  21. package/src/components/shared/transformFactories/helpers/manageImports.ts +4 -2
  22. package/src/components/text-area/transforms/moveTextAreaImport.ts +0 -1
  23. package/src/components/toggle-switch/transforms/moveToggleSwitchImport.ts +0 -1
  24. package/src/components/link/transforms/targetBlankToExternal.test.ts +0 -221
  25. package/src/components/link/transforms/targetBlankToExternal.ts +0 -39
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "3.3.2-qa-891.0",
3
+ "version": "3.4.0",
4
4
  "description": "CLI tool for Tapestry migrations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@emotion/react": "^11.14.0",
33
- "@planningcenter/tapestry": "^3.3.2-qa-891.0",
33
+ "@planningcenter/tapestry": "^3.4.0",
34
34
  "@planningcenter/tapestry-react": "^4.11.5",
35
35
  "@types/jscodeshift": "^17.3.0",
36
36
  "@types/node": "^20.0.0",
@@ -50,5 +50,5 @@
50
50
  "publishConfig": {
51
51
  "access": "public"
52
52
  },
53
- "gitHead": "adf34ae1e2173b33f65f8eac478dc2943d313c05"
53
+ "gitHead": "0a541a2444b2d89e7fab714a3c6772336479b0fd"
54
54
  }
@@ -27,10 +27,17 @@ const transform: Transform = attributeTransformFactory({
27
27
  return true
28
28
  }
29
29
 
30
- const { isSimpleText, textContent } = extractTextContent(element.children!)
30
+ const { expression, isSimpleText, textContent } = extractTextContent(
31
+ element.children!
32
+ )
31
33
 
32
- if (isSimpleText && textContent) {
33
- addAttribute({ element, j, name: "label", value: textContent })
34
+ if (expression || (isSimpleText && textContent)) {
35
+ addAttribute({
36
+ element,
37
+ j,
38
+ name: "label",
39
+ value: expression ?? textContent,
40
+ })
34
41
  removeChildren(element)
35
42
 
36
43
  if (options?.verbose) {
@@ -21,7 +21,6 @@ const transform = attributeTransformFactory({
21
21
  })
22
22
  const iconButtonName = addImport({
23
23
  component: "IconButton",
24
- conflictAlias: "TIconButton",
25
24
  j,
26
25
  pkg: "@planningcenter/tapestry",
27
26
  source,
@@ -4,7 +4,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
4
4
 
5
5
  const transform: Transform = componentTransformFactory({
6
6
  condition: () => true,
7
- conflictAlias: "TButton",
8
7
  fromComponent: "Button",
9
8
  fromPackage: "@planningcenter/tapestry-react",
10
9
  toComponent: "Button",
@@ -27,10 +27,17 @@ const transform: Transform = attributeTransformFactory({
27
27
  return true
28
28
  }
29
29
 
30
- const { isSimpleText, textContent } = extractTextContent(element.children!)
30
+ const { expression, isSimpleText, textContent } = extractTextContent(
31
+ element.children!
32
+ )
31
33
 
32
- if (isSimpleText && textContent) {
33
- addAttribute({ element, j, name: "label", value: textContent })
34
+ if (expression || (isSimpleText && textContent)) {
35
+ addAttribute({
36
+ element,
37
+ j,
38
+ name: "label",
39
+ value: expression ?? textContent,
40
+ })
34
41
  removeChildren(element)
35
42
 
36
43
  if (options?.verbose) {
@@ -4,7 +4,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
4
4
 
5
5
  const transform: Transform = componentTransformFactory({
6
6
  condition: () => true,
7
- conflictAlias: "TCheckbox",
8
7
  fromComponent: "Checkbox",
9
8
  fromPackage: "@planningcenter/tapestry-react",
10
9
  toComponent: "Checkbox",
@@ -5,7 +5,6 @@ import { transformableInput } from "../transformableInput"
5
5
 
6
6
  const transform: Transform = componentTransformFactory({
7
7
  condition: transformableInput,
8
- conflictAlias: "TInput",
9
8
  fromComponent: "Input",
10
9
  fromPackage: "@planningcenter/tapestry-react",
11
10
  toComponent: "Input",
@@ -2,7 +2,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
2
2
 
3
3
  export default componentTransformFactory({
4
4
  condition: () => true,
5
- conflictAlias: "TInput",
6
5
  fromComponent: "NumberField",
7
6
  fromPackage: "@planningcenter/tapestry-react",
8
7
  toComponent: "Input",
@@ -9,7 +9,6 @@ import removeAs from "./transforms/removeAs"
9
9
  import removeInlineMember from "./transforms/removeInlineMember"
10
10
  import removeInlineProp from "./transforms/removeInlineProp"
11
11
  import reviewStyles from "./transforms/reviewStyles"
12
- import targetBlankToExternal from "./transforms/targetBlankToExternal"
13
12
  import tooltipToWrapper from "./transforms/tooltipToWrapper"
14
13
  import toToHref from "./transforms/toToHref"
15
14
  import unsupportedProps from "./transforms/unsupportedProps"
@@ -23,7 +22,6 @@ const transform: Transform = (fileInfo, api, options) => {
23
22
  removeInlineProp,
24
23
  tooltipToWrapper,
25
24
  toToHref,
26
- targetBlankToExternal,
27
25
  innerRefToRef,
28
26
  removeAs,
29
27
  childrenToLabel,
@@ -81,6 +81,39 @@ function Test() {
81
81
  const result = applyTransform(input)
82
82
  expect(result).toContain('<Link href="/help" label="GetHelpNow" />')
83
83
  })
84
+
85
+ it("should convert a lone identifier expression to label prop", () => {
86
+ const input = `
87
+ import { Link } from "@planningcenter/tapestry-react"
88
+
89
+ function Test({ registration_form_url }) {
90
+ return (
91
+ <Link external href={registration_form_url}>
92
+ {registration_form_url}
93
+ </Link>
94
+ )
95
+ }
96
+ `.trim()
97
+
98
+ const result = applyTransform(input)
99
+ expect(result).toContain("label={registration_form_url}")
100
+ expect(result).not.toContain("complex children")
101
+ expect(result).not.toContain(">\n")
102
+ })
103
+
104
+ it("should convert a lone member expression to label prop", () => {
105
+ const input = `
106
+ import { Link } from "@planningcenter/tapestry-react"
107
+
108
+ function Test({ data }) {
109
+ return <Link href={data.href}>{data.label}</Link>
110
+ }
111
+ `.trim()
112
+
113
+ const result = applyTransform(input)
114
+ expect(result).toContain("label={data.label}")
115
+ expect(result).not.toContain("complex children")
116
+ })
84
117
  })
85
118
 
86
119
  describe("complex children scenarios", () => {
@@ -116,12 +149,12 @@ function Test() {
116
149
  expect(result).toContain("</Link>")
117
150
  })
118
151
 
119
- it("should add comment for expression children", () => {
152
+ it("should add comment for conditional expression children", () => {
120
153
  const input = `
121
154
  import { Link } from "@planningcenter/tapestry-react"
122
155
 
123
- function Test({ linkText }) {
124
- return <Link href="/home">{linkText}</Link>
156
+ function Test({ isActive }) {
157
+ return <Link href="/home">{isActive ? "Active" : "Inactive"}</Link>
125
158
  }
126
159
  `.trim()
127
160
 
@@ -27,10 +27,17 @@ const transform: Transform = attributeTransformFactory({
27
27
  return true
28
28
  }
29
29
 
30
- const { isSimpleText, textContent } = extractTextContent(element.children!)
30
+ const { expression, isSimpleText, textContent } = extractTextContent(
31
+ element.children!
32
+ )
31
33
 
32
- if (isSimpleText && textContent) {
33
- addAttribute({ element, j, name: "label", value: textContent })
34
+ if (expression || (isSimpleText && textContent)) {
35
+ addAttribute({
36
+ element,
37
+ j,
38
+ name: "label",
39
+ value: expression ?? textContent,
40
+ })
34
41
  removeChildren(element)
35
42
 
36
43
  if (options?.verbose) {
@@ -4,7 +4,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
4
4
 
5
5
  const transform: Transform = componentTransformFactory({
6
6
  condition: () => true,
7
- conflictAlias: "TLink",
8
7
  fromComponent: "Link",
9
8
  fromPackage: "@planningcenter/tapestry-react",
10
9
  toComponent: "Link",
@@ -1,7 +1,6 @@
1
1
  import { BUTTON_LINK_SUPPORTED_PROPS } from "../../shared/helpers/unsupportedPropsHelpers"
2
2
  import { unsupportedPropsFactory } from "../../shared/transformFactories/unsupportedPropsFactory"
3
3
 
4
- // Note: 'target' and 'rel' are NOT included because they are handled by targetBlankToExternal transform
5
4
  const LINK_SPECIFIC_PROPS = [
6
5
  "download",
7
6
  "external",
@@ -10,6 +9,8 @@ const LINK_SPECIFIC_PROPS = [
10
9
  "media",
11
10
  "ping",
12
11
  "referrerPolicy",
12
+ "rel",
13
+ "target",
13
14
  "type",
14
15
  ]
15
16
 
@@ -4,7 +4,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
4
4
 
5
5
  const transform: Transform = componentTransformFactory({
6
6
  condition: () => true,
7
- conflictAlias: "TRadio",
8
7
  fromComponent: "Radio",
9
8
  fromPackage: "@planningcenter/tapestry-react",
10
9
  toComponent: "Radio",
@@ -5,7 +5,6 @@ import { transformableSelect } from "../transformableSelect"
5
5
 
6
6
  const transform: Transform = componentTransformFactory({
7
7
  condition: transformableSelect,
8
- conflictAlias: "TSelect",
9
8
  fromComponent: "Select",
10
9
  fromPackage: "@planningcenter/tapestry-react",
11
10
  toComponent: "Select",
@@ -1,4 +1,4 @@
1
- import { JSCodeshift, JSXElement } from "jscodeshift"
1
+ import { JSCodeshift, JSXElement, JSXExpressionContainer } from "jscodeshift"
2
2
 
3
3
  export interface Conditional {
4
4
  alternate: string
@@ -6,6 +6,14 @@ export interface Conditional {
6
6
  test: string
7
7
  }
8
8
 
9
+ type ExpressionNode = JSXExpressionContainer["expression"]
10
+
11
+ function isExpressionNode(
12
+ value: string | boolean | Conditional | ExpressionNode
13
+ ): value is ExpressionNode {
14
+ return typeof value === "object" && value !== null && "type" in value
15
+ }
16
+
9
17
  function formatValue(value: string | boolean, j: JSCodeshift) {
10
18
  if (typeof value === "string") {
11
19
  return j.stringLiteral(value)
@@ -27,11 +35,19 @@ export function addAttribute({
27
35
  element: JSXElement
28
36
  j: JSCodeshift
29
37
  name: string
30
- value: string | boolean | Conditional | null
38
+ value: string | boolean | Conditional | ExpressionNode | null
31
39
  }) {
32
40
  if (value === null) return
33
41
  const attributes = element.openingElement.attributes || []
34
42
 
43
+ // An AST expression node is wrapped in braces, e.g. label={url}
44
+ if (isExpressionNode(value)) {
45
+ attributes.push(
46
+ j.jsxAttribute(j.jsxIdentifier(name), j.jsxExpressionContainer(value))
47
+ )
48
+ return
49
+ }
50
+
35
51
  if (
36
52
  typeof value === "object" &&
37
53
  "test" in value &&
@@ -18,7 +18,8 @@ export function createWrapper({
18
18
  wrapperPackage,
19
19
  wrapperProps = [],
20
20
  }: {
21
- conflictAlias: string
21
+ /** Alias to use if the wrapper conflicts with an existing import. Defaults to `T${wrapperName}`. */
22
+ conflictAlias?: string
22
23
  element: JSXElement
23
24
  j: JSCodeshift
24
25
  source: Collection
@@ -1,11 +1,34 @@
1
- import { JSXElement } from "jscodeshift"
1
+ import { JSXElement, JSXExpressionContainer } from "jscodeshift"
2
+
3
+ type LabelExpression = JSXExpressionContainer["expression"]
2
4
 
3
5
  export function extractTextContent(
4
6
  children: NonNullable<JSXElement["children"]>
5
7
  ): {
8
+ expression?: LabelExpression
6
9
  isSimpleText: boolean
7
10
  textContent: string
8
11
  } {
12
+ const meaningfulChildren = children.filter(
13
+ (child) => !(child.type === "JSXText" && !child.value.trim())
14
+ )
15
+
16
+ // A lone expression that simply references a value (e.g. {url} or {data.href})
17
+ // is not "complex" — it can be passed straight through to the label prop.
18
+ const onlyChild = meaningfulChildren[0]
19
+ if (
20
+ meaningfulChildren.length === 1 &&
21
+ onlyChild.type === "JSXExpressionContainer" &&
22
+ (onlyChild.expression.type === "Identifier" ||
23
+ onlyChild.expression.type === "MemberExpression")
24
+ ) {
25
+ return {
26
+ expression: onlyChild.expression,
27
+ isSimpleText: false,
28
+ textContent: "",
29
+ }
30
+ }
31
+
9
32
  let textContent = ""
10
33
 
11
34
  for (const child of children) {
@@ -254,6 +254,51 @@ describe("addImport", () => {
254
254
  expect(tapestryImport.at(0).get().value.specifiers).toHaveLength(1)
255
255
  })
256
256
 
257
+ it("should default conflictAlias to `T${component}` when none is provided and a conflict exists", () => {
258
+ const source = createSource(`
259
+ import React from "react"
260
+ import { Button } from "@some/other-package"
261
+ `)
262
+
263
+ const result = addImport({
264
+ component: "Button",
265
+ j,
266
+ pkg: "@planningcenter/tapestry",
267
+ source,
268
+ })
269
+
270
+ expect(result).toBe("TButton")
271
+
272
+ const tapestryImport = source.find(j.ImportDeclaration, {
273
+ source: { value: "@planningcenter/tapestry" },
274
+ })
275
+ expect(tapestryImport).toHaveLength(1)
276
+
277
+ const buttonSpec = tapestryImport.at(0).get().value.specifiers?.[0]
278
+ expect(buttonSpec?.imported?.name).toBe("Button")
279
+ expect(buttonSpec?.local?.name).toBe("TButton")
280
+ })
281
+
282
+ it("should not alias when no conflict exists even without a conflictAlias", () => {
283
+ const source = createSource('import React from "react"')
284
+
285
+ const result = addImport({
286
+ component: "Button",
287
+ j,
288
+ pkg: "@planningcenter/tapestry",
289
+ source,
290
+ })
291
+
292
+ expect(result).toBe("Button")
293
+
294
+ const tapestryImport = source.find(j.ImportDeclaration, {
295
+ source: { value: "@planningcenter/tapestry" },
296
+ })
297
+ const buttonSpec = tapestryImport.at(0).get().value.specifiers?.[0]
298
+ expect(buttonSpec?.imported?.name).toBe("Button")
299
+ expect(buttonSpec?.local).toBeNull()
300
+ })
301
+
257
302
  it("should not use conflictAlias when no conflict exists", () => {
258
303
  const source = createSource('import React from "react"')
259
304
 
@@ -157,7 +157,8 @@ export function addImport({
157
157
  source,
158
158
  }: {
159
159
  component: string
160
- conflictAlias: string
160
+ /** Alias to use if the target component conflicts with an existing import. Defaults to `T${component}`. */
161
+ conflictAlias?: string
161
162
  fromPackage?: string
162
163
  j: JSCodeshift
163
164
  pkg: string
@@ -190,7 +191,8 @@ export function addImport({
190
191
  return component
191
192
  }
192
193
 
193
- const finalComponentName = hasConflict ? conflictAlias : component
194
+ const resolvedConflictAlias = conflictAlias ?? `T${component}`
195
+ const finalComponentName = hasConflict ? resolvedConflictAlias : component
194
196
  const alias =
195
197
  finalComponentName !== component ? finalComponentName : undefined
196
198
 
@@ -4,7 +4,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
4
4
 
5
5
  const transform: Transform = componentTransformFactory({
6
6
  condition: () => true,
7
- conflictAlias: "TTextArea",
8
7
  fromComponent: "TextArea",
9
8
  fromPackage: "@planningcenter/tapestry-react",
10
9
  toComponent: "TextArea",
@@ -4,7 +4,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
4
4
 
5
5
  const transform: Transform = componentTransformFactory({
6
6
  condition: () => true,
7
- conflictAlias: "TToggleSwitch",
8
7
  fromComponent: "ToggleSwitch",
9
8
  fromPackage: "@planningcenter/tapestry-react",
10
9
  toComponent: "ToggleSwitch",
@@ -1,221 +0,0 @@
1
- import jscodeshift from "jscodeshift"
2
- import { describe, expect, it } from "vitest"
3
-
4
- import targetBlankToExternal from "./targetBlankToExternal"
5
-
6
- const j = jscodeshift.withParser("tsx")
7
-
8
- function applyTransform(source: string, verbose = false): string | null {
9
- return targetBlankToExternal(
10
- { path: "test.tsx", source },
11
- { j, jscodeshift: j, report: () => {}, stats: () => {} },
12
- { verbose }
13
- ) as string | null
14
- }
15
-
16
- describe("targetBlankToExternal transform", () => {
17
- describe("basic transformations", () => {
18
- it("should transform Link with target='_blank' to external", () => {
19
- const input = `
20
- import { Link } from "@planningcenter/tapestry-react"
21
-
22
- export default function Test() {
23
- return <Link href="/external" target="_blank">External Link</Link>
24
- }
25
- `.trim()
26
-
27
- const expected = `
28
- import { Link } from "@planningcenter/tapestry-react"
29
-
30
- export default function Test() {
31
- return <Link href="/external" external>External Link</Link>;
32
- }
33
- `.trim()
34
-
35
- const result = applyTransform(input)
36
- expect(result?.trim()).toBe(expected)
37
- })
38
-
39
- it("should remove target and rel attributes while adding external", () => {
40
- const input = `
41
- import { Link } from "@planningcenter/tapestry-react"
42
-
43
- export default function Test() {
44
- return (
45
- <Link
46
- href="/external"
47
- target="_blank"
48
- rel="noopener noreferrer"
49
- className="external-link"
50
- >
51
- External Link
52
- </Link>
53
- )
54
- }
55
- `.trim()
56
-
57
- const result = applyTransform(input)
58
- expect(result).toContain("external")
59
- expect(result).toContain('className="external-link"')
60
- expect(result).not.toContain('target="_blank"')
61
- expect(result).not.toContain('rel="noopener noreferrer"')
62
- })
63
-
64
- it("should only transform Links with target='_blank'", () => {
65
- const input = `
66
- import { Link } from "@planningcenter/tapestry-react"
67
-
68
- export default function Test() {
69
- return (
70
- <div>
71
- <Link href="/internal" target="_self">Internal Link</Link>
72
- <Link href="/external" target="_blank">External Link</Link>
73
- </div>
74
- )
75
- }
76
- `.trim()
77
-
78
- const result = applyTransform(input)
79
- expect(result).toContain('target="_self"') // Should remain
80
- expect(result).toContain("external") // Should be added
81
- expect(result).not.toContain('target="_blank"') // Should be removed
82
- })
83
-
84
- it("should handle Links without rel attribute", () => {
85
- const input = `
86
- import { Link } from "@planningcenter/tapestry-react"
87
-
88
- export default function Test() {
89
- return <Link href="/external" target="_blank">External Link</Link>
90
- }
91
- `.trim()
92
-
93
- const result = applyTransform(input)
94
- expect(result).toContain("external")
95
- expect(result).not.toContain('target="_blank"')
96
- })
97
-
98
- it("should handle Links with only rel attribute (no target)", () => {
99
- const input = `
100
- import { Link } from "@planningcenter/tapestry-react"
101
-
102
- export default function Test() {
103
- return <Link href="/internal" rel="noopener">Internal Link</Link>
104
- }
105
- `.trim()
106
-
107
- const result = applyTransform(input)
108
- expect(result).toBeNull() // No changes should be made, transform returns null
109
- })
110
- })
111
-
112
- describe("edge cases", () => {
113
- it("should handle multiple Link components", () => {
114
- const input = `
115
- import { Link } from "@planningcenter/tapestry-react"
116
-
117
- export default function Test() {
118
- return (
119
- <div>
120
- <Link href="/page1" target="_blank">Page 1</Link>
121
- <Link href="/page2" target="_blank" rel="noopener">Page 2</Link>
122
- <Link href="/page3">Page 3</Link>
123
- </div>
124
- )
125
- }
126
- `.trim()
127
-
128
- const result = applyTransform(input)
129
- expect(result).toContain("external") // Should appear twice
130
- expect(result).not.toContain('target="_blank"')
131
- expect(result).not.toContain('rel="noopener"')
132
- expect(result).toContain('href="/page3"') // Should remain unchanged
133
- })
134
-
135
- it("should handle target={'_blank'} format", () => {
136
- const input = `
137
- import { Link } from "@planningcenter/tapestry-react"
138
-
139
- export default function Test() {
140
- return <Link href="/external" target={'_blank'}>External Link</Link>
141
- }
142
- `.trim()
143
-
144
- const result = applyTransform(input)
145
- expect(result).toContain("external")
146
- expect(result).not.toContain("target={'_blank'}")
147
- })
148
-
149
- it("should handle dynamic target values", () => {
150
- const input = `
151
- import { Link } from "@planningcenter/tapestry-react"
152
-
153
- export default function Test() {
154
- return <Link href="/external" target={isExternal ? "_blank" : "_self"}>Link</Link>
155
- }
156
- `.trim()
157
-
158
- const result = applyTransform(input)
159
- expect(result).toBeNull() // No changes should be made for dynamic values
160
- })
161
-
162
- it("should handle self-closing Link components", () => {
163
- const input = `
164
- import { Link } from "@planningcenter/tapestry-react"
165
-
166
- export default function Test() {
167
- return <Link href="/external" target="_blank" />
168
- }
169
- `.trim()
170
-
171
- const result = applyTransform(input)
172
- expect(result).toContain("external")
173
- expect(result).not.toContain('target="_blank"')
174
- })
175
- })
176
-
177
- describe("import handling", () => {
178
- it("should only transform when Link is imported from correct package", () => {
179
- const input = `
180
- import { Link } from "react-router-dom"
181
-
182
- export default function Test() {
183
- return <Link to="/external" target="_blank">External Link</Link>
184
- }
185
- `.trim()
186
-
187
- const result = applyTransform(input)
188
- expect(result).toBeNull() // No changes should be made
189
- })
190
-
191
- it("should handle aliased imports", () => {
192
- const input = `
193
- import { Link as TapestryLink } from "@planningcenter/tapestry-react"
194
-
195
- export default function Test() {
196
- return <TapestryLink href="/external" target="_blank">External Link</TapestryLink>
197
- }
198
- `.trim()
199
-
200
- const result = applyTransform(input)
201
- expect(result).toContain("external")
202
- expect(result).not.toContain('target="_blank"')
203
- })
204
-
205
- it("should add CHANGED comment when verbose is enabled", () => {
206
- const input = `
207
- import { Link } from "@planningcenter/tapestry-react"
208
-
209
- export default function Test() {
210
- return <Link href="/external" target="_blank">External Link</Link>
211
- }
212
- `.trim()
213
-
214
- const result = applyTransform(input, true)
215
- expect(result).toContain("external")
216
- expect(result).toContain(
217
- "CHANGED: tapestry-migration (external): target='_blank' converted to external prop"
218
- )
219
- })
220
- })
221
- })