@planningcenter/tapestry-migration-cli 2.4.0-rc.1 → 2.4.0-rc.11

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 (28) hide show
  1. package/package.json +2 -2
  2. package/src/components/button/transforms/childrenToLabel.test.ts +5 -4
  3. package/src/components/button/transforms/childrenToLabel.ts +9 -39
  4. package/src/components/button/transforms/unsupportedProps.ts +7 -31
  5. package/src/components/link/index.ts +16 -2
  6. package/src/components/link/transforms/childrenToLabel.test.ts +331 -0
  7. package/src/components/link/transforms/childrenToLabel.ts +54 -0
  8. package/src/components/link/transforms/convertStyleProps.test.ts +391 -0
  9. package/src/components/link/transforms/convertStyleProps.ts +10 -0
  10. package/src/components/link/transforms/{inlineToKind.test.ts → inlineMemberToKind.test.ts} +2 -2
  11. package/src/components/link/transforms/{inlineToKind.ts → inlineMemberToKind.ts} +0 -2
  12. package/src/components/link/transforms/inlinePropToKind.test.ts +312 -0
  13. package/src/components/link/transforms/inlinePropToKind.ts +24 -0
  14. package/src/components/link/transforms/moveLinkImport.test.ts +295 -0
  15. package/src/components/link/transforms/moveLinkImport.ts +14 -0
  16. package/src/components/link/transforms/removeAs.test.ts +192 -0
  17. package/src/components/link/transforms/removeAs.ts +17 -0
  18. package/src/components/link/transforms/reviewStyles.test.ts +172 -0
  19. package/src/components/link/transforms/reviewStyles.ts +17 -0
  20. package/src/components/link/transforms/targetBlankToExternal.test.ts +14 -0
  21. package/src/components/link/transforms/unsupportedProps.test.ts +265 -0
  22. package/src/components/link/transforms/unsupportedProps.ts +58 -0
  23. package/src/components/shared/conditions/hasAttributeValue.test.ts +22 -1
  24. package/src/components/shared/conditions/hasAttributeValue.ts +24 -6
  25. package/src/components/shared/helpers/childrenToLabelHelpers.ts +43 -0
  26. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +35 -0
  27. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +5 -1
  28. package/src/stubs/textPlugin.ts +45 -0
