@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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Collection, JSCodeshift } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gets the local name of Button imported from tapestry-react
|
|
5
|
+
* Returns null if Button is not imported from tapestry-react
|
|
6
|
+
* Handles both normal and renamed imports:
|
|
7
|
+
* - import { Button } from "@planningcenter/tapestry-react" -> "Button"
|
|
8
|
+
* - import { Button as TapestryButton } from "@planningcenter/tapestry-react" -> "TapestryButton"
|
|
9
|
+
*/
|
|
10
|
+
export function getTapestryReactImportName(
|
|
11
|
+
importName: string,
|
|
12
|
+
{ source, j }: { j: JSCodeshift; source: Collection }
|
|
13
|
+
): string | null {
|
|
14
|
+
let localName: string | null = null
|
|
15
|
+
|
|
16
|
+
source
|
|
17
|
+
.find(j.ImportDeclaration, {
|
|
18
|
+
source: { value: "@planningcenter/tapestry-react" },
|
|
19
|
+
})
|
|
20
|
+
.forEach((path) => {
|
|
21
|
+
const specifiers = path.value.specifiers || []
|
|
22
|
+
|
|
23
|
+
specifiers.forEach((spec) => {
|
|
24
|
+
if (
|
|
25
|
+
spec.type === "ImportSpecifier" &&
|
|
26
|
+
spec.imported?.name === importName
|
|
27
|
+
) {
|
|
28
|
+
// spec.local.name is the local name (after "as" if renamed)
|
|
29
|
+
localName = (spec.local?.name as string) || importName
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return localName
|
|
35
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import jscodeshift, { JSXElement } from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
andConditions,
|
|
6
|
+
hasAttribute,
|
|
7
|
+
hasAttributeValue,
|
|
8
|
+
orConditions,
|
|
9
|
+
} from "./transformConfig"
|
|
10
|
+
|
|
11
|
+
const j = jscodeshift.withParser("tsx")
|
|
12
|
+
|
|
13
|
+
// Helper function to create a JSX element for testing
|
|
14
|
+
function createJSXElement(attributes: string): JSXElement {
|
|
15
|
+
const code = `<Button${attributes}>Content</Button>`
|
|
16
|
+
const ast = j(code)
|
|
17
|
+
return ast.find(j.JSXElement).get().value as JSXElement
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("transformConfig", () => {
|
|
21
|
+
describe("hasAttribute", () => {
|
|
22
|
+
it("should return true when attribute exists", () => {
|
|
23
|
+
const condition = hasAttribute("href")
|
|
24
|
+
const element = createJSXElement(' href="/test"')
|
|
25
|
+
|
|
26
|
+
expect(condition(element)).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("should return false when attribute does not exist", () => {
|
|
30
|
+
const condition = hasAttribute("href")
|
|
31
|
+
const element = createJSXElement(' className="test"')
|
|
32
|
+
|
|
33
|
+
expect(condition(element)).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("should return true for attribute without value", () => {
|
|
37
|
+
const condition = hasAttribute("disabled")
|
|
38
|
+
const element = createJSXElement(" disabled")
|
|
39
|
+
|
|
40
|
+
expect(condition(element)).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("should return false for empty element", () => {
|
|
44
|
+
const condition = hasAttribute("href")
|
|
45
|
+
const element = createJSXElement("")
|
|
46
|
+
|
|
47
|
+
expect(condition(element)).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("should handle multiple attributes", () => {
|
|
51
|
+
const condition = hasAttribute("onClick")
|
|
52
|
+
const element = createJSXElement(
|
|
53
|
+
' className="test" onClick={handleClick} disabled'
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
expect(condition(element)).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe("hasAttributeValue", () => {
|
|
61
|
+
it("should return true when attribute has exact string value", () => {
|
|
62
|
+
const condition = hasAttributeValue("as", "a")
|
|
63
|
+
const element = createJSXElement(' as="a"')
|
|
64
|
+
|
|
65
|
+
expect(condition(element)).toBe(true)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("should return false when attribute has different value", () => {
|
|
69
|
+
const condition = hasAttributeValue("as", "a")
|
|
70
|
+
const element = createJSXElement(' as="button"')
|
|
71
|
+
|
|
72
|
+
expect(condition(element)).toBe(false)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("should return false when attribute does not exist", () => {
|
|
76
|
+
const condition = hasAttributeValue("as", "a")
|
|
77
|
+
const element = createJSXElement(' className="test"')
|
|
78
|
+
|
|
79
|
+
expect(condition(element)).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("should return false when attribute exists but has no value", () => {
|
|
83
|
+
const condition = hasAttributeValue("disabled", "true")
|
|
84
|
+
const element = createJSXElement(" disabled")
|
|
85
|
+
|
|
86
|
+
expect(condition(element)).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("should return false for expression values", () => {
|
|
90
|
+
const condition = hasAttributeValue("onClick", "handleClick")
|
|
91
|
+
const element = createJSXElement(" onClick={handleClick}")
|
|
92
|
+
|
|
93
|
+
expect(condition(element)).toBe(false)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("should handle multiple attributes with different values", () => {
|
|
97
|
+
const condition = hasAttributeValue("type", "submit")
|
|
98
|
+
const element = createJSXElement(
|
|
99
|
+
' className="test" type="submit" disabled'
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
expect(condition(element)).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe("andConditions", () => {
|
|
107
|
+
it("should return true when all conditions are met", () => {
|
|
108
|
+
const condition = andConditions(
|
|
109
|
+
hasAttribute("href"),
|
|
110
|
+
hasAttributeValue("target", "_blank")
|
|
111
|
+
)
|
|
112
|
+
const element = createJSXElement(' href="/test" target="_blank"')
|
|
113
|
+
|
|
114
|
+
expect(condition(element)).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("should return false when one condition fails", () => {
|
|
118
|
+
const condition = andConditions(
|
|
119
|
+
hasAttribute("href"),
|
|
120
|
+
hasAttributeValue("target", "_blank")
|
|
121
|
+
)
|
|
122
|
+
const element = createJSXElement(' href="/test" target="_self"')
|
|
123
|
+
|
|
124
|
+
expect(condition(element)).toBe(false)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("should return false when all conditions fail", () => {
|
|
128
|
+
const condition = andConditions(
|
|
129
|
+
hasAttribute("href"),
|
|
130
|
+
hasAttributeValue("target", "_blank")
|
|
131
|
+
)
|
|
132
|
+
const element = createJSXElement(' className="test"')
|
|
133
|
+
|
|
134
|
+
expect(condition(element)).toBe(false)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("should handle single condition", () => {
|
|
138
|
+
const condition = andConditions(hasAttribute("disabled"))
|
|
139
|
+
const element = createJSXElement(" disabled")
|
|
140
|
+
|
|
141
|
+
expect(condition(element)).toBe(true)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it("should handle empty conditions (return true)", () => {
|
|
145
|
+
const condition = andConditions()
|
|
146
|
+
const element = createJSXElement("")
|
|
147
|
+
|
|
148
|
+
expect(condition(element)).toBe(true)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it("should handle complex combinations", () => {
|
|
152
|
+
const condition = andConditions(
|
|
153
|
+
hasAttribute("href"),
|
|
154
|
+
hasAttribute("className"),
|
|
155
|
+
hasAttributeValue("role", "button")
|
|
156
|
+
)
|
|
157
|
+
const element = createJSXElement(
|
|
158
|
+
' href="/test" className="btn" role="button"'
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
expect(condition(element)).toBe(true)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe("orConditions", () => {
|
|
166
|
+
it("should return true when at least one condition is met", () => {
|
|
167
|
+
const condition = orConditions(
|
|
168
|
+
hasAttribute("href"),
|
|
169
|
+
hasAttribute("onClick")
|
|
170
|
+
)
|
|
171
|
+
const element = createJSXElement(' href="/test"')
|
|
172
|
+
|
|
173
|
+
expect(condition(element)).toBe(true)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it("should return true when all conditions are met", () => {
|
|
177
|
+
const condition = orConditions(
|
|
178
|
+
hasAttribute("href"),
|
|
179
|
+
hasAttribute("onClick")
|
|
180
|
+
)
|
|
181
|
+
const element = createJSXElement(' href="/test" onClick={handleClick}')
|
|
182
|
+
|
|
183
|
+
expect(condition(element)).toBe(true)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it("should return false when no conditions are met", () => {
|
|
187
|
+
const condition = orConditions(
|
|
188
|
+
hasAttribute("href"),
|
|
189
|
+
hasAttribute("onClick")
|
|
190
|
+
)
|
|
191
|
+
const element = createJSXElement(' className="test"')
|
|
192
|
+
|
|
193
|
+
expect(condition(element)).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("should handle single condition", () => {
|
|
197
|
+
const condition = orConditions(hasAttribute("disabled"))
|
|
198
|
+
const element = createJSXElement(" disabled")
|
|
199
|
+
|
|
200
|
+
expect(condition(element)).toBe(true)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it("should handle empty conditions (return false)", () => {
|
|
204
|
+
const condition = orConditions()
|
|
205
|
+
const element = createJSXElement("")
|
|
206
|
+
|
|
207
|
+
expect(condition(element)).toBe(false)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it("should handle mixed attribute and value conditions", () => {
|
|
211
|
+
const condition = orConditions(
|
|
212
|
+
hasAttributeValue("type", "submit"),
|
|
213
|
+
hasAttribute("href"),
|
|
214
|
+
hasAttributeValue("role", "button")
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
// Should match type="submit"
|
|
218
|
+
const element1 = createJSXElement(' type="submit"')
|
|
219
|
+
expect(condition(element1)).toBe(true)
|
|
220
|
+
|
|
221
|
+
// Should match href
|
|
222
|
+
const element2 = createJSXElement(' href="/test"')
|
|
223
|
+
expect(condition(element2)).toBe(true)
|
|
224
|
+
|
|
225
|
+
// Should match role="button"
|
|
226
|
+
const element3 = createJSXElement(' role="button"')
|
|
227
|
+
expect(condition(element3)).toBe(true)
|
|
228
|
+
|
|
229
|
+
// Should not match
|
|
230
|
+
const element4 = createJSXElement(' className="test" type="button"')
|
|
231
|
+
expect(condition(element4)).toBe(false)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
describe("complex condition combinations", () => {
|
|
236
|
+
it("should handle nested and/or conditions", () => {
|
|
237
|
+
const condition = andConditions(
|
|
238
|
+
hasAttribute("className"),
|
|
239
|
+
orConditions(hasAttribute("href"), hasAttribute("onClick"))
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
// Should match className + href
|
|
243
|
+
const element1 = createJSXElement(' className="btn" href="/test"')
|
|
244
|
+
expect(condition(element1)).toBe(true)
|
|
245
|
+
|
|
246
|
+
// Should match className + onClick
|
|
247
|
+
const element2 = createJSXElement(' className="btn" onClick={handle}')
|
|
248
|
+
expect(condition(element2)).toBe(true)
|
|
249
|
+
|
|
250
|
+
// Should not match (missing className)
|
|
251
|
+
const element3 = createJSXElement(' href="/test"')
|
|
252
|
+
expect(condition(element3)).toBe(false)
|
|
253
|
+
|
|
254
|
+
// Should not match (missing href/onClick)
|
|
255
|
+
const element4 = createJSXElement(' className="btn"')
|
|
256
|
+
expect(condition(element4)).toBe(false)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it("should handle or of and conditions", () => {
|
|
260
|
+
const condition = orConditions(
|
|
261
|
+
andConditions(
|
|
262
|
+
hasAttribute("href"),
|
|
263
|
+
hasAttributeValue("target", "_blank")
|
|
264
|
+
),
|
|
265
|
+
andConditions(
|
|
266
|
+
hasAttribute("onClick"),
|
|
267
|
+
hasAttributeValue("type", "button")
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
// Should match first AND condition
|
|
272
|
+
const element1 = createJSXElement(' href="/test" target="_blank"')
|
|
273
|
+
expect(condition(element1)).toBe(true)
|
|
274
|
+
|
|
275
|
+
// Should match second AND condition
|
|
276
|
+
const element2 = createJSXElement(' onClick={handle} type="button"')
|
|
277
|
+
expect(condition(element2)).toBe(true)
|
|
278
|
+
|
|
279
|
+
// Should not match (partial first condition)
|
|
280
|
+
const element3 = createJSXElement(' href="/test" target="_self"')
|
|
281
|
+
expect(condition(element3)).toBe(false)
|
|
282
|
+
|
|
283
|
+
// Should not match (partial second condition)
|
|
284
|
+
const element4 = createJSXElement(' onClick={handle} type="submit"')
|
|
285
|
+
expect(condition(element4)).toBe(false)
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { JSXElement } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Condition function that determines whether a JSX element should be transformed
|
|
5
|
+
*/
|
|
6
|
+
export type TransformCondition = (element: JSXElement) => boolean
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Configuration for a component transformation
|
|
10
|
+
*/
|
|
11
|
+
export interface ComponentTransformConfig {
|
|
12
|
+
/** Condition that must be met for the transform to occur */
|
|
13
|
+
condition: TransformCondition
|
|
14
|
+
/** Optional alias to use if target component conflicts with existing imports */
|
|
15
|
+
conflictAlias?: string
|
|
16
|
+
/** The source component name to transform from */
|
|
17
|
+
fromComponent: string
|
|
18
|
+
/** The package to import the source component from */
|
|
19
|
+
fromPackage: string
|
|
20
|
+
/** The target component name to transform to */
|
|
21
|
+
toComponent: string
|
|
22
|
+
/** The package to import the target component from */
|
|
23
|
+
toPackage: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Helper function to create a condition that checks for the presence of an attribute
|
|
28
|
+
*/
|
|
29
|
+
export function hasAttribute(attributeName: string): TransformCondition {
|
|
30
|
+
return (element: JSXElement) => {
|
|
31
|
+
const attributes = element.openingElement.attributes || []
|
|
32
|
+
return attributes.some(
|
|
33
|
+
(attr) =>
|
|
34
|
+
attr.type === "JSXAttribute" &&
|
|
35
|
+
attr.name?.type === "JSXIdentifier" &&
|
|
36
|
+
attr.name.name === attributeName
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Helper function to create a condition that checks for an attribute with a specific value
|
|
43
|
+
*/
|
|
44
|
+
export function hasAttributeValue(
|
|
45
|
+
attributeName: string,
|
|
46
|
+
value: string
|
|
47
|
+
): TransformCondition {
|
|
48
|
+
return (element: JSXElement) => {
|
|
49
|
+
const attributes = element.openingElement.attributes || []
|
|
50
|
+
return attributes.some(
|
|
51
|
+
(attr) =>
|
|
52
|
+
attr.type === "JSXAttribute" &&
|
|
53
|
+
attr.name?.type === "JSXIdentifier" &&
|
|
54
|
+
attr.name.name === attributeName &&
|
|
55
|
+
attr.value?.type === "StringLiteral" &&
|
|
56
|
+
attr.value.value === value
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Helper function to combine multiple conditions with AND logic
|
|
63
|
+
*/
|
|
64
|
+
export function andConditions(
|
|
65
|
+
...conditions: TransformCondition[]
|
|
66
|
+
): TransformCondition {
|
|
67
|
+
return (element: JSXElement) =>
|
|
68
|
+
conditions.every((condition) => condition(element))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Helper function to combine multiple conditions with OR logic
|
|
73
|
+
*/
|
|
74
|
+
export function orConditions(
|
|
75
|
+
...conditions: TransformCondition[]
|
|
76
|
+
): TransformCondition {
|
|
77
|
+
return (element: JSXElement) =>
|
|
78
|
+
conditions.some((condition) => condition(element))
|
|
79
|
+
}
|