@planningcenter/tapestry-migration-cli 2.2.1-qa-362.0 → 2.3.0-rc.10

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.
Files changed (64) hide show
  1. package/dist/tapestry-react-shim.cjs +5065 -0
  2. package/package.json +9 -5
  3. package/src/components/button/index.ts +53 -3
  4. package/src/components/button/transforms/auditSpreadProps.test.ts +352 -0
  5. package/src/components/button/transforms/auditSpreadProps.ts +24 -0
  6. package/src/components/button/transforms/childrenToLabel.test.ts +363 -0
  7. package/src/components/button/transforms/childrenToLabel.ts +84 -0
  8. package/src/components/button/transforms/commentOnVisualKindDifference.ts +24 -0
  9. package/src/components/button/transforms/convertStyleProps.test.ts +464 -0
  10. package/src/components/button/transforms/convertStyleProps.ts +16 -0
  11. package/src/components/button/transforms/iconLeftToPrefix.test.ts +432 -0
  12. package/src/components/button/transforms/iconLeftToPrefix.ts +33 -0
  13. package/src/components/button/transforms/iconRightToSuffix.test.ts +407 -0
  14. package/src/components/button/transforms/iconRightToSuffix.ts +33 -0
  15. package/src/components/button/transforms/iconToIconButton.test.ts +377 -0
  16. package/src/components/button/transforms/iconToIconButton.ts +53 -0
  17. package/src/components/button/transforms/removeAsButton.ts +15 -0
  18. package/src/components/button/transforms/removeDuplicateKeys.test.ts +302 -0
  19. package/src/components/button/transforms/removeDuplicateKeys.ts +8 -0
  20. package/src/components/button/transforms/reviewStyles.ts +17 -0
  21. package/src/components/button/transforms/spinnerToLoadingButton.test.ts +165 -0
  22. package/src/components/button/transforms/spinnerToLoadingButton.ts +14 -0
  23. package/src/components/button/transforms/themeVariantToKind.test.ts +401 -0
  24. package/src/components/button/transforms/themeVariantToKind.ts +90 -0
  25. package/src/components/button/transforms/tooltipToWrapper.test.ts +392 -0
  26. package/src/components/button/transforms/tooltipToWrapper.ts +35 -0
  27. package/src/components/button/transforms/unsupportedProps.ts +73 -0
  28. package/src/components/shared/actions/addAttribute.test.ts +300 -0
  29. package/src/components/shared/actions/addAttribute.ts +65 -0
  30. package/src/components/shared/actions/addComment.test.ts +1 -1
  31. package/src/components/shared/actions/addCommentToAttribute.test.ts +45 -0
  32. package/src/components/shared/actions/addCommentToAttribute.ts +28 -0
  33. package/src/components/shared/actions/addCommentToUnsupportedProps.ts +29 -0
  34. package/src/components/shared/actions/convertAttributeFromObjectToJSXElement.test.ts +139 -0
  35. package/src/components/shared/actions/convertAttributeFromObjectToJSXElement.ts +81 -0
  36. package/src/components/shared/actions/createWrapper.test.ts +642 -0
  37. package/src/components/shared/actions/createWrapper.ts +70 -0
  38. package/src/components/shared/actions/getAttribute.ts +18 -0
  39. package/src/components/shared/actions/getAttributeValue.test.ts +261 -0
  40. package/src/components/shared/actions/getAttributeValue.ts +15 -0
  41. package/src/components/shared/actions/getAttributeValueAsProps.ts +57 -0
  42. package/src/components/shared/actions/getSpreadProps.ts +7 -0
  43. package/src/components/shared/actions/hasSpreadProps.ts +7 -0
  44. package/src/components/shared/actions/removeChildren.ts +7 -0
  45. package/src/components/shared/actions/removeDuplicateKeys.test.ts +280 -0
  46. package/src/components/shared/actions/removeDuplicateKeys.ts +45 -0
  47. package/src/components/shared/actions/removeUnusedImport.test.ts +302 -0
  48. package/src/components/shared/actions/removeUnusedImport.ts +81 -0
  49. package/src/components/shared/actions/transformElementName.test.ts +9 -9
  50. package/src/components/shared/actions/transformElementName.ts +13 -16
  51. package/src/components/shared/conditions/hasChildren.ts +5 -0
  52. package/src/components/shared/getJavaScriptTheme.ts +68 -0
  53. package/src/components/shared/jsThemeLoader.ts +85 -0
  54. package/src/components/shared/transformFactories/attributeCombineFactory.test.ts +374 -0
  55. package/src/components/shared/transformFactories/attributeCombineFactory.ts +300 -0
  56. package/src/components/shared/transformFactories/attributeTransformFactory.ts +14 -6
  57. package/src/components/shared/transformFactories/componentTransformFactory.ts +1 -1
  58. package/src/components/shared/transformFactories/helpers/addImport.test.ts +278 -0
  59. package/src/components/shared/transformFactories/helpers/manageImports.ts +53 -20
  60. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +362 -0
  61. package/src/index.ts +4 -0
  62. package/src/stubs/stackViewPlugin.ts +33 -0
  63. package/src/stubs/tapestry-stub.ts +16 -0
  64. package/src/tapestry-react-shim.ts +7 -0
