@planningcenter/tapestry-migration-cli 3.4.1-rc.3 → 3.4.1-rc.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "3.4.1-rc.3",
3
+ "version": "3.4.1-rc.5",
4
4
  "description": "CLI tool for Tapestry migrations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@emotion/react": "^11.14.0",
33
- "@planningcenter/tapestry": "^3.4.1-rc.3",
33
+ "@planningcenter/tapestry": "^3.4.1-rc.5",
34
34
  "@planningcenter/tapestry-react": "^4.11.5",
35
35
  "@types/jscodeshift": "^17.3.0",
36
36
  "@types/node": "^20.0.0",
@@ -50,5 +50,5 @@
50
50
  "publishConfig": {
51
51
  "access": "public"
52
52
  },
53
- "gitHead": "51487ee80b4a2012c20b9666d74c8251e45b0efc"
53
+ "gitHead": "5da2559bdcc3347b6442ebbc2a523dab90ca5ccd"
54
54
  }
@@ -15,14 +15,32 @@ function applyTransform(source: string): string | null {
15
15
  }
16
16
 
17
17
  describe("dropdown orchestrator", () => {
18
- it("returns null for a legacy Dropdown source (no-op until rules are added)", () => {
18
+ it("applies itemToAction through the chain", () => {
19
19
  const input = `
20
20
  import { Dropdown } from "@planningcenter/tapestry-react"
21
21
 
22
22
  export default function Test() {
23
23
  return (
24
- <Dropdown title="Actions" variant="outline">
25
- <Dropdown.Item onSelect={() => {}}>Edit</Dropdown.Item>
24
+ <Dropdown title="Actions">
25
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
26
+ </Dropdown>
27
+ )
28
+ }
29
+ `.trim()
30
+
31
+ const result = applyTransform(input)
32
+ expect(result).toContain("<DropdownAction")
33
+ expect(result).toContain("onAction={fn}")
34
+ expect(result).not.toContain("Dropdown.Item")
35
+ })
36
+
37
+ it("returns null when there is nothing to transform", () => {
38
+ const input = `
39
+ import { Dropdown } from "@planningcenter/tapestry-react"
40
+
41
+ export default function Test() {
42
+ return (
43
+ <Dropdown title="Actions">
26
44
  <Dropdown.Link to="/docs">Docs</Dropdown.Link>
27
45
  </Dropdown>
28
46
  )
@@ -1,12 +1,13 @@
1
1
  import { Transform } from "jscodeshift"
2
2
 
3
- // Transforms are added one per commit as rules are built out.
4
- // moveDropdownImport MUST remain last in the chain.
3
+ import itemToAction from "./transforms/itemToAction"
4
+
5
+ // When moveDropdownImport is added, it must remain last in the chain.
5
6
  const transform: Transform = (fileInfo, api, options) => {
6
7
  let currentSource = fileInfo.source
7
8
  let hasAnyChanges = false
8
9
 
9
- const transforms: Transform[] = []
10
+ const transforms: Transform[] = [itemToAction]
10
11
 
11
12
  for (const individualTransform of transforms) {
12
13
  const result = individualTransform(
@@ -0,0 +1,335 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./itemToAction"
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("itemToAction transform", () => {
18
+ describe("element renaming", () => {
19
+ it("renames Dropdown.Item to DropdownAction", () => {
20
+ const input = `
21
+ import { Dropdown } from "@planningcenter/tapestry-react"
22
+
23
+ export default function Test() {
24
+ return (
25
+ <Dropdown title="Actions">
26
+ <Dropdown.Item onSelect={handleEdit}>Edit</Dropdown.Item>
27
+ </Dropdown>
28
+ )
29
+ }
30
+ `.trim()
31
+
32
+ const result = applyTransform(input)
33
+ expect(result).toContain("<DropdownAction")
34
+ expect(result).toContain("</DropdownAction>")
35
+ expect(result).not.toContain("Dropdown.Item")
36
+ })
37
+
38
+ it("preserves children through the rename", () => {
39
+ const input = `
40
+ import { Dropdown } from "@planningcenter/tapestry-react"
41
+
42
+ export default function Test() {
43
+ return (
44
+ <Dropdown title="Actions">
45
+ <Dropdown.Item onSelect={fn}>Edit item</Dropdown.Item>
46
+ </Dropdown>
47
+ )
48
+ }
49
+ `.trim()
50
+
51
+ const result = applyTransform(input)
52
+ expect(result).toContain("Edit item")
53
+ })
54
+ })
55
+
56
+ describe("prop renaming", () => {
57
+ it("renames onSelect to onAction", () => {
58
+ const input = `
59
+ import { Dropdown } from "@planningcenter/tapestry-react"
60
+
61
+ export default function Test() {
62
+ return (
63
+ <Dropdown title="Actions">
64
+ <Dropdown.Item onSelect={handleEdit}>Edit</Dropdown.Item>
65
+ </Dropdown>
66
+ )
67
+ }
68
+ `.trim()
69
+
70
+ const result = applyTransform(input)
71
+ expect(result).toContain("onAction={handleEdit}")
72
+ expect(result).not.toContain("onSelect")
73
+ })
74
+
75
+ it("renames value to id", () => {
76
+ const input = `
77
+ import { Dropdown } from "@planningcenter/tapestry-react"
78
+
79
+ export default function Test() {
80
+ return (
81
+ <Dropdown title="Actions">
82
+ <Dropdown.Item value="edit" onSelect={fn}>Edit</Dropdown.Item>
83
+ </Dropdown>
84
+ )
85
+ }
86
+ `.trim()
87
+
88
+ const result = applyTransform(input)
89
+ expect(result).toContain('id="edit"')
90
+ expect(result).not.toContain("value=")
91
+ })
92
+
93
+ it("renames text to textValue", () => {
94
+ const input = `
95
+ import { Dropdown } from "@planningcenter/tapestry-react"
96
+
97
+ export default function Test() {
98
+ return (
99
+ <Dropdown title="Actions">
100
+ <Dropdown.Item text="Edit item" onSelect={fn}>
101
+ <Icon name="edit" />
102
+ </Dropdown.Item>
103
+ </Dropdown>
104
+ )
105
+ }
106
+ `.trim()
107
+
108
+ const result = applyTransform(input)
109
+ expect(result).toContain('textValue="Edit item"')
110
+ expect(result).not.toContain(" text=")
111
+ })
112
+
113
+ it("renames all props in a single pass", () => {
114
+ const input = `
115
+ import { Dropdown } from "@planningcenter/tapestry-react"
116
+
117
+ export default function Test() {
118
+ return (
119
+ <Dropdown title="Actions">
120
+ <Dropdown.Item value="edit" text="Edit" onSelect={handleEdit}>Edit</Dropdown.Item>
121
+ </Dropdown>
122
+ )
123
+ }
124
+ `.trim()
125
+
126
+ const result = applyTransform(input)
127
+ expect(result).toContain('id="edit"')
128
+ expect(result).toContain('textValue="Edit"')
129
+ expect(result).toContain("onAction={handleEdit}")
130
+ expect(result).not.toContain("value=")
131
+ expect(result).not.toContain("onSelect")
132
+ expect(result).not.toContain(" text=")
133
+ })
134
+
135
+ it("preserves disabled", () => {
136
+ const input = `
137
+ import { Dropdown } from "@planningcenter/tapestry-react"
138
+
139
+ export default function Test() {
140
+ return (
141
+ <Dropdown title="Actions">
142
+ <Dropdown.Item onSelect={fn} disabled>Delete</Dropdown.Item>
143
+ </Dropdown>
144
+ )
145
+ }
146
+ `.trim()
147
+
148
+ const result = applyTransform(input)
149
+ expect(result).toContain("disabled")
150
+ })
151
+
152
+ it("preserves unrecognised props", () => {
153
+ const input = `
154
+ import { Dropdown } from "@planningcenter/tapestry-react"
155
+
156
+ export default function Test() {
157
+ return (
158
+ <Dropdown title="Actions">
159
+ <Dropdown.Item onSelect={fn} data-pendo="bulk-edit">Edit</Dropdown.Item>
160
+ </Dropdown>
161
+ )
162
+ }
163
+ `.trim()
164
+
165
+ const result = applyTransform(input)
166
+ expect(result).toContain('data-pendo="bulk-edit"')
167
+ })
168
+ })
169
+
170
+ describe("isolation", () => {
171
+ it("does not affect the parent Dropdown element", () => {
172
+ const input = `
173
+ import { Dropdown } from "@planningcenter/tapestry-react"
174
+
175
+ export default function Test() {
176
+ return (
177
+ <Dropdown title="Actions">
178
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
179
+ </Dropdown>
180
+ )
181
+ }
182
+ `.trim()
183
+
184
+ const result = applyTransform(input)
185
+ expect(result).toContain("<Dropdown ")
186
+ expect(result).toContain("</Dropdown>")
187
+ })
188
+
189
+ it("does not affect Dropdown.Link elements", () => {
190
+ const input = `
191
+ import { Dropdown } from "@planningcenter/tapestry-react"
192
+
193
+ export default function Test() {
194
+ return (
195
+ <Dropdown title="Actions">
196
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
197
+ <Dropdown.Link to="/docs">Docs</Dropdown.Link>
198
+ </Dropdown>
199
+ )
200
+ }
201
+ `.trim()
202
+
203
+ const result = applyTransform(input)
204
+ expect(result).toContain("Dropdown.Link")
205
+ expect(result).not.toContain("Dropdown.Item")
206
+ })
207
+
208
+ it("does not touch the import statement", () => {
209
+ const input = `
210
+ import { Dropdown } from "@planningcenter/tapestry-react"
211
+
212
+ export default function Test() {
213
+ return (
214
+ <Dropdown title="Actions">
215
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
216
+ </Dropdown>
217
+ )
218
+ }
219
+ `.trim()
220
+
221
+ const result = applyTransform(input)
222
+ expect(result).toContain(
223
+ 'import { Dropdown } from "@planningcenter/tapestry-react"'
224
+ )
225
+ })
226
+ })
227
+
228
+ describe("multiple items", () => {
229
+ it("renames all Dropdown.Item instances", () => {
230
+ const input = `
231
+ import { Dropdown } from "@planningcenter/tapestry-react"
232
+
233
+ export default function Test() {
234
+ return (
235
+ <Dropdown title="Bulk edit options">
236
+ <Dropdown.Item onSelect={handleBulkEdit}>Edit groups…</Dropdown.Item>
237
+ <Dropdown.Item onSelect={handleBulkArchive}>Archive groups…</Dropdown.Item>
238
+ <Dropdown.Item onSelect={openBulkCreateEventModal}>Create events…</Dropdown.Item>
239
+ </Dropdown>
240
+ )
241
+ }
242
+ `.trim()
243
+
244
+ const result = applyTransform(input)
245
+ expect(result).not.toContain("Dropdown.Item")
246
+ const actionCount = (result?.match(/<DropdownAction/g) || []).length
247
+ expect(actionCount).toBe(3)
248
+ })
249
+
250
+ it("handles items built with .map()", () => {
251
+ const input = `
252
+ import { Dropdown } from "@planningcenter/tapestry-react"
253
+
254
+ export default function Test({ actions }) {
255
+ return (
256
+ <Dropdown title="Actions">
257
+ {actions.map(action => (
258
+ <Dropdown.Item key={action.id} value={action.id} onSelect={action.fn}>
259
+ {action.label}
260
+ </Dropdown.Item>
261
+ ))}
262
+ </Dropdown>
263
+ )
264
+ }
265
+ `.trim()
266
+
267
+ const result = applyTransform(input)
268
+ expect(result).toContain("<DropdownAction")
269
+ expect(result).toContain("id={action.id}")
270
+ expect(result).toContain("onAction={action.fn}")
271
+ expect(result).not.toContain("Dropdown.Item")
272
+ })
273
+ })
274
+
275
+ describe("aliased import", () => {
276
+ it("handles aliased Dropdown import", () => {
277
+ const input = `
278
+ import { Dropdown as TapestryDropdown } from "@planningcenter/tapestry-react"
279
+
280
+ export default function Test() {
281
+ return (
282
+ <TapestryDropdown title="Actions">
283
+ <TapestryDropdown.Item onSelect={fn}>Edit</TapestryDropdown.Item>
284
+ </TapestryDropdown>
285
+ )
286
+ }
287
+ `.trim()
288
+
289
+ const result = applyTransform(input)
290
+ expect(result).toContain("<DropdownAction")
291
+ expect(result).toContain("onAction={fn}")
292
+ expect(result).not.toContain("TapestryDropdown.Item")
293
+ })
294
+ })
295
+
296
+ describe("no changes scenarios", () => {
297
+ it("returns null when Dropdown is not imported from tapestry-react", () => {
298
+ const input = `
299
+ import { Dropdown } from "some-other-library"
300
+
301
+ export default function Test() {
302
+ return (
303
+ <Dropdown>
304
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
305
+ </Dropdown>
306
+ )
307
+ }
308
+ `.trim()
309
+
310
+ const result = applyTransform(input)
311
+ expect(result).toBe(null)
312
+ })
313
+
314
+ it("returns null when no Dropdown.Item is used", () => {
315
+ const input = `
316
+ import { Dropdown } from "@planningcenter/tapestry-react"
317
+
318
+ export default function Test() {
319
+ return (
320
+ <Dropdown title="Actions">
321
+ <Dropdown.Link to="/docs">Docs</Dropdown.Link>
322
+ </Dropdown>
323
+ )
324
+ }
325
+ `.trim()
326
+
327
+ const result = applyTransform(input)
328
+ expect(result).toBe(null)
329
+ })
330
+
331
+ it("returns null for empty file", () => {
332
+ expect(applyTransform("")).toBe(null)
333
+ })
334
+ })
335
+ })
@@ -0,0 +1,34 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { transformAttributeName } from "../../shared/actions/transformAttributeName"
4
+ import { transformElementName } from "../../shared/actions/transformElementName"
5
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
6
+
7
+ const transform: Transform = attributeTransformFactory({
8
+ targetComponent: "Dropdown.Item",
9
+ targetPackage: "@planningcenter/tapestry-react",
10
+ transform: (element, { j, options }) => {
11
+ const onSelectRenamed = transformAttributeName("onSelect", "onAction", {
12
+ element,
13
+ j,
14
+ options,
15
+ })
16
+ const valueRenamed = transformAttributeName("value", "id", {
17
+ element,
18
+ j,
19
+ options,
20
+ })
21
+ const textRenamed = transformAttributeName("text", "textValue", {
22
+ element,
23
+ j,
24
+ options,
25
+ })
26
+ const elementRenamed = transformElementName({
27
+ element,
28
+ name: "DropdownAction",
29
+ })
30
+ return onSelectRenamed || valueRenamed || textRenamed || elementRenamed
31
+ },
32
+ })
33
+
34
+ export default transform