@planningcenter/tapestry-migration-cli 2.4.0 → 2.4.1-rc.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "2.4.0",
3
+ "version": "2.4.1-rc.0",
4
4
  "description": "CLI tool for Tapestry migrations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -51,5 +51,5 @@
51
51
  "publishConfig": {
52
52
  "access": "public"
53
53
  },
54
- "gitHead": "affade8550b5721f1e1b46c4e9bba0f134787bff"
54
+ "gitHead": "c643ce32f1e79ee51403daeaaeac4940729ab69d"
55
55
  }
@@ -0,0 +1,38 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import childrenToLabel from "./transforms/childrenToLabel"
4
+ import convertStyleProps from "./transforms/convertStyleProps"
5
+ import innerRefToRef from "./transforms/innerRefToRef"
6
+ import moveCheckboxImport from "./transforms/moveCheckboxImport"
7
+ import sizeMapping from "./transforms/sizeMapping"
8
+ import unsupportedProps from "./transforms/unsupportedProps"
9
+
10
+ const transform: Transform = (fileInfo, api, options) => {
11
+ let currentSource = fileInfo.source
12
+ let hasAnyChanges = false
13
+
14
+ const transforms = [
15
+ childrenToLabel,
16
+ sizeMapping,
17
+ innerRefToRef,
18
+ convertStyleProps,
19
+ unsupportedProps,
20
+ moveCheckboxImport,
21
+ ]
22
+
23
+ for (const individualTransform of transforms) {
24
+ const result = individualTransform(
25
+ { ...fileInfo, source: currentSource },
26
+ api,
27
+ options
28
+ )
29
+ if (result && result !== currentSource) {
30
+ currentSource = result as string
31
+ hasAnyChanges = true
32
+ }
33
+ }
34
+
35
+ return hasAnyChanges ? currentSource : null
36
+ }
37
+
38
+ export default transform
@@ -0,0 +1,146 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./childrenToLabel"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ const result = transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ {}
14
+ ) as string | null
15
+ return result || source
16
+ }
17
+
18
+ describe("childrenToLabel transform for Checkbox", () => {
19
+ describe("simple text conversion", () => {
20
+ it("should convert simple text children to label prop", () => {
21
+ const input = `
22
+ import { Checkbox } from "@planningcenter/tapestry-react"
23
+
24
+ function Test() {
25
+ return <Checkbox>Accept terms</Checkbox>
26
+ }
27
+ `.trim()
28
+
29
+ const result = applyTransform(input)
30
+ expect(result).toContain('<Checkbox label="Accept terms" />')
31
+ expect(result).not.toContain("Accept terms</Checkbox>")
32
+ })
33
+
34
+ it("should not convert when label prop already exists", () => {
35
+ const input = `
36
+ import { Checkbox } from "@planningcenter/tapestry-react"
37
+
38
+ function Test() {
39
+ return <Checkbox label="Already has label">Accept terms</Checkbox>
40
+ }
41
+ `.trim()
42
+
43
+ const result = applyTransform(input)
44
+ expect(result).toContain('label="Already has label"')
45
+ expect(result).toContain("Accept terms</Checkbox>")
46
+ })
47
+
48
+ it("should handle whitespace in text content", () => {
49
+ const input = `
50
+ import { Checkbox } from "@planningcenter/tapestry-react"
51
+
52
+ function Test() {
53
+ return <Checkbox> Accept terms </Checkbox>
54
+ }
55
+ `.trim()
56
+
57
+ const result = applyTransform(input)
58
+ expect(result).toContain('<Checkbox label="Accept terms" />')
59
+ })
60
+
61
+ it("should not convert complex children", () => {
62
+ const input = `
63
+ import { Checkbox } from "@planningcenter/tapestry-react"
64
+
65
+ function Test() {
66
+ return (
67
+ <Checkbox>
68
+ <span>Accept</span> terms
69
+ </Checkbox>
70
+ )
71
+ }
72
+ `.trim()
73
+
74
+ const result = applyTransform(input)
75
+ expect(result).not.toContain("label=")
76
+ expect(result).toContain("<span>Accept</span>terms")
77
+ expect(result).toContain(
78
+ "TODO: tapestry-migration (children): complex children cannot be converted to label prop"
79
+ )
80
+ })
81
+
82
+ it("should not convert when children contain JSX elements", () => {
83
+ const input = `
84
+ import { Checkbox } from "@planningcenter/tapestry-react"
85
+
86
+ function Test() {
87
+ return (
88
+ <Checkbox>
89
+ Accept <strong>terms</strong>
90
+ </Checkbox>
91
+ )
92
+ }
93
+ `.trim()
94
+
95
+ const result = applyTransform(input)
96
+ expect(result).not.toContain("label=")
97
+ expect(result).toContain("Accept <strong>terms</strong>")
98
+ expect(result).toContain(
99
+ "TODO: tapestry-migration (children): complex children cannot be converted to label prop"
100
+ )
101
+ })
102
+ })
103
+
104
+ describe("edge cases", () => {
105
+ it("should handle empty children", () => {
106
+ const input = `
107
+ import { Checkbox } from "@planningcenter/tapestry-react"
108
+
109
+ function Test() {
110
+ return <Checkbox></Checkbox>
111
+ }
112
+ `.trim()
113
+
114
+ const result = applyTransform(input)
115
+ expect(result).toContain("<Checkbox></Checkbox>")
116
+ })
117
+
118
+ it("should handle self-closing checkbox", () => {
119
+ const input = `
120
+ import { Checkbox } from "@planningcenter/tapestry-react"
121
+
122
+ function Test() {
123
+ return <Checkbox />
124
+ }
125
+ `.trim()
126
+
127
+ const result = applyTransform(input)
128
+ expect(result).toContain("<Checkbox />")
129
+ })
130
+
131
+ it("should handle checkbox with other props", () => {
132
+ const input = `
133
+ import { Checkbox } from "@planningcenter/tapestry-react"
134
+
135
+ function Test() {
136
+ return <Checkbox checked disabled>Accept terms</Checkbox>
137
+ }
138
+ `.trim()
139
+
140
+ const result = applyTransform(input)
141
+ expect(result).toContain(
142
+ '<Checkbox checked disabled label="Accept terms" />'
143
+ )
144
+ })
145
+ })
146
+ })
@@ -0,0 +1,54 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { addAttribute } from "../../shared/actions/addAttribute"
4
+ import { addComment } from "../../shared/actions/addComment"
5
+ import { removeChildren } from "../../shared/actions/removeChildren"
6
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
7
+ import { hasChildren } from "../../shared/conditions/hasChildren"
8
+ import {
9
+ buildComment,
10
+ extractTextContent,
11
+ } from "../../shared/helpers/childrenToLabelHelpers"
12
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
13
+
14
+ const transform: Transform = attributeTransformFactory({
15
+ condition: hasChildren,
16
+ targetComponent: "Checkbox",
17
+ targetPackage: "@planningcenter/tapestry-react",
18
+ transform: (element, { j, source }) => {
19
+ if (hasAttribute("label")(element)) {
20
+ addComment({
21
+ element,
22
+ j,
23
+ scope: "label",
24
+ source,
25
+ text: buildComment("Checkbox has both label prop and children", false),
26
+ })
27
+ return true
28
+ }
29
+
30
+ const { isSimpleText, textContent } = extractTextContent(element.children!)
31
+
32
+ if (isSimpleText && textContent) {
33
+ addAttribute({ element, j, name: "label", value: textContent })
34
+ removeChildren(element)
35
+ return true
36
+ } else if (!isSimpleText) {
37
+ addComment({
38
+ element,
39
+ j,
40
+ scope: "children",
41
+ source,
42
+ text: buildComment(
43
+ "complex children cannot be converted to label prop",
44
+ true
45
+ ),
46
+ })
47
+ return true
48
+ }
49
+
50
+ return false
51
+ },
52
+ })
53
+
54
+ export default transform
@@ -0,0 +1,161 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./convertStyleProps"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string, options = {}) {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ const result = transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ options
14
+ ) as string | null
15
+ return result || source
16
+ }
17
+
18
+ describe("convertStyleProps transform", () => {
19
+ describe("visible prop - handled by theme system", () => {
20
+ it("should convert visible={false} to display: none", () => {
21
+ const source = `
22
+ import { Checkbox } from "@planningcenter/tapestry-react"
23
+
24
+ export function TestComponent() {
25
+ return <Checkbox visible={false} label="Hidden Checkbox" />
26
+ }
27
+ `
28
+
29
+ const result = applyTransform(source)
30
+
31
+ expect(result).toContain("style={{")
32
+ expect(result).toContain('display: "none"')
33
+ expect(result).not.toContain("visible={false}")
34
+ })
35
+
36
+ it("should remove visible={true} with no style changes (true is default)", () => {
37
+ const source = `
38
+ import { Checkbox } from "@planningcenter/tapestry-react"
39
+
40
+ export function TestComponent() {
41
+ return <Checkbox visible={true} label="Visible Checkbox" />
42
+ }
43
+ `
44
+
45
+ const result = applyTransform(source)
46
+
47
+ // visible={true} is default behavior, so it should be removed with no style added
48
+ expect(result).not.toContain("visible={true}")
49
+ expect(result).not.toContain("style={{") // No style should be added
50
+ expect(result).toContain('<Checkbox label="Visible Checkbox" />')
51
+ })
52
+
53
+ it("should handle visible expressions", () => {
54
+ const source = `
55
+ import { Checkbox } from "@planningcenter/tapestry-react"
56
+
57
+ export function TestComponent() {
58
+ const isVisible = true
59
+ return <Checkbox visible={isVisible} label="Variable Checkbox" />
60
+ }
61
+ `
62
+
63
+ const result = applyTransform(source)
64
+
65
+ // Note: visible expressions are handled by handleVisibleExpressions transform,
66
+ // not convertStyleProps, so the visible prop should remain
67
+ expect(result).toContain("visible: isVisible")
68
+ expect(result).toContain("style={{") // Style should be added for expressions
69
+ })
70
+ })
71
+
72
+ describe("style prop removal", () => {
73
+ it("should remove alignItems prop", () => {
74
+ const source = `
75
+ import { Checkbox } from "@planningcenter/tapestry-react"
76
+
77
+ export function TestComponent() {
78
+ return <Checkbox alignItems="center" label="Test" />
79
+ }
80
+ `
81
+
82
+ const result = applyTransform(source)
83
+
84
+ expect(result).not.toContain("alignItems=")
85
+ })
86
+
87
+ it("should remove marginTop prop", () => {
88
+ const source = `
89
+ import { Checkbox } from "@planningcenter/tapestry-react"
90
+
91
+ export function TestComponent() {
92
+ return <Checkbox marginTop={16} label="Test" />
93
+ }
94
+ `
95
+
96
+ const result = applyTransform(source)
97
+
98
+ expect(result).not.toContain("marginTop=")
99
+ })
100
+
101
+ it("should remove multiple style props", () => {
102
+ const source = `
103
+ import { Checkbox } from "@planningcenter/tapestry-react"
104
+
105
+ export function TestComponent() {
106
+ return <Checkbox alignItems="center" marginTop={16} label="Test" />
107
+ }
108
+ `
109
+
110
+ const result = applyTransform(source)
111
+
112
+ expect(result).not.toContain("alignItems=")
113
+ expect(result).not.toContain("marginTop=")
114
+ })
115
+ })
116
+
117
+ describe("combination of style props", () => {
118
+ it("should handle combination of visible and style props", () => {
119
+ const source = `
120
+ import { Checkbox } from "@planningcenter/tapestry-react"
121
+
122
+ export function TestComponent() {
123
+ return (
124
+ <Checkbox
125
+ visible={false}
126
+ alignItems="center"
127
+ marginTop={16}
128
+ label="Combined Checkbox"
129
+ />
130
+ )
131
+ }
132
+ `
133
+
134
+ const result = applyTransform(source)
135
+
136
+ expect(result).toContain("style={{")
137
+ expect(result).toContain('display: "none"') // from visible={false}
138
+ expect(result).not.toContain("visible={false}")
139
+ expect(result).not.toContain("alignItems=")
140
+ expect(result).not.toContain("marginTop=")
141
+ })
142
+ })
143
+
144
+ describe("import handling", () => {
145
+ it("should not affect imports", () => {
146
+ const source = `
147
+ import { Checkbox } from "@planningcenter/tapestry-react"
148
+
149
+ export function TestComponent() {
150
+ return <Checkbox visible={false} label="Test" />
151
+ }
152
+ `
153
+
154
+ const result = applyTransform(source)
155
+
156
+ expect(result).toContain(
157
+ 'import { Checkbox } from "@planningcenter/tapestry-react"'
158
+ )
159
+ })
160
+ })
161
+ })
@@ -0,0 +1,10 @@
1
+ import { stackViewPlugin } from "../../../stubs/stackViewPlugin"
2
+ import { stylePropTransformFactory } from "../../shared/transformFactories/stylePropTransformFactory"
3
+
4
+ export default stylePropTransformFactory({
5
+ plugin: stackViewPlugin,
6
+ stylesToKeep: ["visible"],
7
+ stylesToRemove: [],
8
+ targetComponent: "Checkbox",
9
+ targetPackage: "@planningcenter/tapestry-react",
10
+ })
@@ -0,0 +1,161 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./innerRefToRef"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ const result = transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ {}
14
+ ) as string | null
15
+ return result || source
16
+ }
17
+
18
+ describe("innerRefToRef transform", () => {
19
+ describe("innerRef to ref conversion", () => {
20
+ it("should convert innerRef to ref", () => {
21
+ const input = `
22
+ import { Checkbox } from "@planningcenter/tapestry-react"
23
+
24
+ function Test() {
25
+ const checkboxRef = React.useRef()
26
+ return <Checkbox innerRef={checkboxRef} label="Test" />
27
+ }
28
+ `.trim()
29
+
30
+ const result = applyTransform(input)
31
+ expect(result).toContain("ref={checkboxRef}")
32
+ expect(result).not.toContain("innerRef=")
33
+ })
34
+
35
+ it("should convert innerRef with expression", () => {
36
+ const input = `
37
+ import { Checkbox } from "@planningcenter/tapestry-react"
38
+
39
+ function Test() {
40
+ return <Checkbox innerRef={myRef} label="Test" />
41
+ }
42
+ `.trim()
43
+
44
+ const result = applyTransform(input)
45
+ expect(result).toContain("ref={myRef}")
46
+ expect(result).not.toContain("innerRef=")
47
+ })
48
+
49
+ it("should convert innerRef with callback", () => {
50
+ const input = `
51
+ import { Checkbox } from "@planningcenter/tapestry-react"
52
+
53
+ function Test() {
54
+ return <Checkbox innerRef={(el) => console.log(el)} label="Test" />
55
+ }
56
+ `.trim()
57
+
58
+ const result = applyTransform(input)
59
+ expect(result).toContain("ref={(el) => console.log(el)}")
60
+ expect(result).not.toContain("innerRef=")
61
+ })
62
+
63
+ it("should not affect existing ref props", () => {
64
+ const input = `
65
+ import { Checkbox } from "@planningcenter/tapestry-react"
66
+
67
+ function Test() {
68
+ const checkboxRef = React.useRef()
69
+ return <Checkbox ref={checkboxRef} label="Test" />
70
+ }
71
+ `.trim()
72
+
73
+ const result = applyTransform(input)
74
+ expect(result).toContain("ref={checkboxRef}")
75
+ expect(result).not.toContain("innerRef=")
76
+ })
77
+
78
+ it("should handle multiple checkboxes", () => {
79
+ const input = `
80
+ import { Checkbox } from "@planningcenter/tapestry-react"
81
+
82
+ function Test() {
83
+ const ref1 = React.useRef()
84
+ const ref2 = React.useRef()
85
+ return (
86
+ <div>
87
+ <Checkbox innerRef={ref1} label="First" />
88
+ <Checkbox innerRef={ref2} label="Second" />
89
+ </div>
90
+ )
91
+ }
92
+ `.trim()
93
+
94
+ const result = applyTransform(input)
95
+ expect(result).toContain("ref={ref1}")
96
+ expect(result).toContain("ref={ref2}")
97
+ expect(result).not.toContain("innerRef=")
98
+ })
99
+ })
100
+
101
+ describe("edge cases", () => {
102
+ it("should not affect other components", () => {
103
+ const input = `
104
+ import { Button, Checkbox } from "@planningcenter/tapestry-react"
105
+
106
+ function Test() {
107
+ const buttonRef = React.useRef()
108
+ const checkboxRef = React.useRef()
109
+ return (
110
+ <div>
111
+ <Button innerRef={buttonRef}>Click me</Button>
112
+ <Checkbox innerRef={checkboxRef} label="Test" />
113
+ </div>
114
+ )
115
+ }
116
+ `.trim()
117
+
118
+ const result = applyTransform(input)
119
+ expect(result).toContain("<Button innerRef={buttonRef}>Click me</Button>")
120
+ expect(result).toContain("ref={checkboxRef}")
121
+ })
122
+
123
+ it("should handle checkbox without innerRef", () => {
124
+ const input = `
125
+ import { Checkbox } from "@planningcenter/tapestry-react"
126
+
127
+ function Test() {
128
+ return <Checkbox label="Test" />
129
+ }
130
+ `.trim()
131
+
132
+ const result = applyTransform(input)
133
+ expect(result).toContain('<Checkbox label="Test" />')
134
+ })
135
+
136
+ it("should preserve other props", () => {
137
+ const input = `
138
+ import { Checkbox } from "@planningcenter/tapestry-react"
139
+
140
+ function Test() {
141
+ const checkboxRef = React.useRef()
142
+ return (
143
+ <Checkbox
144
+ innerRef={checkboxRef}
145
+ label="Test"
146
+ checked
147
+ disabled
148
+ />
149
+ )
150
+ }
151
+ `.trim()
152
+
153
+ const result = applyTransform(input)
154
+ expect(result).toContain("ref={checkboxRef}")
155
+ expect(result).toContain('label="Test"')
156
+ expect(result).toContain("checked")
157
+ expect(result).toContain("disabled")
158
+ expect(result).not.toContain("innerRef=")
159
+ })
160
+ })
161
+ })
@@ -0,0 +1,14 @@
1
+ import { transformAttributeName } from "../../shared/actions/transformAttributeName"
2
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
3
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
4
+
5
+ const transform = attributeTransformFactory({
6
+ condition: hasAttribute("innerRef"),
7
+ targetComponent: "Checkbox",
8
+ targetPackage: "@planningcenter/tapestry-react",
9
+ transform: (element) => {
10
+ return transformAttributeName("innerRef", "ref", { element })
11
+ },
12
+ })
13
+
14
+ export default transform