@planningcenter/tapestry-migration-cli 2.1.1-rc.2 → 2.1.1-rc.3

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 (29) hide show
  1. package/package.json +2 -2
  2. package/src/components/button/index.ts +2 -1
  3. package/src/components/button/transforms/linkToButton.ts +4 -9
  4. package/src/components/button/transforms/titleToLabel.test.ts +418 -0
  5. package/src/components/button/transforms/titleToLabel.ts +19 -0
  6. package/src/components/shared/actions/transformAttributeName.ts +20 -0
  7. package/src/components/shared/actions/transformElementName.test.ts +59 -0
  8. package/src/components/shared/actions/transformElementName.ts +27 -0
  9. package/src/components/shared/conditions/andConditions.test.ts +65 -0
  10. package/src/components/shared/conditions/andConditions.ts +13 -0
  11. package/src/components/shared/conditions/hasAttribute.test.ts +43 -0
  12. package/src/components/shared/conditions/hasAttribute.ts +18 -0
  13. package/src/components/shared/conditions/hasAttributeValue.test.ts +48 -0
  14. package/src/components/shared/conditions/hasAttributeValue.ts +23 -0
  15. package/src/components/shared/conditions/helpers/createJSXElement.ts +9 -0
  16. package/src/components/shared/conditions/index.test.ts +63 -0
  17. package/src/components/shared/conditions/orConditions.test.ts +76 -0
  18. package/src/components/shared/conditions/orConditions.ts +13 -0
  19. package/src/components/shared/findAttribute.ts +15 -0
  20. package/src/components/shared/transformFactories/attributeTransformFactory.test.ts +88 -0
  21. package/src/components/shared/transformFactories/attributeTransformFactory.ts +51 -0
  22. package/src/components/shared/transformFactories/componentTransformFactory.test.ts +7 -0
  23. package/src/components/shared/transformFactories/componentTransformFactory.ts +77 -0
  24. package/src/components/shared/{componentTransformUtilities.test.ts → transformFactories/helpers/manageImports.test.ts} +1 -55
  25. package/src/components/shared/{componentTransformUtilities.ts → transformFactories/helpers/manageImports.ts} +17 -93
  26. package/src/components/shared/types.ts +6 -0
  27. package/src/components/shared/getTapestryReactImportName.ts +0 -35
  28. package/src/components/shared/transformConfig.test.ts +0 -288
  29. package/src/components/shared/transformConfig.ts +0 -79
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ import { hasAttribute } from "./hasAttribute"
4
+ import { createJSXElement } from "./helpers/createJSXElement"
5
+
6
+ describe("hasAttribute", () => {
7
+ it("should return true when attribute exists", () => {
8
+ const condition = hasAttribute("href")
9
+ const element = createJSXElement(' href="/test"')
10
+
11
+ expect(condition(element)).toBe(true)
12
+ })
13
+
14
+ it("should return false when attribute does not exist", () => {
15
+ const condition = hasAttribute("href")
16
+ const element = createJSXElement(' className="test"')
17
+
18
+ expect(condition(element)).toBe(false)
19
+ })
20
+
21
+ it("should return true for attribute without value", () => {
22
+ const condition = hasAttribute("disabled")
23
+ const element = createJSXElement(" disabled")
24
+
25
+ expect(condition(element)).toBe(true)
26
+ })
27
+
28
+ it("should return false for empty element", () => {
29
+ const condition = hasAttribute("href")
30
+ const element = createJSXElement("")
31
+
32
+ expect(condition(element)).toBe(false)
33
+ })
34
+
35
+ it("should handle multiple attributes", () => {
36
+ const condition = hasAttribute("onClick")
37
+ const element = createJSXElement(
38
+ ' className="test" onClick={handleClick} disabled'
39
+ )
40
+
41
+ expect(condition(element)).toBe(true)
42
+ })
43
+ })
@@ -0,0 +1,18 @@
1
+ import { JSXElement } from "jscodeshift"
2
+
3
+ import { TransformCondition } from "../types"
4
+
5
+ /**
6
+ * Helper function to create a condition that checks for the presence of an attribute
7
+ */
8
+ export function hasAttribute(attributeName: string): TransformCondition {
9
+ return (element: JSXElement) => {
10
+ const attributes = element.openingElement.attributes || []
11
+ return attributes.some(
12
+ (attr) =>
13
+ attr.type === "JSXAttribute" &&
14
+ attr.name?.type === "JSXIdentifier" &&
15
+ attr.name.name === attributeName
16
+ )
17
+ }
18
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ import { hasAttributeValue } from "./hasAttributeValue"
4
+ import { createJSXElement } from "./helpers/createJSXElement"
5
+
6
+ describe("hasAttributeValue", () => {
7
+ it("should return true when attribute has exact string value", () => {
8
+ const condition = hasAttributeValue("as", "a")
9
+ const element = createJSXElement(' as="a"')
10
+
11
+ expect(condition(element)).toBe(true)
12
+ })
13
+
14
+ it("should return false when attribute has different value", () => {
15
+ const condition = hasAttributeValue("as", "a")
16
+ const element = createJSXElement(' as="button"')
17
+
18
+ expect(condition(element)).toBe(false)
19
+ })
20
+
21
+ it("should return false when attribute does not exist", () => {
22
+ const condition = hasAttributeValue("as", "a")
23
+ const element = createJSXElement(' className="test"')
24
+
25
+ expect(condition(element)).toBe(false)
26
+ })
27
+
28
+ it("should return false when attribute exists but has no value", () => {
29
+ const condition = hasAttributeValue("disabled", "true")
30
+ const element = createJSXElement(" disabled")
31
+
32
+ expect(condition(element)).toBe(false)
33
+ })
34
+
35
+ it("should return false for expression values", () => {
36
+ const condition = hasAttributeValue("onClick", "handleClick")
37
+ const element = createJSXElement(" onClick={handleClick}")
38
+
39
+ expect(condition(element)).toBe(false)
40
+ })
41
+
42
+ it("should handle multiple attributes with different values", () => {
43
+ const condition = hasAttributeValue("type", "submit")
44
+ const element = createJSXElement(' className="test" type="submit" disabled')
45
+
46
+ expect(condition(element)).toBe(true)
47
+ })
48
+ })
@@ -0,0 +1,23 @@
1
+ import { JSXElement } from "jscodeshift"
2
+
3
+ import { TransformCondition } from "../types"
4
+
5
+ /**
6
+ * Helper function to create a condition that checks for an attribute with a specific value
7
+ */
8
+ export function hasAttributeValue(
9
+ attributeName: string,
10
+ value: string
11
+ ): TransformCondition {
12
+ return (element: JSXElement) => {
13
+ const attributes = element.openingElement.attributes || []
14
+ return attributes.some(
15
+ (attr) =>
16
+ attr.type === "JSXAttribute" &&
17
+ attr.name?.type === "JSXIdentifier" &&
18
+ attr.name.name === attributeName &&
19
+ attr.value?.type === "StringLiteral" &&
20
+ attr.value.value === value
21
+ )
22
+ }
23
+ }
@@ -0,0 +1,9 @@
1
+ import jscodeshift, { JSXElement } from "jscodeshift"
2
+
3
+ const j = jscodeshift.withParser("tsx")
4
+
5
+ export function createJSXElement(attributes: string): JSXElement {
6
+ const code = `<Button${attributes}>Content</Button>`
7
+ const ast = j(code)
8
+ return ast.find(j.JSXElement).get().value as JSXElement
9
+ }
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ import { andConditions } from "./andConditions"
4
+ import { hasAttribute } from "./hasAttribute"
5
+ import { hasAttributeValue } from "./hasAttributeValue"
6
+ import { createJSXElement } from "./helpers/createJSXElement"
7
+ import { orConditions } from "./orConditions"
8
+
9
+ describe("transformConfig", () => {
10
+ describe("complex condition combinations", () => {
11
+ it("should handle nested and/or conditions", () => {
12
+ const condition = andConditions(
13
+ hasAttribute("className"),
14
+ orConditions(hasAttribute("href"), hasAttribute("onClick"))
15
+ )
16
+
17
+ // Should match className + href
18
+ const element1 = createJSXElement(' className="btn" href="/test"')
19
+ expect(condition(element1)).toBe(true)
20
+
21
+ // Should match className + onClick
22
+ const element2 = createJSXElement(' className="btn" onClick={handle}')
23
+ expect(condition(element2)).toBe(true)
24
+
25
+ // Should not match (missing className)
26
+ const element3 = createJSXElement(' href="/test"')
27
+ expect(condition(element3)).toBe(false)
28
+
29
+ // Should not match (missing href/onClick)
30
+ const element4 = createJSXElement(' className="btn"')
31
+ expect(condition(element4)).toBe(false)
32
+ })
33
+
34
+ it("should handle or of and conditions", () => {
35
+ const condition = orConditions(
36
+ andConditions(
37
+ hasAttribute("href"),
38
+ hasAttributeValue("target", "_blank")
39
+ ),
40
+ andConditions(
41
+ hasAttribute("onClick"),
42
+ hasAttributeValue("type", "button")
43
+ )
44
+ )
45
+
46
+ // Should match first AND condition
47
+ const element1 = createJSXElement(' href="/test" target="_blank"')
48
+ expect(condition(element1)).toBe(true)
49
+
50
+ // Should match second AND condition
51
+ const element2 = createJSXElement(' onClick={handle} type="button"')
52
+ expect(condition(element2)).toBe(true)
53
+
54
+ // Should not match (partial first condition)
55
+ const element3 = createJSXElement(' href="/test" target="_self"')
56
+ expect(condition(element3)).toBe(false)
57
+
58
+ // Should not match (partial second condition)
59
+ const element4 = createJSXElement(' onClick={handle} type="submit"')
60
+ expect(condition(element4)).toBe(false)
61
+ })
62
+ })
63
+ })
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ import { hasAttribute } from "./hasAttribute"
4
+ import { hasAttributeValue } from "./hasAttributeValue"
5
+ import { createJSXElement } from "./helpers/createJSXElement"
6
+ import { orConditions } from "./orConditions"
7
+
8
+ describe("orConditions", () => {
9
+ it("should return true when at least one condition is met", () => {
10
+ const condition = orConditions(
11
+ hasAttribute("href"),
12
+ hasAttribute("onClick")
13
+ )
14
+ const element = createJSXElement(' href="/test"')
15
+
16
+ expect(condition(element)).toBe(true)
17
+ })
18
+
19
+ it("should return true when all conditions are met", () => {
20
+ const condition = orConditions(
21
+ hasAttribute("href"),
22
+ hasAttribute("onClick")
23
+ )
24
+ const element = createJSXElement(' href="/test" onClick={handleClick}')
25
+
26
+ expect(condition(element)).toBe(true)
27
+ })
28
+
29
+ it("should return false when no conditions are met", () => {
30
+ const condition = orConditions(
31
+ hasAttribute("href"),
32
+ hasAttribute("onClick")
33
+ )
34
+ const element = createJSXElement(' className="test"')
35
+
36
+ expect(condition(element)).toBe(false)
37
+ })
38
+
39
+ it("should handle single condition", () => {
40
+ const condition = orConditions(hasAttribute("disabled"))
41
+ const element = createJSXElement(" disabled")
42
+
43
+ expect(condition(element)).toBe(true)
44
+ })
45
+
46
+ it("should handle empty conditions (return false)", () => {
47
+ const condition = orConditions()
48
+ const element = createJSXElement("")
49
+
50
+ expect(condition(element)).toBe(false)
51
+ })
52
+
53
+ it("should handle mixed attribute and value conditions", () => {
54
+ const condition = orConditions(
55
+ hasAttributeValue("type", "submit"),
56
+ hasAttribute("href"),
57
+ hasAttributeValue("role", "button")
58
+ )
59
+
60
+ // Should match type="submit"
61
+ const element1 = createJSXElement(' type="submit"')
62
+ expect(condition(element1)).toBe(true)
63
+
64
+ // Should match href
65
+ const element2 = createJSXElement(' href="/test"')
66
+ expect(condition(element2)).toBe(true)
67
+
68
+ // Should match role="button"
69
+ const element3 = createJSXElement(' role="button"')
70
+ expect(condition(element3)).toBe(true)
71
+
72
+ // Should not match
73
+ const element4 = createJSXElement(' className="test" type="button"')
74
+ expect(condition(element4)).toBe(false)
75
+ })
76
+ })
@@ -0,0 +1,13 @@
1
+ import { JSXElement } from "jscodeshift"
2
+
3
+ import { TransformCondition } from "../types"
4
+
5
+ /**
6
+ * Helper function to combine multiple conditions with OR logic
7
+ */
8
+ export function orConditions(
9
+ ...conditions: TransformCondition[]
10
+ ): TransformCondition {
11
+ return (element: JSXElement) =>
12
+ conditions.some((condition) => condition(element))
13
+ }
@@ -0,0 +1,15 @@
1
+ import { JSXAttribute, JSXOpeningElement } from "jscodeshift"
2
+
3
+ /**
4
+ * Finds an attribute by name in a JSX element's attributes array
5
+ */
6
+ export function findAttribute(
7
+ attributes: JSXOpeningElement["attributes"],
8
+ attributeName: string
9
+ ): JSXAttribute | undefined {
10
+ if (!attributes) return
11
+
12
+ return attributes.find(
13
+ (attr) => attr.type === "JSXAttribute" && attr.name?.name === attributeName
14
+ ) as JSXAttribute | undefined
15
+ }
@@ -0,0 +1,88 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import { hasAttribute } from "../conditions/hasAttribute"
5
+ import { attributeTransformFactory } from "./attributeTransformFactory"
6
+
7
+ const j = jscodeshift.withParser("tsx")
8
+
9
+ describe("attributeTransformFactory", () => {
10
+ it("should create a working attribute transform", () => {
11
+ const transform = attributeTransformFactory({
12
+ condition: hasAttribute("title"),
13
+ targetComponent: "Button",
14
+ targetPackage: "@planningcenter/tapestry-react",
15
+ transform: (element) => {
16
+ const attributes = element.openingElement.attributes || []
17
+ const titleAttr = attributes.find(
18
+ (attr) => attr.type === "JSXAttribute" && attr.name?.name === "title"
19
+ )
20
+ if (titleAttr?.type === "JSXAttribute") {
21
+ titleAttr.name.name = "label"
22
+ return true
23
+ }
24
+ return false
25
+ },
26
+ })
27
+ const fileInfo = {
28
+ path: "test.tsx",
29
+ source: `import { Button } from "@planningcenter/tapestry-react";<Button title="Save">Test</Button>`,
30
+ }
31
+ const api = { j, jscodeshift: j, report: () => {}, stats: () => {} }
32
+
33
+ const result = transform(fileInfo, api, {})
34
+ expect(result).toContain('label="Save"')
35
+ expect(result).not.toContain("title")
36
+ })
37
+
38
+ it("should return null when target component not imported", () => {
39
+ const transform = attributeTransformFactory({
40
+ condition: hasAttribute("title"),
41
+ targetComponent: "Button",
42
+ targetPackage: "@planningcenter/tapestry-react",
43
+ transform: () => true,
44
+ })
45
+ const fileInfo = {
46
+ path: "test.tsx",
47
+ source: `import { Link } from "@planningcenter/tapestry-react";<Link title="Test">Test</Link>`,
48
+ }
49
+ const api = { j, jscodeshift: j, report: () => {}, stats: () => {} }
50
+
51
+ const result = transform(fileInfo, api, {})
52
+ expect(result).toBe(null)
53
+ })
54
+
55
+ it("should return null when condition not met", () => {
56
+ const transform = attributeTransformFactory({
57
+ condition: hasAttribute("nonexistent"),
58
+ targetComponent: "Button",
59
+ targetPackage: "@planningcenter/tapestry-react",
60
+ transform: () => true,
61
+ })
62
+ const fileInfo = {
63
+ path: "test.tsx",
64
+ source: `import { Button } from "@planningcenter/tapestry-react";<Button>Test</Button>`,
65
+ }
66
+ const api = { j, jscodeshift: j, report: () => {}, stats: () => {} }
67
+
68
+ const result = transform(fileInfo, api, {})
69
+ expect(result).toBe(null)
70
+ })
71
+
72
+ it("should return null when transform returns false", () => {
73
+ const transform = attributeTransformFactory({
74
+ condition: hasAttribute("title"),
75
+ targetComponent: "Button",
76
+ targetPackage: "@planningcenter/tapestry-react",
77
+ transform: () => false,
78
+ })
79
+ const fileInfo = {
80
+ path: "test.tsx",
81
+ source: `import { Button } from "@planningcenter/tapestry-react";<Button title="Test">Test</Button>`,
82
+ }
83
+ const api = { j, jscodeshift: j, report: () => {}, stats: () => {} }
84
+
85
+ const result = transform(fileInfo, api, {})
86
+ expect(result).toBe(null)
87
+ })
88
+ })
@@ -0,0 +1,51 @@
1
+ import { JSCodeshift, JSXElement, Transform } from "jscodeshift"
2
+
3
+ import { TransformCondition } from "../types"
4
+ import { getImportName } from "./helpers/manageImports"
5
+
6
+ /**
7
+ * Main function to create an attribute transform based on configuration
8
+ */
9
+ export function attributeTransformFactory(config: {
10
+ /** Condition that must be met for the transform to occur */
11
+ condition: TransformCondition
12
+ /** Component to target for attribute transformation */
13
+ targetComponent: string
14
+ /** Package the target component is imported from */
15
+ targetPackage: string
16
+ /** Function that performs the actual attribute transformation */
17
+ transform: (element: JSXElement, j: JSCodeshift) => boolean
18
+ }): Transform {
19
+ return (fileInfo, api) => {
20
+ const j = api.jscodeshift
21
+ const source = j(fileInfo.source)
22
+ let hasChanges = false
23
+
24
+ // Get the local name of the target component
25
+ const targetComponentName = getImportName(
26
+ config.targetComponent,
27
+ config.targetPackage,
28
+ { j, source }
29
+ )
30
+
31
+ // Only proceed if target component is imported
32
+ if (!targetComponentName) {
33
+ return null
34
+ }
35
+
36
+ // Find and transform matching JSX elements
37
+ source
38
+ .find(j.JSXOpeningElement, { name: { name: targetComponentName } })
39
+ .forEach((path) => {
40
+ const element = path.parent.value
41
+
42
+ if (config.condition(element)) {
43
+ if (config.transform(element, j)) {
44
+ hasChanges = true
45
+ }
46
+ }
47
+ })
48
+
49
+ return hasChanges ? source.toSource() : null
50
+ }
51
+ }
@@ -0,0 +1,7 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ describe("componentTransformFactory", () => {
4
+ it("is a placeholder test", () => {
5
+ expect(true).toBe(true)
6
+ })
7
+ })
@@ -0,0 +1,77 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { transformElementName } from "../actions/transformElementName"
4
+ import { TransformCondition } from "../types"
5
+ import {
6
+ getImportName,
7
+ hasConflictingImport,
8
+ manageImports,
9
+ } from "./helpers/manageImports"
10
+
11
+ /**
12
+ * Main function to create a component transform based on configuration
13
+ */
14
+
15
+ export function componentTransformFactory(config: {
16
+ /** Condition that must be met for the transform to occur */
17
+ condition: TransformCondition
18
+ /** Optional alias to use if target component conflicts with existing imports */
19
+ conflictAlias?: string
20
+ /** The source component name to transform from */
21
+ fromComponent: string
22
+ /** The package to import the source component from */
23
+ fromPackage: string
24
+ /** The target component name to transform to */
25
+ toComponent: string
26
+ /** The package to import the target component from */
27
+ toPackage: string
28
+ }): Transform {
29
+ return (fileInfo, api) => {
30
+ const j = api.jscodeshift
31
+ const source = j(fileInfo.source)
32
+ let hasChanges = false
33
+
34
+ // Get the local name of the source component
35
+ const sourceComponentName = getImportName(
36
+ config.fromComponent,
37
+ config.fromPackage,
38
+ { j, source }
39
+ )
40
+
41
+ // Only proceed if source component is imported
42
+ if (!sourceComponentName) {
43
+ return null
44
+ }
45
+
46
+ // Check for conflicting imports
47
+ const hasConflict = hasConflictingImport(
48
+ config.toComponent,
49
+ config.toPackage,
50
+ { j, source }
51
+ )
52
+
53
+ const targetComponentName = hasConflict
54
+ ? config.conflictAlias || `T${config.toComponent}`
55
+ : config.toComponent
56
+
57
+ // Transform matching JSX elements
58
+ source
59
+ .find(j.JSXOpeningElement, { name: { name: sourceComponentName } })
60
+ .forEach((path) => {
61
+ const element = path.parent.value
62
+
63
+ if (config.condition(element)) {
64
+ if (transformElementName(path, targetComponentName)) {
65
+ hasChanges = true
66
+ }
67
+ }
68
+ })
69
+
70
+ if (hasChanges) {
71
+ // Handle import management
72
+ manageImports(source, config, sourceComponentName, targetComponentName, j)
73
+ }
74
+
75
+ return hasChanges ? source.toSource() : null
76
+ }
77
+ }
@@ -7,8 +7,7 @@ import {
7
7
  getImportName,
8
8
  hasConflictingImport,
9
9
  removeImportFromDeclaration,
10
- transformElementName,
11
- } from "./componentTransformUtilities"
10
+ } from "./manageImports"
12
11
 
