@planningcenter/tapestry-migration-cli 3.4.1-rc.0 → 3.4.1-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.
@@ -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
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ const COMPONENTS_SET = new Set([
15
15
  "button",
16
16
  "checkbox",
17
17
  "date-picker",
18
+ "dropdown",
18
19
  "input",
19
20
  "link",
20
21
  "radio",