@planningcenter/tapestry-migration-cli 3.6.0 → 3.7.0-rc.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 (34) hide show
  1. package/dist/tapestry-react-shim.cjs +195 -11
  2. package/package.json +3 -3
  3. package/src/components/flex/index.test.ts +181 -0
  4. package/src/components/flex/index.ts +16 -0
  5. package/src/components/flex/transforms/alignmentToAlign.test.ts +101 -0
  6. package/src/components/flex/transforms/alignmentToAlign.ts +74 -20
  7. package/src/components/flex/transforms/auditSpreadProps.test.ts +93 -0
  8. package/src/components/flex/transforms/auditSpreadProps.ts +6 -0
  9. package/src/components/flex/transforms/axisToDirection.test.ts +15 -0
  10. package/src/components/flex/transforms/axisToDirection.ts +26 -0
  11. package/src/components/flex/transforms/constants.ts +4 -0
  12. package/src/components/flex/transforms/convertStyleProps.test.ts +147 -0
  13. package/src/components/flex/transforms/convertStyleProps.ts +23 -0
  14. package/src/components/flex/transforms/cssFlexAliasesToProps.test.ts +202 -0
  15. package/src/components/flex/transforms/cssFlexAliasesToProps.ts +106 -0
  16. package/src/components/flex/transforms/dedupeAttributes.test.ts +120 -0
  17. package/src/components/flex/transforms/dedupeAttributes.ts +49 -0
  18. package/src/components/flex/transforms/distributionToJustify.test.ts +59 -0
  19. package/src/components/flex/transforms/distributionToJustify.ts +69 -20
  20. package/src/components/flex/transforms/flexPropToExpand.test.ts +172 -0
  21. package/src/components/flex/transforms/flexPropToExpand.ts +100 -0
  22. package/src/components/flex/transforms/mediaQueriesToResponsive.test.ts +795 -0
  23. package/src/components/flex/transforms/mediaQueriesToResponsive.ts +562 -0
  24. package/src/components/flex/transforms/paddingPropsToFlex.test.ts +252 -0
  25. package/src/components/flex/transforms/paddingPropsToFlex.ts +150 -0
  26. package/src/components/flex/transforms/setDefaultAxis.test.ts +12 -0
  27. package/src/components/flex/transforms/setDefaultAxis.ts +1 -0
  28. package/src/components/flex/transforms/spacingToGap.test.ts +87 -0
  29. package/src/components/flex/transforms/spacingToGap.ts +28 -10
  30. package/src/components/flex/transforms/unsupportedProps.test.ts +141 -0
  31. package/src/components/flex/transforms/unsupportedProps.ts +17 -0
  32. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +41 -0
  33. package/src/components/shared/transformFactories/stylePropTransformFactory.test.ts +47 -0
  34. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +16 -0
