@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,252 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./paddingPropsToFlex"
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("paddingPropsToFlex transform", () => {
18
+ it("leaves a valid scale-number padding unchanged", () => {
19
+ const input = `
20
+ import { StackView } from "@planningcenter/tapestry-react"
21
+ export default function Test() {
22
+ return <StackView padding={2}>Test</StackView>
23
+ }
24
+ `.trim()
25
+
26
+ expect(applyTransform(input)).toBe(null)
27
+ })
28
+
29
+ it('converts padding="0" to padding={0}', () => {
30
+ const input = `
31
+ import { StackView } from "@planningcenter/tapestry-react"
32
+ export default function Test() {
33
+ return <StackView padding="0">Test</StackView>
34
+ }
35
+ `.trim()
36
+
37
+ const result = applyTransform(input)
38
+ expect(result).toContain("padding={0}")
39
+ expect(result).not.toContain('padding="0"')
40
+ expect(result).not.toContain("TODO")
41
+ })
42
+
43
+ it("converts a pixel string to the corresponding spacing token", () => {
44
+ const input = `
45
+ import { StackView } from "@planningcenter/tapestry-react"
46
+ export default function Test() {
47
+ return <StackView padding="8px">Test</StackView>
48
+ }
49
+ `.trim()
50
+
51
+ const result = applyTransform(input)
52
+ expect(result).toContain("padding={1}")
53
+ expect(result).not.toContain('padding="8px"')
54
+ expect(result).not.toContain("TODO")
55
+ })
56
+
57
+ it("converts all pixel values in the scale", () => {
58
+ const cases: Array<[string, number]> = [
59
+ ["0px", 0],
60
+ ["2px", 0.25],
61
+ ["4px", 0.5],
62
+ ["8px", 1],
63
+ ["12px", 1.5],
64
+ ["16px", 2],
65
+ ["24px", 3],
66
+ ["32px", 4],
67
+ ["40px", 5],
68
+ ["48px", 6],
69
+ ["56px", 7],
70
+ ]
71
+ for (const [px, token] of cases) {
72
+ const input = `
73
+ import { StackView } from "@planningcenter/tapestry-react"
74
+ export default function Test() {
75
+ return <StackView padding="${px}">Test</StackView>
76
+ }
77
+ `.trim()
78
+ const result = applyTransform(input)
79
+ expect(result, `${px} should map to ${token}`).toContain(
80
+ `padding={${token}}`
81
+ )
82
+ }
83
+ })
84
+
85
+ it("adds a TODO on the padding attribute for a value that cannot be mapped", () => {
86
+ const input = `
87
+ import { StackView } from "@planningcenter/tapestry-react"
88
+ export default function Test() {
89
+ return <StackView padding="10px">Test</StackView>
90
+ }
91
+ `.trim()
92
+
93
+ const result = applyTransform(input)
94
+ expect(result).toMatch(/\/\*.*TODO.*\*\/\s*padding="10px"/)
95
+ })
96
+
97
+ it("renames paddingHorizontal to paddingInline for a valid scale number", () => {
98
+ const input = `
99
+ import { StackView } from "@planningcenter/tapestry-react"
100
+ export default function Test() {
101
+ return <StackView paddingHorizontal={2}>Test</StackView>
102
+ }
103
+ `.trim()
104
+
105
+ const result = applyTransform(input)
106
+ expect(result).toContain("paddingInline={2}")
107
+ expect(result).not.toContain("paddingHorizontal")
108
+ expect(result).not.toContain("TODO")
109
+ })
110
+
111
+ it("renames paddingHorizontal and converts a pixel string", () => {
112
+ const input = `
113
+ import { StackView } from "@planningcenter/tapestry-react"
114
+ export default function Test() {
115
+ return <StackView paddingHorizontal="16px">Test</StackView>
116
+ }
117
+ `.trim()
118
+
119
+ const result = applyTransform(input)
120
+ expect(result).toContain("paddingInline={2}")
121
+ expect(result).not.toContain("paddingHorizontal")
122
+ expect(result).not.toContain("TODO")
123
+ })
124
+
125
+ it("renames paddingHorizontal to paddingInline and adds a TODO for an unmappable value", () => {
126
+ const input = `
127
+ import { StackView } from "@planningcenter/tapestry-react"
128
+ export default function Test() {
129
+ return <StackView paddingHorizontal="1rem">Test</StackView>
130
+ }
131
+ `.trim()
132
+
133
+ const result = applyTransform(input)
134
+ expect(result).toContain("paddingInline")
135
+ expect(result).not.toContain("paddingHorizontal")
136
+ expect(result).toMatch(/TODO/)
137
+ })
138
+
139
+ it("renames paddingHorizontal to paddingInline without a TODO when both ternary branches are valid scale numbers", () => {
140
+ const input = `
141
+ import { StackView } from "@planningcenter/tapestry-react"
142
+ export default function Test() {
143
+ return <StackView paddingHorizontal={isNarrow ? 2 : 6}>Test</StackView>
144
+ }
145
+ `.trim()
146
+
147
+ const result = applyTransform(input)
148
+ expect(result).toContain("paddingInline")
149
+ expect(result).not.toContain("paddingHorizontal")
150
+ expect(result).not.toMatch(/TODO/)
151
+ })
152
+
153
+ it("renames paddingHorizontal to paddingInline without a TODO when one ternary branch is null", () => {
154
+ const input = `
155
+ import { StackView } from "@planningcenter/tapestry-react"
156
+ export default function Test() {
157
+ return <StackView paddingHorizontal={isRoom ? null : 2}>Test</StackView>
158
+ }
159
+ `.trim()
160
+
161
+ const result = applyTransform(input)
162
+ expect(result).toContain("paddingInline")
163
+ expect(result).not.toContain("paddingHorizontal")
164
+ expect(result).not.toMatch(/TODO/)
165
+ })
166
+
167
+ it("renames paddingVertical to paddingBlock for a valid scale number", () => {
168
+ const input = `
169
+ import { StackView } from "@planningcenter/tapestry-react"
170
+ export default function Test() {
171
+ return <StackView paddingVertical={1}>Test</StackView>
172
+ }
173
+ `.trim()
174
+
175
+ const result = applyTransform(input)
176
+ expect(result).toContain("paddingBlock={1}")
177
+ expect(result).not.toContain("paddingVertical")
178
+ expect(result).not.toContain("TODO")
179
+ })
180
+
181
+ it("renames paddingVertical and converts a pixel string", () => {
182
+ const input = `
183
+ import { StackView } from "@planningcenter/tapestry-react"
184
+ export default function Test() {
185
+ return <StackView paddingVertical="8px">Test</StackView>
186
+ }
187
+ `.trim()
188
+
189
+ const result = applyTransform(input)
190
+ expect(result).toContain("paddingBlock={1}")
191
+ expect(result).not.toContain("paddingVertical")
192
+ expect(result).not.toContain("TODO")
193
+ })
194
+
195
+ it("renames paddingVertical to paddingBlock without a TODO when both ternary branches are valid scale numbers", () => {
196
+ const input = `
197
+ import { StackView } from "@planningcenter/tapestry-react"
198
+ export default function Test() {
199
+ return <StackView paddingVertical={isNarrow ? 1 : 3}>Test</StackView>
200
+ }
201
+ `.trim()
202
+
203
+ const result = applyTransform(input)
204
+ expect(result).toContain("paddingBlock")
205
+ expect(result).not.toContain("paddingVertical")
206
+ expect(result).not.toMatch(/TODO/)
207
+ })
208
+
209
+ it("leaves padding with a valid-scale ternary unchanged", () => {
210
+ const input = `
211
+ import { StackView } from "@planningcenter/tapestry-react"
212
+ export default function Test() {
213
+ return <StackView padding={isNarrow ? 2 : 6}>Test</StackView>
214
+ }
215
+ `.trim()
216
+
217
+ expect(applyTransform(input)).toBe(null)
218
+ })
219
+
220
+ it("leaves padding with a null ternary branch unchanged", () => {
221
+ const input = `
222
+ import { StackView } from "@planningcenter/tapestry-react"
223
+ export default function Test() {
224
+ return <StackView padding={isRoom ? null : 2}>Test</StackView>
225
+ }
226
+ `.trim()
227
+
228
+ expect(applyTransform(input)).toBe(null)
229
+ })
230
+
231
+ it("returns null when StackView has no padding props", () => {
232
+ const input = `
233
+ import { StackView } from "@planningcenter/tapestry-react"
234
+ export default function Test() {
235
+ return <StackView axis="horizontal">Test</StackView>
236
+ }
237
+ `.trim()
238
+
239
+ expect(applyTransform(input)).toBe(null)
240
+ })
241
+
242
+ it("returns null when StackView is not imported from tapestry-react", () => {
243
+ const input = `
244
+ import { StackView } from "some-other-package"
245
+ export default function Test() {
246
+ return <StackView padding="8px">Test</StackView>
247
+ }
248
+ `.trim()
249
+
250
+ expect(applyTransform(input)).toBe(null)
251
+ })
252
+ })
@@ -0,0 +1,150 @@
1
+ import { JSXAttribute } from "jscodeshift"
2
+
3
+ import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
4
+ import { getAttribute } from "../../shared/actions/getAttribute"
5
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
6
+ import { orConditions } from "../../shared/conditions/orConditions"
7
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
8
+
9
+ const PADDING_SCALE = new Set([0, 0.25, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 7])
10
+
11
+ const PIXEL_TO_TOKEN: Record<string, number> = {
12
+ "0": 0,
13
+ "0px": 0,
14
+ "2px": 0.25,
15
+ "4px": 0.5,
16
+ "8px": 1,
17
+ "12px": 1.5,
18
+ "16px": 2,
19
+ "24px": 3,
20
+ "32px": 4,
21
+ "40px": 5,
22
+ "48px": 6,
23
+ "56px": 7,
24
+ }
25
+
26
+ function isScaleNumber(node: unknown): boolean {
27
+ return (
28
+ typeof node === "object" &&
29
+ node !== null &&
30
+ (node as { type: string }).type === "NumericLiteral" &&
31
+ PADDING_SCALE.has((node as { value: number }).value)
32
+ )
33
+ }
34
+
35
+ function isNullLiteral(node: unknown): boolean {
36
+ return (
37
+ typeof node === "object" &&
38
+ node !== null &&
39
+ (node as { type: string }).type === "NullLiteral"
40
+ )
41
+ }
42
+
43
+ function resolveToSpacingToken(attr: JSXAttribute): number | null {
44
+ const value = attr.value
45
+ if (!value) return null
46
+ if (value.type === "JSXExpressionContainer") {
47
+ const expr = value.expression
48
+ if (
49
+ expr.type === "NumericLiteral" &&
50
+ PADDING_SCALE.has(expr.value as number)
51
+ ) {
52
+ return expr.value as number
53
+ }
54
+ }
55
+ if (value.type === "StringLiteral") {
56
+ const mapped = PIXEL_TO_TOKEN[value.value as string]
57
+ if (mapped !== undefined) return mapped
58
+ }
59
+ return null
60
+ }
61
+
62
+ function isAlreadyValidScaleValue(attr: JSXAttribute): boolean {
63
+ const value = attr.value
64
+ if (value?.type !== "JSXExpressionContainer") return false
65
+ const expr = value.expression
66
+ if (isScaleNumber(expr)) return true
67
+ if (expr.type === "ConditionalExpression") {
68
+ const validBranch = (n: unknown) => isScaleNumber(n) || isNullLiteral(n)
69
+ return validBranch(expr.consequent) && validBranch(expr.alternate)
70
+ }
71
+ return false
72
+ }
73
+
74
+ const PADDING_SCALE_LIST = [...PADDING_SCALE].join(", ")
75
+
76
+ const TODO_TEXT =
77
+ "could not migrate padding automatically; " +
78
+ `use a Tapestry spacing scale number (${PADDING_SCALE_LIST}) ` +
79
+ "for Flex padding, or use style={{ padding: ... }} for arbitrary CSS values"
80
+
81
+ export default attributeTransformFactory({
82
+ condition: orConditions(
83
+ hasAttribute("padding"),
84
+ hasAttribute("paddingHorizontal"),
85
+ hasAttribute("paddingVertical")
86
+ ),
87
+ targetComponent: "StackView",
88
+ targetPackage: "@planningcenter/tapestry-react",
89
+ transform: (element, { j }) => {
90
+ let changed = false
91
+
92
+ const paddingAttr = getAttribute({ element, name: "padding" })
93
+ if (paddingAttr) {
94
+ const token = resolveToSpacingToken(paddingAttr)
95
+ if (token !== null) {
96
+ const alreadyCorrect =
97
+ paddingAttr.value?.type === "JSXExpressionContainer" &&
98
+ paddingAttr.value.expression.type === "NumericLiteral" &&
99
+ (paddingAttr.value.expression as { value: number }).value === token
100
+ if (!alreadyCorrect) {
101
+ paddingAttr.value = j.jsxExpressionContainer(j.numericLiteral(token))
102
+ changed = true
103
+ }
104
+ } else if (!isAlreadyValidScaleValue(paddingAttr)) {
105
+ addCommentToAttribute({ attribute: paddingAttr, j, text: TODO_TEXT })
106
+ changed = true
107
+ }
108
+ }
109
+
110
+ const horizontalAttr = getAttribute({ element, name: "paddingHorizontal" })
111
+ if (horizontalAttr) {
112
+ horizontalAttr.name.name = "paddingInline"
113
+ const token = resolveToSpacingToken(horizontalAttr)
114
+ if (token !== null) {
115
+ const alreadyCorrect =
116
+ horizontalAttr.value?.type === "JSXExpressionContainer" &&
117
+ horizontalAttr.value.expression.type === "NumericLiteral" &&
118
+ (horizontalAttr.value.expression as { value: number }).value === token
119
+ if (!alreadyCorrect) {
120
+ horizontalAttr.value = j.jsxExpressionContainer(
121
+ j.numericLiteral(token)
122
+ )
123
+ }
124
+ } else if (!isAlreadyValidScaleValue(horizontalAttr)) {
125
+ addCommentToAttribute({ attribute: horizontalAttr, j, text: TODO_TEXT })
126
+ }
127
+ changed = true
128
+ }
129
+
130
+ const verticalAttr = getAttribute({ element, name: "paddingVertical" })
131
+ if (verticalAttr) {
132
+ verticalAttr.name.name = "paddingBlock"
133
+ const token = resolveToSpacingToken(verticalAttr)
134
+ if (token !== null) {
135
+ const alreadyCorrect =
136
+ verticalAttr.value?.type === "JSXExpressionContainer" &&
137
+ verticalAttr.value.expression.type === "NumericLiteral" &&
138
+ (verticalAttr.value.expression as { value: number }).value === token
139
+ if (!alreadyCorrect) {
140
+ verticalAttr.value = j.jsxExpressionContainer(j.numericLiteral(token))
141
+ }
142
+ } else if (!isAlreadyValidScaleValue(verticalAttr)) {
143
+ addCommentToAttribute({ attribute: verticalAttr, j, text: TODO_TEXT })
144
+ }
145
+ changed = true
146
+ }
147
+
148
+ return changed
149
+ },
150
+ })
@@ -71,6 +71,18 @@ export default function Test() {
71
71
  expect(applyTransform(input)).toBe(null)
72
72
  })
