@planningcenter/tapestry-migration-cli 2.1.1-rc.0 → 2.1.1-rc.2
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 +6 -4
- package/src/components/button/index.ts +3 -1
- package/src/components/button/transforms/linkToButton.test.ts +426 -0
- package/src/components/button/transforms/linkToButton.ts +20 -0
- package/src/components/shared/componentTransformUtilities.test.ts +437 -0
- package/src/components/shared/componentTransformUtilities.ts +280 -0
- package/src/components/shared/getTapestryReactImportName.ts +35 -0
- package/src/components/shared/transformConfig.test.ts +288 -0
- package/src/components/shared/transformConfig.ts +79 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/tapestry-migration-cli",
|
|
3
|
-
"version": "2.1.1-rc.
|
|
3
|
+
"version": "2.1.1-rc.2",
|
|
4
4
|
"description": "CLI tool for Tapestry migrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"dev": "tsc --watch",
|
|
13
|
-
"test": "
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
14
15
|
"prepublishOnly": "npm run build"
|
|
15
16
|
},
|
|
16
17
|
"keywords": [
|
|
@@ -32,7 +33,8 @@
|
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/jscodeshift": "^17.3.0",
|
|
34
35
|
"@types/node": "^20.0.0",
|
|
35
|
-
"typescript": "^5.8.3"
|
|
36
|
+
"typescript": "^5.8.3",
|
|
37
|
+
"vitest": "^3.0.0"
|
|
36
38
|
},
|
|
37
39
|
"files": [
|
|
38
40
|
"src/**/*"
|
|
@@ -45,5 +47,5 @@
|
|
|
45
47
|
"publishConfig": {
|
|
46
48
|
"access": "public"
|
|
47
49
|
},
|
|
48
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "5b408e235d4603be13e17be3117fb2ca454078f7"
|
|
49
51
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Transform } from "jscodeshift"
|
|
2
2
|
|
|
3
|
+
import linkToButton from "./transforms/linkToButton"
|
|
4
|
+
|
|
3
5
|
const transform: Transform = (fileInfo, api, options) => {
|
|
4
6
|
let currentSource = fileInfo.source
|
|
5
7
|
let hasAnyChanges = false
|
|
6
8
|
|
|
7
|
-
const transforms: Transform[] = []
|
|
9
|
+
const transforms: Transform[] = [linkToButton]
|
|
8
10
|
|
|
9
11
|
for (const individualTransform of transforms) {
|
|
10
12
|
const result = individualTransform(
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./linkToButton"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
// Helper to run transform and get result
|
|
9
|
+
function runTransform(input: string): string | null {
|
|
10
|
+
const fileInfo = { path: "test.tsx", source: input }
|
|
11
|
+
const api = {
|
|
12
|
+
j,
|
|
13
|
+
jscodeshift: j,
|
|
14
|
+
report: () => {},
|
|
15
|
+
stats: () => {},
|
|
16
|
+
}
|
|
17
|
+
const result = transform(fileInfo, api, {})
|
|
18
|
+
return result as string | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("linkToButton transform", () => {
|
|
22
|
+
describe("basic transformation", () => {
|
|
23
|
+
it("should transform Link with onClick to Button", () => {
|
|
24
|
+
const input = `
|
|
25
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
26
|
+
|
|
27
|
+
function Component() {
|
|
28
|
+
return <Link onClick={handleClick}>Click me</Link>
|
|
29
|
+
}
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
const result = runTransform(input)
|
|
33
|
+
|
|
34
|
+
expect(result).toContain(
|
|
35
|
+
"<Button onClick={handleClick}>Click me</Button>"
|
|
36
|
+
)
|
|
37
|
+
expect(result).toContain(
|
|
38
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
39
|
+
)
|
|
40
|
+
expect(result).not.toContain("Link")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("should preserve Link without onClick", () => {
|
|
44
|
+
const input = `
|
|
45
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
46
|
+
|
|
47
|
+
function Component() {
|
|
48
|
+
return <Link href="/test">Go to test</Link>
|
|
49
|
+
}
|
|
50
|
+
`
|
|
51
|
+
|
|
52
|
+
const result = runTransform(input)
|
|
53
|
+
|
|
54
|
+
expect(result).toBeNull()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("should handle mixed Link usage", () => {
|
|
58
|
+
const input = `
|
|
59
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
60
|
+
|
|
61
|
+
function Component() {
|
|
62
|
+
return (
|
|
63
|
+
<div>
|
|
64
|
+
<Link onClick={handleClick}>Click me</Link>
|
|
65
|
+
<Link href="/home">Home</Link>
|
|
66
|
+
<Link onClick={handleOther}>Other action</Link>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
`
|
|
71
|
+
|
|
72
|
+
const result = runTransform(input)
|
|
73
|
+
|
|
74
|
+
expect(result).toContain(
|
|
75
|
+
"<Button onClick={handleClick}>Click me</Button>"
|
|
76
|
+
)
|
|
77
|
+
expect(result).toContain('<Link href="/home">Home</Link>')
|
|
78
|
+
expect(result).toContain(
|
|
79
|
+
"<Button onClick={handleOther}>Other action</Button>"
|
|
80
|
+
)
|
|
81
|
+
expect(result).toContain(
|
|
82
|
+
'import { Link, Button } from "@planningcenter/tapestry-react"'
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe("import management", () => {
|
|
88
|
+
it("should remove Link import when no longer used", () => {
|
|
89
|
+
const input = `
|
|
90
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
91
|
+
|
|
92
|
+
function Component() {
|
|
93
|
+
return <Link onClick={handleClick}>Button</Link>
|
|
94
|
+
}
|
|
95
|
+
`
|
|
96
|
+
|
|
97
|
+
const result = runTransform(input)
|
|
98
|
+
|
|
99
|
+
expect(result).toContain(
|
|
100
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
101
|
+
)
|
|
102
|
+
expect(result).not.toContain("Link")
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("should keep Link import when still used", () => {
|
|
106
|
+
const input = `
|
|
107
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
108
|
+
|
|
109
|
+
function Component() {
|
|
110
|
+
return (
|
|
111
|
+
<div>
|
|
112
|
+
<Link onClick={handleClick}>Button</Link>
|
|
113
|
+
<Link href="/test">Link</Link>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
`
|
|
118
|
+
|
|
119
|
+
const result = runTransform(input)
|
|
120
|
+
|
|
121
|
+
expect(result).toContain(
|
|
122
|
+
'import { Link, Button } from "@planningcenter/tapestry-react"'
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it("should add Button to existing imports", () => {
|
|
127
|
+
const input = `
|
|
128
|
+
import { Link, Input } from "@planningcenter/tapestry-react"
|
|
129
|
+
|
|
130
|
+
function Component() {
|
|
131
|
+
return (
|
|
132
|
+
<div>
|
|
133
|
+
<Input />
|
|
134
|
+
<Link onClick={handleClick}>Button</Link>
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
`
|
|
139
|
+
|
|
140
|
+
const result = runTransform(input)
|
|
141
|
+
|
|
142
|
+
expect(result).toContain(
|
|
143
|
+
'import { Input, Button } from "@planningcenter/tapestry-react"'
|
|
144
|
+
)
|
|
145
|
+
expect(result).not.toContain("Link")
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("should handle existing Button import", () => {
|
|
149
|
+
const input = `
|
|
150
|
+
import { Link, Button } from "@planningcenter/tapestry-react"
|
|
151
|
+
|
|
152
|
+
function Component() {
|
|
153
|
+
return (
|
|
154
|
+
<div>
|
|
155
|
+
<Button type="submit">Submit</Button>
|
|
156
|
+
<Link onClick={handleClick}>Clickable</Link>
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
`
|
|
161
|
+
|
|
162
|
+
const result = runTransform(input)
|
|
163
|
+
|
|
164
|
+
expect(result).toContain('<Button type="submit">Submit</Button>')
|
|
165
|
+
expect(result).toContain(
|
|
166
|
+
"<Button onClick={handleClick}>Clickable</Button>"
|
|
167
|
+
)
|
|
168
|
+
expect(result).toContain(
|
|
169
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
170
|
+
)
|
|
171
|
+
expect(result).not.toContain("Link")
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe("component attributes", () => {
|
|
176
|
+
it("should preserve all attributes", () => {
|
|
177
|
+
const input = `
|
|
178
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
179
|
+
|
|
180
|
+
function Component() {
|
|
181
|
+
return (
|
|
182
|
+
<Link
|
|
183
|
+
onClick={handleClick}
|
|
184
|
+
className="link-button"
|
|
185
|
+
disabled
|
|
186
|
+
data-testid="clickable-link"
|
|
187
|
+
role="button"
|
|
188
|
+
>
|
|
189
|
+
Click me
|
|
190
|
+
</Link>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
`
|
|
194
|
+
|
|
195
|
+
const result = runTransform(input)
|
|
196
|
+
|
|
197
|
+
expect(result).toContain("onClick={handleClick}")
|
|
198
|
+
expect(result).toContain('className="link-button"')
|
|
199
|
+
expect(result).toContain("disabled")
|
|
200
|
+
expect(result).toContain('data-testid="clickable-link"')
|
|
201
|
+
expect(result).toContain('role="button"')
|
|
202
|
+
expect(result).toContain("<Button")
|
|
203
|
+
expect(result).toContain("</Button>")
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it("should handle self-closing tags", () => {
|
|
207
|
+
const input = `
|
|
208
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
209
|
+
|
|
210
|
+
function Component() {
|
|
211
|
+
return <Link onClick={handleClick} />
|
|
212
|
+
}
|
|
213
|
+
`
|
|
214
|
+
|
|
215
|
+
const result = runTransform(input)
|
|
216
|
+
|
|
217
|
+
expect(result).toContain("<Button onClick={handleClick} />")
|
|
218
|
+
expect(result).not.toContain("Link")
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("should handle complex onClick expressions", () => {
|
|
222
|
+
const input = `
|
|
223
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
224
|
+
|
|
225
|
+
function Component() {
|
|
226
|
+
return <Link onClick={() => handleAction(id, 'delete')}>Delete</Link>
|
|
227
|
+
}
|
|
228
|
+
`
|
|
229
|
+
|
|
230
|
+
const result = runTransform(input)
|
|
231
|
+
|
|
232
|
+
expect(result).toContain("onClick={() => handleAction(id, 'delete')}")
|
|
233
|
+
expect(result).toContain("<Button")
|
|
234
|
+
expect(result).toContain("Delete</Button>")
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
describe("edge cases", () => {
|
|
239
|
+
it("should return null when no Link imported from tapestry-react", () => {
|
|
240
|
+
const input = `
|
|
241
|
+
import { Link } from "react-router-dom"
|
|
242
|
+
|
|
243
|
+
function Component() {
|
|
244
|
+
return <Link onClick={handleClick}>Button</Link>
|
|
245
|
+
}
|
|
246
|
+
`
|
|
247
|
+
|
|
248
|
+
const result = runTransform(input)
|
|
249
|
+
|
|
250
|
+
expect(result).toBeNull()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it("should return null when no Link with onClick found", () => {
|
|
254
|
+
const input = `
|
|
255
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
256
|
+
|
|
257
|
+
function Component() {
|
|
258
|
+
return <Link href="/test">Go to test</Link>
|
|
259
|
+
}
|
|
260
|
+
`
|
|
261
|
+
|
|
262
|
+
const result = runTransform(input)
|
|
263
|
+
|
|
264
|
+
expect(result).toBeNull()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it("should handle aliased Link import", () => {
|
|
268
|
+
const input = `
|
|
269
|
+
import { Link as TapestryLink } from "@planningcenter/tapestry-react"
|
|
270
|
+
|
|
271
|
+
function Component() {
|
|
272
|
+
return <TapestryLink onClick={handleClick}>Button</TapestryLink>
|
|
273
|
+
}
|
|
274
|
+
`
|
|
275
|
+
|
|
276
|
+
const result = runTransform(input)
|
|
277
|
+
|
|
278
|
+
expect(result).toContain("<Button onClick={handleClick}>Button</Button>")
|
|
279
|
+
expect(result).toContain(
|
|
280
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
281
|
+
)
|
|
282
|
+
expect(result).not.toContain("TapestryLink")
|
|
283
|
+
expect(result).not.toContain("Link")
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it("should handle nested JSX structures", () => {
|
|
287
|
+
const input = `
|
|
288
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
289
|
+
|
|
290
|
+
function Component() {
|
|
291
|
+
return (
|
|
292
|
+
<div className="container">
|
|
293
|
+
<Link onClick={handleDelete}>
|
|
294
|
+
<span className="icon">🗑️</span>
|
|
295
|
+
<span>Delete</span>
|
|
296
|
+
</Link>
|
|
297
|
+
</div>
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
`
|
|
301
|
+
|
|
302
|
+
const result = runTransform(input)
|
|
303
|
+
|
|
304
|
+
expect(result).toContain("<Button onClick={handleDelete}>")
|
|
305
|
+
expect(result).toContain('<span className="icon">🗑️</span>')
|
|
306
|
+
expect(result).toContain("<span>Delete</span>")
|
|
307
|
+
expect(result).toContain("</Button>")
|
|
308
|
+
expect(result).not.toContain("Link")
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it("should handle multiple onClick links in same component", () => {
|
|
312
|
+
const input = `
|
|
313
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
314
|
+
|
|
315
|
+
function Actions() {
|
|
316
|
+
return (
|
|
317
|
+
<div>
|
|
318
|
+
<Link onClick={handleSave}>Save</Link>
|
|
319
|
+
<Link onClick={handleCancel}>Cancel</Link>
|
|
320
|
+
<Link onClick={handleDelete}>Delete</Link>
|
|
321
|
+
</div>
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
`
|
|
325
|
+
|
|
326
|
+
const result = runTransform(input)
|
|
327
|
+
|
|
328
|
+
expect(result).toContain("<Button onClick={handleSave}>Save</Button>")
|
|
329
|
+
expect(result).toContain("<Button onClick={handleCancel}>Cancel</Button>")
|
|
330
|
+
expect(result).toContain("<Button onClick={handleDelete}>Delete</Button>")
|
|
331
|
+
expect(result).toContain(
|
|
332
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
333
|
+
)
|
|
334
|
+
expect(result).not.toContain("Link")
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it("should handle Link with both href and onClick (should transform)", () => {
|
|
338
|
+
const input = `
|
|
339
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
340
|
+
|
|
341
|
+
function Component() {
|
|
342
|
+
return <Link href="/test" onClick={handleClick}>Link with both</Link>
|
|
343
|
+
}
|
|
344
|
+
`
|
|
345
|
+
|
|
346
|
+
const result = runTransform(input)
|
|
347
|
+
|
|
348
|
+
// Should transform because it has onClick (href is ignored for this transform)
|
|
349
|
+
expect(result).toContain(
|
|
350
|
+
'<Button href="/test" onClick={handleClick}>Link with both</Button>'
|
|
351
|
+
)
|
|
352
|
+
expect(result).toContain(
|
|
353
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
354
|
+
)
|
|
355
|
+
expect(result).not.toContain("import { Link }")
|
|
356
|
+
expect(result).not.toContain("<Link")
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it("should not transform Link without onClick even if it has other handlers", () => {
|
|
360
|
+
const input = `
|
|
361
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
362
|
+
|
|
363
|
+
function Component() {
|
|
364
|
+
return <Link href="/test" onMouseOver={handleHover}>Hover me</Link>
|
|
365
|
+
}
|
|
366
|
+
`
|
|
367
|
+
|
|
368
|
+
const result = runTransform(input)
|
|
369
|
+
|
|
370
|
+
expect(result).toBeNull()
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
describe("conflict resolution", () => {
|
|
375
|
+
it("should use TRButton alias when Button conflicts with existing import", () => {
|
|
376
|
+
const input = `
|
|
377
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
378
|
+
import { Button } from "other-ui-lib"
|
|
379
|
+
|
|
380
|
+
function Component() {
|
|
381
|
+
return (
|
|
382
|
+
<div>
|
|
383
|
+
<Link onClick={handleClick}>Tapestry Button</Link>
|
|
384
|
+
<Button variant="primary">Other Button</Button>
|
|
385
|
+
</div>
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
`
|
|
389
|
+
|
|
390
|
+
const result = runTransform(input)
|
|
391
|
+
|
|
392
|
+
expect(result).toContain(
|
|
393
|
+
"<TRButton onClick={handleClick}>Tapestry Button</TRButton>"
|
|
394
|
+
)
|
|
395
|
+
expect(result).toContain(
|
|
396
|
+
'<Button variant="primary">Other Button</Button>'
|
|
397
|
+
)
|
|
398
|
+
expect(result).toContain('import { Button } from "other-ui-lib"')
|
|
399
|
+
expect(result).toContain(
|
|
400
|
+
'import { Button as TRButton } from "@planningcenter/tapestry-react"'
|
|
401
|
+
)
|
|
402
|
+
expect(result).not.toContain("import { Link }")
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
describe("detecting renamed import of Link", () => {
|
|
407
|
+
it("should handle renamed Link import", () => {
|
|
408
|
+
const input = `
|
|
409
|
+
import { Link as TapestryLink } from "@planningcenter/tapestry-react"
|
|
410
|
+
|
|
411
|
+
function Component() {
|
|
412
|
+
return <TapestryLink onClick={handleClick}>Button</TapestryLink>
|
|
413
|
+
}
|
|
414
|
+
`
|
|
415
|
+
|
|
416
|
+
const result = runTransform(input)
|
|
417
|
+
|
|
418
|
+
expect(result).toContain("<Button onClick={handleClick}>Button</Button>")
|
|
419
|
+
expect(result).toContain(
|
|
420
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
421
|
+
)
|
|
422
|
+
expect(result).not.toContain("TapestryLink")
|
|
423
|
+
expect(result).not.toContain("Link")
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { createComponentTransform } from "../../shared/componentTransformUtilities"
|
|
4
|
+
import {
|
|
5
|
+
ComponentTransformConfig,
|
|
6
|
+
hasAttribute,
|
|
7
|
+
} from "../../shared/transformConfig"
|
|
8
|
+
|
|
9
|
+
const config: ComponentTransformConfig = {
|
|
10
|
+
condition: hasAttribute("onClick"),
|
|
11
|
+
conflictAlias: "TRButton",
|
|
12
|
+
fromComponent: "Link",
|
|
13
|
+
fromPackage: "@planningcenter/tapestry-react",
|
|
14
|
+
toComponent: "Button",
|
|
15
|
+
toPackage: "@planningcenter/tapestry-react",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const transform: Transform = createComponentTransform(config)
|
|
19
|
+
|
|
20
|
+
export default transform
|