@planningcenter/tapestry-migration-cli 2.2.0 → 2.3.0-rc.1
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/button/index.ts +4 -0
- package/src/components/button/transforms/iconLeftToPrefix.test.ts +432 -0
- package/src/components/button/transforms/iconLeftToPrefix.ts +33 -0
- package/src/components/button/transforms/iconRightToSuffix.test.ts +407 -0
- package/src/components/button/transforms/iconRightToSuffix.ts +33 -0
- package/src/components/shared/actions/convertAttributeFromObjectToJSXElement.test.ts +139 -0
- package/src/components/shared/actions/convertAttributeFromObjectToJSXElement.ts +81 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/tapestry-migration-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0-rc.1",
|
|
4
4
|
"description": "CLI tool for Tapestry migrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "6e606540c9f564f579c3e56e2c3f844823be15c6"
|
|
51
51
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Transform } from "jscodeshift"
|
|
2
2
|
|
|
3
|
+
import iconLeftToPrefix from "./transforms/iconLeftToPrefix"
|
|
4
|
+
import iconRightToSuffix from "./transforms/iconRightToSuffix"
|
|
3
5
|
import linkToButton from "./transforms/linkToButton"
|
|
4
6
|
import moveButtonImport from "./transforms/moveButtonImport"
|
|
5
7
|
import removeToTransform from "./transforms/removeTo"
|
|
@@ -14,6 +16,8 @@ const transform: Transform = (fileInfo, api, options) => {
|
|
|
14
16
|
linkToButton,
|
|
15
17
|
titleToLabel,
|
|
16
18
|
tooltipToWrapper,
|
|
19
|
+
iconRightToSuffix,
|
|
20
|
+
iconLeftToPrefix,
|
|
17
21
|
removeToTransform,
|
|
18
22
|
moveButtonImport,
|
|
19
23
|
]
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./iconLeftToPrefix"
|
|
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("iconLeftToPrefix transform", () => {
|
|
18
|
+
describe("basic transformations", () => {
|
|
19
|
+
it("should convert iconLeft to prefix with Icon component", () => {
|
|
20
|
+
const input = `
|
|
21
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
export default function Test() {
|
|
24
|
+
return <Button iconLeft={{name: "check"}}>Save</Button>
|
|
25
|
+
}
|
|
26
|
+
`.trim()
|
|
27
|
+
|
|
28
|
+
const result = applyTransform(input)
|
|
29
|
+
|
|
30
|
+
expect(result).toContain(
|
|
31
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
32
|
+
)
|
|
33
|
+
expect(result).toContain('prefix={<Icon {...{name: "check"}} />}')
|
|
34
|
+
expect(result).toContain("<Button")
|
|
35
|
+
expect(result).toContain("Save</Button>")
|
|
36
|
+
expect(result).not.toContain("iconLeft=")
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("should handle complex icon props", () => {
|
|
40
|
+
const input = `
|
|
41
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
42
|
+
|
|
43
|
+
export default function Test() {
|
|
44
|
+
return <Button iconLeft={{name: "save", size: "sm", color: "blue"}}>Save</Button>
|
|
45
|
+
}
|
|
46
|
+
`.trim()
|
|
47
|
+
|
|
48
|
+
const result = applyTransform(input)
|
|
49
|
+
|
|
50
|
+
expect(result).toContain(
|
|
51
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
52
|
+
)
|
|
53
|
+
expect(result).toContain(
|
|
54
|
+
'prefix={<Icon {...{name: "save", size: "sm", color: "blue"}} />}'
|
|
55
|
+
)
|
|
56
|
+
expect(result).not.toContain("iconLeft=")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("should preserve other Button attributes", () => {
|
|
60
|
+
const input = `
|
|
61
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
62
|
+
|
|
63
|
+
export default function Test() {
|
|
64
|
+
return <Button iconLeft={{name: "check"}} variant="primary" onClick={handleClick} disabled>Save</Button>
|
|
65
|
+
}
|
|
66
|
+
`.trim()
|
|
67
|
+
|
|
68
|
+
const result = applyTransform(input)
|
|
69
|
+
|
|
70
|
+
expect(result).toContain('prefix={<Icon {...{name: "check"}} />}')
|
|
71
|
+
expect(result).toContain(
|
|
72
|
+
'variant="primary" onClick={handleClick} disabled'
|
|
73
|
+
)
|
|
74
|
+
expect(result).not.toContain("iconLeft=")
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("should handle self-closing Button with iconLeft", () => {
|
|
78
|
+
const input = `
|
|
79
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
80
|
+
|
|
81
|
+
export default function Test() {
|
|
82
|
+
return <Button iconLeft={{name: "check"}} />
|
|
83
|
+
}
|
|
84
|
+
`.trim()
|
|
85
|
+
|
|
86
|
+
const result = applyTransform(input)
|
|
87
|
+
|
|
88
|
+
expect(result).toContain(
|
|
89
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
90
|
+
)
|
|
91
|
+
expect(result).toContain('prefix={<Icon {...{name: "check"}} />}')
|
|
92
|
+
expect(result).not.toContain("iconLeft=")
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("should handle variable expressions", () => {
|
|
96
|
+
const input = `
|
|
97
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
98
|
+
|
|
99
|
+
export default function Test() {
|
|
100
|
+
return <Button iconLeft={iconProps}>Save</Button>
|
|
101
|
+
}
|
|
102
|
+
`.trim()
|
|
103
|
+
|
|
104
|
+
const result = applyTransform(input)
|
|
105
|
+
|
|
106
|
+
expect(result).toContain(
|
|
107
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
108
|
+
)
|
|
109
|
+
expect(result).toContain("prefix={<Icon {...iconProps} />}")
|
|
110
|
+
expect(result).not.toContain("iconLeft=")
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe("import conflict handling", () => {
|
|
115
|
+
it("should use TRIcon alias when Icon already imported from another package", () => {
|
|
116
|
+
const input = `
|
|
117
|
+
import { Icon } from "react-feather"
|
|
118
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
119
|
+
|
|
120
|
+
export default function Test() {
|
|
121
|
+
return <Button iconLeft={{name: "check"}}>Save</Button>
|
|
122
|
+
}
|
|
123
|
+
`.trim()
|
|
124
|
+
|
|
125
|
+
const result = applyTransform(input)
|
|
126
|
+
|
|
127
|
+
expect(result).toContain('import { Icon } from "react-feather"')
|
|
128
|
+
expect(result).toContain(
|
|
129
|
+
'import { Button, Icon as TRIcon } from "@planningcenter/tapestry-react"'
|
|
130
|
+
)
|
|
131
|
+
expect(result).toContain('prefix={<TRIcon {...{name: "check"}} />}')
|
|
132
|
+
expect(result).not.toContain("iconLeft=")
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it("should use TRIcon alias when Icon is default import from another package", () => {
|
|
136
|
+
const input = `
|
|
137
|
+
import Icon from "heroicons"
|
|
138
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
139
|
+
|
|
140
|
+
export default function Test() {
|
|
141
|
+
return <Button iconLeft={{name: "save"}}>Save</Button>
|
|
142
|
+
}
|
|
143
|
+
`.trim()
|
|
144
|
+
|
|
145
|
+
const result = applyTransform(input)
|
|
146
|
+
|
|
147
|
+
expect(result).toContain('import Icon from "heroicons"')
|
|
148
|
+
expect(result).toContain(
|
|
149
|
+
'import { Button, Icon as TRIcon } from "@planningcenter/tapestry-react"'
|
|
150
|
+
)
|
|
151
|
+
expect(result).toContain('prefix={<TRIcon {...{name: "save"}} />}')
|
|
152
|
+
expect(result).not.toContain("iconLeft=")
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it("should not conflict with existing tapestry-react Icon import", () => {
|
|
156
|
+
const input = `
|
|
157
|
+
import { Button, Icon } from "@planningcenter/tapestry-react"
|
|
158
|
+
|
|
159
|
+
export default function Test() {
|
|
160
|
+
return <Button iconLeft={{name: "check"}}>Save</Button>
|
|
161
|
+
}
|
|
162
|
+
`.trim()
|
|
163
|
+
|
|
164
|
+
const result = applyTransform(input)
|
|
165
|
+
|
|
166
|
+
expect(result).toContain(
|
|
167
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
168
|
+
)
|
|
169
|
+
expect(result).toContain('prefix={<Icon {...{name: "check"}} />}')
|
|
170
|
+
expect(result).not.toContain("TRIcon")
|
|
171
|
+
expect(result).not.toContain("iconLeft=")
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it("should handle existing tapestry-react import without Icon", () => {
|
|
175
|
+
const input = `
|
|
176
|
+
import { Button, Link } from "@planningcenter/tapestry-react"
|
|
177
|
+
|
|
178
|
+
export default function Test() {
|
|
179
|
+
return <Button iconLeft={{name: "settings"}}>Settings</Button>
|
|
180
|
+
}
|
|
181
|
+
`.trim()
|
|
182
|
+
|
|
183
|
+
const result = applyTransform(input)
|
|
184
|
+
|
|
185
|
+
expect(result).toContain(
|
|
186
|
+
'import { Button, Link, Icon } from "@planningcenter/tapestry-react"'
|
|
187
|
+
)
|
|
188
|
+
expect(result).toContain('prefix={<Icon {...{name: "settings"}} />}')
|
|
189
|
+
expect(result).not.toContain("iconLeft=")
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("should create new tapestry-react import when none exists", () => {
|
|
193
|
+
const input = `
|
|
194
|
+
import React from "react"
|
|
195
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
196
|
+
|
|
197
|
+
export default function Test() {
|
|
198
|
+
return <Button iconLeft={{name: "home"}}>Home</Button>
|
|
199
|
+
}
|
|
200
|
+
`.trim()
|
|
201
|
+
|
|
202
|
+
const result = applyTransform(input)
|
|
203
|
+
|
|
204
|
+
expect(result).toContain(
|
|
205
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
206
|
+
)
|
|
207
|
+
expect(result).toContain('prefix={<Icon {...{name: "home"}} />}')
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
describe("multiple elements", () => {
|
|
212
|
+
it("should handle multiple Buttons with iconLeft", () => {
|
|
213
|
+
const input = `
|
|
214
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
215
|
+
|
|
216
|
+
export default function Test() {
|
|
217
|
+
return (
|
|
218
|
+
<div>
|
|
219
|
+
<Button iconLeft={{name: "save"}}>Save</Button>
|
|
220
|
+
<Button iconLeft={{name: "cancel"}}>Cancel</Button>
|
|
221
|
+
</div>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
`.trim()
|
|
225
|
+
|
|
226
|
+
const result = applyTransform(input)
|
|
227
|
+
|
|
228
|
+
expect(result).toContain(
|
|
229
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
230
|
+
)
|
|
231
|
+
expect(result).toContain('prefix={<Icon {...{name: "save"}} />}')
|
|
232
|
+
expect(result).toContain('prefix={<Icon {...{name: "cancel"}} />}')
|
|
233
|
+
expect(result).not.toContain("iconLeft=")
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it("should only transform Buttons with iconLeft attribute", () => {
|
|
237
|
+
const input = `
|
|
238
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
239
|
+
|
|
240
|
+
export default function Test() {
|
|
241
|
+
return (
|
|
242
|
+
<div>
|
|
243
|
+
<Button iconLeft={{name: "check"}}>With Icon</Button>
|
|
244
|
+
<Button>Without Icon</Button>
|
|
245
|
+
</div>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
`.trim()
|
|
249
|
+
|
|
250
|
+
const result = applyTransform(input)
|
|
251
|
+
|
|
252
|
+
expect(result).toContain('prefix={<Icon {...{name: "check"}} />}')
|
|
253
|
+
expect(result).toContain("<Button>Without Icon</Button>")
|
|
254
|
+
// Only one prefix should be created
|
|
255
|
+
const prefixCount = (result?.match(/prefix=/g) || []).length
|
|
256
|
+
expect(prefixCount).toBe(1)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it("should handle mixed import conflicts", () => {
|
|
260
|
+
const input = `
|
|
261
|
+
import { Icon } from "lucide-react"
|
|
262
|
+
import { Button, Link } from "@planningcenter/tapestry-react"
|
|
263
|
+
|
|
264
|
+
export default function Test() {
|
|
265
|
+
return (
|
|
266
|
+
<div>
|
|
267
|
+
<Button iconLeft={{name: "save"}}>Save</Button>
|
|
268
|
+
<Button iconLeft={{name: "delete", color: "red"}}>Delete</Button>
|
|
269
|
+
<Link>Cancel</Link>
|
|
270
|
+
</div>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
`.trim()
|
|
274
|
+
|
|
275
|
+
const result = applyTransform(input)
|
|
276
|
+
|
|
277
|
+
expect(result).toContain('import { Icon } from "lucide-react"')
|
|
278
|
+
expect(result).toContain(
|
|
279
|
+
'import { Button, Link, Icon as TRIcon } from "@planningcenter/tapestry-react"'
|
|
280
|
+
)
|
|
281
|
+
expect(result).toContain('prefix={<TRIcon {...{name: "save"}} />}')
|
|
282
|
+
expect(result).toContain(
|
|
283
|
+
'prefix={<TRIcon {...{name: "delete", color: "red"}} />}'
|
|
284
|
+
)
|
|
285
|
+
expect(result).toContain("<Link>Cancel</Link>")
|
|
286
|
+
expect(result).not.toContain("iconLeft=")
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe("edge cases", () => {
|
|
291
|
+
it("should return null when Button is not imported from tapestry-react", () => {
|
|
292
|
+
const input = `
|
|
293
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
294
|
+
import { Button } from "other-library"
|
|
295
|
+
|
|
296
|
+
export default function Test() {
|
|
297
|
+
return <Button iconLeft={{name: "check"}}>Save</Button>
|
|
298
|
+
}
|
|
299
|
+
`.trim()
|
|
300
|
+
|
|
301
|
+
const result = applyTransform(input)
|
|
302
|
+
expect(result).toBe(null)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it("should return null when no Button is imported", () => {
|
|
306
|
+
const input = `
|
|
307
|
+
import React from "react"
|
|
308
|
+
|
|
309
|
+
export default function Test() {
|
|
310
|
+
return <div>No buttons here</div>
|
|
311
|
+
}
|
|
312
|
+
`.trim()
|
|
313
|
+
|
|
314
|
+
const result = applyTransform(input)
|
|
315
|
+
expect(result).toBe(null)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it("should return null when no iconLeft attributes are present", () => {
|
|
319
|
+
const input = `
|
|
320
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
321
|
+
|
|
322
|
+
export default function Test() {
|
|
323
|
+
return <Button variant="primary">Save</Button>
|
|
324
|
+
}
|
|
325
|
+
`.trim()
|
|
326
|
+
|
|
327
|
+
const result = applyTransform(input)
|
|
328
|
+
expect(result).toBe(null)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it("should handle aliased Button import", () => {
|
|
332
|
+
const input = `
|
|
333
|
+
import { Button as TapestryButton } from "@planningcenter/tapestry-react"
|
|
334
|
+
|
|
335
|
+
export default function Test() {
|
|
336
|
+
return <TapestryButton iconLeft={{name: "settings"}}>Settings</TapestryButton>
|
|
337
|
+
}
|
|
338
|
+
`.trim()
|
|
339
|
+
|
|
340
|
+
const result = applyTransform(input)
|
|
341
|
+
|
|
342
|
+
expect(result).toContain(
|
|
343
|
+
'import { Button as TapestryButton, Icon } from "@planningcenter/tapestry-react"'
|
|
344
|
+
)
|
|
345
|
+
expect(result).toContain('prefix={<Icon {...{name: "settings"}} />}')
|
|
346
|
+
expect(result).toContain("<TapestryButton")
|
|
347
|
+
expect(result).not.toContain("iconLeft=")
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it("should handle empty iconLeft attribute", () => {
|
|
351
|
+
const input = `
|
|
352
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
353
|
+
|
|
354
|
+
export default function Test() {
|
|
355
|
+
return <Button iconLeft>Save</Button>
|
|
356
|
+
}
|
|
357
|
+
`.trim()
|
|
358
|
+
|
|
359
|
+
const result = applyTransform(input)
|
|
360
|
+
// Should not transform empty iconLeft attribute
|
|
361
|
+
expect(result).toBe(null)
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
describe("complex scenarios", () => {
|
|
366
|
+
it("should handle nested JSX and multiple import conflicts", () => {
|
|
367
|
+
const input = `
|
|
368
|
+
import React from "react"
|
|
369
|
+
import { Icon as FeatherIcon } from "react-feather"
|
|
370
|
+
import Icon from "heroicons"
|
|
371
|
+
import { Button, Link } from "@planningcenter/tapestry-react"
|
|
372
|
+
|
|
373
|
+
export default function Test() {
|
|
374
|
+
return (
|
|
375
|
+
<div>
|
|
376
|
+
<Button iconLeft={{name: "save", size: "lg"}} variant="primary">
|
|
377
|
+
Save Document
|
|
378
|
+
</Button>
|
|
379
|
+
<Button iconLeft={{name: "trash", color: "danger"}} variant="outline">
|
|
380
|
+
Delete
|
|
381
|
+
</Button>
|
|
382
|
+
<Link href="/cancel">Cancel</Link>
|
|
383
|
+
</div>
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
`.trim()
|
|
387
|
+
|
|
388
|
+
const result = applyTransform(input)
|
|
389
|
+
|
|
390
|
+
expect(result).toContain(
|
|
391
|
+
'import { Icon as FeatherIcon } from "react-feather"'
|
|
392
|
+
)
|
|
393
|
+
expect(result).toContain('import Icon from "heroicons"')
|
|
394
|
+
expect(result).toContain(
|
|
395
|
+
'import { Button, Link, Icon as TRIcon } from "@planningcenter/tapestry-react"'
|
|
396
|
+
)
|
|
397
|
+
expect(result).toContain(
|
|
398
|
+
'prefix={<TRIcon {...{name: "save", size: "lg"}} />}'
|
|
399
|
+
)
|
|
400
|
+
expect(result).toContain(
|
|
401
|
+
'prefix={<TRIcon {...{name: "trash", color: "danger"}} />}'
|
|
402
|
+
)
|
|
403
|
+
expect(result).toContain('<Link href="/cancel">Cancel</Link>')
|
|
404
|
+
expect(result).not.toContain("iconLeft=")
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('converts string values to {{ name: "value" }}', () => {
|
|
409
|
+
const input = `
|
|
410
|
+
import React from "react"
|
|
411
|
+
import { Button} from "@planningcenter/tapestry-react"
|
|
412
|
+
|
|
413
|
+
export default function Test() {
|
|
414
|
+
return (
|
|
415
|
+
<div>
|
|
416
|
+
<Button iconLeft="save" variant="primary">
|
|
417
|
+
Save Document
|
|
418
|
+
</Button>
|
|
419
|
+
</div>
|
|
420
|
+
)
|
|
421
|
+
}`.trim()
|
|
422
|
+
|
|
423
|
+
const result = applyTransform(input)
|
|
424
|
+
|
|
425
|
+
expect(result).not.toBeNull()
|
|
426
|
+
expect(result).toContain(`prefix={<Icon
|
|
427
|
+
{...{
|
|
428
|
+
name: "save"
|
|
429
|
+
}} />}`)
|
|
430
|
+
expect(result).not.toContain("iconLeft=")
|
|
431
|
+
})
|
|
432
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { convertAttributeFromObjectToJSXElement } from "../../shared/actions/convertAttributeFromObjectToJSXElement"
|
|
2
|
+
import { transformAttributeName } from "../../shared/actions/transformAttributeName"
|
|
3
|
+
import { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
4
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
5
|
+
import { addImport } from "../../shared/transformFactories/helpers/manageImports"
|
|
6
|
+
|
|
7
|
+
const transform = attributeTransformFactory({
|
|
8
|
+
condition: hasAttribute("iconLeft"),
|
|
9
|
+
targetComponent: "Button",
|
|
10
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
11
|
+
transform: (element, { j, source }) => {
|
|
12
|
+
const name = addImport({
|
|
13
|
+
component: "Icon",
|
|
14
|
+
conflictAlias: "TRIcon",
|
|
15
|
+
j,
|
|
16
|
+
pkg: "@planningcenter/tapestry-react",
|
|
17
|
+
source,
|
|
18
|
+
})
|
|
19
|
+
const updatedElement = convertAttributeFromObjectToJSXElement({
|
|
20
|
+
attributeName: "iconLeft",
|
|
21
|
+
element,
|
|
22
|
+
elementName: name,
|
|
23
|
+
j,
|
|
24
|
+
stringValueKey: "name",
|
|
25
|
+
})
|
|
26
|
+
if (!updatedElement) return false
|
|
27
|
+
|
|
28
|
+
transformAttributeName("iconLeft", "prefix", { element })
|
|
29
|
+
return true
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export default transform
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./iconRightToSuffix"
|
|
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("iconRightToSuffix transform", () => {
|
|
18
|
+
describe("basic transformations", () => {
|
|
19
|
+
it("should convert iconRight to suffix with Icon component", () => {
|
|
20
|
+
const input = `
|
|
21
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
export default function Test() {
|
|
24
|
+
return <Button iconRight={{name: "check"}}>Save</Button>
|
|
25
|
+
}
|
|
26
|
+
`.trim()
|
|
27
|
+
|
|
28
|
+
const result = applyTransform(input)
|
|
29
|
+
|
|
30
|
+
expect(result).toContain(
|
|
31
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
32
|
+
)
|
|
33
|
+
expect(result).toContain('suffix={<Icon {...{name: "check"}} />}')
|
|
34
|
+
expect(result).toContain("<Button")
|
|
35
|
+
expect(result).toContain("Save</Button>")
|
|
36
|
+
expect(result).not.toContain("iconRight=")
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("should handle complex icon props", () => {
|
|
40
|
+
const input = `
|
|
41
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
42
|
+
|
|
43
|
+
export default function Test() {
|
|
44
|
+
return <Button iconRight={{name: "save", size: "sm", color: "blue"}}>Save</Button>
|
|
45
|
+
}
|
|
46
|
+
`.trim()
|
|
47
|
+
|
|
48
|
+
const result = applyTransform(input)
|
|
49
|
+
|
|
50
|
+
expect(result).toContain(
|
|
51
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
52
|
+
)
|
|
53
|
+
expect(result).toContain(
|
|
54
|
+
'suffix={<Icon {...{name: "save", size: "sm", color: "blue"}} />}'
|
|
55
|
+
)
|
|
56
|
+
expect(result).not.toContain("iconRight=")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("should preserve other Button attributes", () => {
|
|
60
|
+
const input = `
|
|
61
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
62
|
+
|
|
63
|
+
export default function Test() {
|
|
64
|
+
return <Button iconRight={{name: "check"}} variant="primary" onClick={handleClick} disabled>Save</Button>
|
|
65
|
+
}
|
|
66
|
+
`.trim()
|
|
67
|
+
|
|
68
|
+
const result = applyTransform(input)
|
|
69
|
+
|
|
70
|
+
expect(result).toContain('suffix={<Icon {...{name: "check"}} />}')
|
|
71
|
+
expect(result).toContain(
|
|
72
|
+
'variant="primary" onClick={handleClick} disabled'
|
|
73
|
+
)
|
|
74
|
+
expect(result).not.toContain("iconRight=")
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("should handle self-closing Button with iconRight", () => {
|
|
78
|
+
const input = `
|
|
79
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
80
|
+
|
|
81
|
+
export default function Test() {
|
|
82
|
+
return <Button iconRight={{name: "check"}} />
|
|
83
|
+
}
|
|
84
|
+
`.trim()
|
|
85
|
+
|
|
86
|
+
const result = applyTransform(input)
|
|
87
|
+
|
|
88
|
+
expect(result).toContain(
|
|
89
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
90
|
+
)
|
|
91
|
+
expect(result).toContain('suffix={<Icon {...{name: "check"}} />}')
|
|
92
|
+
expect(result).not.toContain("iconRight=")
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("should handle variable expressions", () => {
|
|
96
|
+
const input = `
|
|
97
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
98
|
+
|
|
99
|
+
export default function Test() {
|
|
100
|
+
return <Button iconRight={iconProps}>Save</Button>
|
|
101
|
+
}
|
|
102
|
+
`.trim()
|
|
103
|
+
|
|
104
|
+
const result = applyTransform(input)
|
|
105
|
+
|
|
106
|
+
expect(result).toContain(
|
|
107
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
108
|
+
)
|
|
109
|
+
expect(result).toContain("suffix={<Icon {...iconProps} />}")
|
|
110
|
+
expect(result).not.toContain("iconRight=")
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe("import conflict handling", () => {
|
|
115
|
+
it("should use TRIcon alias when Icon already imported from another package", () => {
|
|
116
|
+
const input = `
|
|
117
|
+
import { Icon } from "react-feather"
|
|
118
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
119
|
+
|
|
120
|
+
export default function Test() {
|
|
121
|
+
return <Button iconRight={{name: "check"}}>Save</Button>
|
|
122
|
+
}
|
|
123
|
+
`.trim()
|
|
124
|
+
|
|
125
|
+
const result = applyTransform(input)
|
|
126
|
+
|
|
127
|
+
expect(result).toContain('import { Icon } from "react-feather"')
|
|
128
|
+
expect(result).toContain(
|
|
129
|
+
'import { Button, Icon as TRIcon } from "@planningcenter/tapestry-react"'
|
|
130
|
+
)
|
|
131
|
+
expect(result).toContain('suffix={<TRIcon {...{name: "check"}} />}')
|
|
132
|
+
expect(result).not.toContain("iconRight=")
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it("should use TRIcon alias when Icon is default import from another package", () => {
|
|
136
|
+
const input = `
|
|
137
|
+
import Icon from "heroicons"
|
|
138
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
139
|
+
|
|
140
|
+
export default function Test() {
|
|
141
|
+
return <Button iconRight={{name: "save"}}>Save</Button>
|
|
142
|
+
}
|
|
143
|
+
`.trim()
|
|
144
|
+
|
|
145
|
+
const result = applyTransform(input)
|
|
146
|
+
|
|
147
|
+
expect(result).toContain('import Icon from "heroicons"')
|
|
148
|
+
expect(result).toContain(
|
|
149
|
+
'import { Button, Icon as TRIcon } from "@planningcenter/tapestry-react"'
|
|
150
|
+
)
|
|
151
|
+
expect(result).toContain('suffix={<TRIcon {...{name: "save"}} />}')
|
|
152
|
+
expect(result).not.toContain("iconRight=")
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it("should not conflict with existing tapestry-react Icon import", () => {
|
|
156
|
+
const input = `
|
|
157
|
+
import { Button, Icon } from "@planningcenter/tapestry-react"
|
|
158
|
+
|
|
159
|
+
export default function Test() {
|
|
160
|
+
return <Button iconRight={{name: "check"}}>Save</Button>
|
|
161
|
+
}
|
|
162
|
+
`.trim()
|
|
163
|
+
|
|
164
|
+
const result = applyTransform(input)
|
|
165
|
+
|
|
166
|
+
expect(result).toContain(
|
|
167
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
168
|
+
)
|
|
169
|
+
expect(result).toContain('suffix={<Icon {...{name: "check"}} />}')
|
|
170
|
+
expect(result).not.toContain("TRIcon")
|
|
171
|
+
expect(result).not.toContain("iconRight=")
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it("should handle existing tapestry-react import without Icon", () => {
|
|
175
|
+
const input = `
|
|
176
|
+
import { Button, Link } from "@planningcenter/tapestry-react"
|
|
177
|
+
|
|
178
|
+
export default function Test() {
|
|
179
|
+
return <Button iconRight={{name: "settings"}}>Settings</Button>
|
|
180
|
+
}
|
|
181
|
+
`.trim()
|
|
182
|
+
|
|
183
|
+
const result = applyTransform(input)
|
|
184
|
+
|
|
185
|
+
expect(result).toContain(
|
|
186
|
+
'import { Button, Link, Icon } from "@planningcenter/tapestry-react"'
|
|
187
|
+
)
|
|
188
|
+
expect(result).toContain('suffix={<Icon {...{name: "settings"}} />}')
|
|
189
|
+
expect(result).not.toContain("iconRight=")
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("should create new tapestry-react import when none exists", () => {
|
|
193
|
+
const input = `
|
|
194
|
+
import React from "react"
|
|
195
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
196
|
+
|
|
197
|
+
export default function Test() {
|
|
198
|
+
return <Button iconRight={{name: "home"}}>Home</Button>
|
|
199
|
+
}
|
|
200
|
+
`.trim()
|
|
201
|
+
|
|
202
|
+
const result = applyTransform(input)
|
|
203
|
+
|
|
204
|
+
expect(result).toContain(
|
|
205
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
206
|
+
)
|
|
207
|
+
expect(result).toContain('suffix={<Icon {...{name: "home"}} />}')
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
describe("multiple elements", () => {
|
|
212
|
+
it("should handle multiple Buttons with iconRight", () => {
|
|
213
|
+
const input = `
|
|
214
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
215
|
+
|
|
216
|
+
export default function Test() {
|
|
217
|
+
return (
|
|
218
|
+
<div>
|
|
219
|
+
<Button iconRight={{name: "save"}}>Save</Button>
|
|
220
|
+
<Button iconRight={{name: "cancel"}}>Cancel</Button>
|
|
221
|
+
</div>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
`.trim()
|
|
225
|
+
|
|
226
|
+
const result = applyTransform(input)
|
|
227
|
+
|
|
228
|
+
expect(result).toContain(
|
|
229
|
+
'import { Button, Icon } from "@planningcenter/tapestry-react"'
|
|
230
|
+
)
|
|
231
|
+
expect(result).toContain('suffix={<Icon {...{name: "save"}} />}')
|
|
232
|
+
expect(result).toContain('suffix={<Icon {...{name: "cancel"}} />}')
|
|
233
|
+
expect(result).not.toContain("iconRight=")
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it("should only transform Buttons with iconRight attribute", () => {
|
|
237
|
+
const input = `
|
|
238
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
239
|
+
|
|
240
|
+
export default function Test() {
|
|
241
|
+
return (
|
|
242
|
+
<div>
|
|
243
|
+
<Button iconRight={{name: "check"}}>With Icon</Button>
|
|
244
|
+
<Button>Without Icon</Button>
|
|
245
|
+
</div>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
`.trim()
|
|
249
|
+
|
|
250
|
+
const result = applyTransform(input)
|
|
251
|
+
|
|
252
|
+
expect(result).toContain('suffix={<Icon {...{name: "check"}} />}')
|
|
253
|
+
expect(result).toContain("<Button>Without Icon</Button>")
|
|
254
|
+
// Only one suffix should be created
|
|
255
|
+
const prefixCount = (result?.match(/suffix=/g) || []).length
|
|
256
|
+
expect(prefixCount).toBe(1)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it("should handle mixed import conflicts", () => {
|
|
260
|
+
const input = `
|
|
261
|
+
import { Icon } from "lucide-react"
|
|
262
|
+
import { Button, Link } from "@planningcenter/tapestry-react"
|
|
263
|
+
|
|
264
|
+
export default function Test() {
|
|
265
|
+
return (
|
|
266
|
+
<div>
|
|
267
|
+
<Button iconRight={{name: "save"}}>Save</Button>
|
|
268
|
+
<Button iconRight={{name: "delete", color: "red"}}>Delete</Button>
|
|
269
|
+
<Link>Cancel</Link>
|
|
270
|
+
</div>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
`.trim()
|
|
274
|
+
|
|
275
|
+
const result = applyTransform(input)
|
|
276
|
+
|
|
277
|
+
expect(result).toContain('import { Icon } from "lucide-react"')
|
|
278
|
+
expect(result).toContain(
|
|
279
|
+
'import { Button, Link, Icon as TRIcon } from "@planningcenter/tapestry-react"'
|
|
280
|
+
)
|
|
281
|
+
expect(result).toContain('suffix={<TRIcon {...{name: "save"}} />}')
|
|
282
|
+
expect(result).toContain(
|
|
283
|
+
'suffix={<TRIcon {...{name: "delete", color: "red"}} />}'
|
|
284
|
+
)
|
|
285
|
+
expect(result).toContain("<Link>Cancel</Link>")
|
|
286
|
+
expect(result).not.toContain("iconRight=")
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe("edge cases", () => {
|
|
291
|
+
it("should return null when Button is not imported from tapestry-react", () => {
|
|
292
|
+
const input = `
|
|
293
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
294
|
+
import { Button } from "other-library"
|
|
295
|
+
|
|
296
|
+
export default function Test() {
|
|
297
|
+
return <Button iconRight={{name: "check"}}>Save</Button>
|
|
298
|
+
}
|
|
299
|
+
`.trim()
|
|
300
|
+
|
|
301
|
+
const result = applyTransform(input)
|
|
302
|
+
expect(result).toBe(null)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it("should return null when no Button is imported", () => {
|
|
306
|
+
const input = `
|
|
307
|
+
import React from "react"
|
|
308
|
+
|
|
309
|
+
export default function Test() {
|
|
310
|
+
return <div>No buttons here</div>
|
|
311
|
+
}
|
|
312
|
+
`.trim()
|
|
313
|
+
|
|
314
|
+
const result = applyTransform(input)
|
|
315
|
+
expect(result).toBe(null)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it("should return null when no iconRight attributes are present", () => {
|
|
319
|
+
const input = `
|
|
320
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
321
|
+
|
|
322
|
+
export default function Test() {
|
|
323
|
+
return <Button variant="primary">Save</Button>
|
|
324
|
+
}
|
|
325
|
+
`.trim()
|
|
326
|
+
|
|
327
|
+
const result = applyTransform(input)
|
|
328
|
+
expect(result).toBe(null)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it("should handle aliased Button import", () => {
|
|
332
|
+
const input = `
|
|
333
|
+
import { Button as TapestryButton } from "@planningcenter/tapestry-react"
|
|
334
|
+
|
|
335
|
+
export default function Test() {
|
|
336
|
+
return <TapestryButton iconRight={{name: "settings"}}>Settings</TapestryButton>
|
|
337
|
+
}
|
|
338
|
+
`.trim()
|
|
339
|
+
|
|
340
|
+
const result = applyTransform(input)
|
|
341
|
+
|
|
342
|
+
expect(result).toContain(
|
|
343
|
+
'import { Button as TapestryButton, Icon } from "@planningcenter/tapestry-react"'
|
|
344
|
+
)
|
|
345
|
+
expect(result).toContain('suffix={<Icon {...{name: "settings"}} />}')
|
|
346
|
+
expect(result).toContain("<TapestryButton")
|
|
347
|
+
expect(result).not.toContain("iconRight=")
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it("should handle empty iconRight attribute", () => {
|
|
351
|
+
const input = `
|
|
352
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
353
|
+
|
|
354
|
+
export default function Test() {
|
|
355
|
+
return <Button iconRight>Save</Button>
|
|
356
|
+
}
|
|
357
|
+
`.trim()
|
|
358
|
+
|
|
359
|
+
const result = applyTransform(input)
|
|
360
|
+
// Should not transform empty iconRight attribute
|
|
361
|
+
expect(result).toBe(null)
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
describe("complex scenarios", () => {
|
|
366
|
+
it("should handle nested JSX and multiple import conflicts", () => {
|
|
367
|
+
const input = `
|
|
368
|
+
import React from "react"
|
|
369
|
+
import { Icon as FeatherIcon } from "react-feather"
|
|
370
|
+
import Icon from "heroicons"
|
|
371
|
+
import { Button, Link } from "@planningcenter/tapestry-react"
|
|
372
|
+
|
|
373
|
+
export default function Test() {
|
|
374
|
+
return (
|
|
375
|
+
<div>
|
|
376
|
+
<Button iconRight={{name: "save", size: "lg"}} variant="primary">
|
|
377
|
+
Save Document
|
|
378
|
+
</Button>
|
|
379
|
+
<Button iconRight={{name: "trash", color: "danger"}} variant="outline">
|
|
380
|
+
Delete
|
|
381
|
+
</Button>
|
|
382
|
+
<Link href="/cancel">Cancel</Link>
|
|
383
|
+
</div>
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
`.trim()
|
|
387
|
+
|
|
388
|
+
const result = applyTransform(input)
|
|
389
|
+
|
|
390
|
+
expect(result).toContain(
|
|
391
|
+
'import { Icon as FeatherIcon } from "react-feather"'
|
|
392
|
+
)
|
|
393
|
+
expect(result).toContain('import Icon from "heroicons"')
|
|
394
|
+
expect(result).toContain(
|
|
395
|
+
'import { Button, Link, Icon as TRIcon } from "@planningcenter/tapestry-react"'
|
|
396
|
+
)
|
|
397
|
+
expect(result).toContain(
|
|
398
|
+
'suffix={<TRIcon {...{name: "save", size: "lg"}} />}'
|
|
399
|
+
)
|
|
400
|
+
expect(result).toContain(
|
|
401
|
+
'suffix={<TRIcon {...{name: "trash", color: "danger"}} />}'
|
|
402
|
+
)
|
|
403
|
+
expect(result).toContain('<Link href="/cancel">Cancel</Link>')
|
|
404
|
+
expect(result).not.toContain("iconRight=")
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { convertAttributeFromObjectToJSXElement } from "../../shared/actions/convertAttributeFromObjectToJSXElement"
|
|
2
|
+
import { transformAttributeName } from "../../shared/actions/transformAttributeName"
|
|
3
|
+
import { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
4
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
5
|
+
import { addImport } from "../../shared/transformFactories/helpers/manageImports"
|
|
6
|
+
|
|
7
|
+
const transform = attributeTransformFactory({
|
|
8
|
+
condition: hasAttribute("iconRight"),
|
|
9
|
+
targetComponent: "Button",
|
|
10
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
11
|
+
transform: (element, { j, source }) => {
|
|
12
|
+
const name = addImport({
|
|
13
|
+
component: "Icon",
|
|
14
|
+
conflictAlias: "TRIcon",
|
|
15
|
+
j,
|
|
16
|
+
pkg: "@planningcenter/tapestry-react",
|
|
17
|
+
source,
|
|
18
|
+
})
|
|
19
|
+
const updatedElement = convertAttributeFromObjectToJSXElement({
|
|
20
|
+
attributeName: "iconRight",
|
|
21
|
+
element,
|
|
22
|
+
elementName: name,
|
|
23
|
+
j,
|
|
24
|
+
stringValueKey: "name",
|
|
25
|
+
})
|
|
26
|
+
if (!updatedElement) return false
|
|
27
|
+
|
|
28
|
+
transformAttributeName("iconRight", "suffix", { element })
|
|
29
|
+
return true
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export default transform
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import jscodeshift, {
|
|
2
|
+
JSXAttribute,
|
|
3
|
+
JSXElement,
|
|
4
|
+
JSXExpressionContainer,
|
|
5
|
+
JSXIdentifier,
|
|
6
|
+
} from "jscodeshift"
|
|
7
|
+
import { describe, expect, it } from "vitest"
|
|
8
|
+
|
|
9
|
+
import { convertAttributeFromObjectToJSXElement } from "./convertAttributeFromObjectToJSXElement"
|
|
10
|
+
|
|
11
|
+
const j = jscodeshift.withParser("tsx")
|
|
12
|
+
|
|
13
|
+
function createSource(code: string) {
|
|
14
|
+
return j(code)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createElementFromCode(code: string): JSXElement {
|
|
18
|
+
const source = createSource(`<div>${code}</div>`)
|
|
19
|
+
return source.find(j.JSXElement).at(0).get().value.children?.[0] as JSXElement
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("convertAttributeFromObjectToJSXElement", () => {
|
|
23
|
+
it("should convert object attribute to JSX element", () => {
|
|
24
|
+
const element = createElementFromCode(
|
|
25
|
+
'<Button iconLeft={{ name: "star", size: 16 }} />'
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const result = convertAttributeFromObjectToJSXElement({
|
|
29
|
+
attributeName: "iconLeft",
|
|
30
|
+
element,
|
|
31
|
+
elementName: "Icon",
|
|
32
|
+
j,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
expect(result).not.toBeNull()
|
|
36
|
+
expect(result?.type).toBe("JSXElement")
|
|
37
|
+
expect((result?.openingElement.name as JSXIdentifier).name).toBe("Icon")
|
|
38
|
+
expect(result?.openingElement.selfClosing).toBe(true)
|
|
39
|
+
expect(result?.openingElement.attributes).toHaveLength(1)
|
|
40
|
+
expect(result?.openingElement.attributes?.[0].type).toBe(
|
|
41
|
+
"JSXSpreadAttribute"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// Check that the attribute still exists but now has JSX element value
|
|
45
|
+
const iconLeftAttr = element.openingElement.attributes?.find(
|
|
46
|
+
(attr) => attr.type === "JSXAttribute" && attr.name?.name === "iconLeft"
|
|
47
|
+
) as JSXAttribute
|
|
48
|
+
expect(iconLeftAttr).toBeDefined()
|
|
49
|
+
expect(iconLeftAttr?.value?.type).toBe("JSXExpressionContainer")
|
|
50
|
+
expect(
|
|
51
|
+
(iconLeftAttr?.value as JSXExpressionContainer)?.expression?.type
|
|
52
|
+
).toBe("JSXElement")
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("should handle expression container attributes", () => {
|
|
56
|
+
const element = createElementFromCode("<Button iconLeft={iconProps} />")
|
|
57
|
+
|
|
58
|
+
const result = convertAttributeFromObjectToJSXElement({
|
|
59
|
+
attributeName: "iconLeft",
|
|
60
|
+
element,
|
|
61
|
+
elementName: "Icon",
|
|
62
|
+
j,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(result).not.toBeNull()
|
|
66
|
+
expect(result?.openingElement.attributes).toHaveLength(1)
|
|
67
|
+
expect(result?.openingElement.attributes?.[0].type).toBe(
|
|
68
|
+
"JSXSpreadAttribute"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// Check that the attribute still exists but now has JSX element value
|
|
72
|
+
const iconLeftAttr = element.openingElement.attributes?.find(
|
|
73
|
+
(attr) => attr.type === "JSXAttribute" && attr.name?.name === "iconLeft"
|
|
74
|
+
) as JSXAttribute
|
|
75
|
+
expect(iconLeftAttr).toBeDefined()
|
|
76
|
+
expect(iconLeftAttr?.value?.type).toBe("JSXExpressionContainer")
|
|
77
|
+
expect(
|
|
78
|
+
(iconLeftAttr?.value as JSXExpressionContainer)?.expression?.type
|
|
79
|
+
).toBe("JSXElement")
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("should return null for empty expression container", () => {
|
|
83
|
+
const element = createElementFromCode("<Button iconLeft={{}} />")
|
|
84
|
+
|
|
85
|
+
const result = convertAttributeFromObjectToJSXElement({
|
|
86
|
+
attributeName: "iconLeft",
|
|
87
|
+
element,
|
|
88
|
+
elementName: "Icon",
|
|
89
|
+
j,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Should return null since empty object has no props to spread
|
|
93
|
+
expect(result).toBeNull()
|
|
94
|
+
|
|
95
|
+
// Check that the attribute was not modified since no conversion occurred
|
|
96
|
+
const iconLeftAttr = element.openingElement.attributes?.find(
|
|
97
|
+
(attr) => attr.type === "JSXAttribute" && attr.name?.name === "iconLeft"
|
|
98
|
+
) as JSXAttribute
|
|
99
|
+
expect(iconLeftAttr).toBeDefined()
|
|
100
|
+
expect(iconLeftAttr?.value?.type).toBe("JSXExpressionContainer")
|
|
101
|
+
expect(
|
|
102
|
+
(iconLeftAttr?.value as JSXExpressionContainer)?.expression?.type
|
|
103
|
+
).toBe("ObjectExpression")
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("should return null for missing attribute", () => {
|
|
107
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
108
|
+
|
|
109
|
+
const result = convertAttributeFromObjectToJSXElement({
|
|
110
|
+
attributeName: "iconLeft",
|
|
111
|
+
element,
|
|
112
|
+
elementName: "Icon",
|
|
113
|
+
j,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(result).toBeNull()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it("should return null for attribute with no value", () => {
|
|
120
|
+
const element = j.jsxElement(
|
|
121
|
+
j.jsxOpeningElement(
|
|
122
|
+
j.jsxIdentifier("Button"),
|
|
123
|
+
[j.jsxAttribute(j.jsxIdentifier("iconLeft"), null)],
|
|
124
|
+
false
|
|
125
|
+
),
|
|
126
|
+
j.jsxClosingElement(j.jsxIdentifier("Button")),
|
|
127
|
+
[j.jsxText("Save")]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const result = convertAttributeFromObjectToJSXElement({
|
|
131
|
+
attributeName: "iconLeft",
|
|
132
|
+
element,
|
|
133
|
+
elementName: "Icon",
|
|
134
|
+
j,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
expect(result).toBeNull()
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JSCodeshift,
|
|
3
|
+
JSXAttribute,
|
|
4
|
+
JSXElement,
|
|
5
|
+
JSXSpreadAttribute,
|
|
6
|
+
} from "jscodeshift"
|
|
7
|
+
|
|
8
|
+
import { getAttribute } from "./getAttribute"
|
|
9
|
+
|
|
10
|
+
export function convertAttributeFromObjectToJSXElement({
|
|
11
|
+
attributeName,
|
|
12
|
+
element,
|
|
13
|
+
elementName,
|
|
14
|
+
j,
|
|
15
|
+
stringValueKey,
|
|
16
|
+
}: {
|
|
17
|
+
attributeName: string
|
|
18
|
+
element: JSXElement
|
|
19
|
+
elementName: string
|
|
20
|
+
j: JSCodeshift
|
|
21
|
+
stringValueKey?: string
|
|
22
|
+
}): JSXElement | null {
|
|
23
|
+
const attribute = getAttribute({ element, name: attributeName })
|
|
24
|
+
if (!attribute || !attribute.value) return null
|
|
25
|
+
|
|
26
|
+
const elementProps = buildProps({ attribute, j, stringValueKey })
|
|
27
|
+
if (!elementProps) return null
|
|
28
|
+
|
|
29
|
+
const newElement = j.jsxElement(
|
|
30
|
+
j.jsxOpeningElement(j.jsxIdentifier(elementName), elementProps, true),
|
|
31
|
+
null,
|
|
32
|
+
[]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
attribute.value = j.jsxExpressionContainer(newElement)
|
|
36
|
+
|
|
37
|
+
return newElement
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildProps({
|
|
41
|
+
attribute,
|
|
42
|
+
j,
|
|
43
|
+
stringValueKey,
|
|
44
|
+
}: {
|
|
45
|
+
attribute: JSXAttribute
|
|
46
|
+
j: JSCodeshift
|
|
47
|
+
stringValueKey?: string
|
|
48
|
+
}): JSXSpreadAttribute[] | null {
|
|
49
|
+
if (!attribute.value) return null
|
|
50
|
+
if (attribute.value.type == "StringLiteral")
|
|
51
|
+
return stringToProps({ attribute, j, stringValueKey })
|
|
52
|
+
if (attribute.value.type !== "JSXExpressionContainer") return null
|
|
53
|
+
if (attribute.value.expression.type === "JSXEmptyExpression") return null
|
|
54
|
+
if (
|
|
55
|
+
attribute.value.expression.type === "ObjectExpression" &&
|
|
56
|
+
attribute.value.expression.properties.length === 0
|
|
57
|
+
)
|
|
58
|
+
return null
|
|
59
|
+
return [j.jsxSpreadAttribute(attribute.value.expression)]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function stringToProps({
|
|
63
|
+
attribute,
|
|
64
|
+
j,
|
|
65
|
+
stringValueKey,
|
|
66
|
+
}: {
|
|
67
|
+
attribute: JSXAttribute
|
|
68
|
+
j: JSCodeshift
|
|
69
|
+
stringValueKey?: string
|
|
70
|
+
}): JSXSpreadAttribute[] | null {
|
|
71
|
+
if (!attribute.value) return null
|
|
72
|
+
if (stringValueKey === undefined) return null
|
|
73
|
+
|
|
74
|
+
return [
|
|
75
|
+
j.jsxSpreadAttribute(
|
|
76
|
+
j.objectExpression([
|
|
77
|
+
j.objectProperty(j.identifier(stringValueKey), attribute.value),
|
|
78
|
+
])
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
}
|