@planningcenter/tapestry-migration-cli 2.3.0-rc.11 → 2.3.0-rc.12
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/package.json +2 -2
- package/src/components/link/index.ts +27 -0
- package/src/components/link/transforms/inlineToKind.test.ts +308 -0
- package/src/components/link/transforms/inlineToKind.ts +51 -0
- package/src/components/link/transforms/toToHref.test.ts +245 -0
- package/src/components/link/transforms/toToHref.ts +14 -0
- package/src/components/shared/actions/transformElementName.ts +23 -9
- package/src/components/shared/transformFactories/attributeTransformFactory.test.ts +83 -0
- package/src/components/shared/transformFactories/attributeTransformFactory.ts +21 -14
- package/src/components/shared/transformFactories/componentTransformFactory.test.ts +85 -2
- package/src/components/shared/transformFactories/componentTransformFactory.ts +41 -22
- package/src/components/shared/transformFactories/helpers/findJSXElements.ts +37 -0
- package/src/components/shared/types.ts +19 -1
- package/src/index.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/tapestry-migration-cli",
|
|
3
|
-
"version": "2.3.0-rc.
|
|
3
|
+
"version": "2.3.0-rc.12",
|
|
4
4
|
"description": "CLI tool for Tapestry migrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "6da3662a1766ecb928eaa3ba24c347ab1a37e6e7"
|
|
55
55
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import inlineToKind from "./transforms/inlineToKind"
|
|
4
|
+
import toToHref from "./transforms/toToHref"
|
|
5
|
+
|
|
6
|
+
const transform: Transform = (fileInfo, api, options) => {
|
|
7
|
+
let currentSource = fileInfo.source
|
|
8
|
+
let hasAnyChanges = false
|
|
9
|
+
|
|
10
|
+
const transforms: Transform[] = [inlineToKind, toToHref]
|
|
11
|
+
|
|
12
|
+
for (const individualTransform of transforms) {
|
|
13
|
+
const result = individualTransform(
|
|
14
|
+
{ ...fileInfo, source: currentSource },
|
|
15
|
+
api,
|
|
16
|
+
options
|
|
17
|
+
)
|
|
18
|
+
if (result && result !== currentSource) {
|
|
19
|
+
currentSource = result as string
|
|
20
|
+
hasAnyChanges = true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return hasAnyChanges ? currentSource : null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default transform
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./inlineToKind"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function applyTransform(source: string): string | null {
|
|
9
|
+
const fileInfo = { path: "test.tsx", source }
|
|
10
|
+
return transform(
|
|
11
|
+
fileInfo,
|
|
12
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
13
|
+
{}
|
|
14
|
+
) as string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("inlineToKind transform", () => {
|
|
18
|
+
describe("basic transformations", () => {
|
|
19
|
+
it("should transform Link.Inline to Link with kind='inline-text'", () => {
|
|
20
|
+
const input = `
|
|
21
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
export default function Test() {
|
|
24
|
+
return <Link.Inline href="/profile">Profile</Link.Inline>
|
|
25
|
+
}
|
|
26
|
+
`.trim()
|
|
27
|
+
|
|
28
|
+
const expected = `
|
|
29
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
30
|
+
|
|
31
|
+
export default function Test() {
|
|
32
|
+
return <Link href="/profile" kind="inline-text">Profile</Link>;
|
|
33
|
+
}
|
|
34
|
+
`.trim()
|
|
35
|
+
|
|
36
|
+
const result = applyTransform(input)
|
|
37
|
+
expect(result?.trim()).toBe(expected)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("should preserve all existing props when transforming Link.Inline", () => {
|
|
41
|
+
const input = `
|
|
42
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
43
|
+
|
|
44
|
+
export default function Test() {
|
|
45
|
+
return (
|
|
46
|
+
<Link.Inline
|
|
47
|
+
href="/dashboard"
|
|
48
|
+
className="nav-link"
|
|
49
|
+
target="_blank"
|
|
50
|
+
rel="noopener"
|
|
51
|
+
data-testid="profile-link"
|
|
52
|
+
>
|
|
53
|
+
Dashboard
|
|
54
|
+
</Link.Inline>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
`.trim()
|
|
58
|
+
|
|
59
|
+
const result = applyTransform(input)
|
|
60
|
+
expect(result).toContain('kind="inline-text"')
|
|
61
|
+
expect(result).toContain('href="/dashboard"')
|
|
62
|
+
expect(result).toContain('className="nav-link"')
|
|
63
|
+
expect(result).toContain('target="_blank"')
|
|
64
|
+
expect(result).toContain('rel="noopener"')
|
|
65
|
+
expect(result).toContain('data-testid="profile-link"')
|
|
66
|
+
expect(result).toContain('kind="inline-text"')
|
|
67
|
+
expect(result).toContain("<Link")
|
|
68
|
+
expect(result).not.toContain("Link.Inline")
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("should handle self-closing Link.Inline tags", () => {
|
|
72
|
+
const input = `
|
|
73
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
74
|
+
|
|
75
|
+
export default function Test() {
|
|
76
|
+
return <Link.Inline href="/dashboard" />
|
|
77
|
+
}
|
|
78
|
+
`.trim()
|
|
79
|
+
|
|
80
|
+
const expected = `
|
|
81
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
82
|
+
|
|
83
|
+
export default function Test() {
|
|
84
|
+
return <Link href="/dashboard" kind="inline-text" />;
|
|
85
|
+
}
|
|
86
|
+
`.trim()
|
|
87
|
+
|
|
88
|
+
const result = applyTransform(input)
|
|
89
|
+
expect(result?.trim()).toBe(expected)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("should handle multiple Link.Inline components", () => {
|
|
93
|
+
const input = `
|
|
94
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
95
|
+
|
|
96
|
+
export default function Test() {
|
|
97
|
+
return (
|
|
98
|
+
<div>
|
|
99
|
+
<Link.Inline href="/home">Home</Link.Inline>
|
|
100
|
+
<Link.Inline href="/about">About</Link.Inline>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
`.trim()
|
|
105
|
+
|
|
106
|
+
const result = applyTransform(input)
|
|
107
|
+
expect(result).toContain(
|
|
108
|
+
'<Link href="/home" kind="inline-text">Home</Link>'
|
|
109
|
+
)
|
|
110
|
+
expect(result).toContain(
|
|
111
|
+
'<Link href="/about" kind="inline-text">About</Link>'
|
|
112
|
+
)
|
|
113
|
+
expect(result).not.toContain("Link.Inline")
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe("mixed scenarios", () => {
|
|
118
|
+
it("should only transform Link.Inline and leave regular Link unchanged", () => {
|
|
119
|
+
const input = `
|
|
120
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
121
|
+
|
|
122
|
+
export default function Test() {
|
|
123
|
+
return (
|
|
124
|
+
<nav>
|
|
125
|
+
<Link href="/home">Home</Link>
|
|
126
|
+
<Link.Inline href="/profile">Profile</Link.Inline>
|
|
127
|
+
<Link as="button" href="/dashboard">Dashboard</Link>
|
|
128
|
+
</nav>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
`.trim()
|
|
132
|
+
|
|
133
|
+
const result = applyTransform(input)
|
|
134
|
+
expect(result).toContain('<Link href="/home">Home</Link>')
|
|
135
|
+
expect(result).toContain(
|
|
136
|
+
'<Link href="/profile" kind="inline-text">Profile</Link>'
|
|
137
|
+
)
|
|
138
|
+
expect(result).toContain(
|
|
139
|
+
'<Link as="button" href="/dashboard">Dashboard</Link>'
|
|
140
|
+
)
|
|
141
|
+
expect(result).not.toContain("Link.Inline")
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it("should handle aliased Link import", () => {
|
|
145
|
+
const input = `
|
|
146
|
+
import { Link as TapestryLink } from "@planningcenter/tapestry-react"
|
|
147
|
+
|
|
148
|
+
export default function Test() {
|
|
149
|
+
return <TapestryLink.Inline href="/profile">Profile</TapestryLink.Inline>
|
|
150
|
+
}
|
|
151
|
+
`.trim()
|
|
152
|
+
|
|
153
|
+
const result = applyTransform(input)
|
|
154
|
+
expect(result).toContain(
|
|
155
|
+
'<TapestryLink href="/profile" kind="inline-text">Profile</TapestryLink>'
|
|
156
|
+
)
|
|
157
|
+
expect(result).not.toContain("TapestryLink.Inline")
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it("should handle complex JSX expressions", () => {
|
|
161
|
+
const input = `
|
|
162
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
163
|
+
|
|
164
|
+
export default function Test({ items, showInline }) {
|
|
165
|
+
return (
|
|
166
|
+
<div>
|
|
167
|
+
{items.map(item => (
|
|
168
|
+
<Link.Inline key={item.id} href={item.url}>{item.name}</Link.Inline>
|
|
169
|
+
))}
|
|
170
|
+
{showInline && <Link.Inline href="/conditional">Conditional</Link.Inline>}
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
`.trim()
|
|
175
|
+
|
|
176
|
+
const result = applyTransform(input)
|
|
177
|
+
expect(result).toContain(
|
|
178
|
+
'<Link key={item.id} href={item.url} kind="inline-text">{item.name}</Link>'
|
|
179
|
+
)
|
|
180
|
+
expect(result).toContain(
|
|
181
|
+
'<Link href="/conditional" kind="inline-text">Conditional</Link>'
|
|
182
|
+
)
|
|
183
|
+
expect(result).not.toContain("Link.Inline")
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe("edge cases", () => {
|
|
188
|
+
it("should not transform if Link is not imported from tapestry-react", () => {
|
|
189
|
+
const input = `
|
|
190
|
+
import { Link } from "react-router-dom"
|
|
191
|
+
|
|
192
|
+
export default function Test() {
|
|
193
|
+
return <Link.Inline to="/profile">Profile</Link.Inline>
|
|
194
|
+
}
|
|
195
|
+
`.trim()
|
|
196
|
+
|
|
197
|
+
const result = applyTransform(input)
|
|
198
|
+
expect(result).toBe(null) // No changes needed
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it("should handle Link.Inline with dynamic props", () => {
|
|
202
|
+
const input = `
|
|
203
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
204
|
+
|
|
205
|
+
export default function Test({ url, isExternal }) {
|
|
206
|
+
return (
|
|
207
|
+
<Link.Inline
|
|
208
|
+
href={url}
|
|
209
|
+
target={isExternal ? "_blank" : "_self"}
|
|
210
|
+
className={\`link-\${isExternal ? 'external' : 'internal'}\`}
|
|
211
|
+
>
|
|
212
|
+
Dynamic Link
|
|
213
|
+
</Link.Inline>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
`.trim()
|
|
217
|
+
|
|
218
|
+
const result = applyTransform(input)
|
|
219
|
+
expect(result).toContain('kind="inline-text"')
|
|
220
|
+
expect(result).toContain("<Link")
|
|
221
|
+
expect(result).toContain("href={url}")
|
|
222
|
+
expect(result).toContain('target={isExternal ? "_blank" : "_self"}')
|
|
223
|
+
expect(result).toContain(
|
|
224
|
+
"className={`link-${isExternal ? 'external' : 'internal'}`}"
|
|
225
|
+
)
|
|
226
|
+
expect(result).not.toContain("Link.Inline")
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it("should handle nested Link.Inline components", () => {
|
|
230
|
+
const input = `
|
|
231
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
232
|
+
|
|
233
|
+
export default function Test() {
|
|
234
|
+
return (
|
|
235
|
+
<div>
|
|
236
|
+
<p>
|
|
237
|
+
Visit our <Link.Inline href="/about">about page</Link.Inline> for more info.
|
|
238
|
+
</p>
|
|
239
|
+
<span>
|
|
240
|
+
Or check the <Link.Inline href="/help">help section</Link.Inline>.
|
|
241
|
+
</span>
|
|
242
|
+
</div>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
`.trim()
|
|
246
|
+
|
|
247
|
+
const result = applyTransform(input)
|
|
248
|
+
expect(result).toContain(
|
|
249
|
+
'<Link href="/about" kind="inline-text">about page</Link>'
|
|
250
|
+
)
|
|
251
|
+
expect(result).toContain(
|
|
252
|
+
'<Link href="/help" kind="inline-text">help section</Link>'
|
|
253
|
+
)
|
|
254
|
+
expect(result).not.toContain("Link.Inline")
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe("no changes scenarios", () => {
|
|
259
|
+
it("should return null when no Link.Inline components exist", () => {
|
|
260
|
+
const input = `
|
|
261
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
262
|
+
|
|
263
|
+
export default function Test() {
|
|
264
|
+
return <Link href="/home">Home</Link>
|
|
265
|
+
}
|
|
266
|
+
`.trim()
|
|
267
|
+
|
|
268
|
+
const result = applyTransform(input)
|
|
269
|
+
expect(result).toBe(null)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it("should return null when no tapestry-react imports exist", () => {
|
|
273
|
+
const input = `
|
|
274
|
+
import React from "react"
|
|
275
|
+
|
|
276
|
+
export default function Test() {
|
|
277
|
+
return <div>No Links here</div>
|
|
278
|
+
}
|
|
279
|
+
`.trim()
|
|
280
|
+
|
|
281
|
+
const result = applyTransform(input)
|
|
282
|
+
expect(result).toBe(null)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it("should return null for empty file", () => {
|
|
286
|
+
const result = applyTransform("")
|
|
287
|
+
expect(result).toBe(null)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it("should return null when Link is imported but no Link.Inline used", () => {
|
|
291
|
+
const input = `
|
|
292
|
+
import { Link, Button } from "@planningcenter/tapestry-react"
|
|
293
|
+
|
|
294
|
+
export default function Test() {
|
|
295
|
+
return (
|
|
296
|
+
<div>
|
|
297
|
+
<Link href="/home">Home</Link>
|
|
298
|
+
<Button>Click me</Button>
|
|
299
|
+
</div>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
`.trim()
|
|
303
|
+
|
|
304
|
+
const result = applyTransform(input)
|
|
305
|
+
expect(result).toBe(null)
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addAttribute } from "../../shared/actions/addAttribute"
|
|
4
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
5
|
+
import { componentTransformFactory } from "../../shared/transformFactories/componentTransformFactory"
|
|
6
|
+
|
|
7
|
+
// Step 1: Add kind="inline-text" attribute to Link.Inline elements
|
|
8
|
+
const addKindAttribute = attributeTransformFactory({
|
|
9
|
+
condition: () => true, // Add to all Link.Inline elements
|
|
10
|
+
targetComponent: "Link.Inline",
|
|
11
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
12
|
+
transform: (element, { j }) => {
|
|
13
|
+
addAttribute({
|
|
14
|
+
element,
|
|
15
|
+
j,
|
|
16
|
+
name: "kind",
|
|
17
|
+
value: "inline-text",
|
|
18
|
+
})
|
|
19
|
+
return true
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Step 2: Transform Link.Inline to Link (preserving original component name)
|
|
24
|
+
const transformComponent = componentTransformFactory({
|
|
25
|
+
condition: () => true, // Transform all Link.Inline elements
|
|
26
|
+
fromComponent: "Link.Inline",
|
|
27
|
+
fromPackage: "@planningcenter/tapestry-react",
|
|
28
|
+
toComponent: "Link",
|
|
29
|
+
toPackage: "@planningcenter/tapestry-react",
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Combined transform that runs both steps
|
|
33
|
+
const transform: Transform = (fileInfo, api, options) => {
|
|
34
|
+
// Step 1: Add kind attribute to Link.Inline elements
|
|
35
|
+
const result1 = addKindAttribute(fileInfo, api, options)
|
|
36
|
+
|
|
37
|
+
if (!result1) {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Step 2: Transform Link.Inline to Link
|
|
42
|
+
const result2 = transformComponent(
|
|
43
|
+
{ ...fileInfo, source: result1 as string },
|
|
44
|
+
api,
|
|
45
|
+
options
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return result2 || result1
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default transform
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./toToHref"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function applyTransform(source: string): string | null {
|
|
9
|
+
const fileInfo = { path: "test.tsx", source }
|
|
10
|
+
return transform(
|
|
11
|
+
fileInfo,
|
|
12
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
13
|
+
{}
|
|
14
|
+
) as string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("toToHref transform", () => {
|
|
18
|
+
describe("basic transformations", () => {
|
|
19
|
+
it("should transform Link 'to' prop to 'href' prop", () => {
|
|
20
|
+
const input = `
|
|
21
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
export default function Test() {
|
|
24
|
+
return <Link to="/dashboard">Dashboard</Link>
|
|
25
|
+
}
|
|
26
|
+
`.trim()
|
|
27
|
+
|
|
28
|
+
const expected = `
|
|
29
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
30
|
+
|
|
31
|
+
export default function Test() {
|
|
32
|
+
return <Link href="/dashboard">Dashboard</Link>;
|
|
33
|
+
}
|
|
34
|
+
`.trim()
|
|
35
|
+
|
|
36
|
+
const result = applyTransform(input)
|
|
37
|
+
expect(result?.trim()).toBe(expected)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("should handle multiple Link components with 'to' props", () => {
|
|
41
|
+
const input = `
|
|
42
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
43
|
+
|
|
44
|
+
export default function Test() {
|
|
45
|
+
return (
|
|
46
|
+
<div>
|
|
47
|
+
<Link to="/home">Home</Link>
|
|
48
|
+
<Link to="/about">About</Link>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
`.trim()
|
|
53
|
+
|
|
54
|
+
const result = applyTransform(input)
|
|
55
|
+
expect(result).toContain('href="/home"')
|
|
56
|
+
expect(result).toContain('href="/about"')
|
|
57
|
+
expect(result).not.toContain('to="/home"')
|
|
58
|
+
expect(result).not.toContain('to="/about"')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("should preserve other props while transforming 'to' to 'href'", () => {
|
|
62
|
+
const input = `
|
|
63
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
64
|
+
|
|
65
|
+
export default function Test() {
|
|
66
|
+
return (
|
|
67
|
+
<Link
|
|
68
|
+
to="/dashboard"
|
|
69
|
+
className="nav-link"
|
|
70
|
+
target="_blank"
|
|
71
|
+
rel="noopener"
|
|
72
|
+
>
|
|
73
|
+
Dashboard
|
|
74
|
+
</Link>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
`.trim()
|
|
78
|
+
|
|
79
|
+
const result = applyTransform(input)
|
|
80
|
+
expect(result).toContain('href="/dashboard"')
|
|
81
|
+
expect(result).toContain('className="nav-link"')
|
|
82
|
+
expect(result).toContain('target="_blank"')
|
|
83
|
+
expect(result).toContain('rel="noopener"')
|
|
84
|
+
expect(result).not.toContain('to="/dashboard"')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("should handle dynamic 'to' prop values", () => {
|
|
88
|
+
const input = `
|
|
89
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
90
|
+
|
|
91
|
+
export default function Test({ userId }) {
|
|
92
|
+
return <Link to={\`/user/\${userId}\`}>User Profile</Link>
|
|
93
|
+
}
|
|
94
|
+
`.trim()
|
|
95
|
+
|
|
96
|
+
const result = applyTransform(input)
|
|
97
|
+
expect(result).toContain("href={`/user/${userId}`}")
|
|
98
|
+
expect(result).not.toContain("to={`/user/${userId}`}")
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe("edge cases", () => {
|
|
103
|
+
it("should not transform Links without 'to' prop", () => {
|
|
104
|
+
const input = `
|
|
105
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
106
|
+
|
|
107
|
+
export default function Test() {
|
|
108
|
+
return <Link href="/already-href">Already has href</Link>
|
|
109
|
+
}
|
|
110
|
+
`.trim()
|
|
111
|
+
|
|
112
|
+
const result = applyTransform(input)
|
|
113
|
+
expect(result).toBe(null) // No changes needed
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it("should not transform if Link is not imported from tapestry-react", () => {
|
|
117
|
+
const input = `
|
|
118
|
+
import { Link } from "react-router-dom"
|
|
119
|
+
|
|
120
|
+
export default function Test() {
|
|
121
|
+
return <Link to="/dashboard">Dashboard</Link>
|
|
122
|
+
}
|
|
123
|
+
`.trim()
|
|
124
|
+
|
|
125
|
+
const result = applyTransform(input)
|
|
126
|
+
expect(result).toBe(null) // No changes needed
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("should handle aliased Link import", () => {
|
|
130
|
+
const input = `
|
|
131
|
+
import { Link as TapestryLink } from "@planningcenter/tapestry-react"
|
|
132
|
+
|
|
133
|
+
export default function Test() {
|
|
134
|
+
return <TapestryLink to="/dashboard">Dashboard</TapestryLink>
|
|
135
|
+
}
|
|
136
|
+
`.trim()
|
|
137
|
+
|
|
138
|
+
const result = applyTransform(input)
|
|
139
|
+
expect(result).toContain('href="/dashboard"')
|
|
140
|
+
expect(result).not.toContain('to="/dashboard"')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("should handle self-closing Link tags", () => {
|
|
144
|
+
const input = `
|
|
145
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
146
|
+
|
|
147
|
+
export default function Test() {
|
|
148
|
+
return <Link to="/dashboard" />
|
|
149
|
+
}
|
|
150
|
+
`.trim()
|
|
151
|
+
|
|
152
|
+
const expected = `
|
|
153
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
154
|
+
|
|
155
|
+
export default function Test() {
|
|
156
|
+
return <Link href="/dashboard" />;
|
|
157
|
+
}
|
|
158
|
+
`.trim()
|
|
159
|
+
|
|
160
|
+
const result = applyTransform(input)
|
|
161
|
+
expect(result?.trim()).toBe(expected)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe("mixed scenarios", () => {
|
|
166
|
+
it("should handle mixed Link elements with various props", () => {
|
|
167
|
+
const input = `
|
|
168
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
169
|
+
|
|
170
|
+
export default function Test() {
|
|
171
|
+
return (
|
|
172
|
+
<nav>
|
|
173
|
+
<Link to="/home" className="home-link">Home</Link>
|
|
174
|
+
<Link to="/profile" target="_blank">Profile</Link>
|
|
175
|
+
<Link href="/already-href">Already Good</Link>
|
|
176
|
+
</nav>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
`.trim()
|
|
180
|
+
|
|
181
|
+
const result = applyTransform(input)
|
|
182
|
+
expect(result).toContain('href="/home"')
|
|
183
|
+
expect(result).toContain('href="/profile"')
|
|
184
|
+
expect(result).toContain('href="/already-href"')
|
|
185
|
+
expect(result).not.toContain('to="/home"')
|
|
186
|
+
expect(result).not.toContain('to="/profile"')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it("should handle complex JSX expressions", () => {
|
|
190
|
+
const input = `
|
|
191
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
192
|
+
|
|
193
|
+
export default function Test({ items }) {
|
|
194
|
+
return (
|
|
195
|
+
<div>
|
|
196
|
+
{items.map(item => (
|
|
197
|
+
<Link key={item.id} to={item.url}>{item.name}</Link>
|
|
198
|
+
))}
|
|
199
|
+
{showConditional && <Link to="/conditional">Conditional</Link>}
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
`.trim()
|
|
204
|
+
|
|
205
|
+
const result = applyTransform(input)
|
|
206
|
+
expect(result).toContain("href={item.url}")
|
|
207
|
+
expect(result).toContain('href="/conditional"')
|
|
208
|
+
expect(result).not.toContain("to={item.url}")
|
|
209
|
+
expect(result).not.toContain('to="/conditional"')
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe("no changes scenarios", () => {
|
|
214
|
+
it("should return null when no tapestry-react Links exist", () => {
|
|
215
|
+
const input = `
|
|
216
|
+
import React from "react"
|
|
217
|
+
|
|
218
|
+
export default function Test() {
|
|
219
|
+
return <div>No Links here</div>
|
|
220
|
+
}
|
|
221
|
+
`.trim()
|
|
222
|
+
|
|
223
|
+
const result = applyTransform(input)
|
|
224
|
+
expect(result).toBe(null)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it("should return null when no Links have 'to' prop", () => {
|
|
228
|
+
const input = `
|
|
229
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
230
|
+
|
|
231
|
+
export default function Test() {
|
|
232
|
+
return <Link href="/already-good">Already Good</Link>
|
|
233
|
+
}
|
|
234
|
+
`.trim()
|
|
235
|
+
|
|
236
|
+
const result = applyTransform(input)
|
|
237
|
+
expect(result).toBe(null)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it("should return null for empty file", () => {
|
|
241
|
+
const result = applyTransform("")
|
|
242
|
+
expect(result).toBe(null)
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { transformAttributeName } from "../../shared/actions/transformAttributeName"
|
|
2
|
+
import { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
3
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
4
|
+
|
|
5
|
+
const transform = attributeTransformFactory({
|
|
6
|
+
condition: hasAttribute("to"),
|
|
7
|
+
targetComponent: "Link",
|
|
8
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
9
|
+
transform: (element) => {
|
|
10
|
+
return transformAttributeName("to", "href", { element })
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export default transform
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { JSXElement } from "jscodeshift"
|
|
1
|
+
import { JSXElement, jsxIdentifier } from "jscodeshift"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Transforms JSX element names (both opening and closing tags)
|
|
5
|
+
* Supports both JSXIdentifier and JSXMemberExpression
|
|
5
6
|
*/
|
|
6
7
|
export function transformElementName({
|
|
7
8
|
element,
|
|
@@ -10,15 +11,28 @@ export function transformElementName({
|
|
|
10
11
|
element: JSXElement
|
|
11
12
|
name: string
|
|
12
13
|
}): boolean {
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
const openingName = element?.openingElement?.name
|
|
15
|
+
if (!openingName) return false
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
// Transform opening element
|
|
18
|
+
if (openingName.type === "JSXIdentifier") {
|
|
19
|
+
openingName.name = name
|
|
20
|
+
} else if (openingName.type === "JSXMemberExpression") {
|
|
21
|
+
// Transform JSXMemberExpression (e.g., Link.Inline) to JSXIdentifier (e.g., Link)
|
|
22
|
+
element.openingElement.name = jsxIdentifier(name)
|
|
23
|
+
} else {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Transform closing element if it exists
|
|
28
|
+
if (element.closingElement) {
|
|
29
|
+
const closingName = element.closingElement.name
|
|
30
|
+
if (closingName.type === "JSXIdentifier") {
|
|
31
|
+
closingName.name = name
|
|
32
|
+
} else if (closingName.type === "JSXMemberExpression") {
|
|
33
|
+
element.closingElement.name = jsxIdentifier(name)
|
|
20
34
|
}
|
|
21
|
-
return true
|
|
22
35
|
}
|
|
23
|
-
|
|
36
|
+
|
|
37
|
+
return true
|
|
24
38
|
}
|
|
@@ -85,4 +85,87 @@ describe("attributeTransformFactory", () => {
|
|
|
85
85
|
const result = transform(fileInfo, api, {})
|
|
86
86
|
expect(result).toBe(null)
|
|
87
87
|
})
|
|
88
|
+
|
|
89
|
+
describe("member expression support", () => {
|
|
90
|
+
it("should transform attributes on Link.Inline elements", () => {
|
|
91
|
+
const transform = attributeTransformFactory({
|
|
92
|
+
condition: () => true,
|
|
93
|
+
targetComponent: "Link.Inline",
|
|
94
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
95
|
+
transform: (element, { j }) => {
|
|
96
|
+
const attributes = element.openingElement.attributes || []
|
|
97
|
+
const kindAttr = j.jsxAttribute(
|
|
98
|
+
j.jsxIdentifier("kind"),
|
|
99
|
+
j.stringLiteral("inline-text")
|
|
100
|
+
)
|
|
101
|
+
attributes.push(kindAttr)
|
|
102
|
+
return true
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
const fileInfo = {
|
|
106
|
+
path: "test.tsx",
|
|
107
|
+
source: `import { Link } from "@planningcenter/tapestry-react";<Link.Inline>Test</Link.Inline>`,
|
|
108
|
+
}
|
|
109
|
+
const api = { j, jscodeshift: j, report: () => {}, stats: () => {} }
|
|
110
|
+
|
|
111
|
+
const result = transform(fileInfo, api, {})
|
|
112
|
+
expect(result).toContain('kind="inline-text"')
|
|
113
|
+
expect(result).toContain("<Link.Inline")
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it("should not transform other member expressions", () => {
|
|
117
|
+
const transform = attributeTransformFactory({
|
|
118
|
+
condition: () => true,
|
|
119
|
+
targetComponent: "Link.Inline",
|
|
120
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
121
|
+
transform: (element, { j }) => {
|
|
122
|
+
const attributes = element.openingElement.attributes || []
|
|
123
|
+
const kindAttr = j.jsxAttribute(
|
|
124
|
+
j.jsxIdentifier("kind"),
|
|
125
|
+
j.stringLiteral("inline-text")
|
|
126
|
+
)
|
|
127
|
+
attributes.push(kindAttr)
|
|
128
|
+
return true
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
const fileInfo = {
|
|
132
|
+
path: "test.tsx",
|
|
133
|
+
source: `import { Link } from "@planningcenter/tapestry-react";<div><Link.Inline>Inline</Link.Inline><Link.Primary>Primary</Link.Primary></div>`,
|
|
134
|
+
}
|
|
135
|
+
const api = { j, jscodeshift: j, report: () => {}, stats: () => {} }
|
|
136
|
+
|
|
137
|
+
const result = transform(fileInfo, api, {})
|
|
138
|
+
expect(result).toContain(
|
|
139
|
+
'<Link.Inline kind="inline-text">Inline</Link.Inline>'
|
|
140
|
+
)
|
|
141
|
+
expect(result).toContain("<Link.Primary>Primary</Link.Primary>")
|
|
142
|
+
expect(result).not.toContain('<Link.Primary kind="inline-text">')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("should handle aliased imports with member expressions", () => {
|
|
146
|
+
const transform = attributeTransformFactory({
|
|
147
|
+
condition: () => true,
|
|
148
|
+
targetComponent: "Link.Inline",
|
|
149
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
150
|
+
transform: (element, { j }) => {
|
|
151
|
+
const attributes = element.openingElement.attributes || []
|
|
152
|
+
const kindAttr = j.jsxAttribute(
|
|
153
|
+
j.jsxIdentifier("kind"),
|
|
154
|
+
j.stringLiteral("inline-text")
|
|
155
|
+
)
|
|
156
|
+
attributes.push(kindAttr)
|
|
157
|
+
return true
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
const fileInfo = {
|
|
161
|
+
path: "test.tsx",
|
|
162
|
+
source: `import { Link as TapestryLink } from "@planningcenter/tapestry-react";<TapestryLink.Inline>Test</TapestryLink.Inline>`,
|
|
163
|
+
}
|
|
164
|
+
const api = { j, jscodeshift: j, report: () => {}, stats: () => {} }
|
|
165
|
+
|
|
166
|
+
const result = transform(fileInfo, api, {})
|
|
167
|
+
expect(result).toContain('kind="inline-text"')
|
|
168
|
+
expect(result).toContain("<TapestryLink.Inline")
|
|
169
|
+
})
|
|
170
|
+
})
|
|
88
171
|
})
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "jscodeshift"
|
|
8
8
|
|
|
9
9
|
import { TransformCondition } from "../types"
|
|
10
|
+
import { findJSXElements } from "./helpers/findJSXElements"
|
|
10
11
|
import { getImportName } from "./helpers/manageImports"
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -15,7 +16,7 @@ import { getImportName } from "./helpers/manageImports"
|
|
|
15
16
|
export function attributeTransformFactory(config: {
|
|
16
17
|
/** Condition that must be met for the transform to occur */
|
|
17
18
|
condition?: TransformCondition
|
|
18
|
-
/** Component to target for attribute transformation */
|
|
19
|
+
/** Component to target for attribute transformation (supports member expressions like "Link.Inline") */
|
|
19
20
|
targetComponent: string
|
|
20
21
|
/** Package the target component is imported from */
|
|
21
22
|
targetPackage: string
|
|
@@ -30,32 +31,38 @@ export function attributeTransformFactory(config: {
|
|
|
30
31
|
const source = j(fileInfo.source)
|
|
31
32
|
let hasChanges = false
|
|
32
33
|
|
|
34
|
+
// Parse the targetComponent to handle member expressions
|
|
35
|
+
const [targetComponentName] = config.targetComponent.split(".")
|
|
36
|
+
|
|
33
37
|
// Get the local name of the target component
|
|
34
|
-
const
|
|
35
|
-
|
|
38
|
+
const targetComponentLocalName = getImportName(
|
|
39
|
+
targetComponentName,
|
|
36
40
|
config.targetPackage,
|
|
37
41
|
{ j, source }
|
|
38
42
|
)
|
|
39
43
|
|
|
40
44
|
// Only proceed if target component is imported
|
|
41
|
-
if (!
|
|
45
|
+
if (!targetComponentLocalName) {
|
|
42
46
|
return null
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
const { condition = () => true, transform } = config
|
|
46
50
|
|
|
47
51
|
// Find and transform matching JSX elements
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
findJSXElements(
|
|
53
|
+
source,
|
|
54
|
+
j,
|
|
55
|
+
config.targetComponent,
|
|
56
|
+
targetComponentLocalName
|
|
57
|
+
).forEach((path) => {
|
|
58
|
+
const element = path.parent.value
|
|
59
|
+
|
|
60
|
+
if (condition(element)) {
|
|
61
|
+
if (transform(element, { j, options, source })) {
|
|
62
|
+
hasChanges = true
|
|
57
63
|
}
|
|
58
|
-
}
|
|
64
|
+
}
|
|
65
|
+
})
|
|
59
66
|
|
|
60
67
|
return hasChanges ? source.toSource() : null
|
|
61
68
|
}
|
|
@@ -1,7 +1,90 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
1
2
|
import { describe, expect, it } from "vitest"
|
|
2
3
|
|
|
4
|
+
import { componentTransformFactory } from "./componentTransformFactory"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function applyTransform(source: string): string | null {
|
|
9
|
+
const fileInfo = { path: "test.tsx", source }
|
|
10
|
+
return componentTransformFactory({
|
|
11
|
+
condition: () => true,
|
|
12
|
+
fromComponent: "Link.Inline",
|
|
13
|
+
fromPackage: "@planningcenter/tapestry-react",
|
|
14
|
+
toComponent: "Link",
|
|
15
|
+
toPackage: "@planningcenter/tapestry-react",
|
|
16
|
+
})(fileInfo, { j, jscodeshift: j, report: () => {}, stats: () => {} }, {}) as
|
|
17
|
+
| string
|
|
18
|
+
| null
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
describe("componentTransformFactory", () => {
|
|
4
|
-
it("
|
|
5
|
-
|
|
22
|
+
it("should transform Link.Inline to Link", () => {
|
|
23
|
+
const input = `
|
|
24
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
25
|
+
|
|
26
|
+
export default function Test() {
|
|
27
|
+
return <Link.Inline>Test</Link.Inline>
|
|
28
|
+
}
|
|
29
|
+
`.trim()
|
|
30
|
+
|
|
31
|
+
const result = applyTransform(input)
|
|
32
|
+
expect(result).toContain("<Link>Test</Link>")
|
|
33
|
+
expect(result).not.toContain("<Link.Inline>")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("should handle multiple Link.Inline elements", () => {
|
|
37
|
+
const input = `
|
|
38
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
39
|
+
|
|
40
|
+
export default function Test() {
|
|
41
|
+
return (
|
|
42
|
+
<div>
|
|
43
|
+
<Link.Inline>First</Link.Inline>
|
|
44
|
+
<Link.Inline>Second</Link.Inline>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
`.trim()
|
|
49
|
+
|
|
50
|
+
const result = applyTransform(input)
|
|
51
|
+
expect(result).toContain("<Link>First</Link>")
|
|
52
|
+
expect(result).toContain("<Link>Second</Link>")
|
|
53
|
+
expect(result).not.toContain("<Link.Inline>")
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("should not transform other member expressions", () => {
|
|
57
|
+
const input = `
|
|
58
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
59
|
+
|
|
60
|
+
export default function Test() {
|
|
61
|
+
return (
|
|
62
|
+
<div>
|
|
63
|
+
<Link.Inline>Inline</Link.Inline>
|
|
64
|
+
<Link.Primary>Primary</Link.Primary>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
`.trim()
|
|
69
|
+
|
|
70
|
+
const result = applyTransform(input)
|
|
71
|
+
expect(result).toContain("<Link>Inline</Link>")
|
|
72
|
+
expect(result).toContain("<Link.Primary>Primary</Link.Primary>")
|
|
73
|
+
expect(result).not.toContain("<Link.Inline>")
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("should preserve original component name when removing member expressions", () => {
|
|
77
|
+
const input = `
|
|
78
|
+
import { Link as TapestryLink } from "@planningcenter/tapestry-react"
|
|
79
|
+
|
|
80
|
+
export default function Test() {
|
|
81
|
+
return <TapestryLink.Inline>Test</TapestryLink.Inline>
|
|
82
|
+
}
|
|
83
|
+
`.trim()
|
|
84
|
+
|
|
85
|
+
const result = applyTransform(input)
|
|
86
|
+
expect(result).toContain("<TapestryLink>Test</TapestryLink>")
|
|
87
|
+
expect(result).not.toContain("<TapestryLink.Inline>")
|
|
88
|
+
expect(result).not.toContain("<Link>")
|
|
6
89
|
})
|
|
7
90
|
})
|
|
@@ -2,6 +2,7 @@ import { Transform } from "jscodeshift"
|
|
|
2
2
|
|
|
3
3
|
import { transformElementName } from "../actions/transformElementName"
|
|
4
4
|
import { TransformCondition } from "../types"
|
|
5
|
+
import { findJSXElements } from "./helpers/findJSXElements"
|
|
5
6
|
import {
|
|
6
7
|
getImportName,
|
|
7
8
|
hasConflictingImport,
|
|
@@ -17,7 +18,7 @@ export function componentTransformFactory(config: {
|
|
|
17
18
|
condition: TransformCondition
|
|
18
19
|
/** Optional alias to use if target component conflicts with existing imports */
|
|
19
20
|
conflictAlias?: string
|
|
20
|
-
/** The source component name to transform from */
|
|
21
|
+
/** The source component name to transform from (supports member expressions like "Link.Inline") */
|
|
21
22
|
fromComponent: string
|
|
22
23
|
/** The package to import the source component from */
|
|
23
24
|
fromPackage: string
|
|
@@ -31,44 +32,62 @@ export function componentTransformFactory(config: {
|
|
|
31
32
|
const source = j(fileInfo.source)
|
|
32
33
|
let hasChanges = false
|
|
33
34
|
|
|
35
|
+
// Parse the fromComponent to handle member expressions
|
|
36
|
+
const [fromComponentName, fromComponentExpression] =
|
|
37
|
+
config.fromComponent.split(".")
|
|
38
|
+
|
|
34
39
|
// Get the local name of the source component
|
|
35
|
-
const
|
|
36
|
-
|
|
40
|
+
const sourceComponentLocalName = getImportName(
|
|
41
|
+
fromComponentName,
|
|
37
42
|
config.fromPackage,
|
|
38
43
|
{ j, source }
|
|
39
44
|
)
|
|
40
45
|
|
|
41
46
|
// Only proceed if source component is imported
|
|
42
|
-
if (!
|
|
47
|
+
if (!sourceComponentLocalName) {
|
|
43
48
|
return null
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
)
|
|
51
|
+
// Determine the target component name
|
|
52
|
+
// If we're removing a member expression (same base name), preserve the original component name
|
|
53
|
+
const isRemovingMemberExpression =
|
|
54
|
+
fromComponentExpression && fromComponentName === config.toComponent
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
let targetComponentName: string
|
|
57
|
+
if (isRemovingMemberExpression) {
|
|
58
|
+
// Preserve the original component name (e.g., TapestryLink.Inline → TapestryLink)
|
|
59
|
+
targetComponentName = sourceComponentLocalName
|
|
60
|
+
} else {
|
|
61
|
+
// Use the specified toComponent (e.g., Button → TapestryButton)
|
|
62
|
+
const hasConflict = hasConflictingImport(
|
|
63
|
+
config.toComponent,
|
|
64
|
+
[config.fromPackage, config.toPackage],
|
|
65
|
+
{ j, source }
|
|
66
|
+
)
|
|
67
|
+
targetComponentName = hasConflict
|
|
68
|
+
? config.conflictAlias || `T${config.toComponent}`
|
|
69
|
+
: config.toComponent
|
|
70
|
+
}
|
|
55
71
|
|
|
56
72
|
// Transform matching JSX elements
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
findJSXElements(
|
|
74
|
+
source,
|
|
75
|
+
j,
|
|
76
|
+
config.fromComponent,
|
|
77
|
+
sourceComponentLocalName
|
|
78
|
+
).forEach((path) => {
|
|
79
|
+
const element = path.parent.value
|
|
61
80
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
81
|
+
if (config.condition(element)) {
|
|
82
|
+
if (transformElementName({ element, name: targetComponentName })) {
|
|
83
|
+
hasChanges = true
|
|
66
84
|
}
|
|
67
|
-
}
|
|
85
|
+
}
|
|
86
|
+
})
|
|
68
87
|
|
|
69
88
|
if (hasChanges) {
|
|
70
89
|
// Handle import management
|
|
71
|
-
manageImports(source, config,
|
|
90
|
+
manageImports(source, config, fromComponentName, targetComponentName, j)
|
|
72
91
|
}
|
|
73
92
|
|
|
74
93
|
return hasChanges ? source.toSource() : null
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Collection, JSCodeshift, JSXElement } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper function to find JSX elements, supporting both regular components and member expressions
|
|
5
|
+
*/
|
|
6
|
+
export function findJSXElements(
|
|
7
|
+
source: Collection,
|
|
8
|
+
j: JSCodeshift,
|
|
9
|
+
componentName: string,
|
|
10
|
+
localComponentName: string
|
|
11
|
+
) {
|
|
12
|
+
// Parse the component name to handle member expressions
|
|
13
|
+
const [, memberExpression] = componentName.split(".")
|
|
14
|
+
|
|
15
|
+
if (memberExpression) {
|
|
16
|
+
// Find member expressions (e.g., Link.Inline)
|
|
17
|
+
return source
|
|
18
|
+
.find(j.JSXOpeningElement, {
|
|
19
|
+
name: {
|
|
20
|
+
object: { name: localComponentName },
|
|
21
|
+
type: "JSXMemberExpression",
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
.filter((path) => {
|
|
25
|
+
const element = path.parent.value as JSXElement
|
|
26
|
+
return (
|
|
27
|
+
element.openingElement.name.type === "JSXMemberExpression" &&
|
|
28
|
+
element.openingElement.name.property.name === memberExpression
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
} else {
|
|
32
|
+
// Find regular components (e.g., Link)
|
|
33
|
+
return source.find(j.JSXOpeningElement, {
|
|
34
|
+
name: { name: localComponentName },
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,6 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ASTPath,
|
|
3
|
+
Collection,
|
|
4
|
+
JSCodeshift,
|
|
5
|
+
JSXElement,
|
|
6
|
+
JSXOpeningElement,
|
|
7
|
+
} from "jscodeshift"
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* Condition function that determines whether a JSX element should be transformed
|
|
5
11
|
*/
|
|
6
12
|
export type TransformCondition = (element: JSXElement) => boolean
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Transform function for JSX elements
|
|
16
|
+
*/
|
|
17
|
+
export type TransformFunction = (
|
|
18
|
+
element: JSXElement,
|
|
19
|
+
resources: {
|
|
20
|
+
j: JSCodeshift
|
|
21
|
+
path: ASTPath<JSXOpeningElement>
|
|
22
|
+
source: Collection
|
|
23
|
+
}
|
|
24
|
+
) => boolean
|