@planningcenter/tapestry-migration-cli 3.4.1-rc.1 → 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,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
@@ -0,0 +1,248 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./linkToLink"
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("linkToLink transform", () => {
18
+ describe("element renaming", () => {
19
+ it("renames Dropdown.Link to DropdownLink", () => {
20
+ const input = `
21
+ import { Dropdown } from "@planningcenter/tapestry-react"
22
+
23
+ export default function Test() {
24
+ return (
25
+ <Dropdown title="Nav">
26
+ <Dropdown.Link to="/docs">Docs</Dropdown.Link>
27
+ </Dropdown>
28
+ )
29
+ }
30
+ `.trim()
31
+
32
+ const result = applyTransform(input)
33
+ expect(result).toContain("<DropdownLink")
34
+ expect(result).toContain("</DropdownLink>")
35
+ expect(result).not.toContain("Dropdown.Link")
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="Nav">
45
+ <Dropdown.Link to="/docs">Documentation</Dropdown.Link>
46
+ </Dropdown>
47
+ )
48
+ }
49
+ `.trim()
50
+
51
+ const result = applyTransform(input)
52
+ expect(result).toContain("Documentation")
53
+ })
54
+ })
55
+
56
+ describe("prop renaming", () => {
57
+ it("renames to to href", () => {
58
+ const input = `
59
+ import { Dropdown } from "@planningcenter/tapestry-react"
60
+
61
+ export default function Test() {
62
+ return (
63
+ <Dropdown title="Nav">
64
+ <Dropdown.Link to="/docs">Docs</Dropdown.Link>
65
+ </Dropdown>
66
+ )
67
+ }
68
+ `.trim()
69
+
70
+ const result = applyTransform(input)
71
+ expect(result).toContain('href="/docs"')
72
+ expect(result).not.toContain("to=")
73
+ })
74
+
75
+ it("renames dynamic to expression to href", () => {
76
+ const input = `
77
+ import { Dropdown } from "@planningcenter/tapestry-react"
78
+
79
+ export default function Test() {
80
+ return (
81
+ <Dropdown title="Nav">
82
+ <Dropdown.Link to={docsPath()}>Docs</Dropdown.Link>
83
+ </Dropdown>
84
+ )
85
+ }
86
+ `.trim()
87
+
88
+ const result = applyTransform(input)
89
+ expect(result).toContain("href={docsPath()}")
90
+ expect(result).not.toContain("to=")
91
+ })
92
+
93
+ it("preserves external prop", () => {
94
+ const input = `
95
+ import { Dropdown } from "@planningcenter/tapestry-react"
96
+
97
+ export default function Test() {
98
+ return (
99
+ <Dropdown title="Nav">
100
+ <Dropdown.Link to="https://example.com" external>External</Dropdown.Link>
101
+ </Dropdown>
102
+ )
103
+ }
104
+ `.trim()
105
+
106
+ const result = applyTransform(input)
107
+ expect(result).toContain("external")
108
+ expect(result).toContain("<DropdownLink")
109
+ })
110
+
111
+ it("preserves unrecognised props", () => {
112
+ const input = `
113
+ import { Dropdown } from "@planningcenter/tapestry-react"
114
+
115
+ export default function Test() {
116
+ return (
117
+ <Dropdown title="Nav">
118
+ <Dropdown.Link to="/docs" data-pendo="nav-docs">Docs</Dropdown.Link>
119
+ </Dropdown>
120
+ )
121
+ }
122
+ `.trim()
123
+
124
+ const result = applyTransform(input)
125
+ expect(result).toContain('data-pendo="nav-docs"')
126
+ })
127
+ })
128
+
129
+ describe("multiple links", () => {
130
+ it("renames all Dropdown.Link elements", () => {
131
+ const input = `
132
+ import { Dropdown } from "@planningcenter/tapestry-react"
133
+
134
+ export default function Test() {
135
+ return (
136
+ <Dropdown title="Nav">
137
+ <Dropdown.Link to="/docs">Docs</Dropdown.Link>
138
+ <Dropdown.Link to="/status" external>Status</Dropdown.Link>
139
+ </Dropdown>
140
+ )
141
+ }
142
+ `.trim()
143
+
144
+ const result = applyTransform(input)
145
+ expect(result).not.toContain("Dropdown.Link")
146
+ const linkCount = (result?.match(/<DropdownLink/g) || []).length
147
+ expect(linkCount).toBe(2)
148
+ })
149
+ })
150
+
151
+ describe("isolation", () => {
152
+ it("does not affect the parent Dropdown element", () => {
153
+ const input = `
154
+ import { Dropdown } from "@planningcenter/tapestry-react"
155
+
156
+ export default function Test() {
157
+ return (
158
+ <Dropdown title="Nav">
159
+ <Dropdown.Link to="/docs">Docs</Dropdown.Link>
160
+ </Dropdown>
161
+ )
162
+ }
163
+ `.trim()
164
+
165
+ const result = applyTransform(input)
166
+ expect(result).toContain("<Dropdown ")
167
+ expect(result).toContain("</Dropdown>")
168
+ })
169
+
170
+ it("does not affect Dropdown.Item elements", () => {
171
+ const input = `
172
+ import { Dropdown } from "@planningcenter/tapestry-react"
173
+
174
+ export default function Test() {
175
+ return (
176
+ <Dropdown title="Nav">
177
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
178
+ <Dropdown.Link to="/docs">Docs</Dropdown.Link>
179
+ </Dropdown>
180
+ )
181
+ }
182
+ `.trim()
183
+
184
+ const result = applyTransform(input)
185
+ expect(result).toContain("Dropdown.Item")
186
+ expect(result).toContain("<DropdownLink")
187
+ })
188
+
189
+ it("does not touch the import statement", () => {
190
+ const input = `
191
+ import { Dropdown } from "@planningcenter/tapestry-react"
192
+
193
+ export default function Test() {
194
+ return (
195
+ <Dropdown title="Nav">
196
+ <Dropdown.Link to="/docs">Docs</Dropdown.Link>
197
+ </Dropdown>
198
+ )
199
+ }
200
+ `.trim()
201
+
202
+ const result = applyTransform(input)
203
+ expect(result).toContain(
204
+ 'import { Dropdown } from "@planningcenter/tapestry-react"'
205
+ )
206
+ })
207
+ })
208
+
209
+ describe("no changes scenarios", () => {
210
+ it("returns null when Dropdown is not imported from tapestry-react", () => {
211
+ const input = `
212
+ import { Dropdown } from "some-other-library"
213
+
214
+ export default function Test() {
215
+ return (
216
+ <Dropdown>
217
+ <Dropdown.Link to="/docs">Docs</Dropdown.Link>
218
+ </Dropdown>
219
+ )
220
+ }
221
+ `.trim()
222
+
223
+ const result = applyTransform(input)
224
+ expect(result).toBe(null)
225
+ })
226
+
227
+ it("returns null when no Dropdown.Link is present", () => {
228
+ const input = `
229
+ import { Dropdown } from "@planningcenter/tapestry-react"
230
+
231
+ export default function Test() {
232
+ return (
233
+ <Dropdown title="Nav">
234
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
235
+ </Dropdown>
236
+ )
237
+ }
238
+ `.trim()
239
+
240
+ const result = applyTransform(input)
241
+ expect(result).toBe(null)
242
+ })
243
+
244
+ it("returns null for empty file", () => {
245
+ expect(applyTransform("")).toBe(null)
246
+ })
247
+ })
248
+ })
@@ -0,0 +1,24 @@
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.Link",
9
+ targetPackage: "@planningcenter/tapestry-react",
10
+ transform: (element, { j, options }) => {
11
+ const toRenamed = transformAttributeName("to", "href", {
12
+ element,
13
+ j,
14
+ options,
15
+ })
16
+ const elementRenamed = transformElementName({
17
+ element,
18
+ name: "DropdownLink",
19
+ })
20
+ return toRenamed || elementRenamed
21
+ },
22
+ })
23
+
24
+ export default transform