73
73
 
74
+ it("does not inject axis when direction is already present", () => {
75
+ const input = `
76
+ import { StackView } from "@planningcenter/tapestry-react"
77
+
78
+ export default function Test() {
79
+ return <StackView direction="row">Test</StackView>
80
+ }
81
+ `.trim()
82
+
83
+ expect(applyTransform(input)).toBe(null)
84
+ })
85
+
74
86
  it("skips elements with spread props", () => {
75
87
  const input = `
76
88
  import { StackView } from "@planningcenter/tapestry-react"
@@ -10,6 +10,7 @@ import { attributeTransformFactory } from "../../shared/transformFactories/attri
10
10
  const transform: Transform = attributeTransformFactory({
11
11
  condition: andConditions(
12
12
  notCondition(hasAttribute("axis")),
13
+ notCondition(hasAttribute("direction")),
13
14
  notCondition(hasSpreadProps)
14
15
  ),
15
16
  targetComponent: "StackView",
@@ -136,6 +136,93 @@ export default function Test({ spacingProp }) {
136
136
  expect(result).not.toContain("gap=")
137
137
  })
138
138
 
139
+ it("renames spacing to gap for a ternary with two in-scale numeric branches", () => {
140
+ const input = `
141
+ import { StackView } from "@planningcenter/tapestry-react"
142
+
143
+ export default function Test({ compact }) {
144
+ return <StackView spacing={compact ? 1.5 : 2}>Test</StackView>
145
+ }
146
+ `.trim()
147
+
148
+ const result = applyTransform(input)
149
+ expect(result).toContain("gap={compact ? 1.5 : 2}")
150
+ expect(result).not.toContain("spacing")
151
+ })
152
+
153
+ it("renames spacing to gap for a ternary with an undefined pass-through branch", () => {
154
+ const input = `
155
+ import { StackView } from "@planningcenter/tapestry-react"
156
+
157
+ export default function Test({ compact }) {
158
+ return <StackView spacing={compact ? 2 : undefined}>Test</StackView>
159
+ }
160
+ `.trim()
161
+
162
+ const result = applyTransform(input)
163
+ expect(result).toContain("gap={compact ? 2 : undefined}")
164
+ expect(result).not.toContain("spacing")
165
+ })
166
+
167
+ it("renames spacing to gap for a ternary with a null pass-through branch", () => {
168
+ const input = `
169
+ import { StackView } from "@planningcenter/tapestry-react"
170
+
171
+ export default function Test({ compact }) {
172
+ return <StackView spacing={compact ? 2 : null}>Test</StackView>
173
+ }
174
+ `.trim()
175
+
176
+ const result = applyTransform(input)
177
+ expect(result).toContain("gap={compact ? 2 : null}")
178
+ expect(result).not.toContain("spacing")
179
+ })
180
+
181
+ it("flags a ternary with one off-scale branch with a TODO", () => {
182
+ const input = `
183
+ import { StackView } from "@planningcenter/tapestry-react"
184
+
185
+ export default function Test({ compact }) {
186
+ return <StackView spacing={compact ? 1.5 : 10}>Test</StackView>
187
+ }
188
+ `.trim()
189
+
190
+ const result = applyTransform(input)
191
+ expect(result).toContain("spacing={compact ? 1.5 : 10}")
192
+ expect(result).toContain("TODO: tapestry-migration (spacing)")
193
+ expect(result).not.toContain("gap=")
194
+ })
195
+
196
+ it("flags a ternary with a ReactNode branch with a TODO", () => {
197
+ const input = `
198
+ import { StackView } from "@planningcenter/tapestry-react"
199
+
200
+ export default function Test({ compact }) {
201
+ return <StackView spacing={compact ? 1.5 : <Divider />}>Test</StackView>
202
+ }
203
+ `.trim()
204
+
205
+ const result = applyTransform(input)
206
+ expect(result).toContain("spacing={compact ? 1.5 : <Divider />}")
207
+ expect(result).toContain("TODO: tapestry-migration (spacing)")
208
+ expect(result).not.toContain("gap=")
209
+ })
210
+
211
+ it("renames spacing to gap for a ternary with a dynamic branch, trusting it at runtime", () => {
212
+ const input = `
213
+ import { StackView } from "@planningcenter/tapestry-react"
214
+
215
+ export default function Test({ compact, dynamicValue }) {
216
+ return <StackView spacing={compact ? 1.5 : dynamicValue}>Test</StackView>
217
+ }
218
+ `.trim()
219
+
220
+ const result = applyTransform(input)
221
+ expect(result).toContain("gap={compact ? 1.5 : dynamicValue}")
222
+ expect(result).not.toContain("spacing")
223
+ expect(result).not.toContain("TODO")
224
+ })
225
+
139
226
  it("respects an aliased StackView import", () => {
140
227
  const input = `
141
228
  import { StackView as Stack } from "@planningcenter/tapestry-react"
@@ -1,12 +1,25 @@
1
1
  import { JSXAttribute, 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 { hasAttribute } from "../../shared/conditions/hasAttribute"
6
6
  import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
7
7
 
8
8
  const VALID_GAP_VALUES = new Set([0, 0.25, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 7])
9
9
 
10
+ const TODO_TEXT =
11
+ "could not migrate spacing to gap automatically; use a Tapestry spacing scale number (0, 0.25, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 7) for gap, or for ReactNode spacers add explicit margin to the spacer element instead"
12
+
13
+ function isKnownInvalidBranch(node: unknown): boolean {
14
+ if (typeof node !== "object" || node === null) return false
15
+ const typed = node as { type: string; value?: unknown }
16
+ if (typed.type === "NumericLiteral" && typeof typed.value === "number") {
17
+ return !VALID_GAP_VALUES.has(typed.value)
18
+ }
19
+ if (typed.type === "StringLiteral") return true
20
+ return typed.type === "JSXElement" || typed.type === "JSXFragment"
21
+ }
22
+
10
23
  function resolveSpacingValue(attribute: JSXAttribute): number | null {
11
24
  const value = attribute.value
12
25
  if (
@@ -19,24 +32,29 @@ function resolveSpacingValue(attribute: JSXAttribute): number | null {
19
32
  return null
20
33
  }
21
34
 
35
+ function isAlreadyValidTernary(attribute: JSXAttribute): boolean {
36
+ const value = attribute.value
37
+ if (value?.type !== "JSXExpressionContainer") return false
38
+ const expr = value.expression
39
+ if (expr.type !== "ConditionalExpression") return false
40
+ return (
41
+ !isKnownInvalidBranch(expr.consequent) &&
42
+ !isKnownInvalidBranch(expr.alternate)
43
+ )
44
+ }
45
+
22
46
  const transform: Transform = attributeTransformFactory({
23
47
  condition: hasAttribute("spacing"),
24
48
  targetComponent: "StackView",
25
49
  targetPackage: "@planningcenter/tapestry-react",
26
- transform: (element, { j, source }) => {
50
+ transform: (element, { j }) => {
27
51
  const spacingAttribute = getAttribute({ element, name: "spacing" })
28
52
  if (!spacingAttribute) return false
29
53
 
30
54
  const spacingValue = resolveSpacingValue(spacingAttribute)
31
55
 
32
- if (spacingValue === null) {
33
- addComment({
34
- element,
35
- j,
36
- scope: "spacing",
37
- source,
38
- text: "could not migrate spacing to gap automatically; use a Tapestry spacing scale number (0, 0.25, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 7) for gap, or for ReactNode spacers add explicit margin to the spacer element instead",
39
- })
56
+ if (spacingValue === null && !isAlreadyValidTernary(spacingAttribute)) {
57
+ addCommentToAttribute({ attribute: spacingAttribute, j, text: TODO_TEXT })
40
58
  return true
41
59
  }
42
60