@planningcenter/tapestry-migration-cli 3.5.0 → 3.6.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.
Files changed (34) hide show
  1. package/README.md +37 -0
  2. package/package.json +3 -3
  3. package/src/availableComponents.ts +1 -0
  4. package/src/components/button/transforms/tooltipToWrapper.test.ts +50 -3
  5. package/src/components/button/transforms/tooltipToWrapper.ts +18 -0
  6. package/src/components/flex/index.test.ts +140 -0
  7. package/src/components/flex/index.ts +40 -0
  8. package/src/components/flex/transforms/alignmentToAlign.test.ts +185 -0
  9. package/src/components/flex/transforms/alignmentToAlign.ts +73 -0
  10. package/src/components/flex/transforms/axisToDirection.test.ts +140 -0
  11. package/src/components/flex/transforms/axisToDirection.ts +51 -0
  12. package/src/components/flex/transforms/distributionToJustify.test.ts +214 -0
  13. package/src/components/flex/transforms/distributionToJustify.ts +76 -0
  14. package/src/components/flex/transforms/innerRefToRef.test.ts +100 -0
  15. package/src/components/flex/transforms/innerRefToRef.ts +14 -0
  16. package/src/components/flex/transforms/moveFlexImport.test.ts +202 -0
  17. package/src/components/flex/transforms/moveFlexImport.ts +14 -0
  18. package/src/components/flex/transforms/setDefaultAxis.test.ts +114 -0
  19. package/src/components/flex/transforms/setDefaultAxis.ts +29 -0
  20. package/src/components/flex/transforms/spacingToGap.test.ts +176 -0
  21. package/src/components/flex/transforms/spacingToGap.ts +49 -0
  22. package/src/components/select/index.ts +2 -0
  23. package/src/components/select/transforms/onChangeSignature.test.ts +224 -0
  24. package/src/components/select/transforms/onChangeSignature.ts +172 -0
  25. package/src/components/shared/actions/resolveConstantAttributeValue.test.ts +122 -0
  26. package/src/components/shared/actions/resolveConstantAttributeValue.ts +31 -0
  27. package/src/components/toggle-switch/index.ts +2 -0
  28. package/src/components/toggle-switch/transforms/onClickToOnChange.test.ts +210 -0
  29. package/src/components/toggle-switch/transforms/onClickToOnChange.ts +71 -0
  30. package/src/index.test.ts +79 -0
  31. package/src/index.ts +29 -0
  32. package/src/migrationList.ts +22 -0
  33. package/src/utils/componentLabel.test.ts +61 -0
  34. package/src/utils/componentLabel.ts +15 -0
