@planningcenter/tapestry-migration-cli 2.3.0-rc.6 → 2.3.0-rc.8

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 (41) hide show
  1. package/dist/tapestry-react-shim.cjs +5064 -0
  2. package/package.json +9 -5
  3. package/src/components/button/index.ts +45 -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/iconToIconButton.test.ts +377 -0
  12. package/src/components/button/transforms/iconToIconButton.ts +53 -0
  13. package/src/components/button/transforms/removeAsButton.ts +15 -0
  14. package/src/components/button/transforms/removeDuplicateKeys.test.ts +302 -0
  15. package/src/components/button/transforms/removeDuplicateKeys.ts +8 -0
  16. package/src/components/button/transforms/reviewStyles.ts +17 -0
  17. package/src/components/button/transforms/spinnerToLoadingButton.test.ts +165 -0
  18. package/src/components/button/transforms/spinnerToLoadingButton.ts +14 -0
  19. package/src/components/button/transforms/unsupportedProps.ts +73 -0
  20. package/src/components/shared/actions/addCommentToAttribute.test.ts +45 -0
  21. package/src/components/shared/actions/addCommentToAttribute.ts +28 -0
  22. package/src/components/shared/actions/addCommentToUnsupportedProps.ts +29 -0
  23. package/src/components/shared/actions/getSpreadProps.ts +7 -0
  24. package/src/components/shared/actions/hasSpreadProps.ts +7 -0
  25. package/src/components/shared/actions/removeChildren.ts +7 -0
  26. package/src/components/shared/actions/removeDuplicateKeys.test.ts +280 -0
  27. package/src/components/shared/actions/removeDuplicateKeys.ts +45 -0
  28. package/src/components/shared/actions/removeUnusedImport.test.ts +302 -0
  29. package/src/components/shared/actions/removeUnusedImport.ts +81 -0
  30. package/src/components/shared/actions/transformElementName.test.ts +9 -9
  31. package/src/components/shared/actions/transformElementName.ts +13 -16
  32. package/src/components/shared/conditions/hasChildren.ts +5 -0
  33. package/src/components/shared/getJavaScriptTheme.ts +68 -0
  34. package/src/components/shared/jsThemeLoader.ts +85 -0
  35. package/src/components/shared/transformFactories/attributeTransformFactory.ts +14 -6
  36. package/src/components/shared/transformFactories/componentTransformFactory.ts +1 -1
  37. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +362 -0
  38. package/src/index.ts +4 -0
  39. package/src/stubs/stackViewPlugin.ts +33 -0
  40. package/src/stubs/tapestry-stub.ts +16 -0
  41. package/src/tapestry-react-shim.ts +7 -0
