@planningcenter/tapestry-migration-cli 2.4.0-rc.4 → 2.4.0-rc.6
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 +4 -2
- package/src/components/link/transforms/{inlineToKind.test.ts → inlineMemberToKind.test.ts} +2 -2
- package/src/components/link/transforms/{inlineToKind.ts → inlineMemberToKind.ts} +0 -2
- package/src/components/link/transforms/inlinePropToKind.test.ts +312 -0
- package/src/components/link/transforms/inlinePropToKind.ts +24 -0
- package/src/components/link/transforms/targetBlankToExternal.test.ts +14 -0
- package/src/components/shared/conditions/hasAttributeValue.test.ts +22 -1
- package/src/components/shared/conditions/hasAttributeValue.ts +24 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/tapestry-migration-cli",
|
|
3
|
-
"version": "2.4.0-rc.
|
|
3
|
+
"version": "2.4.0-rc.6",
|
|
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": "dd1114c63befe02778d61d70ca5162e838d4fcb0"
|
|
55
55
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Transform } from "jscodeshift"
|
|
2
2
|
|
|
3
3
|
import convertStyleProps from "./transforms/convertStyleProps"
|
|
4
|
-
import
|
|
4
|
+
import inlineMemberToKind from "./transforms/inlineMemberToKind"
|
|
5
|
+
import inlinePropToKind from "./transforms/inlinePropToKind"
|
|
5
6
|
import removeAs from "./transforms/removeAs"
|
|
6
7
|
import reviewStyles from "./transforms/reviewStyles"
|
|
7
8
|
import targetBlankToExternal from "./transforms/targetBlankToExternal"
|
|
@@ -12,7 +13,8 @@ const transform: Transform = (fileInfo, api, options) => {
|
|
|
12
13
|
let hasAnyChanges = false
|
|
13
14
|
|
|
14
15
|
const transforms: Transform[] = [
|
|
15
|
-
|
|
16
|
+
inlineMemberToKind,
|
|
17
|
+
inlinePropToKind,
|
|
16
18
|
toToHref,
|
|
17
19
|
targetBlankToExternal,
|
|
18
20
|
removeAs,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import jscodeshift from "jscodeshift"
|
|
2
2
|
import { describe, expect, it } from "vitest"
|
|
3
3
|
|
|
4
|
-
import transform from "./
|
|
4
|
+
import transform from "./inlineMemberToKind"
|
|
5
5
|
|
|
6
6
|
const j = jscodeshift.withParser("tsx")
|
|
7
7
|
|
|
@@ -14,7 +14,7 @@ function applyTransform(source: string): string | null {
|
|
|
14
14
|
) as string | null
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
describe("
|
|
17
|
+
describe("inlineMemberToKind transform", () => {
|
|
18
18
|
describe("basic transformations", () => {
|
|
19
19
|
it("should transform Link.Inline to Link with kind='inline-text'", () => {
|
|
20
20
|
const input = `
|
|
@@ -4,7 +4,6 @@ import { addAttribute } from "../../shared/actions/addAttribute"
|
|
|
4
4
|
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
5
5
|
import { componentTransformFactory } from "../../shared/transformFactories/componentTransformFactory"
|
|
6
6
|
|
|
7
|
-
// Step 1: Add kind="inline-text" attribute to Link.Inline elements
|
|
8
7
|
const addKindAttribute = attributeTransformFactory({
|
|
9
8
|
condition: () => true, // Add to all Link.Inline elements
|
|
10
9
|
targetComponent: "Link.Inline",
|
|
@@ -20,7 +19,6 @@ const addKindAttribute = attributeTransformFactory({
|
|
|
20
19
|
},
|
|
21
20
|
})
|
|
22
21
|
|
|
23
|
-
// Step 2: Transform Link.Inline to Link (preserving original component name)
|
|
24
22
|
const transformComponent = componentTransformFactory({
|
|
25
23
|
condition: () => true, // Transform all Link.Inline elements
|
|
26
24
|
fromComponent: "Link.Inline",
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./inlinePropToKind"
|
|
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("inlinePropToKind transform", () => {
|
|
18
|
+
describe("basic transformations", () => {
|
|
19
|
+
it("should transform Link with inline prop to kind='inline-text'", () => {
|
|
20
|
+
const input = `
|
|
21
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
export default function Test() {
|
|
24
|
+
return <Link href="/profile" inline>Profile</Link>
|
|
25
|
+
}
|
|
26
|
+
`.trim()
|
|
27
|
+
|
|
28
|
+
const result = applyTransform(input)
|
|
29
|
+
expect(result).toContain('kind="inline-text"')
|
|
30
|
+
expect(result).not.toContain("inline>")
|
|
31
|
+
expect(result).not.toContain("inline />")
|
|
32
|
+
expect(result).not.toContain("inline=")
|
|
33
|
+
expect(result).toContain('href="/profile"')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("should transform Link with inline={true} to kind='inline-text'", () => {
|
|
37
|
+
const input = `
|
|
38
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
39
|
+
|
|
40
|
+
export default function Test() {
|
|
41
|
+
return <Link href="/profile" inline={true}>Profile</Link>
|
|
42
|
+
}
|
|
43
|
+
`.trim()
|
|
44
|
+
|
|
45
|
+
const result = applyTransform(input)
|
|
46
|
+
expect(result).toContain('kind="inline-text"')
|
|
47
|
+
expect(result).not.toContain("inline>")
|
|
48
|
+
expect(result).not.toContain("inline />")
|
|
49
|
+
expect(result).not.toContain("inline=")
|
|
50
|
+
expect(result).toContain('href="/profile"')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("should preserve all other props", () => {
|
|
54
|
+
const input = `
|
|
55
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
56
|
+
|
|
57
|
+
export default function Test() {
|
|
58
|
+
return (
|
|
59
|
+
<Link
|
|
60
|
+
href="/dashboard"
|
|
61
|
+
className="nav-link"
|
|
62
|
+
inline
|
|
63
|
+
data-testid="profile-link"
|
|
64
|
+
>
|
|
65
|
+
Dashboard
|
|
66
|
+
</Link>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
`.trim()
|
|
70
|
+
|
|
71
|
+
const result = applyTransform(input)
|
|
72
|
+
expect(result).toContain('kind="inline-text"')
|
|
73
|
+
expect(result).toContain('href="/dashboard"')
|
|
74
|
+
expect(result).toContain('className="nav-link"')
|
|
75
|
+
expect(result).toContain('data-testid="profile-link"')
|
|
76
|
+
expect(result).not.toContain("inline>")
|
|
77
|
+
expect(result).not.toContain("inline />")
|
|
78
|
+
expect(result).not.toContain("inline=")
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("should handle self-closing Link with inline prop", () => {
|
|
82
|
+
const input = `
|
|
83
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
84
|
+
|
|
85
|
+
export default function Test() {
|
|
86
|
+
return <Link href="/dashboard" inline />
|
|
87
|
+
}
|
|
88
|
+
`.trim()
|
|
89
|
+
|
|
90
|
+
const result = applyTransform(input)
|
|
91
|
+
expect(result).toContain('kind="inline-text"')
|
|
92
|
+
expect(result).toContain('href="/dashboard"')
|
|
93
|
+
expect(result).not.toContain("inline>")
|
|
94
|
+
expect(result).not.toContain("inline />")
|
|
95
|
+
expect(result).not.toContain("inline=")
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("should handle multiple Link components with inline prop", () => {
|
|
99
|
+
const input = `
|
|
100
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
101
|
+
|
|
102
|
+
export default function Test() {
|
|
103
|
+
return (
|
|
104
|
+
<div>
|
|
105
|
+
<Link href="/home" inline>Home</Link>
|
|
106
|
+
<Link href="/about" inline>About</Link>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
`.trim()
|
|
111
|
+
|
|
112
|
+
const result = applyTransform(input)
|
|
113
|
+
expect(result).toContain(
|
|
114
|
+
'<Link href="/home" kind="inline-text">Home</Link>'
|
|
115
|
+
)
|
|
116
|
+
expect(result).toContain(
|
|
117
|
+
'<Link href="/about" kind="inline-text">About</Link>'
|
|
118
|
+
)
|
|
119
|
+
expect(result).not.toContain("inline>")
|
|
120
|
+
expect(result).not.toContain("inline />")
|
|
121
|
+
expect(result).not.toContain("inline=")
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe("mixed scenarios", () => {
|
|
126
|
+
it("should only transform Link with inline prop and leave regular Link unchanged", () => {
|
|
127
|
+
const input = `
|
|
128
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
129
|
+
|
|
130
|
+
export default function Test() {
|
|
131
|
+
return (
|
|
132
|
+
<nav>
|
|
133
|
+
<Link href="/home">Home</Link>
|
|
134
|
+
<Link href="/profile" inline>Profile</Link>
|
|
135
|
+
<Link href="/dashboard" kind="primary">Dashboard</Link>
|
|
136
|
+
</nav>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
`.trim()
|
|
140
|
+
|
|
141
|
+
const result = applyTransform(input)
|
|
142
|
+
expect(result).toContain('<Link href="/home">Home</Link>')
|
|
143
|
+
expect(result).toContain(
|
|
144
|
+
'<Link href="/profile" kind="inline-text">Profile</Link>'
|
|
145
|
+
)
|
|
146
|
+
expect(result).toContain(
|
|
147
|
+
'<Link href="/dashboard" kind="primary">Dashboard</Link>'
|
|
148
|
+
)
|
|
149
|
+
expect(result).not.toContain("inline>")
|
|
150
|
+
expect(result).not.toContain("inline />")
|
|
151
|
+
expect(result).not.toContain("inline=")
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it("should handle aliased Link import", () => {
|
|
155
|
+
const input = `
|
|
156
|
+
import { Link as TapestryLink } from "@planningcenter/tapestry-react"
|
|
157
|
+
|
|
158
|
+
export default function Test() {
|
|
159
|
+
return <TapestryLink href="/profile" inline>Profile</TapestryLink>
|
|
160
|
+
}
|
|
161
|
+
`.trim()
|
|
162
|
+
|
|
163
|
+
const result = applyTransform(input)
|
|
164
|
+
expect(result).toContain('kind="inline-text"')
|
|
165
|
+
expect(result).not.toContain("inline>")
|
|
166
|
+
expect(result).not.toContain("inline />")
|
|
167
|
+
expect(result).not.toContain("inline=")
|
|
168
|
+
expect(result).toContain("TapestryLink")
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe("edge cases", () => {
|
|
173
|
+
it("should not transform if Link is not imported from tapestry-react", () => {
|
|
174
|
+
const input = `
|
|
175
|
+
import { Link } from "react-router-dom"
|
|
176
|
+
|
|
177
|
+
export default function Test() {
|
|
178
|
+
return <Link to="/profile" inline>Profile</Link>
|
|
179
|
+
}
|
|
180
|
+
`.trim()
|
|
181
|
+
|
|
182
|
+
const result = applyTransform(input)
|
|
183
|
+
expect(result).toBe(null) // No changes
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it("should handle Link with spread props", () => {
|
|
187
|
+
const input = `
|
|
188
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
189
|
+
|
|
190
|
+
export default function Test({ linkProps }) {
|
|
191
|
+
return <Link {...linkProps} inline>Profile</Link>
|
|
192
|
+
}
|
|
193
|
+
`.trim()
|
|
194
|
+
|
|
195
|
+
const result = applyTransform(input)
|
|
196
|
+
expect(result).toContain('kind="inline-text"')
|
|
197
|
+
expect(result).toContain("{...linkProps}")
|
|
198
|
+
expect(result).not.toContain("inline>")
|
|
199
|
+
expect(result).not.toContain("inline />")
|
|
200
|
+
expect(result).not.toContain("inline=")
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it("should handle complex JSX expressions", () => {
|
|
204
|
+
const input = `
|
|
205
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
206
|
+
|
|
207
|
+
export default function Test({ items, showInline }) {
|
|
208
|
+
return (
|
|
209
|
+
<div>
|
|
210
|
+
{items.map(item => (
|
|
211
|
+
<Link key={item.id} href={item.url} inline>{item.name}</Link>
|
|
212
|
+
))}
|
|
213
|
+
{showInline && <Link href="/conditional" inline>Conditional</Link>}
|
|
214
|
+
</div>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
`.trim()
|
|
218
|
+
|
|
219
|
+
const result = applyTransform(input)
|
|
220
|
+
expect(result).toContain(
|
|
221
|
+
'<Link key={item.id} href={item.url} kind="inline-text">{item.name}</Link>'
|
|
222
|
+
)
|
|
223
|
+
expect(result).toContain(
|
|
224
|
+
'<Link href="/conditional" kind="inline-text">Conditional</Link>'
|
|
225
|
+
)
|
|
226
|
+
expect(result).not.toContain("inline>")
|
|
227
|
+
expect(result).not.toContain("inline />")
|
|
228
|
+
expect(result).not.toContain("inline=")
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it("should handle nested Link components", () => {
|
|
232
|
+
const input = `
|
|
233
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
234
|
+
|
|
235
|
+
export default function Test() {
|
|
236
|
+
return (
|
|
237
|
+
<div>
|
|
238
|
+
<p>
|
|
239
|
+
Visit our <Link href="/about" inline>about page</Link> for more info.
|
|
240
|
+
</p>
|
|
241
|
+
<span>
|
|
242
|
+
Or check the <Link href="/help" inline>help section</Link>.
|
|
243
|
+
</span>
|
|
244
|
+
</div>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
`.trim()
|
|
248
|
+
|
|
249
|
+
const result = applyTransform(input)
|
|
250
|
+
expect(result).toContain(
|
|
251
|
+
'<Link href="/about" kind="inline-text">about page</Link>'
|
|
252
|
+
)
|
|
253
|
+
expect(result).toContain(
|
|
254
|
+
'<Link href="/help" kind="inline-text">help section</Link>'
|
|
255
|
+
)
|
|
256
|
+
expect(result).not.toContain("inline>")
|
|
257
|
+
expect(result).not.toContain("inline />")
|
|
258
|
+
expect(result).not.toContain("inline=")
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe("no changes scenarios", () => {
|
|
263
|
+
it("should return null when no Link with inline prop exists", () => {
|
|
264
|
+
const input = `
|
|
265
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
266
|
+
|
|
267
|
+
export default function Test() {
|
|
268
|
+
return <Link href="/home">Home</Link>
|
|
269
|
+
}
|
|
270
|
+
`.trim()
|
|
271
|
+
|
|
272
|
+
const result = applyTransform(input)
|
|
273
|
+
expect(result).toBe(null)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it("should return null when no tapestry-react imports exist", () => {
|
|
277
|
+
const input = `
|
|
278
|
+
import React from "react"
|
|
279
|
+
|
|
280
|
+
export default function Test() {
|
|
281
|
+
return <div>No Links here</div>
|
|
282
|
+
}
|
|
283
|
+
`.trim()
|
|
284
|
+
|
|
285
|
+
const result = applyTransform(input)
|
|
286
|
+
expect(result).toBe(null)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it("should return null for empty file", () => {
|
|
290
|
+
const result = applyTransform("")
|
|
291
|
+
expect(result).toBe(null)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it("should return null when Link is imported but no inline prop used", () => {
|
|
295
|
+
const input = `
|
|
296
|
+
import { Link, Button } from "@planningcenter/tapestry-react"
|
|
297
|
+
|
|
298
|
+
export default function Test() {
|
|
299
|
+
return (
|
|
300
|
+
<div>
|
|
301
|
+
<Link href="/home">Home</Link>
|
|
302
|
+
<Button>Click me</Button>
|
|
303
|
+
</div>
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
`.trim()
|
|
307
|
+
|
|
308
|
+
const result = applyTransform(input)
|
|
309
|
+
expect(result).toBe(null)
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { addAttribute } from "../../shared/actions/addAttribute"
|
|
2
|
+
import { removeAttribute } from "../../shared/actions/removeAttribute"
|
|
3
|
+
import { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
4
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
5
|
+
|
|
6
|
+
const transform = attributeTransformFactory({
|
|
7
|
+
condition: hasAttribute("inline"),
|
|
8
|
+
targetComponent: "Link",
|
|
9
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
10
|
+
transform: (element, { j, source }) => {
|
|
11
|
+
removeAttribute("inline", { element, j, source })
|
|
12
|
+
|
|
13
|
+
addAttribute({
|
|
14
|
+
element,
|
|
15
|
+
j,
|
|
16
|
+
name: "kind",
|
|
17
|
+
value: "inline-text",
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
return true
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export default transform
|
|
@@ -132,6 +132,20 @@ export default function Test() {
|
|
|
132
132
|
expect(result).toContain('href="/page3"') // Should remain unchanged
|
|
133
133
|
})
|
|
134
134
|
|
|
135
|
+
it("should handle target={'_blank'} format", () => {
|
|
136
|
+
const input = `
|
|
137
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
138
|
+
|
|
139
|
+
export default function Test() {
|
|
140
|
+
return <Link href="/external" target={'_blank'}>External Link</Link>
|
|
141
|
+
}
|
|
142
|
+
`.trim()
|
|
143
|
+
|
|
144
|
+
const result = applyTransform(input)
|
|
145
|
+
expect(result).toContain("external")
|
|
146
|
+
expect(result).not.toContain("target={'_blank'}")
|
|
147
|
+
})
|
|
148
|
+
|
|
135
149
|
it("should handle dynamic target values", () => {
|
|
136
150
|
const input = `
|
|
137
151
|
import { Link } from "@planningcenter/tapestry-react"
|
|
@@ -32,7 +32,28 @@ describe("hasAttributeValue", () => {
|
|
|
32
32
|
expect(condition(element)).toBe(false)
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
it("should return
|
|
35
|
+
it("should return true for expression with string literal", () => {
|
|
36
|
+
const condition = hasAttributeValue("target", "_blank")
|
|
37
|
+
const element = createJSXElement(" target={'_blank'}")
|
|
38
|
+
|
|
39
|
+
expect(condition(element)).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("should return true for expression with double quotes", () => {
|
|
43
|
+
const condition = hasAttributeValue("target", "_blank")
|
|
44
|
+
const element = createJSXElement(' target={"_blank"}')
|
|
45
|
+
|
|
46
|
+
expect(condition(element)).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("should return false for expression with different value", () => {
|
|
50
|
+
const condition = hasAttributeValue("target", "_blank")
|
|
51
|
+
const element = createJSXElement(" target={'_self'}")
|
|
52
|
+
|
|
53
|
+
expect(condition(element)).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("should return false for complex expression values", () => {
|
|
36
57
|
const condition = hasAttributeValue("onClick", "handleClick")
|
|
37
58
|
const element = createJSXElement(" onClick={handleClick}")
|
|
38
59
|
|
|
@@ -4,6 +4,7 @@ import { TransformCondition } from "../types"
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Helper function to create a condition that checks for an attribute with a specific value
|
|
7
|
+
* Handles both string literals and expressions with string literals
|
|
7
8
|
*/
|
|
8
9
|
export function hasAttributeValue(
|
|
9
10
|
attributeName: string,
|
|
@@ -11,13 +12,30 @@ export function hasAttributeValue(
|
|
|
11
12
|
): TransformCondition {
|
|
12
13
|
return (element: JSXElement) => {
|
|
13
14
|
const attributes = element.openingElement.attributes || []
|
|
14
|
-
return attributes.some(
|
|
15
|
-
|
|
15
|
+
return attributes.some((attr) => {
|
|
16
|
+
const hasAttribute =
|
|
16
17
|
attr.type === "JSXAttribute" &&
|
|
17
18
|
attr.name?.type === "JSXIdentifier" &&
|
|
18
|
-
attr.name.name === attributeName
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
attr.name.name === attributeName
|
|
20
|
+
|
|
21
|
+
if (!hasAttribute) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Handle string literal: attribute="value"
|
|
26
|
+
if (attr.value?.type === "StringLiteral") {
|
|
27
|
+
return attr.value.value === value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Handle expression: attribute={"value"} or attribute={'value'}
|
|
31
|
+
if (attr.value?.type === "JSXExpressionContainer") {
|
|
32
|
+
const { expression } = attr.value
|
|
33
|
+
if (expression.type === "StringLiteral") {
|
|
34
|
+
return expression.value === value
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false
|
|
39
|
+
})
|
|
22
40
|
}
|
|
23
41
|
}
|