@@ -0,0 +1,377 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./iconToIconButton"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string, options = {}) {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ return transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ options
14
+ ) as string | null
15
+ }
16
+
17
+ describe("iconToIconButton transform", () => {
18
+ describe("Button with icon prop", () => {
19
+ it("should convert Button with icon object to IconButton with Icon element", () => {
20
+ const source = `
21
+ import { Button } from "@planningcenter/tapestry-react"
22
+
23
+ export function TestComponent() {
24
+ return (
25
+ <Button icon={{ name: "add", size: 16 }} kind="primary">
26
+ Add Item
27
+ </Button>
28
+ )
29
+ }
30
+ `
31
+
32
+ const result = applyTransform(source)
33
+
34
+ expect(result).toContain(
35
+ 'import { Icon } from "@planningcenter/tapestry-react"'
36
+ )
37
+ expect(result).toContain(
38
+ 'import { IconButton } from "@planningcenter/tapestry"'
39
+ )
40
+ expect(result).toContain("<IconButton")
41
+ expect(result).toContain("</IconButton>")
42
+ expect(result).not.toContain("<Button")
43
+ expect(result).not.toContain("</Button>")
44
+ expect(result).toContain('icon={<Icon {...{ name: "add", size: 16 }} />}')
45
+ expect(result).not.toContain('icon={{ name: "add", size: 16 }}')
46
+ expect(result).toContain('kind="primary"')
47
+ expect(result).toContain("Add Item")
48
+ })
49
+
50
+ it("should handle self-closing Button with icon", () => {
51
+ const source = `
52
+ import { Button } from "@planningcenter/tapestry-react"
53
+
54
+ export function TestComponent() {
55
+ return <Button icon={{ name: "close" }} aria-label="Close" />
56
+ }
57
+ `
58
+
59
+ const result = applyTransform(source)
60
+
61
+ expect(result).toContain(
62
+ 'import { Icon } from "@planningcenter/tapestry-react"'
63
+ )
64
+ expect(result).toContain(
65
+ 'import { IconButton } from "@planningcenter/tapestry"'
66
+ )
67
+ expect(result).toContain("<IconButton")
68
+ expect(result).toContain('aria-label="Close" />')
69
+ expect(result).not.toContain("<Button")
70
+ expect(result).toContain('icon={<Icon {...{ name: "close" }} />}')
71
+ })
72
+
73
+ it("should handle complex icon objects with multiple properties", () => {
74
+ const source = `
75
+ import { Button } from "@planningcenter/tapestry-react"
76
+
77
+ export function TestComponent() {
78
+ return (
79
+ <Button
80
+ icon={{
81
+ name: "settings",
82
+ size: 24,
83
+ color: "primary",
84
+ className: "custom-icon"
85
+ }}
86
+ disabled
87
+ >
88
+ Settings
89
+ </Button>
90
+ )
91
+ }
92
+ `
93
+
94
+ const result = applyTransform(source)
95
+
96
+ expect(result).toContain("icon={<Icon")
97
+ expect(result).toContain('name: "settings"')
98
+ expect(result).toContain("size: 24")
99
+ expect(result).toContain('color: "primary"')
100
+ expect(result).toContain('className: "custom-icon"')
101
+ expect(result).toContain("} />}")
102
+ expect(result).toContain("<IconButton")
103
+ expect(result).toContain("disabled")
104
+ expect(result).toContain("Settings")
105
+ })
106
+
107
+ it("should handle icon with variable expressions", () => {
108
+ const source = `
109
+ import { Button } from "@planningcenter/tapestry-react"
110
+
111
+ export function TestComponent() {
112
+ const iconProps = { name: "star", size: iconSize }
113
+ return <Button icon={iconProps}>Favorite</Button>
114
+ }
115
+ `
116
+
117
+ const result = applyTransform(source)
118
+
119
+ expect(result).toContain("icon={<Icon {...iconProps} />}")
120
+ expect(result).toContain("<IconButton")
121
+ expect(result).not.toContain("icon={iconProps}")
122
+ })
123
+ })
124
+
125
+ describe("import management", () => {
126
+ it("should add Icon to existing tapestry-react import", () => {
127
+ const source = `
128
+ import { Button } from "@planningcenter/tapestry-react"
129
+
130
+ export function TestComponent() {
131
+ return <Button icon={{ name: "add" }}>Add</Button>
132
+ }
133
+ `
134
+
135
+ const result = applyTransform(source)
136
+
137
+ expect(result).not.toBeNull()
138
+ expect(result).toContain(
139
+ 'import { Icon } from "@planningcenter/tapestry-react"'
140
+ )
141
+ expect(result).toContain(
142
+ 'import { IconButton } from "@planningcenter/tapestry"'
143
+ )
144
+ })
145
+
146
+ it("should handle existing Icon import with alias", () => {
147
+ const source = `
148
+ import { Button, Icon as TapestryIcon } from "@planningcenter/tapestry-react"
149
+
150
+ export function TestComponent() {
151
+ return <Button icon={{ name: "add" }}>Add</Button>
152
+ }
153
+ `
154
+
155
+ const result = applyTransform(source)
156
+
157
+ expect(result).toContain(
158
+ 'import { Icon as TapestryIcon } from "@planningcenter/tapestry-react"'
159
+ )
160
+ expect(result).toContain('icon={<TapestryIcon {...{ name: "add" }} />}')
161
+ })
162
+
163
+ it("should handle existing IconButton import with alias", () => {
164
+ const source = `
165
+ import { Button } from "@planningcenter/tapestry-react"
166
+ import { IconButton } from "some-other-library"
167
+
168
+ export function TestComponent() {
169
+ return <Button icon={{ name: "add" }}>Add</Button>
170
+ }
171
+ `
172
+
173
+ const result = applyTransform(source)
174
+
175
+ expect(result).toContain(
176
+ 'import { IconButton as TIconButton } from "@planningcenter/tapestry"'
177
+ )
178
+ expect(result).toContain("<TIconButton")
179
+ expect(result).toContain("</TIconButton>")
180
+ })
181
+
182
+ it("should handle multiple existing imports", () => {
183
+ const source = `
184
+ import { Button, Icon } from "@planningcenter/tapestry-react"
185
+ import { IconButton, Tooltip } from "@planningcenter/tapestry"
186
+
187
+ export function TestComponent() {
188
+ return <Button icon={{ name: "info" }}>Info</Button>
189
+ }
190
+ `
191
+
192
+ const result = applyTransform(source)
193
+
194
+ expect(result).toContain(
195
+ 'import { Icon } from "@planningcenter/tapestry-react"'
196
+ )
197
+ expect(result).toContain(
198
+ 'import { IconButton, Tooltip } from "@planningcenter/tapestry"'
199
+ )
200
+ expect(result).toContain('icon={<Icon {...{ name: "info" }} />}')
201
+ expect(result).toContain("<IconButton")
202
+ })
203
+ })
204
+
205
+ describe("Button without icon prop", () => {
206
+ it("should return null when no icon prop is present", () => {
207
+ const source = `
208
+ import { Button } from "@planningcenter/tapestry-react"
209
+
210
+ export function TestComponent() {
211
+ return <Button kind="primary">No Icon</Button>
212
+ }
213
+ `
214
+
215
+ const result = applyTransform(source)
216
+
217
+ expect(result).toBeNull()
218
+ })
219
+
220
+ it("should not process Button with other props but no icon", () => {
221
+ const source = `
222
+ import { Button } from "@planningcenter/tapestry-react"
223
+
224
+ export function TestComponent() {
225
+ return (
226
+ <Button
227
+ kind="secondary"
228
+ disabled
229
+ onClick={handleClick}
230
+ className="custom-button"
231
+ >
232
+ Click Me
233
+ </Button>
234
+ )
235
+ }
236
+ `
237
+
238
+ const result = applyTransform(source)
239
+
240
+ expect(result).toBeNull()
241
+ })
242
+ })
243
+
244
+ describe("import source validation", () => {
245
+ it("should only process Buttons from tapestry-react package", () => {
246
+ const source = `
247
+ import { Button } from "some-other-library"
248
+
249
+ export function TestComponent() {
250
+ return <Button icon={{ name: "add" }}>Add</Button>
251
+ }
252
+ `
253
+
254
+ const result = applyTransform(source)
255
+
256
+ expect(result).toBeNull()
257
+ })
258
+
259
+ it("should handle aliased Button imports", () => {
260
+ const source = `
261
+ import { Button as TapestryButton } from "@planningcenter/tapestry-react"
262
+
263
+ export function TestComponent() {
264
+ return <TapestryButton icon={{ name: "edit" }}>Edit</TapestryButton>
265
+ }
266
+ `
267
+
268
+ const result = applyTransform(source)
269
+
270
+ expect(result).toContain(
271
+ 'import { Button as TapestryButton, Icon } from "@planningcenter/tapestry-react"'
272
+ )
273
+ expect(result).toContain("<IconButton")
274
+ expect(result).toContain('icon={<Icon {...{ name: "edit" }} />}')
275
+ })
276
+ })
277
+
278
+ describe("edge cases", () => {
279
+ it("should handle empty icon object", () => {
280
+ const source = `
281
+ import { Button } from "@planningcenter/tapestry-react"
282
+
283
+ export function TestComponent() {
284
+ return <Button icon={{}}>Empty Icon</Button>
285
+ }
286
+ `
287
+
288
+ const result = applyTransform(source)
289
+
290
+ expect(result).not.toBeNull()
291
+ expect(result).toContain("<IconButton")
292
+ expect(result).toContain("icon={{}}")
293
+ })
294
+
295
+ it("should handle icon with null/undefined expression", () => {
296
+ const source = `
297
+ import { Button } from "@planningcenter/tapestry-react"
298
+
299
+ export function TestComponent() {
300
+ return <Button icon={null}>Null Icon</Button>
301
+ }
302
+ `
303
+
304
+ const result = applyTransform(source)
305
+
306
+ expect(result).not.toBeNull()
307
+ expect(result).toContain("<IconButton")
308
+ expect(result).toContain("icon={<Icon {...null} />}")
309
+ })
310
+
311
+ it("should handle multiple Buttons with different icon configurations", () => {
312
+ const source = `
313
+ import { Button } from "@planningcenter/tapestry-react"
314
+
315
+ export function TestComponent() {
316
+ return (
317
+ <div>
318
+ <Button icon={{ name: "add" }}>Add</Button>
319
+ <Button kind="secondary">No Icon</Button>
320
+ <Button icon={{ name: "delete", color: "danger" }}>Delete</Button>
321
+ </div>
322
+ )
323
+ }
324
+ `
325
+
326
+ const result = applyTransform(source)
327
+
328
+ const iconButtonCount = (result!.match(/<IconButton/g) || []).length
329
+ expect(iconButtonCount).toBe(2)
330
+ expect(result).toContain('<Button kind="secondary">No Icon</Button>')
331
+ expect(result).toContain('icon={<Icon {...{ name: "add" }} />}')
332
+ expect(result).toContain(
333
+ 'icon={<Icon {...{ name: "delete", color: "danger" }} />}'
334
+ )
335
+ })
336
+
337
+ it("should preserve all other Button props during transformation", () => {
338
+ const source = `
339
+ import { Button } from "@planningcenter/tapestry-react"
340
+
341
+ export function TestComponent() {
342
+ return (
343
+ <Button
344
+ icon={{ name: "save" }}
345
+ kind="primary"
346
+ size="large"
347
+ disabled={isLoading}
348
+ onClick={handleSave}
349
+ className="save-button"
350
+ data-testid="save-btn"
351
+ aria-label="Save document"
352
+ type="submit"
353
+ >
354
+ Save Document
355
+ </Button>
356
+ )
357
+ }
358
+ `
359
+
360
+ const result = applyTransform(source)
361
+
362
+ expect(result).toContain('kind="primary"')
363
+ expect(result).toContain('size="large"')
364
+ expect(result).toContain("disabled={isLoading}")
365
+ expect(result).toContain("onClick={handleSave}")
366
+ expect(result).toContain('className="save-button"')
367
+ expect(result).toContain('data-testid="save-btn"')
368
+ expect(result).toContain('aria-label="Save document"')
369
+ expect(result).toContain('type="submit"')
370
+ expect(result).toContain("Save Document")
371
+ expect(result).toContain("<IconButton")
372
+ expect(result).toContain('icon={<Icon {...{ name: "save" }} />}')
373
+ expect(result).not.toContain("<Button")
374
+ expect(result).not.toContain("</Button>")
375
+ })
376
+ })
377
+ })
@@ -0,0 +1,53 @@
1
+ import { JSXIdentifier } from "jscodeshift"
2
+
3
+ import { convertAttributeFromObjectToJSXElement } from "../../shared/actions/convertAttributeFromObjectToJSXElement"
4
+ import { removeUnusedImport } from "../../shared/actions/removeUnusedImport"
5
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
6
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
7
+ import { addImport } from "../../shared/transformFactories/helpers/manageImports"
8
+
9
+ const transform = attributeTransformFactory({
10
+ condition: hasAttribute("icon"),
11
+ targetComponent: "Button",
12
+ targetPackage: "@planningcenter/tapestry-react",
13
+ transform: (element, { j, source }) => {
14
+ const name = addImport({
15
+ component: "Icon",
16
+ conflictAlias: "TRIcon",
17
+ j,
18
+ pkg: "@planningcenter/tapestry-react",
19
+ source,
20
+ })
21
+ const iconButtonName = addImport({
22
+ component: "IconButton",
23
+ conflictAlias: "TIconButton",
24
+ j,
25
+ pkg: "@planningcenter/tapestry",
26
+ source,
27
+ })
28
+ convertAttributeFromObjectToJSXElement({
29
+ attributeName: "icon",
30
+ element,
31
+ elementName: name,
32
+ j,
33
+ stringValueKey: "name",
34
+ })
35
+
36
+ const componentName = (element.openingElement.name as JSXIdentifier)
37
+ .name as string
38
+ element.openingElement.name = j.jsxIdentifier(iconButtonName)
39
+ if (element.closingElement)
40
+ element.closingElement.name = j.jsxIdentifier(iconButtonName)
41
+
42
+ removeUnusedImport({
43
+ componentName,
44
+ j,
45
+ packageName: "@planningcenter/tapestry-react",
46
+ source,
47
+ })
48
+
49
+ return true
50
+ },
51
+ })
52
+
53
+ export default transform
@@ -0,0 +1,15 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { removeAttribute } from "../../shared/actions/removeAttribute"
4
+ import { hasAttributeValue } from "../../shared/conditions/hasAttributeValue"
5
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
6
+
7
+ const transform: Transform = attributeTransformFactory({
8
+ condition: hasAttributeValue("as", "button"),
9
+ targetComponent: "Button",
10
+ targetPackage: "@planningcenter/tapestry-react",
11
+ transform: (element, { j, source }) =>
12
+ removeAttribute("as", { element, j, source }),
13
+ })
14
+
15
+ export default transform