@@ -0,0 +1,106 @@
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
+ import { FLEX_START_END_MAP } from "./constants"
10
+
11
+ const VALID_JUSTIFY_VALUES = new Set([
12
+ "start",
13
+ "center",
14
+ "end",
15
+ "space-between",
16
+ "space-evenly",
17
+ ])
18
+
19
+ const VALID_ALIGN_VALUES = new Set([
20
+ "start",
21
+ "center",
22
+ "end",
23
+ "stretch",
24
+ "baseline",
25
+ ])
26
+
27
+ function getStringValue(attr: JSXAttribute): string | null {
28
+ const { value } = attr
29
+ if (value?.type === "StringLiteral") return value.value
30
+ if (
31
+ value?.type === "JSXExpressionContainer" &&
32
+ value.expression.type === "StringLiteral"
33
+ )
34
+ return value.expression.value
35
+ return null
36
+ }
37
+
38
+ export default attributeTransformFactory({
39
+ condition: orConditions(
40
+ hasAttribute("alignItems"),
41
+ hasAttribute("flexBasis"),
42
+ hasAttribute("flexDirection"),
43
+ hasAttribute("flexGrow"),
44
+ hasAttribute("flexShrink"),
45
+ hasAttribute("flexWrap"),
46
+ hasAttribute("justifyContent")
47
+ ),
48
+ targetComponent: "StackView",
49
+ targetPackage: "@planningcenter/tapestry-react",
50
+ transform: (element, { j, options }) => {
51
+ let changed = false
52
+
53
+ if (transformAttributeName("flexBasis", "basis", { element, j, options }))
54
+ changed = true
55
+ if (
56
+ transformAttributeName("flexDirection", "direction", {
57
+ element,
58
+ j,
59
+ options,
60
+ })
61
+ )
62
+ changed = true
63
+ if (transformAttributeName("flexGrow", "grow", { element, j, options }))
64
+ changed = true
65
+ if (transformAttributeName("flexShrink", "shrink", { element, j, options }))
66
+ changed = true
67
+ if (transformAttributeName("flexWrap", "wrap", { element, j, options }))
68
+ changed = true
69
+
70
+ const justifyAttr = getAttribute({ element, name: "justifyContent" })
71
+ if (justifyAttr) {
72
+ const raw = getStringValue(justifyAttr)
73
+ const normalized = raw !== null ? (FLEX_START_END_MAP[raw] ?? raw) : null
74
+ if (normalized !== null && VALID_JUSTIFY_VALUES.has(normalized)) {
75
+ justifyAttr.name.name = "justify"
76
+ justifyAttr.value = j.stringLiteral(normalized)
77
+ } else {
78
+ addCommentToAttribute({
79
+ attribute: justifyAttr,
80
+ j,
81
+ text: `could not migrate to justify automatically; valid values are: ${[...VALID_JUSTIFY_VALUES].join(", ")}`,
82
+ })
83
+ }
84
+ changed = true
85
+ }
86
+
87
+ const alignAttr = getAttribute({ element, name: "alignItems" })
88
+ if (alignAttr) {
89
+ const raw = getStringValue(alignAttr)
90
+ const normalized = raw !== null ? (FLEX_START_END_MAP[raw] ?? raw) : null
91
+ if (normalized !== null && VALID_ALIGN_VALUES.has(normalized)) {
92
+ alignAttr.name.name = "align"
93
+ alignAttr.value = j.stringLiteral(normalized)
94
+ } else {
95
+ addCommentToAttribute({
96
+ attribute: alignAttr,
97
+ j,
98
+ text: `could not migrate to align automatically; valid values are: ${[...VALID_ALIGN_VALUES].join(", ")}`,
99
+ })
100
+ }
101
+ changed = true
102
+ }
103
+
104
+ return changed
105
+ },
106
+ })
@@ -0,0 +1,120 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./dedupeAttributes"
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("dedupeAttributes transform", () => {
18
+ it("keeps the last of two duplicate attributes and drops the earlier one", () => {
19
+ const input = `
20
+ import { StackView } from "@planningcenter/tapestry-react"
21
+
22
+ export default function Test() {
23
+ return <StackView direction="row" direction="column">Test</StackView>
24
+ }
25
+ `.trim()
26
+
27
+ const result = applyTransform(input)
28
+ expect(result).toContain('direction="column"')
29
+ expect(result?.match(/direction=/g)).toHaveLength(1)
30
+ expect(result).toContain("TODO: tapestry-migration (direction)")
31
+ expect(result).toContain("duplicate direction prop")
32
+ })
33
+
34
+ it("keeps the last of three duplicate attributes and drops the earlier ones", () => {
35
+ const input = `
36
+ import { StackView } from "@planningcenter/tapestry-react"
37
+
38
+ export default function Test() {
39
+ return <StackView align="start" align="center" align="end">Test</StackView>
40
+ }
41
+ `.trim()
42
+
43
+ const result = applyTransform(input)
44
+ expect(result).toContain('align="end"')
45
+ expect(result?.match(/align=/g)).toHaveLength(1)
46
+ expect(result).toContain("TODO: tapestry-migration (align)")
47
+ })
48
+
49
+ it("dedupes multiple different duplicated attribute names independently", () => {
50
+ const input = `
51
+ import { StackView } from "@planningcenter/tapestry-react"
52
+
53
+ export default function Test() {
54
+ return <StackView direction="row" direction="column" align="start" align="end">Test</StackView>
55
+ }
56
+ `.trim()
57
+
58
+ const result = applyTransform(input)
59
+ expect(result?.match(/direction=/g)).toHaveLength(1)
60
+ expect(result?.match(/align=/g)).toHaveLength(1)
61
+ expect(result).toContain('direction="column"')
62
+ expect(result).toContain('align="end"')
63
+ })
64
+
65
+ it("preserves other non-duplicate attributes untouched", () => {
66
+ const input = `
67
+ import { StackView } from "@planningcenter/tapestry-react"
68
+
69
+ export default function Test() {
70
+ return <StackView direction="row" direction="column" gap={2}>Test</StackView>
71
+ }
72
+ `.trim()
73
+
74
+ const result = applyTransform(input)
75
+ expect(result).toContain("gap={2}")
76
+ expect(result?.match(/direction=/g)).toHaveLength(1)
77
+ })
78
+
79
+ it("ignores spread attributes", () => {
80
+ const input = `
81
+ import { StackView } from "@planningcenter/tapestry-react"
82
+
83
+ export default function Test() {
84
+ return <StackView {...props} direction="row" direction="column">Test</StackView>
85
+ }
86
+ `.trim()
87
+
88
+ const result = applyTransform(input)
89
+ expect(result).toContain("{...props}")
90
+ expect(result?.match(/direction=/g)).toHaveLength(1)
91
+ })
92
+
93
+ it("returns null when there are no duplicate attributes", () => {
94
+ const input = `
95
+ import { StackView } from "@planningcenter/tapestry-react"
96
+
97
+ export default function Test() {
98
+ return <StackView direction="row" gap={2}>Test</StackView>
99
+ }
100
+ `.trim()
101
+
102
+ expect(applyTransform(input)).toBe(null)
103
+ })
104
+
105
+ it("returns null when StackView is not imported from tapestry-react", () => {
106
+ const input = `
107
+ import { StackView } from "some-other-package"
108
+
109
+ export default function Test() {
110
+ return <StackView direction="row" direction="column">Test</StackView>
111
+ }
112
+ `.trim()
113
+
114
+ expect(applyTransform(input)).toBe(null)
115
+ })
116
+
117
+ it("returns null for an empty file", () => {
118
+ expect(applyTransform("")).toBe(null)
119
+ })
120
+ })
@@ -0,0 +1,49 @@
1
+ import { JSXAttribute } from "jscodeshift"
2
+
3
+ import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
4
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
5
+
6
+ export default attributeTransformFactory({
7
+ targetComponent: "StackView",
8
+ targetPackage: "@planningcenter/tapestry-react",
9
+ transform: (element, { j }) => {
10
+ const attrs = element.openingElement.attributes ?? []
11
+ const byName = new Map<string, JSXAttribute[]>()
12
+
13
+ for (const attr of attrs) {
14
+ if (attr.type !== "JSXAttribute") continue
15
+ if (attr.name.type !== "JSXIdentifier") continue
16
+ const name = attr.name.name
17
+ const group = byName.get(name)
18
+ if (group) {
19
+ group.push(attr)
20
+ } else {
21
+ byName.set(name, [attr])
22
+ }
23
+ }
24
+
25
+ let changed = false
26
+
27
+ for (const [name, group] of byName) {
28
+ if (group.length < 2) continue
29
+
30
+ const kept = group[group.length - 1]
31
+ const dropped = group.slice(0, -1)
32
+
33
+ for (const attr of dropped) {
34
+ const idx = attrs.indexOf(attr)
35
+ attrs.splice(idx, 1)
36
+ }
37
+
38
+ addCommentToAttribute({
39
+ attribute: kept,
40
+ j,
41
+ text: `duplicate ${name} prop after migration — an earlier ${name} value was dropped in favor of this one; verify this is correct`,
42
+ })
43
+
44
+ changed = true
45
+ }
46
+
47
+ return changed
48
+ },
49
+ })
@@ -114,6 +114,65 @@ export default function Test() {
114
114
  expect(result).not.toContain("StackView.SPACE_EVENLY")
115
115
  })
