@planningcenter/tapestry-migration-cli 2.3.0-rc.7 → 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "2.3.0-rc.7",
3
+ "version": "2.3.0-rc.8",
4
4
  "description": "CLI tool for Tapestry migrations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,9 +8,9 @@
8
8
  "tapestry-migration-cli": "./src/index.ts"
9
9
  },
10
10
  "scripts": {
11
- "build": "tsc",
11
+ "build": "vite build",
12
12
  "dev": "tsc --watch",
13
- "test": "vitest run",
13
+ "test": "yarn build && vitest run",
14
14
  "test:watch": "vitest",
15
15
  "prepublishOnly": "npm run build"
16
16
  },
@@ -31,13 +31,17 @@
31
31
  "tsx": "^4.20.5"
32
32
  },
33
33
  "devDependencies": {
34
+ "@emotion/react": "^11.14.0",
35
+ "@planningcenter/tapestry-react": "^4.11.5",
34
36
  "@types/jscodeshift": "^17.3.0",
35
37
  "@types/node": "^20.0.0",
36
38
  "typescript": "^5.8.3",
39
+ "vite": "^5.0.0",
37
40
  "vitest": "^3.0.0"
38
41
  },
39
42
  "files": [
40
- "src/**/*"
43
+ "src/**/*",
44
+ "dist"
41
45
  ],
42
46
  "repository": {
43
47
  "type": "git",
@@ -47,5 +51,5 @@
47
51
  "publishConfig": {
48
52
  "access": "public"
49
53
  },
50
- "gitHead": "eba3f5ee024bbbff6fbaadf7218862444a22c790"
54
+ "gitHead": "fd77bfbbe8d0562720aaee1ae3e9210d81af8db2"
51
55
  }
@@ -1,26 +1,68 @@
1
1
  import { Transform } from "jscodeshift"
2
2
 
3
+ import { JavaScriptTheme, loadJavaScriptTheme } from "../shared/jsThemeLoader"
4
+ import auditSpreadProps from "./transforms/auditSpreadProps"
5
+ import childrenToLabel from "./transforms/childrenToLabel"
6
+ import commentOnVisualKindDifference from "./transforms/commentOnVisualKindDifference"
7
+ import convertStyleProps from "./transforms/convertStyleProps"
3
8
  import iconLeftToPrefix from "./transforms/iconLeftToPrefix"
4
9
  import iconRightToSuffix from "./transforms/iconRightToSuffix"
10
+ import iconToIconButton from "./transforms/iconToIconButton"
5
11
  import linkToButton from "./transforms/linkToButton"
6
12
  import moveButtonImport from "./transforms/moveButtonImport"
13
+ import removeAsButton from "./transforms/removeAsButton"
14
+ import removeDuplicateKeys from "./transforms/removeDuplicateKeys"
7
15
  import removeToTransform from "./transforms/removeTo"
16
+ import reviewStyles from "./transforms/reviewStyles"
17
+ import spinnerToLoadingButton from "./transforms/spinnerToLoadingButton"
8
18
  import themeVariantToKind from "./transforms/themeVariantToKind"
9
19
  import titleToLabel from "./transforms/titleToLabel"
10
20
  import tooltipToWrapper from "./transforms/tooltipToWrapper"
21
+ import unsupportedProps from "./transforms/unsupportedProps"
11
22
 
