@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
@@ -1,10 +1,11 @@
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"
7
7
  import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
8
+ import { FLEX_START_END_MAP } from "./constants"
8
9
 
9
10
  const VALID_ALIGN_VALUES = new Set([
10
11
  "start",
@@ -23,42 +24,95 @@ const CONSTANT_ALIGN_MAP: Record<string, string> = {
23
24
  STRETCH: "stretch",
24
25
  }
25
26
 
27
+ const TODO_TEXT =
28
+ "could not migrate alignment to align automatically; convert this value to start, center, end, stretch, or baseline manually"
29
+
30
+ function normalizeAlignValue(raw: string): string | null {
31
+ const normalized = FLEX_START_END_MAP[raw] ?? raw
32
+ return VALID_ALIGN_VALUES.has(normalized) ? normalized : null
33
+ }
34
+
35
+ type BranchResolution =
36
+ | { kind: "invalid" }
37
+ | { kind: "passthrough" }
38
+ | { kind: "valid"; value: string }
39
+
40
+ function resolveBranch(node: Expression): BranchResolution {
41
+ if (node.type === "StringLiteral") {
42
+ const normalized = normalizeAlignValue(node.value)
43
+ return normalized !== null
44
+ ? { kind: "valid", value: normalized }
45
+ : { kind: "invalid" }
46
+ }
47
+ if (node.type === "MemberExpression" && node.property.type === "Identifier") {
48
+ const mapped = CONSTANT_ALIGN_MAP[node.property.name]
49
+ if (mapped === undefined) return { kind: "passthrough" }
50
+ const normalized = normalizeAlignValue(mapped)
51
+ return normalized !== null
52
+ ? { kind: "valid", value: normalized }
53
+ : { kind: "invalid" }
54
+ }
55
+ return { kind: "passthrough" }
56
+ }
57
+
26
58
  const transform: Transform = attributeTransformFactory({
27
59
  condition: hasAttribute("alignment"),
28
60
  targetComponent: "StackView",
29
61
  targetPackage: "@planningcenter/tapestry-react",
30
- transform: (element, { j, source }) => {
62
+ transform: (element, { j }) => {
31
63
  const alignmentAttribute = getAttribute({ element, name: "alignment" })
32
64
  if (!alignmentAttribute) return false
33
65
 
66
+ const value = alignmentAttribute.value
67
+
68
+ if (
69
+ value?.type === "JSXExpressionContainer" &&
70
+ value.expression.type === "ConditionalExpression"
71
+ ) {
72
+ const conditional = value.expression
73
+ const consequent = resolveBranch(conditional.consequent as Expression)
74
+ const alternate = resolveBranch(conditional.alternate as Expression)
75
+
76
+ if (consequent.kind !== "invalid" && alternate.kind !== "invalid") {
77
+ if (consequent.kind === "valid") {
78
+ conditional.consequent = j.stringLiteral(consequent.value)
79
+ }
80
+ if (alternate.kind === "valid") {
81
+ conditional.alternate = j.stringLiteral(alternate.value)
82
+ }
83
+ alignmentAttribute.name.name = "align"
84
+ return true
85
+ }
86
+
87
+ addCommentToAttribute({
88
+ attribute: alignmentAttribute,
89
+ j,
90
+ text: TODO_TEXT,
91
+ })
92
+ return true
93
+ }
94
+
34
95
  const resolved = resolveConstantAttributeValue({
35
96
  attribute: alignmentAttribute,
36
97
  constantMap: CONSTANT_ALIGN_MAP,
37
98
  })
38
- const alignValue =
39
- resolved !== null &&
40
- (VALID_ALIGN_VALUES.has(resolved) || resolved === "fill")
41
- ? resolved
42
- : null
43
99
 
44
- if (alignValue === null) {
45
- addComment({
46
- element,
100
+ if (resolved === "fill") {
101
+ addCommentToAttribute({
102
+ attribute: alignmentAttribute,
47
103
  j,
48
- scope: "alignment",
49
- source,
50
- text: "could not migrate alignment to align automatically; convert this value to start, center, end, stretch, or baseline manually",
104
+ text: 'alignment="fill" has no Flex equivalent; remove this prop and use expand on the child element instead',
51
105
  })
52
106
  return true
53
107
  }
54
108
 
55
- if (alignValue === "fill") {
56
- addComment({
57
- element,
109
+ const alignValue = resolved !== null ? normalizeAlignValue(resolved) : null
110
+
111
+ if (alignValue === null) {
112
+ addCommentToAttribute({
113
+ attribute: alignmentAttribute,
58
114
  j,
59
- scope: "alignment",
60
- source,
61
- text: 'alignment="fill" has no Flex equivalent; remove this prop and use expand on the child element instead',
115
+ text: TODO_TEXT,
62
116
  })
63
117
  return true
64
118
  }
@@ -0,0 +1,93 @@
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): string | null {
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
+ it("adds a comment to StackView with spread props", () => {
22
+ const input = `
23
+ import { StackView } from "@planningcenter/tapestry-react"
24
+
25
+ export default function Test() {
26
+ return <StackView {...props}>Test</StackView>
27
+ }
28
+ `.trim()
29
+
30
+ const result = applyTransform(input)
31
+ expect(result).toContain(AUDIT_COMMENT)
32
+ expect(result).toContain("{...props}")
33
+ })
34
+
35
+ it("adds a comment for each spread", () => {
36
+ const input = `
37
+ import { StackView } from "@planningcenter/tapestry-react"
38
+
39
+ export default function Test() {
40
+ return <StackView {...baseProps} {...extraProps}>Test</StackView>
41
+ }
42
+ `.trim()
43
+
44
+ const result = applyTransform(input)
45
+ const commentCount = (result?.match(/spreadAttribute/g) || []).length
46
+ expect(commentCount).toBe(2)
47
+ })
48
+
49
+ it("preserves all other attributes", () => {
50
+ const input = `
51
+ import { StackView } from "@planningcenter/tapestry-react"
52
+
53
+ export default function Test() {
54
+ return <StackView axis="horizontal" spacing={2} {...props}>Test</StackView>
55
+ }
56
+ `.trim()
57
+
58
+ const result = applyTransform(input)
59
+ expect(result).toContain('axis="horizontal"')
60
+ expect(result).toContain("spacing={2}")
61
+ expect(result).toContain("{...props}")
62
+ })
63
+
64
+ describe("no changes scenarios", () => {
65
+ it("returns null when no spread props", () => {
66
+ const input = `
67
+ import { StackView } from "@planningcenter/tapestry-react"
68
+
69
+ export default function Test() {
70
+ return <StackView axis="horizontal">Test</StackView>
71
+ }
72
+ `.trim()
73
+
74
+ expect(applyTransform(input)).toBe(null)
75
+ })
76
+
77
+ it("returns null when not imported from tapestry-react", () => {
78
+ const input = `
79
+ import { StackView } from "some-other-library"
80
+
81
+ export default function Test() {
82
+ return <StackView {...props}>Test</StackView>
83
+ }
84
+ `.trim()
85
+
86
+ expect(applyTransform(input)).toBe(null)
87
+ })
88
+
89
+ it("returns null for empty file", () => {
90
+ expect(applyTransform("")).toBe(null)
91
+ })
92
+ })
93
+ })
@@ -0,0 +1,6 @@
1
+ import { commentOnSpreadPropsFactory } from "../../shared/transformFactories/commentOnSpreadPropsFactory"
2
+
3
+ export default commentOnSpreadPropsFactory({
4
+ targetComponent: "StackView",
5
+ targetPackage: "@planningcenter/tapestry-react",
6
+ })
@@ -101,6 +101,21 @@ export default function Test({ orientation }) {
101
101
  expect(result).not.toContain('direction="')
102
102
  })
103
103
 
104
+ it("migrates a ternary axis value to a ternary direction value", () => {
105
+ const input = `
106
+ import { StackView } from "@planningcenter/tapestry-react"
107
+
108
+ export default function Test() {
109
+ return <StackView axis={isNarrow ? "vertical" : "horizontal"}>Test</StackView>
110
+ }
111
+ `.trim()
112
+
113
+ const result = applyTransform(input)
114
+ expect(result).toContain('direction={isNarrow ? "column" : "row"}')
115
+ expect(result).not.toContain("axis")
116
+ expect(result).not.toContain("TODO")
117
+ })
118
+
104
119
  it("maps an aliased StackView import", () => {
105
120
  const input = `
106
121
  import { StackView as Stack } from "@planningcenter/tapestry-react"
@@ -31,6 +31,32 @@ const transform: Transform = attributeTransformFactory({
31
31
  const direction = resolved ? AXIS_DIRECTION_MAP[resolved] : undefined
32
32
 
33
33
  if (!direction) {
34
+ const value = axisAttribute.value
35
+ if (value?.type === "JSXExpressionContainer") {
36
+ const expr = value.expression
37
+ if (
38
+ expr.type === "ConditionalExpression" &&
39
+ expr.consequent.type === "StringLiteral" &&
40
+ expr.alternate.type === "StringLiteral"
41
+ ) {
42
+ const consequentDir =
43
+ AXIS_DIRECTION_MAP[expr.consequent.value as string]
44
+ const alternateDir =
45
+ AXIS_DIRECTION_MAP[expr.alternate.value as string]
46
+ if (consequentDir && alternateDir) {
47
+ axisAttribute.name.name = "direction"
48
+ axisAttribute.value = j.jsxExpressionContainer(
49
+ j.conditionalExpression(
50
+ expr.test,
51
+ j.stringLiteral(consequentDir),
52
+ j.stringLiteral(alternateDir)
53
+ )
54
+ )
55
+ return true
56
+ }
57
+ }
58
+ }
59
+
34
60
  addComment({
35
61
  element,
36
62
  j,
@@ -0,0 +1,4 @@
1
+ export const FLEX_START_END_MAP: Record<string, string> = {
2
+ "flex-end": "end",
3
+ "flex-start": "start",
4
+ }
@@ -0,0 +1,147 @@
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): 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("convertStyleProps transform", () => {
18
+ it("moves a margin style prop to a style object", () => {
19
+ const input = `
20
+ import { StackView } from "@planningcenter/tapestry-react"
21
+
22
+ export default function Test() {
23
+ return <StackView marginTop={2}>Test</StackView>
24
+ }
25
+ `.trim()
26
+
27
+ expect(applyTransform(input)).toMatchInlineSnapshot(`
28
+ "import { StackView } from "@planningcenter/tapestry-react"
29
+
30
+ export default function Test() {
31
+ return (
32
+ <StackView style={{
33
+ marginTop: "16px"
34
+ }}>Test</StackView>
35
+ );
36
+ }"
37
+ `)
38
+ })
39
+
40
+ it("moves a padding style prop to a style object (splitStyles expands shorthand)", () => {
41
+ const input = `
42
+ import { StackView } from "@planningcenter/tapestry-react"
43
+
44
+ export default function Test() {
45
+ return <StackView paddingTop={2}>Test</StackView>
46
+ }
47
+ `.trim()
48
+
49
+ expect(applyTransform(input)).toMatchInlineSnapshot(`
50
+ "import { StackView } from "@planningcenter/tapestry-react"
51
+
52
+ export default function Test() {
53
+ return (
54
+ <StackView style={{
55
+ paddingTop: "16px"
56
+ }}>Test</StackView>
57
+ );
58
+ }"
59
+ `)
60
+ })
61
+
62
+ it("migrates non-excluded style props while leaving excluded Flex props in place", () => {
63
+ const input = `
64
+ import { StackView } from "@planningcenter/tapestry-react"
65
+
66
+ export default function Test() {
67
+ return <StackView gap={2} marginTop={2}>Test</StackView>
68
+ }
69
+ `.trim()
70
+
71
+ expect(applyTransform(input)).toMatchInlineSnapshot(`
72
+ "import { StackView } from "@planningcenter/tapestry-react"
73
+
74
+ export default function Test() {
75
+ return (
76
+ <StackView gap={2} style={{
77
+ marginTop: "16px"
78
+ }}>Test</StackView>
79
+ );
80
+ }"
81
+ `)
82
+ })
83
+
84
+ it("leaves Flex first-class props untouched", () => {
85
+ const input = `
86
+ import { StackView } from "@planningcenter/tapestry-react"
87
+
88
+ export default function Test() {
89
+ return <StackView gap={2} padding={1} direction="row" wrap="wrap" alignSelf="center" columnGap={1} basis="100%" grow={1} shrink={0}>Test</StackView>
90
+ }
91
+ `.trim()
92
+
93
+ expect(applyTransform(input)).toBe(null)
94
+ })
95
+
96
+ it.each(["grow", "shrink", "basis"] as const)(
97
+ "leaves %s untouched and emits no TODO",
98
+ (prop) => {
99
+ const value = prop === "basis" ? '"100%"' : "{1}"
100
+ const input = `
101
+ import { StackView } from "@planningcenter/tapestry-react"
102
+ export default function Test() {
103
+ return <StackView ${prop}=${value}>Test</StackView>
104
+ }
105
+ `.trim()
106
+
107
+ expect(applyTransform(input)).toBe(null)
108
+ }
109
+ )
110
+
111
+ it("leaves axis/alignment/distribution/spacing for the dedicated rename transforms", () => {
112
+ const input = `
113
+ import { StackView } from "@planningcenter/tapestry-react"
114
+
115
+ export default function Test() {
116
+ return <StackView axis="horizontal" alignment="center" distribution="center" spacing={2}>Test</StackView>
117
+ }
118
+ `.trim()
119
+
120
+ const result = applyTransform(input)
121
+ expect(result).toBe(null)
122
+ })
123
+
124
+ it("returns null when there are no style props to migrate", () => {
125
+ const input = `
126
+ import { StackView } from "@planningcenter/tapestry-react"
127
+
128
+ export default function Test() {
129
+ return <StackView>Test</StackView>
130
+ }
131
+ `.trim()
132
+
133
+ expect(applyTransform(input)).toBe(null)
134
+ })
135
+
136
+ it("returns null when StackView is not imported from tapestry-react", () => {
137
+ const input = `
138
+ import { StackView } from "some-other-package"
139
+
140
+ export default function Test() {
141
+ return <StackView marginTop={2}>Test</StackView>
142
+ }
143
+ `.trim()
144
+
145
+ expect(applyTransform(input)).toBe(null)
146
+ })
147
+ })
@@ -0,0 +1,23 @@
1
+ import { stylePropTransformFactory } from "../../shared/transformFactories/stylePropTransformFactory"
2
+
3
+ const FLEX_PROP_NAMES = [
4
+ "alignItems",
5
+ "alignSelf",
6
+ "basis",
7
+ "columnGap",
8
+ "direction",
9
+ "flex",
10
+ "gap",
11
+ "grow",
12
+ "justifyContent",
13
+ "padding",
14
+ "shrink",
15
+ "wrap",
16
+ ]
17
+
18
+ export default stylePropTransformFactory({
19
+ stylesToExclude: FLEX_PROP_NAMES,
20
+ stylesToRemove: [],
21
+ targetComponent: "StackView",
22
+ targetPackage: "@planningcenter/tapestry-react",
23
+ })
@@ -0,0 +1,202 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./cssFlexAliasesToProps"
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("cssFlexAliasesToProps transform", () => {
18
+ it("renames flexGrow to grow", () => {
19
+ const input = `
20
+ import { StackView } from "@planningcenter/tapestry-react"
21
+ export default function Test() {
22
+ return <StackView flexGrow={1}>Test</StackView>
23
+ }
24
+ `.trim()
25
+
26
+ const result = applyTransform(input)
27
+ expect(result).toContain("grow={1}")
28
+ expect(result).not.toContain("flexGrow")
29
+ })
30
+
31
+ it("renames flexShrink to shrink", () => {
32
+ const input = `
33
+ import { StackView } from "@planningcenter/tapestry-react"
34
+ export default function Test() {
35
+ return <StackView flexShrink={0}>Test</StackView>
36
+ }
37
+ `.trim()
38
+
39
+ const result = applyTransform(input)
40
+ expect(result).toContain("shrink={0}")
41
+ expect(result).not.toContain("flexShrink")
42
+ })
43
+
44
+ it("renames flexBasis to basis", () => {
45
+ const input = `
46
+ import { StackView } from "@planningcenter/tapestry-react"
47
+ export default function Test() {
48
+ return <StackView flexBasis="0%">Test</StackView>
49
+ }
50
+ `.trim()
51
+
52
+ const result = applyTransform(input)
53
+ expect(result).toContain('basis="0%"')
54
+ expect(result).not.toContain("flexBasis")
55
+ })
56
+
57
+ it("handles all three together", () => {
58
+ const input = `
59
+ import { StackView } from "@planningcenter/tapestry-react"
60
+ export default function Test() {
61
+ return <StackView flexGrow={1} flexShrink={0} flexBasis="auto">Test</StackView>
62
+ }
63
+ `.trim()
64
+
65
+ const result = applyTransform(input)
66
+ expect(result).toContain("grow={1}")
67
+ expect(result).toContain("shrink={0}")
68
+ expect(result).toContain('basis="auto"')
69
+ expect(result).not.toContain("flexGrow")
70
+ expect(result).not.toContain("flexShrink")
71
+ expect(result).not.toContain("flexBasis")
72
+ })
73
+
74
+ it("renames flexDirection to direction", () => {
75
+ const input = `
76
+ import { StackView } from "@planningcenter/tapestry-react"
77
+ export default function Test() {
78
+ return <StackView flexDirection="row">Test</StackView>
79
+ }
80
+ `.trim()
81
+
82
+ const result = applyTransform(input)
83
+ expect(result).toContain('direction="row"')
84
+ expect(result).not.toContain("flexDirection")
85
+ expect(result).not.toContain("TODO")
86
+ })
87
+
88
+ it("renames flexWrap to wrap", () => {
89
+ const input = `
90
+ import { StackView } from "@planningcenter/tapestry-react"
91
+ export default function Test() {
92
+ return <StackView flexWrap="wrap">Test</StackView>
93
+ }
94
+ `.trim()
95
+
96
+ const result = applyTransform(input)
97
+ expect(result).toContain('wrap="wrap"')
98
+ expect(result).not.toContain("flexWrap")
99
+ expect(result).not.toContain("TODO")
100
+ })
101
+
102
+ it("migrates justifyContent to justify with value normalization", () => {
103
+ const input = `
104
+ import { StackView } from "@planningcenter/tapestry-react"
105
+ export default function Test() {
106
+ return <StackView justifyContent="flex-start">Test</StackView>
107
+ }
108
+ `.trim()
109
+
110
+ const result = applyTransform(input)
111
+ expect(result).toContain('justify="start"')
112
+ expect(result).not.toContain("justifyContent")
113
+ expect(result).not.toContain("TODO")
114
+ })
115
+
116
+ it("migrates justifyContent center without normalization", () => {
117
+ const input = `
118
+ import { StackView } from "@planningcenter/tapestry-react"
119
+ export default function Test() {
120
+ return <StackView justifyContent="center">Test</StackView>
121
+ }
122
+ `.trim()
123
+
124
+ const result = applyTransform(input)
125
+ expect(result).toContain('justify="center"')
126
+ expect(result).not.toContain("TODO")
127
+ })
128
+
129
+ it("adds a TODO for an unmappable justifyContent value", () => {
130
+ const input = `
131
+ import { StackView } from "@planningcenter/tapestry-react"
132
+ export default function Test() {
133
+ return <StackView justifyContent="stretch">Test</StackView>
134
+ }
135
+ `.trim()
136
+
137
+ const result = applyTransform(input)
138
+ expect(result).toContain("TODO")
139
+ expect(result).toContain("justifyContent")
140
+ })
141
+
142
+ it("adds a TODO for a dynamic justifyContent value", () => {
143
+ const input = `
144
+ import { StackView } from "@planningcenter/tapestry-react"
145
+ export default function Test() {
146
+ return <StackView justifyContent={myVar}>Test</StackView>
147
+ }
148
+ `.trim()
149
+
150
+ const result = applyTransform(input)
151
+ expect(result).toContain("TODO")
152
+ })
153
+
154
+ it("migrates alignItems to align with value normalization", () => {
155
+ const input = `
156
+ import { StackView } from "@planningcenter/tapestry-react"
157
+ export default function Test() {
158
+ return <StackView alignItems="flex-end">Test</StackView>
159
+ }
160
+ `.trim()
161
+
162
+ const result = applyTransform(input)
163
+ expect(result).toContain('align="end"')
164
+ expect(result).not.toContain("alignItems")
165
+ expect(result).not.toContain("TODO")
166
+ })
167
+
168
+ it("adds a TODO for an unmappable alignItems value", () => {
169
+ const input = `
170
+ import { StackView } from "@planningcenter/tapestry-react"
171
+ export default function Test() {
172
+ return <StackView alignItems="space-between">Test</StackView>
173
+ }
174
+ `.trim()
175
+
176
+ const result = applyTransform(input)
177
+ expect(result).toContain("TODO")
178
+ expect(result).toContain("alignItems")
179
+ })
180
+
181
+ it("returns null when StackView has none of the CSS alias props", () => {
182
+ const input = `
183
+ import { StackView } from "@planningcenter/tapestry-react"
184
+ export default function Test() {
185
+ return <StackView gap={2}>Test</StackView>
186
+ }
187
+ `.trim()
188
+
189
+ expect(applyTransform(input)).toBe(null)
190
+ })
191
+
192
+ it("returns null when StackView is not imported from tapestry-react", () => {
193
+ const input = `
194
+ import { StackView } from "some-other-package"
195
+ export default function Test() {
196
+ return <StackView flexGrow={1}>Test</StackView>
197
+ }
198
+ `.trim()
199
+
200
+ expect(applyTransform(input)).toBe(null)
201
+ })
202
+ })