@@ -0,0 +1,192 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import removeAs from "./removeAs"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string | null {
9
+ return removeAs(
10
+ { path: "test.tsx", source },
11
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
12
+ {}
13
+ ) as string | null
14
+ }
15
+
16
+ describe("removeAs transform", () => {
17
+ describe("basic transformations", () => {
18
+ it("should remove as='a' attribute from Link", () => {
19
+ const input = `
20
+ import { Link } from "@planningcenter/tapestry-react"
21
+
22
+ export default function Test() {
23
+ return <Link href="/home" as="a">Home</Link>
24
+ }
25
+ `.trim()
26
+
27
+ const expected = `
28
+ import { Link } from "@planningcenter/tapestry-react"
29
+
30
+ export default function Test() {
31
+ return <Link href="/home">Home</Link>;
32
+ }
33
+ `.trim()
34
+
35
+ const result = applyTransform(input)
36
+ expect(result?.trim()).toBe(expected)
37
+ })
38
+
39
+ it("should only remove as='a' attribute, not other as values", () => {
40
+ const input = `
41
+ import { Link } from "@planningcenter/tapestry-react"
42
+
43
+ export default function Test() {
44
+ return (
45
+ <div>
46
+ <Link href="/home" as="a">Home</Link>
47
+ <Link href="/button" as="button">Button</Link>
48
+ </div>
49
+ )
50
+ }
51
+ `.trim()
52
+
53
+ const result = applyTransform(input)
54
+ expect(result).toContain('href="/home"') // Should remain
55
+ expect(result).not.toContain('as="a"') // Should be removed
56
+ expect(result).toContain('as="button"') // Should remain
57
+ })
58
+
59
+ it("should preserve other attributes while removing as='a'", () => {
60
+ const input = `
61
+ import { Link } from "@planningcenter/tapestry-react"
62
+
63
+ export default function Test() {
64
+ return (
65
+ <Link
66
+ href="/external"
67
+ as="a"
68
+ className="nav-link"
69
+ target="_blank"
70
+ rel="noopener"
71
+ >
72
+ External Link
73
+ </Link>
74
+ )
75
+ }
76
+ `.trim()
77
+
78
+ const result = applyTransform(input)
79
+ expect(result).toContain('href="/external"')
80
+ expect(result).toContain('className="nav-link"')
81
+ expect(result).toContain('target="_blank"')
82
+ expect(result).toContain('rel="noopener"')
83
+ expect(result).not.toContain('as="a"')
84
+ })
85
+
86
+ it("should handle Links without as attribute", () => {
87
+ const input = `
88
+ import { Link } from "@planningcenter/tapestry-react"
89
+
90
+ export default function Test() {
91
+ return <Link href="/internal">Internal Link</Link>
92
+ }
93
+ `.trim()
94
+
95
+ const result = applyTransform(input)
96
+ expect(result).toBeNull() // No changes should be made
97
+ })
98
+
99
+ it("should handle Links with as attribute but different value", () => {
100
+ const input = `
101
+ import { Link } from "@planningcenter/tapestry-react"
102
+
103
+ export default function Test() {
104
+ return <Link href="/button" as="button">Button Link</Link>
105
+ }
106
+ `.trim()
107
+
108
+ const result = applyTransform(input)
109
+ expect(result).toBeNull() // No changes should be made
110
+ })
111
+ })
112
+
113
+ describe("edge cases", () => {
114
+ it("should handle multiple Link components", () => {
115
+ const input = `
116
+ import { Link } from "@planningcenter/tapestry-react"
117
+
118
+ export default function Test() {
119
+ return (
120
+ <div>
121
+ <Link href="/page1" as="a">Page 1</Link>
122
+ <Link href="/page2" as="a">Page 2</Link>
123
+ <Link href="/page3">Page 3</Link>
124
+ </div>
125
+ )
126
+ }
127
+ `.trim()
128
+
129
+ const result = applyTransform(input)
130
+ expect(result).not.toContain('as="a"') // Should be removed from both
131
+ expect(result).toContain('href="/page1"')
132
+ expect(result).toContain('href="/page2"')
133
+ expect(result).toContain('href="/page3"') // Should remain unchanged
134
+ })
135
+
136
+ it("should handle self-closing Link components", () => {
137
+ const input = `
138
+ import { Link } from "@planningcenter/tapestry-react"
139
+
140
+ export default function Test() {
141
+ return <Link href="/external" as="a" />
142
+ }
143
+ `.trim()
144
+
145
+ const result = applyTransform(input)
146
+ expect(result).toContain('href="/external"')
147
+ expect(result).not.toContain('as="a"')
148
+ })
149
+
150
+ it("should handle dynamic as values", () => {
151
+ const input = `
152
+ import { Link } from "@planningcenter/tapestry-react"
153
+
154
+ export default function Test() {
155
+ return <Link href="/external" as={isExternal ? "a" : "button"}>Link</Link>
156
+ }
157
+ `.trim()
158
+
159
+ const result = applyTransform(input)
160
+ expect(result).toBeNull() // No changes should be made for dynamic values
161
+ })
162
+ })
163
+
164
+ describe("import handling", () => {
165
+ it("should only transform when Link is imported from correct package", () => {
166
+ const input = `
167
+ import { Link } from "react-router-dom"
168
+
169
+ export default function Test() {
170
+ return <Link to="/external" as="a">External Link</Link>
171
+ }
172
+ `.trim()
173
+
174
+ const result = applyTransform(input)
175
+ expect(result).toBeNull() // No changes should be made
176
+ })
177
+
178
+ it("should handle aliased imports", () => {
179
+ const input = `
180
+ import { Link as TapestryLink } from "@planningcenter/tapestry-react"
181
+
182
+ export default function Test() {
183
+ return <TapestryLink href="/external" as="a">External Link</TapestryLink>
184
+ }
185
+ `.trim()
186
+
187
+ const result = applyTransform(input)
188
+ expect(result).toContain('href="/external"')
189
+ expect(result).not.toContain('as="a"')
190
+ })
191
+ })
192
+ })
@@ -0,0 +1,17 @@
1
+ import { removeAttribute } from "../../shared/actions/removeAttribute"
2
+ import { hasAttributeValue } from "../../shared/conditions/hasAttributeValue"
3
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
4
+
5
+ const transform = attributeTransformFactory({
6
+ condition: hasAttributeValue("as", "a"),
7
+ targetComponent: "Link",
8
+ targetPackage: "@planningcenter/tapestry-react",
9
+ transform: (element, { j, source }) => {
10
+ // Remove as attribute
11
+ removeAttribute("as", { element, j, source })
12
+
13
+ return true
14
+ },
15
+ })
16
+
17
+ export default transform
@@ -0,0 +1,172 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./reviewStyles"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ // Helper to run transform and get result
9
+ function applyTransform(source: string): string | null {
10
+ const fileInfo = { path: "test.tsx", source }
11
+ const api = {
12
+ j,
13
+ jscodeshift: j,
14
+ report: () => {},
15
+ stats: () => {},
16
+ }
17
+ const result = transform(fileInfo, api, {})
18
+ return result as string | null
19
+ }
20
+
21
+ describe("reviewStyles transform", () => {
22
+ describe("basic transformation", () => {
23
+ it("should add comment to Link with style attribute", () => {
24
+ const input = `
25
+ import { Link } from "@planningcenter/tapestry-react"
26
+
27
+ function Component() {
28
+ return <Link style={{ color: "blue" }} href="/home">Home</Link>
29
+ }
30
+ `
31
+
32
+ const result = applyTransform(input)
33
+
34
+ expect(result).toContain("TODO: tapestry-migration")
35
+ expect(result).toContain(
36
+ "review custom styles - Link styles may need to be updated for new design system"
37
+ )
38
+ expect(result).toContain('style={{ color: "blue" }}')
39
+ })
40
+
41
+ it("should handle Link with complex style object", () => {
42
+ const input = `
43
+ import { Link } from "@planningcenter/tapestry-react"
44
+
45
+ function Component() {
46
+ return (
47
+ <Link
48
+ style={{
49
+ color: "red",
50
+ fontSize: "14px",
51
+ textDecoration: "none"
52
+ }}
53
+ href="/about"
54
+ >
55
+ About
56
+ </Link>
57
+ )
58
+ }
59
+ `
60
+
61
+ const result = applyTransform(input)
62
+
63
+ expect(result).toContain("TODO: tapestry-migration")
64
+ expect(result).toContain("review custom styles")
65
+ expect(result).toContain('color: "red"')
66
+ expect(result).toContain('fontSize: "14px"')
67
+ expect(result).toContain('textDecoration: "none"')
68
+ })
69
+
70
+ it("should preserve Link without style attribute", () => {
71
+ const input = `
72
+ import { Link } from "@planningcenter/tapestry-react"
73
+
74
+ function Component() {
75
+ return <Link href="/home">Home</Link>
76
+ }
77
+ `
78
+
79
+ const result = applyTransform(input)
80
+
81
+ expect(result).toBeNull()
82
+ })
83
+ })
84
+
85
+ describe("multiple links", () => {
86
+ it("should handle mixed Link usage", () => {
87
+ const input = `
88
+ import { Link } from "@planningcenter/tapestry-react"
89
+
90
+ function Component() {
91
+ return (
92
+ <div>
93
+ <Link style={{ color: "blue" }} href="/home">Home</Link>
94
+ <Link href="/about">About</Link>
95
+ <Link style={{ fontSize: "16px" }} href="/contact">Contact</Link>
96
+ </div>
97
+ )
98
+ }
99
+ `
100
+
101
+ const result = applyTransform(input)
102
+
103
+ expect(result).toContain("TODO: tapestry-migration")
104
+ expect(result).toContain("review custom styles")
105
+ // Should have comments for both styled links
106
+ const commentCount = (result?.match(/TODO: tapestry-migration/g) || [])
107
+ .length
108
+ expect(commentCount).toBe(2)
109
+ })
110
+ })
111
+
112
+ describe("edge cases", () => {
113
+ it("should handle Link with style expression", () => {
114
+ const input = `
115
+ import { Link } from "@planningcenter/tapestry-react"
116
+
117
+ function Component() {
118
+ const linkStyles = { color: "green" }
119
+ return <Link style={linkStyles} href="/dynamic">Dynamic</Link>
120
+ }
121
+ `
122
+
123
+ const result = applyTransform(input)
124
+
125
+ expect(result).toContain("TODO: tapestry-migration")
126
+ expect(result).toContain("review custom styles")
127
+ expect(result).toContain("style={linkStyles}")
128
+ })
129
+
130
+ it("should not affect other components with style", () => {
131
+ const input = `
132
+ import { Link } from "@planningcenter/tapestry-react"
133
+
134
+ function Component() {
135
+ return (
136
+ <div>
137
+ <Link style={{ color: "blue" }} href="/home">Link</Link>
138
+ <div style={{ padding: "10px" }}>Div</div>
139
+ </div>
140
+ )
141
+ }
142
+ `
143
+
144
+ const result = applyTransform(input)
145
+
146
+ expect(result).toContain("TODO: tapestry-migration")
147
+ // Should only have one comment (for Link, not div)
148
+ const commentCount = (result?.match(/TODO: tapestry-migration/g) || [])
149
+ .length
150
+ expect(commentCount).toBe(1)
151
+ expect(result).toContain('<div style={{ padding: "10px" }}>Div</div>')
152
+ })
153
+ })
154
+
155
+ describe("import handling", () => {
156
+ it("should not affect imports", () => {
157
+ const input = `
158
+ import { Link } from "@planningcenter/tapestry-react"
159
+
160
+ function Component() {
161
+ return <Link style={{ color: "blue" }} href="/home">Home</Link>
162
+ }
163
+ `
164
+
165
+ const result = applyTransform(input)
166
+
167
+ expect(result).toContain(
168
+ 'import { Link } from "@planningcenter/tapestry-react"'
169
+ )
170
+ })
171
+ })
172
+ })
@@ -0,0 +1,17 @@
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
+ const COMMENT = `review custom styles - Link styles may need to be updated for new design system.`
7
+
8
+ export default attributeTransformFactory({
9
+ condition: hasAttribute("style"),
10
+ targetComponent: "Link",
11
+ targetPackage: "@planningcenter/tapestry-react",
12
+ transform: (element, { j }) => {
13
+ const attribute = getAttribute({ element, name: "style" })!
14
+ addCommentToAttribute({ attribute, j, text: COMMENT })
15
+ return true
16
+ },
17
+ })
@@ -132,6 +132,20 @@ export default function Test() {
132
132
  expect(result).toContain('href="/page3"') // Should remain unchanged
133
133
  })