12
- const transform: Transform = (fileInfo, api, options) => {
23
+ const transform: Transform = async (fileInfo, api, options) => {
13
24
  let currentSource = fileInfo.source
14
25
  let hasAnyChanges = false
15
26
 
16
- const transforms: Transform[] = [
27
+ // Load JavaScript theme if provided
28
+ let jsThemeData: JavaScriptTheme | null = null
29
+ if (options.jsTheme) {
30
+ try {
31
+ jsThemeData = await loadJavaScriptTheme(options.jsTheme)
32
+ if (options.verbose) {
33
+ console.log(`🎨 JS Theme data: ${JSON.stringify(jsThemeData)}`)
34
+ }
35
+ } catch (error) {
36
+ console.error(
37
+ `❌ Failed to prepare JS theme: ${error instanceof Error ? error.message : "Unknown error"}`
38
+ )
39
+ }
40
+ }
41
+
42
+ // Add JS theme info to options for individual transforms
43
+ const enhancedOptions = {
44
+ ...options,
45
+ jsThemeData,
46
+ }
47
+
48
+ const transforms = [
17
49
  linkToButton,
18
50
  tooltipToWrapper,
19
51
  iconRightToSuffix,
20
52
  iconLeftToPrefix,
53
+ commentOnVisualKindDifference,
21
54
  themeVariantToKind,
22
55
  titleToLabel,
56
+ childrenToLabel,
23
57
  removeToTransform,
58
+ removeDuplicateKeys,
59
+ removeAsButton,
60
+ auditSpreadProps,
61
+ reviewStyles,
62
+ convertStyleProps,
63
+ unsupportedProps,
64
+ iconToIconButton,
65
+ spinnerToLoadingButton,
24
66
  moveButtonImport,
25
67
  ]
26
68
 
@@ -28,7 +70,7 @@ const transform: Transform = (fileInfo, api, options) => {
28
70
  const result = individualTransform(
29
71
  { ...fileInfo, source: currentSource },
30
72
  api,
31
- options
73
+ enhancedOptions
32
74
  )
33
75
  if (result && result !== currentSource) {
34
76
  currentSource = result as string
@@ -0,0 +1,352 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./auditSpreadProps"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ const AUDIT_COMMENT =
9
+ "TODO: tapestry-migration (spreadAttribute): Spread props can contain unsupported props, please explore usages and migrate as needed."
10
+
11
+ function applyTransform(source: string) {
12
+ const fileInfo = { path: "test.tsx", source }
13
+ return transform(
14
+ fileInfo,
15
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
16
+ {}
17
+ ) as string | null
18
+ }
19
+
20
+ describe("auditSpreadProps transform", () => {
21
+ describe("basic transformations", () => {
22
+ it("should add comment to Button with single spread prop", () => {
23
+ const input = `
24
+ import { Button } from "@planningcenter/tapestry-react"
25
+
26
+ export default function Test() {
27
+ const props = { onClick: handleClick }
28
+ return <Button {...props}>Save</Button>
29
+ }
30
+ `.trim()
31
+
32
+ const result = applyTransform(input)
33
+ expect(result).toContain(AUDIT_COMMENT)
34
+ expect(result).toContain("{...props}")
35
+ })
36
+
37
+ it("should add comment to Button with multiple spread props", () => {
38
+ const input = `
39
+ import { Button } from "@planningcenter/tapestry-react"
40
+
41
+ export default function Test() {
42
+ const baseProps = { onClick: handleClick }
43
+ const styleProps = { className: "btn" }
44
+ return <Button {...baseProps} {...styleProps}>Save</Button>
45
+ }
46
+ `.trim()
47
+
48
+ const result = applyTransform(input)
49
+ expect(result).toContain(AUDIT_COMMENT)
50
+ expect(result).toContain("{...baseProps}")
51
+ expect(result).toContain("{...styleProps}")
52
+ })
53
+
54
+ it("should handle Button with spread props and regular attributes", () => {
55
+ const input = `
56
+ import { Button } from "@planningcenter/tapestry-react"
57
+
58
+ export default function Test() {
59
+ const props = { onClick: handleClick }
60
+ return <Button label="Save" {...props} disabled />
61
+ }
62
+ `.trim()
63
+
64
+ const result = applyTransform(input)
65
+ expect(result).toContain(AUDIT_COMMENT)
66
+ expect(result).toContain('label="Save"')
67
+ expect(result).toContain("{...props}")
68
+ expect(result).toContain("disabled")
69
+ })
70
+
71
+ it("should handle multiple Button components with spread props", () => {
72
+ const input = `
73
+ import { Button } from "@planningcenter/tapestry-react"
74
+
75
+ export default function Test() {
76
+ const props1 = { onClick: handleClick1 }
77
+ const props2 = { onClick: handleClick2 }
78
+ return (
79
+ <div>
80
+ <Button {...props1}>Save</Button>
81
+ <Button {...props2}>Cancel</Button>
82
+ </div>
83
+ )
84
+ }
85
+ `.trim()
86
+
87
+ const result = applyTransform(input)
88
+ expect(result).toContain(AUDIT_COMMENT)
89
+ expect(result).toContain("{...props1}")
90
+ expect(result).toContain("{...props2}")
91
+ })
92
+ })
93
+
94
+ describe("edge cases", () => {
95
+ it("should not transform Button without spread props", () => {
96
+ const input = `
97
+ import { Button } from "@planningcenter/tapestry-react"
98
+
99
+ export default function Test() {
100
+ return <Button onClick={handleClick}>Save</Button>
101
+ }
102
+ `.trim()
103
+
104
+ const result = applyTransform(input)
105
+ expect(result).toBe(null)
106
+ })
107
+
108
+ it("should not transform if Button is not imported from @planningcenter/tapestry-react", () => {
109
+ const input = `
110
+ import { Button } from "other-library"
111
+
112
+ export default function Test() {
113
+ const props = { onClick: handleClick }
114
+ return <Button {...props}>Save</Button>
115
+ }
116
+ `.trim()
117
+
118
+ const result = applyTransform(input)
119
+ expect(result).toBe(null)
120
+ })
121
+
122
+ it("should handle Button with alias import", () => {
123
+ const input = `
124
+ import { Button as TapestryButton } from "@planningcenter/tapestry-react"
125
+
126
+ export default function Test() {
127
+ const props = { onClick: handleClick }
128
+ return <TapestryButton {...props}>Save</TapestryButton>
129
+ }
130
+ `.trim()
131
+
132
+ const result = applyTransform(input)
133
+ expect(result).toContain(AUDIT_COMMENT)
134
+ expect(result).toContain("{...props}")
135
+ })
136
+
137
+ it("should handle mixed Button components (with and without spread props)", () => {
138
+ const input = `
139
+ import { Button } from "@planningcenter/tapestry-react"
140
+
141
+ export default function Test() {
142
+ const props = { onClick: handleSave }
143
+ return (
144
+ <div>
145
+ <Button {...props}>Save</Button>
146
+ <Button>Cancel</Button>
147
+ <Button onClick={handleDelete}>Delete</Button>
148
+ </div>
149
+ )
150
+ }
151
+ `.trim()
152
+
153
+ const result = applyTransform(input)
154
+ expect(result).toContain(AUDIT_COMMENT)
155
+ expect(result).toContain("{...props}")
156
+ expect(result).toContain("<Button>Cancel</Button>")
157
+ expect(result).toContain("onClick={handleDelete}")
158
+ })
159
+ })
160
+
161
+ describe("complex spread prop scenarios", () => {
162
+ it("should handle Button with complex spread expression", () => {
163
+ const input = `
164
+ import { Button } from "@planningcenter/tapestry-react"
165
+
166
+ export default function Test() {
167
+ const baseProps = { className: "btn" }
168
+ const additionalProps = isDisabled ? { disabled: true } : {}
169
+ return <Button {...baseProps} {...additionalProps} onClick={handleClick}>Save</Button>
170
+ }
171
+ `.trim()
172
+
173
+ const result = applyTransform(input)
174
+ expect(result).toContain(AUDIT_COMMENT)
175
+ expect(result).toContain("{...baseProps}")
176
+ expect(result).toContain("{...additionalProps}")
177
+ })
178
+
179
+ it("should handle Button with spread props in different positions", () => {
180
+ const input = `
181
+ import { Button } from "@planningcenter/tapestry-react"
182
+
183
+ export default function Test() {
184
+ const props = { className: "btn" }
185
+ return (
186
+ <Button
187
+ label="Save"
188
+ {...props}
189
+ disabled={false}
190
+ onClick={handleSave}
191
+ />
192
+ )
193
+ }
194
+ `.trim()
195
+
196
+ const result = applyTransform(input)
197
+ expect(result).toContain(AUDIT_COMMENT)
198
+ expect(result).toContain('label="Save"')
199
+ expect(result).toContain("{...props}")
200
+ expect(result).toContain("disabled={false}")
201
+ expect(result).toContain("onClick={handleSave}")
202
+ })
203
+
204
+ it("should handle spread props with object expressions", () => {
205
+ const input = `
206
+ import { Button } from "@planningcenter/tapestry-react"
207
+
208
+ export default function Test() {
209
+ return <Button {...{ onClick: handleClick, className: "btn" }}>Save</Button>
210
+ }
211
+ `.trim()
212
+
213
+ const result = applyTransform(input)
214
+ expect(result).toContain(AUDIT_COMMENT)
215
+ expect(result).toContain(
216
+ '{...{ onClick: handleClick, className: "btn" }}'
217
+ )
218
+ })
219
+
220
+ it("should handle spread props with function calls", () => {
221
+ const input = `
222
+ import { Button } from "@planningcenter/tapestry-react"
223
+
224
+ export default function Test() {
225
+ return <Button {...getButtonProps()} {...getStyleProps()}>Save</Button>
226
+ }
227
+ `.trim()
228
+
229
+ const result = applyTransform(input)
230
+ expect(result).toContain(AUDIT_COMMENT)
231
+ expect(result).toContain("{...getButtonProps()}")
232
+ expect(result).toContain("{...getStyleProps()}")
233
+ })
234
+ })
235
+
236
+ describe("self-closing Button components", () => {
237
+ it("should handle self-closing Button with spread props", () => {
238
+ const input = `
239
+ import { Button } from "@planningcenter/tapestry-react"
240
+
241
+ export default function Test() {
242
+ const props = { label: "Close", onClick: handleClose }
243
+ return <Button {...props} />
244
+ }
245
+ `.trim()
246
+
247
+ const result = applyTransform(input)
248
+ expect(result).toContain(AUDIT_COMMENT)
249
+ expect(result).toContain("{...props}")
250
+ })
251
+
252
+ it("should handle self-closing Button with multiple spread props and attributes", () => {
253
+ const input = `
254
+ import { Button } from "@planningcenter/tapestry-react"
255
+
256
+ export default function Test() {
257
+ const baseProps = { onClick: handleClick }
258
+ const styleProps = { className: "btn" }
259
+ return <Button label="Action" {...baseProps} {...styleProps} disabled />
260
+ }
261
+ `.trim()
262
+
263
+ const result = applyTransform(input)
264
+ expect(result).toContain(AUDIT_COMMENT)
265
+ expect(result).toContain('label="Action"')
266
+ expect(result).toContain("{...baseProps}")
267
+ expect(result).toContain("{...styleProps}")
268
+ expect(result).toContain("disabled")
269
+ })
270
+ })
271
+
272
+ describe("no changes scenarios", () => {
273
+ it("should return null when no Button components have spread props", () => {
274
+ const input = `
275
+ import { Button } from "@planningcenter/tapestry-react"
276
+
277
+ export default function Test() {
278
+ return (
279
+ <div>
280
+ <Button>Save</Button>
281
+ <Button label="Delete" />
282
+ <Button onClick={handleClick}>Cancel</Button>
283
+ </div>
284
+ )
285
+ }
286
+ `.trim()
287
+
288
+ const result = applyTransform(input)
289
+ expect(result).toBe(null)
290
+ })
291
+
292
+ it("should return null when no Button imports exist", () => {
293
+ const input = `
294
+ import { Link } from "@planningcenter/tapestry-react"
295
+
296
+ export default function Test() {
297
+ const props = { href: "/test" }
298
+ return <Link {...props}>Go</Link>
299
+ }
300
+ `.trim()
301
+
302
+ const result = applyTransform(input)
303
+ expect(result).toBe(null)
304
+ })
305
+
306
+ it("should return null for empty file", () => {
307
+ const result = applyTransform("")
308
+ expect(result).toBe(null)
309
+ })
310
+
311
+ it("should return null when file has no JSX", () => {
312
+ const input = `
313
+ export function handleClick() {
314
+ console.log("clicked")
315
+ }
316
+ `.trim()
317
+
318
+ const result = applyTransform(input)
319
+ expect(result).toBe(null)
320
+ })
321
+ })
322
+
323
+ describe("different import scenarios", () => {
324
+ it("should handle default import with spread props", () => {
325
+ const input = `
326
+ import Button from "@planningcenter/tapestry-react"
327
+
328
+ export default function Test() {
329
+ const props = { onClick: handleClick }
330
+ return <Button {...props}>Save</Button>
331
+ }
332
+ `.trim()
333
+
334
+ const result = applyTransform(input)
335
+ expect(result).toBe(null)
336
+ })
337
+
338
+ it("should handle namespace import", () => {
339
+ const input = `
340
+ import * as Tapestry from "@planningcenter/tapestry-react"
341
+
342
+ export default function Test() {
343
+ const props = { onClick: handleClick }
344
+ return <Tapestry.Button {...props}>Save</Tapestry.Button>
345
+ }
346
+ `.trim()
347
+
348
+ const result = applyTransform(input)
349
+ expect(result).toBe(null)
350
+ })
351
+ })
352
+ })
@@ -0,0 +1,24 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
4
+ import { getSpreadProps } from "../../shared/actions/getSpreadProps"
5
+ import { hasSpreadProps } from "../../shared/actions/hasSpreadProps"
6
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
7
+
8
+ const COMMENT =
9
+ "Spread props can contain unsupported props, please explore usages and migrate as needed."
10
+
11
+ const transform: Transform = attributeTransformFactory({
12
+ condition: hasSpreadProps,
13
+ targetComponent: "Button",
14
+ targetPackage: "@planningcenter/tapestry-react",
15
+ transform: (element, { j }) => {
16
+ const spreadProps = getSpreadProps(element)
17
+ spreadProps.forEach((prop) =>
18
+ addCommentToAttribute({ attribute: prop, j, text: COMMENT })
19
+ )
20
+ return spreadProps.length > 0
21
+ },
22
+ })
23
+
24
+ export default transform