@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "2.2.0-rc.0",
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": "3fbaf6638889e5b7c92af1601869c0b9e8fb3d89"
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 package
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
- excludePackage: string,
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 === excludePackage) {
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().replace()
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
- const stillUsesSource =
169
- source.find(j.JSXOpeningElement, {
170
- name: { name: sourceComponentName },
171
- }).length > 0
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