@planningcenter/tapestry-migration-cli 2.1.1-rc.2 → 2.1.1-rc.3
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 -1
- package/src/components/button/transforms/linkToButton.ts +4 -9
- package/src/components/button/transforms/titleToLabel.test.ts +418 -0
- package/src/components/button/transforms/titleToLabel.ts +19 -0
- package/src/components/shared/actions/transformAttributeName.ts +20 -0
- package/src/components/shared/actions/transformElementName.test.ts +59 -0
- package/src/components/shared/actions/transformElementName.ts +27 -0
- package/src/components/shared/conditions/andConditions.test.ts +65 -0
- package/src/components/shared/conditions/andConditions.ts +13 -0
- package/src/components/shared/conditions/hasAttribute.test.ts +43 -0
- package/src/components/shared/conditions/hasAttribute.ts +18 -0
- package/src/components/shared/conditions/hasAttributeValue.test.ts +48 -0
- package/src/components/shared/conditions/hasAttributeValue.ts +23 -0
- package/src/components/shared/conditions/helpers/createJSXElement.ts +9 -0
- package/src/components/shared/conditions/index.test.ts +63 -0
- package/src/components/shared/conditions/orConditions.test.ts +76 -0
- package/src/components/shared/conditions/orConditions.ts +13 -0
- package/src/components/shared/findAttribute.ts +15 -0
- package/src/components/shared/transformFactories/attributeTransformFactory.test.ts +88 -0
- package/src/components/shared/transformFactories/attributeTransformFactory.ts +51 -0
- package/src/components/shared/transformFactories/componentTransformFactory.test.ts +7 -0
- package/src/components/shared/transformFactories/componentTransformFactory.ts +77 -0
- package/src/components/shared/{componentTransformUtilities.test.ts → transformFactories/helpers/manageImports.test.ts} +1 -55
- package/src/components/shared/{componentTransformUtilities.ts → transformFactories/helpers/manageImports.ts} +17 -93
- package/src/components/shared/types.ts +6 -0
- package/src/components/shared/getTapestryReactImportName.ts +0 -35
- package/src/components/shared/transformConfig.test.ts +0 -288
- package/src/components/shared/transformConfig.ts +0 -79
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/tapestry-migration-cli",
|
|
3
|
-
"version": "2.1.1-rc.
|
|
3
|
+
"version": "2.1.1-rc.3",
|
|
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": "3efd313bf986f8a7d54b6abc633bbc8f91f4fe11"
|
|
51
51
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Transform } from "jscodeshift"
|
|
2
2
|
|
|
3
3
|
import linkToButton from "./transforms/linkToButton"
|
|
4
|
+
import titleToLabel from "./transforms/titleToLabel"
|
|
4
5
|
|
|
5
6
|
const transform: Transform = (fileInfo, api, options) => {
|
|
6
7
|
let currentSource = fileInfo.source
|
|
7
8
|
let hasAnyChanges = false
|
|
8
9
|
|
|
9
|
-
const transforms: Transform[] = [linkToButton]
|
|
10
|
+
const transforms: Transform[] = [linkToButton, titleToLabel]
|
|
10
11
|
|
|
11
12
|
for (const individualTransform of transforms) {
|
|
12
13
|
const result = individualTransform(
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import { Transform } from "jscodeshift"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
ComponentTransformConfig,
|
|
6
|
-
hasAttribute,
|
|
7
|
-
} from "../../shared/transformConfig"
|
|
3
|
+
import { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
4
|
+
import { componentTransformFactory } from "../../shared/transformFactories/componentTransformFactory"
|
|
8
5
|
|
|
9
|
-
const
|
|
6
|
+
const transform: Transform = componentTransformFactory({
|
|
10
7
|
condition: hasAttribute("onClick"),
|
|
11
8
|
conflictAlias: "TRButton",
|
|
12
9
|
fromComponent: "Link",
|
|
13
10
|
fromPackage: "@planningcenter/tapestry-react",
|
|
14
11
|
toComponent: "Button",
|
|
15
12
|
toPackage: "@planningcenter/tapestry-react",
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const transform: Transform = createComponentTransform(config)
|
|
13
|
+
})
|
|
19
14
|
|
|
20
15
|
export default transform
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./titleToLabel"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function applyTransform(source: string) {
|
|
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("titleToLabel transform", () => {
|
|
18
|
+
describe("basic transformations", () => {
|
|
19
|
+
it("should transform Button title to label when no icon prop", () => {
|
|
20
|
+
const input = `
|
|
21
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
export default function Test() {
|
|
24
|
+
return <Button title="Save"></Button>
|
|
25
|
+
}
|
|
26
|
+
`.trim()
|
|
27
|
+
|
|
28
|
+
const expected = `
|
|
29
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
30
|
+
|
|
31
|
+
export default function Test() {
|
|
32
|
+
return <Button label="Save"></Button>;
|
|
33
|
+
}
|
|
34
|
+
`.trim()
|
|
35
|
+
|
|
36
|
+
const result = applyTransform(input)
|
|
37
|
+
expect(result?.trim()).toBe(expected)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("should transform Button title to aria-label when icon prop is present", () => {
|
|
41
|
+
const input = `
|
|
42
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
43
|
+
|
|
44
|
+
export default function Test() {
|
|
45
|
+
return <Button title="Save" icon="save"></Button>
|
|
46
|
+
}
|
|
47
|
+
`.trim()
|
|
48
|
+
|
|
49
|
+
const expected = `
|
|
50
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
51
|
+
|
|
52
|
+
export default function Test() {
|
|
53
|
+
return <Button aria-label="Save" icon="save"></Button>;
|
|
54
|
+
}
|
|
55
|
+
`.trim()
|
|
56
|
+
|
|
57
|
+
const result = applyTransform(input)
|
|
58
|
+
expect(result?.trim()).toBe(expected)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("should handle multiple Button components", () => {
|
|
62
|
+
const input = `
|
|
63
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
64
|
+
|
|
65
|
+
export default function Test() {
|
|
66
|
+
return (
|
|
67
|
+
<div>
|
|
68
|
+
<Button title="Save" />
|
|
69
|
+
<Button title="Delete" icon="trash" />
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
`.trim()
|
|
74
|
+
|
|
75
|
+
const expected = `
|
|
76
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
77
|
+
|
|
78
|
+
export default function Test() {
|
|
79
|
+
return (
|
|
80
|
+
<div>
|
|
81
|
+
<Button label="Save" />
|
|
82
|
+
<Button aria-label="Delete" icon="trash" />
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
`.trim()
|
|
87
|
+
|
|
88
|
+
const result = applyTransform(input)
|
|
89
|
+
expect(result?.trim()).toBe(expected)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe("edge cases", () => {
|
|
94
|
+
it("should not transform Button without title attribute", () => {
|
|
95
|
+
const input = `
|
|
96
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
97
|
+
|
|
98
|
+
export default function Test() {
|
|
99
|
+
return <Button>Save</Button>
|
|
100
|
+
}
|
|
101
|
+
`.trim()
|
|
102
|
+
|
|
103
|
+
const result = applyTransform(input)
|
|
104
|
+
expect(result).toBe(null)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("should not transform if Button is not imported from @planningcenter/tapestry-react", () => {
|
|
108
|
+
const input = `
|
|
109
|
+
import { Button } from "other-library"
|
|
110
|
+
|
|
111
|
+
export default function Test() {
|
|
112
|
+
return <Button title="Save" />
|
|
113
|
+
}
|
|
114
|
+
`.trim()
|
|
115
|
+
|
|
116
|
+
const result = applyTransform(input)
|
|
117
|
+
expect(result).toBe(null)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("should handle Button with alias import", () => {
|
|
121
|
+
const input = `
|
|
122
|
+
import { Button as TapestryButton } from "@planningcenter/tapestry-react"
|
|
123
|
+
|
|
124
|
+
export default function Test() {
|
|
125
|
+
return <TapestryButton title="Save" />;
|
|
126
|
+
}
|
|
127
|
+
`.trim()
|
|
128
|
+
|
|
129
|
+
const expected = `
|
|
130
|
+
import { Button as TapestryButton } from "@planningcenter/tapestry-react"
|
|
131
|
+
|
|
132
|
+
export default function Test() {
|
|
133
|
+
return <TapestryButton label="Save" />;
|
|
134
|
+
}
|
|
135
|
+
`.trim()
|
|
136
|
+
|
|
137
|
+
const result = applyTransform(input)
|
|
138
|
+
expect(result?.trim()).toBe(expected)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("should handle mixed Button components (with and without title)", () => {
|
|
142
|
+
const input = `
|
|
143
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
144
|
+
|
|
145
|
+
export default function Test() {
|
|
146
|
+
return (
|
|
147
|
+
<div>
|
|
148
|
+
<Button title="Save" />
|
|
149
|
+
<Button>Cancel</Button>
|
|
150
|
+
<Button title="Delete" icon="trash" />
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
`.trim()
|
|
155
|
+
|
|
156
|
+
const expected = `
|
|
157
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
158
|
+
|
|
159
|
+
export default function Test() {
|
|
160
|
+
return (
|
|
161
|
+
<div>
|
|
162
|
+
<Button label="Save" />
|
|
163
|
+
<Button>Cancel</Button>
|
|
164
|
+
<Button aria-label="Delete" icon="trash" />
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
`.trim()
|
|
169
|
+
|
|
170
|
+
const result = applyTransform(input)
|
|
171
|
+
expect(result?.trim()).toBe(expected)
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe("complex attribute scenarios", () => {
|
|
176
|
+
it("should handle Button with multiple attributes including icon", () => {
|
|
177
|
+
const input = `
|
|
178
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
179
|
+
|
|
180
|
+
export default function Test() {
|
|
181
|
+
return (
|
|
182
|
+
<Button
|
|
183
|
+
title="Save Document"
|
|
184
|
+
icon="save"
|
|
185
|
+
className="primary"
|
|
186
|
+
disabled={false}
|
|
187
|
+
onClick={handleSave}
|
|
188
|
+
/>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
`.trim()
|
|
192
|
+
|
|
193
|
+
const expected = `
|
|
194
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
195
|
+
|
|
196
|
+
export default function Test() {
|
|
197
|
+
return (
|
|
198
|
+
<Button
|
|
199
|
+
aria-label="Save Document"
|
|
200
|
+
icon="save"
|
|
201
|
+
className="primary"
|
|
202
|
+
disabled={false}
|
|
203
|
+
onClick={handleSave}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
`.trim()
|
|
208
|
+
|
|
209
|
+
const result = applyTransform(input)
|
|
210
|
+
expect(result?.trim()).toBe(expected)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it("should handle Button with multiple attributes without icon", () => {
|
|
214
|
+
const input = `
|
|
215
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
216
|
+
|
|
217
|
+
export default function Test() {
|
|
218
|
+
return (
|
|
219
|
+
<Button
|
|
220
|
+
title="Save Document"
|
|
221
|
+
className="primary"
|
|
222
|
+
disabled={false}
|
|
223
|
+
onClick={handleSave}
|
|
224
|
+
/>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
`.trim()
|
|
228
|
+
|
|
229
|
+
const expected = `
|
|
230
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
231
|
+
|
|
232
|
+
export default function Test() {
|
|
233
|
+
return (
|
|
234
|
+
<Button
|
|
235
|
+
label="Save Document"
|
|
236
|
+
className="primary"
|
|
237
|
+
disabled={false}
|
|
238
|
+
onClick={handleSave}
|
|
239
|
+
/>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
`.trim()
|
|
243
|
+
|
|
244
|
+
const result = applyTransform(input)
|
|
245
|
+
expect(result?.trim()).toBe(expected)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it("should handle title with expression value", () => {
|
|
249
|
+
const input = `
|
|
250
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
251
|
+
|
|
252
|
+
export default function Test() {
|
|
253
|
+
const saveText = "Save Document"
|
|
254
|
+
return <Button title={saveText} />;
|
|
255
|
+
}
|
|
256
|
+
`.trim()
|
|
257
|
+
|
|
258
|
+
const expected = `
|
|
259
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
260
|
+
|
|
261
|
+
export default function Test() {
|
|
262
|
+
const saveText = "Save Document"
|
|
263
|
+
return <Button label={saveText} />;
|
|
264
|
+
}
|
|
265
|
+
`.trim()
|
|
266
|
+
|
|
267
|
+
const result = applyTransform(input)
|
|
268
|
+
expect(result?.trim()).toBe(expected)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it("should handle title with expression and icon", () => {
|
|
272
|
+
const input = `
|
|
273
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
274
|
+
|
|
275
|
+
export default function Test() {
|
|
276
|
+
const deleteText = "Delete Item"
|
|
277
|
+
return <Button title={deleteText} icon="trash" />
|
|
278
|
+
}
|
|
279
|
+
`.trim()
|
|
280
|
+
|
|
281
|
+
const expected = `
|
|
282
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
283
|
+
|
|
284
|
+
export default function Test() {
|
|
285
|
+
const deleteText = "Delete Item"
|
|
286
|
+
return <Button aria-label={deleteText} icon="trash" />;
|
|
287
|
+
}
|
|
288
|
+
`.trim()
|
|
289
|
+
|
|
290
|
+
const result = applyTransform(input)
|
|
291
|
+
expect(result?.trim()).toBe(expected)
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
describe("different icon attribute formats", () => {
|
|
296
|
+
it("should detect icon prop with string value", () => {
|
|
297
|
+
const input = `
|
|
298
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
299
|
+
|
|
300
|
+
export default function Test() {
|
|
301
|
+
return <Button title="Settings" icon="gear" />;
|
|
302
|
+
}
|
|
303
|
+
`.trim()
|
|
304
|
+
|
|
305
|
+
const result = applyTransform(input)
|
|
306
|
+
expect(result).toContain('aria-label="Settings"')
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it("should detect icon prop with expression value", () => {
|
|
310
|
+
const input = `
|
|
311
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
312
|
+
|
|
313
|
+
export default function Test() {
|
|
314
|
+
const iconName = "gear"
|
|
315
|
+
return <Button title="Settings" icon={iconName} />
|
|
316
|
+
}
|
|
317
|
+
`.trim()
|
|
318
|
+
|
|
319
|
+
const result = applyTransform(input)
|
|
320
|
+
expect(result).toContain('aria-label="Settings"')
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it("should detect icon prop without value", () => {
|
|
324
|
+
const input = `
|
|
325
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
326
|
+
|
|
327
|
+
export default function Test() {
|
|
328
|
+
return <Button title="Settings" icon />;
|
|
329
|
+
}
|
|
330
|
+
`.trim()
|
|
331
|
+
|
|
332
|
+
const result = applyTransform(input)
|
|
333
|
+
expect(result).toContain('aria-label="Settings"')
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe("no changes scenarios", () => {
|
|
338
|
+
it("should return null when no Button components have title", () => {
|
|
339
|
+
const input = `
|
|
340
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
341
|
+
|
|
342
|
+
export default function Test() {
|
|
343
|
+
return (
|
|
344
|
+
<div>
|
|
345
|
+
<Button>Save</Button>
|
|
346
|
+
<Button icon="trash" />
|
|
347
|
+
</div>
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
`.trim()
|
|
351
|
+
|
|
352
|
+
const result = applyTransform(input)
|
|
353
|
+
expect(result).toBe(null)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it("should return null when no Button imports exist", () => {
|
|
357
|
+
const input = `
|
|
358
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
359
|
+
|
|
360
|
+
export default function Test() {
|
|
361
|
+
return <Link href="/test">Go</Link>
|
|
362
|
+
}
|
|
363
|
+
`.trim()
|
|
364
|
+
|
|
365
|
+
const result = applyTransform(input)
|
|
366
|
+
expect(result).toBe(null)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it("should return null for empty file", () => {
|
|
370
|
+
const result = applyTransform("")
|
|
371
|
+
expect(result).toBe(null)
|
|
372
|
+
})
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
describe("self-closing Button components", () => {
|
|
376
|
+
it("should handle self-closing Button without icon", () => {
|
|
377
|
+
const input = `
|
|
378
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
379
|
+
|
|
380
|
+
export default function Test() {
|
|
381
|
+
return <Button title="Close" />
|
|
382
|
+
}
|
|
383
|
+
`.trim()
|
|
384
|
+
|
|
385
|
+
const expected = `
|
|
386
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
387
|
+
|
|
388
|
+
export default function Test() {
|
|
389
|
+
return <Button label="Close" />;
|
|
390
|
+
}
|
|
391
|
+
`.trim()
|
|
392
|
+
|
|
393
|
+
const result = applyTransform(input)
|
|
394
|
+
expect(result?.trim()).toBe(expected)
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
it("should handle self-closing Button with icon", () => {
|
|
398
|
+
const input = `
|
|
399
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
400
|
+
|
|
401
|
+
export default function Test() {
|
|
402
|
+
return <Button title="Close" icon="x" />
|
|
403
|
+
}
|
|
404
|
+
`.trim()
|
|
405
|
+
|
|
406
|
+
const expected = `
|
|
407
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
408
|
+
|
|
409
|
+
export default function Test() {
|
|
410
|
+
return <Button aria-label="Close" icon="x" />;
|
|
411
|
+
}
|
|
412
|
+
`.trim()
|
|
413
|
+
|
|
414
|
+
const result = applyTransform(input)
|
|
415
|
+
expect(result?.trim()).toBe(expected)
|
|
416
|
+
})
|
|
417
|
+
})
|
|
418
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { JSXElement, Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { transformAttributeName } from "../../shared/actions/transformAttributeName"
|
|
4
|
+
import { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
5
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
6
|
+
|
|
7
|
+
const transform: Transform = attributeTransformFactory({
|
|
8
|
+
condition: hasAttribute("title"),
|
|
9
|
+
targetComponent: "Button",
|
|
10
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
11
|
+
transform: (element: JSXElement) =>
|
|
12
|
+
transformAttributeName(
|
|
13
|
+
"title",
|
|
14
|
+
() => (hasAttribute("icon")(element) ? "aria-label" : "label"),
|
|
15
|
+
{ element }
|
|
16
|
+
),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export default transform
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { JSXElement } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { findAttribute } from "../findAttribute"
|
|
4
|
+
|
|
5
|
+
export function transformAttributeName(
|
|
6
|
+
name: string,
|
|
7
|
+
nameTransform: string | ((element: JSXElement) => string),
|
|
8
|
+
{ element }: { element: JSXElement }
|
|
9
|
+
): boolean {
|
|
10
|
+
if (!nameTransform) return false
|
|
11
|
+
const attributes = element.openingElement.attributes || []
|
|
12
|
+
const attribute = findAttribute(attributes, name)
|
|
13
|
+
if (!attribute) return false
|
|
14
|
+
|
|
15
|
+
const resolvedName =
|
|
16
|
+
typeof nameTransform === "string" ? nameTransform : nameTransform(element)
|
|
17
|
+
attribute.name.name = resolvedName
|
|
18
|
+
|
|
19
|
+
return true
|
|
20
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import { transformElementName } from "./transformElementName"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
describe("transformElementName", () => {
|
|
9
|
+
it("should transform JSX element name", () => {
|
|
10
|
+
const code = `<Button>Click me</Button>`
|
|
11
|
+
const source = j(code)
|
|
12
|
+
const elementPath = source.find(j.JSXOpeningElement).at(0)
|
|
13
|
+
|
|
14
|
+
const result = transformElementName(elementPath.get(), "Link")
|
|
15
|
+
|
|
16
|
+
expect(result).toBe(true)
|
|
17
|
+
expect(source.toSource()).toContain("<Link>Click me</Link>")
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("should transform both opening and closing tags", () => {
|
|
21
|
+
const code = `<Button className="test">Content</Button>`
|
|
22
|
+
const source = j(code)
|
|
23
|
+
const elementPath = source.find(j.JSXOpeningElement).at(0)
|
|
24
|
+
|
|
25
|
+
const result = transformElementName(elementPath.get(), "Link")
|
|
26
|
+
|
|
27
|
+
expect(result).toBe(true)
|
|
28
|
+
const output = source.toSource()
|
|
29
|
+
expect(output).toContain('<Link className="test">')
|
|
30
|
+
expect(output).toContain("</Link>")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("should handle self-closing tags", () => {
|
|
34
|
+
const code = `<Button />`
|
|
35
|
+
const source = j(code)
|
|
36
|
+
const elementPath = source.find(j.JSXOpeningElement).at(0)
|
|
37
|
+
|
|
38
|
+
const result = transformElementName(elementPath.get(), "Link")
|
|
39
|
+
|
|
40
|
+
expect(result).toBe(true)
|
|
41
|
+
expect(source.toSource()).toContain("<Link />")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("should preserve all attributes", () => {
|
|
45
|
+
const code = `<Button className="test" onClick={handler} disabled>Content</Button>`
|
|
46
|
+
const source = j(code)
|
|
47
|
+
const elementPath = source.find(j.JSXOpeningElement).at(0)
|
|
48
|
+
|
|
49
|
+
const result = transformElementName(elementPath.get(), "OtherButton")
|
|
50
|
+
|
|
51
|
+
expect(result).toBe(true)
|
|
52
|
+
const output = source.toSource()
|
|
53
|
+
expect(output).toContain('className="test"')
|
|
54
|
+
expect(output).toContain("onClick={handler}")
|
|
55
|
+
expect(output).toContain("disabled")
|
|
56
|
+
expect(output).toContain("<OtherButton")
|
|
57
|
+
expect(output).toContain("</OtherButton>")
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ASTPath, JSXOpeningElement } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transforms JSX element names (both opening and closing tags)
|
|
5
|
+
*/
|
|
6
|
+
export function transformElementName(
|
|
7
|
+
elementPath: ASTPath<JSXOpeningElement>,
|
|
8
|
+
newName: string
|
|
9
|
+
): boolean {
|
|
10
|
+
if (elementPath.value.name.type === "JSXIdentifier") {
|
|
11
|
+
elementPath.value.name.name = newName
|
|
12
|
+
|
|
13
|
+
// Update closing tag if it exists
|
|
14
|
+
const parent = elementPath.parent
|
|
15
|
+
if (
|
|
16
|
+
parent &&
|
|
17
|
+
parent.value.type === "JSXElement" &&
|
|
18
|
+
parent.value.closingElement
|
|
19
|
+
) {
|
|
20
|
+
if (parent.value.closingElement.name.type === "JSXIdentifier") {
|
|
21
|
+
parent.value.closingElement.name.name = newName
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { andConditions } from "./andConditions"
|
|
4
|
+
import { hasAttribute } from "./hasAttribute"
|
|
5
|
+
import { hasAttributeValue } from "./hasAttributeValue"
|
|
6
|
+
import { createJSXElement } from "./helpers/createJSXElement"
|
|
7
|
+
|
|
8
|
+
describe("andConditions", () => {
|
|
9
|
+
it("should return true when all conditions are met", () => {
|
|
10
|
+
const condition = andConditions(
|
|
11
|
+
hasAttribute("href"),
|
|
12
|
+
hasAttributeValue("target", "_blank")
|
|
13
|
+
)
|
|
14
|
+
const element = createJSXElement(' href="/test" target="_blank"')
|
|
15
|
+
|
|
16
|
+
expect(condition(element)).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("should return false when one condition fails", () => {
|
|
20
|
+
const condition = andConditions(
|
|
21
|
+
hasAttribute("href"),
|
|
22
|
+
hasAttributeValue("target", "_blank")
|
|
23
|
+
)
|
|
24
|
+
const element = createJSXElement(' href="/test" target="_self"')
|
|
25
|
+
|
|
26
|
+
expect(condition(element)).toBe(false)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("should return false when all conditions fail", () => {
|
|
30
|
+
const condition = andConditions(
|
|
31
|
+
hasAttribute("href"),
|
|
32
|
+
hasAttributeValue("target", "_blank")
|
|
33
|
+
)
|
|
34
|
+
const element = createJSXElement(' className="test"')
|
|
35
|
+
|
|
36
|
+
expect(condition(element)).toBe(false)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("should handle single condition", () => {
|
|
40
|
+
const condition = andConditions(hasAttribute("disabled"))
|
|
41
|
+
const element = createJSXElement(" disabled")
|
|
42
|
+
|
|
43
|
+
expect(condition(element)).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("should handle empty conditions (return true)", () => {
|
|
47
|
+
const condition = andConditions()
|
|
48
|
+
const element = createJSXElement("")
|
|
49
|
+
|
|
50
|
+
expect(condition(element)).toBe(true)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("should handle complex combinations", () => {
|
|
54
|
+
const condition = andConditions(
|
|
55
|
+
hasAttribute("href"),
|
|
56
|
+
hasAttribute("className"),
|
|
57
|
+
hasAttributeValue("role", "button")
|
|
58
|
+
)
|
|
59
|
+
const element = createJSXElement(
|
|
60
|
+
' href="/test" className="btn" role="button"'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
expect(condition(element)).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { JSXElement } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { TransformCondition } from "../types"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper function to combine multiple conditions with AND logic
|
|
7
|
+
*/
|
|
8
|
+
export function andConditions(
|
|
9
|
+
...conditions: TransformCondition[]
|
|
10
|
+
): TransformCondition {
|
|
11
|
+
return (element: JSXElement) =>
|
|
12
|
+
conditions.every((condition) => condition(element))
|
|
13
|
+
}
|