@planningcenter/tapestry-migration-cli 2.8.0-rc.13 → 2.8.0-rc.15

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.
@@ -0,0 +1,49 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
4
+ import { getAttributeValue } from "../../shared/actions/getAttributeValue"
5
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
6
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
7
+
8
+ const SIZE_MAPPING = {
9
+ lg: "md",
10
+ xl: "md",
11
+ xs: "sm",
12
+ }
13
+
14
+ const transform: Transform = attributeTransformFactory({
15
+ condition: hasAttribute("size"),
16
+ targetComponent: "Radio",
17
+ targetPackage: "@planningcenter/tapestry-react",
18
+ transform: (element, { j }) => {
19
+ let hasChanges = false
20
+
21
+ const sizeAttr = element.openingElement.attributes?.find(
22
+ (attr) => attr.type === "JSXAttribute" && attr.name.name === "size"
23
+ )
24
+
25
+ if (sizeAttr && sizeAttr.type === "JSXAttribute") {
26
+ const sizeValue = getAttributeValue({ attribute: sizeAttr, j })
27
+
28
+ if (sizeValue) {
29
+ const mappedSize = SIZE_MAPPING[sizeValue as keyof typeof SIZE_MAPPING]
30
+
31
+ if (mappedSize && mappedSize !== sizeValue) {
32
+ // Normalize to a plain string attribute value (size="md")
33
+ sizeAttr.value = j.stringLiteral(mappedSize)
34
+ hasChanges = true
35
+
36
+ addCommentToAttribute({
37
+ attribute: sizeAttr,
38
+ j,
39
+ text: `Size "${sizeValue}" was mapped to "${mappedSize}". Verify visual appearance as sizes may differ slightly.`,
40
+ })
41
+ }
42
+ }
43
+ }
44
+
45
+ return hasChanges
46
+ },
47
+ })
48
+
49
+ export default transform
@@ -0,0 +1,241 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./unsupportedProps"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ const result = transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ {}
14
+ ) as string | null
15
+ return result || source
16
+ }
17
+
18
+ describe("unsupportedProps transform", () => {
19
+ describe("unsupported prop detection", () => {
20
+ it("should add comment for css prop", () => {
21
+ const input = `
22
+ import { Radio } from "@planningcenter/tapestry-react"
23
+
24
+ function Test() {
25
+ return <Radio css={{ color: 'red' }} label="Test" />
26
+ }
27
+ `.trim()
28
+
29
+ const result = applyTransform(input)
30
+ expect(result).toContain(
31
+ "/* TODO: tapestry-migration (css): 'css' is not supported, please migrate as needed."
32
+ )
33
+ expect(result).toContain(
34
+ "CSS prop is not supported. Use className or style prop instead."
35
+ )
36
+ expect(result).toContain("css={{ color: 'red' }}")
37
+ })
38
+
39
+ it("should add comment for custom unsupported prop", () => {
40
+ const input = `
41
+ import { Radio } from "@planningcenter/tapestry-react"
42
+
43
+ function Test() {
44
+ return <Radio customProp="value" label="Test" />
45
+ }
46
+ `.trim()
47
+
48
+ const result = applyTransform(input)
49
+ expect(result).toContain(
50
+ "/* TODO: tapestry-migration (customProp): 'customProp' is not supported, please migrate as needed."
51
+ )
52
+ expect(result).toContain('customProp="value"')
53
+ })
54
+
55
+ it("should add comments for multiple unsupported props", () => {
56
+ const input = `
57
+ import { Radio } from "@planningcenter/tapestry-react"
58
+
59
+ function Test() {
60
+ return (
61
+ <Radio
62
+ css={{ color: 'red' }}
63
+ ref={ref}
64
+ customProp="value"
65
+ label="Test"
66
+ />
67
+ )
68
+ }
69
+ `.trim()
70
+
71
+ const result = applyTransform(input)
72
+ expect(result).toContain("/* TODO: tapestry-migration (css):")
73
+ expect(result).toContain("/* TODO: tapestry-migration (customProp):")
74
+ })
75
+ })
76
+
77
+ describe("supported props", () => {
78
+ it("should not add comments for supported radio props", () => {
79
+ const input = `
80
+ import { Radio } from "@planningcenter/tapestry-react"
81
+
82
+ function Test() {
83
+ const ref = React.useRef()
84
+ return (
85
+ <Radio
86
+ checked
87
+ disabled
88
+ ref={ref}
89
+ label="Test"
90
+ name="radio"
91
+ onChange={() => {}}
92
+ value="test"
93
+ />
94
+ )
95
+ }
96
+ `.trim()
97
+
98
+ const result = applyTransform(input)
99
+ expect(result).not.toContain("TODO: tapestry-migration")
100
+ expect(result).toContain("checked")
101
+ expect(result).toContain("disabled")
102
+ expect(result).toContain("ref={ref}")
103
+ expect(result).toContain('label="Test"')
104
+ expect(result).toContain('name="radio"')
105
+ expect(result).toContain("onChange={() => {}}")
106
+ expect(result).toContain('value="test"')
107
+ })
108
+
109
+ it("should not add comments for common props", () => {
110
+ const input = `
111
+ import { Radio } from "@planningcenter/tapestry-react"
112
+
113
+ function Test() {
114
+ return (
115
+ <Radio
116
+ className="test"
117
+ id="radio"
118
+ key="key"
119
+ style={{ color: 'red' }}
120
+ tabIndex={0}
121
+ label="Test"
122
+ />
123
+ )
124
+ }
125
+ `.trim()
126
+
127
+ const result = applyTransform(input)
128
+ expect(result).not.toContain("TODO: tapestry-migration")
129
+ expect(result).toContain('className="test"')
130
+ expect(result).toContain('id="radio"')
131
+ expect(result).toContain('key="key"')
132
+ expect(result).toContain("style={{ color: 'red' }}")
133
+ expect(result).toContain("tabIndex={0}")
134
+ })
135
+
136
+ it("should not add comments for aria props", () => {
137
+ const input = `
138
+ import { Radio } from "@planningcenter/tapestry-react"
139
+
140
+ function Test() {
141
+ return (
142
+ <Radio
143
+ aria-label="Test radio"
144
+ aria-describedby="description"
145
+ label="Test"
146
+ />
147
+ )
148
+ }
149
+ `.trim()
150
+
151
+ const result = applyTransform(input)
152
+ expect(result).not.toContain("TODO: tapestry-migration")
153
+ expect(result).toContain('aria-label="Test radio"')
154
+ expect(result).toContain('aria-describedby="description"')
155
+ })
156
+
157
+ it("should not add comments for data props", () => {
158
+ const input = `
159
+ import { Radio } from "@planningcenter/tapestry-react"
160
+
161
+ function Test() {
162
+ return (
163
+ <Radio
164
+ data-testid="radio"
165
+ data-cy="test-radio"
166
+ label="Test"
167
+ />
168
+ )
169
+ }
170
+ `.trim()
171
+
172
+ const result = applyTransform(input)
173
+ expect(result).not.toContain("TODO: tapestry-migration")
174
+ expect(result).toContain('data-testid="radio"')
175
+ expect(result).toContain('data-cy="test-radio"')
176
+ })
177
+ })
178
+
179
+ describe("edge cases", () => {
180
+ it("should not affect radio without unsupported props", () => {
181
+ const input = `
182
+ import { Radio } from "@planningcenter/tapestry-react"
183
+
184
+ function Test() {
185
+ return <Radio label="Test" />
186
+ }
187
+ `.trim()
188
+
189
+ const result = applyTransform(input)
190
+ expect(result).not.toContain("TODO: tapestry-migration")
191
+ expect(result).toBe(input)
192
+ })
193
+
194
+ it("should not affect other components", () => {
195
+ const input = `
196
+ import { Button, Radio } from "@planningcenter/tapestry-react"
197
+
198
+ function Test() {
199
+ return (
200
+ <div>
201
+ <Button css={{ color: 'red' }}>Click me</Button>
202
+ <Radio label="Test" />
203
+ </div>
204
+ )
205
+ }
206
+ `.trim()
207
+
208
+ const result = applyTransform(input)
209
+ expect(result).not.toContain("TODO: tapestry-migration")
210
+ expect(result).toContain("css={{ color: 'red' }}")
211
+ })
212
+
213
+ it("should handle mixed supported and unsupported props", () => {
214
+ const input = `
215
+ import { Radio } from "@planningcenter/tapestry-react"
216
+
217
+ function Test() {
218
+ return (
219
+ <Radio
220
+ checked
221
+ css={{ color: 'red' }}
222
+ disabled
223
+ customProp="value"
224
+ label="Test"
225
+ />
226
+ )
227
+ }
228
+ `.trim()
229
+
230
+ const result = applyTransform(input)
231
+ expect(result).toContain("/* TODO: tapestry-migration (css):")
232
+ expect(result).toContain("/* TODO: tapestry-migration (customProp):")
233
+ expect(result).not.toContain("TODO: tapestry-migration.*checked")
234
+ expect(result).not.toContain("TODO: tapestry-migration.*disabled")
235
+ expect(result).not.toContain("TODO: tapestry-migration.*label")
236
+ expect(result).toContain("checked")
237
+ expect(result).toContain("disabled")
238
+ expect(result).toContain('label="Test"')
239
+ })
240
+ })
241
+ })
@@ -0,0 +1,35 @@
1
+ import { JSXAttribute, Transform } from "jscodeshift"
2
+
3
+ import { addCommentToUnsupportedProps } from "../../shared/actions/addCommentToUnsupportedProps"
4
+ import { CHECKBOX_RADIO_SUPPORTED_PROPS } from "../../shared/helpers/unsupportedPropsHelpers"
5
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
6
+
7
+ const transform: Transform = attributeTransformFactory({
8
+ targetComponent: "Radio",
9
+ targetPackage: "@planningcenter/tapestry-react",
10
+ transform: (element, { j }) => {
11
+ const UNSUPPORTED_PROPS = (element.openingElement.attributes || [])
12
+ .filter(
13
+ (attr) =>
14
+ attr.type === "JSXAttribute" &&
15
+ !CHECKBOX_RADIO_SUPPORTED_PROPS.includes(attr.name.name as string) &&
16
+ !(attr.name.name as string).startsWith("aria-") &&
17
+ !(attr.name.name as string).startsWith("data-")
18
+ )
19
+ .map((attr) => (attr as JSXAttribute).name.name as string)
20
+
21
+ return addCommentToUnsupportedProps({
22
+ element,
23
+ j,
24
+ messageSuffix: (prop) => {
25
+ if (prop === "css") {
26
+ return "\n * CSS prop is not supported. Use className or style prop instead.\n"
27
+ }
28
+ return ""
29
+ },
30
+ props: UNSUPPORTED_PROPS,
31
+ })
32
+ },
33
+ })
34
+
35
+ export default transform
@@ -74,6 +74,24 @@ describe("getAttributeValue", () => {
74
74
  })
