@planningcenter/tapestry-migration-cli 2.3.0-rc.9 → 2.3.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.
- package/dist/tapestry-react-shim.cjs +1 -1
- package/package.json +2 -2
- package/src/components/button/transforms/convertStyleProps.test.ts +4 -5
- package/src/components/link/index.ts +32 -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/targetBlankToExternal.test.ts +191 -0
- package/src/components/link/transforms/targetBlankToExternal.ts +30 -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/addAttribute.test.ts +108 -0
- package/src/components/shared/actions/addAttribute.ts +14 -0
- package/src/components/shared/actions/removeAttribute.ts +9 -2
- 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/transformFactories/stylePropTransformFactory.ts +2 -27
- package/src/components/shared/types.ts +19 -1
- package/src/index.ts +7 -2
- package/src/jscodeshiftRunner.ts +7 -0
- package/src/reportGenerator.ts +450 -0
- package/src/shared/types.ts +1 -0
|
@@ -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
|
|
@@ -266,6 +266,94 @@ describe("addAttribute", () => {
|
|
|
266
266
|
})
|
|
267
267
|
})
|
|
268
268
|
|
|
269
|
+
describe("boolean shorthand values", () => {
|
|
270
|
+
it("should add boolean true attribute as shorthand", () => {
|
|
271
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
272
|
+
|
|
273
|
+
addAttribute({
|
|
274
|
+
booleanAsShorthand: true,
|
|
275
|
+
element,
|
|
276
|
+
j,
|
|
277
|
+
name: "external",
|
|
278
|
+
value: true,
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const externalAttr = getAttributeFromElement(element, "external")
|
|
282
|
+
expect(externalAttr).not.toBeNull()
|
|
283
|
+
expect(externalAttr?.value).toBeNull() // Shorthand boolean has no value
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it("should not add boolean false attribute as shorthand", () => {
|
|
287
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
288
|
+
const initialAttrCount = (element.openingElement.attributes || []).length
|
|
289
|
+
|
|
290
|
+
addAttribute({
|
|
291
|
+
booleanAsShorthand: true,
|
|
292
|
+
element,
|
|
293
|
+
j,
|
|
294
|
+
name: "external",
|
|
295
|
+
value: false,
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
const finalAttrCount = (element.openingElement.attributes || []).length
|
|
299
|
+
expect(finalAttrCount).toBe(initialAttrCount)
|
|
300
|
+
|
|
301
|
+
const externalAttr = getAttributeFromElement(element, "external")
|
|
302
|
+
expect(externalAttr).toBeNull()
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it("should render shorthand boolean correctly in source", () => {
|
|
306
|
+
const source = j("<Button>Save</Button>")
|
|
307
|
+
const element = source.find(j.JSXElement).at(0).get().value as JSXElement
|
|
308
|
+
|
|
309
|
+
addAttribute({
|
|
310
|
+
booleanAsShorthand: true,
|
|
311
|
+
element,
|
|
312
|
+
j,
|
|
313
|
+
name: "external",
|
|
314
|
+
value: true,
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const result = source.toSource()
|
|
318
|
+
expect(result).toContain("external") // Should be just "external", not "external={true}"
|
|
319
|
+
expect(result).not.toContain("external={true}")
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it("should handle multiple shorthand boolean attributes", () => {
|
|
323
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
324
|
+
|
|
325
|
+
addAttribute({
|
|
326
|
+
booleanAsShorthand: true,
|
|
327
|
+
element,
|
|
328
|
+
j,
|
|
329
|
+
name: "external",
|
|
330
|
+
value: true,
|
|
331
|
+
})
|
|
332
|
+
addAttribute({
|
|
333
|
+
booleanAsShorthand: true,
|
|
334
|
+
element,
|
|
335
|
+
j,
|
|
336
|
+
name: "loading",
|
|
337
|
+
value: false,
|
|
338
|
+
})
|
|
339
|
+
addAttribute({
|
|
340
|
+
booleanAsShorthand: true,
|
|
341
|
+
element,
|
|
342
|
+
j,
|
|
343
|
+
name: "disabled",
|
|
344
|
+
value: true,
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
const externalAttr = getAttributeFromElement(element, "external")
|
|
348
|
+
const loadingAttr = getAttributeFromElement(element, "loading")
|
|
349
|
+
const disabledAttr = getAttributeFromElement(element, "disabled")
|
|
350
|
+
|
|
351
|
+
expect(externalAttr).not.toBeNull()
|
|
352
|
+
expect(loadingAttr).toBeNull() // false values should not be added
|
|
353
|
+
expect(disabledAttr).not.toBeNull()
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
269
357
|
describe("integration with JSCodeshift", () => {
|
|
270
358
|
it("should render correctly in transformed source", () => {
|
|
271
359
|
const source = j("<Button>Save</Button>")
|
|
@@ -296,5 +384,25 @@ describe("addAttribute", () => {
|
|
|
296
384
|
expect((testIdAttr?.value as StringLiteral)?.value).toBe("save-button")
|
|
297
385
|
expect((ariaAttr?.value as StringLiteral)?.value).toBe("help-text")
|
|
298
386
|
})
|
|
387
|
+
|
|
388
|
+
it("should render shorthand boolean attributes correctly in source", () => {
|
|
389
|
+
const source = j("<Button>Save</Button>")
|
|
390
|
+
const element = source.find(j.JSXElement).at(0).get().value as JSXElement
|
|
391
|
+
|
|
392
|
+
addAttribute({
|
|
393
|
+
booleanAsShorthand: true,
|
|
394
|
+
element,
|
|
395
|
+
j,
|
|
396
|
+
name: "external",
|
|
397
|
+
value: true,
|
|
398
|
+
})
|
|
399
|
+
addAttribute({ element, j, name: "disabled", value: true }) // Regular boolean
|
|
400
|
+
|
|
401
|
+
const result = source.toSource()
|
|
402
|
+
|
|
403
|
+
expect(result).toContain("external") // Shorthand boolean
|
|
404
|
+
expect(result).toContain("disabled={true}") // Regular boolean
|
|
405
|
+
expect(result).not.toContain("external={true}")
|
|
406
|
+
})
|
|
299
407
|
})
|
|
300
408
|
})
|
|
@@ -21,7 +21,9 @@ export function addAttribute({
|
|
|
21
21
|
name,
|
|
22
22
|
j,
|
|
23
23
|
value,
|
|
24
|
+
booleanAsShorthand = false,
|
|
24
25
|
}: {
|
|
26
|
+
booleanAsShorthand?: boolean
|
|
25
27
|
element: JSXElement
|
|
26
28
|
j: JSCodeshift
|
|
27
29
|
name: string
|
|
@@ -40,6 +42,18 @@ export function addAttribute({
|
|
|
40
42
|
return
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
// Handle boolean shorthand
|
|
46
|
+
// booleanAsShorthand is a temporary flag to allow for boolean attributes to be added as shorthand
|
|
47
|
+
// This will be removed in a follow-up PR for button[disabled]
|
|
48
|
+
if (typeof value === "boolean" && booleanAsShorthand) {
|
|
49
|
+
if (value === true) {
|
|
50
|
+
// Add as shorthand boolean attribute (just "external")
|
|
51
|
+
attributes.push(j.jsxAttribute(j.jsxIdentifier(name), null))
|
|
52
|
+
}
|
|
53
|
+
// For false, don't add the attribute at all
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
const formattedValue = formatValue(value, j)
|
|
44
58
|
attributes.push(j.jsxAttribute(j.jsxIdentifier(name), formattedValue))
|
|
45
59
|
}
|
|
@@ -7,6 +7,7 @@ import { getPrintableAttributeValue } from "./getPrintableAttributeValue"
|
|
|
7
7
|
type RemoveAttributeConfig = {
|
|
8
8
|
/** Function to build comment text given the prop name and formatted value */
|
|
9
9
|
buildComment?: (propName: string, formattedValue: string) => string | void
|
|
10
|
+
commentScope?: string
|
|
10
11
|
element: JSXElement
|
|
11
12
|
j: JSCodeshift
|
|
12
13
|
source: Collection
|
|
@@ -14,7 +15,7 @@ type RemoveAttributeConfig = {
|
|
|
14
15
|
|
|
15
16
|
export function removeAttribute(
|
|
16
17
|
name: string,
|
|
17
|
-
{ element, buildComment, j, source }: RemoveAttributeConfig
|
|
18
|
+
{ element, buildComment, j, source, commentScope }: RemoveAttributeConfig
|
|
18
19
|
): boolean {
|
|
19
20
|
const attributes = element.openingElement.attributes || []
|
|
20
21
|
const attribute = findAttribute(attributes, name)
|
|
@@ -28,7 +29,13 @@ export function removeAttribute(
|
|
|
28
29
|
const commentText = buildComment(name, printableValue || "")
|
|
29
30
|
|
|
30
31
|
if (commentText) {
|
|
31
|
-
addComment({
|
|
32
|
+
addComment({
|
|
33
|
+
element,
|
|
34
|
+
j,
|
|
35
|
+
scope: commentScope || name,
|
|
36
|
+
source,
|
|
37
|
+
text: commentText,
|
|
38
|
+
})
|
|
32
39
|
}
|
|
33
40
|
}
|
|
34
41
|
|
|
@@ -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
|
}
|