@planningcenter/tapestry-migration-cli 2.3.0-rc.1 → 2.3.0-rc.11

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.
Files changed (50) hide show
  1. package/dist/tapestry-react-shim.cjs +5065 -0
  2. package/package.json +9 -5
  3. package/src/components/button/index.ts +48 -4
  4. package/src/components/button/transforms/auditSpreadProps.test.ts +352 -0
  5. package/src/components/button/transforms/auditSpreadProps.ts +24 -0
  6. package/src/components/button/transforms/childrenToLabel.test.ts +363 -0
  7. package/src/components/button/transforms/childrenToLabel.ts +84 -0
  8. package/src/components/button/transforms/commentOnVisualKindDifference.ts +24 -0
  9. package/src/components/button/transforms/convertStyleProps.test.ts +464 -0
  10. package/src/components/button/transforms/convertStyleProps.ts +16 -0
  11. package/src/components/button/transforms/iconToIconButton.test.ts +377 -0
  12. package/src/components/button/transforms/iconToIconButton.ts +53 -0
  13. package/src/components/button/transforms/removeAsButton.ts +15 -0
  14. package/src/components/button/transforms/removeDuplicateKeys.test.ts +302 -0
  15. package/src/components/button/transforms/removeDuplicateKeys.ts +8 -0
  16. package/src/components/button/transforms/reviewStyles.ts +17 -0
  17. package/src/components/button/transforms/spinnerToLoadingButton.test.ts +165 -0
  18. package/src/components/button/transforms/spinnerToLoadingButton.ts +14 -0
  19. package/src/components/button/transforms/themeVariantToKind.test.ts +401 -0
  20. package/src/components/button/transforms/themeVariantToKind.ts +90 -0
  21. package/src/components/button/transforms/unsupportedProps.ts +73 -0
  22. package/src/components/shared/actions/addAttribute.test.ts +300 -0
  23. package/src/components/shared/actions/addAttribute.ts +65 -0
  24. package/src/components/shared/actions/addComment.test.ts +1 -1
  25. package/src/components/shared/actions/addCommentToAttribute.test.ts +45 -0
  26. package/src/components/shared/actions/addCommentToAttribute.ts +28 -0
  27. package/src/components/shared/actions/addCommentToUnsupportedProps.ts +29 -0
  28. package/src/components/shared/actions/getAttributeValue.test.ts +261 -0
  29. package/src/components/shared/actions/getAttributeValue.ts +15 -0
  30. package/src/components/shared/actions/getSpreadProps.ts +7 -0
  31. package/src/components/shared/actions/hasSpreadProps.ts +7 -0
  32. package/src/components/shared/actions/removeChildren.ts +7 -0
  33. package/src/components/shared/actions/removeDuplicateKeys.test.ts +280 -0
  34. package/src/components/shared/actions/removeDuplicateKeys.ts +45 -0
  35. package/src/components/shared/actions/removeUnusedImport.test.ts +302 -0
  36. package/src/components/shared/actions/removeUnusedImport.ts +81 -0
  37. package/src/components/shared/actions/transformElementName.test.ts +9 -9
  38. package/src/components/shared/actions/transformElementName.ts +13 -16
  39. package/src/components/shared/conditions/hasChildren.ts +5 -0
  40. package/src/components/shared/getJavaScriptTheme.ts +68 -0
  41. package/src/components/shared/jsThemeLoader.ts +85 -0
  42. package/src/components/shared/transformFactories/attributeCombineFactory.test.ts +374 -0
  43. package/src/components/shared/transformFactories/attributeCombineFactory.ts +300 -0
  44. package/src/components/shared/transformFactories/attributeTransformFactory.ts +14 -6
  45. package/src/components/shared/transformFactories/componentTransformFactory.ts +1 -1
  46. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +362 -0
  47. package/src/index.ts +4 -0
  48. package/src/stubs/stackViewPlugin.ts +33 -0
  49. package/src/stubs/tapestry-stub.ts +16 -0
  50. package/src/tapestry-react-shim.ts +7 -0
