@planningcenter/tapestry-migration-cli 3.5.0 → 3.6.1-qa-1035.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 (52) hide show
  1. package/README.md +37 -0
  2. package/dist/tapestry-react-shim.cjs +195 -11
  3. package/package.json +3 -3
  4. package/src/availableComponents.ts +1 -0
  5. package/src/components/button/transforms/tooltipToWrapper.test.ts +50 -3
  6. package/src/components/button/transforms/tooltipToWrapper.ts +18 -0
  7. package/src/components/flex/index.test.ts +291 -0
  8. package/src/components/flex/index.ts +54 -0
  9. package/src/components/flex/transforms/alignmentToAlign.test.ts +185 -0
  10. package/src/components/flex/transforms/alignmentToAlign.ts +73 -0
  11. package/src/components/flex/transforms/auditSpreadProps.test.ts +93 -0
  12. package/src/components/flex/transforms/auditSpreadProps.ts +6 -0
  13. package/src/components/flex/transforms/axisToDirection.test.ts +155 -0
  14. package/src/components/flex/transforms/axisToDirection.ts +77 -0
  15. package/src/components/flex/transforms/convertStyleProps.test.ts +147 -0
  16. package/src/components/flex/transforms/convertStyleProps.ts +23 -0
  17. package/src/components/flex/transforms/cssFlexAliasesToProps.test.ts +202 -0
  18. package/src/components/flex/transforms/cssFlexAliasesToProps.ts +110 -0
  19. package/src/components/flex/transforms/distributionToJustify.test.ts +214 -0
  20. package/src/components/flex/transforms/distributionToJustify.ts +76 -0
  21. package/src/components/flex/transforms/flexPropToExpand.test.ts +156 -0
  22. package/src/components/flex/transforms/flexPropToExpand.ts +100 -0
  23. package/src/components/flex/transforms/innerRefToRef.test.ts +100 -0
  24. package/src/components/flex/transforms/innerRefToRef.ts +14 -0
  25. package/src/components/flex/transforms/mediaQueriesToResponsive.test.ts +714 -0
  26. package/src/components/flex/transforms/mediaQueriesToResponsive.ts +523 -0
  27. package/src/components/flex/transforms/moveFlexImport.test.ts +202 -0
  28. package/src/components/flex/transforms/moveFlexImport.ts +14 -0
  29. package/src/components/flex/transforms/paddingPropsToFlex.test.ts +252 -0
  30. package/src/components/flex/transforms/paddingPropsToFlex.ts +150 -0
  31. package/src/components/flex/transforms/setDefaultAxis.test.ts +126 -0
  32. package/src/components/flex/transforms/setDefaultAxis.ts +30 -0
  33. package/src/components/flex/transforms/spacingToGap.test.ts +176 -0
  34. package/src/components/flex/transforms/spacingToGap.ts +49 -0
  35. package/src/components/flex/transforms/unsupportedProps.test.ts +141 -0
  36. package/src/components/flex/transforms/unsupportedProps.ts +17 -0
  37. package/src/components/select/index.ts +2 -0
  38. package/src/components/select/transforms/onChangeSignature.test.ts +224 -0
  39. package/src/components/select/transforms/onChangeSignature.ts +172 -0
  40. package/src/components/shared/actions/resolveConstantAttributeValue.test.ts +122 -0
  41. package/src/components/shared/actions/resolveConstantAttributeValue.ts +31 -0
  42. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +41 -0
  43. package/src/components/shared/transformFactories/stylePropTransformFactory.test.ts +47 -0
  44. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +16 -0
  45. package/src/components/toggle-switch/index.ts +2 -0
  46. package/src/components/toggle-switch/transforms/onClickToOnChange.test.ts +210 -0
  47. package/src/components/toggle-switch/transforms/onClickToOnChange.ts +71 -0
  48. package/src/index.test.ts +79 -0
  49. package/src/index.ts +29 -0
  50. package/src/migrationList.ts +22 -0
  51. package/src/utils/componentLabel.test.ts +61 -0
  52. package/src/utils/componentLabel.ts +15 -0
