@planningcenter/tapestry-migration-cli 2.8.0-rc.13 → 2.8.0-rc.15

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "2.8.0-rc.13",
3
+ "version": "2.8.0-rc.15",
4
4
  "description": "CLI tool for Tapestry migrations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -52,5 +52,5 @@
52
52
  "publishConfig": {
53
53
  "access": "public"
54
54
  },
55
- "gitHead": "420b064d8d040be10e3837b4c287692791564d08"
55
+ "gitHead": "6fca04d14f1d3e001938f95095612537fd3654ea"
56
56
  }
@@ -253,7 +253,7 @@ describe("themeVariantToKind", () => {
253
253
  expect(result).toContain(
254
254
  "TODO: tapestry-migration (theme/variant): cannot be converted"
255
255
  )
256
- expect(result).toContain('{"primary"}')
256
+ expect(result).toContain('theme="primary"')
257
257
  })
258
258
  })
259
259
 
@@ -0,0 +1,38 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import auditSpreadProps from "./transforms/auditSpreadProps"
4
+ import convertStyleProps from "./transforms/convertStyleProps"
5
+ import moveRadioImport from "./transforms/moveRadioImport"
6
+ import setDefaultSize from "./transforms/setDefaultSize"
7
+ import sizeMapping from "./transforms/sizeMapping"
8
+ import unsupportedProps from "./transforms/unsupportedProps"
9
+
10
+ const transform: Transform = (fileInfo, api, options) => {
11
+ let currentSource = fileInfo.source
12
+ let hasAnyChanges = false
13
+
14
+ const transforms = [
15
+ auditSpreadProps,
16
+ setDefaultSize,
17
+ sizeMapping,
18
+ convertStyleProps,
19
+ unsupportedProps,
20
+ moveRadioImport,
21
+ ]
22
+
23
+ for (const individualTransform of transforms) {
24
+ const result = individualTransform(
25
+ { ...fileInfo, source: currentSource },
26
+ api,
27
+ options
28
+ )
29
+ if (result && result !== currentSource) {
30
+ currentSource = result as string
31
+ hasAnyChanges = true
32
+ }
33
+ }
34
+
35
+ return hasAnyChanges ? currentSource : null
36
+ }
37
+
38
+ export default transform
@@ -0,0 +1,356 @@
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 Radio with single spread prop", () => {
23
+ const input = `
24
+ import { Radio } from "@planningcenter/tapestry-react"
25
+
26
+ export default function Test() {
27
+ const props = { onChange: handleChange }
28
+ return <Radio {...props} label="Save" name="choice" />
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 Radio with multiple spread props", () => {
38
+ const input = `
39
+ import { Radio } from "@planningcenter/tapestry-react"
40
+
41
+ export default function Test() {
42
+ const baseProps = { onChange: handleChange }
43
+ const styleProps = { className: "radio" }
44
+ return <Radio {...baseProps} {...styleProps} label="Save" name="choice" />
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 Radio with spread props and regular attributes", () => {
55
+ const input = `
56
+ import { Radio } from "@planningcenter/tapestry-react"
57
+
58
+ export default function Test() {
59
+ const props = { onChange: handleChange }
60
+ return <Radio label="Save" name="choice" {...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('name="choice"')
68
+ expect(result).toContain("{...props}")
69
+ expect(result).toContain("disabled")
70
+ })
71
+
72
+ it("should handle multiple Radio components with spread props", () => {
73
+ const input = `
74
+ import { Radio } from "@planningcenter/tapestry-react"
75
+
76
+ export default function Test() {
77
+ const props1 = { onChange: handleChange1 }
78
+ const props2 = { onChange: handleChange2 }
79
+ return (
80
+ <div>
81
+ <Radio {...props1} label="Save" name="choice" />
82
+ <Radio {...props2} label="Cancel" name="choice" />
83
+ </div>
84
+ )
85
+ }
86
+ `.trim()
87
+
88
+ const result = applyTransform(input)
89
+ expect(result).toContain(AUDIT_COMMENT)
90
+ expect(result).toContain("{...props1}")
91
+ expect(result).toContain("{...props2}")
92
+ })
93
+ })
94
+
95
+ describe("edge cases", () => {
96
+ it("should not transform Radio without spread props", () => {
97
+ const input = `
98
+ import { Radio } from "@planningcenter/tapestry-react"
99
+
100
+ export default function Test() {
101
+ return <Radio onChange={handleChange} label="Save" name="choice" />
102
+ }
103
+ `.trim()
104
+
105
+ const result = applyTransform(input)
106
+ expect(result).toBe(null)
107
+ })
108
+
109
+ it("should not transform if Radio is not imported from @planningcenter/tapestry-react", () => {
110
+ const input = `
111
+ import { Radio } from "other-library"
112
+
113
+ export default function Test() {
114
+ const props = { onChange: handleChange }
115
+ return <Radio {...props} label="Save" name="choice" />
116
+ }
117
+ `.trim()
118
+
119
+ const result = applyTransform(input)
120
+ expect(result).toBe(null)
121
+ })
122
+
123
+ it("should handle Radio with alias import", () => {
124
+ const input = `
125
+ import { Radio as TapestryRadio } from "@planningcenter/tapestry-react"
126
+
127
+ export default function Test() {
128
+ const props = { onChange: handleChange }
129
+ return <TapestryRadio {...props} label="Save" name="choice" />
130
+ }
131
+ `.trim()
132
+
133
+ const result = applyTransform(input)
134
+ expect(result).toContain(AUDIT_COMMENT)
135
+ expect(result).toContain("{...props}")
136
+ })
137
+
138
+ it("should handle mixed Radio components (with and without spread props)", () => {
139
+ const input = `
140
+ import { Radio } from "@planningcenter/tapestry-react"
141
+
142
+ export default function Test() {
143
+ const props = { onChange: handleSave }
144
+ return (
145
+ <div>
146
+ <Radio {...props} label="Save" name="choice" />
147
+ <Radio label="Cancel" name="choice" />
148
+ <Radio onChange={handleDelete} label="Delete" name="choice" />
149
+ </div>
150
+ )
151
+ }
152
+ `.trim()
153
+
154
+ const result = applyTransform(input)
155
+ expect(result).toContain(AUDIT_COMMENT)
156
+ expect(result).toContain("{...props}")
157
+ expect(result).toContain('<Radio label="Cancel" name="choice" />')
158
+ expect(result).toContain("onChange={handleDelete}")
159
+ })
160
+ })
161
+
162
+ describe("complex spread prop scenarios", () => {
163
+ it("should handle Radio with complex spread expression", () => {
164
+ const input = `
165
+ import { Radio } from "@planningcenter/tapestry-react"
166
+
167
+ export default function Test() {
168
+ const baseProps = { className: "radio" }
169
+ const additionalProps = isDisabled ? { disabled: true } : {}
170
+ return <Radio {...baseProps} {...additionalProps} label="Save" name="choice" />
171
+ }
172
+ `.trim()
173
+
174
+ const result = applyTransform(input)
175
+ expect(result).toContain(AUDIT_COMMENT)
176
+ expect(result).toContain("{...baseProps}")
177
+ expect(result).toContain("{...additionalProps}")
178
+ })
179
+
180
+ it("should handle Radio with spread props in different positions", () => {
181
+ const input = `
182
+ import { Radio } from "@planningcenter/tapestry-react"
183
+
184
+ export default function Test() {
185
+ const props = { className: "radio" }
186
+ return (
187
+ <Radio
188
+ label="Save"
189
+ name="choice"
190
+ {...props}
191
+ disabled={false}
192
+ onChange={handleSave}
193
+ />
194
+ )
195
+ }
196
+ `.trim()
197
+
198
+ const result = applyTransform(input)
199
+ expect(result).toContain(AUDIT_COMMENT)
200
+ expect(result).toContain('label="Save"')
201
+ expect(result).toContain('name="choice"')
202
+ expect(result).toContain("{...props}")
203
+ expect(result).toContain("disabled={false}")
204
+ expect(result).toContain("onChange={handleSave}")
205
+ })
206
+
207
+ it("should handle spread props with object expressions", () => {
208
+ const input = `
209
+ import { Radio } from "@planningcenter/tapestry-react"
210
+
211
+ export default function Test() {
212
+ return <Radio {...{ onChange: handleChange, className: "radio" }} label="Save" name="choice" />
213
+ }
214
+ `.trim()
215
+
216
+ const result = applyTransform(input)
217
+ expect(result).toContain(AUDIT_COMMENT)
218
+ expect(result).toContain(
219
+ '{...{ onChange: handleChange, className: "radio" }}'
220
+ )
221
+ })
222
+
223
+ it("should handle spread props with function calls", () => {
224
+ const input = `
225
+ import { Radio } from "@planningcenter/tapestry-react"
226
+
227
+ export default function Test() {
228
+ return <Radio {...getRadioProps()} {...getStyleProps()} label="Save" name="choice" />
229
+ }
230
+ `.trim()
231
+
232
+ const result = applyTransform(input)
233
+ expect(result).toContain(AUDIT_COMMENT)
234
+ expect(result).toContain("{...getRadioProps()}")
235
+ expect(result).toContain("{...getStyleProps()}")
236
+ })
237
+ })
238
+
239
+ describe("self-closing Radio components", () => {
240
+ it("should handle self-closing Radio with spread props", () => {
241
+ const input = `
242
+ import { Radio } from "@planningcenter/tapestry-react"
243
+
244
+ export default function Test() {
245
+ const props = { label: "Close", name: "choice", onChange: handleClose }
246
+ return <Radio {...props} />
247
+ }
248
+ `.trim()
249
+
250
+ const result = applyTransform(input)
251
+ expect(result).toContain(AUDIT_COMMENT)
252
+ expect(result).toContain("{...props}")
253
+ })
254
+
255
+ it("should handle self-closing Radio with multiple spread props and attributes", () => {
256
+ const input = `
257
+ import { Radio } from "@planningcenter/tapestry-react"
258
+
259
+ export default function Test() {
260
+ const baseProps = { onChange: handleChange }
261
+ const styleProps = { className: "radio" }
262
+ return <Radio label="Action" name="choice" {...baseProps} {...styleProps} disabled />
263
+ }
264
+ `.trim()
265
+
266
+ const result = applyTransform(input)
267
+ expect(result).toContain(AUDIT_COMMENT)
268
+ expect(result).toContain('label="Action"')
269
+ expect(result).toContain('name="choice"')
270
+ expect(result).toContain("{...baseProps}")
271
+ expect(result).toContain("{...styleProps}")
272
+ expect(result).toContain("disabled")
273
+ })
274
+ })
275
+
276
+ describe("no changes scenarios", () => {
277
+ it("should return null when no Radio components have spread props", () => {
278
+ const input = `
279
+ import { Radio } from "@planningcenter/tapestry-react"
280
+
281
+ export default function Test() {
282
+ return (
283
+ <div>
284
+ <Radio label="Save" name="choice" />
285
+ <Radio label="Delete" name="choice" />
286
+ <Radio onChange={handleChange} label="Cancel" name="choice" />
287
+ </div>
288
+ )
289
+ }
290
+ `.trim()
291
+
292
+ const result = applyTransform(input)
293
+ expect(result).toBe(null)
294
+ })
295
+
296
+ it("should return null when no Radio imports exist", () => {
297
+ const input = `
298
+ import { Button } from "@planningcenter/tapestry-react"
299
+
300
+ export default function Test() {
301
+ const props = { onClick: handleClick }
302
+ return <Button {...props}>Save</Button>
303
+ }
304
+ `.trim()
305
+
306
+ const result = applyTransform(input)
307
+ expect(result).toBe(null)
308
+ })
309
+
310
+ it("should return null for empty file", () => {
311
+ const result = applyTransform("")
312
+ expect(result).toBe(null)
313
+ })
314
+
315
+ it("should return null when file has no JSX", () => {
316
+ const input = `
317
+ export function handleChange() {
318
+ console.log("changed")
319
+ }
320
+ `.trim()
321
+
322
+ const result = applyTransform(input)
323
+ expect(result).toBe(null)
324
+ })
325
+ })
326
+
327
+ describe("different import scenarios", () => {
328
+ it("should handle default import with spread props", () => {
329
+ const input = `
330
+ import Radio from "@planningcenter/tapestry-react"
331
+
332
+ export default function Test() {
333
+ const props = { onChange: handleChange }
334
+ return <Radio {...props} label="Save" name="choice" />
335
+ }
336
+ `.trim()
337
+
338
+ const result = applyTransform(input)
339
+ expect(result).toBe(null)
340
+ })
341
+
342
+ it("should handle namespace import", () => {
343
+ const input = `
344
+ import * as Tapestry from "@planningcenter/tapestry-react"
345
+
346
+ export default function Test() {
347
+ const props = { onChange: handleChange }
348
+ return <Tapestry.Radio {...props} label="Save" name="choice" />
349
+ }
350
+ `.trim()
351
+
352
+ const result = applyTransform(input)
353
+ expect(result).toBe(null)
354
+ })
355
+ })
356
+ })
@@ -0,0 +1,10 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { commentOnSpreadPropsFactory } from "../../shared/transformFactories/commentOnSpreadPropsFactory"
4
+
5
+ const transform: Transform = commentOnSpreadPropsFactory({
6
+ targetComponent: "Radio",
7
+ targetPackage: "@planningcenter/tapestry-react",
8
+ })
9
+
10
+ export default transform
@@ -0,0 +1,161 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./convertStyleProps"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string, options = {}) {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ const result = transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ options
14
+ ) as string | null
15
+ return result || source
16
+ }
17
+
18
+ describe("convertStyleProps transform", () => {
19
+ describe("visible prop - handled by theme system", () => {
20
+ it("should convert visible={false} to display: none", () => {
21
+ const source = `
22
+ import { Radio } from "@planningcenter/tapestry-react"
23
+
24
+ export function TestComponent() {
25
+ return <Radio visible={false} label="Hidden Radio" />
26
+ }
27
+ `
28
+
29
+ const result = applyTransform(source)
30
+
31
+ expect(result).toContain("style={{")
32
+ expect(result).toContain('display: "none"')
33
+ expect(result).not.toContain("visible={false}")
34
+ })
35
+
36
+ it("should remove visible={true} with no style changes (true is default)", () => {
37
+ const source = `
38
+ import { Radio } from "@planningcenter/tapestry-react"
39
+
40
+ export function TestComponent() {
41
+ return <Radio visible={true} label="Visible Radio" />
42
+ }
43
+ `
44
+
45
+ const result = applyTransform(source)
46
+
47
+ // visible={true} is default behavior, so it should be removed with no style added
48
+ expect(result).not.toContain("visible={true}")
49
+ expect(result).not.toContain("style={{") // No style should be added
50
+ expect(result).toContain('<Radio label="Visible Radio" />')
51
+ })
52
+
53
+ it("should handle visible expressions", () => {
54
+ const source = `
55
+ import { Radio } from "@planningcenter/tapestry-react"
56
+
57
+ export function TestComponent() {
58
+ const isVisible = true
59
+ return <Radio visible={isVisible} label="Variable Radio" />
60
+ }
61
+ `
62
+
63
+ const result = applyTransform(source)
64
+
65
+ // Note: visible expressions are handled by handleVisibleExpressions transform,
66
+ // not convertStyleProps, so the visible prop should remain
67
+ expect(result).toContain("visible: isVisible")
68
+ expect(result).toContain("style={{") // Style should be added for expressions
69
+ })
70
+ })
71
+
72
+ describe("style prop removal", () => {
73
+ it("should remove alignItems prop", () => {
74
+ const source = `
75
+ import { Radio } from "@planningcenter/tapestry-react"
76
+
77
+ export function TestComponent() {
78
+ return <Radio alignItems="center" label="Test" />
79
+ }
80
+ `
81
+
82
+ const result = applyTransform(source)
83
+
84
+ expect(result).not.toContain("alignItems=")
85
+ })
86
+
87
+ it("should remove marginTop prop", () => {
88
+ const source = `
89
+ import { Radio } from "@planningcenter/tapestry-react"
90
+
91
+ export function TestComponent() {
92
+ return <Radio marginTop={16} label="Test" />
93
+ }
94
+ `
95
+
96
+ const result = applyTransform(source)
97
+
98
+ expect(result).not.toContain("marginTop=")
99
+ })
100
+
101
+ it("should remove multiple style props", () => {
102
+ const source = `
103
+ import { Radio } from "@planningcenter/tapestry-react"
104
+
105
+ export function TestComponent() {
106
+ return <Radio alignItems="center" marginTop={16} label="Test" />
107
+ }
108
+ `
109
+
110
+ const result = applyTransform(source)
111
+
112
+ expect(result).not.toContain("alignItems=")
113
+ expect(result).not.toContain("marginTop=")
114
+ })
115
+ })
116
+
117
+ describe("combination of style props", () => {
118
+ it("should handle combination of visible and style props", () => {
119
+ const source = `
120
+ import { Radio } from "@planningcenter/tapestry-react"
121
+
122
+ export function TestComponent() {
123
+ return (
124
+ <Radio
125
+ visible={false}
126
+ alignItems="center"
127
+ marginTop={16}
128
+ label="Combined Radio"
129
+ />
130
+ )
131
+ }
132
+ `
133
+
134
+ const result = applyTransform(source)
135
+
136
+ expect(result).toContain("style={{")
137
+ expect(result).toContain('display: "none"') // from visible={false}
138
+ expect(result).not.toContain("visible={false}")
139
+ expect(result).not.toContain("alignItems=")
140
+ expect(result).not.toContain("marginTop=")
141
+ })
142
+ })
143
+
144
+ describe("import handling", () => {
145
+ it("should not affect imports", () => {
146
+ const source = `
147
+ import { Radio } from "@planningcenter/tapestry-react"
148
+
149
+ export function TestComponent() {
150
+ return <Radio visible={false} label="Test" />
151
+ }
152
+ `
153
+
154
+ const result = applyTransform(source)
155
+
156
+ expect(result).toContain(
157
+ 'import { Radio } from "@planningcenter/tapestry-react"'
158
+ )
159
+ })
160
+ })
161
+ })
@@ -0,0 +1,10 @@
1
+ import { stackViewPlugin } from "../../../stubs/stackViewPlugin"
2
+ import { stylePropTransformFactory } from "../../shared/transformFactories/stylePropTransformFactory"
3
+
4
+ export default stylePropTransformFactory({
5
+ plugin: stackViewPlugin,
6
+ stylesToKeep: ["visible"],
7
+ stylesToRemove: [],
8
+ targetComponent: "Radio",
9
+ targetPackage: "@planningcenter/tapestry-react",
10
+ })