@planningcenter/tapestry-migration-cli 2.2.0-rc.0 → 2.2.0-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 +2 -2
- package/src/components/button/index.ts +2 -1
- package/src/components/button/transforms/moveButtonImport.test.ts +293 -0
- package/src/components/button/transforms/moveButtonImport.ts +14 -0
- package/src/components/shared/transformFactories/componentTransformFactory.ts +1 -2
- package/src/components/shared/transformFactories/helpers/manageImports.ts +16 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/tapestry-migration-cli",
|
|
3
|
-
"version": "2.2.0-rc.
|
|
3
|
+
"version": "2.2.0-rc.2",
|
|
4
4
|
"description": "CLI tool for Tapestry migrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "7f90b9957e693f9cd1e601ea0e74ab4d087eeb91"
|
|
51
51
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Transform } from "jscodeshift"
|
|
2
2
|
|
|
3
3
|
import linkToButton from "./transforms/linkToButton"
|
|
4
|
+
import moveButtonImport from "./transforms/moveButtonImport"
|
|
4
5
|
import titleToLabel from "./transforms/titleToLabel"
|
|
5
6
|
|
|
6
7
|
const transform: Transform = (fileInfo, api, options) => {
|
|
7
8
|
let currentSource = fileInfo.source
|
|
8
9
|
let hasAnyChanges = false
|
|
9
10
|
|
|
10
|
-
const transforms: Transform[] = [linkToButton, titleToLabel]
|
|
11
|
+
const transforms: Transform[] = [linkToButton, titleToLabel, moveButtonImport]
|
|
11
12
|
|
|
12
13
|
for (const individualTransform of transforms) {
|
|
13
14
|
const result = individualTransform(
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./moveButtonImport"
|
|
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("moveButtonImport transform", () => {
|
|
18
|
+
describe("basic transformations", () => {
|
|
19
|
+
it("should move Button import from tapestry-react to tapestry", () => {
|
|
20
|
+
const input = `
|
|
21
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
export default function Test() {
|
|
24
|
+
return <Button>Test</Button>
|
|
25
|
+
}
|
|
26
|
+
`.trim()
|
|
27
|
+
|
|
28
|
+
const expected = `
|
|
29
|
+
import { Button } from "@planningcenter/tapestry";
|
|
30
|
+
|
|
31
|
+
export default function Test() {
|
|
32
|
+
return <Button>Test</Button>
|
|
33
|
+
}
|
|
34
|
+
`.trim()
|
|
35
|
+
|
|
36
|
+
const result = applyTransform(input)
|
|
37
|
+
expect(result?.trim()).toBe(expected)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("should handle multiple Button components", () => {
|
|
41
|
+
const input = `
|
|
42
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
43
|
+
|
|
44
|
+
export default function Test() {
|
|
45
|
+
return (
|
|
46
|
+
<div>
|
|
47
|
+
<Button>Save</Button>
|
|
48
|
+
<Button>Cancel</Button>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
`.trim()
|
|
53
|
+
|
|
54
|
+
const expected = `
|
|
55
|
+
import { Button } from "@planningcenter/tapestry";
|
|
56
|
+
|
|
57
|
+
export default function Test() {
|
|
58
|
+
return (
|
|
59
|
+
<div>
|
|
60
|
+
<Button>Save</Button>
|
|
61
|
+
<Button>Cancel</Button>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
`.trim()
|
|
66
|
+
|
|
67
|
+
const result = applyTransform(input)
|
|
68
|
+
expect(result?.trim()).toBe(expected)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("should preserve other imports from tapestry-react", () => {
|
|
72
|
+
const input = `
|
|
73
|
+
import { Button, Link, Input } from "@planningcenter/tapestry-react"
|
|
74
|
+
|
|
75
|
+
export default function Test() {
|
|
76
|
+
return <Button>Test</Button>
|
|
77
|
+
}
|
|
78
|
+
`.trim()
|
|
79
|
+
|
|
80
|
+
const result = applyTransform(input)
|
|
81
|
+
expect(result).toContain(
|
|
82
|
+
'import { Link, Input } from "@planningcenter/tapestry-react"'
|
|
83
|
+
)
|
|
84
|
+
expect(result).toContain(
|
|
85
|
+
'import { Button } from "@planningcenter/tapestry"'
|
|
86
|
+
)
|
|
87
|
+
expect(result).toContain("<Button>Test</Button>")
|
|
88
|
+
expect(result).not.toContain(
|
|
89
|
+
'Button, Link } from "@planningcenter/tapestry-react"'
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe("import conflict handling", () => {
|
|
95
|
+
it("should use alias when Button already imported from tapestry", () => {
|
|
96
|
+
const input = `
|
|
97
|
+
import { Button } from "@planningcenter/tapestry"
|
|
98
|
+
import { Button as ReactButton } from "@planningcenter/tapestry-react"
|
|
99
|
+
|
|
100
|
+
export default function Test() {
|
|
101
|
+
return <ReactButton>Test</ReactButton>
|
|
102
|
+
}
|
|
103
|
+
`.trim()
|
|
104
|
+
|
|
105
|
+
const result = applyTransform(input)
|
|
106
|
+
expect(result).toContain(
|
|
107
|
+
'import { Button } from "@planningcenter/tapestry"'
|
|
108
|
+
)
|
|
109
|
+
expect(result).toContain("<Button>Test</Button>")
|
|
110
|
+
expect(result).not.toContain('from "@planningcenter/tapestry-react"')
|
|
111
|
+
expect(result).not.toContain("ReactButton")
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("should handle existing tapestry import with other components", () => {
|
|
115
|
+
const input = `
|
|
116
|
+
import { Link, Input } from "@planningcenter/tapestry"
|
|
117
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
118
|
+
|
|
119
|
+
export default function Test() {
|
|
120
|
+
return <Button>Test</Button>
|
|
121
|
+
}
|
|
122
|
+
`.trim()
|
|
123
|
+
|
|
124
|
+
const result = applyTransform(input)
|
|
125
|
+
expect(result).toContain(
|
|
126
|
+
'import { Link, Input, Button } from "@planningcenter/tapestry"'
|
|
127
|
+
)
|
|
128
|
+
expect(result).toContain("<Button>Test</Button>")
|
|
129
|
+
expect(result).not.toContain('from "@planningcenter/tapestry-react"')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe("edge cases", () => {
|
|
134
|
+
it("should not transform if Button is not imported from tapestry-react", () => {
|
|
135
|
+
const input = `
|
|
136
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
137
|
+
|
|
138
|
+
export default function Test() {
|
|
139
|
+
return <Link href="/test">Test</Link>
|
|
140
|
+
}
|
|
141
|
+
`.trim()
|
|
142
|
+
|
|
143
|
+
const result = applyTransform(input)
|
|
144
|
+
expect(result).toBe(null)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("should handle Button with aliased import", () => {
|
|
148
|
+
const input = `
|
|
149
|
+
import { Button as ReactButton } from "@planningcenter/tapestry-react"
|
|
150
|
+
|
|
151
|
+
export default function Test() {
|
|
152
|
+
return <ReactButton>Test</ReactButton>
|
|
153
|
+
}
|
|
154
|
+
`.trim()
|
|
155
|
+
|
|
156
|
+
const result = applyTransform(input)
|
|
157
|
+
expect(result).toContain(
|
|
158
|
+
'import { Button } from "@planningcenter/tapestry"'
|
|
159
|
+
)
|
|
160
|
+
expect(result).toContain("<Button>Test</Button>")
|
|
161
|
+
expect(result).not.toContain("ReactButton")
|
|
162
|
+
expect(result).not.toContain('from "@planningcenter/tapestry-react"')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it("should handle self-closing Button components", () => {
|
|
166
|
+
const input = `
|
|
167
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
168
|
+
|
|
169
|
+
export default function Test() {
|
|
170
|
+
return <Button />
|
|
171
|
+
}
|
|
172
|
+
`.trim()
|
|
173
|
+
|
|
174
|
+
const expected = `
|
|
175
|
+
import { Button } from "@planningcenter/tapestry";
|
|
176
|
+
|
|
177
|
+
export default function Test() {
|
|
178
|
+
return <Button />
|
|
179
|
+
}
|
|
180
|
+
`.trim()
|
|
181
|
+
|
|
182
|
+
const result = applyTransform(input)
|
|
183
|
+
expect(result?.trim()).toBe(expected)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it("should handle Button with props", () => {
|
|
187
|
+
const input = `
|
|
188
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
189
|
+
|
|
190
|
+
export default function Test() {
|
|
191
|
+
return <Button kind="primary" onClick={handleClick}>Save</Button>
|
|
192
|
+
}
|
|
193
|
+
`.trim()
|
|
194
|
+
|
|
195
|
+
const result = applyTransform(input)
|
|
196
|
+
expect(result).toContain(
|
|
197
|
+
'import { Button } from "@planningcenter/tapestry"'
|
|
198
|
+
)
|
|
199
|
+
expect(result).toContain('kind="primary"')
|
|
200
|
+
expect(result).toContain("onClick={handleClick}")
|
|
201
|
+
expect(result).not.toContain('from "@planningcenter/tapestry-react"')
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe("no changes scenarios", () => {
|
|
206
|
+
it("should return null when no Button import exists", () => {
|
|
207
|
+
const input = `
|
|
208
|
+
import React from "react"
|
|
209
|
+
|
|
210
|
+
export default function Test() {
|
|
211
|
+
return <div>Hello</div>
|
|
212
|
+
}
|
|
213
|
+
`.trim()
|
|
214
|
+
|
|
215
|
+
const result = applyTransform(input)
|
|
216
|
+
expect(result).toBe(null)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it("should return null when Button already imported from tapestry", () => {
|
|
220
|
+
const input = `
|
|
221
|
+
import { Button } from "@planningcenter/tapestry"
|
|
222
|
+
|
|
223
|
+
export default function Test() {
|
|
224
|
+
return <Button>Test</Button>
|
|
225
|
+
}
|
|
226
|
+
`.trim()
|
|
227
|
+
|
|
228
|
+
const result = applyTransform(input)
|
|
229
|
+
expect(result).toBe(null)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it("should return null for empty file", () => {
|
|
233
|
+
const result = applyTransform("")
|
|
234
|
+
expect(result).toBe(null)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
describe("complex scenarios", () => {
|
|
239
|
+
it("should handle mixed imports and multiple components", () => {
|
|
240
|
+
const input = `
|
|
241
|
+
import React from "react"
|
|
242
|
+
import { Button, Link } from "@planningcenter/tapestry-react"
|
|
243
|
+
import { Input } from "@planningcenter/tapestry"
|
|
244
|
+
|
|
245
|
+
export default function Test() {
|
|
246
|
+
return (
|
|
247
|
+
<form>
|
|
248
|
+
<Input name="email" />
|
|
249
|
+
<Button type="submit">Submit</Button>
|
|
250
|
+
<Link href="/cancel">Cancel</Link>
|
|
251
|
+
</form>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
`.trim()
|
|
255
|
+
|
|
256
|
+
const result = applyTransform(input)
|
|
257
|
+
expect(result).toContain(
|
|
258
|
+
'import { Link } from "@planningcenter/tapestry-react"'
|
|
259
|
+
)
|
|
260
|
+
expect(result).toContain(
|
|
261
|
+
'import { Input, Button } from "@planningcenter/tapestry"'
|
|
262
|
+
)
|
|
263
|
+
expect(result).toContain('<Button type="submit">Submit</Button>')
|
|
264
|
+
expect(result).toContain('<Link href="/cancel">Cancel</Link>')
|
|
265
|
+
expect(result).not.toContain(
|
|
266
|
+
'Button, Link } from "@planningcenter/tapestry-react"'
|
|
267
|
+
)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it("should handle Button usage in JSX expressions", () => {
|
|
271
|
+
const input = `
|
|
272
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
273
|
+
|
|
274
|
+
export default function Test({ showButton }) {
|
|
275
|
+
return (
|
|
276
|
+
<div>
|
|
277
|
+
{showButton && <Button>Conditional</Button>}
|
|
278
|
+
{items.map(item => <Button key={item.id}>{item.name}</Button>)}
|
|
279
|
+
</div>
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
`.trim()
|
|
283
|
+
|
|
284
|
+
const result = applyTransform(input)
|
|
285
|
+
expect(result).toContain(
|
|
286
|
+
'import { Button } from "@planningcenter/tapestry"'
|
|
287
|
+
)
|
|
288
|
+
expect(result).toContain("<Button>Conditional</Button>")
|
|
289
|
+
expect(result).toContain("<Button key={item.id}>{item.name}</Button>")
|
|
290
|
+
expect(result).not.toContain('from "@planningcenter/tapestry-react"')
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { componentTransformFactory } from "../../shared/transformFactories/componentTransformFactory"
|
|
4
|
+
|
|
5
|
+
const transform: Transform = componentTransformFactory({
|
|
6
|
+
condition: () => true,
|
|
7
|
+
conflictAlias: "TButton",
|
|
8
|
+
fromComponent: "Button",
|
|
9
|
+
fromPackage: "@planningcenter/tapestry-react",
|
|
10
|
+
toComponent: "Button",
|
|
11
|
+
toPackage: "@planningcenter/tapestry",
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export default transform
|
|
@@ -43,10 +43,9 @@ export function componentTransformFactory(config: {
|
|
|
43
43
|
return null
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Check for conflicting imports
|
|
47
46
|
const hasConflict = hasConflictingImport(
|
|
48
47
|
config.toComponent,
|
|
49
|
-
config.toPackage,
|
|
48
|
+
[config.fromPackage, config.toPackage],
|
|
50
49
|
{ j, source }
|
|
51
50
|
)
|
|
52
51
|
|
|
@@ -33,15 +33,19 @@ export function getImportName(
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Checks if a component is already imported from sources other than the specified
|
|
36
|
+
* Checks if a component is already imported from sources other than the specified packages
|
|
37
37
|
*/
|
|
38
38
|
export function hasConflictingImport(
|
|
39
39
|
componentName: string,
|
|
40
|
-
|
|
40
|
+
excludePackages: string | string[],
|
|
41
41
|
{ source, j }: { j: JSCodeshift; source: Collection }
|
|
42
42
|
): boolean {
|
|
43
|
+
const excludePackageList = Array.isArray(excludePackages)
|
|
44
|
+
? excludePackages
|
|
45
|
+
: [excludePackages]
|
|
46
|
+
|
|
43
47
|
const conflictingImport = source.find(j.ImportDeclaration).filter((path) => {
|
|
44
|
-
if (path.value.source.value
|
|
48
|
+
if (excludePackageList.includes(path.value.source.value as string)) {
|
|
45
49
|
return false
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -134,7 +138,7 @@ export function removeImportFromDeclaration(
|
|
|
134
138
|
|
|
135
139
|
// Remove entire import if no specifiers left
|
|
136
140
|
if (specifiers.length === 0) {
|
|
137
|
-
importPath.get().
|
|
141
|
+
importPath.get().prune()
|
|
138
142
|
}
|
|
139
143
|
return true
|
|
140
144
|
}
|
|
@@ -165,10 +169,14 @@ export function manageImports(
|
|
|
165
169
|
j: JSCodeshift
|
|
166
170
|
): void {
|
|
167
171
|
// Check if source component is still being used
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
// Special case: if we're migrating same component name between packages,
|
|
173
|
+
// we should always remove the old import since JSX elements will now refer to new package
|
|
174
|
+
const isSameComponentMigration = config.fromComponent === config.toComponent
|
|
175
|
+
const stillUsesSource = isSameComponentMigration
|
|
176
|
+
? false
|
|
177
|
+
: source.find(j.JSXOpeningElement, {
|
|
178
|
+
name: { name: sourceComponentName },
|
|
179
|
+
}).length > 0
|
|
172
180
|
|
|
173
181
|
// Handle source package import cleanup
|
|
174
182
|
const sourceImport = source
|