@planningcenter/tapestry-migration-cli 3.1.0-rc.8 → 3.1.0

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 (72) hide show
  1. package/dist/tapestry-react-shim.cjs +7 -1
  2. package/package.json +3 -3
  3. package/src/components/input/transformableInput.ts +47 -6
  4. package/src/components/input/transforms/mergeFieldIntoInput.test.ts +78 -0
  5. package/src/components/input/transforms/mergeFieldIntoInput.ts +6 -212
  6. package/src/components/input/transforms/removeDuplicateKeys.test.ts +3 -3
  7. package/src/components/input/transforms/removeTypeInput.test.ts +212 -0
  8. package/src/components/input/transforms/removeTypeInput.ts +22 -0
  9. package/src/components/input/transforms/removeTypeText.ts +2 -3
  10. package/src/components/input/transforms/unsupportedProps.test.ts +20 -20
  11. package/src/components/select/index.ts +58 -0
  12. package/src/components/select/transformableSelect.ts +7 -0
  13. package/src/components/select/transforms/auditSpreadProps.test.ts +103 -0
  14. package/src/components/select/transforms/auditSpreadProps.ts +26 -0
  15. package/src/components/select/transforms/childrenToOptions.test.ts +367 -0
  16. package/src/components/select/transforms/childrenToOptions.ts +295 -0
  17. package/src/components/select/transforms/convertLegacyOptions.test.ts +150 -0
  18. package/src/components/select/transforms/convertLegacyOptions.ts +105 -0
  19. package/src/components/select/transforms/convertStyleProps.test.ts +73 -0
  20. package/src/components/select/transforms/convertStyleProps.ts +12 -0
  21. package/src/components/select/transforms/emptyValueToPlaceholder.test.ts +122 -0
  22. package/src/components/select/transforms/emptyValueToPlaceholder.ts +22 -0
  23. package/src/components/select/transforms/innerRefToRef.test.ts +89 -0
  24. package/src/components/select/transforms/innerRefToRef.ts +18 -0
  25. package/src/components/select/transforms/mapChildrenToOptions.test.ts +521 -0
  26. package/src/components/select/transforms/mapChildrenToOptions.ts +312 -0
  27. package/src/components/select/transforms/mergeFieldIntoSelect.test.ts +506 -0
  28. package/src/components/select/transforms/mergeFieldIntoSelect.ts +7 -0
  29. package/src/components/select/transforms/mergeSelectLabel.test.ts +458 -0
  30. package/src/components/select/transforms/mergeSelectLabel.ts +225 -0
  31. package/src/components/select/transforms/moveSelectImport.test.ts +148 -0
  32. package/src/components/select/transforms/moveSelectImport.ts +14 -0
  33. package/src/components/select/transforms/removeDefaultProps.test.ts +249 -0
  34. package/src/components/select/transforms/removeDefaultProps.ts +112 -0
  35. package/src/components/select/transforms/sizeMapping.test.ts +188 -0
  36. package/src/components/select/transforms/sizeMapping.ts +17 -0
  37. package/src/components/select/transforms/skipMultipleSelect.test.ts +148 -0
  38. package/src/components/select/transforms/skipMultipleSelect.ts +23 -0
  39. package/src/components/select/transforms/stateToInvalid.test.ts +217 -0
  40. package/src/components/select/transforms/stateToInvalid.ts +59 -0
  41. package/src/components/select/transforms/stateToInvalidTernary.test.ts +146 -0
  42. package/src/components/select/transforms/stateToInvalidTernary.ts +13 -0
  43. package/src/components/select/transforms/unsupportedProps.test.ts +252 -0
  44. package/src/components/select/transforms/unsupportedProps.ts +44 -0
  45. package/src/components/shared/helpers/getAttributeExpression.ts +26 -0
  46. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +52 -2
  47. package/src/components/shared/transformFactories/mergeFieldFactory.ts +244 -0
  48. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +2 -1
  49. package/src/components/text-area/index.ts +48 -0
  50. package/src/components/text-area/transforms/auditSpreadProps.test.ts +139 -0
  51. package/src/components/text-area/transforms/auditSpreadProps.ts +10 -0
  52. package/src/components/text-area/transforms/convertStyleProps.test.ts +158 -0
  53. package/src/components/text-area/transforms/convertStyleProps.ts +10 -0
  54. package/src/components/text-area/transforms/innerRefToRef.test.ts +206 -0
  55. package/src/components/text-area/transforms/innerRefToRef.ts +14 -0
  56. package/src/components/text-area/transforms/mergeFieldIntoTextArea.test.ts +477 -0
  57. package/src/components/text-area/transforms/mergeFieldIntoTextArea.ts +5 -0
  58. package/src/components/text-area/transforms/moveTextAreaImport.test.ts +168 -0
  59. package/src/components/text-area/transforms/moveTextAreaImport.ts +13 -0
  60. package/src/components/text-area/transforms/removeDuplicateKeys.test.ts +129 -0
  61. package/src/components/text-area/transforms/removeDuplicateKeys.ts +8 -0
  62. package/src/components/text-area/transforms/removeRedundantAriaLabel.test.ts +183 -0
  63. package/src/components/text-area/transforms/removeRedundantAriaLabel.ts +59 -0
  64. package/src/components/text-area/transforms/sizeMapping.test.ts +199 -0
  65. package/src/components/text-area/transforms/sizeMapping.ts +15 -0
  66. package/src/components/text-area/transforms/stateToInvalid.test.ts +204 -0
  67. package/src/components/text-area/transforms/stateToInvalid.ts +57 -0
  68. package/src/components/text-area/transforms/stateToInvalidTernary.test.ts +133 -0
  69. package/src/components/text-area/transforms/stateToInvalidTernary.ts +11 -0
  70. package/src/components/text-area/transforms/unsupportedProps.test.ts +275 -0
  71. package/src/components/text-area/transforms/unsupportedProps.ts +35 -0
  72. package/src/index.ts +4 -1
