@planningcenter/tapestry-migration-cli 2.4.0-rc.2 → 2.4.0-rc.4
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.
- package/package.json +2 -2
- package/src/components/link/index.ts +6 -0
- package/src/components/link/transforms/convertStyleProps.test.ts +239 -0
- package/src/components/link/transforms/convertStyleProps.ts +8 -0
- package/src/components/link/transforms/removeAs.test.ts +192 -0
- package/src/components/link/transforms/removeAs.ts +17 -0
- package/src/components/link/transforms/reviewStyles.test.ts +172 -0
- package/src/components/link/transforms/reviewStyles.ts +17 -0
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/tapestry-migration-cli",
|
|
3
|
-
"version": "2.4.0-rc.
|
|
3
|
+
"version": "2.4.0-rc.4",
|
|
4
4
|
"description": "CLI tool for Tapestry migrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "aa40507b01cc6ea8c044aac697970f5bcc93fda2"
|
|
55
55
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Transform } from "jscodeshift"
|
|
2
2
|
|
|
3
|
+
import convertStyleProps from "./transforms/convertStyleProps"
|
|
3
4
|
import inlineToKind from "./transforms/inlineToKind"
|
|
5
|
+
import removeAs from "./transforms/removeAs"
|
|
6
|
+
import reviewStyles from "./transforms/reviewStyles"
|
|
4
7
|
import targetBlankToExternal from "./transforms/targetBlankToExternal"
|
|
5
8
|
import toToHref from "./transforms/toToHref"
|
|
6
9
|
|
|
@@ -12,6 +15,9 @@ const transform: Transform = (fileInfo, api, options) => {
|
|
|
12
15
|
inlineToKind,
|
|
13
16
|
toToHref,
|
|
14
17
|
targetBlankToExternal,
|
|
18
|
+
removeAs,
|
|
19
|
+
reviewStyles,
|
|
20
|
+
convertStyleProps,
|
|
15
21
|
]
|
|
16
22
|
|
|
17
23
|
for (const individualTransform of transforms) {
|
|
@@ -0,0 +1,239 @@
|
|
|
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, options = {}) {
|
|
9
|
+
const fileInfo = { path: "test.tsx", source }
|
|
10
|
+
return transform(
|
|
11
|
+
fileInfo,
|
|
12
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
13
|
+
options
|
|
14
|
+
) as string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("convertStyleProps transform", () => {
|
|
18
|
+
describe("visible prop - handled by theme system", () => {
|
|
19
|
+
it("should convert visible={false} to display: none like Button", () => {
|
|
20
|
+
const source = `
|
|
21
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
export function TestComponent() {
|
|
24
|
+
return <Link visible={false} href="/hidden">Hidden Link</Link>
|
|
25
|
+
}
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
const result = applyTransform(source)
|
|
29
|
+
|
|
30
|
+
expect(result).toContain("style={{")
|
|
31
|
+
expect(result).toContain('display: "none"')
|
|
32
|
+
expect(result).not.toContain("visible={false}")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("should remove visible={true} with no style changes (true is default)", () => {
|
|
36
|
+
const source = `
|
|
37
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
38
|
+
|
|
39
|
+
export function TestComponent() {
|
|
40
|
+
return <Link visible={true} href="/visible">Visible Link</Link>
|
|
41
|
+
}
|
|
42
|
+
`
|
|
43
|
+
|
|
44
|
+
const result = applyTransform(source)
|
|
45
|
+
|
|
46
|
+
// visible={true} is default behavior, so it should be removed with no style added
|
|
47
|
+
expect(result).not.toContain("visible={true}")
|
|
48
|
+
expect(result).not.toContain("style={{") // No style should be added
|
|
49
|
+
expect(result).toContain('<Link href="/visible">Visible Link</Link>')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe("supported props - preserved like Button", () => {
|
|
54
|
+
it("should preserve size prop as-is like Button does", () => {
|
|
55
|
+
const source = `
|
|
56
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
57
|
+
|
|
58
|
+
export function TestComponent() {
|
|
59
|
+
return <Link size="lg" href="/sized">Sized Link</Link>
|
|
60
|
+
}
|
|
61
|
+
`
|
|
62
|
+
|
|
63
|
+
const result = applyTransform(source)
|
|
64
|
+
|
|
65
|
+
// size should be preserved as a supported prop, not converted to CSS
|
|
66
|
+
expect(result).toBeNull() // No transformation needed
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("should preserve all valid Link size values", () => {
|
|
70
|
+
const sizes = ["xs", "sm", "md", "lg", "xl"]
|
|
71
|
+
|
|
72
|
+
sizes.forEach((size) => {
|
|
73
|
+
const source = `
|
|
74
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
75
|
+
|
|
76
|
+
export function TestComponent() {
|
|
77
|
+
return <Link size="${size}" href="/test">Test Link</Link>
|
|
78
|
+
}
|
|
79
|
+
`
|
|
80
|
+
|
|
81
|
+
const result = applyTransform(source)
|
|
82
|
+
|
|
83
|
+
// No transformation should occur for valid size props
|
|
84
|
+
expect(result).toBeNull()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Note: underline and truncate props will be handled by future custom transforms
|
|
89
|
+
// since they require boolean-to-string conversion that stylePropMapping doesn't support
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe("standard style props", () => {
|
|
93
|
+
it("should convert standard style props to style object", () => {
|
|
94
|
+
const source = `
|
|
95
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
96
|
+
|
|
97
|
+
export function TestComponent() {
|
|
98
|
+
return (
|
|
99
|
+
<Link
|
|
100
|
+
marginBottom={8}
|
|
101
|
+
color="blue"
|
|
102
|
+
fontSize="14px"
|
|
103
|
+
href="/styled"
|
|
104
|
+
>
|
|
105
|
+
Styled Link
|
|
106
|
+
</Link>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
`
|
|
110
|
+
|
|
111
|
+
const result = applyTransform(source)
|
|
112
|
+
|
|
113
|
+
expect(result).toContain("style={{")
|
|
114
|
+
expect(result).toContain('color: "blue"')
|
|
115
|
+
expect(result).toContain('fontSize: "14px"')
|
|
116
|
+
expect(result).not.toContain("marginBottom={8}")
|
|
117
|
+
expect(result).not.toContain('color="blue"')
|
|
118
|
+
expect(result).not.toContain('fontSize="14px"')
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe("mixed props", () => {
|
|
123
|
+
it("should handle combination of visible and standard style props", () => {
|
|
124
|
+
const source = `
|
|
125
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
126
|
+
|
|
127
|
+
export function TestComponent() {
|
|
128
|
+
return (
|
|
129
|
+
<Link
|
|
130
|
+
visible={false}
|
|
131
|
+
marginLeft={16}
|
|
132
|
+
color="red"
|
|
133
|
+
href="/mixed"
|
|
134
|
+
>
|
|
135
|
+
Mixed Props
|
|
136
|
+
</Link>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
`
|
|
140
|
+
|
|
141
|
+
const result = applyTransform(source)
|
|
142
|
+
|
|
143
|
+
expect(result).toContain("style={{")
|
|
144
|
+
expect(result).toContain('display: "none"') // from visible={false}
|
|
145
|
+
expect(result).toContain('color: "red"') // standard style prop
|
|
146
|
+
expect(result).not.toContain("visible={false}")
|
|
147
|
+
expect(result).not.toContain("marginLeft={16}")
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it("should handle existing style prop and merge with converted props", () => {
|
|
151
|
+
const source = `
|
|
152
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
153
|
+
|
|
154
|
+
export function TestComponent() {
|
|
155
|
+
return (
|
|
156
|
+
<Link
|
|
157
|
+
style={{ padding: "10px", border: "1px solid gray" }}
|
|
158
|
+
visible={false}
|
|
159
|
+
marginTop={8}
|
|
160
|
+
href="/existing"
|
|
161
|
+
>
|
|
162
|
+
Existing Style
|
|
163
|
+
</Link>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
`
|
|
167
|
+
|
|
168
|
+
const result = applyTransform(source)
|
|
169
|
+
|
|
170
|
+
expect(result).toContain("style={{")
|
|
171
|
+
expect(result).toContain('display: "none"') // from visible={false}
|
|
172
|
+
expect(result).not.toContain("visible={false}")
|
|
173
|
+
expect(result).not.toContain("marginTop={8}")
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe("no style props", () => {
|
|
178
|
+
it("should return null when no style props are present", () => {
|
|
179
|
+
const source = `
|
|
180
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
181
|
+
|
|
182
|
+
export function TestComponent() {
|
|
183
|
+
return <Link href="/plain" external>Plain Link</Link>
|
|
184
|
+
}
|
|
185
|
+
`
|
|
186
|
+
|
|
187
|
+
const result = applyTransform(source)
|
|
188
|
+
|
|
189
|
+
expect(result).toBeNull()
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe("variable expressions", () => {
|
|
194
|
+
it("should handle variable expressions in style props", () => {
|
|
195
|
+
const source = `
|
|
196
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
197
|
+
|
|
198
|
+
export function TestComponent() {
|
|
199
|
+
const isVisible = true
|
|
200
|
+
const linkColor = "purple"
|
|
201
|
+
return (
|
|
202
|
+
<Link
|
|
203
|
+
visible={isVisible}
|
|
204
|
+
color={linkColor}
|
|
205
|
+
href="/variables"
|
|
206
|
+
>
|
|
207
|
+
Variable Link
|
|
208
|
+
</Link>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
`
|
|
212
|
+
|
|
213
|
+
const result = applyTransform(source)
|
|
214
|
+
|
|
215
|
+
expect(result).toContain("style={{")
|
|
216
|
+
expect(result).toContain("color: linkColor")
|
|
217
|
+
expect(result).not.toContain("visible={isVisible}")
|
|
218
|
+
expect(result).not.toContain("color={linkColor}")
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe("import handling", () => {
|
|
223
|
+
it("should not affect imports", () => {
|
|
224
|
+
const source = `
|
|
225
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
226
|
+
|
|
227
|
+
export function TestComponent() {
|
|
228
|
+
return <Link visible={false} href="/test">Test</Link>
|
|
229
|
+
}
|
|
230
|
+
`
|
|
231
|
+
|
|
232
|
+
const result = applyTransform(source)
|
|
233
|
+
|
|
234
|
+
expect(result).toContain(
|
|
235
|
+
'import { Link } from "@planningcenter/tapestry-react"'
|
|
236
|
+
)
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { stylePropTransformFactory } from "../../shared/transformFactories/stylePropTransformFactory"
|
|
2
|
+
|
|
3
|
+
export default stylePropTransformFactory({
|
|
4
|
+
stylesToKeep: ["visible"],
|
|
5
|
+
stylesToRemove: [],
|
|
6
|
+
targetComponent: "Link",
|
|
7
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
8
|
+
})
|
|
@@ -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
|
+
})
|
|
@@ -319,7 +319,10 @@ export function stylePropTransformFactory(config: {
|
|
|
319
319
|
styles = { ...styles, ...directStyleProps }
|
|
320
320
|
if (options.verbose) console.log("Final generated styles:", styles)
|
|
321
321
|
|
|
322
|
-
|
|
322
|
+
// Only apply styles if there are actual CSS properties to add
|
|
323
|
+
if (Object.keys(styles).length > 0) {
|
|
324
|
+
applyStylesToComponent({ element, j, styles })
|
|
325
|
+
}
|
|
323
326
|
} catch (error) {
|
|
324
327
|
console.log("Error processing style props:", error)
|
|
325
328
|
console.log("Style props that caused error:", allStyleProps)
|