@planningcenter/tapestry-migration-cli 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/package.json +3 -3
- package/src/availableComponents.ts +1 -0
- package/src/components/button/transforms/tooltipToWrapper.test.ts +50 -3
- package/src/components/button/transforms/tooltipToWrapper.ts +18 -0
- package/src/components/flex/index.test.ts +140 -0
- package/src/components/flex/index.ts +40 -0
- package/src/components/flex/transforms/alignmentToAlign.test.ts +185 -0
- package/src/components/flex/transforms/alignmentToAlign.ts +73 -0
- package/src/components/flex/transforms/axisToDirection.test.ts +140 -0
- package/src/components/flex/transforms/axisToDirection.ts +51 -0
- package/src/components/flex/transforms/distributionToJustify.test.ts +214 -0
- package/src/components/flex/transforms/distributionToJustify.ts +76 -0
- package/src/components/flex/transforms/innerRefToRef.test.ts +100 -0
- package/src/components/flex/transforms/innerRefToRef.ts +14 -0
- package/src/components/flex/transforms/moveFlexImport.test.ts +202 -0
- package/src/components/flex/transforms/moveFlexImport.ts +14 -0
- package/src/components/flex/transforms/setDefaultAxis.test.ts +114 -0
- package/src/components/flex/transforms/setDefaultAxis.ts +29 -0
- package/src/components/flex/transforms/spacingToGap.test.ts +176 -0
- package/src/components/flex/transforms/spacingToGap.ts +49 -0
- package/src/components/select/index.ts +2 -0
- package/src/components/select/transforms/onChangeSignature.test.ts +224 -0
- package/src/components/select/transforms/onChangeSignature.ts +172 -0
- package/src/components/shared/actions/resolveConstantAttributeValue.test.ts +122 -0
- package/src/components/shared/actions/resolveConstantAttributeValue.ts +31 -0
- package/src/components/toggle-switch/index.ts +2 -0
- package/src/components/toggle-switch/transforms/onClickToOnChange.test.ts +210 -0
- package/src/components/toggle-switch/transforms/onClickToOnChange.ts +71 -0
- package/src/index.test.ts +79 -0
- package/src/index.ts +29 -0
- package/src/migrationList.ts +22 -0
- package/src/utils/componentLabel.test.ts +61 -0
- package/src/utils/componentLabel.ts +15 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./onChangeSignature"
|
|
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("onChangeSignature transform", () => {
|
|
19
|
+
it("auto-rewrites ({ value }) => expr to (e) => e.target.value", () => {
|
|
20
|
+
const input = `
|
|
21
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
function Test() {
|
|
24
|
+
return <Select onChange={({ value }) => setX(value)} emptyValue="Pick" />
|
|
25
|
+
}
|
|
26
|
+
`.trim()
|
|
27
|
+
|
|
28
|
+
const result = applyTransform(input)
|
|
29
|
+
// jscodeshift omits parens for a single identifier param: `e =>` not `(e) =>`
|
|
30
|
+
expect(result).toContain("e =>")
|
|
31
|
+
expect(result).toContain("e.target.value")
|
|
32
|
+
expect(result).not.toContain("({ value })")
|
|
33
|
+
expect(result).not.toContain("TODO")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("replaces all value references in the expression body", () => {
|
|
37
|
+
const input = `
|
|
38
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
39
|
+
|
|
40
|
+
function Test() {
|
|
41
|
+
return <Select onChange={({ value }) => fn(value, value)} emptyValue="Pick" />
|
|
42
|
+
}
|
|
43
|
+
`.trim()
|
|
44
|
+
|
|
45
|
+
const result = applyTransform(input)
|
|
46
|
+
expect(result).toContain("fn(e.target.value, e.target.value)")
|
|
47
|
+
expect(result).not.toContain("({ value })")
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("adds a TODO comment for a function reference", () => {
|
|
51
|
+
const input = `
|
|
52
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
53
|
+
|
|
54
|
+
function Test() {
|
|
55
|
+
return <Select onChange={handler} emptyValue="Pick" />
|
|
56
|
+
}
|
|
57
|
+
`.trim()
|
|
58
|
+
|
|
59
|
+
const result = applyTransform(input)
|
|
60
|
+
expect(result).toContain("onChange={handler}")
|
|
61
|
+
expect(result).toContain("TODO: tapestry-migration (onChange)")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("adds a TODO comment when ({ selectedValue }) is destructured", () => {
|
|
65
|
+
const input = `
|
|
66
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
67
|
+
|
|
68
|
+
function Test() {
|
|
69
|
+
return <Select onChange={({ selectedValue }) => setX(selectedValue)} emptyValue="Pick" />
|
|
70
|
+
}
|
|
71
|
+
`.trim()
|
|
72
|
+
|
|
73
|
+
const result = applyTransform(input)
|
|
74
|
+
expect(result).toContain("({ selectedValue })")
|
|
75
|
+
expect(result).toContain("TODO: tapestry-migration (onChange)")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("adds a TODO comment for a block-body arrow (too complex to auto-rewrite)", () => {
|
|
79
|
+
const input = `
|
|
80
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
81
|
+
|
|
82
|
+
function Test() {
|
|
83
|
+
return <Select onChange={({ value }) => { setX(value) }} emptyValue="Pick" />
|
|
84
|
+
}
|
|
85
|
+
`.trim()
|
|
86
|
+
|
|
87
|
+
const result = applyTransform(input)
|
|
88
|
+
expect(result).toContain("{ setX(value) }")
|
|
89
|
+
expect(result).toContain("TODO: tapestry-migration (onChange)")
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("expands object-literal shorthand return ({ value }) to ({ value: e.target.value })", () => {
|
|
93
|
+
const input = `
|
|
94
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
95
|
+
|
|
96
|
+
function Test() {
|
|
97
|
+
return <Select onChange={({ value }) => ({ value })} emptyValue="Pick" />
|
|
98
|
+
}
|
|
99
|
+
`.trim()
|
|
100
|
+
|
|
101
|
+
const result = applyTransform(input)
|
|
102
|
+
expect(result).toContain("value: e.target.value")
|
|
103
|
+
expect(result).not.toContain("TODO")
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("leaves member-property `value` access untouched (obj.value and obj?.value)", () => {
|
|
107
|
+
const dot = `
|
|
108
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
109
|
+
|
|
110
|
+
function Test() {
|
|
111
|
+
return <Select onChange={({ value }) => setX(obj.value)} emptyValue="Pick" />
|
|
112
|
+
}
|
|
113
|
+
`.trim()
|
|
114
|
+
const dotResult = applyTransform(dot)
|
|
115
|
+
expect(dotResult).toContain("setX(obj.value)")
|
|
116
|
+
expect(dotResult).not.toContain("e.target.value")
|
|
117
|
+
|
|
118
|
+
const optional = `
|
|
119
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
120
|
+
|
|
121
|
+
function Test() {
|
|
122
|
+
return <Select onChange={({ value }) => setX(obj?.value)} emptyValue="Pick" />
|
|
123
|
+
}
|
|
124
|
+
`.trim()
|
|
125
|
+
const optionalResult = applyTransform(optional)
|
|
126
|
+
expect(optionalResult).toContain("obj?.value")
|
|
127
|
+
expect(optionalResult).not.toContain("obj?.e.target.value")
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it("adds a TODO when a nested callback shadows `value` (would corrupt the binding)", () => {
|
|
131
|
+
const input = `
|
|
132
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
133
|
+
|
|
134
|
+
function Test() {
|
|
135
|
+
return <Select onChange={({ value }) => list.map(value => value)} emptyValue="Pick" />
|
|
136
|
+
}
|
|
137
|
+
`.trim()
|
|
138
|
+
|
|
139
|
+
const result = applyTransform(input)
|
|
140
|
+
expect(result).toContain("list.map(value => value)")
|
|
141
|
+
expect(result).toContain("TODO: tapestry-migration (onChange)")
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it("adds a TODO when a nested callback destructures its own `value`", () => {
|
|
145
|
+
const input = `
|
|
146
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
147
|
+
|
|
148
|
+
function Test() {
|
|
149
|
+
return <Select onChange={({ value }) => list.map(({ value }) => value)} emptyValue="Pick" />
|
|
150
|
+
}
|
|
151
|
+
`.trim()
|
|
152
|
+
|
|
153
|
+
const result = applyTransform(input)
|
|
154
|
+
expect(result).toContain("TODO: tapestry-migration (onChange)")
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it("rewrites references inside a nested callback that does NOT shadow `value`", () => {
|
|
158
|
+
const input = `
|
|
159
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
160
|
+
|
|
161
|
+
function Test() {
|
|
162
|
+
return <Select onChange={({ value }) => arr.filter(x => x === value)} emptyValue="Pick" />
|
|
163
|
+
}
|
|
164
|
+
`.trim()
|
|
165
|
+
|
|
166
|
+
const result = applyTransform(input)
|
|
167
|
+
expect(result).toContain("x === e.target.value")
|
|
168
|
+
expect(result).not.toContain("TODO")
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("does not transform Select with no onChange", () => {
|
|
172
|
+
const input = `
|
|
173
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
174
|
+
|
|
175
|
+
function Test() {
|
|
176
|
+
return <Select emptyValue="Pick" />
|
|
177
|
+
}
|
|
178
|
+
`.trim()
|
|
179
|
+
|
|
180
|
+
const result = applyTransform(input)
|
|
181
|
+
expect(result).toBe(input)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it("does not transform other components", () => {
|
|
185
|
+
const input = `
|
|
186
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
187
|
+
|
|
188
|
+
function Test() {
|
|
189
|
+
return <Button onChange={({ value }) => setX(value)}>Click</Button>
|
|
190
|
+
}
|
|
191
|
+
`.trim()
|
|
192
|
+
|
|
193
|
+
const result = applyTransform(input)
|
|
194
|
+
expect(result).toBe(input)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it("does not transform a multiple Select (skipped by transformableSelect)", () => {
|
|
198
|
+
const input = `
|
|
199
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
200
|
+
|
|
201
|
+
function Test() {
|
|
202
|
+
return <Select multiple onChange={({ value }) => setX(value)} emptyValue="Pick" />
|
|
203
|
+
}
|
|
204
|
+
`.trim()
|
|
205
|
+
|
|
206
|
+
const result = applyTransform(input)
|
|
207
|
+
expect(result).toBe(input)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it("preserves other props when auto-rewriting", () => {
|
|
211
|
+
const input = `
|
|
212
|
+
import { Select } from "@planningcenter/tapestry-react"
|
|
213
|
+
|
|
214
|
+
function Test() {
|
|
215
|
+
return <Select disabled emptyValue="Pick" onChange={({ value }) => setX(value)} />
|
|
216
|
+
}
|
|
217
|
+
`.trim()
|
|
218
|
+
|
|
219
|
+
const result = applyTransform(input)
|
|
220
|
+
expect(result).toContain("disabled")
|
|
221
|
+
expect(result).toContain("emptyValue")
|
|
222
|
+
expect(result).toContain("e.target.value")
|
|
223
|
+
})
|
|
224
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
|
|
4
|
+
import { andConditions } from "../../shared/conditions/andConditions"
|
|
5
|
+
import { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
6
|
+
import { findAttribute } from "../../shared/findAttribute"
|
|
7
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
8
|
+
import { transformableSelect } from "../transformableSelect"
|
|
9
|
+
|
|
10
|
+
const TODO_TEXT =
|
|
11
|
+
"onChange now receives a React.ChangeEvent<HTMLSelectElement>; read the value via e.target.value instead of the old { selectedValue, value } argument"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns true if a parameter pattern mentions a `value` identifier, e.g.
|
|
15
|
+
* `value`, `{ value }`, `[value]`. Any occurrence is treated conservatively as
|
|
16
|
+
* a potential binding so we bail to a TODO rather than risk corrupting it.
|
|
17
|
+
*/
|
|
18
|
+
function paramBindsValue(
|
|
19
|
+
param: import("jscodeshift").ASTNode,
|
|
20
|
+
j: import("jscodeshift").JSCodeshift
|
|
21
|
+
): boolean {
|
|
22
|
+
// `find` only searches descendants, so check the param node itself first
|
|
23
|
+
// (a bare `value` param) before scanning destructuring patterns.
|
|
24
|
+
if (param.type === "Identifier" && param.name === "value") return true
|
|
25
|
+
return j(param).find(j.Identifier, { name: "value" }).size() > 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns true if a nested function/arrow inside the body binds its own `value`
|
|
30
|
+
* parameter, which would shadow the destructured param. Rewriting these would
|
|
31
|
+
* corrupt the inner binding, so they fall through to a TODO comment instead.
|
|
32
|
+
*
|
|
33
|
+
* e.g. `({ value }) => list.map(value => value)` → ambiguous, leave for the dev
|
|
34
|
+
*/
|
|
35
|
+
function bodyShadowsValue(
|
|
36
|
+
body: import("jscodeshift").ASTNode,
|
|
37
|
+
j: import("jscodeshift").JSCodeshift
|
|
38
|
+
): boolean {
|
|
39
|
+
let shadows = false
|
|
40
|
+
const visit = (collection: ReturnType<typeof j>) =>
|
|
41
|
+
collection.forEach((path) => {
|
|
42
|
+
if (
|
|
43
|
+
path.node.params.some((p: import("jscodeshift").ASTNode) =>
|
|
44
|
+
paramBindsValue(p, j)
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
shadows = true
|
|
48
|
+
})
|
|
49
|
+
visit(j(body).find(j.ArrowFunctionExpression))
|
|
50
|
+
visit(j(body).find(j.FunctionExpression))
|
|
51
|
+
return shadows
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns true if the onChange attribute is an expression-bodied arrow function
|
|
56
|
+
* with exactly one `{ value }` destructuring param and no other keys.
|
|
57
|
+
*
|
|
58
|
+
* e.g. `onChange={({ value }) => setX(value)}` → auto-migratable
|
|
59
|
+
* e.g. `onChange={handler}` or `onChange={({ selectedValue }) => …}` → TODO comment
|
|
60
|
+
*/
|
|
61
|
+
function isSimpleValueDestructure(
|
|
62
|
+
attribute: ReturnType<typeof findAttribute>,
|
|
63
|
+
j: import("jscodeshift").JSCodeshift
|
|
64
|
+
): boolean {
|
|
65
|
+
if (!attribute || attribute.type !== "JSXAttribute") return false
|
|
66
|
+
|
|
67
|
+
const container = attribute.value
|
|
68
|
+
if (!container || container.type !== "JSXExpressionContainer") return false
|
|
69
|
+
|
|
70
|
+
const arrow = container.expression
|
|
71
|
+
if (!j.ArrowFunctionExpression.check(arrow)) return false
|
|
72
|
+
|
|
73
|
+
// Block bodies are too complex to auto-rewrite safely
|
|
74
|
+
if (arrow.body.type === "BlockStatement") return false
|
|
75
|
+
|
|
76
|
+
const params = arrow.params
|
|
77
|
+
if (params.length !== 1) return false
|
|
78
|
+
|
|
79
|
+
const param = params[0]
|
|
80
|
+
if (param.type !== "ObjectPattern") return false
|
|
81
|
+
|
|
82
|
+
const props = param.properties
|
|
83
|
+
if (props.length !== 1) return false
|
|
84
|
+
|
|
85
|
+
const prop = props[0]
|
|
86
|
+
if (prop.type !== "ObjectProperty" && prop.type !== "Property") return false
|
|
87
|
+
if (!("shorthand" in prop) || !prop.shorthand) return false
|
|
88
|
+
if (prop.key.type !== "Identifier" || prop.key.name !== "value") return false
|
|
89
|
+
|
|
90
|
+
// A nested function that rebinds `value` would be corrupted by the rewrite.
|
|
91
|
+
if (bodyShadowsValue(arrow.body, j)) return false
|
|
92
|
+
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const transform: Transform = attributeTransformFactory({
|
|
97
|
+
condition: andConditions(transformableSelect, hasAttribute("onChange")),
|
|
98
|
+
targetComponent: "Select",
|
|
99
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
100
|
+
transform: (element, { j }) => {
|
|
101
|
+
const attributes = element.openingElement.attributes || []
|
|
102
|
+
const attribute = findAttribute(attributes, "onChange")
|
|
103
|
+
if (!attribute) return false
|
|
104
|
+
|
|
105
|
+
if (isSimpleValueDestructure(attribute, j)) {
|
|
106
|
+
// Safe to auto-rewrite: ({ value }) => expr → (e) => expr[value→e.target.value]
|
|
107
|
+
const container =
|
|
108
|
+
attribute.value as import("jscodeshift").JSXExpressionContainer
|
|
109
|
+
const arrow =
|
|
110
|
+
container.expression as import("jscodeshift").ArrowFunctionExpression
|
|
111
|
+
|
|
112
|
+
// Replace the param with identifier `e`
|
|
113
|
+
arrow.params = [j.identifier("e")]
|
|
114
|
+
|
|
115
|
+
// Replace all `value` identifier references in the expression body
|
|
116
|
+
// (skip object key / member-property positions to avoid false positives)
|
|
117
|
+
const body = arrow.body as import("jscodeshift").Expression
|
|
118
|
+
j(body)
|
|
119
|
+
.find(j.Identifier, { name: "value" })
|
|
120
|
+
.filter((path) => {
|
|
121
|
+
const parent = path.parent.node
|
|
122
|
+
// Skip member-property positions (`obj.value`, `obj?.value`) — these
|
|
123
|
+
// are property names, not references to the destructured param.
|
|
124
|
+
if (
|
|
125
|
+
(parent.type === "MemberExpression" ||
|
|
126
|
+
parent.type === "OptionalMemberExpression") &&
|
|
127
|
+
parent.property === path.node &&
|
|
128
|
+
!parent.computed
|
|
129
|
+
) {
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
// Skip object property keys (`{ value: … }`)
|
|
133
|
+
if (
|
|
134
|
+
(parent.type === "Property" || parent.type === "ObjectProperty") &&
|
|
135
|
+
parent.key === path.node &&
|
|
136
|
+
!parent.computed
|
|
137
|
+
) {
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
return true
|
|
141
|
+
})
|
|
142
|
+
.replaceWith((path) => {
|
|
143
|
+
// Object shorthand (`{ value }`) must be expanded to `{ value: … }`,
|
|
144
|
+
// otherwise the printed property loses its value.
|
|
145
|
+
const parent = path.parent.node
|
|
146
|
+
if (
|
|
147
|
+
(parent.type === "Property" || parent.type === "ObjectProperty") &&
|
|
148
|
+
"shorthand" in parent
|
|
149
|
+
) {
|
|
150
|
+
parent.shorthand = false
|
|
151
|
+
}
|
|
152
|
+
return j.memberExpression(
|
|
153
|
+
j.memberExpression(j.identifier("e"), j.identifier("target")),
|
|
154
|
+
j.identifier("value")
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Not auto-migratable — add a TODO comment so the developer handles it
|
|
162
|
+
addCommentToAttribute({
|
|
163
|
+
attribute,
|
|
164
|
+
commentKind: "todo",
|
|
165
|
+
j,
|
|
166
|
+
text: TODO_TEXT,
|
|
167
|
+
})
|
|
168
|
+
return true
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
export default transform
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import jscodeshift, {
|
|
2
|
+
JSXAttribute,
|
|
3
|
+
JSXElement,
|
|
4
|
+
JSXIdentifier,
|
|
5
|
+
} from "jscodeshift"
|
|
6
|
+
import { describe, expect, it } from "vitest"
|
|
7
|
+
|
|
8
|
+
import { resolveConstantAttributeValue } from "./resolveConstantAttributeValue"
|
|
9
|
+
|
|
10
|
+
const j = jscodeshift.withParser("tsx")
|
|
11
|
+
|
|
12
|
+
const CONSTANT_MAP: Record<string, string> = {
|
|
13
|
+
CENTER: "center",
|
|
14
|
+
FILL: "fill",
|
|
15
|
+
START: "start",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createElementFromCode(code: string): JSXElement {
|
|
19
|
+
const source = j(`<div>${code}</div>`)
|
|
20
|
+
return source.find(j.JSXElement).at(0).get().value.children?.[0] as JSXElement
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getAttributeFromElement(
|
|
24
|
+
element: JSXElement,
|
|
25
|
+
name: string
|
|
26
|
+
): JSXAttribute {
|
|
27
|
+
const attributes = element.openingElement.attributes || []
|
|
28
|
+
return attributes.find(
|
|
29
|
+
(attr) =>
|
|
30
|
+
attr.type === "JSXAttribute" &&
|
|
31
|
+
(attr.name as JSXIdentifier)?.name === name
|
|
32
|
+
) as JSXAttribute
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("resolveConstantAttributeValue", () => {
|
|
36
|
+
it("resolves a string literal", () => {
|
|
37
|
+
const element = createElementFromCode('<StackView distribution="center" />')
|
|
38
|
+
const attr = getAttributeFromElement(element, "distribution")
|
|
39
|
+
expect(
|
|
40
|
+
resolveConstantAttributeValue({
|
|
41
|
+
attribute: attr,
|
|
42
|
+
constantMap: CONSTANT_MAP,
|
|
43
|
+
})
|
|
44
|
+
).toBe("center")
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("resolves an expression-wrapped string literal", () => {
|
|
48
|
+
const element = createElementFromCode(
|
|
49
|
+
'<StackView distribution={"center"} />'
|
|
50
|
+
)
|
|
51
|
+
const attr = getAttributeFromElement(element, "distribution")
|
|
52
|
+
expect(
|
|
53
|
+
resolveConstantAttributeValue({
|
|
54
|
+
attribute: attr,
|
|
55
|
+
constantMap: CONSTANT_MAP,
|
|
56
|
+
})
|
|
57
|
+
).toBe("center")
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("resolves a MemberExpression constant present in the map", () => {
|
|
61
|
+
const element = createElementFromCode(
|
|
62
|
+
"<StackView distribution={StackView.CENTER} />"
|
|
63
|
+
)
|
|
64
|
+
const attr = getAttributeFromElement(element, "distribution")
|
|
65
|
+
expect(
|
|
66
|
+
resolveConstantAttributeValue({
|
|
67
|
+
attribute: attr,
|
|
68
|
+
constantMap: CONSTANT_MAP,
|
|
69
|
+
})
|
|
70
|
+
).toBe("center")
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("resolves a MemberExpression constant that maps to fill", () => {
|
|
74
|
+
const element = createElementFromCode(
|
|
75
|
+
"<StackView distribution={StackView.FILL} />"
|
|
76
|
+
)
|
|
77
|
+
const attr = getAttributeFromElement(element, "distribution")
|
|
78
|
+
expect(
|
|
79
|
+
resolveConstantAttributeValue({
|
|
80
|
+
attribute: attr,
|
|
81
|
+
constantMap: CONSTANT_MAP,
|
|
82
|
+
})
|
|
83
|
+
).toBe("fill")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("returns null for a MemberExpression constant not in the map", () => {
|
|
87
|
+
const element = createElementFromCode(
|
|
88
|
+
"<StackView distribution={StackView.UNKNOWN} />"
|
|
89
|
+
)
|
|
90
|
+
const attr = getAttributeFromElement(element, "distribution")
|
|
91
|
+
expect(
|
|
92
|
+
resolveConstantAttributeValue({
|
|
93
|
+
attribute: attr,
|
|
94
|
+
constantMap: CONSTANT_MAP,
|
|
95
|
+
})
|
|
96
|
+
).toBeNull()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("returns null for a dynamic variable", () => {
|
|
100
|
+
const element = createElementFromCode(
|
|
101
|
+
"<StackView distribution={someVar} />"
|
|
102
|
+
)
|
|
103
|
+
const attr = getAttributeFromElement(element, "distribution")
|
|
104
|
+
expect(
|
|
105
|
+
resolveConstantAttributeValue({
|
|
106
|
+
attribute: attr,
|
|
107
|
+
constantMap: CONSTANT_MAP,
|
|
108
|
+
})
|
|
109
|
+
).toBeNull()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("returns null for a boolean (valueless) attribute", () => {
|
|
113
|
+
const element = createElementFromCode("<StackView distribution />")
|
|
114
|
+
const attr = getAttributeFromElement(element, "distribution")
|
|
115
|
+
expect(
|
|
116
|
+
resolveConstantAttributeValue({
|
|
117
|
+
attribute: attr,
|
|
118
|
+
constantMap: CONSTANT_MAP,
|
|
119
|
+
})
|
|
120
|
+
).toBeNull()
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { JSXAttribute } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
export function resolveConstantAttributeValue({
|
|
4
|
+
attribute,
|
|
5
|
+
constantMap,
|
|
6
|
+
}: {
|
|
7
|
+
attribute: JSXAttribute
|
|
8
|
+
constantMap: Record<string, string>
|
|
9
|
+
}): string | null {
|
|
10
|
+
const value = attribute.value
|
|
11
|
+
if (!value) return null
|
|
12
|
+
|
|
13
|
+
if (value.type === "StringLiteral") {
|
|
14
|
+
return value.value
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (value.type === "JSXExpressionContainer") {
|
|
18
|
+
if (value.expression.type === "StringLiteral") {
|
|
19
|
+
return value.expression.value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
value.expression.type === "MemberExpression" &&
|
|
24
|
+
value.expression.property.type === "Identifier"
|
|
25
|
+
) {
|
|
26
|
+
return constantMap[value.expression.property.name] ?? null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
@@ -5,6 +5,7 @@ import auditSpreadProps from "./transforms/auditSpreadProps"
|
|
|
5
5
|
import convertStyleProps from "./transforms/convertStyleProps"
|
|
6
6
|
import isCheckedToChecked from "./transforms/isCheckedToChecked"
|
|
7
7
|
import moveToggleSwitchImport from "./transforms/moveToggleSwitchImport"
|
|
8
|
+
import onClickToOnChange from "./transforms/onClickToOnChange"
|
|
8
9
|
import sizeMapping from "./transforms/sizeMapping"
|
|
9
10
|
import unsupportedProps from "./transforms/unsupportedProps"
|
|
10
11
|
|
|
@@ -17,6 +18,7 @@ const transform: Transform = (fileInfo, api, options) => {
|
|
|
17
18
|
sizeMapping,
|
|
18
19
|
ariaLabelToLabel,
|
|
19
20
|
isCheckedToChecked,
|
|
21
|
+
onClickToOnChange,
|
|
20
22
|
convertStyleProps,
|
|
21
23
|
unsupportedProps,
|
|
22
24
|
moveToggleSwitchImport,
|