13
12
  const j = jscodeshift.withParser("tsx")
14
13
 
@@ -176,59 +175,6 @@ describe("componentTransformUtilities", () => {
176
175
  })
177
176
  })
178
177
 
179
- describe("transformElementName", () => {
180
- it("should transform JSX element name", () => {
181
- const code = `<Button>Click me</Button>`
182
- const source = j(code)
183
- const elementPath = source.find(j.JSXOpeningElement).at(0)
184
-
185
- const result = transformElementName(elementPath.get(), "Link")
186
-
187
- expect(result).toBe(true)
188
- expect(source.toSource()).toContain("<Link>Click me</Link>")
189
- })
190
-
191
- it("should transform both opening and closing tags", () => {
192
- const code = `<Button className="test">Content</Button>`
193
- const source = j(code)
194
- const elementPath = source.find(j.JSXOpeningElement).at(0)
195
-
196
- const result = transformElementName(elementPath.get(), "Link")
197
-
198
- expect(result).toBe(true)
199
- const output = source.toSource()
200
- expect(output).toContain('<Link className="test">')
201
- expect(output).toContain("</Link>")
202
- })
203
-
204
- it("should handle self-closing tags", () => {
205
- const code = `<Button />`
206
- const source = j(code)
207
- const elementPath = source.find(j.JSXOpeningElement).at(0)
208
-
209
- const result = transformElementName(elementPath.get(), "Link")
210
-
211
- expect(result).toBe(true)
212
- expect(source.toSource()).toContain("<Link />")
213
- })
214
-
215
- it("should preserve all attributes", () => {
216
- const code = `<Button className="test" onClick={handler} disabled>Content</Button>`
217
- const source = j(code)
218
- const elementPath = source.find(j.JSXOpeningElement).at(0)
219
-
220
- const result = transformElementName(elementPath.get(), "Link")
221
-
222
- expect(result).toBe(true)
223
- const output = source.toSource()
224
- expect(output).toContain('className="test"')
225
- expect(output).toContain("onClick={handler}")
226
- expect(output).toContain("disabled")
227
- expect(output).toContain("<Link")
228
- expect(output).toContain("</Link>")
229
- })
230
- })
231
-
232
178
  describe("addImportToExisting", () => {
233
179
  it("should add new import to existing import declaration", () => {
234
180
  const code = `import { Button } from "@planningcenter/tapestry-react"`