@planningcenter/tapestry-migration-cli 2.3.0-rc.8 → 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 +2 -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
|
@@ -3084,8 +3084,8 @@ const tokens = {
|
|
|
3084
3084
|
"--t-fill-color-transparency-dark-070": "hsla(0, 0%, 0%, 0.7)",
|
|
3085
3085
|
"--t-fill-color-transparency-dark-080": "hsla(0, 0%, 0%, 0.8)",
|
|
3086
3086
|
"--t-fill-color-transparency-dark-090": "hsla(0, 0%, 0%, 0.9)",
|
|
3087
|
-
"--t-surface-color-canvas": "hsl(0, 0%, 98%)",
|
|
3088
3087
|
"--t-surface-color-card": "hsl(0, 0%, 100%)",
|
|
3088
|
+
"--t-surface-color-canvas": "hsl(0, 0%, 100%)",
|
|
3089
3089
|
"--t-border-color-default-base": "hsl(0, 0%, 88%)",
|
|
3090
3090
|
"--t-border-color-default-dark": "hsl(0, 0%, 81%)",
|
|
3091
3091
|
"--t-border-color-default-darker": "hsl(0, 0%, 68%)",
|
|
@@ -3124,6 +3124,7 @@ const tokens = {
|
|
|
3124
3124
|
"--t-form-font-color-disabled": "hsl(0, 0%, 81%)",
|
|
3125
3125
|
"--t-form-font-color-error": "hsl(8, 60%, 45%)",
|
|
3126
3126
|
"--t-form-font-color-readonly": "hsl(0, 0%, 24%)",
|
|
3127
|
+
"--t-form-picker-icon-color": "hsl(0, 0%, 24%)",
|
|
3127
3128
|
"--t-form-placeholder-color": "hsl(0, 0%, 58%)"
|
|
3128
3129
|
};
|
|
3129
3130
|
function token(varName) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/tapestry-migration-cli",
|
|
3
|
-
"version": "2.3.0
|
|
3
|
+
"version": "2.3.0",
|
|
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": "5645e6618bcb6253064988fabea2efa6b3a52669"
|
|
55
55
|
}
|
|
@@ -110,9 +110,9 @@ describe("convertStyleProps transform", () => {
|
|
|
110
110
|
|
|
111
111
|
const result = applyTransform(source)
|
|
112
112
|
|
|
113
|
-
expect(result).not.toContain('height="40px"')
|
|
113
|
+
expect(result).not.toContain('<Button height="40px"')
|
|
114
114
|
expect(result).toContain(
|
|
115
|
-
'TODO: tapestry-migration (
|
|
115
|
+
'TODO: tapestry-migration (styleProp): height has been removed as this is covered by default styling: height="40px"'
|
|
116
116
|
)
|
|
117
117
|
expect(result).toContain('kind="primary"')
|
|
118
118
|
})
|
|
@@ -128,7 +128,7 @@ describe("convertStyleProps transform", () => {
|
|
|
128
128
|
|
|
129
129
|
const result = applyTransform(source)
|
|
130
130
|
|
|
131
|
-
expect(result).not.toContain("height={buttonHeight}")
|
|
131
|
+
expect(result).not.toContain("<Button height={buttonHeight}")
|
|
132
132
|
expect(result).toContain("TODO: tapestry-migration")
|
|
133
133
|
expect(result).toContain("buttonHeight")
|
|
134
134
|
expect(result).toContain("disabled")
|
|
@@ -219,10 +219,9 @@ describe("convertStyleProps transform", () => {
|
|
|
219
219
|
expect(result).toContain('marginLeft: "128px"')
|
|
220
220
|
expect(result).toContain("}}>")
|
|
221
221
|
expect(result).toContain(
|
|
222
|
-
'TODO: tapestry-migration (
|
|
222
|
+
'TODO: tapestry-migration (styleProp): height has been removed as this is covered by default styling: height="40px"'
|
|
223
223
|
)
|
|
224
224
|
expect(result).not.toContain("marginLeft={16}")
|
|
225
|
-
expect(result).not.toContain('height="40px"')
|
|
226
225
|
expect(result).not.toContain('distribution="center"')
|
|
227
226
|
expect(result).not.toContain('paddingTop="8px"')
|
|
228
227
|
expect(result).toContain('kind="primary"')
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import inlineToKind from "./transforms/inlineToKind"
|
|
4
|
+
import targetBlankToExternal from "./transforms/targetBlankToExternal"
|
|
5
|
+
import toToHref from "./transforms/toToHref"
|
|
6
|
+
|
|
7
|
+
const transform: Transform = (fileInfo, api, options) => {
|
|
8
|
+
let currentSource = fileInfo.source
|
|
9
|
+
let hasAnyChanges = false
|
|
10
|
+
|
|
11
|
+
const transforms: Transform[] = [
|
|
12
|
+
inlineToKind,
|
|
13
|
+
toToHref,
|
|
14
|
+
targetBlankToExternal,
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
for (const individualTransform of transforms) {
|
|
18
|
+
const result = individualTransform(
|
|
19
|
+
{ ...fileInfo, source: currentSource },
|
|
20
|
+
api,
|
|
21
|
+
options
|
|
22
|
+
)
|
|
23
|
+
if (result && result !== currentSource) {
|
|
24
|
+
currentSource = result as string
|
|
25
|
+
hasAnyChanges = true
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return hasAnyChanges ? currentSource : null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
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,191 @@
|
|
|
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): string | null {
|
|
9
|
+
return targetBlankToExternal(
|
|
10
|
+
{ path: "test.tsx", source },
|
|
11
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
12
|
+
{}
|
|
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 dynamic target values", () => {
|
|
136
|
+
const input = `
|
|
137
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
138
|
+
|
|
139
|
+
export default function Test() {
|
|
140
|
+
return <Link href="/external" target={isExternal ? "_blank" : "_self"}>Link</Link>
|
|
141
|
+
}
|
|
142
|
+
`.trim()
|
|
143
|
+
|
|
144
|
+
const result = applyTransform(input)
|
|
145
|
+
expect(result).toBeNull() // No changes should be made for dynamic values
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("should handle self-closing Link components", () => {
|
|
149
|
+
const input = `
|
|
150
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
151
|
+
|
|
152
|
+
export default function Test() {
|
|
153
|
+
return <Link href="/external" target="_blank" />
|
|
154
|
+
}
|
|
155
|
+
`.trim()
|
|
156
|
+
|
|
157
|
+
const result = applyTransform(input)
|
|
158
|
+
expect(result).toContain("external")
|
|
159
|
+
expect(result).not.toContain('target="_blank"')
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe("import handling", () => {
|
|
164
|
+
it("should only transform when Link is imported from correct package", () => {
|
|
165
|
+
const input = `
|
|
166
|
+
import { Link } from "react-router-dom"
|
|
167
|
+
|
|
168
|
+
export default function Test() {
|
|
169
|
+
return <Link to="/external" target="_blank">External Link</Link>
|
|
170
|
+
}
|
|
171
|
+
`.trim()
|
|
172
|
+
|
|
173
|
+
const result = applyTransform(input)
|
|
174
|
+
expect(result).toBeNull() // No changes should be made
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it("should handle aliased imports", () => {
|
|
178
|
+
const input = `
|
|
179
|
+
import { Link as TapestryLink } from "@planningcenter/tapestry-react"
|
|
180
|
+
|
|
181
|
+
export default function Test() {
|
|
182
|
+
return <TapestryLink href="/external" target="_blank">External Link</TapestryLink>
|
|
183
|
+
}
|
|
184
|
+
`.trim()
|
|
185
|
+
|
|
186
|
+
const result = applyTransform(input)
|
|
187
|
+
expect(result).toContain("external")
|
|
188
|
+
expect(result).not.toContain('target="_blank"')
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { addAttribute } from "../../shared/actions/addAttribute"
|
|
2
|
+
import { removeAttribute } from "../../shared/actions/removeAttribute"
|
|
3
|
+
import { hasAttributeValue } from "../../shared/conditions/hasAttributeValue"
|
|
4
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
5
|
+
|
|
6
|
+
const transform = attributeTransformFactory({
|
|
7
|
+
condition: hasAttributeValue("target", "_blank"),
|
|
8
|
+
targetComponent: "Link",
|
|
9
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
10
|
+
transform: (element, { j, source }) => {
|
|
11
|
+
// Remove target attribute
|
|
12
|
+
removeAttribute("target", { element, j, source })
|
|
13
|
+
|
|
14
|
+
// Remove rel attribute if it exists
|
|
15
|
+
removeAttribute("rel", { element, j, source })
|
|
16
|
+
|
|
17
|
+
// Add external attribute as shorthand boolean
|
|
18
|
+
addAttribute({
|
|
19
|
+
booleanAsShorthand: true,
|
|
20
|
+
element,
|
|
21
|
+
j,
|
|
22
|
+
name: "external",
|
|
23
|
+
value: true, // This will create just "external" not "external={true}"
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
return true
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export default transform
|