@@ -0,0 +1,202 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./moveFlexImport"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string | null {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ return transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ {}
14
+ ) as string | null
15
+ }
16
+
17
+ describe("moveFlexImport transform", () => {
18
+ describe("basic transformations", () => {
19
+ it("renames StackView to Flex and moves the import to tapestry", () => {
20
+ const input = `
21
+ import { StackView } from "@planningcenter/tapestry-react"
22
+
23
+ export default function Test() {
24
+ return <StackView>Test</StackView>
25
+ }
26
+ `.trim()
27
+
28
+ const result = applyTransform(input)
29
+ expect(result).toContain(
30
+ 'import { Flex } from "@planningcenter/tapestry"'
31
+ )
32
+ expect(result).toContain("<Flex>Test</Flex>")
33
+ expect(result).not.toContain("StackView")
34
+ expect(result).not.toContain('from "@planningcenter/tapestry-react"')
35
+ })
36
+
37
+ it("renames multiple StackView usages", () => {
38
+ const input = `
39
+ import { StackView } from "@planningcenter/tapestry-react"
40
+
41
+ export default function Test() {
42
+ return (
43
+ <StackView>
44
+ <StackView>Inner</StackView>
45
+ </StackView>
46
+ )
47
+ }
48
+ `.trim()
49
+
50
+ const result = applyTransform(input)
51
+ expect(result).toContain(
52
+ 'import { Flex } from "@planningcenter/tapestry"'
53
+ )
54
+ expect(result).toContain("<Flex>")
55
+ expect(result).not.toContain("StackView")
56
+ })
57
+
58
+ it("preserves other imports from tapestry-react", () => {
59
+ const input = `
60
+ import { StackView, Button, Input } from "@planningcenter/tapestry-react"
61
+
62
+ export default function Test() {
63
+ return <StackView>Test</StackView>
64
+ }
65
+ `.trim()
66
+
67
+ const result = applyTransform(input)
68
+ expect(result).toContain(
69
+ 'import { Button, Input } from "@planningcenter/tapestry-react"'
70
+ )
71
+ expect(result).toContain(
72
+ 'import { Flex } from "@planningcenter/tapestry"'
73
+ )
74
+ expect(result).toContain("<Flex>Test</Flex>")
75
+ })
76
+
77
+ it("handles self-closing and propped StackView elements", () => {
78
+ const input = `
79
+ import { StackView } from "@planningcenter/tapestry-react"
80
+
81
+ export default function Test() {
82
+ return <StackView axis="vertical" spacing={2} />
83
+ }
84
+ `.trim()
85
+
86
+ const result = applyTransform(input)
87
+ expect(result).toContain(
88
+ 'import { Flex } from "@planningcenter/tapestry"'
89
+ )
90
+ expect(result).toContain('<Flex axis="vertical" spacing={2} />')
91
+ expect(result).not.toContain('from "@planningcenter/tapestry-react"')
92
+ })
93
+ })
94
+
95
+ describe("aliased imports", () => {
96
+ it("normalizes an aliased StackView import to Flex", () => {
97
+ const input = `
98
+ import { StackView as Stack } from "@planningcenter/tapestry-react"
99
+
100
+ export default function Test() {
101
+ return <Stack>Test</Stack>
102
+ }
103
+ `.trim()
104
+
105
+ const result = applyTransform(input)
106
+ expect(result).toContain(
107
+ 'import { Flex } from "@planningcenter/tapestry"'
108
+ )
109
+ expect(result).toContain("<Flex>Test</Flex>")
110
+ expect(result).not.toContain("Stack")
111
+ expect(result).not.toContain('from "@planningcenter/tapestry-react"')
112
+ })
113
+ })
114
+
115
+ describe("import conflict handling", () => {
116
+ it("aliases to TFlex when a Flex from another package is in scope", () => {
117
+ const input = `
118
+ import { Flex } from "some-other-lib"
119
+ import { StackView } from "@planningcenter/tapestry-react"
120
+
121
+ export default function Test() {
122
+ return <StackView>Test</StackView>
123
+ }
124
+ `.trim()
125
+
126
+ const result = applyTransform(input)
127
+ expect(result).toContain(
128
+ 'import { Flex as TFlex } from "@planningcenter/tapestry"'
129
+ )
130
+ expect(result).toContain('import { Flex } from "some-other-lib"')
131
+ expect(result).toContain("<TFlex>Test</TFlex>")
132
+ expect(result).not.toContain('from "@planningcenter/tapestry-react"')
133
+ })
134
+
135
+ it("merges with an existing Flex import from the same tapestry package", () => {
136
+ const input = `
137
+ import { Flex } from "@planningcenter/tapestry"
138
+ import { StackView } from "@planningcenter/tapestry-react"
139
+
140
+ export default function Test() {
141
+ return <StackView>Test</StackView>
142
+ }
143
+ `.trim()
144
+
145
+ const result = applyTransform(input)
146
+ expect(result).toContain(
147
+ 'import { Flex } from "@planningcenter/tapestry"'
148
+ )
149
+ expect(result).toContain("<Flex>Test</Flex>")
150
+ expect(result).not.toContain("TFlex")
151
+ expect(result).not.toContain('from "@planningcenter/tapestry-react"')
152
+ })
153
+
154
+ it("merges into an existing tapestry import that has other components", () => {
155
+ const input = `
156
+ import { Button, Input } from "@planningcenter/tapestry"
157
+ import { StackView } from "@planningcenter/tapestry-react"
158
+
159
+ export default function Test() {
160
+ return <StackView>Test</StackView>
161
+ }
162
+ `.trim()
163
+
164
+ const result = applyTransform(input)
165
+ expect(result).toContain(
166
+ 'import { Button, Input, Flex } from "@planningcenter/tapestry"'
167
+ )
168
+ expect(result).toContain("<Flex>Test</Flex>")
169
+ expect(result).not.toContain('from "@planningcenter/tapestry-react"')
170
+ })
171
+ })
172
+
173
+ describe("no changes scenarios", () => {
174
+ it("returns null when StackView is not imported from tapestry-react", () => {
175
+ const input = `
176
+ import { Button } from "@planningcenter/tapestry-react"
177
+
178
+ export default function Test() {
179
+ return <Button>Test</Button>
180
+ }
181
+ `.trim()
182
+
183
+ expect(applyTransform(input)).toBe(null)
184
+ })
185
+
186
+ it("returns null when StackView comes from a different package", () => {
187
+ const input = `
188
+ import { StackView } from "some-other-package"
189
+
190
+ export default function Test() {
191
+ return <StackView>Test</StackView>
192
+ }
193
+ `.trim()
194
+
195
+ expect(applyTransform(input)).toBe(null)
196
+ })
197
+
198
+ it("returns null for an empty file", () => {
199
+ expect(applyTransform("")).toBe(null)
200
+ })
201
+ })
202
+ })
@@ -0,0 +1,14 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { componentTransformFactory } from "../../shared/transformFactories/componentTransformFactory"
4
+
5
+ const transform: Transform = componentTransformFactory({
6
+ condition: () => true,
7
+ conflictAlias: "TFlex",
8
+ fromComponent: "StackView",
9
+ fromPackage: "@planningcenter/tapestry-react",
10
+ toComponent: "Flex",
11
+ toPackage: "@planningcenter/tapestry",
12
+ })
13
+
14
+ export default transform
@@ -0,0 +1,114 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./setDefaultAxis"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string | null {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ return transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ {}
14
+ ) as string | null
15
+ }
16
+
17
+ describe("setDefaultAxis transform", () => {
18
+ it('adds axis="vertical" to a StackView with no axis', () => {
19
+ const input = `
20
+ import { StackView } from "@planningcenter/tapestry-react"
21
+
22
+ export default function Test() {
23
+ return <StackView>Test</StackView>
24
+ }
25
+ `.trim()
26
+
27
+ const result = applyTransform(input)
28
+ expect(result).toContain('<StackView axis="vertical">Test</StackView>')
29
+ })
30
+
31
+ it("adds the default to self-closing elements", () => {
32
+ const input = `
33
+ import { StackView } from "@planningcenter/tapestry-react"
34
+
35
+ export default function Test() {
36
+ return <StackView spacing={2} />
37
+ }
38
+ `.trim()
39
+
40
+ const result = applyTransform(input)
41
+ expect(result).toContain('axis="vertical"')
42
+ expect(result).toContain("spacing={2}")
43
+ })
44
+
45
+ it("adds the default to every StackView lacking an axis", () => {
46
+ const input = `
47
+ import { StackView } from "@planningcenter/tapestry-react"
48
+
49
+ export default function Test() {
50
+ return (
51
+ <StackView>
52
+ <StackView>Inner</StackView>
53
+ </StackView>
54
+ )
55
+ }
56
+ `.trim()
57
+
58
+ const result = applyTransform(input)
59
+ expect(result?.match(/axis="vertical"/g)).toHaveLength(2)
60
+ })
61
+
62
+ it("does not change a StackView that already has an axis", () => {
63
+ const input = `
64
+ import { StackView } from "@planningcenter/tapestry-react"
65
+
66
+ export default function Test() {
67
+ return <StackView axis="horizontal">Test</StackView>
68
+ }
69
+ `.trim()
70
+
71
+ expect(applyTransform(input)).toBe(null)
72
+ })
73
+
74
+ it("skips elements with spread props", () => {
75
+ const input = `
76
+ import { StackView } from "@planningcenter/tapestry-react"
77
+
78
+ export default function Test(props) {
79
+ return <StackView {...props}>Test</StackView>
80
+ }
81
+ `.trim()
82
+
83
+ expect(applyTransform(input)).toBe(null)
84
+ })
85
+
86
+ it("respects an aliased StackView import", () => {
87
+ const input = `
88
+ import { StackView as Stack } from "@planningcenter/tapestry-react"
89
+
90
+ export default function Test() {
91
+ return <Stack>Test</Stack>
92
+ }
93
+ `.trim()
94
+
95
+ const result = applyTransform(input)
96
+ expect(result).toContain('<Stack axis="vertical">Test</Stack>')
97
+ })
98
+
99
+ it("returns null when StackView is not imported from tapestry-react", () => {
100
+ const input = `
101
+ import { StackView } from "some-other-package"
102
+
103
+ export default function Test() {
104
+ return <StackView>Test</StackView>
105
+ }
106
+ `.trim()
107
+
108
+ expect(applyTransform(input)).toBe(null)
109
+ })
110
+
111
+ it("returns null for an empty file", () => {
112
+ expect(applyTransform("")).toBe(null)
113
+ })
114
+ })
@@ -0,0 +1,29 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { addAttribute } from "../../shared/actions/addAttribute"
4
+ import { hasSpreadProps } from "../../shared/actions/hasSpreadProps"
5
+ import { andConditions } from "../../shared/conditions/andConditions"
6
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
7
+ import { notCondition } from "../../shared/conditions/notCondition"
8
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
9
+
10
+ const transform: Transform = attributeTransformFactory({
11
+ condition: andConditions(
12
+ notCondition(hasAttribute("axis")),
13
+ notCondition(hasSpreadProps)
14
+ ),
15
+ targetComponent: "StackView",
16
+ targetPackage: "@planningcenter/tapestry-react",
17
+ transform: (element, { j }) => {
18
+ addAttribute({
19
+ element,
20
+ j,
21
+ name: "axis",
22
+ value: "vertical",
23
+ })
24
+
25
+ return true
26
+ },
27
+ })
28
+
29
+ export default transform
@@ -0,0 +1,176 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./spacingToGap"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string | null {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ return transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ {}
14
+ ) as string | null
15
+ }
16
+
17
+ describe("spacingToGap transform", () => {
18
+ it("renames spacing to gap for all valid scale values", () => {
19
+ const input = `
20
+ import { StackView } from "@planningcenter/tapestry-react"
21
+
22
+ export default function Test() {
23
+ return (
24
+ <StackView spacing={0}>
25
+ <StackView spacing={0.25}>
26
+ <StackView spacing={0.5}>
27
+ <StackView spacing={1}>
28
+ <StackView spacing={1.5}>
29
+ <StackView spacing={2}>
30
+ <StackView spacing={3}>
31
+ <StackView spacing={4}>
32
+ <StackView spacing={5}>
33
+ <StackView spacing={6}>
34
+ <StackView spacing={7}>Test</StackView>
35
+ </StackView>
36
+ </StackView>
37
+ </StackView>
38
+ </StackView>
39
+ </StackView>
40
+ </StackView>
41
+ </StackView>
42
+ </StackView>
43
+ </StackView>
44
+ </StackView>
45
+ )
46
+ }
47
+ `.trim()
48
+
49
+ const result = applyTransform(input)
50
+ expect(result).toContain("gap={0}")
51
+ expect(result).toContain("gap={0.25}")
52
+ expect(result).toContain("gap={0.5}")
53
+ expect(result).toContain("gap={1}")
54
+ expect(result).toContain("gap={1.5}")
55
+ expect(result).toContain("gap={2}")
56
+ expect(result).toContain("gap={3}")
57
+ expect(result).toContain("gap={4}")
58
+ expect(result).toContain("gap={5}")
59
+ expect(result).toContain("gap={6}")
60
+ expect(result).toContain("gap={7}")
61
+ expect(result).not.toContain("spacing")
62
+ })
63
+
64
+ it("flags an off-scale number with a TODO", () => {
65
+ const input = `
66
+ import { StackView } from "@planningcenter/tapestry-react"
67
+
68
+ export default function Test() {
69
+ return <StackView spacing={10}>Test</StackView>
70
+ }
71
+ `.trim()
72
+
73
+ const result = applyTransform(input)
74
+ expect(result).toContain("spacing={10}")
75
+ expect(result).toContain("TODO: tapestry-migration (spacing)")
76
+ expect(result).not.toContain("gap=")
77
+ })
78
+
79
+ it("flags a string spacing value with a TODO", () => {
80
+ const input = `
81
+ import { StackView } from "@planningcenter/tapestry-react"
82
+
83
+ export default function Test() {
84
+ return <StackView spacing="8px">Test</StackView>
85
+ }
86
+ `.trim()
87
+
88
+ const result = applyTransform(input)
89
+ expect(result).toContain('spacing="8px"')
90
+ expect(result).toContain("TODO: tapestry-migration (spacing)")
91
+ expect(result).not.toContain("gap=")
92
+ })
93
+
94
+ it("flags a ReactNode spacer with a TODO", () => {
95
+ const input = `
96
+ import { StackView } from "@planningcenter/tapestry-react"
97
+
98
+ export default function Test() {
99
+ return <StackView spacing={<Divider />}>Test</StackView>
100
+ }
101
+ `.trim()
102
+
103
+ const result = applyTransform(input)
104
+ expect(result).toContain("spacing={<Divider />}")
105
+ expect(result).toContain("TODO: tapestry-migration (spacing)")
106
+ expect(result).not.toContain("gap=")
107
+ })
108
+
109
+ it("flags an expression-wrapped string spacing value with a TODO", () => {
110
+ const input = `
111
+ import { StackView } from "@planningcenter/tapestry-react"
112
+
113
+ export default function Test() {
114
+ return <StackView spacing={"8px"}>Test</StackView>
115
+ }
116
+ `.trim()
117
+
118
+ const result = applyTransform(input)
119
+ expect(result).toContain('spacing={"8px"}')
120
+ expect(result).toContain("TODO: tapestry-migration (spacing)")
121
+ expect(result).not.toContain("gap=")
122
+ })
123
+
124
+ it("flags a dynamic spacing value with a TODO", () => {
125
+ const input = `
126
+ import { StackView } from "@planningcenter/tapestry-react"
127
+
128
+ export default function Test({ spacingProp }) {
129
+ return <StackView spacing={spacingProp}>Test</StackView>
130
+ }
131
+ `.trim()
132
+
133
+ const result = applyTransform(input)
134
+ expect(result).toContain("spacing={spacingProp}")
135
+ expect(result).toContain("TODO: tapestry-migration (spacing)")
136
+ expect(result).not.toContain("gap=")
137
+ })
138
+
139
+ it("respects an aliased StackView import", () => {
140
+ const input = `
141
+ import { StackView as Stack } from "@planningcenter/tapestry-react"
142
+
143
+ export default function Test() {
144
+ return <Stack spacing={2}>Test</Stack>
145
+ }
146
+ `.trim()
147
+
148
+ const result = applyTransform(input)
149
+ expect(result).toContain("gap={2}")
150
+ expect(result).not.toContain("spacing")
151
+ })
152
+
153
+ it("returns null when there is no spacing to migrate", () => {
154
+ const input = `
155
+ import { StackView } from "@planningcenter/tapestry-react"
156
+
157
+ export default function Test() {
158
+ return <StackView>Test</StackView>
159
+ }
160
+ `.trim()
161
+
162
+ expect(applyTransform(input)).toBe(null)
163
+ })
164
+
165
+ it("returns null when StackView is not imported from tapestry-react", () => {
166
+ const input = `
167
+ import { StackView } from "some-other-package"
168
+
169
+ export default function Test() {
170
+ return <StackView spacing={2}>Test</StackView>
171
+ }
172
+ `.trim()
173
+
174
+ expect(applyTransform(input)).toBe(null)
175
+ })
176
+ })
@@ -0,0 +1,49 @@
1
+ import { JSXAttribute, Transform } from "jscodeshift"
2
+
3
+ import { addComment } from "../../shared/actions/addComment"
4
+ import { getAttribute } from "../../shared/actions/getAttribute"
5
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
6
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
7
+
8
+ const VALID_GAP_VALUES = new Set([0, 0.25, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 7])
9
+
10
+ function resolveSpacingValue(attribute: JSXAttribute): number | null {
11
+ const value = attribute.value
12
+ if (
13
+ value?.type === "JSXExpressionContainer" &&
14
+ value.expression.type === "NumericLiteral" &&
15
+ VALID_GAP_VALUES.has(value.expression.value)
16
+ ) {
17
+ return value.expression.value
18
+ }
19
+ return null
20
+ }
21
+
22
+ const transform: Transform = attributeTransformFactory({
23
+ condition: hasAttribute("spacing"),
24
+ targetComponent: "StackView",
25
+ targetPackage: "@planningcenter/tapestry-react",
26
+ transform: (element, { j, source }) => {
27
+ const spacingAttribute = getAttribute({ element, name: "spacing" })
28
+ if (!spacingAttribute) return false
29
+
30
+ const spacingValue = resolveSpacingValue(spacingAttribute)
31
+
32
+ if (spacingValue === null) {
33
+ addComment({
34
+ element,
35
+ j,
36
+ scope: "spacing",
37
+ source,
38
+ text: "could not migrate spacing to gap automatically; use a Tapestry spacing scale number (0, 0.25, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 7) for gap, or for ReactNode spacers add explicit margin to the spacer element instead",
39
+ })
40
+ return true
41
+ }
42
+
43
+ spacingAttribute.name.name = "gap"
44
+
45
+ return true
46
+ },
47
+ })
48
+
49
+ export default transform
@@ -10,6 +10,7 @@ import mapChildrenToOptions from "./transforms/mapChildrenToOptions"
10
10
  import mergeFieldIntoSelect from "./transforms/mergeFieldIntoSelect"
11
11
  import mergeSelectLabel from "./transforms/mergeSelectLabel"
12
12
  import moveSelectImport from "./transforms/moveSelectImport"
13
+ import onChangeSignature from "./transforms/onChangeSignature"
13
14
  import removeDefaultProps from "./transforms/removeDefaultProps"
14
15
  import sizeMapping from "./transforms/sizeMapping"
15
16
  import skipMultipleSelect from "./transforms/skipMultipleSelect"
@@ -36,6 +37,7 @@ const transform: Transform = (fileInfo, api, options) => {
36
37
  stateToInvalid,
37
38
  removeDefaultProps,
38
39
  convertStyleProps,
40
+ onChangeSignature,
39
41
  unsupportedProps,
40
42
  moveSelectImport,
41
43
  ]