@planningcenter/tapestry-migration-cli 2.2.0-rc.5 → 2.2.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 +2 -2
- package/src/components/button/index.ts +2 -0
- package/src/components/button/transforms/tooltipToWrapper.test.ts +392 -0
- package/src/components/button/transforms/tooltipToWrapper.ts +35 -0
- package/src/components/shared/actions/createWrapper.test.ts +642 -0
- package/src/components/shared/actions/createWrapper.ts +70 -0
- package/src/components/shared/actions/getAttribute.ts +18 -0
- package/src/components/shared/actions/getAttributeValueAsProps.ts +57 -0
- package/src/components/shared/transformFactories/helpers/addImport.test.ts +278 -0
- package/src/components/shared/transformFactories/helpers/manageImports.ts +53 -20
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { JSCodeshift, JSXAttribute, JSXElement } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
export function getAttribute({
|
|
4
|
+
element,
|
|
5
|
+
|
|
6
|
+
name,
|
|
7
|
+
}: {
|
|
8
|
+
element: JSXElement
|
|
9
|
+
name: string
|
|
10
|
+
}) {
|
|
11
|
+
const attributes = element.openingElement.attributes || []
|
|
12
|
+
const attribute = attributes.find(
|
|
13
|
+
(attr) => attr.type === "JSXAttribute" && attr.name.name === name
|
|
14
|
+
)
|
|
15
|
+
if (!attribute) return
|
|
16
|
+
|
|
17
|
+
return attribute as JSXAttribute
|
|
18
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JSCodeshift,
|
|
3
|
+
JSXAttribute,
|
|
4
|
+
JSXElement,
|
|
5
|
+
JSXSpreadAttribute,
|
|
6
|
+
} from "jscodeshift"
|
|
7
|
+
|
|
8
|
+
import { getAttribute } from "./getAttribute"
|
|
9
|
+
|
|
10
|
+
export function getAttributeValueAsProps({
|
|
11
|
+
j,
|
|
12
|
+
element,
|
|
13
|
+
name,
|
|
14
|
+
stringValueKey,
|
|
15
|
+
}: {
|
|
16
|
+
element: JSXElement
|
|
17
|
+
j: JSCodeshift
|
|
18
|
+
name: string
|
|
19
|
+
stringValueKey?: string
|
|
20
|
+
}): JSXSpreadAttribute[] {
|
|
21
|
+
const attribute = getAttribute({ element, name })
|
|
22
|
+
if (attribute && attribute.value) {
|
|
23
|
+
if (attribute.value.type === "StringLiteral") {
|
|
24
|
+
return stringToProps({ attribute, j, stringValueKey })
|
|
25
|
+
}
|
|
26
|
+
if (attribute.value.type === "JSXExpressionContainer") {
|
|
27
|
+
if (attribute.value.expression.type !== "JSXEmptyExpression") {
|
|
28
|
+
return [j.jsxSpreadAttribute(attribute.value.expression)]
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
return [j.jsxSpreadAttribute(attribute.value)]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stringToProps({
|
|
39
|
+
attribute,
|
|
40
|
+
j,
|
|
41
|
+
stringValueKey,
|
|
42
|
+
}: {
|
|
43
|
+
attribute: JSXAttribute
|
|
44
|
+
j: JSCodeshift
|
|
45
|
+
stringValueKey?: string
|
|
46
|
+
}): JSXSpreadAttribute[] {
|
|
47
|
+
if (!attribute.value) return []
|
|
48
|
+
if (stringValueKey === undefined) return []
|
|
49
|
+
|
|
50
|
+
return [
|
|
51
|
+
j.jsxSpreadAttribute(
|
|
52
|
+
j.objectExpression([
|
|
53
|
+
j.objectProperty(j.identifier(stringValueKey), attribute.value),
|
|
54
|
+
])
|
|
55
|
+
),
|
|
56
|
+
]
|
|
57
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import { addImport } from "./manageImports"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function createSource(code: string) {
|
|
9
|
+
return j(code)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("addImport", () => {
|
|
13
|
+
describe("when component already imported from target package", () => {
|
|
14
|
+
it("should return existing import name", () => {
|
|
15
|
+
const source = createSource(
|
|
16
|
+
'import { Button } from "@planningcenter/tapestry"'
|
|
17
|
+
)
|
|
18
|
+
const result = addImport({
|
|
19
|
+
component: "Button",
|
|
20
|
+
conflictAlias: "TButton",
|
|
21
|
+
j,
|
|
22
|
+
pkg: "@planningcenter/tapestry",
|
|
23
|
+
source,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
expect(result).toBe("Button")
|
|
27
|
+
// Should not modify the source
|
|
28
|
+
const imports = source.find(j.ImportDeclaration)
|
|
29
|
+
expect(imports).toHaveLength(1)
|
|
30
|
+
expect(imports.at(0).get().value.source.value).toBe(
|
|
31
|
+
"@planningcenter/tapestry"
|
|
32
|
+
)
|
|
33
|
+
expect(imports.at(0).get().value.specifiers).toHaveLength(1)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("should return existing aliased import name", () => {
|
|
37
|
+
const source = createSource(
|
|
38
|
+
'import { Button as TapestryButton } from "@planningcenter/tapestry"'
|
|
39
|
+
)
|
|
40
|
+
const result = addImport({
|
|
41
|
+
component: "Button",
|
|
42
|
+
conflictAlias: "TButton",
|
|
43
|
+
j,
|
|
44
|
+
pkg: "@planningcenter/tapestry",
|
|
45
|
+
source,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
expect(result).toBe("TapestryButton")
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe("when adding to existing import declaration", () => {
|
|
53
|
+
it("should add component to existing import without conflict", () => {
|
|
54
|
+
const source = createSource(
|
|
55
|
+
'import { Link } from "@planningcenter/tapestry"'
|
|
56
|
+
)
|
|
57
|
+
const result = addImport({
|
|
58
|
+
component: "Button",
|
|
59
|
+
conflictAlias: "TButton",
|
|
60
|
+
j,
|
|
61
|
+
pkg: "@planningcenter/tapestry",
|
|
62
|
+
source,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(result).toBe("Button")
|
|
66
|
+
|
|
67
|
+
const imports = source.find(j.ImportDeclaration)
|
|
68
|
+
expect(imports).toHaveLength(1)
|
|
69
|
+
expect(imports.at(0).get().value.specifiers).toHaveLength(2)
|
|
70
|
+
|
|
71
|
+
const specifierNames = imports
|
|
72
|
+
.at(0)
|
|
73
|
+
.get()
|
|
74
|
+
.value.specifiers?.map(
|
|
75
|
+
(spec: { imported: { name: string }; type: string }) =>
|
|
76
|
+
spec.type === "ImportSpecifier" ? spec.imported?.name : null
|
|
77
|
+
)
|
|
78
|
+
expect(specifierNames).toContain("Link")
|
|
79
|
+
expect(specifierNames).toContain("Button")
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("should add component with auto-generated alias when conflict exists", () => {
|
|
83
|
+
const source = createSource(`
|
|
84
|
+
import { Button } from "@some/other-package"
|
|
85
|
+
import { Link } from "@planningcenter/tapestry"
|
|
86
|
+
`)
|
|
87
|
+
|
|
88
|
+
const result = addImport({
|
|
89
|
+
component: "Button",
|
|
90
|
+
conflictAlias: "TButton",
|
|
91
|
+
j,
|
|
92
|
+
pkg: "@planningcenter/tapestry",
|
|
93
|
+
source,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
expect(result).toBe("TButton")
|
|
97
|
+
|
|
98
|
+
const tapestryImport = source.find(j.ImportDeclaration, {
|
|
99
|
+
source: { value: "@planningcenter/tapestry" },
|
|
100
|
+
})
|
|
101
|
+
expect(tapestryImport).toHaveLength(1)
|
|
102
|
+
|
|
103
|
+
const specifiers = tapestryImport.at(0).get().value.specifiers
|
|
104
|
+
const buttonSpec = specifiers?.find(
|
|
105
|
+
(spec: { imported: { name: string }; type: string }) =>
|
|
106
|
+
spec.type === "ImportSpecifier" && spec.imported?.name === "Button"
|
|
107
|
+
)
|
|
108
|
+
expect(buttonSpec?.local?.name).toBe("TButton")
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it("should add component with custom alias when conflict exists", () => {
|
|
112
|
+
const source = createSource(`
|
|
113
|
+
import { Button } from "@some/other-package"
|
|
114
|
+
import { Link } from "@planningcenter/tapestry"
|
|
115
|
+
`)
|
|
116
|
+
|
|
117
|
+
const result = addImport({
|
|
118
|
+
component: "Button",
|
|
119
|
+
conflictAlias: "TapestryButton",
|
|
120
|
+
j,
|
|
121
|
+
pkg: "@planningcenter/tapestry",
|
|
122
|
+
source,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
expect(result).toBe("TapestryButton")
|
|
126
|
+
|
|
127
|
+
const tapestryImport = source.find(j.ImportDeclaration, {
|
|
128
|
+
source: { value: "@planningcenter/tapestry" },
|
|
129
|
+
})
|
|
130
|
+
expect(tapestryImport).toHaveLength(1)
|
|
131
|
+
|
|
132
|
+
const specifiers = tapestryImport.at(0).get().value.specifiers
|
|
133
|
+
const buttonSpec = specifiers?.find(
|
|
134
|
+
(spec: { imported: { name: string }; type: string }) =>
|
|
135
|
+
spec.type === "ImportSpecifier" && spec.imported?.name === "Button"
|
|
136
|
+
)
|
|
137
|
+
expect(buttonSpec?.local?.name).toBe("TapestryButton")
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe("when creating new import declaration", () => {
|
|
142
|
+
it("should create new import without conflict", () => {
|
|
143
|
+
const source = createSource('import React from "react"')
|
|
144
|
+
const result = addImport({
|
|
145
|
+
component: "Button",
|
|
146
|
+
conflictAlias: "TButton",
|
|
147
|
+
j,
|
|
148
|
+
pkg: "@planningcenter/tapestry",
|
|
149
|
+
source,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(result).toBe("Button")
|
|
153
|
+
|
|
154
|
+
const imports = source.find(j.ImportDeclaration)
|
|
155
|
+
expect(imports).toHaveLength(2)
|
|
156
|
+
|
|
157
|
+
const tapestryImport = source.find(j.ImportDeclaration, {
|
|
158
|
+
source: { value: "@planningcenter/tapestry" },
|
|
159
|
+
})
|
|
160
|
+
expect(tapestryImport).toHaveLength(1)
|
|
161
|
+
expect(tapestryImport.at(0).get().value.specifiers).toHaveLength(1)
|
|
162
|
+
expect(
|
|
163
|
+
tapestryImport.at(0).get().value.specifiers?.[0].imported?.name
|
|
164
|
+
).toBe("Button")
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("should create new import with auto-generated alias when conflict exists", () => {
|
|
168
|
+
const source = createSource(`
|
|
169
|
+
import React from "react"
|
|
170
|
+
import { Button } from "@some/other-package"
|
|
171
|
+
`)
|
|
172
|
+
|
|
173
|
+
const result = addImport({
|
|
174
|
+
component: "Button",
|
|
175
|
+
conflictAlias: "TButton",
|
|
176
|
+
j,
|
|
177
|
+
pkg: "@planningcenter/tapestry",
|
|
178
|
+
source,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
expect(result).toBe("TButton")
|
|
182
|
+
|
|
183
|
+
const imports = source.find(j.ImportDeclaration)
|
|
184
|
+
expect(imports).toHaveLength(3)
|
|
185
|
+
|
|
186
|
+
const tapestryImport = source.find(j.ImportDeclaration, {
|
|
187
|
+
source: { value: "@planningcenter/tapestry" },
|
|
188
|
+
})
|
|
189
|
+
expect(tapestryImport).toHaveLength(1)
|
|
190
|
+
|
|
191
|
+
const buttonSpec = tapestryImport.at(0).get().value.specifiers?.[0]
|
|
192
|
+
expect(buttonSpec?.imported?.name).toBe("Button")
|
|
193
|
+
expect(buttonSpec?.local?.name).toBe("TButton")
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("should create new import with custom alias when conflict exists", () => {
|
|
197
|
+
const source = createSource(`
|
|
198
|
+
import React from "react"
|
|
199
|
+
import { Button } from "@some/other-package"
|
|
200
|
+
`)
|
|
201
|
+
|
|
202
|
+
const result = addImport({
|
|
203
|
+
component: "Button",
|
|
204
|
+
conflictAlias: "TapestryButton",
|
|
205
|
+
j,
|
|
206
|
+
pkg: "@planningcenter/tapestry",
|
|
207
|
+
source,
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
expect(result).toBe("TapestryButton")
|
|
211
|
+
|
|
212
|
+
const tapestryImport = source.find(j.ImportDeclaration, {
|
|
213
|
+
source: { value: "@planningcenter/tapestry" },
|
|
214
|
+
})
|
|
215
|
+
expect(tapestryImport).toHaveLength(1)
|
|
216
|
+
|
|
217
|
+
const buttonSpec = tapestryImport.at(0).get().value.specifiers?.[0]
|
|
218
|
+
expect(buttonSpec?.imported?.name).toBe("Button")
|
|
219
|
+
expect(buttonSpec?.local?.name).toBe("TapestryButton")
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe("edge cases", () => {
|
|
224
|
+
it("should handle duplicate add calls", () => {
|
|
225
|
+
const source = createSource('import React from "react"')
|
|
226
|
+
|
|
227
|
+
const result1 = addImport({
|
|
228
|
+
component: "Button",
|
|
229
|
+
conflictAlias: "TButton",
|
|
230
|
+
j,
|
|
231
|
+
pkg: "@planningcenter/tapestry",
|
|
232
|
+
source,
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const result2 = addImport({
|
|
236
|
+
component: "Button",
|
|
237
|
+
conflictAlias: "TButton",
|
|
238
|
+
j,
|
|
239
|
+
pkg: "@planningcenter/tapestry",
|
|
240
|
+
source,
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
expect(result1).toBe("Button")
|
|
244
|
+
expect(result2).toBe("Button")
|
|
245
|
+
|
|
246
|
+
// Should still only have 2 imports (React + Button)
|
|
247
|
+
const imports = source.find(j.ImportDeclaration)
|
|
248
|
+
expect(imports).toHaveLength(2)
|
|
249
|
+
|
|
250
|
+
// Button should only appear once in the tapestry import
|
|
251
|
+
const tapestryImport = source.find(j.ImportDeclaration, {
|
|
252
|
+
source: { value: "@planningcenter/tapestry" },
|
|
253
|
+
})
|
|
254
|
+
expect(tapestryImport.at(0).get().value.specifiers).toHaveLength(1)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it("should not use conflictAlias when no conflict exists", () => {
|
|
258
|
+
const source = createSource('import React from "react"')
|
|
259
|
+
|
|
260
|
+
const result = addImport({
|
|
261
|
+
component: "Button",
|
|
262
|
+
conflictAlias: "TapestryButton",
|
|
263
|
+
j,
|
|
264
|
+
pkg: "@planningcenter/tapestry",
|
|
265
|
+
source,
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
expect(result).toBe("Button")
|
|
269
|
+
|
|
270
|
+
const tapestryImport = source.find(j.ImportDeclaration, {
|
|
271
|
+
source: { value: "@planningcenter/tapestry" },
|
|
272
|
+
})
|
|
273
|
+
const buttonSpec = tapestryImport.at(0).get().value.specifiers?.[0]
|
|
274
|
+
expect(buttonSpec?.imported?.name).toBe("Button")
|
|
275
|
+
expect(buttonSpec?.local).toBeNull()
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
})
|
|
@@ -145,6 +145,51 @@ export function removeImportFromDeclaration(
|
|
|
145
145
|
return false
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Adds an import to the source, handling conflicts and existing imports
|
|
150
|
+
* Returns the final name to use for the component (original or aliased)
|
|
151
|
+
*/
|
|
152
|
+
export function addImport({
|
|
153
|
+
component,
|
|
154
|
+
conflictAlias,
|
|
155
|
+
j,
|
|
156
|
+
pkg,
|
|
157
|
+
source,
|
|
158
|
+
}: {
|
|
159
|
+
component: string
|
|
160
|
+
conflictAlias: string
|
|
161
|
+
j: JSCodeshift
|
|
162
|
+
pkg: string
|
|
163
|
+
source: Collection
|
|
164
|
+
}): string {
|
|
165
|
+
// Check if component is already imported from the target package
|
|
166
|
+
const existingImportName = getImportName(component, pkg, { j, source })
|
|
167
|
+
if (existingImportName) {
|
|
168
|
+
return existingImportName
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check for conflicts with imports from other packages
|
|
172
|
+
const hasConflict = hasConflictingImport(component, pkg, { j, source })
|
|
173
|
+
const finalComponentName = hasConflict ? conflictAlias : component
|
|
174
|
+
const alias =
|
|
175
|
+
finalComponentName !== component ? finalComponentName : undefined
|
|
176
|
+
|
|
177
|
+
// Handle target package import addition
|
|
178
|
+
const targetImport = source
|
|
179
|
+
.find(j.ImportDeclaration, {
|
|
180
|
+
source: { value: pkg },
|
|
181
|
+
})
|
|
182
|
+
.at(0)
|
|
183
|
+
|
|
184
|
+
if (targetImport.length > 0) {
|
|
185
|
+
addImportToExisting(targetImport, component, j, alias)
|
|
186
|
+
} else {
|
|
187
|
+
createNewImport(source, component, pkg, j, alias)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return finalComponentName
|
|
191
|
+
}
|
|
192
|
+
|
|
148
193
|
/**
|
|
149
194
|
* Manages imports after transformation - adds target imports and removes unused source imports
|
|
150
195
|
*/
|
|
@@ -189,24 +234,12 @@ export function manageImports(
|
|
|
189
234
|
removeImportFromDeclaration(sourceImport, config.fromComponent)
|
|
190
235
|
}
|
|
191
236
|
|
|
192
|
-
// Handle target package import addition
|
|
193
|
-
|
|
194
|
-
.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const alias =
|
|
201
|
-
targetComponentName !== config.toComponent
|
|
202
|
-
? targetComponentName
|
|
203
|
-
: undefined
|
|
204
|
-
addImportToExisting(targetImport, config.toComponent, j, alias)
|
|
205
|
-
} else {
|
|
206
|
-
const alias =
|
|
207
|
-
targetComponentName !== config.toComponent
|
|
208
|
-
? targetComponentName
|
|
209
|
-
: undefined
|
|
210
|
-
createNewImport(source, config.toComponent, config.toPackage, j, alias)
|
|
211
|
-
}
|
|
237
|
+
// Handle target package import addition using addImport
|
|
238
|
+
addImport({
|
|
239
|
+
component: config.toComponent,
|
|
240
|
+
conflictAlias: config.conflictAlias || targetComponentName,
|
|
241
|
+
j,
|
|
242
|
+
pkg: config.toPackage,
|
|
243
|
+
source,
|
|
244
|
+
})
|
|
212
245
|
}
|