@@ -0,0 +1,363 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./childrenToLabel"
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("childrenToLabel transform", () => {
18
+ describe("simple text conversion", () => {
19
+ it("should convert simple text children to label prop", () => {
20
+ const input = `
21
+ import { Button } from "@planningcenter/tapestry-react"
22
+
23
+ function Test() {
24
+ return <Button>Save</Button>
25
+ }
26
+ `.trim()
27
+
28
+ const result = applyTransform(input)
29
+ expect(result).toContain('<Button label="Save" />')
30
+ expect(result).not.toContain("Save</Button>")
31
+ })
32
+
33
+ it("should handle text with whitespace", () => {
34
+ const input = `
35
+ import { Button } from "@planningcenter/tapestry-react"
36
+
37
+ function Test() {
38
+ return <Button> Save Changes </Button>
39
+ }
40
+ `.trim()
41
+
42
+ const result = applyTransform(input)
43
+ expect(result).toContain('<Button label="Save Changes" />')
44
+ })
45
+
46
+ it("should convert string literal expressions", () => {
47
+ const input = `
48
+ import { Button } from "@planningcenter/tapestry-react"
49
+
50
+ function Test() {
51
+ return <Button>{"Delete"}</Button>
52
+ }
53
+ `.trim()
54
+
55
+ const result = applyTransform(input)
56
+ expect(result).toContain('<Button label="Delete" />')
57
+ })
58
+
59
+ it("should convert simple template literals without expressions", () => {
60
+ const input = `
61
+ import { Button } from "@planningcenter/tapestry-react"
62
+
63
+ function Test() {
64
+ return <Button>{\`Submit Form\`}</Button>
65
+ }
66
+ `.trim()
67
+
68
+ const result = applyTransform(input)
69
+ expect(result).toContain('<Button label="Submit Form" />')
70
+ })
71
+
72
+ it("should combine multiple text nodes", () => {
73
+ const input = `
74
+ import { Button } from "@planningcenter/tapestry-react"
75
+
76
+ function Test() {
77
+ return <Button>Save{""}{"File"}</Button>
78
+ }
79
+ `.trim()
80
+
81
+ const result = applyTransform(input)
82
+ expect(result).toContain('<Button label="SaveFile" />')
83
+ })
84
+ })
85
+
86
+ describe("complex children scenarios", () => {
87
+ it("should add comment for complex JSX children", () => {
88
+ const input = `
89
+ import { Button } from "@planningcenter/tapestry-react"
90
+
91
+ function Test() {
92
+ return (
93
+ <div>
94
+ <Button>
95
+ <Icon name="save" />
96
+ Save
97
+ </Button>
98
+ </div>
99
+ )
100
+ }
101
+ `.trim()
102
+
103
+ const result = applyTransform(input)
104
+ expect(result).toContain(
105
+ "{/* TODO: tapestry-migration (children): complex children cannot be converted to label prop"
106
+ )
107
+ expect(result).toContain(
108
+ "take time to find the right text for the button"
109
+ )
110
+ expect(result).toContain(
111
+ "prefix and suffix to correctly display those icons"
112
+ )
113
+ expect(result).toContain('<Icon name="save" />')
114
+ expect(result).toContain("Save")
115
+ expect(result).toContain("</Button>")
116
+ })
117
+
118
+ it("should add comment for expression children", () => {
119
+ const input = `
120
+ import { Button } from "@planningcenter/tapestry-react"
121
+
122
+ function Test() {
123
+ return (
124
+ <div>
125
+ <Button>{isLoading ? "Loading..." : "Submit"}</Button>
126
+ </div>
127
+ )
128
+ }
129
+ `.trim()
130
+
131
+ const result = applyTransform(input)
132
+ expect(result).toContain(
133
+ "{/* TODO: tapestry-migration (children): complex children cannot be converted to label prop"
134
+ )
135
+ expect(result).toContain('{isLoading ? "Loading..." : "Submit"}')
136
+ })
137
+
138
+ it("should add comment for template literals with expressions", () => {
139
+ const input = `
140
+ import { Button } from "@planningcenter/tapestry-react"
141
+
142
+ function Test() {
143
+ return (
144
+ <div>
145
+ <Button>{\`Save \${count} items\`}</Button>
146
+ </div>
147
+ )
148
+ }
149
+ `.trim()
150
+
151
+ const result = applyTransform(input)
152
+ expect(result).toContain(
153
+ "{/* TODO: tapestry-migration (children): complex children cannot be converted to label prop"
154
+ )
155
+ expect(result).toContain("{`Save ${count} items`}")
156
+ })
157
+ })
158
+
159
+ describe("existing label prop scenarios", () => {
160
+ it("should add comment when Button has both label and children", () => {
161
+ const input = `
162
+ import { Button } from "@planningcenter/tapestry-react"
163
+
164
+ function Test() {
165
+ return (
166
+ <div>
167
+ <Button label="Existing Label">
168
+ Child Content
169
+ </Button>
170
+ </div>
171
+ )
172
+ }
173
+ `.trim()
174
+
175
+ const result = applyTransform(input)
176
+ expect(result).toContain(
177
+ "{/* TODO: tapestry-migration (label): Button has both label prop and children"
178
+ )
179
+ expect(result).toContain(
180
+ "take time to find the right text for the button"
181
+ )
182
+ expect(result).toContain('label="Existing Label"')
183
+ expect(result).toContain("Child Content")
184
+ })
185
+
186
+ it("should handle Button with label and simple text children", () => {
187
+ const input = `
188
+ import { Button } from "@planningcenter/tapestry-react"
189
+
190
+ function Test() {
191
+ return (
192
+ <div>
193
+ <Button label="Save">Click Me</Button>
194
+ </div>
195
+ )
196
+ }
197
+ `.trim()
198
+
199
+ const result = applyTransform(input)
200
+ expect(result).toContain(
201
+ "{/* TODO: tapestry-migration (label): Button has both label prop and children"
202
+ )
203
+ expect(result).toContain('label="Save"')
204
+ expect(result).toContain("Click Me")
205
+ })
206
+ })
207
+
208
+ describe("edge cases", () => {
209
+ it("should skip Buttons with no children", () => {
210
+ const input = `
211
+ import { Button } from "@planningcenter/tapestry-react"
212
+
213
+ function Test() {
214
+ return <Button onClick={handleClick} />
215
+ }
216
+ `.trim()
217
+
218
+ const result = applyTransform(input)
219
+ expect(result).toBe(null) // No changes
220
+ })
221
+
222
+ it("should skip Buttons with only whitespace children", () => {
223
+ const input = `
224
+ import { Button } from "@planningcenter/tapestry-react"
225
+
226
+ function Test() {
227
+ return <Button> </Button>
228
+ }
229
+ `.trim()
230
+
231
+ const result = applyTransform(input)
232
+ expect(result).toBe(null) // No changes since whitespace is trimmed to empty
233
+ })
234
+
235
+ it("should handle mixed whitespace and text", () => {
236
+ const input = `
237
+ import { Button } from "@planningcenter/tapestry-react"
238
+
239
+ function Test() {
240
+ return <Button> Submit Form </Button>
241
+ }
242
+ `.trim()
243
+
244
+ const result = applyTransform(input)
245
+ expect(result).toContain('<Button label="Submit Form" />')
246
+ })
247
+
248
+ it("should not transform Button from other packages", () => {
249
+ const input = `
250
+ import { Button } from "@other/package"
251
+ import { Button as TapestryButton } from "@planningcenter/tapestry-react"
252
+
253
+ function Test() {
254
+ return (
255
+ <div>
256
+ <Button>Should not change</Button>
257
+ <TapestryButton>Should change</TapestryButton>
258
+ </div>
259
+ )
260
+ }
261
+ `.trim()
262
+
263
+ const result = applyTransform(input)
264
+ expect(result).toContain("<Button>Should not change</Button>")
265
+ expect(result).toContain('<TapestryButton label="Should change" />')
266
+ })
267
+
268
+ it("should handle aliased imports", () => {
269
+ const input = `
270
+ import { Button as TapestryButton } from "@planningcenter/tapestry-react"
271
+
272
+ function Test() {
273
+ return <TapestryButton>Save</TapestryButton>
274
+ }
275
+ `.trim()
276
+
277
+ const result = applyTransform(input)
278
+ expect(result).toContain('<TapestryButton label="Save" />')
279
+ })
280
+
281
+ it("should return null for files without tapestry-react Button", () => {
282
+ const input = `
283
+ import { Button } from "react-bootstrap"
284
+
285
+ function Test() {
286
+ return <Button>Save</Button>
287
+ }
288
+ `.trim()
289
+
290
+ const result = applyTransform(input)
291
+ expect(result).toBe(null)
292
+ })
293
+ })
294
+
295
+ describe("multiple buttons", () => {
296
+ it("should handle multiple buttons with different scenarios", () => {
297
+ const input = `
298
+ import { Button } from "@planningcenter/tapestry-react"
299
+
300
+ function Test() {
301
+ return (
302
+ <div>
303
+ <Button>Simple Text</Button>
304
+ <Button label="existing">Has Label</Button>
305
+ <Button>
306
+ <Icon name="save" />
307
+ Complex
308
+ </Button>
309
+ <Button>{"String Expression"}</Button>
310
+ </div>
311
+ )
312
+ }
313
+ `.trim()
314
+
315
+ const result = applyTransform(input)
316
+
317
+ // Simple text should be converted
318
+ expect(result).toContain('<Button label="Simple Text" />')
319
+
320
+ // Existing label should get comment
321
+ expect(result).toContain(
322
+ "{/* TODO: tapestry-migration (label): Button has both label prop and children"
323
+ )
324
+ expect(result).toContain('label="existing"')
325
+
326
+ // Complex JSX should get comment
327
+ expect(result).toContain(
328
+ "{/* TODO: tapestry-migration (children): complex children cannot be converted to label prop"
329
+ )
330
+ expect(result).toContain('<Icon name="save" />')
331
+
332
+ // String expression should be converted
333
+ expect(result).toContain('<Button label="String Expression" />')
334
+ })
335
+ })
336
+
337
+ describe("comment formatting", () => {
338
+ it("should properly format JSX comments in JSX context", () => {
339
+ const input = `
340
+ import { Button } from "@planningcenter/tapestry-react"
341
+
342
+ function Test() {
343
+ return (
344
+ <div>
345
+ <Button>
346
+ <span>Complex</span>
347
+ </Button>
348
+ </div>
349
+ )
350
+ }
351
+ `.trim()
352
+
353
+ const result = applyTransform(input)
354
+
355
+ // Should have proper JSX comment syntax
356
+ expect(result).toMatch(/\{\s*\/\*\s*TODO: tapestry-migration/)
357
+ expect(result).toMatch(/\*\/\s*\}/)
358
+ expect(result).toContain(
359
+ "take time to find the right text for the button"
360
+ )
361
+ })
362
+ })
363
+ })
@@ -0,0 +1,84 @@
1
+ import { JSXElement } from "jscodeshift"
2
+
3
+ import { addAttribute } from "../../shared/actions/addAttribute"
4
+ import { addComment } from "../../shared/actions/addComment"
5
+ import { removeChildren } from "../../shared/actions/removeChildren"
6
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
7
+ import { hasChildren } from "../../shared/conditions/hasChildren"
8
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
9
+
10
+ function extractTextContent(children: NonNullable<JSXElement["children"]>): {
11
+ isSimpleText: boolean
12
+ textContent: string
13
+ } {
14
+ let textContent = ""
15
+
16
+ for (const child of children) {
17
+ if (child.type === "JSXText") {
18
+ const text = child.value.trim()
19
+ if (text) textContent += text
20
+ } else if (
21
+ child.type === "JSXExpressionContainer" &&
22
+ child.expression.type === "StringLiteral"
23
+ ) {
24
+ textContent += child.expression.value
25
+ } else if (
26
+ child.type === "JSXExpressionContainer" &&
27
+ child.expression.type === "TemplateLiteral" &&
28
+ child.expression.expressions.length === 0
29
+ ) {
30
+ // Simple template literal with no expressions like `hello`
31
+ textContent += child.expression.quasis[0].value.raw
32
+ } else {
33
+ // Complex content (JSX elements, expressions, etc.)
34
+ return { isSimpleText: false, textContent: "" }
35
+ }
36
+ }
37
+
38
+ return { isSimpleText: true, textContent }
39
+ }
40
+
41
+ function buildComment(message: string): string {
42
+ return `${message} - take time to find the right text for the button. If icons are used in the Button, you can use prefix and suffix to correctly display those icons.`
43
+ }
44
+
45
+ const transform = attributeTransformFactory({
46
+ condition: hasChildren,
47
+ targetComponent: "Button",
48
+ targetPackage: "@planningcenter/tapestry-react",
49
+ transform: (element, { j, source }) => {
50
+ if (hasAttribute("label")(element)) {
51
+ addComment({
52
+ element,
53
+ j,
54
+ scope: "label",
55
+ source,
56
+ text: buildComment("Button has both label prop and children"),
57
+ })
58
+ return true
59
+ }
60
+
61
+ const { isSimpleText, textContent } = extractTextContent(element.children!)
62
+
63
+ if (isSimpleText && textContent) {
64
+ addAttribute({ element, j, name: "label", value: textContent })
65
+ removeChildren(element)
66
+ return true
67
+ } else if (!isSimpleText) {
68
+ addComment({
69
+ element,
70
+ j,
71
+ scope: "children",
72
+ source,
73
+ text: buildComment(
74
+ "complex children cannot be converted to label prop"
75
+ ),
76
+ })
77
+ return true
78
+ }
79
+
80
+ return false
81
+ },
82
+ })
83
+
84
+ export default transform
@@ -0,0 +1,24 @@
1
+ import { addComment } from "../../shared/actions/addComment"
2
+ import { hasAttributeValue } from "../../shared/conditions/hasAttributeValue"
3
+ import { orConditions } from "../../shared/conditions/orConditions"
4
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
5
+
6
+ export default attributeTransformFactory({
7
+ condition: orConditions(
8
+ hasAttributeValue("theme", "info"),
9
+ hasAttributeValue("theme", "success")
10
+ ),
11
+ targetComponent: "Button",
12
+ targetPackage: "@planningcenter/tapestry-react",
13
+ transform: (element, { j, source }) => {
14
+ addComment({
15
+ element,
16
+ j,
17
+ scope: "theme",
18
+ source,
19
+ text: "Your button's theme is 'info' or 'success', which both map to the same 'primary' kind. This will create a visual change, but it is the recommended adjustment.",
20
+ })
21
+
22
+ return true
23
+ },
24
+ })