@planningcenter/tapestry-migration-cli 3.4.1 → 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 +59 -0
- package/dist/tapestry-react-shim.cjs +5 -1
- package/package.json +3 -3
- package/src/availableComponents.ts +14 -0
- package/src/components/button/transforms/tooltipToWrapper.test.ts +50 -3
- package/src/components/button/transforms/tooltipToWrapper.ts +18 -0
- package/src/components/dropdown/index.test.ts +67 -0
- package/src/components/dropdown/index.ts +4 -0
- package/src/components/dropdown/transforms/placementIdToMenu.test.ts +6 -1
- package/src/components/dropdown/transforms/placementIdToMenu.ts +1 -1
- package/src/components/dropdown/transforms/variantToKind.test.ts +292 -0
- package/src/components/dropdown/transforms/variantToKind.ts +138 -0
- package/src/components/dropdown/transforms/wrapIconTrigger.test.ts +336 -0
- package/src/components/dropdown/transforms/wrapIconTrigger.ts +233 -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 +34 -18
- package/src/migrationList.ts +22 -0
- package/src/utils/componentLabel.test.ts +61 -0
- package/src/utils/componentLabel.ts +15 -0
|
@@ -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,
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./onClickToOnChange"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function applyTransform(source: string, verbose = false): string {
|
|
9
|
+
const fileInfo = { path: "test.tsx", source }
|
|
10
|
+
const result = transform(
|
|
11
|
+
fileInfo,
|
|
12
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
13
|
+
{ verbose }
|
|
14
|
+
) as string | null
|
|
15
|
+
return result || source
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("onClickToOnChange transform", () => {
|
|
19
|
+
describe("onClick to onChange conversion", () => {
|
|
20
|
+
it("should rename onClick to onChange", () => {
|
|
21
|
+
const input = `
|
|
22
|
+
import { ToggleSwitch } from "@planningcenter/tapestry-react"
|
|
23
|
+
|
|
24
|
+
function Test() {
|
|
25
|
+
return <ToggleSwitch checked={true} onClick={() => setChecked(!checked)} label="Toggle" />
|
|
26
|
+
}
|
|
27
|
+
`.trim()
|
|
28
|
+
|
|
29
|
+
const result = applyTransform(input)
|
|
30
|
+
expect(result).toContain("onChange={() => setChecked(!checked)}")
|
|
31
|
+
expect(result).not.toContain("onClick=")
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("should rename onClick with named function reference", () => {
|
|
35
|
+
const input = `
|
|
36
|
+
import { ToggleSwitch } from "@planningcenter/tapestry-react"
|
|
37
|
+
|
|
38
|
+
function Test() {
|
|
39
|
+
return <ToggleSwitch checked={true} onClick={handleChange} label="Toggle" />
|
|
40
|
+
}
|
|
41
|
+
`.trim()
|
|
42
|
+
|
|
43
|
+
const result = applyTransform(input)
|
|
44
|
+
expect(result).toContain("onChange={handleChange}")
|
|
45
|
+
expect(result).not.toContain("onClick=")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("should handle multiple ToggleSwitch components", () => {
|
|
49
|
+
const input = `
|
|
50
|
+
import { ToggleSwitch } from "@planningcenter/tapestry-react"
|
|
51
|
+
|
|
52
|
+
function Test() {
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<ToggleSwitch checked={true} onClick={handleA} label="First" />
|
|
56
|
+
<ToggleSwitch checked={false} onClick={handleB} label="Second" />
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
`.trim()
|
|
61
|
+
|
|
62
|
+
const result = applyTransform(input)
|
|
63
|
+
expect(result).toContain("onChange={handleA}")
|
|
64
|
+
expect(result).toContain("onChange={handleB}")
|
|
65
|
+
expect(result).not.toContain("onClick=")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("should preserve other props", () => {
|
|
69
|
+
const input = `
|
|
70
|
+
import { ToggleSwitch } from "@planningcenter/tapestry-react"
|
|
71
|
+
|
|
72
|
+
function Test() {
|
|
73
|
+
return (
|
|
74
|
+
<ToggleSwitch
|
|
75
|
+
checked={true}
|
|
76
|
+
label="Toggle"
|
|
77
|
+
disabled
|
|
78
|
+
onClick={() => {}}
|
|
79
|
+
/>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
`.trim()
|
|
83
|
+
|
|
84
|
+
const result = applyTransform(input)
|
|
85
|
+
expect(result).toContain("onChange={() => {}}")
|
|
86
|
+
expect(result).toContain('label="Toggle"')
|
|
87
|
+
expect(result).toContain("checked={true}")
|
|
88
|
+
expect(result).toContain("disabled")
|
|
89
|
+
expect(result).not.toContain("onClick=")
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("should add CHANGED comment when verbose is enabled", () => {
|
|
93
|
+
const input = `
|
|
94
|
+
import { ToggleSwitch } from "@planningcenter/tapestry-react"
|
|
95
|
+
|
|
96
|
+
function Test() {
|
|
97
|
+
return <ToggleSwitch checked={true} onClick={() => {}} label="Toggle" />
|
|
98
|
+
}
|
|
99
|
+
`.trim()
|
|
100
|
+
|
|
101
|
+
const result = applyTransform(input, true)
|
|
102
|
+
expect(result).toContain("onChange={() => {}}")
|
|
103
|
+
expect(result).not.toContain("onClick=")
|
|
104
|
+
expect(result).toContain(
|
|
105
|
+
"CHANGED: tapestry-migration (onChange): renamed from onClick"
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe("edge cases", () => {
|
|
111
|
+
it("should not affect ToggleSwitch without onClick", () => {
|
|
112
|
+
const input = `
|
|
113
|
+
import { ToggleSwitch } from "@planningcenter/tapestry-react"
|
|
114
|
+
|
|
115
|
+
function Test() {
|
|
116
|
+
return <ToggleSwitch checked={true} label="Toggle" />
|
|
117
|
+
}
|
|
118
|
+
`.trim()
|
|
119
|
+
|
|
120
|
+
const result = applyTransform(input)
|
|
121
|
+
expect(result).toBe(input)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("should not affect other components with onClick", () => {
|
|
125
|
+
const input = `
|
|
126
|
+
import { Button, ToggleSwitch } from "@planningcenter/tapestry-react"
|
|
127
|
+
|
|
128
|
+
function Test() {
|
|
129
|
+
return (
|
|
130
|
+
<div>
|
|
131
|
+
<Button onClick={() => {}}>Click me</Button>
|
|
132
|
+
<ToggleSwitch checked={true} onClick={handleChange} label="Toggle" />
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
`.trim()
|
|
137
|
+
|
|
138
|
+
const result = applyTransform(input)
|
|
139
|
+
expect(result).toContain("<Button onClick={() => {}}>Click me</Button>")
|
|
140
|
+
expect(result).toContain("onChange={handleChange}")
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("should rename onClick and add TODO comment when handler takes an argument", () => {
|
|
144
|
+
const input = `
|
|
145
|
+
import { ToggleSwitch } from "@planningcenter/tapestry-react"
|
|
146
|
+
|
|
147
|
+
function Test() {
|
|
148
|
+
return <ToggleSwitch checked={true} onClick={(e) => handle(e)} label="Toggle" />
|
|
149
|
+
}
|
|
150
|
+
`.trim()
|
|
151
|
+
|
|
152
|
+
const result = applyTransform(input)
|
|
153
|
+
expect(result).toContain("onChange={(e) => handle(e)}")
|
|
154
|
+
expect(result).not.toContain("onClick=")
|
|
155
|
+
expect(result).toContain("TODO")
|
|
156
|
+
expect(result).toContain("ChangeEvent<HTMLInputElement>")
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("should not add TODO when handler takes no arguments", () => {
|
|
160
|
+
const input = `
|
|
161
|
+
import { ToggleSwitch } from "@planningcenter/tapestry-react"
|
|
162
|
+
|
|
163
|
+
function Test() {
|
|
164
|
+
return <ToggleSwitch checked={true} onClick={() => toggle()} label="Toggle" />
|
|
165
|
+
}
|
|
166
|
+
`.trim()
|
|
167
|
+
|
|
168
|
+
const result = applyTransform(input)
|
|
169
|
+
expect(result).toContain("onChange={() => toggle()}")
|
|
170
|
+
expect(result).not.toContain("onClick=")
|
|
171
|
+
expect(result).not.toContain("TODO")
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it("should handle aliased ToggleSwitch import", () => {
|
|
175
|
+
const input = `
|
|
176
|
+
import { ToggleSwitch as TapestryToggleSwitch } from "@planningcenter/tapestry-react"
|
|
177
|
+
|
|
178
|
+
function Test() {
|
|
179
|
+
return <TapestryToggleSwitch checked={true} onClick={handleChange} label="Toggle" />
|
|
180
|
+
}
|
|
181
|
+
`.trim()
|
|
182
|
+
|
|
183
|
+
const result = applyTransform(input)
|
|
184
|
+
expect(result).toContain("onChange={handleChange}")
|
|
185
|
+
expect(result).not.toContain("onClick=")
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it("should leave onClick and add TODO when both onClick and onChange are present", () => {
|
|
189
|
+
const input = `
|
|
190
|
+
import { ToggleSwitch } from "@planningcenter/tapestry-react"
|
|
191
|
+
|
|
192
|
+
function Test() {
|
|
193
|
+
return (
|
|
194
|
+
<ToggleSwitch
|
|
195
|
+
checked={true}
|
|
196
|
+
onClick={() => handleClick()}
|
|
197
|
+
onChange={(e) => handleChange(e)}
|
|
198
|
+
label="Toggle"
|
|
199
|
+
/>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
`.trim()
|
|
203
|
+
|
|
204
|
+
const result = applyTransform(input)
|
|
205
|
+
expect(result).toContain("onClick={() => handleClick()}")
|
|
206
|
+
expect(result).toContain("onChange={(e) => handleChange(e)}")
|
|
207
|
+
expect(result).toContain("TODO")
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
|
|
4
|
+
import { transformAttributeName } from "../../shared/actions/transformAttributeName"
|
|
5
|
+
import { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
6
|
+
import { findAttribute } from "../../shared/findAttribute"
|
|
7
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
8
|
+
|
|
9
|
+
const transform: Transform = attributeTransformFactory({
|
|
10
|
+
condition: hasAttribute("onClick"),
|
|
11
|
+
targetComponent: "ToggleSwitch",
|
|
12
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
13
|
+
transform: (element, { j, options }) => {
|
|
14
|
+
const attributes = element.openingElement.attributes || []
|
|
15
|
+
|
|
16
|
+
// When both onClick and onChange are present, don't rename (would clobber
|
|
17
|
+
// the existing onChange). Instead, leave onClick in place with a TODO so a
|
|
18
|
+
// developer can resolve the conflict manually.
|
|
19
|
+
if (hasAttribute("onChange")(element)) {
|
|
20
|
+
const onClickAttr = findAttribute(attributes, "onClick")
|
|
21
|
+
if (onClickAttr) {
|
|
22
|
+
addCommentToAttribute({
|
|
23
|
+
attribute: onClickAttr,
|
|
24
|
+
commentKind: "todo",
|
|
25
|
+
j,
|
|
26
|
+
text: "ToggleSwitch uses onChange, not onClick. Both are present — remove onClick and verify the onChange handler is correct (event: ChangeEvent<HTMLInputElement>, read new state via e.target.checked)",
|
|
27
|
+
})
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Rename onClick → onChange. transformAttributeName also adds the CHANGED
|
|
34
|
+
// comment when options.verbose is enabled.
|
|
35
|
+
const renamed = transformAttributeName("onClick", "onChange", {
|
|
36
|
+
element,
|
|
37
|
+
j,
|
|
38
|
+
options,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if (renamed) {
|
|
42
|
+
// If the handler references an argument, its event type has changed from
|
|
43
|
+
// MouseEvent (button) to ChangeEvent<HTMLInputElement> (checkbox). Flag
|
|
44
|
+
// it so the developer can verify the handler logic.
|
|
45
|
+
const onChangeAttr = findAttribute(attributes, "onChange")
|
|
46
|
+
if (onChangeAttr) {
|
|
47
|
+
const value = onChangeAttr.value
|
|
48
|
+
if (value?.type === "JSXExpressionContainer") {
|
|
49
|
+
const expr = value.expression
|
|
50
|
+
if (
|
|
51
|
+
(expr.type === "ArrowFunctionExpression" ||
|
|
52
|
+
expr.type === "FunctionExpression") &&
|
|
53
|
+
expr.params.length > 0
|
|
54
|
+
) {
|
|
55
|
+
addCommentToAttribute({
|
|
56
|
+
attribute: onChangeAttr,
|
|
57
|
+
commentKind: "todo",
|
|
58
|
+
j,
|
|
59
|
+
text: "renamed from onClick: the event type has changed from MouseEvent to ChangeEvent<HTMLInputElement> — verify the handler uses e.target.checked if needed",
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return false
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
export default transform
|