@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,110 @@
1
+ import { JSXAttribute } from "jscodeshift"
2
+
3
+ import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
4
+ import { getAttribute } from "../../shared/actions/getAttribute"
5
+ import { transformAttributeName } from "../../shared/actions/transformAttributeName"
6
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
7
+ import { orConditions } from "../../shared/conditions/orConditions"
8
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
9
+
10
+ const FLEX_START_END_MAP: Record<string, string> = {
11
+ "flex-end": "end",
12
+ "flex-start": "start",
13
+ }
14
+
15
+ const VALID_JUSTIFY_VALUES = new Set([
16
+ "start",
17
+ "center",
18
+ "end",
19
+ "space-between",
20
+ "space-evenly",
21
+ ])
22
+
23
+ const VALID_ALIGN_VALUES = new Set([
24
+ "start",
25
+ "center",
26
+ "end",
27
+ "stretch",
28
+ "baseline",
29
+ ])
30
+
31
+ function getStringValue(attr: JSXAttribute): string | null {
32
+ const { value } = attr
33
+ if (value?.type === "StringLiteral") return value.value
34
+ if (
35
+ value?.type === "JSXExpressionContainer" &&
36
+ value.expression.type === "StringLiteral"
37
+ )
38
+ return value.expression.value
39
+ return null
40
+ }
41
+
42
+ export default attributeTransformFactory({
43
+ condition: orConditions(
44
+ hasAttribute("alignItems"),
45
+ hasAttribute("flexBasis"),
46
+ hasAttribute("flexDirection"),
47
+ hasAttribute("flexGrow"),
48
+ hasAttribute("flexShrink"),
49
+ hasAttribute("flexWrap"),
50
+ hasAttribute("justifyContent")
51
+ ),
52
+ targetComponent: "StackView",
53
+ targetPackage: "@planningcenter/tapestry-react",
54
+ transform: (element, { j, options }) => {
55
+ let changed = false
56
+
57
+ if (transformAttributeName("flexBasis", "basis", { element, j, options }))
58
+ changed = true
59
+ if (
60
+ transformAttributeName("flexDirection", "direction", {
61
+ element,
62
+ j,
63
+ options,
64
+ })
65
+ )
66
+ changed = true
67
+ if (transformAttributeName("flexGrow", "grow", { element, j, options }))
68
+ changed = true
69
+ if (transformAttributeName("flexShrink", "shrink", { element, j, options }))
70
+ changed = true
71
+ if (transformAttributeName("flexWrap", "wrap", { element, j, options }))
72
+ changed = true
73
+
74
+ const justifyAttr = getAttribute({ element, name: "justifyContent" })
75
+ if (justifyAttr) {
76
+ const raw = getStringValue(justifyAttr)
77
+ const normalized = raw !== null ? (FLEX_START_END_MAP[raw] ?? raw) : null
78
+ if (normalized !== null && VALID_JUSTIFY_VALUES.has(normalized)) {
79
+ justifyAttr.name.name = "justify"
80
+ justifyAttr.value = j.stringLiteral(normalized)
81
+ } else {
82
+ addCommentToAttribute({
83
+ attribute: justifyAttr,
84
+ j,
85
+ text: `could not migrate to justify automatically; valid values are: ${[...VALID_JUSTIFY_VALUES].join(", ")}`,
86
+ })
87
+ }
88
+ changed = true
89
+ }
90
+
91
+ const alignAttr = getAttribute({ element, name: "alignItems" })
92
+ if (alignAttr) {
93
+ const raw = getStringValue(alignAttr)
94
+ const normalized = raw !== null ? (FLEX_START_END_MAP[raw] ?? raw) : null
95
+ if (normalized !== null && VALID_ALIGN_VALUES.has(normalized)) {
96
+ alignAttr.name.name = "align"
97
+ alignAttr.value = j.stringLiteral(normalized)
98
+ } else {
99
+ addCommentToAttribute({
100
+ attribute: alignAttr,
101
+ j,
102
+ text: `could not migrate to align automatically; valid values are: ${[...VALID_ALIGN_VALUES].join(", ")}`,
103
+ })
104
+ }
105
+ changed = true
106
+ }
107
+
108
+ return changed
109
+ },
110
+ })
@@ -0,0 +1,214 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./distributionToJustify"
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("distributionToJustify transform", () => {
18
+ it("renames distribution to justify", () => {
19
+ const input = `
20
+ import { StackView } from "@planningcenter/tapestry-react"
21
+
22
+ export default function Test() {
23
+ return (
24
+ <StackView distribution="start">
25
+ <StackView distribution="center">
26
+ <StackView distribution="end">
27
+ <StackView distribution="space-between">
28
+ <StackView distribution="space-evenly">Test</StackView>
29
+ </StackView>
30
+ </StackView>
31
+ </StackView>
32
+ </StackView>
33
+ )
34
+ }
35
+ `.trim()
36
+
37
+ const result = applyTransform(input)
38
+ expect(result).toContain('justify="start"')
39
+ expect(result).toContain('justify="center"')
40
+ expect(result).toContain('justify="end"')
41
+ expect(result).toContain('justify="space-between"')
42
+ expect(result).toContain('justify="space-evenly"')
43
+ expect(result).not.toContain("distribution")
44
+ })
45
+
46
+ it("resolves StackView.CENTER constant to justify", () => {
47
+ const input = `
48
+ import { StackView } from "@planningcenter/tapestry-react"
49
+
50
+ export default function Test() {
51
+ return <StackView distribution={StackView.CENTER}>Test</StackView>
52
+ }
53
+ `.trim()
54
+
55
+ const result = applyTransform(input)
56
+ expect(result).toContain('justify="center"')
57
+ expect(result).not.toContain("distribution")
58
+ expect(result).not.toContain("StackView.CENTER")
59
+ })
60
+
61
+ it("resolves StackView.START constant to justify", () => {
62
+ const input = `
63
+ import { StackView } from "@planningcenter/tapestry-react"
64
+
65
+ export default function Test() {
66
+ return <StackView distribution={StackView.START}>Test</StackView>
67
+ }
68
+ `.trim()
69
+
70
+ const result = applyTransform(input)
71
+ expect(result).toContain('justify="start"')
72
+ expect(result).not.toContain("StackView.START")
73
+ })
74
+
75
+ it("resolves StackView.END constant to justify", () => {
76
+ const input = `
77
+ import { StackView } from "@planningcenter/tapestry-react"
78
+
79
+ export default function Test() {
80
+ return <StackView distribution={StackView.END}>Test</StackView>
81
+ }
82
+ `.trim()
83
+
84
+ const result = applyTransform(input)
85
+ expect(result).toContain('justify="end"')
86
+ expect(result).not.toContain("StackView.END")
87
+ })
88
+
89
+ it("resolves StackView.SPACE_BETWEEN constant to justify", () => {
90
+ const input = `
91
+ import { StackView } from "@planningcenter/tapestry-react"
92
+
93
+ export default function Test() {
94
+ return <StackView distribution={StackView.SPACE_BETWEEN}>Test</StackView>
95
+ }
96
+ `.trim()
97
+
98
+ const result = applyTransform(input)
99
+ expect(result).toContain('justify="space-between"')
100
+ expect(result).not.toContain("StackView.SPACE_BETWEEN")
101
+ })
102
+
103
+ it("resolves StackView.SPACE_EVENLY constant to justify", () => {
104
+ const input = `
105
+ import { StackView } from "@planningcenter/tapestry-react"
106
+
107
+ export default function Test() {
108
+ return <StackView distribution={StackView.SPACE_EVENLY}>Test</StackView>
109
+ }
110
+ `.trim()
111
+
112
+ const result = applyTransform(input)
113
+ expect(result).toContain('justify="space-evenly"')
114
+ expect(result).not.toContain("StackView.SPACE_EVENLY")
115
+ })
116
+
117
+ it("flags distribution={StackView.FILL} with a TODO and leaves it in place", () => {
118
+ const input = `
119
+ import { StackView } from "@planningcenter/tapestry-react"
120
+
121
+ export default function Test() {
122
+ return <StackView distribution={StackView.FILL}>Test</StackView>
123
+ }
124
+ `.trim()
125
+
126
+ const result = applyTransform(input)
127
+ expect(result).toContain("distribution={StackView.FILL}")
128
+ expect(result).toContain("TODO: tapestry-migration (distribution)")
129
+ expect(result).not.toContain('justify="')
130
+ })
131
+
132
+ it('flags distribution="fill" with a TODO and leaves it in place', () => {
133
+ const input = `
134
+ import { StackView } from "@planningcenter/tapestry-react"
135
+
136
+ export default function Test() {
137
+ return <StackView distribution="fill">Test</StackView>
138
+ }
139
+ `.trim()
140
+
141
+ const result = applyTransform(input)
142
+ expect(result).toContain('distribution="fill"')
143
+ expect(result).toContain("TODO: tapestry-migration (distribution)")
144
+ expect(result).not.toContain('justify="')
145
+ })
146
+
147
+ it("flags an unrecognized string literal with a TODO", () => {
148
+ const input = `
149
+ import { StackView } from "@planningcenter/tapestry-react"
150
+
151
+ export default function Test() {
152
+ return <StackView distribution="space-around">Test</StackView>
153
+ }
154
+ `.trim()
155
+
156
+ const result = applyTransform(input)
157
+ expect(result).toContain('distribution="space-around"')
158
+ expect(result).toContain("TODO: tapestry-migration (distribution)")
159
+ expect(result).not.toContain('justify="')
160
+ })
161
+
162
+ it("flags a dynamic distribution value with a TODO", () => {
163
+ const input = `
164
+ import { StackView } from "@planningcenter/tapestry-react"
165
+
166
+ export default function Test({ distributionProp }) {
167
+ return <StackView distribution={distributionProp}>Test</StackView>
168
+ }
169
+ `.trim()
170
+
171
+ const result = applyTransform(input)
172
+ expect(result).toContain("distribution={distributionProp}")
173
+ expect(result).toContain("TODO: tapestry-migration (distribution)")
174
+ expect(result).not.toContain('justify="')
175
+ })
176
+
177
+ it("respects an aliased StackView import", () => {
178
+ const input = `
179
+ import { StackView as Stack } from "@planningcenter/tapestry-react"
180
+
181
+ export default function Test() {
182
+ return <Stack distribution="center">Test</Stack>
183
+ }
184
+ `.trim()
185
+
186
+ const result = applyTransform(input)
187
+ expect(result).toContain('justify="center"')
188
+ expect(result).not.toContain("distribution")
189
+ })
190
+
191
+ it("returns null when there is no distribution to migrate", () => {
192
+ const input = `
193
+ import { StackView } from "@planningcenter/tapestry-react"
194
+
195
+ export default function Test() {
196
+ return <StackView>Test</StackView>
197
+ }
198
+ `.trim()
199
+
200
+ expect(applyTransform(input)).toBe(null)
201
+ })
202
+
203
+ it("returns null when StackView is not imported from tapestry-react", () => {
204
+ const input = `
205
+ import { StackView } from "some-other-package"
206
+
207
+ export default function Test() {
208
+ return <StackView distribution="center">Test</StackView>
209
+ }
210
+ `.trim()
211
+
212
+ expect(applyTransform(input)).toBe(null)
213
+ })
214
+ })
@@ -0,0 +1,76 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { addComment } from "../../shared/actions/addComment"
4
+ import { getAttribute } from "../../shared/actions/getAttribute"
5
+ import { resolveConstantAttributeValue } from "../../shared/actions/resolveConstantAttributeValue"
6
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
7
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
8
+
9
+ const VALID_JUSTIFY_VALUES = new Set([
10
+ "start",
11
+ "center",
12
+ "end",
13
+ "space-between",
14
+ "space-evenly",
15
+ ])
16
+
17
+ const CONSTANT_DISTRIBUTION_MAP: Record<string, string> = {
18
+ CENTER: "center",
19
+ END: "end",
20
+ FILL: "fill",
21
+ SPACE_BETWEEN: "space-between",
22
+ SPACE_EVENLY: "space-evenly",
23
+ START: "start",
24
+ }
25
+
26
+ const transform: Transform = attributeTransformFactory({
27
+ condition: hasAttribute("distribution"),
28
+ targetComponent: "StackView",
29
+ targetPackage: "@planningcenter/tapestry-react",
30
+ transform: (element, { j, source }) => {
31
+ const distributionAttribute = getAttribute({
32
+ element,
33
+ name: "distribution",
34
+ })
35
+ if (!distributionAttribute) return false
36
+
37
+ const resolved = resolveConstantAttributeValue({
38
+ attribute: distributionAttribute,
39
+ constantMap: CONSTANT_DISTRIBUTION_MAP,
40
+ })
41
+ const distributionValue =
42
+ resolved !== null &&
43
+ (VALID_JUSTIFY_VALUES.has(resolved) || resolved === "fill")
44
+ ? resolved
45
+ : null
46
+
47
+ if (distributionValue === null) {
48
+ addComment({
49
+ element,
50
+ j,
51
+ scope: "distribution",
52
+ source,
53
+ text: "could not migrate distribution to justify automatically; convert this value to start, center, end, space-between, or space-evenly manually",
54
+ })
55
+ return true
56
+ }
57
+
58
+ if (distributionValue === "fill") {
59
+ addComment({
60
+ element,
61
+ j,
62
+ scope: "distribution",
63
+ source,
64
+ text: 'distribution="fill" has no Flex equivalent; remove this prop and use expand on the child elements instead',
65
+ })
66
+ return true
67
+ }
68
+
69
+ distributionAttribute.name.name = "justify"
70
+ distributionAttribute.value = j.stringLiteral(distributionValue)
71
+
72
+ return true
73
+ },
74
+ })
75
+
76
+ export default transform
@@ -0,0 +1,156 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./flexPropToExpand"
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("flexPropToExpand transform", () => {
18
+ it("converts flex={1} to expand", () => {
19
+ const input = `
20
+ import { StackView } from "@planningcenter/tapestry-react"
21
+ export default function Test() {
22
+ return <StackView flex={1}>Test</StackView>
23
+ }
24
+ `.trim()
25
+
26
+ const result = applyTransform(input)
27
+ expect(result).toContain("expand")
28
+ expect(result).not.toContain("flex")
29
+ expect(result).not.toContain("TODO")
30
+ })
31
+
32
+ it('converts flex="1" to expand', () => {
33
+ const input = `
34
+ import { StackView } from "@planningcenter/tapestry-react"
35
+ export default function Test() {
36
+ return <StackView flex="1">Test</StackView>
37
+ }
38
+ `.trim()
39
+
40
+ const result = applyTransform(input)
41
+ expect(result).toContain("expand")
42
+ expect(result).not.toContain("flex")
43
+ expect(result).not.toContain("TODO")
44
+ })
45
+
46
+ it('expands flex="1 1 0" into grow, shrink, and basis', () => {
47
+ const input = `
48
+ import { StackView } from "@planningcenter/tapestry-react"
49
+ export default function Test() {
50
+ return <StackView flex="1 1 0">Test</StackView>
51
+ }
52
+ `.trim()
53
+
54
+ const result = applyTransform(input)
55
+ expect(result).toContain("grow={1}")
56
+ expect(result).toContain("shrink={1}")
57
+ expect(result).toContain('basis="0"')
58
+ expect(result).not.toContain("flex")
59
+ expect(result).not.toContain("TODO")
60
+ })
61
+
62
+ it('expands flex="1 0 0%" into grow, shrink, and basis', () => {
63
+ const input = `
64
+ import { StackView } from "@planningcenter/tapestry-react"
65
+ export default function Test() {
66
+ return <StackView flex="1 0 0%">Test</StackView>
67
+ }
68
+ `.trim()
69
+
70
+ const result = applyTransform(input)
71
+ expect(result).toContain("grow={1}")
72
+ expect(result).toContain("shrink={0}")
73
+ expect(result).toContain('basis="0%"')
74
+ expect(result).not.toContain("flex")
75
+ expect(result).not.toContain("TODO")
76
+ })
77
+
78
+ it('expands flex="0 0 auto" into grow, shrink, and basis', () => {
79
+ const input = `
80
+ import { StackView } from "@planningcenter/tapestry-react"
81
+ export default function Test() {
82
+ return <StackView flex="0 0 auto">Test</StackView>
83
+ }
84
+ `.trim()
85
+
86
+ const result = applyTransform(input)
87
+ expect(result).toContain("grow={0}")
88
+ expect(result).toContain("shrink={0}")
89
+ expect(result).toContain('basis="auto"')
90
+ expect(result).not.toContain("flex")
91
+ expect(result).not.toContain("TODO")
92
+ })
93
+
94
+ it("expands flex={0} into grow={0} shrink={1} basis='0'", () => {
95
+ const input = `
96
+ import { StackView } from "@planningcenter/tapestry-react"
97
+ export default function Test() {
98
+ return <StackView flex={0}>Test</StackView>
99
+ }
100
+ `.trim()
101
+
102
+ const result = applyTransform(input)
103
+ expect(result).toContain("grow={0}")
104
+ expect(result).toContain("shrink={1}")
105
+ expect(result).toContain('basis="0"')
106
+ expect(result).not.toContain("flex=")
107
+ expect(result).not.toContain("TODO")
108
+ })
109
+
110
+ it("adds a TODO for a dynamic flex value", () => {
111
+ const input = `
112
+ import { StackView } from "@planningcenter/tapestry-react"
113
+ export default function Test() {
114
+ return <StackView flex={flexValue}>Test</StackView>
115
+ }
116
+ `.trim()
117
+
118
+ const result = applyTransform(input)
119
+ expect(result).toContain("TODO")
120
+ expect(result).toContain("flex={flexValue}")
121
+ })
122
+
123
+ it('adds a TODO for flex="auto"', () => {
124
+ const input = `
125
+ import { StackView } from "@planningcenter/tapestry-react"
126
+ export default function Test() {
127
+ return <StackView flex="auto">Test</StackView>
128
+ }
129
+ `.trim()
130
+
131
+ const result = applyTransform(input)
132
+ expect(result).toContain("TODO")
133
+ })
134
+
135
+ it("returns null when StackView has no flex prop", () => {
136
+ const input = `
137
+ import { StackView } from "@planningcenter/tapestry-react"
138
+ export default function Test() {
139
+ return <StackView gap={2}>Test</StackView>
140
+ }
141
+ `.trim()
142
+
143
+ expect(applyTransform(input)).toBe(null)
144
+ })
145
+
146
+ it("returns null when StackView is not imported from tapestry-react", () => {
147
+ const input = `
148
+ import { StackView } from "some-other-package"
149
+ export default function Test() {
150
+ return <StackView flex={1}>Test</StackView>
151
+ }
152
+ `.trim()
153
+
154
+ expect(applyTransform(input)).toBe(null)
155
+ })
156
+ })
@@ -0,0 +1,100 @@
1
+ import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
2
+ import { getAttribute } from "../../shared/actions/getAttribute"
3
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
4
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
5
+
6
+ function parseFlexShorthand(
7
+ value: string
8
+ ): { basis: string; grow: number; shrink: number } | null {
9
+ const parts = value.trim().split(/\s+/)
10
+ if (parts.length !== 3) return null
11
+
12
+ const grow = Number(parts[0])
13
+ const shrink = Number(parts[1])
14
+ const basis = parts[2]
15
+
16
+ if (!Number.isFinite(grow) || !Number.isFinite(shrink)) return null
17
+
18
+ return { basis, grow, shrink }
19
+ }
20
+
21
+ const TODO_TEXT =
22
+ "could not migrate flex automatically; use grow, shrink, basis, or expand individually"
23
+
24
+ export default attributeTransformFactory({
25
+ condition: hasAttribute("flex"),
26
+ targetComponent: "StackView",
27
+ targetPackage: "@planningcenter/tapestry-react",
28
+ transform: (element, { j }) => {
29
+ const flexAttr = getAttribute({ element, name: "flex" })
30
+ if (!flexAttr) return false
31
+
32
+ const value = flexAttr.value
33
+ const attrs = element.openingElement.attributes
34
+
35
+ // flex={1} or flex="1" → expand
36
+ const isExpandValue =
37
+ (value?.type === "JSXExpressionContainer" &&
38
+ value.expression.type === "NumericLiteral" &&
39
+ (value.expression as { value: number }).value === 1) ||
40
+ (value?.type === "StringLiteral" &&
41
+ (value as { value: string }).value === "1")
42
+
43
+ if (isExpandValue) {
44
+ const idx = attrs.indexOf(flexAttr)
45
+ attrs.splice(idx, 1, j.jsxAttribute(j.jsxIdentifier("expand")))
46
+ return true
47
+ }
48
+
49
+ // flex={0} → grow={0} shrink={1} basis="0"
50
+ if (
51
+ value?.type === "JSXExpressionContainer" &&
52
+ value.expression.type === "NumericLiteral" &&
53
+ (value.expression as { value: number }).value === 0
54
+ ) {
55
+ const idx = attrs.indexOf(flexAttr)
56
+ attrs.splice(
57
+ idx,
58
+ 1,
59
+ j.jsxAttribute(
60
+ j.jsxIdentifier("grow"),
61
+ j.jsxExpressionContainer(j.numericLiteral(0))
62
+ ),
63
+ j.jsxAttribute(
64
+ j.jsxIdentifier("shrink"),
65
+ j.jsxExpressionContainer(j.numericLiteral(1))
66
+ ),
67
+ j.jsxAttribute(j.jsxIdentifier("basis"), j.stringLiteral("0"))
68
+ )
69
+ return true
70
+ }
71
+
72
+ // flex="N N basis" → grow={N} shrink={N} basis="X"
73
+ if (value?.type === "StringLiteral") {
74
+ const parsed = parseFlexShorthand((value as { value: string }).value)
75
+ if (parsed) {
76
+ const idx = attrs.indexOf(flexAttr)
77
+ attrs.splice(
78
+ idx,
79
+ 1,
80
+ j.jsxAttribute(
81
+ j.jsxIdentifier("grow"),
82
+ j.jsxExpressionContainer(j.numericLiteral(parsed.grow))
83
+ ),
84
+ j.jsxAttribute(
85
+ j.jsxIdentifier("shrink"),
86
+ j.jsxExpressionContainer(j.numericLiteral(parsed.shrink))
87
+ ),
88
+ j.jsxAttribute(
89
+ j.jsxIdentifier("basis"),
90
+ j.stringLiteral(parsed.basis)
91
+ )
92
+ )
93
+ return true
94
+ }
95
+ }
96
+
97
+ addCommentToAttribute({ attribute: flexAttr, j, text: TODO_TEXT })
98
+ return true
99
+ },
100
+ })