@planningcenter/tapestry-migration-cli 2.2.0-rc.5 → 2.2.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/package.json +2 -2
- package/src/components/button/index.ts +2 -0
- package/src/components/button/transforms/tooltipToWrapper.test.ts +392 -0
- package/src/components/button/transforms/tooltipToWrapper.ts +35 -0
- package/src/components/shared/actions/createWrapper.test.ts +642 -0
- package/src/components/shared/actions/createWrapper.ts +70 -0
- package/src/components/shared/actions/getAttribute.ts +18 -0
- package/src/components/shared/actions/getAttributeValueAsProps.ts +57 -0
- package/src/components/shared/transformFactories/helpers/addImport.test.ts +278 -0
- package/src/components/shared/transformFactories/helpers/manageImports.ts +53 -20
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import jscodeshift, { StringLiteral } from "jscodeshift"
|
|
2
|
+
import {
|
|
3
|
+
ImportSpecifier,
|
|
4
|
+
JSXAttribute,
|
|
5
|
+
JSXElement,
|
|
6
|
+
JSXIdentifier,
|
|
7
|
+
} from "jscodeshift"
|
|
8
|
+
import { describe, expect, it } from "vitest"
|
|
9
|
+
|
|
10
|
+
import { createWrapper } from "./createWrapper"
|
|
11
|
+
|
|
12
|
+
const j = jscodeshift.withParser("tsx")
|
|
13
|
+
|
|
14
|
+
function createSource(code: string) {
|
|
15
|
+
return j(code)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createElementFromCode(code: string) {
|
|
19
|
+
const source = createSource(`<div>${code}</div>`)
|
|
20
|
+
const element = source.find(j.JSXElement).at(0).get().value.children?.[0]
|
|
21
|
+
return element
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("createWrapper", () => {
|
|
25
|
+
describe("basic wrapper creation", () => {
|
|
26
|
+
it("should wrap element with simple wrapper", () => {
|
|
27
|
+
const source = createSource('import React from "react"')
|
|
28
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
29
|
+
|
|
30
|
+
const result = createWrapper({
|
|
31
|
+
conflictAlias: "TRTooltip",
|
|
32
|
+
element,
|
|
33
|
+
j,
|
|
34
|
+
source,
|
|
35
|
+
wrapperName: "Tooltip",
|
|
36
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
expect(result.type).toBe("JSXElement")
|
|
40
|
+
expect((result.openingElement.name as JSXIdentifier).name).toBe("Tooltip")
|
|
41
|
+
expect((result.closingElement?.name as JSXIdentifier)?.name).toBe(
|
|
42
|
+
"Tooltip"
|
|
43
|
+
)
|
|
44
|
+
expect(result.children).toHaveLength(1)
|
|
45
|
+
const wrappedElement = result.children?.[0] as JSXElement
|
|
46
|
+
expect(wrappedElement.type).toBe("JSXElement")
|
|
47
|
+
expect((wrappedElement.openingElement.name as JSXIdentifier).name).toBe(
|
|
48
|
+
"Button"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Check that import was added
|
|
52
|
+
const imports = source.find(j.ImportDeclaration, {
|
|
53
|
+
source: { value: "@planningcenter/tapestry" },
|
|
54
|
+
})
|
|
55
|
+
expect(imports).toHaveLength(1)
|
|
56
|
+
expect(imports.at(0).get().value.specifiers?.[0].imported?.name).toBe(
|
|
57
|
+
"Tooltip"
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("should wrap element with wrapper props", () => {
|
|
62
|
+
const source = createSource('import React from "react"')
|
|
63
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
64
|
+
|
|
65
|
+
const wrapperProps = [
|
|
66
|
+
j.jsxAttribute(j.jsxIdentifier("title"), j.stringLiteral("Click me")),
|
|
67
|
+
j.jsxAttribute(j.jsxIdentifier("placement"), j.stringLiteral("top")),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
const result = createWrapper({
|
|
71
|
+
conflictAlias: "TRTooltip",
|
|
72
|
+
element,
|
|
73
|
+
j,
|
|
74
|
+
source,
|
|
75
|
+
wrapperName: "Tooltip",
|
|
76
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
77
|
+
wrapperProps,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
expect(result.openingElement.attributes).toHaveLength(2)
|
|
81
|
+
|
|
82
|
+
const titleAttr = result.openingElement.attributes?.find(
|
|
83
|
+
(attr) =>
|
|
84
|
+
attr.type === "JSXAttribute" &&
|
|
85
|
+
(attr.name as JSXIdentifier)?.name === "title"
|
|
86
|
+
) as JSXAttribute
|
|
87
|
+
expect((titleAttr?.value as StringLiteral)?.value).toBe("Click me")
|
|
88
|
+
|
|
89
|
+
const placementAttr = result.openingElement.attributes?.find(
|
|
90
|
+
(attr) =>
|
|
91
|
+
attr.type === "JSXAttribute" &&
|
|
92
|
+
(attr.name as JSXIdentifier)?.name === "placement"
|
|
93
|
+
) as JSXAttribute
|
|
94
|
+
expect((placementAttr?.value as StringLiteral)?.value).toBe("top")
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("should handle spread props", () => {
|
|
98
|
+
const source = createSource('import React from "react"')
|
|
99
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
100
|
+
|
|
101
|
+
const wrapperProps = [
|
|
102
|
+
j.jsxSpreadAttribute(j.identifier("tooltipProps")),
|
|
103
|
+
j.jsxAttribute(j.jsxIdentifier("title"), j.stringLiteral("Click me")),
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
const result = createWrapper({
|
|
107
|
+
conflictAlias: "TRTooltip",
|
|
108
|
+
element,
|
|
109
|
+
j,
|
|
110
|
+
source,
|
|
111
|
+
wrapperName: "Tooltip",
|
|
112
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
113
|
+
wrapperProps,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(result.openingElement.attributes).toHaveLength(2)
|
|
117
|
+
expect(result.openingElement.attributes?.[0].type).toBe(
|
|
118
|
+
"JSXSpreadAttribute"
|
|
119
|
+
)
|
|
120
|
+
expect(result.openingElement.attributes?.[1].type).toBe("JSXAttribute")
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe("import handling", () => {
|
|
125
|
+
it("should add import when wrapper not already imported", () => {
|
|
126
|
+
const source = createSource(
|
|
127
|
+
'import { Button } from "@planningcenter/tapestry"'
|
|
128
|
+
)
|
|
129
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
130
|
+
|
|
131
|
+
const result = createWrapper({
|
|
132
|
+
conflictAlias: "TRTooltip",
|
|
133
|
+
element,
|
|
134
|
+
j,
|
|
135
|
+
source,
|
|
136
|
+
wrapperName: "Tooltip",
|
|
137
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
expect((result.openingElement.name as JSXIdentifier).name).toBe("Tooltip")
|
|
141
|
+
|
|
142
|
+
// Should merge with existing import
|
|
143
|
+
const imports = source.find(j.ImportDeclaration, {
|
|
144
|
+
source: { value: "@planningcenter/tapestry" },
|
|
145
|
+
})
|
|
146
|
+
expect(imports).toHaveLength(1)
|
|
147
|
+
expect(imports.at(0).get().value.specifiers).toHaveLength(2)
|
|
148
|
+
|
|
149
|
+
const specifierNames = imports
|
|
150
|
+
.at(0)
|
|
151
|
+
.get()
|
|
152
|
+
.value.specifiers?.map((spec: ImportSpecifier) =>
|
|
153
|
+
spec.type === "ImportSpecifier" ? spec.imported?.name : null
|
|
154
|
+
)
|
|
155
|
+
expect(specifierNames).toContain("Button")
|
|
156
|
+
expect(specifierNames).toContain("Tooltip")
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("should use existing import when wrapper already imported", () => {
|
|
160
|
+
const source = createSource(
|
|
161
|
+
'import { Button, Tooltip } from "@planningcenter/tapestry"'
|
|
162
|
+
)
|
|
163
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
164
|
+
|
|
165
|
+
const result = createWrapper({
|
|
166
|
+
conflictAlias: "TRTooltip",
|
|
167
|
+
element,
|
|
168
|
+
j,
|
|
169
|
+
source,
|
|
170
|
+
wrapperName: "Tooltip",
|
|
171
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
expect((result.openingElement.name as JSXIdentifier).name).toBe("Tooltip")
|
|
175
|
+
|
|
176
|
+
// Should not modify existing import
|
|
177
|
+
const imports = source.find(j.ImportDeclaration, {
|
|
178
|
+
source: { value: "@planningcenter/tapestry" },
|
|
179
|
+
})
|
|
180
|
+
expect(imports).toHaveLength(1)
|
|
181
|
+
expect(imports.at(0).get().value.specifiers).toHaveLength(2)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it("should handle import conflicts with auto-generated alias", () => {
|
|
185
|
+
const source = createSource(`
|
|
186
|
+
import { Tooltip } from "@some/other-package"
|
|
187
|
+
import { Button } from "@planningcenter/tapestry"
|
|
188
|
+
`)
|
|
189
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
190
|
+
|
|
191
|
+
const result = createWrapper({
|
|
192
|
+
conflictAlias: "TRTooltip",
|
|
193
|
+
element,
|
|
194
|
+
j,
|
|
195
|
+
source,
|
|
196
|
+
wrapperName: "Tooltip",
|
|
197
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
expect((result.openingElement.name as JSXIdentifier).name).toBe(
|
|
201
|
+
"TRTooltip"
|
|
202
|
+
)
|
|
203
|
+
expect((result.closingElement?.name as JSXIdentifier)?.name).toBe(
|
|
204
|
+
"TRTooltip"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
const tapestryImport = source.find(j.ImportDeclaration, {
|
|
208
|
+
source: { value: "@planningcenter/tapestry" },
|
|
209
|
+
})
|
|
210
|
+
const tooltipSpec = tapestryImport
|
|
211
|
+
.at(0)
|
|
212
|
+
.get()
|
|
213
|
+
.value.specifiers?.find(
|
|
214
|
+
(spec: ImportSpecifier) =>
|
|
215
|
+
spec.type === "ImportSpecifier" && spec.imported?.name === "Tooltip"
|
|
216
|
+
) as ImportSpecifier
|
|
217
|
+
expect(tooltipSpec?.local?.name).toBe("TRTooltip")
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it("should handle import conflicts with custom alias", () => {
|
|
221
|
+
const source = createSource(`
|
|
222
|
+
import { Tooltip } from "@some/other-package"
|
|
223
|
+
import { Button } from "@planningcenter/tapestry"
|
|
224
|
+
`)
|
|
225
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
226
|
+
|
|
227
|
+
const result = createWrapper({
|
|
228
|
+
conflictAlias: "TapestryTooltip",
|
|
229
|
+
element,
|
|
230
|
+
j,
|
|
231
|
+
source,
|
|
232
|
+
wrapperName: "Tooltip",
|
|
233
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
expect((result.openingElement.name as JSXIdentifier).name).toBe(
|
|
237
|
+
"TapestryTooltip"
|
|
238
|
+
)
|
|
239
|
+
expect((result.closingElement?.name as JSXIdentifier)?.name).toBe(
|
|
240
|
+
"TapestryTooltip"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
const tapestryImport = source.find(j.ImportDeclaration, {
|
|
244
|
+
source: { value: "@planningcenter/tapestry" },
|
|
245
|
+
})
|
|
246
|
+
const tooltipSpec = tapestryImport
|
|
247
|
+
.at(0)
|
|
248
|
+
.get()
|
|
249
|
+
.value.specifiers?.find(
|
|
250
|
+
(spec: ImportSpecifier) =>
|
|
251
|
+
spec.type === "ImportSpecifier" && spec.imported?.name === "Tooltip"
|
|
252
|
+
) as ImportSpecifier
|
|
253
|
+
expect(tooltipSpec?.local?.name).toBe("TapestryTooltip")
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
describe("element preservation", () => {
|
|
258
|
+
it("should preserve element structure", () => {
|
|
259
|
+
const source = createSource('import React from "react"')
|
|
260
|
+
const element = createElementFromCode(
|
|
261
|
+
'<Button variant="primary" onClick={handleClick}>Save</Button>'
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
const result = createWrapper({
|
|
265
|
+
conflictAlias: "TRTooltip",
|
|
266
|
+
element,
|
|
267
|
+
j,
|
|
268
|
+
source,
|
|
269
|
+
wrapperName: "Tooltip",
|
|
270
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const wrappedButton = result.children?.[0] as JSXElement
|
|
274
|
+
expect((wrappedButton?.openingElement.name as JSXIdentifier).name).toBe(
|
|
275
|
+
"Button"
|
|
276
|
+
)
|
|
277
|
+
expect(wrappedButton?.openingElement.attributes).toHaveLength(2)
|
|
278
|
+
|
|
279
|
+
const variantAttr = wrappedButton?.openingElement.attributes?.find(
|
|
280
|
+
(attr) =>
|
|
281
|
+
attr.type === "JSXAttribute" &&
|
|
282
|
+
(attr.name as JSXIdentifier)?.name === "variant"
|
|
283
|
+
) as JSXAttribute
|
|
284
|
+
expect((variantAttr?.value as StringLiteral)?.value).toBe("primary")
|
|
285
|
+
|
|
286
|
+
const onClickAttr = wrappedButton?.openingElement.attributes?.find(
|
|
287
|
+
(attr) =>
|
|
288
|
+
attr.type === "JSXAttribute" &&
|
|
289
|
+
(attr.name as JSXIdentifier)?.name === "onClick"
|
|
290
|
+
) as JSXAttribute
|
|
291
|
+
expect(onClickAttr?.value?.type).toBe("JSXExpressionContainer")
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it("should preserve self-closing elements", () => {
|
|
295
|
+
const source = createSource('import React from "react"')
|
|
296
|
+
const element = createElementFromCode('<Button variant="primary" />')
|
|
297
|
+
|
|
298
|
+
const result = createWrapper({
|
|
299
|
+
conflictAlias: "TRTooltip",
|
|
300
|
+
element,
|
|
301
|
+
j,
|
|
302
|
+
source,
|
|
303
|
+
wrapperName: "Tooltip",
|
|
304
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
const wrappedButton = result.children?.[0] as JSXElement
|
|
308
|
+
expect((wrappedButton?.openingElement.name as JSXIdentifier).name).toBe(
|
|
309
|
+
"Button"
|
|
310
|
+
)
|
|
311
|
+
expect(wrappedButton?.openingElement.selfClosing).toBe(true)
|
|
312
|
+
expect(wrappedButton?.closingElement).toBeNull()
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it("should preserve complex nested elements", () => {
|
|
316
|
+
const source = createSource('import React from "react"')
|
|
317
|
+
const element = createElementFromCode(
|
|
318
|
+
'<Button><Icon name="save" /><span>Save Changes</span></Button>'
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
const result = createWrapper({
|
|
322
|
+
conflictAlias: "TRTooltip",
|
|
323
|
+
element,
|
|
324
|
+
j,
|
|
325
|
+
source,
|
|
326
|
+
wrapperName: "Tooltip",
|
|
327
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
const wrappedButton = result.children?.[0] as JSXElement
|
|
331
|
+
expect(wrappedButton?.type).toBe("JSXElement")
|
|
332
|
+
expect((wrappedButton?.openingElement.name as JSXIdentifier).name).toBe(
|
|
333
|
+
"Button"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
// Check that the button has children
|
|
337
|
+
expect(wrappedButton?.children).toBeDefined()
|
|
338
|
+
expect(Array.isArray(wrappedButton?.children)).toBe(true)
|
|
339
|
+
expect(wrappedButton?.children?.length).toBeGreaterThan(0)
|
|
340
|
+
|
|
341
|
+
const iconElement = wrappedButton?.children?.find(
|
|
342
|
+
(child) =>
|
|
343
|
+
child?.type === "JSXElement" &&
|
|
344
|
+
(child.openingElement?.name as JSXIdentifier)?.name === "Icon"
|
|
345
|
+
)
|
|
346
|
+
expect(iconElement).toBeDefined()
|
|
347
|
+
|
|
348
|
+
const spanElement = wrappedButton?.children?.find(
|
|
349
|
+
(child) =>
|
|
350
|
+
child?.type === "JSXElement" &&
|
|
351
|
+
(child.openingElement?.name as JSXIdentifier)?.name === "span"
|
|
352
|
+
)
|
|
353
|
+
expect(spanElement).toBeDefined()
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe("edge cases", () => {
|
|
358
|
+
it("should handle empty wrapperProps", () => {
|
|
359
|
+
const source = createSource('import React from "react"')
|
|
360
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
361
|
+
|
|
362
|
+
const result = createWrapper({
|
|
363
|
+
conflictAlias: "TRTooltip",
|
|
364
|
+
element,
|
|
365
|
+
j,
|
|
366
|
+
source,
|
|
367
|
+
wrapperName: "Tooltip",
|
|
368
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
369
|
+
wrapperProps: [],
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
expect(result.openingElement.attributes).toHaveLength(0)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it("should handle undefined wrapperProps", () => {
|
|
376
|
+
const source = createSource('import React from "react"')
|
|
377
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
378
|
+
|
|
379
|
+
const result = createWrapper({
|
|
380
|
+
conflictAlias: "TRTooltip",
|
|
381
|
+
element,
|
|
382
|
+
j,
|
|
383
|
+
source,
|
|
384
|
+
wrapperName: "Tooltip",
|
|
385
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
expect(result.openingElement.attributes).toHaveLength(0)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it("should create proper JSX structure", () => {
|
|
392
|
+
const source = createSource('import React from "react"')
|
|
393
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
394
|
+
|
|
395
|
+
const result = createWrapper({
|
|
396
|
+
conflictAlias: "TRTooltip",
|
|
397
|
+
element,
|
|
398
|
+
j,
|
|
399
|
+
source,
|
|
400
|
+
wrapperName: "Tooltip",
|
|
401
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
// Verify the JSX structure is valid
|
|
405
|
+
expect(result.type).toBe("JSXElement")
|
|
406
|
+
expect(result.openingElement.type).toBe("JSXOpeningElement")
|
|
407
|
+
expect(result.closingElement?.type).toBe("JSXClosingElement")
|
|
408
|
+
expect(result.openingElement.selfClosing).toBe(false)
|
|
409
|
+
expect(result.children).toHaveLength(1)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it("should not add parentheses around wrapped elements", () => {
|
|
413
|
+
const source = createSource('import React from "react"')
|
|
414
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
415
|
+
|
|
416
|
+
createWrapper({
|
|
417
|
+
conflictAlias: "TRTooltip",
|
|
418
|
+
element,
|
|
419
|
+
j,
|
|
420
|
+
source,
|
|
421
|
+
wrapperName: "Tooltip",
|
|
422
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
// Check that the output doesn't contain parentheses around the Button
|
|
426
|
+
const result = source.toSource()
|
|
427
|
+
expect(result).not.toContain("(<Button>Save</Button>)")
|
|
428
|
+
expect(result).not.toContain("(<Button")
|
|
429
|
+
expect(result).not.toContain("Button>)")
|
|
430
|
+
})
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
describe("key attribute handling", () => {
|
|
434
|
+
it("should move key attribute from element to wrapper", () => {
|
|
435
|
+
const source = createSource('import React from "react"')
|
|
436
|
+
const element = createElementFromCode(
|
|
437
|
+
'<Button key="test-key">Save</Button>'
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
const result = createWrapper({
|
|
441
|
+
conflictAlias: "TRTooltip",
|
|
442
|
+
element,
|
|
443
|
+
j,
|
|
444
|
+
source,
|
|
445
|
+
wrapperName: "Tooltip",
|
|
446
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
// Wrapper should have the key attribute
|
|
450
|
+
const keyAttr = result.openingElement.attributes?.find(
|
|
451
|
+
(attr) =>
|
|
452
|
+
attr.type === "JSXAttribute" &&
|
|
453
|
+
(attr.name as JSXIdentifier)?.name === "key"
|
|
454
|
+
) as JSXAttribute
|
|
455
|
+
expect(keyAttr).toBeDefined()
|
|
456
|
+
expect((keyAttr?.value as StringLiteral)?.value).toBe("test-key")
|
|
457
|
+
|
|
458
|
+
// Inner element should not have key attribute
|
|
459
|
+
const innerElement = result.children?.[0] as JSXElement
|
|
460
|
+
const innerKeyAttr = innerElement.openingElement.attributes?.find(
|
|
461
|
+
(attr) =>
|
|
462
|
+
attr.type === "JSXAttribute" &&
|
|
463
|
+
(attr.name as JSXIdentifier)?.name === "key"
|
|
464
|
+
)
|
|
465
|
+
expect(innerKeyAttr).toBeUndefined()
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it("should move key attribute and preserve wrapper props", () => {
|
|
469
|
+
const source = createSource('import React from "react"')
|
|
470
|
+
const element = createElementFromCode(
|
|
471
|
+
'<Button key="item-1">Save</Button>'
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
const wrapperProps = [
|
|
475
|
+
j.jsxAttribute(j.jsxIdentifier("title"), j.stringLiteral("Click me")),
|
|
476
|
+
j.jsxAttribute(j.jsxIdentifier("placement"), j.stringLiteral("top")),
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
const result = createWrapper({
|
|
480
|
+
conflictAlias: "TRTooltip",
|
|
481
|
+
element,
|
|
482
|
+
j,
|
|
483
|
+
source,
|
|
484
|
+
wrapperName: "Tooltip",
|
|
485
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
486
|
+
wrapperProps,
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
expect(result.openingElement.attributes).toHaveLength(3)
|
|
490
|
+
|
|
491
|
+
// Key should be first
|
|
492
|
+
const keyAttr = result.openingElement.attributes?.[0] as JSXAttribute
|
|
493
|
+
expect((keyAttr.name as JSXIdentifier)?.name).toBe("key")
|
|
494
|
+
expect((keyAttr?.value as StringLiteral)?.value).toBe("item-1")
|
|
495
|
+
|
|
496
|
+
// Other props should follow
|
|
497
|
+
const titleAttr = result.openingElement.attributes?.find(
|
|
498
|
+
(attr) =>
|
|
499
|
+
attr.type === "JSXAttribute" &&
|
|
500
|
+
(attr.name as JSXIdentifier)?.name === "title"
|
|
501
|
+
) as JSXAttribute
|
|
502
|
+
expect((titleAttr?.value as StringLiteral)?.value).toBe("Click me")
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it("should handle key attribute with JSX expression", () => {
|
|
506
|
+
const source = createSource('import React from "react"')
|
|
507
|
+
const element = createElementFromCode(
|
|
508
|
+
"<Button key={item.id}>Save</Button>"
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
const result = createWrapper({
|
|
512
|
+
conflictAlias: "TRTooltip",
|
|
513
|
+
element,
|
|
514
|
+
j,
|
|
515
|
+
source,
|
|
516
|
+
wrapperName: "Tooltip",
|
|
517
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
const keyAttr = result.openingElement.attributes?.find(
|
|
521
|
+
(attr) =>
|
|
522
|
+
attr.type === "JSXAttribute" &&
|
|
523
|
+
(attr.name as JSXIdentifier)?.name === "key"
|
|
524
|
+
) as JSXAttribute
|
|
525
|
+
expect(keyAttr).toBeDefined()
|
|
526
|
+
expect(keyAttr.value?.type).toBe("JSXExpressionContainer")
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
it("should preserve other attributes when moving key", () => {
|
|
530
|
+
const source = createSource('import React from "react"')
|
|
531
|
+
const element = createElementFromCode(
|
|
532
|
+
'<Button key="btn-1" variant="primary" disabled onClick={handler}>Save</Button>'
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
const result = createWrapper({
|
|
536
|
+
conflictAlias: "TRTooltip",
|
|
537
|
+
element,
|
|
538
|
+
j,
|
|
539
|
+
source,
|
|
540
|
+
wrapperName: "Tooltip",
|
|
541
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
// Wrapper should have key
|
|
545
|
+
const wrapperKeyAttr = result.openingElement.attributes?.find(
|
|
546
|
+
(attr) =>
|
|
547
|
+
attr.type === "JSXAttribute" &&
|
|
548
|
+
(attr.name as JSXIdentifier)?.name === "key"
|
|
549
|
+
)
|
|
550
|
+
expect(wrapperKeyAttr).toBeDefined()
|
|
551
|
+
|
|
552
|
+
// Inner element should have other attributes but not key
|
|
553
|
+
const innerElement = result.children?.[0] as JSXElement
|
|
554
|
+
expect(innerElement.openingElement.attributes).toHaveLength(3)
|
|
555
|
+
|
|
556
|
+
const variantAttr = innerElement.openingElement.attributes?.find(
|
|
557
|
+
(attr) =>
|
|
558
|
+
attr.type === "JSXAttribute" &&
|
|
559
|
+
(attr.name as JSXIdentifier)?.name === "variant"
|
|
560
|
+
)
|
|
561
|
+
expect(variantAttr).toBeDefined()
|
|
562
|
+
|
|
563
|
+
const disabledAttr = innerElement.openingElement.attributes?.find(
|
|
564
|
+
(attr) =>
|
|
565
|
+
attr.type === "JSXAttribute" &&
|
|
566
|
+
(attr.name as JSXIdentifier)?.name === "disabled"
|
|
567
|
+
)
|
|
568
|
+
expect(disabledAttr).toBeDefined()
|
|
569
|
+
|
|
570
|
+
const onClickAttr = innerElement.openingElement.attributes?.find(
|
|
571
|
+
(attr) =>
|
|
572
|
+
attr.type === "JSXAttribute" &&
|
|
573
|
+
(attr.name as JSXIdentifier)?.name === "onClick"
|
|
574
|
+
)
|
|
575
|
+
expect(onClickAttr).toBeDefined()
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it("should work normally when no key attribute exists", () => {
|
|
579
|
+
const source = createSource('import React from "react"')
|
|
580
|
+
const element = createElementFromCode(
|
|
581
|
+
'<Button variant="primary">Save</Button>'
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
const result = createWrapper({
|
|
585
|
+
conflictAlias: "TRTooltip",
|
|
586
|
+
element,
|
|
587
|
+
j,
|
|
588
|
+
source,
|
|
589
|
+
wrapperName: "Tooltip",
|
|
590
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
// Wrapper should not have key attribute
|
|
594
|
+
const wrapperKeyAttr = result.openingElement.attributes?.find(
|
|
595
|
+
(attr) =>
|
|
596
|
+
attr.type === "JSXAttribute" &&
|
|
597
|
+
(attr.name as JSXIdentifier)?.name === "key"
|
|
598
|
+
)
|
|
599
|
+
expect(wrapperKeyAttr).toBeUndefined()
|
|
600
|
+
|
|
601
|
+
// Inner element should preserve its attributes
|
|
602
|
+
const innerElement = result.children?.[0] as JSXElement
|
|
603
|
+
expect(innerElement.openingElement.attributes).toHaveLength(1)
|
|
604
|
+
|
|
605
|
+
const variantAttr = innerElement.openingElement.attributes?.find(
|
|
606
|
+
(attr) =>
|
|
607
|
+
attr.type === "JSXAttribute" &&
|
|
608
|
+
(attr.name as JSXIdentifier)?.name === "variant"
|
|
609
|
+
)
|
|
610
|
+
expect(variantAttr).toBeDefined()
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it("should generate correct source code with key attribute", () => {
|
|
614
|
+
const source = createSource(`
|
|
615
|
+
import React from "react"
|
|
616
|
+
const Test = () => <Button key="test">Save</Button>
|
|
617
|
+
`)
|
|
618
|
+
|
|
619
|
+
// Find the Button element that's actually in the source
|
|
620
|
+
const element = source
|
|
621
|
+
.find(j.JSXElement, {
|
|
622
|
+
openingElement: { name: { name: "Button" } },
|
|
623
|
+
})
|
|
624
|
+
.at(0)
|
|
625
|
+
.get().value
|
|
626
|
+
|
|
627
|
+
createWrapper({
|
|
628
|
+
conflictAlias: "TRTooltip",
|
|
629
|
+
element,
|
|
630
|
+
j,
|
|
631
|
+
source,
|
|
632
|
+
wrapperName: "Tooltip",
|
|
633
|
+
wrapperPackage: "@planningcenter/tapestry",
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
const result = source.toSource()
|
|
637
|
+
expect(result).toContain('<Tooltip key="test">')
|
|
638
|
+
expect(result).toContain("<Button>Save</Button>")
|
|
639
|
+
expect(result).not.toContain('<Button key="test">')
|
|
640
|
+
})
|
|
641
|
+
})
|
|
642
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Collection,
|
|
3
|
+
JSCodeshift,
|
|
4
|
+
JSXAttribute,
|
|
5
|
+
JSXElement,
|
|
6
|
+
JSXSpreadAttribute,
|
|
7
|
+
} from "jscodeshift"
|
|
8
|
+
|
|
9
|
+
import { addImport } from "../transformFactories/helpers/manageImports"
|
|
10
|
+
import { getAttribute } from "./getAttribute"
|
|
11
|
+
|
|
12
|
+
export function createWrapper({
|
|
13
|
+
conflictAlias,
|
|
14
|
+
element,
|
|
15
|
+
j,
|
|
16
|
+
source,
|
|
17
|
+
wrapperName,
|
|
18
|
+
wrapperPackage,
|
|
19
|
+
wrapperProps = [],
|
|
20
|
+
}: {
|
|
21
|
+
conflictAlias: string
|
|
22
|
+
element: JSXElement
|
|
23
|
+
j: JSCodeshift
|
|
24
|
+
source: Collection
|
|
25
|
+
wrapperName: string
|
|
26
|
+
wrapperPackage: string
|
|
27
|
+
wrapperProps?: (JSXAttribute | JSXSpreadAttribute)[]
|
|
28
|
+
}): JSXElement {
|
|
29
|
+
const actualWrapperName = addImport({
|
|
30
|
+
component: wrapperName,
|
|
31
|
+
conflictAlias,
|
|
32
|
+
j,
|
|
33
|
+
pkg: wrapperPackage,
|
|
34
|
+
source,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const keyAttribute = getAttribute({ element, name: "key" })
|
|
38
|
+
const cleanAttributes = (element.openingElement.attributes || []).filter(
|
|
39
|
+
(attr) => !(attr.type === "JSXAttribute" && attr.name.name === "key")
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const cleanElement = j.jsxElement(
|
|
43
|
+
j.jsxOpeningElement(
|
|
44
|
+
element.openingElement.name,
|
|
45
|
+
cleanAttributes,
|
|
46
|
+
element.openingElement.selfClosing
|
|
47
|
+
),
|
|
48
|
+
element.closingElement,
|
|
49
|
+
element.children
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const wrapperElement = j.jsxElement(
|
|
53
|
+
j.jsxOpeningElement(
|
|
54
|
+
j.jsxIdentifier(actualWrapperName),
|
|
55
|
+
keyAttribute ? [keyAttribute, ...wrapperProps] : wrapperProps,
|
|
56
|
+
false
|
|
57
|
+
),
|
|
58
|
+
j.jsxClosingElement(j.jsxIdentifier(actualWrapperName)),
|
|
59
|
+
[cleanElement]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
source
|
|
63
|
+
.find(j.JSXElement)
|
|
64
|
+
.filter((path) => path.value === element)
|
|
65
|
+
.forEach((path) => {
|
|
66
|
+
path.replace(wrapperElement)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
return wrapperElement
|
|
70
|
+
}
|