134
134
 
135
+ it("should handle target={'_blank'} format", () => {
136
+ const input = `
137
+ import { Link } from "@planningcenter/tapestry-react"
138
+
139
+ export default function Test() {
140
+ return <Link href="/external" target={'_blank'}>External Link</Link>
141
+ }
142
+ `.trim()
143
+
144
+ const result = applyTransform(input)
145
+ expect(result).toContain("external")
146
+ expect(result).not.toContain("target={'_blank'}")
147
+ })
148
+
135
149
  it("should handle dynamic target values", () => {
136
150
  const input = `
137
151
  import { Link } from "@planningcenter/tapestry-react"
@@ -0,0 +1,265 @@
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) {
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("Link unsupportedProps transform", () => {
18
+ describe("basic transformations", () => {
19
+ it("should add comment to Link with unsupported 'css' prop", () => {
20
+ const input = `
21
+ import { Link } from "@planningcenter/tapestry-react"
22
+
23
+ export default function Test() {
24
+ return <Link href="/about" css={{ color: "red" }}>About</Link>
25
+ }
26
+ `.trim()
27
+
28
+ const result = applyTransform(input)
29
+ expect(result).toContain("TODO: tapestry-migration (css)")
30
+ expect(result).toContain("'css' is not supported")
31
+ expect(result).toContain("Use 'className' prop with CSS classes")
32
+ expect(result).toContain('css={{ color: "red" }}')
33
+ })
34
+
35
+ it("should add comment to Link with unsupported 'disabled' prop", () => {
36
+ const input = `
37
+ import { Link } from "@planningcenter/tapestry-react"
38
+
39
+ export default function Test() {
40
+ return <Link href="/about" disabled>About</Link>
41
+ }
42
+ `.trim()
43
+
44
+ const result = applyTransform(input)
45
+ expect(result).toContain("TODO: tapestry-migration (disabled)")
46
+ expect(result).toContain("'disabled' is not supported")
47
+ expect(result).toContain("Links do not support the disabled prop")
48
+ expect(result).toContain("disabled")
49
+ })
50
+
51
+ it("should add comments to Link with multiple unsupported props", () => {
52
+ const input = `
53
+ import { Link } from "@planningcenter/tapestry-react"
54
+
55
+ export default function Test() {
56
+ return <Link href="/about" css={{ color: "red" }} disabled truncate underline={false}>About</Link>
57
+ }
58
+ `.trim()
59
+
60
+ const result = applyTransform(input)
61
+ expect(result).toContain("TODO: tapestry-migration (css)")
62
+ expect(result).toContain("TODO: tapestry-migration (disabled)")
63
+ expect(result).toContain("TODO: tapestry-migration (truncate)")
64
+ expect(result).toContain("'truncate' is not supported")
65
+ expect(result).toContain("TODO: tapestry-migration (underline)")
66
+ expect(result).toContain("'underline' is not supported")
67
+ })
68
+
69
+ it("should add comment for hover/focus/active state props", () => {
70
+ const input = `
71
+ import { Link } from "@planningcenter/tapestry-react"
72
+
73
+ export default function Test() {
74
+ return <Link href="/about" hover={{ color: "blue" }}>About</Link>
75
+ }
76
+ `.trim()
77
+
78
+ const result = applyTransform(input)
79
+ expect(result).toContain("TODO: tapestry-migration (hover)")
80
+ expect(result).toContain("State-based styles (hover, focus, active)")
81
+ })
82
+
83
+ it("should add comment for mediaQueries prop", () => {
84
+ const input = `
85
+ import { Link } from "@planningcenter/tapestry-react"
86
+
87
+ export default function Test() {
88
+ return <Link href="/about" mediaQueries={{ sm: { fontSize: "12px" } }}>About</Link>
89
+ }
90
+ `.trim()
91
+
92
+ const result = applyTransform(input)
93
+ expect(result).toContain("TODO: tapestry-migration (mediaQueries)")
94
+ expect(result).toContain("use CSS media queries in a class")
95
+ })
96
+ })
97
+
98
+ describe("supported props should not be flagged", () => {
99
+ it("should not flag standard anchor attributes", () => {
100
+ const input = `
101
+ import { Link } from "@planningcenter/tapestry-react"
102
+
103
+ export default function Test() {
104
+ return (
105
+ <Link
106
+ href="/about"
107
+ download
108
+ hrefLang="en"
109
+ type="text/html"
110
+ referrerPolicy="no-referrer"
111
+ >
112
+ About
113
+ </Link>
114
+ )
115
+ }
116
+ `.trim()
117
+
118
+ const result = applyTransform(input)
119
+ expect(result).toBeNull()
120
+ })
121
+
122
+ it("should not flag Tapestry Link props", () => {
123
+ const input = `
124
+ import { Link } from "@planningcenter/tapestry-react"
125
+
126
+ export default function Test() {
127
+ return (
128
+ <Link
129
+ href="/about"
130
+ kind="primary"
131
+ size="lg"
132
+ external
133
+ prefix={<Icon />}
134
+ suffix={<Icon />}
135
+ label="About"
136
+ className="custom-class"
137
+ >
138
+ About
139
+ </Link>
140
+ )
141
+ }
142
+ `.trim()
143
+
144
+ const result = applyTransform(input)
145
+ expect(result).toBeNull()
146
+ })
147
+
148
+ it("should not flag common React/HTML props", () => {
149
+ const input = `
150
+ import { Link } from "@planningcenter/tapestry-react"
151
+
152
+ export default function Test() {
153
+ return (
154
+ <Link
155
+ href="/about"
156
+ id="about-link"
157
+ onClick={handleClick}
158
+ onFocus={handleFocus}
159
+ onBlur={handleBlur}
160
+ role="link"
161
+ tabIndex={0}
162
+ title="Go to About page"
163
+ style={{ margin: 10 }}
164
+ >
165
+ About
166
+ </Link>
167
+ )
168
+ }
169
+ `.trim()
170
+
171
+ const result = applyTransform(input)
172
+ expect(result).toBeNull()
173
+ })
174
+
175
+ it("should not flag aria-* attributes", () => {
176
+ const input = `
177
+ import { Link } from "@planningcenter/tapestry-react"
178
+
179
+ export default function Test() {
180
+ return (
181
+ <Link
182
+ href="/about"
183
+ aria-label="About page"
184
+ aria-describedby="desc"
185
+ aria-hidden={false}
186
+ >
187
+ About
188
+ </Link>
189
+ )
190
+ }
191
+ `.trim()
192
+
193
+ const result = applyTransform(input)
194
+ expect(result).toBeNull()
195
+ })
196
+
197
+ it("should not flag data-* attributes", () => {
198
+ const input = `
199
+ import { Link } from "@planningcenter/tapestry-react"
200
+
201
+ export default function Test() {
202
+ return (
203
+ <Link
204
+ href="/about"
205
+ data-testid="about-link"
206
+ data-tracking="nav-about"
207
+ >
208
+ About
209
+ </Link>
210
+ )
211
+ }
212
+ `.trim()
213
+
214
+ const result = applyTransform(input)
215
+ expect(result).toBeNull()
216
+ })
217
+ })
218
+
219
+ describe("edge cases", () => {
220
+ it("should handle Link with no unsupported props", () => {
221
+ const input = `
222
+ import { Link } from "@planningcenter/tapestry-react"
223
+
224
+ export default function Test() {
225
+ return <Link href="/about">About</Link>
226
+ }
227
+ `.trim()
228
+
229
+ const result = applyTransform(input)
230
+ expect(result).toBeNull()
231
+ })
232
+
233
+ it("should not transform Link from other packages", () => {
234
+ const input = `
235
+ import { Link } from "react-router-dom"
236
+
237
+ export default function Test() {
238
+ return <Link to="/about" css={{ color: "red" }}>About</Link>
239
+ }
240
+ `.trim()
241
+
242
+ const result = applyTransform(input)
243
+ expect(result).toBeNull()
244
+ })
245
+
246
+ it("should handle multiple Link components", () => {
247
+ const input = `
248
+ import { Link } from "@planningcenter/tapestry-react"
249
+
250
+ export default function Test() {
251
+ return (
252
+ <div>
253
+ <Link href="/about" css={{ color: "red" }}>About</Link>
254
+ <Link href="/contact" disabled>Contact</Link>
255
+ </div>
256
+ )
257
+ }
258
+ `.trim()
259
+
260
+ const result = applyTransform(input)
261
+ expect(result).toContain("TODO: tapestry-migration (css)")
262
+ expect(result).toContain("TODO: tapestry-migration (disabled)")
263
+ })
264
+ })
265
+ })