@planningcenter/tapestry-migration-cli 3.4.1-rc.1 → 3.4.1-rc.11

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
@@ -156,19 +156,6 @@ function Test() {
156
156
  expect(result).toContain("TODO: tapestry-migration (hover)")
157
157
  })
158
158
 
159
- it("should flag autoFocus", () => {
160
- const input = `
161
- import { Input } from "@planningcenter/tapestry-react"
162
-
163
- function Test() {
164
- return <Input autoFocus label="Name" />
165
- }
166
- `.trim()
167
-
168
- const result = applyTransform(input)
169
- expect(result).toContain("TODO: tapestry-migration (autoFocus)")
170
- })
171
-
172
159
  it("should flag readOnlyBackgroundColor", () => {
173
160
  const input = `
174
161
  import { Input } from "@planningcenter/tapestry-react"
@@ -231,6 +218,20 @@ function Test() {
231
218
  expect(result).not.toContain("TODO: tapestry-migration")
232
219
  })
233
220
 
221
+ it("should not flag autoFocus", () => {
222
+ const input = `
223
+ import { Input } from "@planningcenter/tapestry-react"
224
+
225
+ function Test() {
226
+ return <Input autoFocus label="Name" />
227
+ }
228
+ `.trim()
229
+
230
+ const result = applyTransform(input)
231
+ expect(result).not.toContain("TODO: tapestry-migration")
232
+ expect(result).toContain("autoFocus")
233
+ })
234
+
234
235
  it("should not flag standard HTML attributes", () => {
235
236
  const input = `
236
237
  import { Input } from "@planningcenter/tapestry-react"
@@ -56,6 +56,7 @@ export const CHECKBOX_RADIO_SUPPORTED_PROPS = [
56
56
 
57
57
  export const INPUT_SPECIFIC_PROPS = [
58
58
  "autoComplete",
59
+ "autoFocus",
59
60
  "autoWidth",
60
61
  "defaultValue",
61
62
  "description",
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",