@@ -0,0 +1,300 @@
1
+ import jscodeshift, {
2
+ BooleanLiteral,
3
+ ConditionalExpression,
4
+ Identifier,
5
+ JSXAttribute,
6
+ JSXElement,
7
+ JSXExpressionContainer,
8
+ JSXIdentifier,
9
+ StringLiteral,
10
+ } from "jscodeshift"
11
+ import { describe, expect, it } from "vitest"
12
+
13
+ import { addAttribute, Conditional } from "./addAttribute"
14
+
15
+ const j = jscodeshift.withParser("tsx")
16
+
17
+ function createElementFromCode(code: string): JSXElement {
18
+ const source = j(`<div>${code}</div>`)
19
+ return source.find(j.JSXElement).at(0).get().value.children?.[0] as JSXElement
20
+ }
21
+
22
+ function getAttributeFromElement(
23
+ element: JSXElement,
24
+ name: string
25
+ ): JSXAttribute | null {
26
+ const attributes = element.openingElement.attributes || []
27
+ return (
28
+ (attributes.find(
29
+ (attr) =>
30
+ attr.type === "JSXAttribute" &&
31
+ (attr.name as JSXIdentifier)?.name === name
32
+ ) as JSXAttribute) || null
33
+ )
34
+ }
35
+
36
+ describe("addAttribute", () => {
37
+ describe("string values", () => {
38
+ it("should add string attribute to element", () => {
39
+ const element = createElementFromCode("<Button>Save</Button>")
40
+
41
+ addAttribute({ element, j, name: "kind", value: "primary" })
42
+
43
+ const kindAttr = getAttributeFromElement(element, "kind")
44
+ expect(kindAttr).not.toBeNull()
45
+ expect(kindAttr?.value?.type).toBe("StringLiteral")
46
+ expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
47
+ })
48
+
49
+ it("should add multiple string attributes", () => {
50
+ const element = createElementFromCode("<Button>Save</Button>")
51
+
52
+ addAttribute({ element, j, name: "kind", value: "primary" })
53
+ addAttribute({ element, j, name: "size", value: "large" })
54
+
55
+ const kindAttr = getAttributeFromElement(element, "kind")
56
+ const sizeAttr = getAttributeFromElement(element, "size")
57
+
58
+ expect(kindAttr).not.toBeNull()
59
+ expect(sizeAttr).not.toBeNull()
60
+ expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
61
+ expect((sizeAttr?.value as StringLiteral)?.value).toBe("large")
62
+ })
63
+
64
+ it("should handle empty string values", () => {
65
+ const element = createElementFromCode("<Button>Save</Button>")
66
+
67
+ addAttribute({ element, j, name: "title", value: "" })
68
+
69
+ const titleAttr = getAttributeFromElement(element, "title")
70
+ expect(titleAttr).not.toBeNull()
71
+ expect(titleAttr?.value?.type).toBe("StringLiteral")
72
+ expect((titleAttr?.value as StringLiteral)?.value).toBe("")
73
+ })
74
+
75
+ it("should handle special characters in string values", () => {
76
+ const element = createElementFromCode("<Button>Save</Button>")
77
+
78
+ addAttribute({ element, j, name: "aria-label", value: "Save & Continue" })
79
+
80
+ const ariaAttr = getAttributeFromElement(element, "aria-label")
81
+ expect(ariaAttr).not.toBeNull()
82
+ expect((ariaAttr?.value as StringLiteral)?.value).toBe("Save & Continue")
83
+ })
84
+ })
85
+
86
+ describe("boolean values", () => {
87
+ it("should add boolean true attribute", () => {
88
+ const element = createElementFromCode("<Button>Save</Button>")
89
+
90
+ addAttribute({ element, j, name: "disabled", value: true })
91
+
92
+ const disabledAttr = getAttributeFromElement(element, "disabled")
93
+ expect(disabledAttr).not.toBeNull()
94
+ expect(disabledAttr?.value?.type).toBe("JSXExpressionContainer")
95
+
96
+ const container = disabledAttr?.value as JSXExpressionContainer
97
+ expect(container.expression.type).toBe("BooleanLiteral")
98
+ expect((container.expression as BooleanLiteral).value).toBe(true)
99
+ })
100
+
101
+ it("should add boolean false attribute", () => {
102
+ const element = createElementFromCode("<Button>Save</Button>")
103
+
104
+ addAttribute({ element, j, name: "loading", value: false })
105
+
106
+ const loadingAttr = getAttributeFromElement(element, "loading")
107
+ expect(loadingAttr).not.toBeNull()
108
+ expect(loadingAttr?.value?.type).toBe("JSXExpressionContainer")
109
+
110
+ const container = loadingAttr?.value as JSXExpressionContainer
111
+ expect(container.expression.type).toBe("BooleanLiteral")
112
+ expect((container.expression as BooleanLiteral).value).toBe(false)
113
+ })
114
+ })
115
+
116
+ describe("null values", () => {
117
+ it("should not add attribute when value is null", () => {
118
+ const element = createElementFromCode("<Button>Save</Button>")
119
+ const initialAttrCount = (element.openingElement.attributes || []).length
120
+
121
+ addAttribute({ element, j, name: "kind", value: null })
122
+
123
+ const finalAttrCount = (element.openingElement.attributes || []).length
124
+ expect(finalAttrCount).toBe(initialAttrCount)
125
+
126
+ const kindAttr = getAttributeFromElement(element, "kind")
127
+ expect(kindAttr).toBeNull()
128
+ })
129
+ })
130
+
131
+ describe("conditional values", () => {
132
+ it("should add conditional attribute with test, consequent, and alternate", () => {
133
+ const element = createElementFromCode("<Button>Save</Button>")
134
+ const conditionalValue: Conditional = {
135
+ alternate: "secondary",
136
+ consequent: "primary",
137
+ test: "isPrimary",
138
+ }
139
+
140
+ addAttribute({ element, j, name: "kind", value: conditionalValue })
141
+
142
+ const kindAttr = getAttributeFromElement(element, "kind")
143
+ expect(kindAttr).not.toBeNull()
144
+ expect(kindAttr?.value?.type).toBe("JSXExpressionContainer")
145
+
146
+ const container = kindAttr?.value as JSXExpressionContainer
147
+ expect(container.expression.type).toBe("ConditionalExpression")
148
+
149
+ const conditional = container.expression as ConditionalExpression
150
+ expect(conditional.test.type).toBe("Identifier")
151
+ expect((conditional.test as Identifier).name).toBe("isPrimary")
152
+ expect(conditional.consequent.type).toBe("StringLiteral")
153
+ expect((conditional.consequent as StringLiteral).value).toBe("primary")
154
+ expect(conditional.alternate.type).toBe("StringLiteral")
155
+ expect((conditional.alternate as StringLiteral).value).toBe("secondary")
156
+ })
157
+
158
+ it("should handle conditional with empty string values", () => {
159
+ const element = createElementFromCode("<Button>Save</Button>")
160
+ const conditionalValue: Conditional = {
161
+ alternate: "",
162
+ consequent: "primary",
163
+ test: "hasTheme",
164
+ }
165
+
166
+ addAttribute({ element, j, name: "kind", value: conditionalValue })
167
+
168
+ const kindAttr = getAttributeFromElement(element, "kind")
169
+ expect(kindAttr).not.toBeNull()
170
+
171
+ const container = kindAttr?.value as JSXExpressionContainer
172
+ const conditional = container.expression as ConditionalExpression
173
+ expect((conditional.consequent as StringLiteral).value).toBe("primary")
174
+ expect((conditional.alternate as StringLiteral).value).toBe("")
175
+ })
176
+
177
+ it("should handle conditional with same consequent and alternate", () => {
178
+ const element = createElementFromCode("<Button>Save</Button>")
179
+ const conditionalValue: Conditional = {
180
+ alternate: "neutral",
181
+ consequent: "neutral",
182
+ test: "condition",
183
+ }
184
+
185
+ addAttribute({ element, j, name: "kind", value: conditionalValue })
186
+
187
+ const kindAttr = getAttributeFromElement(element, "kind")
188
+ expect(kindAttr).not.toBeNull()
189
+
190
+ const container = kindAttr?.value as JSXExpressionContainer
191
+ const conditional = container.expression as ConditionalExpression
192
+ expect((conditional.consequent as StringLiteral).value).toBe("neutral")
193
+ expect((conditional.alternate as StringLiteral).value).toBe("neutral")
194
+ })
195
+
196
+ it("should render conditional attribute correctly in source", () => {
197
+ const source = j("<Button>Save</Button>")
198
+ const element = source.find(j.JSXElement).at(0).get().value as JSXElement
199
+ const conditionalValue: Conditional = {
200
+ alternate: "secondary",
201
+ consequent: "primary",
202
+ test: "isPrimary",
203
+ }
204
+
205
+ addAttribute({ element, j, name: "kind", value: conditionalValue })
206
+
207
+ const result = source.toSource()
208
+ expect(result).toContain('kind={isPrimary ? "primary" : "secondary"}')
209
+ })
210
+
211
+ it("should handle multiple conditional attributes", () => {
212
+ const element = createElementFromCode("<Button>Save</Button>")
213
+ const kindConditional: Conditional = {
214
+ alternate: "secondary",
215
+ consequent: "primary",
216
+ test: "isPrimary",
217
+ }
218
+ const sizeConditional: Conditional = {
219
+ alternate: "small",
220
+ consequent: "large",
221
+ test: "isLarge",
222
+ }
223
+
224
+ addAttribute({ element, j, name: "kind", value: kindConditional })
225
+ addAttribute({ element, j, name: "size", value: sizeConditional })
226
+
227
+ const kindAttr = getAttributeFromElement(element, "kind")
228
+ const sizeAttr = getAttributeFromElement(element, "size")
229
+
230
+ expect(kindAttr).not.toBeNull()
231
+ expect(sizeAttr).not.toBeNull()
232
+ expect(element.openingElement.attributes).toHaveLength(2)
233
+ })
234
+ })
235
+
236
+ describe("adding to elements with existing attributes", () => {
237
+ it("should add attribute to element that already has attributes", () => {
238
+ const element = createElementFromCode(
239
+ '<Button onClick={handleClick} className="btn">Save</Button>'
240
+ )
241
+
242
+ addAttribute({ element, j, name: "kind", value: "primary" })
243
+
244
+ expect(element.openingElement.attributes).toHaveLength(3)
245
+
246
+ const kindAttr = getAttributeFromElement(element, "kind")
247
+ expect(kindAttr).not.toBeNull()
248
+ expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
249
+
250
+ const onClickAttr = getAttributeFromElement(element, "onClick")
251
+ const classAttr = getAttributeFromElement(element, "className")
252
+ expect(onClickAttr).not.toBeNull()
253
+ expect(classAttr).not.toBeNull()
254
+ })
255
+ })
256
+
257
+ describe("adding to self-closing elements", () => {
258
+ it("should add attribute to self-closing element", () => {
259
+ const element = createElementFromCode("<Button />")
260
+
261
+ addAttribute({ element, j, name: "kind", value: "primary" })
262
+
263
+ const kindAttr = getAttributeFromElement(element, "kind")
264
+ expect(kindAttr).not.toBeNull()
265
+ expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
266
+ })
267
+ })
268
+
269
+ describe("integration with JSCodeshift", () => {
270
+ it("should render correctly in transformed source", () => {
271
+ const source = j("<Button>Save</Button>")
272
+ const element = source.find(j.JSXElement).at(0).get().value as JSXElement
273
+
274
+ addAttribute({ element, j, name: "kind", value: "primary" })
275
+ addAttribute({ element, j, name: "disabled", value: true })
276
+
277
+ const result = source.toSource()
278
+
279
+ expect(result).toContain('kind="primary"')
280
+ expect(result).toContain("disabled={true}")
281
+ expect(result).toContain("<Button")
282
+ expect(result).toContain(">Save</Button>")
283
+ })
284
+
285
+ it("should handle complex attribute names", () => {
286
+ const element = createElementFromCode("<Button>Save</Button>")
287
+
288
+ addAttribute({ element, j, name: "data-testid", value: "save-button" })
289
+ addAttribute({ element, j, name: "aria-describedby", value: "help-text" })
290
+
291
+ const testIdAttr = getAttributeFromElement(element, "data-testid")
292
+ const ariaAttr = getAttributeFromElement(element, "aria-describedby")
293
+
294
+ expect(testIdAttr).not.toBeNull()
295
+ expect(ariaAttr).not.toBeNull()
296
+ expect((testIdAttr?.value as StringLiteral)?.value).toBe("save-button")
297
+ expect((ariaAttr?.value as StringLiteral)?.value).toBe("help-text")
298
+ })
299
+ })
300
+ })
@@ -0,0 +1,65 @@
1
+ import { JSCodeshift, JSXElement } from "jscodeshift"
2
+
3
+ export interface Conditional {
4
+ alternate: string
5
+ consequent: string
6
+ test: string
7
+ }
8
+
9
+ function formatValue(value: string | boolean, j: JSCodeshift) {
10
+ if (typeof value === "string") {
11
+ return j.stringLiteral(value)
12
+ } else if (typeof value === "boolean") {
13
+ return j.jsxExpressionContainer(j.booleanLiteral(value))
14
+ } else {
15
+ throw new Error(`Unsupported attribute value type: ${typeof value}`)
16
+ }
17
+ }
18
+
19
+ export function addAttribute({
20
+ element,
21
+ name,
22
+ j,
23
+ value,
24
+ }: {
25
+ element: JSXElement
26
+ j: JSCodeshift
27
+ name: string
28
+ value: string | boolean | Conditional | null
29
+ }) {
30
+ if (value === null) return
31
+ const attributes = element.openingElement.attributes || []
32
+
33
+ if (
34
+ typeof value === "object" &&
35
+ "test" in value &&
36
+ "consequent" in value &&
37
+ "alternate" in value
38
+ ) {
39
+ addConditionalAttribute(value, element, j, name)
40
+ return
41
+ }
42
+
43
+ const formattedValue = formatValue(value, j)
44
+ attributes.push(j.jsxAttribute(j.jsxIdentifier(name), formattedValue))
45
+ }
46
+
47
+ function addConditionalAttribute(
48
+ { test, consequent, alternate }: Conditional,
49
+ element: JSXElement,
50
+ j: JSCodeshift,
51
+ targetAttribute: string
52
+ ) {
53
+ const conditional = j.jsxExpressionContainer(
54
+ j.conditionalExpression(
55
+ j.identifier(test),
56
+ j.stringLiteral(consequent),
57
+ j.stringLiteral(alternate)
58
+ )
59
+ )
60
+ const attribute = j.jsxAttribute(
61
+ j.jsxIdentifier(targetAttribute),
62
+ conditional
63
+ )
64
+ element.openingElement.attributes?.push(attribute)
65
+ }
@@ -84,7 +84,7 @@ describe("addComment", () => {
84
84
  .at(0)
85
85
  .get().value as JSXElement
86
86
 
87
- const commentText = "TODO: Update this nested button"
87
+ const commentText = "Update this nested button"
88
88
  addComment({
89
89
  element: buttonElement,
90
90
  j,
@@ -0,0 +1,45 @@
1
+ import jscodeshift, {
2
+ JSXAttribute,
3
+ JSXElement,
4
+ JSXIdentifier,
5
+ } from "jscodeshift"
6
+ import { describe, expect, it } from "vitest"
7
+
8
+ import { addCommentToAttribute } from "./addCommentToAttribute"
9
+
10
+ const j = jscodeshift.withParser("tsx")
11
+
12
+ describe("addCommentToAttribute", () => {
13
+ it("adds comments before attribute if provided", () => {
14
+ const outerCode = `
15
+ <div>
16
+ <Button
17
+ onClick={() => { console.log('hi')}}
18
+ kind='secondary'
19
+ fullWidth
20
+ {...props}>Save</Button>
21
+ </div>
22
+ `
23
+ const source = j(outerCode)
24
+ const buttonElement = source
25
+ .find(j.JSXElement)
26
+ .filter(
27
+ (path) =>
28
+ (path.value.openingElement.name as JSXIdentifier)?.name === "Button"
29
+ )
30
+ .at(0)
31
+ .get().value as JSXElement
32
+ const attribute = buttonElement.openingElement.attributes?.find(
33
+ (attr) => attr.type === "JSXAttribute" && attr.name.name === "kind"
34
+ ) as JSXAttribute
35
+
36
+ const commentText = "This needs to be updated"
37
+ addCommentToAttribute({ attribute, j, text: commentText })
38
+
39
+ const result = source.toSource()
40
+
41
+ expect(result)
42
+ .toContain(`/* TODO: tapestry-migration (kind): ${commentText} */
43
+ kind='secondary'`)
44
+ })
45
+ })
@@ -0,0 +1,28 @@
1
+ import { JSCodeshift, JSXAttribute, JSXSpreadAttribute } from "jscodeshift"
2
+
3
+ import { formatComment } from "./addComment"
4
+
5
+ export function addCommentToAttribute({
6
+ text,
7
+ attribute,
8
+ j,
9
+ }: {
10
+ attribute: JSXAttribute | JSXSpreadAttribute
11
+ j: JSCodeshift
12
+ text: string
13
+ }) {
14
+ const attributeName =
15
+ ((attribute.type === "JSXAttribute" && attribute.name.name) as string) ||
16
+ "spreadAttribute"
17
+ const comment = j.commentBlock(
18
+ formatComment(text, attributeName),
19
+ true,
20
+ false
21
+ )
22
+
23
+ if (attribute.comments) {
24
+ attribute.comments.unshift(comment)
25
+ } else {
26
+ attribute.comments = [comment]
27
+ }
28
+ }
@@ -0,0 +1,29 @@
1
+ import { JSCodeshift, JSXAttribute, JSXElement } from "jscodeshift"
2
+
3
+ import { addCommentToAttribute } from "./addCommentToAttribute"
4
+
5
+ export function addCommentToUnsupportedProps({
6
+ element,
7
+ j,
8
+ props,
9
+ messageSuffix = () => "",
10
+ }: {
11
+ element: JSXElement
12
+ j: JSCodeshift
13
+ messageSuffix?: (prop: string) => string
14
+ props: string[]
15
+ }): boolean {
16
+ const unsupportedAttributes = (
17
+ element.openingElement.attributes || []
18
+ ).filter(
19
+ (attr) =>
20
+ attr.type === "JSXAttribute" && props.includes(attr.name.name as string)
21
+ ) as JSXAttribute[]
22
+
23
+ unsupportedAttributes.forEach((attribute) => {
24
+ const propName = attribute.name.name as string
25
+ const text = `'${propName}' is not supported, please migrate as needed.${messageSuffix(propName)}`
26
+ addCommentToAttribute({ attribute, j, text })
27
+ })
28
+ return unsupportedAttributes.length > 0
29
+ }