@planningcenter/tapestry-migration-cli 3.4.1-rc.8 → 3.4.1
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/dist/tapestry-react-shim.cjs +34 -64
- package/package.json +3 -3
- package/src/components/dropdown/index.test.ts +177 -4
- package/src/components/dropdown/index.ts +24 -2
- package/src/components/dropdown/transforms/auditSpreadProps.test.ts +110 -0
- package/src/components/dropdown/transforms/auditSpreadProps.ts +10 -0
- package/src/components/dropdown/transforms/dividerToSeparator.test.ts +134 -0
- package/src/components/dropdown/transforms/dividerToSeparator.ts +67 -0
- package/src/components/dropdown/transforms/itemToAction.test.ts +19 -1
- package/src/components/dropdown/transforms/itemToAction.ts +14 -2
- package/src/components/dropdown/transforms/linkToLink.test.ts +19 -1
- package/src/components/dropdown/transforms/linkToLink.ts +13 -2
- package/src/components/dropdown/transforms/moveDropdownImport.test.ts +93 -0
- package/src/components/dropdown/transforms/moveDropdownImport.ts +13 -0
- package/src/components/dropdown/transforms/placementIdToMenu.test.ts +135 -0
- package/src/components/dropdown/transforms/placementIdToMenu.ts +96 -0
- package/src/components/dropdown/transforms/unsupportedProps.test.ts +145 -0
- package/src/components/dropdown/transforms/unsupportedProps.ts +12 -0
- package/src/components/dropdown/transforms/unsupportedPropsDivider.test.ts +143 -0
- package/src/components/dropdown/transforms/unsupportedPropsDivider.ts +26 -0
- package/src/components/dropdown/transforms/unsupportedPropsItem.test.ts +123 -0
- package/src/components/dropdown/transforms/unsupportedPropsItem.ts +12 -0
- package/src/components/dropdown/transforms/unsupportedPropsLink.test.ts +107 -0
- package/src/components/dropdown/transforms/unsupportedPropsLink.ts +12 -0
- package/src/components/dropdown/transforms/wrapMenu.test.ts +153 -0
- package/src/components/dropdown/transforms/wrapMenu.ts +54 -0
- package/src/components/dropdown/transforms/wrapTrigger.test.ts +283 -0
- package/src/components/dropdown/transforms/wrapTrigger.ts +98 -0
- package/src/components/input/transforms/unsupportedProps.test.ts +14 -13
- package/src/components/shared/conditions/isChildOf.test.ts +89 -0
- package/src/components/shared/conditions/isChildOf.ts +43 -0
- package/src/components/shared/helpers/unsupportedPropsHelpers.ts +36 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { transformElementName } from "../../shared/actions/transformElementName"
|
|
4
|
+
import { isChildOf } from "../../shared/conditions/isChildOf"
|
|
5
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
6
|
+
import {
|
|
7
|
+
addImport,
|
|
8
|
+
getImportName,
|
|
9
|
+
removeImportFromDeclaration,
|
|
10
|
+
} from "../../shared/transformFactories/helpers/manageImports"
|
|
11
|
+
|
|
12
|
+
const transform: Transform = (fileInfo, api, options) => {
|
|
13
|
+
const j = api.jscodeshift
|
|
14
|
+
const source = j(fileInfo.source)
|
|
15
|
+
|
|
16
|
+
const dropdownLocalName = getImportName(
|
|
17
|
+
"Dropdown",
|
|
18
|
+
"@planningcenter/tapestry-react",
|
|
19
|
+
{ j, source }
|
|
20
|
+
)
|
|
21
|
+
if (!dropdownLocalName) return null
|
|
22
|
+
|
|
23
|
+
const result = attributeTransformFactory({
|
|
24
|
+
condition: isChildOf(dropdownLocalName, source, j),
|
|
25
|
+
targetComponent: "Divider",
|
|
26
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
27
|
+
transform: (element) =>
|
|
28
|
+
transformElementName({ element, name: "DropdownSeparator" }),
|
|
29
|
+
})(fileInfo, api, options)
|
|
30
|
+
|
|
31
|
+
if (!result) return null
|
|
32
|
+
|
|
33
|
+
const updatedSource = j(result as string)
|
|
34
|
+
|
|
35
|
+
addImport({
|
|
36
|
+
component: "DropdownSeparator",
|
|
37
|
+
fromPackage: "@planningcenter/tapestry-react",
|
|
38
|
+
j,
|
|
39
|
+
pkg: "@planningcenter/tapestry",
|
|
40
|
+
source: updatedSource,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const dividerLocalName = getImportName(
|
|
44
|
+
"Divider",
|
|
45
|
+
"@planningcenter/tapestry-react",
|
|
46
|
+
{ j, source: updatedSource }
|
|
47
|
+
)
|
|
48
|
+
if (dividerLocalName) {
|
|
49
|
+
const dividerStillUsed =
|
|
50
|
+
updatedSource.find(j.JSXOpeningElement, {
|
|
51
|
+
name: { name: dividerLocalName },
|
|
52
|
+
}).length > 0
|
|
53
|
+
|
|
54
|
+
if (!dividerStillUsed) {
|
|
55
|
+
removeImportFromDeclaration(
|
|
56
|
+
updatedSource.find(j.ImportDeclaration, {
|
|
57
|
+
source: { value: "@planningcenter/tapestry-react" },
|
|
58
|
+
}),
|
|
59
|
+
"Divider"
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return updatedSource.toSource()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default transform
|
|
@@ -205,7 +205,7 @@ export default function Test() {
|
|
|
205
205
|
expect(result).not.toContain("Dropdown.Item")
|
|
206
206
|
})
|
|
207
207
|
|
|
208
|
-
it("does not touch the import statement", () => {
|
|
208
|
+
it("does not touch the tapestry-react import statement", () => {
|
|
209
209
|
const input = `
|
|
210
210
|
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
211
211
|
|
|
@@ -223,6 +223,24 @@ export default function Test() {
|
|
|
223
223
|
'import { Dropdown } from "@planningcenter/tapestry-react"'
|
|
224
224
|
)
|
|
225
225
|
})
|
|
226
|
+
|
|
227
|
+
it("adds DropdownAction import from tapestry", () => {
|
|
228
|
+
const input = `
|
|
229
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
230
|
+
|
|
231
|
+
export default function Test() {
|
|
232
|
+
return (
|
|
233
|
+
<Dropdown title="Actions">
|
|
234
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
235
|
+
</Dropdown>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
`.trim()
|
|
239
|
+
|
|
240
|
+
const result = applyTransform(input)
|
|
241
|
+
expect(result).toContain('from "@planningcenter/tapestry"')
|
|
242
|
+
expect(result).toContain("DropdownAction")
|
|
243
|
+
})
|
|
226
244
|
})
|
|
227
245
|
|
|
228
246
|
describe("multiple items", () => {
|
|
@@ -3,11 +3,12 @@ import { Transform } from "jscodeshift"
|
|
|
3
3
|
import { transformAttributeName } from "../../shared/actions/transformAttributeName"
|
|
4
4
|
import { transformElementName } from "../../shared/actions/transformElementName"
|
|
5
5
|
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
6
|
+
import { addImport } from "../../shared/transformFactories/helpers/manageImports"
|
|
6
7
|
|
|
7
8
|
const transform: Transform = attributeTransformFactory({
|
|
8
9
|
targetComponent: "Dropdown.Item",
|
|
9
10
|
targetPackage: "@planningcenter/tapestry-react",
|
|
10
|
-
transform: (element, { j, options }) => {
|
|
11
|
+
transform: (element, { j, options, source }) => {
|
|
11
12
|
const onSelectRenamed = transformAttributeName("onSelect", "onAction", {
|
|
12
13
|
element,
|
|
13
14
|
j,
|
|
@@ -27,7 +28,18 @@ const transform: Transform = attributeTransformFactory({
|
|
|
27
28
|
element,
|
|
28
29
|
name: "DropdownAction",
|
|
29
30
|
})
|
|
30
|
-
|
|
31
|
+
const changed =
|
|
32
|
+
onSelectRenamed || valueRenamed || textRenamed || elementRenamed
|
|
33
|
+
if (changed) {
|
|
34
|
+
addImport({
|
|
35
|
+
component: "DropdownAction",
|
|
36
|
+
fromPackage: "@planningcenter/tapestry-react",
|
|
37
|
+
j,
|
|
38
|
+
pkg: "@planningcenter/tapestry",
|
|
39
|
+
source,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
return changed
|
|
31
43
|
},
|
|
32
44
|
})
|
|
33
45
|
|
|
@@ -186,7 +186,7 @@ export default function Test() {
|
|
|
186
186
|
expect(result).toContain("<DropdownLink")
|
|
187
187
|
})
|
|
188
188
|
|
|
189
|
-
it("does not touch the import statement", () => {
|
|
189
|
+
it("does not touch the tapestry-react import statement", () => {
|
|
190
190
|
const input = `
|
|
191
191
|
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
192
192
|
|
|
@@ -204,6 +204,24 @@ export default function Test() {
|
|
|
204
204
|
'import { Dropdown } from "@planningcenter/tapestry-react"'
|
|
205
205
|
)
|
|
206
206
|
})
|
|
207
|
+
|
|
208
|
+
it("adds DropdownLink import from tapestry", () => {
|
|
209
|
+
const input = `
|
|
210
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
211
|
+
|
|
212
|
+
export default function Test() {
|
|
213
|
+
return (
|
|
214
|
+
<Dropdown title="Nav">
|
|
215
|
+
<Dropdown.Link to="/docs">Docs</Dropdown.Link>
|
|
216
|
+
</Dropdown>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
`.trim()
|
|
220
|
+
|
|
221
|
+
const result = applyTransform(input)
|
|
222
|
+
expect(result).toContain('from "@planningcenter/tapestry"')
|
|
223
|
+
expect(result).toContain("DropdownLink")
|
|
224
|
+
})
|
|
207
225
|
})
|
|
208
226
|
|
|
209
227
|
describe("no changes scenarios", () => {
|
|
@@ -3,11 +3,12 @@ import { Transform } from "jscodeshift"
|
|
|
3
3
|
import { transformAttributeName } from "../../shared/actions/transformAttributeName"
|
|
4
4
|
import { transformElementName } from "../../shared/actions/transformElementName"
|
|
5
5
|
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
6
|
+
import { addImport } from "../../shared/transformFactories/helpers/manageImports"
|
|
6
7
|
|
|
7
8
|
const transform: Transform = attributeTransformFactory({
|
|
8
9
|
targetComponent: "Dropdown.Link",
|
|
9
10
|
targetPackage: "@planningcenter/tapestry-react",
|
|
10
|
-
transform: (element, { j, options }) => {
|
|
11
|
+
transform: (element, { j, options, source }) => {
|
|
11
12
|
const toRenamed = transformAttributeName("to", "href", {
|
|
12
13
|
element,
|
|
13
14
|
j,
|
|
@@ -17,7 +18,17 @@ const transform: Transform = attributeTransformFactory({
|
|
|
17
18
|
element,
|
|
18
19
|
name: "DropdownLink",
|
|
19
20
|
})
|
|
20
|
-
|
|
21
|
+
const changed = toRenamed || elementRenamed
|
|
22
|
+
if (changed) {
|
|
23
|
+
addImport({
|
|
24
|
+
component: "DropdownLink",
|
|
25
|
+
fromPackage: "@planningcenter/tapestry-react",
|
|
26
|
+
j,
|
|
27
|
+
pkg: "@planningcenter/tapestry",
|
|
28
|
+
source,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
return changed
|
|
21
32
|
},
|
|
22
33
|
})
|
|
23
34
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./moveDropdownImport"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function applyTransform(source: string): string | null {
|
|
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("moveDropdownImport transform", () => {
|
|
18
|
+
it("moves Dropdown import from tapestry-react to tapestry", () => {
|
|
19
|
+
const input = `
|
|
20
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
21
|
+
|
|
22
|
+
export default function Test() {
|
|
23
|
+
return <Dropdown onClose={fn}><DropdownMenu /></Dropdown>
|
|
24
|
+
}
|
|
25
|
+
`.trim()
|
|
26
|
+
|
|
27
|
+
const result = applyTransform(input)
|
|
28
|
+
expect(result).toContain(
|
|
29
|
+
'import { Dropdown } from "@planningcenter/tapestry"'
|
|
30
|
+
)
|
|
31
|
+
expect(result).not.toContain('from "@planningcenter/tapestry-react"')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("merges Dropdown into an existing tapestry import", () => {
|
|
35
|
+
const input = `
|
|
36
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
37
|
+
import { DropdownAction, DropdownMenu, DropdownTrigger } from "@planningcenter/tapestry"
|
|
38
|
+
|
|
39
|
+
export default function Test() {
|
|
40
|
+
return (
|
|
41
|
+
<Dropdown onClose={fn}>
|
|
42
|
+
<DropdownTrigger><DropdownButton label="Actions" /></DropdownTrigger>
|
|
43
|
+
<DropdownMenu><DropdownAction onAction={fn}>Edit</DropdownAction></DropdownMenu>
|
|
44
|
+
</Dropdown>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
`.trim()
|
|
48
|
+
|
|
49
|
+
const result = applyTransform(input)
|
|
50
|
+
expect(result).toContain("Dropdown")
|
|
51
|
+
expect(result).toContain("DropdownAction")
|
|
52
|
+
expect(result).toContain("DropdownMenu")
|
|
53
|
+
expect(result).toContain("DropdownTrigger")
|
|
54
|
+
expect(result).toContain('from "@planningcenter/tapestry"')
|
|
55
|
+
expect(result).not.toContain('from "@planningcenter/tapestry-react"')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("preserves other tapestry-react imports", () => {
|
|
59
|
+
const input = `
|
|
60
|
+
import { Dropdown, Button } from "@planningcenter/tapestry-react"
|
|
61
|
+
|
|
62
|
+
export default function Test() {
|
|
63
|
+
return <Dropdown onClose={fn}><DropdownMenu /></Dropdown>
|
|
64
|
+
}
|
|
65
|
+
`.trim()
|
|
66
|
+
|
|
67
|
+
const result = applyTransform(input)
|
|
68
|
+
expect(result).toContain(
|
|
69
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
70
|
+
)
|
|
71
|
+
expect(result).toContain(
|
|
72
|
+
'import { Dropdown } from "@planningcenter/tapestry"'
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe("no changes scenarios", () => {
|
|
77
|
+
it("returns null when Dropdown is not imported from tapestry-react", () => {
|
|
78
|
+
const input = `
|
|
79
|
+
import { Dropdown } from "@planningcenter/tapestry"
|
|
80
|
+
|
|
81
|
+
export default function Test() {
|
|
82
|
+
return <Dropdown onClose={fn}><DropdownMenu /></Dropdown>
|
|
83
|
+
}
|
|
84
|
+
`.trim()
|
|
85
|
+
|
|
86
|
+
expect(applyTransform(input)).toBe(null)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("returns null for empty file", () => {
|
|
90
|
+
expect(applyTransform("")).toBe(null)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { componentTransformFactory } from "../../shared/transformFactories/componentTransformFactory"
|
|
4
|
+
|
|
5
|
+
const transform: Transform = componentTransformFactory({
|
|
6
|
+
condition: () => true,
|
|
7
|
+
fromComponent: "Dropdown",
|
|
8
|
+
fromPackage: "@planningcenter/tapestry-react",
|
|
9
|
+
toComponent: "Dropdown",
|
|
10
|
+
toPackage: "@planningcenter/tapestry",
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export default transform
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./placementIdToMenu"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function applyTransform(source: string): string | null {
|
|
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
|
+
const withMenu = (
|
|
18
|
+
attrs: string,
|
|
19
|
+
items = "<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>"
|
|
20
|
+
) =>
|
|
21
|
+
`
|
|
22
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
23
|
+
|
|
24
|
+
export default function Test() {
|
|
25
|
+
return (
|
|
26
|
+
<Dropdown ${attrs}>
|
|
27
|
+
<DropdownMenu>
|
|
28
|
+
${items}
|
|
29
|
+
</DropdownMenu>
|
|
30
|
+
</Dropdown>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
`.trim()
|
|
34
|
+
|
|
35
|
+
describe("placementIdToMenu transform", () => {
|
|
36
|
+
describe("placement", () => {
|
|
37
|
+
it("moves placement string literal to DropdownMenu with dash-to-space conversion", () => {
|
|
38
|
+
const result = applyTransform(withMenu('placement="bottom-start"'))
|
|
39
|
+
expect(result).toContain('<DropdownMenu placement="bottom start"')
|
|
40
|
+
expect(result).not.toMatch(/<Dropdown\s[^>]*placement/)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("converts all dash-separated placement values", () => {
|
|
44
|
+
const result = applyTransform(withMenu('placement="top-end"'))
|
|
45
|
+
expect(result).toContain('<DropdownMenu placement="top end"')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("moves placement from JSXExpressionContainer string literal", () => {
|
|
49
|
+
const result = applyTransform(withMenu('placement={"bottom-end"}'))
|
|
50
|
+
expect(result).toContain('<DropdownMenu placement={"bottom end"}')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("moves dynamic placement expression and adds TODO comment", () => {
|
|
54
|
+
const result = applyTransform(withMenu("placement={myPlacement}"))
|
|
55
|
+
expect(result).toContain("<DropdownMenu placement={myPlacement}")
|
|
56
|
+
expect(result).toContain(
|
|
57
|
+
"TODO: tapestry-migration (placement): placement is a dynamic expression"
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe("id", () => {
|
|
63
|
+
it("moves id string literal to DropdownMenu", () => {
|
|
64
|
+
const result = applyTransform(withMenu('id="my-menu"'))
|
|
65
|
+
expect(result).toContain('<DropdownMenu id="my-menu"')
|
|
66
|
+
expect(result).not.toMatch(/<Dropdown\s[^>]*\bid=/)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("moves dynamic id expression to DropdownMenu", () => {
|
|
70
|
+
const result = applyTransform(withMenu("id={menuId}"))
|
|
71
|
+
expect(result).toContain("<DropdownMenu id={menuId}")
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe("placement and id together", () => {
|
|
76
|
+
it("moves both placement and id to DropdownMenu", () => {
|
|
77
|
+
const result = applyTransform(
|
|
78
|
+
withMenu('placement="bottom-start" id="my-menu"')
|
|
79
|
+
)
|
|
80
|
+
expect(result).toContain("placement=")
|
|
81
|
+
expect(result).toContain("id=")
|
|
82
|
+
expect(result).toContain("<DropdownMenu")
|
|
83
|
+
expect(result).not.toMatch(/<Dropdown\s[^>]*placement/)
|
|
84
|
+
expect(result).not.toMatch(/<Dropdown\s[^>]*\bid=/)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe("no changes scenarios", () => {
|
|
89
|
+
it("returns null when neither placement nor id is present", () => {
|
|
90
|
+
const result = applyTransform(withMenu("onClose={handleClose}"))
|
|
91
|
+
expect(result).toBe(null)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("adds TODO when DropdownMenu child is not found", () => {
|
|
95
|
+
const input = `
|
|
96
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
97
|
+
|
|
98
|
+
export default function Test() {
|
|
99
|
+
return <Dropdown placement="bottom-start">{children}</Dropdown>
|
|
100
|
+
}
|
|
101
|
+
`.trim()
|
|
102
|
+
|
|
103
|
+
const result = applyTransform(input)
|
|
104
|
+
expect(result).toContain("TODO: tapestry-migration (placement)")
|
|
105
|
+
expect(result).toContain("no DropdownMenu child found")
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("returns null when Dropdown is not imported from tapestry-react", () => {
|
|
109
|
+
const input = `
|
|
110
|
+
import { Dropdown } from "some-other-library"
|
|
111
|
+
|
|
112
|
+
export default function Test() {
|
|
113
|
+
return (
|
|
114
|
+
<Dropdown placement="bottom-start">
|
|
115
|
+
<DropdownMenu><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></DropdownMenu>
|
|
116
|
+
</Dropdown>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
`.trim()
|
|
120
|
+
|
|
121
|
+
expect(applyTransform(input)).toBe(null)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("does not touch the import statement", () => {
|
|
125
|
+
const result = applyTransform(withMenu('placement="bottom-start"'))
|
|
126
|
+
expect(result).toContain(
|
|
127
|
+
'import { Dropdown } from "@planningcenter/tapestry-react"'
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("returns null for empty file", () => {
|
|
132
|
+
expect(applyTransform("")).toBe(null)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { JSXAttribute, JSXElement, Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addComment } from "../../shared/actions/addComment"
|
|
4
|
+
import { getAttribute } from "../../shared/actions/getAttribute"
|
|
5
|
+
import { removeAttribute } from "../../shared/actions/removeAttribute"
|
|
6
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
7
|
+
|
|
8
|
+
function findDropdownMenu(element: JSXElement): JSXElement | undefined {
|
|
9
|
+
return (element.children ?? []).find(
|
|
10
|
+
(child): child is JSXElement =>
|
|
11
|
+
child.type === "JSXElement" &&
|
|
12
|
+
child.openingElement.name.type === "JSXIdentifier" &&
|
|
13
|
+
child.openingElement.name.name === "DropdownMenu"
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function applyPlacementConversion(attr: JSXAttribute): { isDynamic: boolean } {
|
|
18
|
+
const val = attr.value
|
|
19
|
+
if (val?.type === "StringLiteral") {
|
|
20
|
+
val.value = val.value.replace(/-/g, " ")
|
|
21
|
+
return { isDynamic: false }
|
|
22
|
+
}
|
|
23
|
+
if (val?.type === "JSXExpressionContainer") {
|
|
24
|
+
if (val.expression.type === "StringLiteral") {
|
|
25
|
+
val.expression.value = val.expression.value.replace(/-/g, " ")
|
|
26
|
+
return { isDynamic: false }
|
|
27
|
+
}
|
|
28
|
+
if (val.expression.type !== "JSXEmptyExpression") {
|
|
29
|
+
return { isDynamic: true }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { isDynamic: false }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function moveAttr(
|
|
36
|
+
name: string,
|
|
37
|
+
attr: JSXAttribute,
|
|
38
|
+
from: JSXElement,
|
|
39
|
+
to: JSXElement,
|
|
40
|
+
{
|
|
41
|
+
j,
|
|
42
|
+
source,
|
|
43
|
+
}: {
|
|
44
|
+
j: Parameters<typeof removeAttribute>[1]["j"]
|
|
45
|
+
source: Parameters<typeof removeAttribute>[1]["source"]
|
|
46
|
+
}
|
|
47
|
+
) {
|
|
48
|
+
to.openingElement.attributes = [...(to.openingElement.attributes ?? []), attr]
|
|
49
|
+
removeAttribute(name, { element: from, j, source })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const transform: Transform = attributeTransformFactory({
|
|
53
|
+
targetComponent: "Dropdown",
|
|
54
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
55
|
+
transform: (element, { j, source }) => {
|
|
56
|
+
const placementAttr = getAttribute({ element, name: "placement" })
|
|
57
|
+
const idAttr = getAttribute({ element, name: "id" })
|
|
58
|
+
|
|
59
|
+
if (!placementAttr && !idAttr) return false
|
|
60
|
+
|
|
61
|
+
const menuChild = findDropdownMenu(element)
|
|
62
|
+
|
|
63
|
+
if (!menuChild) {
|
|
64
|
+
addComment({
|
|
65
|
+
element,
|
|
66
|
+
j,
|
|
67
|
+
scope: "placement",
|
|
68
|
+
source,
|
|
69
|
+
text: "placement/id could not be moved to DropdownMenu — no DropdownMenu child found",
|
|
70
|
+
})
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (idAttr) {
|
|
75
|
+
moveAttr("id", idAttr, element, menuChild, { j, source })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (placementAttr) {
|
|
79
|
+
const { isDynamic } = applyPlacementConversion(placementAttr)
|
|
80
|
+
moveAttr("placement", placementAttr, element, menuChild, { j, source })
|
|
81
|
+
if (isDynamic) {
|
|
82
|
+
addComment({
|
|
83
|
+
element,
|
|
84
|
+
j,
|
|
85
|
+
scope: "placement",
|
|
86
|
+
source,
|
|
87
|
+
text: "placement is a dynamic expression — dash-to-space conversion (e.g. 'bottom-start' → 'bottom start') must be done manually",
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return true
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
export default transform
|
|
@@ -0,0 +1,145 @@
|
|
|
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): string {
|
|
9
|
+
const fileInfo = { path: "test.tsx", source }
|
|
10
|
+
return (
|
|
11
|
+
(transform(
|
|
12
|
+
fileInfo,
|
|
13
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
14
|
+
{}
|
|
15
|
+
) as string | null) || source
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("unsupportedProps transform", () => {
|
|
20
|
+
describe("flags removed props", () => {
|
|
21
|
+
it("flags variant", () => {
|
|
22
|
+
const input = `
|
|
23
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
24
|
+
|
|
25
|
+
export default function Test() {
|
|
26
|
+
return <Dropdown variant="outline" onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
|
|
27
|
+
}
|
|
28
|
+
`.trim()
|
|
29
|
+
|
|
30
|
+
const result = applyTransform(input)
|
|
31
|
+
expect(result).toContain("TODO: tapestry-migration (variant)")
|
|
32
|
+
expect(result).toContain("variant")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("flags theme", () => {
|
|
36
|
+
const input = `
|
|
37
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
38
|
+
|
|
39
|
+
export default function Test() {
|
|
40
|
+
return <Dropdown theme="primary" onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
|
|
41
|
+
}
|
|
42
|
+
`.trim()
|
|
43
|
+
|
|
44
|
+
const result = applyTransform(input)
|
|
45
|
+
expect(result).toContain("TODO: tapestry-migration (theme)")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("flags size", () => {
|
|
49
|
+
const input = `
|
|
50
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
51
|
+
|
|
52
|
+
export default function Test() {
|
|
53
|
+
return <Dropdown size="md" onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
|
|
54
|
+
}
|
|
55
|
+
`.trim()
|
|
56
|
+
|
|
57
|
+
const result = applyTransform(input)
|
|
58
|
+
expect(result).toContain("TODO: tapestry-migration (size)")
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("flags popoverProps", () => {
|
|
62
|
+
const input = `
|
|
63
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
64
|
+
|
|
65
|
+
export default function Test() {
|
|
66
|
+
return <Dropdown popoverProps={{ offset: 8 }} onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
|
|
67
|
+
}
|
|
68
|
+
`.trim()
|
|
69
|
+
|
|
70
|
+
const result = applyTransform(input)
|
|
71
|
+
expect(result).toContain("TODO: tapestry-migration (popoverProps)")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("flags multiple unsupported props", () => {
|
|
75
|
+
const input = `
|
|
76
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
77
|
+
|
|
78
|
+
export default function Test() {
|
|
79
|
+
return <Dropdown variant="outline" theme="primary" css={{ color: "red" }} onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
|
|
80
|
+
}
|
|
81
|
+
`.trim()
|
|
82
|
+
|
|
83
|
+
const result = applyTransform(input)
|
|
84
|
+
expect(result).toContain("TODO: tapestry-migration (variant)")
|
|
85
|
+
expect(result).toContain("TODO: tapestry-migration (theme)")
|
|
86
|
+
expect(result).toContain("TODO: tapestry-migration (css)")
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe("does not flag supported props", () => {
|
|
91
|
+
it("does not flag onClose or onOpen", () => {
|
|
92
|
+
const input = `
|
|
93
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
94
|
+
|
|
95
|
+
export default function Test() {
|
|
96
|
+
return <Dropdown onClose={handleClose} onOpen={handleOpen}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
|
|
97
|
+
}
|
|
98
|
+
`.trim()
|
|
99
|
+
|
|
100
|
+
expect(applyTransform(input)).toBe(input)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("does not flag className or ref", () => {
|
|
104
|
+
const input = `
|
|
105
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
106
|
+
|
|
107
|
+
export default function Test() {
|
|
108
|
+
return <Dropdown className="my-dropdown" ref={ref} onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
|
|
109
|
+
}
|
|
110
|
+
`.trim()
|
|
111
|
+
|
|
112
|
+
expect(applyTransform(input)).toBe(input)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("does not flag aria- and data- attributes", () => {
|
|
116
|
+
const input = `
|
|
117
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
118
|
+
|
|
119
|
+
export default function Test() {
|
|
120
|
+
return <Dropdown aria-label="Options" data-pendo="dropdown" onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
|
|
121
|
+
}
|
|
122
|
+
`.trim()
|
|
123
|
+
|
|
124
|
+
expect(applyTransform(input)).toBe(input)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe("no changes scenarios", () => {
|
|
129
|
+
it("returns source unchanged when not from tapestry-react", () => {
|
|
130
|
+
const input = `
|
|
131
|
+
import { Dropdown } from "some-other-library"
|
|
132
|
+
|
|
133
|
+
export default function Test() {
|
|
134
|
+
return <Dropdown variant="outline"><Dropdown.Item>Edit</Dropdown.Item></Dropdown>
|
|
135
|
+
}
|
|
136
|
+
`.trim()
|
|
137
|
+
|
|
138
|
+
expect(applyTransform(input)).toBe(input)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("returns source unchanged for empty file", () => {
|
|
142
|
+
expect(applyTransform("")).toBe("")
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
})
|