116
116
 
117
+ it("migrates a ternary with a valid string branch and an undefined pass-through branch", () => {
118
+ const input = `
119
+ import { StackView } from "@planningcenter/tapestry-react"
120
+
121
+ export default function Test({ compact }) {
122
+ return <StackView distribution={compact ? 'center' : undefined}>Test</StackView>
123
+ }
124
+ `.trim()
125
+
126
+ const result = applyTransform(input)
127
+ expect(result).toContain('justify={compact ? "center" : undefined}')
128
+ expect(result).not.toContain("distribution")
129
+ })
130
+
131
+ it("migrates a ternary with two valid string branches", () => {
132
+ const input = `
133
+ import { StackView } from "@planningcenter/tapestry-react"
134
+
135
+ export default function Test({ compact }) {
136
+ return <StackView distribution={compact ? 'center' : 'start'}>Test</StackView>
137
+ }
138
+ `.trim()
139
+
140
+ const result = applyTransform(input)
141
+ expect(result).toContain('justify={compact ? "center" : "start"}')
142
+ expect(result).not.toContain("distribution")
143
+ })
144
+
145
+ it("migrates a ternary with a dynamic branch, trusting it at runtime", () => {
146
+ const input = `
147
+ import { StackView } from "@planningcenter/tapestry-react"
148
+
149
+ export default function Test({ compact, dynamicValue }) {
150
+ return <StackView distribution={compact ? 'center' : dynamicValue}>Test</StackView>
151
+ }
152
+ `.trim()
153
+
154
+ const result = applyTransform(input)
155
+ expect(result).toContain('justify={compact ? "center" : dynamicValue}')
156
+ expect(result).not.toContain("distribution")
157
+ expect(result).not.toContain("TODO")
158
+ })
159
+
160
+ it("flags a ternary with a fill branch and leaves it in place", () => {
161
+ const input = `
162
+ import { StackView } from "@planningcenter/tapestry-react"
163
+
164
+ export default function Test({ compact }) {
165
+ return <StackView distribution={compact ? 'center' : 'fill'}>Test</StackView>
166
+ }
167
+ `.trim()
168
+
169
+ const result = applyTransform(input)
170
+ expect(result).toContain("distribution={compact ? 'center' : 'fill'}")
171
+ expect(result).toContain("TODO: tapestry-migration (distribution)")
172
+ expect(result).not.toContain('justify="')
173
+ expect(result).not.toContain("justify={")
174
+ })
175
+
117
176
  it("flags distribution={StackView.FILL} with a TODO and leaves it in place", () => {
118
177
  const input = `
119
178
  import { StackView } from "@planningcenter/tapestry-react"
@@ -1,6 +1,6 @@
1
- import { Transform } from "jscodeshift"
1
+ import { Expression, Transform } from "jscodeshift"
2
2
 
3
- import { addComment } from "../../shared/actions/addComment"
3
+ import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
4
4
  import { getAttribute } from "../../shared/actions/getAttribute"
5
5
  import { resolveConstantAttributeValue } from "../../shared/actions/resolveConstantAttributeValue"
6
6
  import { hasAttribute } from "../../shared/conditions/hasAttribute"
@@ -23,45 +23,94 @@ const CONSTANT_DISTRIBUTION_MAP: Record<string, string> = {
23
23
  START: "start",
24
24
  }
25
25
 
26
+ const TODO_TEXT =
27
+ "could not migrate distribution to justify automatically; convert this value to start, center, end, space-between, or space-evenly manually"
28
+
29
+ function resolveJustifyValue(raw: string): string | null {
30
+ return VALID_JUSTIFY_VALUES.has(raw) ? raw : null
31
+ }
32
+
33
+ type BranchResolution =
34
+ | { kind: "invalid" }
35
+ | { kind: "passthrough" }
36
+ | { kind: "valid"; value: string }
37
+
38
+ function resolveBranch(node: Expression): BranchResolution {
39
+ if (node.type === "StringLiteral") {
40
+ const value = resolveJustifyValue(node.value)
41
+ return value !== null ? { kind: "valid", value } : { kind: "invalid" }
42
+ }
43
+ if (node.type === "MemberExpression" && node.property.type === "Identifier") {
44
+ const mapped = CONSTANT_DISTRIBUTION_MAP[node.property.name]
45
+ if (mapped === undefined) return { kind: "passthrough" }
46
+ const value = resolveJustifyValue(mapped)
47
+ return value !== null ? { kind: "valid", value } : { kind: "invalid" }
48
+ }
49
+ return { kind: "passthrough" }
50
+ }
51
+
26
52
  const transform: Transform = attributeTransformFactory({
27
53
  condition: hasAttribute("distribution"),
28
54
  targetComponent: "StackView",
29
55
  targetPackage: "@planningcenter/tapestry-react",
30
- transform: (element, { j, source }) => {
56
+ transform: (element, { j }) => {
31
57
  const distributionAttribute = getAttribute({
32
58
  element,
33
59
  name: "distribution",
34
60
  })
35
61
  if (!distributionAttribute) return false
36
62
 
63
+ const value = distributionAttribute.value
64
+
65
+ if (
66
+ value?.type === "JSXExpressionContainer" &&
67
+ value.expression.type === "ConditionalExpression"
68
+ ) {
69
+ const conditional = value.expression
70
+ const consequent = resolveBranch(conditional.consequent as Expression)
71
+ const alternate = resolveBranch(conditional.alternate as Expression)
72
+
73
+ if (consequent.kind !== "invalid" && alternate.kind !== "invalid") {
74
+ if (consequent.kind === "valid") {
75
+ conditional.consequent = j.stringLiteral(consequent.value)
76
+ }
77
+ if (alternate.kind === "valid") {
78
+ conditional.alternate = j.stringLiteral(alternate.value)
79
+ }
80
+ distributionAttribute.name.name = "justify"
81
+ return true
82
+ }
83
+
84
+ addCommentToAttribute({
85
+ attribute: distributionAttribute,
86
+ j,
87
+ text: TODO_TEXT,
88
+ })
89
+ return true
90
+ }
91
+
37
92
  const resolved = resolveConstantAttributeValue({
38
93
  attribute: distributionAttribute,
39
94
  constantMap: CONSTANT_DISTRIBUTION_MAP,
40
95
  })
41
- const distributionValue =
42
- resolved !== null &&
43
- (VALID_JUSTIFY_VALUES.has(resolved) || resolved === "fill")
44
- ? resolved
45
- : null
46
96
 
47
- if (distributionValue === null) {
48
- addComment({
49
- element,
97
+ if (resolved === "fill") {
98
+ addCommentToAttribute({
99
+ attribute: distributionAttribute,
50
100
  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",
101
+ text: 'distribution="fill" has no Flex equivalent; remove this prop and use expand on the child elements instead',
54
102
  })
55
103
  return true
56
104
  }
57
105
 
58
- if (distributionValue === "fill") {
59
- addComment({
60
- element,
106
+ const distributionValue =
107
+ resolved !== null ? resolveJustifyValue(resolved) : null
108
+
109
+ if (distributionValue === null) {
110
+ addCommentToAttribute({
111
+ attribute: distributionAttribute,
61
112
  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',
113
+ text: TODO_TEXT,
65
114
  })
66
115
  return true
67
116
  }
@@ -0,0 +1,172 @@
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("expands flex={2} into grow={2} shrink={1} basis='0'", () => {
111
+ const input = `
112
+ import { StackView } from "@planningcenter/tapestry-react"
113
+ export default function Test() {
114
+ return <StackView flex={2}>Test</StackView>
115
+ }
116
+ `.trim()
117
+
118
+ const result = applyTransform(input)
119
+ expect(result).toContain("grow={2}")
120
+ expect(result).toContain("shrink={1}")
121
+ expect(result).toContain('basis="0"')
122
+ expect(result).not.toContain("flex=")
123
+ expect(result).not.toContain("TODO")
124
+ })
125
+
126
+ it("adds a TODO for a dynamic flex value", () => {
127
+ const input = `
128
+ import { StackView } from "@planningcenter/tapestry-react"
129
+ export default function Test() {
130
+ return <StackView flex={flexValue}>Test</StackView>
131
+ }
132
+ `.trim()
133
+
134
+ const result = applyTransform(input)
135
+ expect(result).toContain("TODO")
136
+ expect(result).toContain("flex={flexValue}")
137
+ })
138
+
139
+ it('adds a TODO for flex="auto"', () => {
140
+ const input = `
141
+ import { StackView } from "@planningcenter/tapestry-react"
142
+ export default function Test() {
143
+ return <StackView flex="auto">Test</StackView>
144
+ }
145
+ `.trim()
146
+
147
+ const result = applyTransform(input)
148
+ expect(result).toContain("TODO")
149
+ })
150
+
151
+ it("returns null when StackView has no flex prop", () => {
152
+ const input = `
153
+ import { StackView } from "@planningcenter/tapestry-react"
154
+ export default function Test() {
155
+ return <StackView gap={2}>Test</StackView>
156
+ }
157
+ `.trim()
158
+
159
+ expect(applyTransform(input)).toBe(null)
160
+ })
161
+
162
+ it("returns null when StackView is not imported from tapestry-react", () => {
163
+ const input = `
164
+ import { StackView } from "some-other-package"
165
+ export default function Test() {
166
+ return <StackView flex={1}>Test</StackView>
167
+ }
168
+ `.trim()
169
+
170
+ expect(applyTransform(input)).toBe(null)
171
+ })
172
+ })