75
75
 
76
76
  describe("expression container values", () => {
77
+ it("should extract string literal from JSX expression container", () => {
78
+ const element = createElementFromCode('<Button size={"lg"}>Save</Button>')
79
+ const sizeAttribute = getAttributeFromElement(element, "size")
80
+
81
+ const value = getAttributeValue({ attribute: sizeAttribute, j })
82
+
83
+ expect(value).toBe("lg")
84
+ })
85
+
86
+ it("should extract string literal with single quotes from JSX expression container", () => {
87
+ const element = createElementFromCode("<Button size={'md'}>Save</Button>")
88
+ const sizeAttribute = getAttributeFromElement(element, "size")
89
+
90
+ const value = getAttributeValue({ attribute: sizeAttribute, j })
91
+
92
+ expect(value).toBe("md")
93
+ })
94
+
77
95
  it("should convert boolean expression to source code", () => {
78
96
  const element = createElementFromCode(
79
97
  "<Button disabled={true}>Save</Button>"
@@ -7,9 +7,22 @@ export function getAttributeValue({
7
7
  attribute: JSXAttribute | null | undefined
8
8
  j: JSCodeshift
9
9
  }) {
10
- return attribute && attribute.value
11
- ? attribute?.value?.type === "StringLiteral"
12
- ? attribute.value.value
13
- : j(attribute.value).toSource()
14
- : null
10
+ if (!attribute || !attribute.value) {
11
+ return null
12
+ }
13
+
14
+ // Handle string literal: attribute="value"
15
+ if (attribute.value.type === "StringLiteral") {
16
+ return attribute.value.value
17
+ }
18
+
19
+ // Handle JSX expression container with string literal: attribute={"value"}
20
+ if (attribute.value.type === "JSXExpressionContainer") {
21
+ if (attribute.value.expression.type === "StringLiteral") {
22
+ return attribute.value.expression.value
23
+ }
24
+ }
25
+
26
+ // For all other cases, return the source code representation
27
+ return j(attribute.value).toSource()
15
28
  }
@@ -8,8 +8,13 @@ export const COMMON_PROPS = [
8
8
  "className",
9
9
  "id",
10
10
  "key",
11
- "kind",
12
11
  "label",
12
+ "ref",
13
+ "role",
14
+ "size",
15
+ "style",
16
+ "tabIndex",
17
+ "title",
13
18
  "onBlur",
14
19
  "onFocus",
15
20
  "onClick",
@@ -19,17 +24,27 @@ export const COMMON_PROPS = [
19
24
  "onMouseOut",
20
25
  "onMouseOver",
21
26
  "onMouseUp",
22
- "prefix",
23
- "ref",
24
- "role",
25
- "size",
26
- "style",
27
- "suffix",
28
- "tabIndex",
29
- "title",
30
27
  ]
31
28
 
29
+ export const BUTTON_LINK_SHARED_PROPS = ["kind", "prefix", "suffix"]
30
+
32
31
  export const SUPPORTED_PROPS_BASE = [
32
+ ...COMMON_PROPS,
33
+ ...BUTTON_LINK_SHARED_PROPS,
33
34
  ...STYLE_PROP_NAMES_WITHOUT_CSS,
35
+ ]
36
+
37
+ export const CHECKBOX_RADIO_SHARED_PROPS = [
38
+ "checked",
39
+ "disabled",
40
+ "name",
41
+ "onChange",
42
+ "required",
43
+ "value",
44
+ ]
45
+
46
+ export const CHECKBOX_RADIO_SUPPORTED_PROPS = [
34
47
  ...COMMON_PROPS,
48
+ ...CHECKBOX_RADIO_SHARED_PROPS,
49
+ ...STYLE_PROP_NAMES_WITHOUT_CSS,
35
50
  ]
@@ -0,0 +1,84 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import { commentOnSpreadPropsFactory } from "./commentOnSpreadPropsFactory"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ const COMMENT =
9
+ "Spread props can contain unsupported props, please explore usages and migrate as needed."
10
+
11
+ describe("commentOnSpreadPropsFactory", () => {
12
+ it("should add comments to spread props", () => {
13
+ const transform = commentOnSpreadPropsFactory({
14
+ targetComponent: "Button",
15
+ targetPackage: "@planningcenter/tapestry-react",
16
+ })
17
+
18
+ const fileInfo = {
19
+ path: "test.tsx",
20
+ source: `import { Button } from "@planningcenter/tapestry-react";
21
+ <Button {...props} label="Save" />`,
22
+ }
23
+ const api = { j, jscodeshift: j, report: () => {}, stats: () => {} }
24
+
25
+ const result = transform(fileInfo, api, {})
26
+ expect(result).toContain(COMMENT)
27
+ })
28
+
29
+ it("should only transform components from the specified package", () => {
30
+ const transform = commentOnSpreadPropsFactory({
31
+ targetComponent: "Button",
32
+ targetPackage: "@planningcenter/tapestry-react",
33
+ })
34
+
35
+ const fileInfo = {
36
+ path: "test.tsx",
37
+ source: `import { Link } from "@planningcenter/tapestry-react";
38
+ <Link {...props} />`,
39
+ }
40
+ const api = { j, jscodeshift: j, report: () => {}, stats: () => {} }
41
+
42
+ const result = transform(fileInfo, api, {})
43
+ expect(result).toBe(null)
44
+ })
45
+
46
+ it("should return null when no spread props are present", () => {
47
+ const transform = commentOnSpreadPropsFactory({
48
+ targetComponent: "Button",
49
+ targetPackage: "@planningcenter/tapestry-react",
50
+ })
51
+
52
+ const fileInfo = {
53
+ path: "test.tsx",
54
+ source: `import { Button } from "@planningcenter/tapestry-react";
55
+ <Button label="Save" />`,
56
+ }
57
+ const api = { j, jscodeshift: j, report: () => {}, stats: () => {} }
58
+
59
+ const result = transform(fileInfo, api, {})
60
+ expect(result).toBe(null)
61
+ })
62
+
63
+ it("should handle multiple spread props", () => {
64
+ const transform = commentOnSpreadPropsFactory({
65
+ targetComponent: "Button",
66
+ targetPackage: "@planningcenter/tapestry-react",
67
+ })
68
+
69
+ const fileInfo = {
70
+ path: "test.tsx",
71
+ source: `import { Button } from "@planningcenter/tapestry-react";
72
+ <Button {...props1} {...props2} label="Save" />`,
73
+ }
74
+ const api = { j, jscodeshift: j, report: () => {}, stats: () => {} }
75
+
76
+ const result = transform(fileInfo, api, {})
77
+ expect(result).toContain(COMMENT)
78
+ // Should have the comment twice (once per spread prop)
79
+ const matches = result?.match(
80
+ new RegExp(COMMENT.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")
81
+ )
82
+ expect(matches).toHaveLength(2)
83
+ })
84
+ })
@@ -0,0 +1,26 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { addCommentToAttribute } from "../actions/addCommentToAttribute"
4
+ import { getSpreadProps } from "../actions/getSpreadProps"
5
+ import { hasSpreadProps } from "../actions/hasSpreadProps"
6
+ import { attributeTransformFactory } from "./attributeTransformFactory"
7
+
8
+ const COMMENT =
9
+ "Spread props can contain unsupported props, please explore usages and migrate as needed."
10
+
11
+ export function commentOnSpreadPropsFactory(options: {
12
+ targetComponent: string
13
+ targetPackage: string
14
+ }): Transform {
15
+ return attributeTransformFactory({
16
+ condition: hasSpreadProps,
17
+ transform: (element, { j }) => {
18
+ const spreadProps = getSpreadProps(element)
19
+ spreadProps.forEach((prop) =>
20
+ addCommentToAttribute({ attribute: prop, j, text: COMMENT })
21
+ )
22
+ return spreadProps.length > 0
23
+ },
24
+ ...options,
25
+ })
26
+ }
@@ -30,6 +30,8 @@ export const GLOBAL_MESSAGES = {
30
30
  "Buttons inside of Tapestry Group components are not supported and should not be migrated at this time.",
31
31
  checkbox:
32
32
  "Checkbox sizes have adjusted. Verify visual appearance as sizes may differ slightly.",
33
+ radio:
34
+ "Radio sizes have adjusted. Verify visual appearance as sizes may differ slightly.",
33
35
  }
34
36
 
35
37
  export const WEIGHT_CONFIGS: { [key: string]: WeightConfig } = {