@planningcenter/tapestry-migration-cli 2.4.0-rc.0 → 2.4.0-rc.10
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/button/transforms/childrenToLabel.test.ts +5 -4
- package/src/components/button/transforms/childrenToLabel.ts +9 -39
- package/src/components/button/transforms/unsupportedProps.ts +7 -31
- package/src/components/link/index.ts +14 -2
- package/src/components/link/transforms/childrenToLabel.test.ts +331 -0
- package/src/components/link/transforms/childrenToLabel.ts +54 -0
- package/src/components/link/transforms/convertStyleProps.test.ts +391 -0
- package/src/components/link/transforms/convertStyleProps.ts +10 -0
- package/src/components/link/transforms/{inlineToKind.test.ts → inlineMemberToKind.test.ts} +2 -2
- package/src/components/link/transforms/{inlineToKind.ts → inlineMemberToKind.ts} +0 -2
- package/src/components/link/transforms/inlinePropToKind.test.ts +312 -0
- package/src/components/link/transforms/inlinePropToKind.ts +24 -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/link/transforms/targetBlankToExternal.test.ts +14 -0
- package/src/components/link/transforms/unsupportedProps.test.ts +265 -0
- package/src/components/link/transforms/unsupportedProps.ts +58 -0
- package/src/components/shared/conditions/hasAttributeValue.test.ts +22 -1
- package/src/components/shared/conditions/hasAttributeValue.ts +24 -6
- package/src/components/shared/helpers/childrenToLabelHelpers.ts +43 -0
- package/src/components/shared/helpers/unsupportedPropsHelpers.ts +35 -0
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +5 -1
- package/src/stubs/textPlugin.ts +45 -0
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { JSXAttribute, Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addCommentToUnsupportedProps } from "../../shared/actions/addCommentToUnsupportedProps"
|
|
4
|
+
import { SUPPORTED_PROPS_BASE } from "../../shared/helpers/unsupportedPropsHelpers"
|
|
5
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
6
|
+
|
|
7
|
+
// Note: 'target' and 'rel' are NOT included because they are handled by targetBlankToExternal transform
|
|
8
|
+
const LINK_SPECIFIC_PROPS = [
|
|
9
|
+
"download",
|
|
10
|
+
"external",
|
|
11
|
+
"href",
|
|
12
|
+
"hrefLang",
|
|
13
|
+
"media",
|
|
14
|
+
"ping",
|
|
15
|
+
"referrerPolicy",
|
|
16
|
+
"type",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
const SUPPORTED_PROPS = [...SUPPORTED_PROPS_BASE, ...LINK_SPECIFIC_PROPS]
|
|
20
|
+
|
|
21
|
+
const transform: Transform = attributeTransformFactory({
|
|
22
|
+
targetComponent: "Link",
|
|
23
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
24
|
+
transform: (element, { j }) => {
|
|
25
|
+
const UNSUPPORTED_PROPS = (element.openingElement.attributes || [])
|
|
26
|
+
.filter(
|
|
27
|
+
(attr) =>
|
|
28
|
+
attr.type === "JSXAttribute" &&
|
|
29
|
+
!SUPPORTED_PROPS.includes(attr.name.name as string) &&
|
|
30
|
+
!(attr.name.name as string).startsWith("aria-") &&
|
|
31
|
+
!(attr.name.name as string).startsWith("data-")
|
|
32
|
+
)
|
|
33
|
+
.map((attr) => (attr as JSXAttribute).name.name as string)
|
|
34
|
+
|
|
35
|
+
return addCommentToUnsupportedProps({
|
|
36
|
+
element,
|
|
37
|
+
j,
|
|
38
|
+
messageSuffix: (prop) => {
|
|
39
|
+
if (prop === "css") {
|
|
40
|
+
return "\n * Use 'className' prop with CSS classes instead of the css prop.\n"
|
|
41
|
+
}
|
|
42
|
+
if (prop === "disabled") {
|
|
43
|
+
return "\n * Links do not support the disabled prop. Consider using a button, hiding the link, or using CSS to style it as disabled.\n"
|
|
44
|
+
}
|
|
45
|
+
if (prop === "mediaQueries") {
|
|
46
|
+
return "\n * It is recommended to use CSS media queries in a class that you apply to the component.\n"
|
|
47
|
+
}
|
|
48
|
+
if (prop === "hover" || prop === "focus" || prop === "active") {
|
|
49
|
+
return "\n * State-based styles (hover, focus, active) should be handled with CSS class selectors.\n"
|
|
50
|
+
}
|
|
51
|
+
return ""
|
|
52
|
+
},
|
|
53
|
+
props: UNSUPPORTED_PROPS,
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export default transform
|
|
@@ -32,7 +32,28 @@ describe("hasAttributeValue", () => {
|
|
|
32
32
|
expect(condition(element)).toBe(false)
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
it("should return
|
|
35
|
+
it("should return true for expression with string literal", () => {
|
|
36
|
+
const condition = hasAttributeValue("target", "_blank")
|
|
37
|
+
const element = createJSXElement(" target={'_blank'}")
|
|
38
|
+
|
|
39
|
+
expect(condition(element)).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("should return true for expression with double quotes", () => {
|
|
43
|
+
const condition = hasAttributeValue("target", "_blank")
|
|
44
|
+
const element = createJSXElement(' target={"_blank"}')
|
|
45
|
+
|
|
46
|
+
expect(condition(element)).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("should return false for expression with different value", () => {
|
|
50
|
+
const condition = hasAttributeValue("target", "_blank")
|
|
51
|
+
const element = createJSXElement(" target={'_self'}")
|
|
52
|
+
|
|
53
|
+
expect(condition(element)).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("should return false for complex expression values", () => {
|
|
36
57
|
const condition = hasAttributeValue("onClick", "handleClick")
|
|
37
58
|
const element = createJSXElement(" onClick={handleClick}")
|
|
38
59
|
|
|
@@ -4,6 +4,7 @@ import { TransformCondition } from "../types"
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Helper function to create a condition that checks for an attribute with a specific value
|
|
7
|
+
* Handles both string literals and expressions with string literals
|
|
7
8
|
*/
|
|
8
9
|
export function hasAttributeValue(
|
|
9
10
|
attributeName: string,
|
|
@@ -11,13 +12,30 @@ export function hasAttributeValue(
|
|
|
11
12
|
): TransformCondition {
|
|
12
13
|
return (element: JSXElement) => {
|
|
13
14
|
const attributes = element.openingElement.attributes || []
|
|
14
|
-
return attributes.some(
|
|
15
|
-
|
|
15
|
+
return attributes.some((attr) => {
|
|
16
|
+
const hasAttribute =
|
|
16
17
|
attr.type === "JSXAttribute" &&
|
|
17
18
|
attr.name?.type === "JSXIdentifier" &&
|
|
18
|
-
attr.name.name === attributeName
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
attr.name.name === attributeName
|
|
20
|
+
|
|
21
|
+
if (!hasAttribute) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Handle string literal: attribute="value"
|
|
26
|
+
if (attr.value?.type === "StringLiteral") {
|
|
27
|
+
return attr.value.value === value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Handle expression: attribute={"value"} or attribute={'value'}
|
|
31
|
+
if (attr.value?.type === "JSXExpressionContainer") {
|
|
32
|
+
const { expression } = attr.value
|
|
33
|
+
if (expression.type === "StringLiteral") {
|
|
34
|
+
return expression.value === value
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false
|
|
39
|
+
})
|
|
22
40
|
}
|
|
23
41
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { JSXElement } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
export function extractTextContent(
|
|
4
|
+
children: NonNullable<JSXElement["children"]>
|
|
5
|
+
): {
|
|
6
|
+
isSimpleText: boolean
|
|
7
|
+
textContent: string
|
|
8
|
+
} {
|
|
9
|
+
let textContent = ""
|
|
10
|
+
|
|
11
|
+
for (const child of children) {
|
|
12
|
+
if (child.type === "JSXText") {
|
|
13
|
+
const text = child.value.trim()
|
|
14
|
+
if (text) textContent += text
|
|
15
|
+
} else if (
|
|
16
|
+
child.type === "JSXExpressionContainer" &&
|
|
17
|
+
child.expression.type === "StringLiteral"
|
|
18
|
+
) {
|
|
19
|
+
textContent += child.expression.value
|
|
20
|
+
} else if (
|
|
21
|
+
child.type === "JSXExpressionContainer" &&
|
|
22
|
+
child.expression.type === "TemplateLiteral" &&
|
|
23
|
+
child.expression.expressions.length === 0
|
|
24
|
+
) {
|
|
25
|
+
textContent += child.expression.quasis[0].value.raw
|
|
26
|
+
} else {
|
|
27
|
+
return { isSimpleText: false, textContent: "" }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { isSimpleText: true, textContent }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildComment(
|
|
35
|
+
message: string,
|
|
36
|
+
includeIconGuidance: boolean
|
|
37
|
+
): string {
|
|
38
|
+
const baseMessage = `${message} - take time to find the right text for the component.`
|
|
39
|
+
if (includeIconGuidance) {
|
|
40
|
+
return `${baseMessage} If icons are used in the component, you can use prefix and suffix to correctly display those icons.`
|
|
41
|
+
}
|
|
42
|
+
return baseMessage
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { stylePropNames } from "../../../../dist/tapestry-react-shim.cjs"
|
|
2
|
+
|
|
3
|
+
export const STYLE_PROP_NAMES_WITHOUT_CSS = stylePropNames.filter(
|
|
4
|
+
(prop: string) => prop !== "css"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
export const COMMON_PROPS = [
|
|
8
|
+
"className",
|
|
9
|
+
"id",
|
|
10
|
+
"key",
|
|
11
|
+
"kind",
|
|
12
|
+
"label",
|
|
13
|
+
"onBlur",
|
|
14
|
+
"onFocus",
|
|
15
|
+
"onClick",
|
|
16
|
+
"onKeyDown",
|
|
17
|
+
"onKeyUp",
|
|
18
|
+
"onMouseDown",
|
|
19
|
+
"onMouseOut",
|
|
20
|
+
"onMouseOver",
|
|
21
|
+
"onMouseUp",
|
|
22
|
+
"prefix",
|
|
23
|
+
"ref",
|
|
24
|
+
"role",
|
|
25
|
+
"size",
|
|
26
|
+
"style",
|
|
27
|
+
"suffix",
|
|
28
|
+
"tabIndex",
|
|
29
|
+
"title",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
export const SUPPORTED_PROPS_BASE = [
|
|
33
|
+
...STYLE_PROP_NAMES_WITHOUT_CSS,
|
|
34
|
+
...COMMON_PROPS,
|
|
35
|
+
]
|