@@ -0,0 +1,521 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./mapChildrenToOptions"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ const result = transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ {}
14
+ ) as string | null
15
+ return result || source
16
+ }
17
+
18
+ describe("mapChildrenToOptions transform", () => {
19
+ describe("basic .map() conversion", () => {
20
+ it("should convert .map() with Select.Option to options prop", () => {
21
+ const input = `
22
+ import { Select } from "@planningcenter/tapestry-react"
23
+
24
+ function Test() {
25
+ return (
26
+ <Select>
27
+ {items.map(item => (
28
+ <Select.Option key={item.id} value={item.id}>
29
+ {item.name}
30
+ </Select.Option>
31
+ ))}
32
+ </Select>
33
+ )
34
+ }
35
+ `.trim()
36
+
37
+ const result = applyTransform(input)
38
+ expect(result).toContain("options={items.map(")
39
+ expect(result).toContain("label: item.name")
40
+ expect(result).toContain("value: item.id")
41
+ expect(result).not.toContain("Select.Option")
42
+ expect(result).not.toContain("</Select>")
43
+ })
44
+
45
+ it("should preserve data-* attributes on options", () => {
46
+ const input = `
47
+ import { Select } from "@planningcenter/tapestry-react"
48
+
49
+ function Test() {
50
+ return (
51
+ <Select>
52
+ {items.map(item => (
53
+ <Select.Option key={item.id} value={item.id} data-id={item.id}>
54
+ {item.name}
55
+ </Select.Option>
56
+ ))}
57
+ </Select>
58
+ )
59
+ }
60
+ `.trim()
61
+
62
+ const result = applyTransform(input)
63
+ expect(result).toContain('"data-id": item.id')
64
+ expect(result).toContain("label: item.name")
65
+ expect(result).toContain("value: item.id")
66
+ })
67
+
68
+ it("should handle nested property access in children", () => {
69
+ const input = `
70
+ import { Select } from "@planningcenter/tapestry-react"
71
+
72
+ function Test() {
73
+ return (
74
+ <Select>
75
+ {futureSteps.map(step => (
76
+ <Select.Option key={step.id} value={step.id}>
77
+ {step.attributes.name}
78
+ </Select.Option>
79
+ ))}
80
+ </Select>
81
+ )
82
+ }
83
+ `.trim()
84
+
85
+ const result = applyTransform(input)
86
+ expect(result).toContain("options={futureSteps.map(")
87
+ expect(result).toContain("label: step.attributes.name")
88
+ expect(result).toContain("value: step.id")
89
+ })
90
+
91
+ it("should handle string literal value", () => {
92
+ const input = `
93
+ import { Select } from "@planningcenter/tapestry-react"
94
+
95
+ function Test() {
96
+ return (
97
+ <Select>
98
+ {items.map(item => (
99
+ <Select.Option key={item.id} value={item.id}>
100
+ {item.label}
101
+ </Select.Option>
102
+ ))}
103
+ </Select>
104
+ )
105
+ }
106
+ `.trim()
107
+
108
+ const result = applyTransform(input)
109
+ expect(result).toContain("options={items.map(")
110
+ expect(result).toContain("label: item.label")
111
+ expect(result).toContain("value: item.id")
112
+ })
113
+
114
+ it("should handle disabled prop on Option", () => {
115
+ const input = `
116
+ import { Select } from "@planningcenter/tapestry-react"
117
+
118
+ function Test() {
119
+ return (
120
+ <Select>
121
+ {items.map(item => (
122
+ <Select.Option key={item.id} value={item.id} disabled={item.isDisabled}>
123
+ {item.name}
124
+ </Select.Option>
125
+ ))}
126
+ </Select>
127
+ )
128
+ }
129
+ `.trim()
130
+
131
+ const result = applyTransform(input)
132
+ expect(result).toContain("options={items.map(")
133
+ expect(result).toContain("disabled: item.isDisabled")
134
+ })
135
+
136
+ it("should handle boolean shorthand disabled", () => {
137
+ const input = `
138
+ import { Select } from "@planningcenter/tapestry-react"
139
+
140
+ function Test() {
141
+ return (
142
+ <Select>
143
+ {items.map(item => (
144
+ <Select.Option key={item.id} value={item.id} disabled>
145
+ {item.name}
146
+ </Select.Option>
147
+ ))}
148
+ </Select>
149
+ )
150
+ }
151
+ `.trim()
152
+
153
+ const result = applyTransform(input)
154
+ expect(result).toContain("disabled: true")
155
+ })
156
+
157
+ it("should make Select self-closing after removing children", () => {
158
+ const input = `
159
+ import { Select } from "@planningcenter/tapestry-react"
160
+
161
+ function Test() {
162
+ return (
163
+ <Select emptyValue="Pick one">
164
+ {items.map(item => (
165
+ <Select.Option key={item.id} value={item.id}>
166
+ {item.name}
167
+ </Select.Option>
168
+ ))}
169
+ </Select>
170
+ )
171
+ }
172
+ `.trim()
173
+
174
+ const result = applyTransform(input)
175
+ expect(result).not.toContain("</Select>")
176
+ expect(result).toContain("/>")
177
+ })
178
+
179
+ it("should preserve existing props on Select", () => {
180
+ const input = `
181
+ import { Select } from "@planningcenter/tapestry-react"
182
+
183
+ function Test() {
184
+ return (
185
+ <Select emptyValue="Pick one" disabled onChange={handler}>
186
+ {items.map(item => (
187
+ <Select.Option key={item.id} value={item.id}>
188
+ {item.name}
189
+ </Select.Option>
190
+ ))}
191
+ </Select>
192
+ )
193
+ }
194
+ `.trim()
195
+
196
+ const result = applyTransform(input)
197
+ expect(result).toContain('emptyValue="Pick one"')
198
+ expect(result).toContain("disabled")
199
+ expect(result).toContain("onChange={handler}")
200
+ expect(result).toContain("options={items.map(")
201
+ })
202
+ })
203
+
204
+ describe("callback variations", () => {
205
+ it("should handle arrow function with block body", () => {
206
+ const input = `
207
+ import { Select } from "@planningcenter/tapestry-react"
208
+
209
+ function Test() {
210
+ return (
211
+ <Select>
212
+ {items.map(item => {
213
+ return (
214
+ <Select.Option key={item.id} value={item.id}>
215
+ {item.name}
216
+ </Select.Option>
217
+ )
218
+ })}
219
+ </Select>
220
+ )
221
+ }
222
+ `.trim()
223
+
224
+ const result = applyTransform(input)
225
+ expect(result).toContain("options={items.map(")
226
+ expect(result).toContain("label: item.name")
227
+ expect(result).toContain("value: item.id")
228
+ })
229
+
230
+ it("should handle destructured callback parameter", () => {
231
+ const input = `
232
+ import { Select } from "@planningcenter/tapestry-react"
233
+
234
+ function Test() {
235
+ return (
236
+ <Select>
237
+ {items.map(({ id, name }) => (
238
+ <Select.Option key={id} value={id}>
239
+ {name}
240
+ </Select.Option>
241
+ ))}
242
+ </Select>
243
+ )
244
+ }
245
+ `.trim()
246
+
247
+ const result = applyTransform(input)
248
+ expect(result).toContain("options={items.map(")
249
+ expect(result).toContain("label: name")
250
+ expect(result).toContain("value: id")
251
+ })
252
+ })
253
+
254
+ describe("complex JSX children (ReactNode labels)", () => {
255
+ it("should convert .map() with JSX children to options with label as ReactNode", () => {
256
+ const input = `
257
+ import { Select } from "@planningcenter/tapestry-react"
258
+
259
+ function Test() {
260
+ return (
261
+ <Select>
262
+ {availableConnections.map((connection, idx) => {
263
+ return (
264
+ <Select.Option key={idx} value={connection}>
265
+ <StackView axis="horizontal" alignment="center" spacing={1}>
266
+ <Logo name={connection} size="18px" markOnly theme="color" />
267
+ <Text lineHeight={null}>{capitalize(connection)}</Text>
268
+ </StackView>
269
+ </Select.Option>
270
+ )
271
+ })}
272
+ </Select>
273
+ )
274
+ }
275
+ `.trim()
276
+
277
+ const result = applyTransform(input)
278
+ expect(result).toContain("options={availableConnections.map(")
279
+ expect(result).toContain("label: <StackView")
280
+ expect(result).toContain("value: connection")
281
+ expect(result).toContain('textValue: ""')
282
+ expect(result).toContain("TODO: tapestry-migration (textValue)")
283
+ expect(result).toContain("complex")
284
+ expect(result).not.toContain("Select.Option")
285
+ expect(result).not.toContain("</Select>")
286
+ })
287
+
288
+ it("should add complex prop for JSX label options", () => {
289
+ const input = `
290
+ import { Select } from "@planningcenter/tapestry-react"
291
+
292
+ function Test() {
293
+ return (
294
+ <Select emptyValue="Pick one">
295
+ {items.map(item => (
296
+ <Select.Option key={item.id} value={item.id}>
297
+ <div className="option"><img src={item.avatar} /> {item.name}</div>
298
+ </Select.Option>
299
+ ))}
300
+ </Select>
301
+ )
302
+ }
303
+ `.trim()
304
+
305
+ const result = applyTransform(input)
306
+ expect(result).toContain("complex")
307
+ expect(result).toContain('textValue: ""')
308
+ expect(result).toContain("options={items.map(")
309
+ })
310
+
311
+ it("should not duplicate complex prop if already present", () => {
312
+ const input = `
313
+ import { Select } from "@planningcenter/tapestry-react"
314
+
315
+ function Test() {
316
+ return (
317
+ <Select complex>
318
+ {items.map(item => (
319
+ <Select.Option key={item.id} value={item.id}>
320
+ <span><img src={item.icon} /> {item.label}</span>
321
+ </Select.Option>
322
+ ))}
323
+ </Select>
324
+ )
325
+ }
326
+ `.trim()
327
+
328
+ const result = applyTransform(input)
329
+ const complexMatches = result.match(/\bcomplex\b/g)
330
+ expect(complexMatches).toHaveLength(1)
331
+ })
332
+
333
+ it("should handle multiple JSX children with fragment wrapper", () => {
334
+ const input = `
335
+ import { Select } from "@planningcenter/tapestry-react"
336
+
337
+ function Test() {
338
+ return (
339
+ <Select>
340
+ {items.map(item => (
341
+ <Select.Option key={item.id} value={item.id}>
342
+ <img src={item.avatar} />
343
+ <span>{item.name}</span>
344
+ </Select.Option>
345
+ ))}
346
+ </Select>
347
+ )
348
+ }
349
+ `.trim()
350
+
351
+ const result = applyTransform(input)
352
+ expect(result).toContain("options={items.map(")
353
+ expect(result).toContain("value: item.id")
354
+ expect(result).toContain("complex")
355
+ expect(result).toContain('textValue: ""')
356
+ })
357
+
358
+ it("should not add textValue for simple expression labels", () => {
359
+ const input = `
360
+ import { Select } from "@planningcenter/tapestry-react"
361
+
362
+ function Test() {
363
+ return (
364
+ <Select>
365
+ {items.map(item => (
366
+ <Select.Option key={item.id} value={item.id}>
367
+ {item.name}
368
+ </Select.Option>
369
+ ))}
370
+ </Select>
371
+ )
372
+ }
373
+ `.trim()
374
+
375
+ const result = applyTransform(input)
376
+ expect(result).not.toContain("textValue")
377
+ expect(result).not.toContain("complex")
378
+ })
379
+ })
380
+
381
+ describe("edge cases / no-op", () => {
382
+ it("should not transform Select without children", () => {
383
+ const input = `
384
+ import { Select } from "@planningcenter/tapestry-react"
385
+
386
+ function Test() {
387
+ return <Select options={items} emptyValue="Pick" />
388
+ }
389
+ `.trim()
390
+
391
+ const result = applyTransform(input)
392
+ expect(result).toBe(input)
393
+ })
394
+
395
+ it("should not transform other components", () => {
396
+ const input = `
397
+ import { Dropdown } from "other-library"
398
+
399
+ function Test() {
400
+ return (
401
+ <Dropdown>
402
+ {items.map(item => (
403
+ <Dropdown.Option key={item.id} value={item.id}>
404
+ {item.name}
405
+ </Dropdown.Option>
406
+ ))}
407
+ </Dropdown>
408
+ )
409
+ }
410
+ `.trim()
411
+
412
+ const result = applyTransform(input)
413
+ expect(result).toBe(input)
414
+ })
415
+
416
+ it("should not transform if .map() returns non-Select.Option", () => {
417
+ const input = `
418
+ import { Select } from "@planningcenter/tapestry-react"
419
+
420
+ function Test() {
421
+ return (
422
+ <Select>
423
+ {items.map(item => (
424
+ <option key={item.id} value={item.id}>
425
+ {item.name}
426
+ </option>
427
+ ))}
428
+ </Select>
429
+ )
430
+ }
431
+ `.trim()
432
+
433
+ const result = applyTransform(input)
434
+ expect(result).toBe(input)
435
+ })
436
+
437
+ it("should not transform if children have multiple expressions", () => {
438
+ const input = `
439
+ import { Select } from "@planningcenter/tapestry-react"
440
+
441
+ function Test() {
442
+ return (
443
+ <Select>
444
+ {group1.map(item => (
445
+ <Select.Option key={item.id} value={item.id}>{item.name}</Select.Option>
446
+ ))}
447
+ {group2.map(item => (
448
+ <Select.Option key={item.id} value={item.id}>{item.name}</Select.Option>
449
+ ))}
450
+ </Select>
451
+ )
452
+ }
453
+ `.trim()
454
+
455
+ const result = applyTransform(input)
456
+ expect(result).toBe(input)
457
+ })
458
+
459
+ it("should not transform static Select.Option children (handled by childrenToOptions)", () => {
460
+ const input = `
461
+ import { Select } from "@planningcenter/tapestry-react"
462
+
463
+ function Test() {
464
+ return (
465
+ <Select>
466
+ <Select.Option value="a">A</Select.Option>
467
+ <Select.Option value="b">B</Select.Option>
468
+ </Select>
469
+ )
470
+ }
471
+ `.trim()
472
+
473
+ const result = applyTransform(input)
474
+ expect(result).toBe(input)
475
+ })
476
+
477
+ it("should not transform Select.Option with no value prop", () => {
478
+ const input = `
479
+ import { Select } from "@planningcenter/tapestry-react"
480
+
481
+ function Test() {
482
+ return (
483
+ <Select>
484
+ {items.map(item => (
485
+ <Select.Option key={item.id}>
486
+ {item.name}
487
+ </Select.Option>
488
+ ))}
489
+ </Select>
490
+ )
491
+ }
492
+ `.trim()
493
+
494
+ const result = applyTransform(input)
495
+ expect(result).toBe(input)
496
+ })
497
+
498
+ it("should handle aliased Select import", () => {
499
+ const input = `
500
+ import { Select as TapestrySelect } from "@planningcenter/tapestry-react"
501
+
502
+ function Test() {
503
+ return (
504
+ <TapestrySelect>
505
+ {items.map(item => (
506
+ <TapestrySelect.Option key={item.id} value={item.id}>
507
+ {item.name}
508
+ </TapestrySelect.Option>
509
+ ))}
510
+ </TapestrySelect>
511
+ )
512
+ }
513
+ `.trim()
514
+
515
+ const result = applyTransform(input)
516
+ expect(result).toContain("options={items.map(")
517
+ expect(result).toContain("label: item.name")
518
+ expect(result).toContain("value: item.id")
519
+ })
520
+ })
521
+ })