@planningcenter/tapestry-migration-cli 2.1.0 → 2.1.1-qa-380.0
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 +12 -7
- package/src/components/button/index.ts +27 -0
- package/src/components/button/transforms/linkToButton.test.ts +426 -0
- package/src/components/button/transforms/linkToButton.ts +15 -0
- package/src/components/button/transforms/titleToLabel.test.ts +418 -0
- package/src/components/button/transforms/titleToLabel.ts +19 -0
- package/src/components/shared/actions/transformAttributeName.ts +20 -0
- package/src/components/shared/actions/transformElementName.test.ts +59 -0
- package/src/components/shared/actions/transformElementName.ts +27 -0
- package/src/components/shared/conditions/andConditions.test.ts +65 -0
- package/src/components/shared/conditions/andConditions.ts +13 -0
- package/src/components/shared/conditions/hasAttribute.test.ts +43 -0
- package/src/components/shared/conditions/hasAttribute.ts +18 -0
- package/src/components/shared/conditions/hasAttributeValue.test.ts +48 -0
- package/src/components/shared/conditions/hasAttributeValue.ts +23 -0
- package/src/components/shared/conditions/helpers/createJSXElement.ts +9 -0
- package/src/components/shared/conditions/index.test.ts +63 -0
- package/src/components/shared/conditions/orConditions.test.ts +76 -0
- package/src/components/shared/conditions/orConditions.ts +13 -0
- package/src/components/shared/findAttribute.ts +15 -0
- package/src/components/shared/transformFactories/attributeTransformFactory.test.ts +88 -0
- package/src/components/shared/transformFactories/attributeTransformFactory.ts +51 -0
- package/src/components/shared/transformFactories/componentTransformFactory.test.ts +7 -0
- package/src/components/shared/transformFactories/componentTransformFactory.ts +77 -0
- package/src/components/shared/transformFactories/helpers/manageImports.test.ts +383 -0
- package/src/components/shared/transformFactories/helpers/manageImports.ts +204 -0
- package/src/components/shared/types.ts +6 -0
- package/src/index.ts +47 -0
- package/src/jscodeshiftRunner.ts +49 -0
- package/src/shared/types.ts +6 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -28
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
addImportToExisting,
|
|
6
|
+
createNewImport,
|
|
7
|
+
getImportName,
|
|
8
|
+
hasConflictingImport,
|
|
9
|
+
removeImportFromDeclaration,
|
|
10
|
+
} from "./manageImports"
|
|
11
|
+
|
|
12
|
+
const j = jscodeshift.withParser("tsx")
|
|
13
|
+
|
|
14
|
+
describe("componentTransformUtilities", () => {
|
|
15
|
+
describe("getImportName", () => {
|
|
16
|
+
it("should return local name for imported component", () => {
|
|
17
|
+
const code = `import { Button } from "@planningcenter/tapestry-react"`
|
|
18
|
+
const source = j(code)
|
|
19
|
+
|
|
20
|
+
const result = getImportName("Button", "@planningcenter/tapestry-react", {
|
|
21
|
+
j,
|
|
22
|
+
source,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
expect(result).toBe("Button")
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("should return aliased name for renamed import", () => {
|
|
29
|
+
const code = `import { Button as TapestryButton } from "@planningcenter/tapestry-react"`
|
|
30
|
+
const source = j(code)
|
|
31
|
+
|
|
32
|
+
const result = getImportName("Button", "@planningcenter/tapestry-react", {
|
|
33
|
+
j,
|
|
34
|
+
source,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
expect(result).toBe("TapestryButton")
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("should return null when component is not imported", () => {
|
|
41
|
+
const code = `import { Link } from "@planningcenter/tapestry-react"`
|
|
42
|
+
const source = j(code)
|
|
43
|
+
|
|
44
|
+
const result = getImportName("Button", "@planningcenter/tapestry-react", {
|
|
45
|
+
j,
|
|
46
|
+
source,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
expect(result).toBe(null)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("should return null when package is not imported", () => {
|
|
53
|
+
const code = `import { Button } from "other-package"`
|
|
54
|
+
const source = j(code)
|
|
55
|
+
|
|
56
|
+
const result = getImportName("Button", "@planningcenter/tapestry-react", {
|
|
57
|
+
j,
|
|
58
|
+
source,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(result).toBe(null)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("should handle multiple imports from same package", () => {
|
|
65
|
+
const code = `import { Button, Link as MyLink } from "@planningcenter/tapestry-react"`
|
|
66
|
+
const source = j(code)
|
|
67
|
+
|
|
68
|
+
const buttonResult = getImportName(
|
|
69
|
+
"Button",
|
|
70
|
+
"@planningcenter/tapestry-react",
|
|
71
|
+
{ j, source }
|
|
72
|
+
)
|
|
73
|
+
const linkResult = getImportName(
|
|
74
|
+
"Link",
|
|
75
|
+
"@planningcenter/tapestry-react",
|
|
76
|
+
{ j, source }
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
expect(buttonResult).toBe("Button")
|
|
80
|
+
expect(linkResult).toBe("MyLink")
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it("should handle multiple import declarations", () => {
|
|
84
|
+
const code = `
|
|
85
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
86
|
+
import { useState } from "react"
|
|
87
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
88
|
+
`
|
|
89
|
+
const source = j(code)
|
|
90
|
+
|
|
91
|
+
const buttonResult = getImportName(
|
|
92
|
+
"Button",
|
|
93
|
+
"@planningcenter/tapestry-react",
|
|
94
|
+
{ j, source }
|
|
95
|
+
)
|
|
96
|
+
const linkResult = getImportName(
|
|
97
|
+
"Link",
|
|
98
|
+
"@planningcenter/tapestry-react",
|
|
99
|
+
{ j, source }
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
expect(buttonResult).toBe("Button")
|
|
103
|
+
expect(linkResult).toBe("Link")
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe("hasConflictingImport", () => {
|
|
108
|
+
it("should return true when component is imported from different package", () => {
|
|
109
|
+
const code = `import { Link } from "react-router-dom"`
|
|
110
|
+
const source = j(code)
|
|
111
|
+
|
|
112
|
+
const result = hasConflictingImport(
|
|
113
|
+
"Link",
|
|
114
|
+
"@planningcenter/tapestry-react",
|
|
115
|
+
{ j, source }
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
expect(result).toBe(true)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("should return false when component is only imported from excluded package", () => {
|
|
122
|
+
const code = `import { Link } from "@planningcenter/tapestry-react"`
|
|
123
|
+
const source = j(code)
|
|
124
|
+
|
|
125
|
+
const result = hasConflictingImport(
|
|
126
|
+
"Link",
|
|
127
|
+
"@planningcenter/tapestry-react",
|
|
128
|
+
{ j, source }
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
expect(result).toBe(false)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("should return false when component is not imported at all", () => {
|
|
135
|
+
const code = `import { Button } from "@planningcenter/tapestry-react"`
|
|
136
|
+
const source = j(code)
|
|
137
|
+
|
|
138
|
+
const result = hasConflictingImport(
|
|
139
|
+
"Link",
|
|
140
|
+
"@planningcenter/tapestry-react",
|
|
141
|
+
{ j, source }
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
expect(result).toBe(false)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("should return true for default import conflicts", () => {
|
|
148
|
+
const code = `import Link from "next/link"`
|
|
149
|
+
const source = j(code)
|
|
150
|
+
|
|
151
|
+
const result = hasConflictingImport(
|
|
152
|
+
"Link",
|
|
153
|
+
"@planningcenter/tapestry-react",
|
|
154
|
+
{ j, source }
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
expect(result).toBe(true)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it("should handle multiple conflicting sources", () => {
|
|
161
|
+
const code = `
|
|
162
|
+
import { Link } from "react-router-dom"
|
|
163
|
+
import { Link as NextLink } from "next/link"
|
|
164
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
165
|
+
`
|
|
166
|
+
const source = j(code)
|
|
167
|
+
|
|
168
|
+
const result = hasConflictingImport(
|
|
169
|
+
"Link",
|
|
170
|
+
"@planningcenter/tapestry-react",
|
|
171
|
+
{ j, source }
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
expect(result).toBe(true)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe("addImportToExisting", () => {
|
|
179
|
+
it("should add new import to existing import declaration", () => {
|
|
180
|
+
const code = `import { Button } from "@planningcenter/tapestry-react"`
|
|
181
|
+
const source = j(code)
|
|
182
|
+
const importPath = source.find(j.ImportDeclaration).at(0)
|
|
183
|
+
|
|
184
|
+
addImportToExisting(importPath.get(), "Link", j)
|
|
185
|
+
|
|
186
|
+
const output = source.toSource()
|
|
187
|
+
expect(output).toContain(
|
|
188
|
+
'import { Button, Link } from "@planningcenter/tapestry-react"'
|
|
189
|
+
)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("should add aliased import to existing declaration", () => {
|
|
193
|
+
const code = `import { Button } from "@planningcenter/tapestry-react"`
|
|
194
|
+
const source = j(code)
|
|
195
|
+
const importPath = source.find(j.ImportDeclaration).at(0)
|
|
196
|
+
|
|
197
|
+
addImportToExisting(importPath.get(), "Link", j, "TLink")
|
|
198
|
+
|
|
199
|
+
const output = source.toSource()
|
|
200
|
+
expect(output).toContain(
|
|
201
|
+
'import { Button, Link as TLink } from "@planningcenter/tapestry-react"'
|
|
202
|
+
)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it("should not add duplicate imports", () => {
|
|
206
|
+
const code = `import { Button, Link } from "@planningcenter/tapestry-react"`
|
|
207
|
+
const source = j(code)
|
|
208
|
+
const importPath = source.find(j.ImportDeclaration).at(0)
|
|
209
|
+
|
|
210
|
+
addImportToExisting(importPath.get(), "Link", j)
|
|
211
|
+
|
|
212
|
+
const output = source.toSource()
|
|
213
|
+
// Should still only have one Link import
|
|
214
|
+
expect(output).toContain(
|
|
215
|
+
'import { Button, Link } from "@planningcenter/tapestry-react"'
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it("should handle empty specifiers list", () => {
|
|
220
|
+
const code = `import {} from "@planningcenter/tapestry-react"`
|
|
221
|
+
const source = j(code)
|
|
222
|
+
const importPath = source.find(j.ImportDeclaration).at(0)
|
|
223
|
+
|
|
224
|
+
addImportToExisting(importPath.get(), "Button", j)
|
|
225
|
+
|
|
226
|
+
const output = source.toSource()
|
|
227
|
+
expect(output).toContain(
|
|
228
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
229
|
+
)
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe("createNewImport", () => {
|
|
234
|
+
it("should create new import declaration", () => {
|
|
235
|
+
const code = `const test = "existing code"`
|
|
236
|
+
const source = j(code)
|
|
237
|
+
|
|
238
|
+
createNewImport(source, "Button", "@planningcenter/tapestry-react", j)
|
|
239
|
+
|
|
240
|
+
const output = source.toSource()
|
|
241
|
+
expect(output).toContain(
|
|
242
|
+
'import { Button } from "@planningcenter/tapestry-react";'
|
|
243
|
+
)
|
|
244
|
+
expect(output).toContain('const test = "existing code"')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it("should create new import with alias", () => {
|
|
248
|
+
const code = `const test = "existing"`
|
|
249
|
+
const source = j(code)
|
|
250
|
+
|
|
251
|
+
createNewImport(
|
|
252
|
+
source,
|
|
253
|
+
"Link",
|
|
254
|
+
"@planningcenter/tapestry-react",
|
|
255
|
+
j,
|
|
256
|
+
"TLink"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
const output = source.toSource()
|
|
260
|
+
expect(output).toContain(
|
|
261
|
+
'import { Link as TLink } from "@planningcenter/tapestry-react";'
|
|
262
|
+
)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it("should add new import after existing imports", () => {
|
|
266
|
+
const code = `
|
|
267
|
+
import React from "react"
|
|
268
|
+
import { useState } from "react"
|
|
269
|
+
|
|
270
|
+
const component = () => null
|
|
271
|
+
`
|
|
272
|
+
const source = j(code)
|
|
273
|
+
|
|
274
|
+
createNewImport(source, "Button", "@planningcenter/tapestry-react", j)
|
|
275
|
+
|
|
276
|
+
const output = source.toSource()
|
|
277
|
+
const lines = output.split("\n")
|
|
278
|
+
const reactImportIndex = lines.findIndex((line) =>
|
|
279
|
+
line.includes('import React from "react"')
|
|
280
|
+
)
|
|
281
|
+
const useStateImportIndex = lines.findIndex((line) =>
|
|
282
|
+
line.includes('import { useState } from "react"')
|
|
283
|
+
)
|
|
284
|
+
const buttonImportIndex = lines.findIndex((line) =>
|
|
285
|
+
line.includes('import { Button } from "@planningcenter/tapestry-react"')
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
expect(reactImportIndex).toBeLessThan(buttonImportIndex)
|
|
289
|
+
expect(useStateImportIndex).toBeLessThan(buttonImportIndex)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it("should handle empty file", () => {
|
|
293
|
+
const code = ``
|
|
294
|
+
const source = j(code)
|
|
295
|
+
|
|
296
|
+
createNewImport(source, "Button", "@planningcenter/tapestry-react", j)
|
|
297
|
+
|
|
298
|
+
const output = source.toSource()
|
|
299
|
+
expect(output).toBe(
|
|
300
|
+
'import { Button } from "@planningcenter/tapestry-react";'
|
|
301
|
+
)
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
describe("removeImportFromDeclaration", () => {
|
|
306
|
+
it("should remove import specifier", () => {
|
|
307
|
+
const code = `import { Button, Link } from "@planningcenter/tapestry-react"`
|
|
308
|
+
const source = j(code)
|
|
309
|
+
const importPath = source.find(j.ImportDeclaration).at(0)
|
|
310
|
+
|
|
311
|
+
const result = removeImportFromDeclaration(importPath.get(), "Button")
|
|
312
|
+
|
|
313
|
+
expect(result).toBe(true)
|
|
314
|
+
const output = source.toSource()
|
|
315
|
+
expect(output).toContain(
|
|
316
|
+
'import { Link } from "@planningcenter/tapestry-react"'
|
|
317
|
+
)
|
|
318
|
+
expect(output).not.toContain("Button")
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it("should remove entire import if no specifiers left", () => {
|
|
322
|
+
const code = `
|
|
323
|
+
import React from "react"
|
|
324
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
325
|
+
const component = () => null
|
|
326
|
+
`
|
|
327
|
+
const source = j(code)
|
|
328
|
+
const tapestryImport = source
|
|
329
|
+
.find(j.ImportDeclaration)
|
|
330
|
+
.filter(
|
|
331
|
+
(path) => path.value.source.value === "@planningcenter/tapestry-react"
|
|
332
|
+
)
|
|
333
|
+
.at(0)
|
|
334
|
+
|
|
335
|
+
const result = removeImportFromDeclaration(tapestryImport.get(), "Button")
|
|
336
|
+
|
|
337
|
+
expect(result).toBe(true)
|
|
338
|
+
const output = source.toSource()
|
|
339
|
+
expect(output).not.toContain("@planningcenter/tapestry-react")
|
|
340
|
+
expect(output).toContain('import React from "react"') // Other imports should remain
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it("should return false if import not found", () => {
|
|
344
|
+
const code = `import { Button } from "@planningcenter/tapestry-react"`
|
|
345
|
+
const source = j(code)
|
|
346
|
+
const importPath = source.find(j.ImportDeclaration).at(0)
|
|
347
|
+
|
|
348
|
+
const result = removeImportFromDeclaration(importPath.get(), "Link")
|
|
349
|
+
|
|
350
|
+
expect(result).toBe(false)
|
|
351
|
+
const output = source.toSource()
|
|
352
|
+
expect(output).toContain("Button") // Should be unchanged
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it("should handle single import removal", () => {
|
|
356
|
+
const code = `import { Button } from "@planningcenter/tapestry-react"`
|
|
357
|
+
const source = j(code)
|
|
358
|
+
const importPath = source.find(j.ImportDeclaration).at(0)
|
|
359
|
+
|
|
360
|
+
const result = removeImportFromDeclaration(importPath.get(), "Button")
|
|
361
|
+
|
|
362
|
+
expect(result).toBe(true)
|
|
363
|
+
expect(source.find(j.ImportDeclaration).length).toBe(0)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it("should preserve other specifiers", () => {
|
|
367
|
+
const code = `import { Button, Link, Input } from "@planningcenter/tapestry-react"`
|
|
368
|
+
const source = j(code)
|
|
369
|
+
const importPath = source.find(j.ImportDeclaration).at(0)
|
|
370
|
+
|
|
371
|
+
const result = removeImportFromDeclaration(importPath.get(), "Link")
|
|
372
|
+
|
|
373
|
+
expect(result).toBe(true)
|
|
374
|
+
const output = source.toSource()
|
|
375
|
+
expect(output).toContain("Button")
|
|
376
|
+
expect(output).toContain("Input")
|
|
377
|
+
expect(output).not.toContain("Link")
|
|
378
|
+
expect(output).toContain(
|
|
379
|
+
'import { Button, Input } from "@planningcenter/tapestry-react"'
|
|
380
|
+
)
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
})
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Collection, ImportSpecifier, JSCodeshift } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { TransformCondition } from "../../types"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generic import name getter that works with any package
|
|
7
|
+
*/
|
|
8
|
+
export function getImportName(
|
|
9
|
+
importName: string,
|
|
10
|
+
packageName: string,
|
|
11
|
+
{ source, j }: { j: JSCodeshift; source: Collection }
|
|
12
|
+
): string | null {
|
|
13
|
+
let localName: string | null = null
|
|
14
|
+
|
|
15
|
+
source
|
|
16
|
+
.find(j.ImportDeclaration, {
|
|
17
|
+
source: { value: packageName },
|
|
18
|
+
})
|
|
19
|
+
.forEach((path) => {
|
|
20
|
+
const specifiers = path.value.specifiers || []
|
|
21
|
+
|
|
22
|
+
specifiers.forEach((spec) => {
|
|
23
|
+
if (
|
|
24
|
+
spec.type === "ImportSpecifier" &&
|
|
25
|
+
spec.imported?.name === importName
|
|
26
|
+
) {
|
|
27
|
+
localName = (spec.local?.name as string) || importName
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return localName
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks if a component is already imported from sources other than the specified package
|
|
37
|
+
*/
|
|
38
|
+
export function hasConflictingImport(
|
|
39
|
+
componentName: string,
|
|
40
|
+
excludePackage: string,
|
|
41
|
+
{ source, j }: { j: JSCodeshift; source: Collection }
|
|
42
|
+
): boolean {
|
|
43
|
+
const conflictingImport = source.find(j.ImportDeclaration).filter((path) => {
|
|
44
|
+
if (path.value.source.value === excludePackage) {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
path.value.specifiers?.some(
|
|
50
|
+
(spec) =>
|
|
51
|
+
(spec.type === "ImportSpecifier" &&
|
|
52
|
+
spec.imported?.name === componentName) ||
|
|
53
|
+
(spec.type === "ImportDefaultSpecifier" &&
|
|
54
|
+
spec.local?.name === componentName)
|
|
55
|
+
) || false
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return conflictingImport.length > 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Adds an import to an existing import declaration
|
|
64
|
+
*/
|
|
65
|
+
export function addImportToExisting(
|
|
66
|
+
importPath: Collection,
|
|
67
|
+
componentName: string,
|
|
68
|
+
j: JSCodeshift,
|
|
69
|
+
alias?: string
|
|
70
|
+
): void {
|
|
71
|
+
const specifiers = importPath.get().value.specifiers || []
|
|
72
|
+
|
|
73
|
+
const hasImport = specifiers.some(
|
|
74
|
+
(spec: ImportSpecifier) =>
|
|
75
|
+
spec.type === "ImportSpecifier" && spec.imported?.name === componentName
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if (!hasImport) {
|
|
79
|
+
const importSpecifier = alias
|
|
80
|
+
? j.importSpecifier(j.identifier(componentName), j.identifier(alias))
|
|
81
|
+
: j.importSpecifier(j.identifier(componentName))
|
|
82
|
+
|
|
83
|
+
specifiers.push(importSpecifier)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates a new import declaration
|
|
89
|
+
*/
|
|
90
|
+
export function createNewImport(
|
|
91
|
+
source: Collection,
|
|
92
|
+
componentName: string,
|
|
93
|
+
packageName: string,
|
|
94
|
+
j: JSCodeshift,
|
|
95
|
+
alias?: string
|
|
96
|
+
): void {
|
|
97
|
+
const importSpecifier = alias
|
|
98
|
+
? j.importSpecifier(j.identifier(componentName), j.identifier(alias))
|
|
99
|
+
: j.importSpecifier(j.identifier(componentName))
|
|
100
|
+
|
|
101
|
+
const lastImport = source.find(j.ImportDeclaration).at(-1)
|
|
102
|
+
if (lastImport.length > 0) {
|
|
103
|
+
lastImport.insertAfter(
|
|
104
|
+
j.importDeclaration([importSpecifier], j.stringLiteral(packageName))
|
|
105
|
+
)
|
|
106
|
+
} else {
|
|
107
|
+
const program = source.find(j.Program)
|
|
108
|
+
if (program.length > 0) {
|
|
109
|
+
program
|
|
110
|
+
.get("body", 0)
|
|
111
|
+
.insertBefore(
|
|
112
|
+
j.importDeclaration([importSpecifier], j.stringLiteral(packageName))
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Removes an import from an import declaration and cleans up empty imports
|
|
120
|
+
*/
|
|
121
|
+
export function removeImportFromDeclaration(
|
|
122
|
+
importPath: Collection,
|
|
123
|
+
componentName: string
|
|
124
|
+
): boolean {
|
|
125
|
+
const specifiers = importPath.get().value.specifiers || []
|
|
126
|
+
|
|
127
|
+
const importIndex = specifiers.findIndex(
|
|
128
|
+
(spec: ImportSpecifier) =>
|
|
129
|
+
spec.type === "ImportSpecifier" && spec.imported?.name === componentName
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if (importIndex >= 0) {
|
|
133
|
+
specifiers.splice(importIndex, 1)
|
|
134
|
+
|
|
135
|
+
// Remove entire import if no specifiers left
|
|
136
|
+
if (specifiers.length === 0) {
|
|
137
|
+
importPath.get().replace()
|
|
138
|
+
}
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Manages imports after transformation - adds target imports and removes unused source imports
|
|
146
|
+
*/
|
|
147
|
+
export function manageImports(
|
|
148
|
+
source: Collection,
|
|
149
|
+
config: {
|
|
150
|
+
/** Condition that must be met for the transform to occur */
|
|
151
|
+
condition: TransformCondition
|
|
152
|
+
/** Optional alias to use if target component conflicts with existing imports */
|
|
153
|
+
conflictAlias?: string
|
|
154
|
+
/** The source component name to transform from */
|
|
155
|
+
fromComponent: string
|
|
156
|
+
/** The package to import the source component from */
|
|
157
|
+
fromPackage: string
|
|
158
|
+
/** The target component name to transform to */
|
|
159
|
+
toComponent: string
|
|
160
|
+
/** The package to import the target component from */
|
|
161
|
+
toPackage: string
|
|
162
|
+
},
|
|
163
|
+
sourceComponentName: string,
|
|
164
|
+
targetComponentName: string,
|
|
165
|
+
j: JSCodeshift
|
|
166
|
+
): void {
|
|
167
|
+
// Check if source component is still being used
|
|
168
|
+
const stillUsesSource =
|
|
169
|
+
source.find(j.JSXOpeningElement, {
|
|
170
|
+
name: { name: sourceComponentName },
|
|
171
|
+
}).length > 0
|
|
172
|
+
|
|
173
|
+
// Handle source package import cleanup
|
|
174
|
+
const sourceImport = source
|
|
175
|
+
.find(j.ImportDeclaration, {
|
|
176
|
+
source: { value: config.fromPackage },
|
|
177
|
+
})
|
|
178
|
+
.at(0)
|
|
179
|
+
|
|
180
|
+
if (sourceImport.length > 0 && !stillUsesSource) {
|
|
181
|
+
removeImportFromDeclaration(sourceImport, config.fromComponent)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Handle target package import addition
|
|
185
|
+
const targetImport = source
|
|
186
|
+
.find(j.ImportDeclaration, {
|
|
187
|
+
source: { value: config.toPackage },
|
|
188
|
+
})
|
|
189
|
+
.at(0)
|
|
190
|
+
|
|
191
|
+
if (targetImport.length > 0) {
|
|
192
|
+
const alias =
|
|
193
|
+
targetComponentName !== config.toComponent
|
|
194
|
+
? targetComponentName
|
|
195
|
+
: undefined
|
|
196
|
+
addImportToExisting(targetImport, config.toComponent, j, alias)
|
|
197
|
+
} else {
|
|
198
|
+
const alias =
|
|
199
|
+
targetComponentName !== config.toComponent
|
|
200
|
+
? targetComponentName
|
|
201
|
+
: undefined
|
|
202
|
+
createNewImport(source, config.toComponent, config.toPackage, j, alias)
|
|
203
|
+
}
|
|
204
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander"
|
|
4
|
+
|
|
5
|
+
import { runTransforms } from "./jscodeshiftRunner"
|
|
6
|
+
|
|
7
|
+
const program = new Command()
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name("tapestry-migration-cli")
|
|
11
|
+
.description("CLI tool for Tapestry migrations")
|
|
12
|
+
|
|
13
|
+
const COMPONENTS = ["button"]
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.command("run")
|
|
17
|
+
.description("Run a migration of a component from Tapestry React to Tapestry")
|
|
18
|
+
.argument("<component-name>", "The name of the component to migrate")
|
|
19
|
+
.option("-f, --fix", "Write the changes")
|
|
20
|
+
.option("-p, --path <path>", "The path to the folder/file to migrate")
|
|
21
|
+
.option("-v, --verbose", "Verbose output")
|
|
22
|
+
.action((componentName, options) => {
|
|
23
|
+
console.log("Hello from Tapestry Migration CLI! 🎨")
|
|
24
|
+
console.log(`Component: ${componentName}`)
|
|
25
|
+
const key = componentName.toLowerCase()
|
|
26
|
+
|
|
27
|
+
if (COMPONENTS.includes(key)) {
|
|
28
|
+
runTransforms(key, options)
|
|
29
|
+
} else {
|
|
30
|
+
console.log(
|
|
31
|
+
`Invalid component name: ${componentName}. Valid components are: ${COMPONENTS.join(", ")}`
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command("help")
|
|
38
|
+
.description("Show help information")
|
|
39
|
+
.action(() => {
|
|
40
|
+
program.help()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
program.parse()
|
|
44
|
+
|
|
45
|
+
if (!process.argv.slice(2).length) {
|
|
46
|
+
program.outputHelp()
|
|
47
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execSync } from "child_process"
|
|
2
|
+
import { existsSync } from "fs"
|
|
3
|
+
import { dirname, resolve } from "path"
|
|
4
|
+
import { fileURLToPath } from "url"
|
|
5
|
+
|
|
6
|
+
import { Options } from "./shared/types"
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
9
|
+
const __dirname = dirname(__filename)
|
|
10
|
+
|
|
11
|
+
export function runTransforms(key: string, options: Options): void {
|
|
12
|
+
const transformPath = resolve(__dirname, "components", key, "index.ts")
|
|
13
|
+
const targetPath = options.path
|
|
14
|
+
|
|
15
|
+
if (!existsSync(targetPath)) {
|
|
16
|
+
console.error(`❌ Path not found: ${targetPath}`)
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log("🎯 Running transforms...")
|
|
21
|
+
console.log(`📁 Target: ${targetPath}`)
|
|
22
|
+
console.log(`🔧 Transform: ${transformPath}`)
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const command = [
|
|
26
|
+
"npx",
|
|
27
|
+
"--prefer-offline",
|
|
28
|
+
"jscodeshift",
|
|
29
|
+
"-t",
|
|
30
|
+
transformPath,
|
|
31
|
+
targetPath,
|
|
32
|
+
options.fix ? "" : "--dry",
|
|
33
|
+
options.verbose ? "--verbose=2" : "",
|
|
34
|
+
"--parser=tsx",
|
|
35
|
+
]
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.join(" ")
|
|
38
|
+
|
|
39
|
+
console.log(`🚀 Running: ${command}`)
|
|
40
|
+
|
|
41
|
+
execSync(command, {
|
|
42
|
+
cwd: process.cwd(),
|
|
43
|
+
env: process.env,
|
|
44
|
+
stdio: "inherit",
|
|
45
|
+
})
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error("❌ Transform failed:", error)
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/index.d.ts
DELETED