@@ -0,0 +1,523 @@
1
+ import { JSCodeshift, JSXAttribute, Transform } from "jscodeshift"
2
+
3
+ import { formatComment } from "../../shared/actions/addComment"
4
+ import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
5
+ import { getAttribute } from "../../shared/actions/getAttribute"
6
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
7
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
8
+
9
+ function commentOnAttr(
10
+ attr: JSXAttribute,
11
+ scope: string,
12
+ text: string,
13
+ j: JSCodeshift
14
+ ) {
15
+ const comment = j.commentBlock(formatComment(text, scope), true, false)
16
+ attr.comments = attr.comments ? [...attr.comments, comment] : [comment]
17
+ }
18
+
19
+ const VALID_FLEX_BREAKPOINTS = new Set(["xs", "sm", "md", "lg", "xl"])
20
+
21
+ const CONSTANT_AXIS_MAP: Record<string, string> = {
22
+ HORIZONTAL: "horizontal",
23
+ VERTICAL: "vertical",
24
+ }
25
+
26
+ const AXIS_DIRECTION_MAP: Record<string, string> = {
27
+ horizontal: "row",
28
+ vertical: "column",
29
+ }
30
+
31
+ const VALID_ALIGN_VALUES = new Set([
32
+ "start",
33
+ "center",
34
+ "end",
35
+ "stretch",
36
+ "baseline",
37
+ ])
38
+
39
+ const CONSTANT_ALIGN_MAP: Record<string, string> = {
40
+ BASELINE: "baseline",
41
+ CENTER: "center",
42
+ END: "end",
43
+ FILL: "fill",
44
+ START: "start",
45
+ STRETCH: "stretch",
46
+ }
47
+
48
+ const VALID_JUSTIFY_VALUES = new Set([
49
+ "start",
50
+ "center",
51
+ "end",
52
+ "space-between",
53
+ "space-evenly",
54
+ ])
55
+
56
+ const CONSTANT_DISTRIBUTION_MAP: Record<string, string> = {
57
+ CENTER: "center",
58
+ END: "end",
59
+ FILL: "fill",
60
+ SPACE_BETWEEN: "space-between",
61
+ SPACE_EVENLY: "space-evenly",
62
+ START: "start",
63
+ }
64
+
65
+ const VALID_GAP_VALUES = new Set([0, 0.25, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 7])
66
+
67
+ const PADDING_SCALE = new Set([0, 0.25, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 7])
68
+
69
+ const PIXEL_TO_TOKEN: Record<string, number> = {
70
+ "0": 0,
71
+ "0px": 0,
72
+ "2px": 0.25,
73
+ "4px": 0.5,
74
+ "8px": 1,
75
+ "12px": 1.5,
76
+ "16px": 2,
77
+ "24px": 3,
78
+ "32px": 4,
79
+ "40px": 5,
80
+ "48px": 6,
81
+ "56px": 7,
82
+ }
83
+
84
+ const PADDING_SCALE_LIST = [...PADDING_SCALE].join(", ")
85
+
86
+ const PADDING_TODO =
87
+ "could not migrate padding automatically; " +
88
+ `use a Tapestry spacing scale number (${PADDING_SCALE_LIST}) ` +
89
+ "for Flex padding, or use style={{ padding: ... }} for arbitrary CSS values"
90
+
91
+ const FLEX_START_END_MAP: Record<string, string> = {
92
+ "flex-end": "end",
93
+ "flex-start": "start",
94
+ }
95
+
96
+ const FLEX_RESPONSIVE_PASSTHROUGH = new Set([
97
+ "align",
98
+ "alignSelf",
99
+ "basis",
100
+ "columnGap",
101
+ "direction",
102
+ "gap",
103
+ "grow",
104
+ "justify",
105
+ "paddingBlock",
106
+ "paddingInline",
107
+ "rowGap",
108
+ "shrink",
109
+ "wrap",
110
+ ])
111
+
112
+ type InnerPropEntry = {
113
+ newKey: string
114
+ newValue:
115
+ | ReturnType<JSCodeshift["stringLiteral"]>
116
+ | ReturnType<JSCodeshift["numericLiteral"]>
117
+ }
118
+
119
+ type InnerPropPlan = InnerPropEntry | { newEntries: InnerPropEntry[] }
120
+
121
+ type InnerPropResult = InnerPropPlan | { todo: string } | null
122
+
123
+ function planInnerProp(
124
+ propName: string,
125
+ propValue: unknown,
126
+ j: JSCodeshift
127
+ ): InnerPropResult {
128
+ if (typeof propValue !== "object" || propValue === null)
129
+ return { todo: `could not resolve value; review manually` }
130
+ const node = propValue as {
131
+ property?: unknown
132
+ type: string
133
+ value?: unknown
134
+ }
135
+
136
+ if (propName === "axis") {
137
+ let axisValue: string | null = null
138
+ if (node.type === "StringLiteral" && typeof node.value === "string") {
139
+ axisValue = node.value
140
+ } else if (node.type === "MemberExpression") {
141
+ const prop = node.property as { name?: string; type?: string } | undefined
142
+ if (prop?.type === "Identifier" && typeof prop.name === "string") {
143
+ axisValue = CONSTANT_AXIS_MAP[prop.name] ?? null
144
+ }
145
+ }
146
+ const direction = axisValue !== null ? AXIS_DIRECTION_MAP[axisValue] : null
147
+ if (!direction)
148
+ return {
149
+ todo: `could not migrate to direction automatically; specify "horizontal" or "vertical"`,
150
+ }
151
+ return { newKey: "direction", newValue: j.stringLiteral(direction) }
152
+ }
153
+
154
+ if (propName === "alignment") {
155
+ let alignValue: string | null = null
156
+ if (node.type === "StringLiteral" && typeof node.value === "string") {
157
+ alignValue = FLEX_START_END_MAP[node.value] ?? node.value
158
+ } else if (node.type === "MemberExpression") {
159
+ const prop = node.property as { name?: string; type?: string } | undefined
160
+ if (prop?.type === "Identifier" && typeof prop.name === "string") {
161
+ alignValue = CONSTANT_ALIGN_MAP[prop.name] ?? null
162
+ }
163
+ }
164
+ if (alignValue === "fill")
165
+ return {
166
+ todo: '"fill" has no Flex equivalent; use expand on the child element instead',
167
+ }
168
+ if (alignValue === null || !VALID_ALIGN_VALUES.has(alignValue))
169
+ return {
170
+ todo: `could not migrate to align automatically; valid values are: ${[...VALID_ALIGN_VALUES].join(", ")}`,
171
+ }
172
+ return { newKey: "align", newValue: j.stringLiteral(alignValue) }
173
+ }
174
+
175
+ if (propName === "distribution") {
176
+ let distValue: string | null = null
177
+ if (node.type === "StringLiteral" && typeof node.value === "string") {
178
+ distValue = node.value
179
+ } else if (node.type === "MemberExpression") {
180
+ const prop = node.property as { name?: string; type?: string } | undefined
181
+ if (prop?.type === "Identifier" && typeof prop.name === "string") {
182
+ distValue = CONSTANT_DISTRIBUTION_MAP[prop.name] ?? null
183
+ }
184
+ }
185
+ if (distValue === "fill")
186
+ return {
187
+ todo: '"fill" has no Flex equivalent; use expand on child elements instead',
188
+ }
189
+ if (distValue === null || !VALID_JUSTIFY_VALUES.has(distValue))
190
+ return {
191
+ todo: `could not migrate to justify automatically; valid values are: ${[...VALID_JUSTIFY_VALUES].join(", ")}`,
192
+ }
193
+ return { newKey: "justify", newValue: j.stringLiteral(distValue) }
194
+ }
195
+
196
+ if (propName === "spacing") {
197
+ if (
198
+ node.type === "NumericLiteral" &&
199
+ typeof node.value === "number" &&
200
+ VALID_GAP_VALUES.has(node.value)
201
+ ) {
202
+ return { newKey: "gap", newValue: j.numericLiteral(node.value) }
203
+ }
204
+ return {
205
+ todo: `could not migrate to gap automatically; use a Tapestry spacing scale number (${[...VALID_GAP_VALUES].join(", ")})`,
206
+ }
207
+ }
208
+
209
+ if (propName === "justifyContent") {
210
+ let value: string | null = null
211
+ if (node.type === "StringLiteral" && typeof node.value === "string") {
212
+ const normalized = FLEX_START_END_MAP[node.value] ?? node.value
213
+ value = VALID_JUSTIFY_VALUES.has(normalized) ? normalized : null
214
+ }
215
+ if (value !== null)
216
+ return { newKey: "justify", newValue: j.stringLiteral(value) }
217
+ return {
218
+ todo: `could not migrate to justify automatically; valid values are: ${[...VALID_JUSTIFY_VALUES].join(", ")}`,
219
+ }
220
+ }
221
+
222
+ if (propName === "alignItems") {
223
+ let value: string | null = null
224
+ if (node.type === "StringLiteral" && typeof node.value === "string") {
225
+ const normalized = FLEX_START_END_MAP[node.value] ?? node.value
226
+ value = VALID_ALIGN_VALUES.has(normalized) ? normalized : null
227
+ }
228
+ if (value !== null)
229
+ return { newKey: "align", newValue: j.stringLiteral(value) }
230
+ return {
231
+ todo: `could not migrate to align automatically; valid values are: ${[...VALID_ALIGN_VALUES].join(", ")}`,
232
+ }
233
+ }
234
+
235
+ if (propName === "flexDirection") {
236
+ const VALID_DIRECTION_VALUES = new Set([
237
+ "row",
238
+ "column",
239
+ "row-reverse",
240
+ "column-reverse",
241
+ ])
242
+ let value: string | null = null
243
+ if (node.type === "StringLiteral" && typeof node.value === "string") {
244
+ value = VALID_DIRECTION_VALUES.has(node.value) ? node.value : null
245
+ }
246
+ if (value !== null)
247
+ return { newKey: "direction", newValue: j.stringLiteral(value) }
248
+ return {
249
+ todo: `could not migrate to direction automatically; valid values are: row, column, row-reverse, column-reverse`,
250
+ }
251
+ }
252
+
253
+ if (propName === "flexGrow" || propName === "flexShrink") {
254
+ const newKey = propName === "flexGrow" ? "grow" : "shrink"
255
+ if (node.type === "NumericLiteral" && typeof node.value === "number") {
256
+ return { newKey, newValue: j.numericLiteral(node.value) }
257
+ }
258
+ return {
259
+ todo: `could not migrate to ${newKey} automatically; use a numeric value`,
260
+ }
261
+ }
262
+
263
+ if (propName === "flexBasis") {
264
+ if (node.type === "StringLiteral" && typeof node.value === "string") {
265
+ return { newKey: "basis", newValue: j.stringLiteral(node.value) }
266
+ }
267
+ if (node.type === "NumericLiteral" && typeof node.value === "number") {
268
+ return { newKey: "basis", newValue: j.numericLiteral(node.value) }
269
+ }
270
+ return {
271
+ todo: "could not migrate to basis automatically; use a string or numeric value",
272
+ }
273
+ }
274
+
275
+ if (propName === "flexWrap") {
276
+ if (node.type === "StringLiteral" && typeof node.value === "string") {
277
+ return { newKey: "wrap", newValue: j.stringLiteral(node.value) }
278
+ }
279
+ return {
280
+ todo: "could not migrate to wrap automatically; use a string value",
281
+ }
282
+ }
283
+
284
+ if (propName === "padding") {
285
+ if (
286
+ node.type === "NumericLiteral" &&
287
+ typeof node.value === "number" &&
288
+ PADDING_SCALE.has(node.value)
289
+ ) {
290
+ return null
291
+ }
292
+ if (node.type === "StringLiteral" && typeof node.value === "string") {
293
+ const mapped = PIXEL_TO_TOKEN[node.value]
294
+ if (mapped !== undefined)
295
+ return { newKey: "padding", newValue: j.numericLiteral(mapped) }
296
+ }
297
+ return { todo: PADDING_TODO }
298
+ }
299
+
300
+ if (propName === "paddingHorizontal") {
301
+ if (
302
+ node.type === "NumericLiteral" &&
303
+ typeof node.value === "number" &&
304
+ PADDING_SCALE.has(node.value)
305
+ ) {
306
+ return { newKey: "paddingInline", newValue: j.numericLiteral(node.value) }
307
+ }
308
+ if (node.type === "StringLiteral" && typeof node.value === "string") {
309
+ const mapped = PIXEL_TO_TOKEN[node.value]
310
+ if (mapped !== undefined)
311
+ return { newKey: "paddingInline", newValue: j.numericLiteral(mapped) }
312
+ }
313
+ return { todo: PADDING_TODO }
314
+ }
315
+
316
+ if (propName === "paddingVertical") {
317
+ if (
318
+ node.type === "NumericLiteral" &&
319
+ typeof node.value === "number" &&
320
+ PADDING_SCALE.has(node.value)
321
+ ) {
322
+ return { newKey: "paddingBlock", newValue: j.numericLiteral(node.value) }
323
+ }
324
+ if (node.type === "StringLiteral" && typeof node.value === "string") {
325
+ const mapped = PIXEL_TO_TOKEN[node.value]
326
+ if (mapped !== undefined)
327
+ return { newKey: "paddingBlock", newValue: j.numericLiteral(mapped) }
328
+ }
329
+ return { todo: PADDING_TODO }
330
+ }
331
+
332
+ if (propName === "flex") {
333
+ if (node.type === "StringLiteral" && typeof node.value === "string") {
334
+ const parts = (node.value as string).trim().split(/\s+/)
335
+ if (parts.length === 3) {
336
+ const grow = Number(parts[0])
337
+ const shrink = Number(parts[1])
338
+ const basis = parts[2]
339
+ if (Number.isFinite(grow) && Number.isFinite(shrink)) {
340
+ return {
341
+ newEntries: [
342
+ { newKey: "grow", newValue: j.numericLiteral(grow) },
343
+ { newKey: "shrink", newValue: j.numericLiteral(shrink) },
344
+ { newKey: "basis", newValue: j.stringLiteral(basis) },
345
+ ],
346
+ }
347
+ }
348
+ }
349
+ return {
350
+ todo: "could not migrate flex automatically; use grow, shrink, and basis individually",
351
+ }
352
+ }
353
+ if (
354
+ node.type === "NumericLiteral" &&
355
+ typeof node.value === "number" &&
356
+ node.value === 0
357
+ ) {
358
+ return {
359
+ newEntries: [
360
+ { newKey: "grow", newValue: j.numericLiteral(0) },
361
+ { newKey: "shrink", newValue: j.numericLiteral(1) },
362
+ { newKey: "basis", newValue: j.stringLiteral("0") },
363
+ ],
364
+ }
365
+ }
366
+ return {
367
+ todo: "expand is not supported in responsive; use grow, shrink, and basis individually",
368
+ }
369
+ }
370
+
371
+ if (FLEX_RESPONSIVE_PASSTHROUGH.has(propName)) return null
372
+
373
+ return {
374
+ todo: `not a valid Flex responsive prop; remove or replace manually`,
375
+ }
376
+ }
377
+
378
+ const TODO_TEXT =
379
+ "could not migrate mediaQueries to responsive automatically; " +
380
+ "ensure breakpoints are xs/sm/md/lg/xl with static object values"
381
+
382
+ const transform: Transform = attributeTransformFactory({
383
+ condition: hasAttribute("mediaQueries"),
384
+ targetComponent: "StackView",
385
+ targetPackage: "@planningcenter/tapestry-react",
386
+ transform: (element, { j }) => {
387
+ const mediaQueriesAttr = getAttribute({ element, name: "mediaQueries" })
388
+ if (!mediaQueriesAttr) return false
389
+
390
+ const attrValue = mediaQueriesAttr.value
391
+
392
+ if (attrValue?.type !== "JSXExpressionContainer") {
393
+ addCommentToAttribute({ attribute: mediaQueriesAttr, j, text: TODO_TEXT })
394
+ return true
395
+ }
396
+
397
+ if (attrValue.expression.type !== "ObjectExpression") {
398
+ addCommentToAttribute({ attribute: mediaQueriesAttr, j, text: TODO_TEXT })
399
+ return true
400
+ }
401
+
402
+ const outerObj = attrValue.expression
403
+ const mutations: Array<() => void> = []
404
+ const innerTodos: Array<{ key: string; message: string }> = []
405
+
406
+ for (const outerProp of outerObj.properties) {
407
+ if (outerProp.type !== "ObjectProperty") {
408
+ addCommentToAttribute({
409
+ attribute: mediaQueriesAttr,
410
+ j,
411
+ text: TODO_TEXT,
412
+ })
413
+ return true
414
+ }
415
+
416
+ const outerKey = outerProp.key
417
+ if (outerProp.computed || outerKey.type !== "Identifier") {
418
+ addCommentToAttribute({
419
+ attribute: mediaQueriesAttr,
420
+ j,
421
+ text: TODO_TEXT,
422
+ })
423
+ return true
424
+ }
425
+
426
+ if (!VALID_FLEX_BREAKPOINTS.has(outerKey.name)) {
427
+ addCommentToAttribute({
428
+ attribute: mediaQueriesAttr,
429
+ j,
430
+ text: TODO_TEXT,
431
+ })
432
+ return true
433
+ }
434
+
435
+ const outerVal = outerProp.value
436
+ if (outerVal.type !== "ObjectExpression") {
437
+ addCommentToAttribute({
438
+ attribute: mediaQueriesAttr,
439
+ j,
440
+ text: TODO_TEXT,
441
+ })
442
+ return true
443
+ }
444
+
445
+ for (const innerProp of outerVal.properties) {
446
+ if (innerProp.type !== "ObjectProperty") {
447
+ innerTodos.push({
448
+ key: "[spread]",
449
+ message:
450
+ "spread elements inside a breakpoint object cannot be migrated automatically; review manually",
451
+ })
452
+ continue
453
+ }
454
+
455
+ const innerKey = innerProp.key
456
+ if (innerProp.computed || innerKey.type !== "Identifier") {
457
+ innerTodos.push({
458
+ key: "[computed key]",
459
+ message:
460
+ "computed property keys inside a breakpoint object cannot be migrated automatically; review manually",
461
+ })
462
+ continue
463
+ }
464
+
465
+ const result = planInnerProp(innerKey.name, innerProp.value, j)
466
+ if (result === null) continue
467
+ if ("todo" in result) {
468
+ innerTodos.push({ key: innerKey.name, message: result.todo })
469
+ continue
470
+ }
471
+
472
+ if ("newEntries" in result) {
473
+ const entries = result.newEntries
474
+ mutations.push(() => {
475
+ const idx = outerVal.properties.indexOf(innerProp)
476
+ outerVal.properties.splice(
477
+ idx,
478
+ 1,
479
+ ...entries.map(({ newKey, newValue }) =>
480
+ j.objectProperty(j.identifier(newKey), newValue)
481
+ )
482
+ )
483
+ })
484
+ } else {
485
+ mutations.push(() => {
486
+ innerKey.name = result.newKey
487
+ innerProp.value = result.newValue
488
+ })
489
+ }
490
+ }
491
+ }
492
+
493
+ mediaQueriesAttr.name.name = "responsive"
494
+ for (const mutation of mutations) {
495
+ mutation()
496
+ }
497
+
498
+ if (innerTodos.length > 0) {
499
+ const messageToKeys = new Map<string, string[]>()
500
+ for (const { key, message } of innerTodos) {
501
+ const existing = messageToKeys.get(message)
502
+ if (existing) {
503
+ if (!existing.includes(key)) existing.push(key)
504
+ } else {
505
+ messageToKeys.set(message, [key])
506
+ }
507
+ }
508
+ const parts = [...messageToKeys].map(
509
+ ([message, keys]) => `${keys.join(", ")}: ${message}`
510
+ )
511
+ commentOnAttr(
512
+ mediaQueriesAttr,
513
+ "responsive",
514
+ `could not automatically migrate all breakpoint props — ${parts.join("; ")}`,
515
+ j
516
+ )
517
+ }
518
+
519
+ return true
520
+ },
521
+ })
522
+
523
+ export default transform