@planningcenter/tapestry-migration-cli 3.2.2-rc.16 → 3.2.2-rc.17

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 +3 -3
  2. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +24 -0
  3. package/src/components/shared/transformFactories/stylePropTransformFactory.test.ts +36 -0
  4. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +43 -10
  5. package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +17 -12
  6. package/src/components/time-field/index.ts +48 -0
  7. package/src/components/time-field/transforms/auditSpreadProps.test.ts +76 -0
  8. package/src/components/time-field/transforms/auditSpreadProps.ts +10 -0
  9. package/src/components/time-field/transforms/convertStyleProps.test.ts +43 -0
  10. package/src/components/time-field/transforms/convertStyleProps.ts +10 -0
  11. package/src/components/time-field/transforms/flagMinMax.test.ts +103 -0
  12. package/src/components/time-field/transforms/flagMinMax.ts +31 -0
  13. package/src/components/time-field/transforms/mergeFieldIntoTimeField.test.ts +106 -0
  14. package/src/components/time-field/transforms/mergeFieldIntoTimeField.ts +5 -0
  15. package/src/components/time-field/transforms/moveTimeFieldImport.test.ts +153 -0
  16. package/src/components/time-field/transforms/moveTimeFieldImport.ts +14 -0
  17. package/src/components/time-field/transforms/sizeMapping.test.ts +173 -0
  18. package/src/components/time-field/transforms/sizeMapping.ts +15 -0
  19. package/src/components/time-field/transforms/stateToInvalid.test.ts +87 -0
  20. package/src/components/time-field/transforms/stateToInvalid.ts +56 -0
  21. package/src/components/time-field/transforms/stateToInvalidTernary.test.ts +100 -0
  22. package/src/components/time-field/transforms/stateToInvalidTernary.ts +11 -0
  23. package/src/components/time-field/transforms/tupleToTime.test.ts +182 -0
  24. package/src/components/time-field/transforms/tupleToTime.ts +107 -0
  25. package/src/components/time-field/transforms/twelveHourClockToHourCycle.test.ts +117 -0
  26. package/src/components/time-field/transforms/twelveHourClockToHourCycle.ts +65 -0
  27. package/src/components/time-field/transforms/unsupportedProps.test.ts +160 -0
  28. package/src/components/time-field/transforms/unsupportedProps.ts +37 -0
  29. package/src/index.ts +2 -1
@@ -0,0 +1,153 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./moveTimeFieldImport"
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("moveTimeFieldImport transform", () => {
19
+ describe("import migration", () => {
20
+ it("should change import package and keep TimeField name", () => {
21
+ const input = `
22
+ import { TimeField } from "@planningcenter/tapestry-react"
23
+
24
+ function Test() {
25
+ return <TimeField value={time} onChange={setTime} />
26
+ }
27
+ `.trim()
28
+
29
+ const result = applyTransform(input)
30
+ expect(result).toContain(
31
+ 'import { TimeField } from "@planningcenter/tapestry"'
32
+ )
33
+ expect(result).not.toContain("@planningcenter/tapestry-react")
34
+ expect(result).toContain("<TimeField")
35
+ })
36
+
37
+ it("should only move TimeField, leaving other tapestry-react imports", () => {
38
+ const input = `
39
+ import { Button } from "@planningcenter/tapestry-react"
40
+ import { TimeField } from "@planningcenter/tapestry-react"
41
+
42
+ function Test() {
43
+ return (
44
+ <div>
45
+ <Button>Click</Button>
46
+ <TimeField value={time} onChange={setTime} />
47
+ </div>
48
+ )
49
+ }
50
+ `.trim()
51
+
52
+ const result = applyTransform(input)
53
+ expect(result).toContain(
54
+ 'import { Button } from "@planningcenter/tapestry-react"'
55
+ )
56
+ expect(result).toContain(
57
+ 'import { TimeField } from "@planningcenter/tapestry"'
58
+ )
59
+ expect(result).toContain("<TimeField")
60
+ })
61
+ })
62
+
63
+ describe("import conflict handling", () => {
64
+ it("should use TdsTimeField alias when TimeField is already imported elsewhere", () => {
65
+ const input = `
66
+ import { TimeField } from "some-other-library"
67
+ import { TimeField as LegacyTimeField } from "@planningcenter/tapestry-react"
68
+
69
+ function Test() {
70
+ return (
71
+ <div>
72
+ <TimeField />
73
+ <LegacyTimeField value={time} onChange={setTime} />
74
+ </div>
75
+ )
76
+ }
77
+ `.trim()
78
+
79
+ const result = applyTransform(input)
80
+ expect(result).toContain('import { TimeField } from "some-other-library"')
81
+ expect(result).toContain(
82
+ 'import { TimeField as TdsTimeField } from "@planningcenter/tapestry"'
83
+ )
84
+ expect(result).toContain("<TdsTimeField")
85
+ })
86
+ })
87
+
88
+ describe("edge cases", () => {
89
+ it("should not affect other components", () => {
90
+ const input = `
91
+ import { Button } from "@planningcenter/tapestry-react"
92
+
93
+ function Test() {
94
+ return <Button>Click me</Button>
95
+ }
96
+ `.trim()
97
+
98
+ const result = applyTransform(input)
99
+ expect(result).toContain(
100
+ 'import { Button } from "@planningcenter/tapestry-react"'
101
+ )
102
+ })
103
+
104
+ it("should handle no imports", () => {
105
+ const input = `
106
+ function Test() {
107
+ return <div>No imports</div>
108
+ }
109
+ `.trim()
110
+
111
+ const result = applyTransform(input)
112
+ expect(result).toBe(input)
113
+ })
114
+
115
+ it("should preserve all attributes", () => {
116
+ const input = `
117
+ import { TimeField } from "@planningcenter/tapestry-react"
118
+
119
+ function Test() {
120
+ return (
121
+ <TimeField
122
+ value={time}
123
+ onChange={setTime}
124
+ disabled
125
+ />
126
+ )
127
+ }
128
+ `.trim()
129
+
130
+ const result = applyTransform(input)
131
+ expect(result).toContain(
132
+ 'import { TimeField } from "@planningcenter/tapestry"'
133
+ )
134
+ expect(result).toContain("value={time}")
135
+ expect(result).toContain("onChange={setTime}")
136
+ expect(result).toContain("disabled")
137
+ })
138
+
139
+ it("should handle alias import", () => {
140
+ const input = `
141
+ import { TimeField as TapestryTimeField } from "@planningcenter/tapestry-react"
142
+
143
+ function Test() {
144
+ return <TapestryTimeField value={time} onChange={setTime} />
145
+ }
146
+ `.trim()
147
+
148
+ const result = applyTransform(input)
149
+ expect(result).toContain("@planningcenter/tapestry")
150
+ expect(result).not.toContain("@planningcenter/tapestry-react")
151
+ })
152
+ })
153
+ })
@@ -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: "TdsTimeField",
8
+ fromComponent: "TimeField",
9
+ fromPackage: "@planningcenter/tapestry-react",
10
+ toComponent: "TimeField",
11
+ toPackage: "@planningcenter/tapestry",
12
+ })
13
+
14
+ export default transform
@@ -0,0 +1,173 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./sizeMapping"
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("sizeMapping transform", () => {
19
+ describe("size value transformations", () => {
20
+ it("should transform xs to md", () => {
21
+ const input = `
22
+ import { TimeField } from "@planningcenter/tapestry-react"
23
+
24
+ function Test() {
25
+ return <TimeField size="xs" value={time} onChange={setTime} />
26
+ }
27
+ `.trim()
28
+
29
+ const result = applyTransform(input)
30
+ expect(result).toContain('size="md"')
31
+ expect(result).not.toContain('size="xs"')
32
+ expect(result).toContain('Size "xs" was mapped to "md"')
33
+ })
34
+
35
+ it("should transform sm to md", () => {
36
+ const input = `
37
+ import { TimeField } from "@planningcenter/tapestry-react"
38
+
39
+ function Test() {
40
+ return <TimeField size="sm" value={time} onChange={setTime} />
41
+ }
42
+ `.trim()
43
+
44
+ const result = applyTransform(input)
45
+ expect(result).toContain('size="md"')
46
+ expect(result).not.toContain('size="sm"')
47
+ expect(result).toContain('Size "sm" was mapped to "md"')
48
+ })
49
+
50
+ it("should transform xl to lg", () => {
51
+ const input = `
52
+ import { TimeField } from "@planningcenter/tapestry-react"
53
+
54
+ function Test() {
55
+ return <TimeField size="xl" value={time} onChange={setTime} />
56
+ }
57
+ `.trim()
58
+
59
+ const result = applyTransform(input)
60
+ expect(result).toContain('size="lg"')
61
+ expect(result).not.toContain('size="xl"')
62
+ expect(result).toContain('Size "xl" was mapped to "lg"')
63
+ })
64
+
65
+ it('should transform size={"sm"} expression container', () => {
66
+ const input = `
67
+ import { TimeField } from "@planningcenter/tapestry-react"
68
+
69
+ function Test() {
70
+ return <TimeField size={"sm"} value={time} onChange={setTime} />
71
+ }
72
+ `.trim()
73
+
74
+ const result = applyTransform(input)
75
+ expect(result).toContain('size="md"')
76
+ expect(result).toContain('Size "sm" was mapped to "md"')
77
+ })
78
+
79
+ it("should not transform already valid sizes", () => {
80
+ const input = `
81
+ import { TimeField } from "@planningcenter/tapestry-react"
82
+
83
+ function Test() {
84
+ return (
85
+ <div>
86
+ <TimeField size="md" value={time1} onChange={setTime1} />
87
+ <TimeField size="lg" value={time2} onChange={setTime2} />
88
+ </div>
89
+ )
90
+ }
91
+ `.trim()
92
+
93
+ const result = applyTransform(input)
94
+ expect(result).toContain('size="md"')
95
+ expect(result).toContain('size="lg"')
96
+ expect(result).not.toContain("TODO: tapestry-migration")
97
+ })
98
+ })
99
+
100
+ describe("edge cases", () => {
101
+ it("should not affect TimeField without size prop", () => {
102
+ const input = `
103
+ import { TimeField } from "@planningcenter/tapestry-react"
104
+
105
+ function Test() {
106
+ return <TimeField value={time} onChange={setTime} />
107
+ }
108
+ `.trim()
109
+
110
+ const result = applyTransform(input)
111
+ expect(result).toBe(input)
112
+ })
113
+
114
+ it("should not transform expression values (variables)", () => {
115
+ const input = `
116
+ import { TimeField } from "@planningcenter/tapestry-react"
117
+
118
+ function Test() {
119
+ const size = "sm"
120
+ return <TimeField size={size} value={time} onChange={setTime} />
121
+ }
122
+ `.trim()
123
+
124
+ const result = applyTransform(input)
125
+ expect(result).toContain("size={size}")
126
+ expect(result).not.toContain("Size")
127
+ })
128
+
129
+ it("should not affect other components", () => {
130
+ const input = `
131
+ import { TimeField } from "@planningcenter/tapestry-react"
132
+ import { Input } from "@planningcenter/tapestry-react"
133
+
134
+ function Test() {
135
+ return (
136
+ <div>
137
+ <Input size="sm" label="Name" />
138
+ <TimeField size="md" value={time} onChange={setTime} />
139
+ </div>
140
+ )
141
+ }
142
+ `.trim()
143
+
144
+ const result = applyTransform(input)
145
+ expect(result).toContain('<Input size="sm"')
146
+ expect(result).toContain('size="md"')
147
+ })
148
+
149
+ it("should handle multiple TimeFields with different sizes", () => {
150
+ const input = `
151
+ import { TimeField } from "@planningcenter/tapestry-react"
152
+
153
+ function Test() {
154
+ return (
155
+ <div>
156
+ <TimeField size="xs" value={time1} onChange={setTime1} />
157
+ <TimeField size="sm" value={time2} onChange={setTime2} />
158
+ <TimeField size="md" value={time3} onChange={setTime3} />
159
+ <TimeField size="xl" value={time4} onChange={setTime4} />
160
+ </div>
161
+ )
162
+ }
163
+ `.trim()
164
+
165
+ const result = applyTransform(input)
166
+ expect(result).not.toContain('size="xs"')
167
+ expect(result).not.toContain('size="sm"')
168
+ expect(result).not.toContain('size="xl"')
169
+ const sizeMappingMatches = result.match(/Size ".*" was mapped to/g)
170
+ expect(sizeMappingMatches).toHaveLength(3)
171
+ })
172
+ })
173
+ })
@@ -0,0 +1,15 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { sizeMappingFactory } from "../../shared/transformFactories/sizeMappingFactory"
4
+
5
+ const transform: Transform = sizeMappingFactory({
6
+ sizeMapping: {
7
+ sm: "md",
8
+ xl: "lg",
9
+ xs: "md",
10
+ },
11
+ targetComponent: "TimeField",
12
+ targetPackage: "@planningcenter/tapestry-react",
13
+ })
14
+
15
+ export default transform
@@ -0,0 +1,87 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./stateToInvalid"
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("stateToInvalid transform", () => {
19
+ it("converts state='error' to invalid", () => {
20
+ const input = `
21
+ import { TimeField } from "@planningcenter/tapestry-react"
22
+
23
+ function Test() {
24
+ return <TimeField state="error" value={time} onChange={setTime} />
25
+ }
26
+ `.trim()
27
+
28
+ const result = applyTransform(input)
29
+ expect(result).toContain("invalid")
30
+ expect(result).not.toContain('state="error"')
31
+ })
32
+
33
+ it("removes state='success' and adds TODO", () => {
34
+ const input = `
35
+ import { TimeField } from "@planningcenter/tapestry-react"
36
+
37
+ function Test() {
38
+ return <TimeField state="success" value={time} onChange={setTime} />
39
+ }
40
+ `.trim()
41
+
42
+ const result = applyTransform(input)
43
+ expect(result).not.toContain('<TimeField state="success"')
44
+ expect(result).toContain("TODO: tapestry-migration")
45
+ expect(result).toContain("no equivalent")
46
+ })
47
+
48
+ it("adds TODO comment for dynamic state value", () => {
49
+ const input = `
50
+ import { TimeField } from "@planningcenter/tapestry-react"
51
+
52
+ function Test() {
53
+ return <TimeField state={fieldState} value={time} onChange={setTime} />
54
+ }
55
+ `.trim()
56
+
57
+ const result = applyTransform(input)
58
+ expect(result).toContain("TODO: tapestry-migration")
59
+ expect(result).toContain("Migrate this prop manually")
60
+ })
61
+
62
+ it("returns null when state prop is absent", () => {
63
+ const input = `
64
+ import { TimeField } from "@planningcenter/tapestry-react"
65
+
66
+ function Test() {
67
+ return <TimeField value={time} onChange={setTime} />
68
+ }
69
+ `.trim()
70
+
71
+ const result = applyTransform(input)
72
+ expect(result).toBe(input)
73
+ })
74
+
75
+ it("does not affect other components", () => {
76
+ const input = `
77
+ import { Button } from "@planningcenter/tapestry-react"
78
+
79
+ function Test() {
80
+ return <Button state="error">Click</Button>
81
+ }
82
+ `.trim()
83
+
84
+ const result = applyTransform(input)
85
+ expect(result).toBe(input)
86
+ })
87
+ })
@@ -0,0 +1,56 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { addAttribute } from "../../shared/actions/addAttribute"
4
+ import { addComment } from "../../shared/actions/addComment"
5
+ import { getAttribute } from "../../shared/actions/getAttribute"
6
+ import { getAttributeValue } from "../../shared/actions/getAttributeValue"
7
+ import { removeAttribute } from "../../shared/actions/removeAttribute"
8
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
9
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
10
+
11
+ const transform: Transform = attributeTransformFactory({
12
+ condition: hasAttribute("state"),
13
+ targetComponent: "TimeField",
14
+ targetPackage: "@planningcenter/tapestry-react",
15
+ transform: (element, { j, source }) => {
16
+ const attr = getAttribute({ element, name: "state" })
17
+ if (!attr) return false
18
+
19
+ const value = getAttributeValue({ attribute: attr, j })
20
+
21
+ if (value === "error") {
22
+ removeAttribute("state", { element, j, source })
23
+ addAttribute({
24
+ booleanAsShorthand: true,
25
+ element,
26
+ j,
27
+ name: "invalid",
28
+ value: true,
29
+ })
30
+ return true
31
+ }
32
+
33
+ if (value === "success") {
34
+ removeAttribute("state", { element, j, source })
35
+ addComment({
36
+ element,
37
+ j,
38
+ scope: "state",
39
+ source,
40
+ text: "'state=\"success\"' has no equivalent in the new TimeField API. Remove this prop and implement success feedback separately.",
41
+ })
42
+ return true
43
+ }
44
+
45
+ addComment({
46
+ element,
47
+ j,
48
+ scope: "state",
49
+ source,
50
+ text: "'state' has been replaced by 'invalid' (boolean). For error state, use invalid={true}. 'success' has no equivalent. Migrate this prop manually.",
51
+ })
52
+ return true
53
+ },
54
+ })
55
+
56
+ export default transform
@@ -0,0 +1,100 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./stateToInvalidTernary"
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("stateToInvalidTernary transform", () => {
19
+ it("converts state={x ? 'error' : undefined} to invalid={x}", () => {
20
+ const input = `
21
+ import { TimeField } from "@planningcenter/tapestry-react"
22
+
23
+ function Test() {
24
+ return <TimeField state={hasError ? "error" : undefined} value={time} onChange={setTime} />
25
+ }
26
+ `.trim()
27
+
28
+ const result = applyTransform(input)
29
+ expect(result).toContain("invalid={hasError}")
30
+ expect(result).not.toContain("state=")
31
+ })
32
+
33
+ it("converts state={x ? 'error' : null} to invalid={x}", () => {
34
+ const input = `
35
+ import { TimeField } from "@planningcenter/tapestry-react"
36
+
37
+ function Test() {
38
+ return <TimeField state={hasError ? "error" : null} value={time} onChange={setTime} />
39
+ }
40
+ `.trim()
41
+
42
+ const result = applyTransform(input)
43
+ expect(result).toContain("invalid={hasError}")
44
+ expect(result).not.toContain("state=")
45
+ })
46
+
47
+ it("converts state={x ? undefined : 'error'} to invalid={!x}", () => {
48
+ const input = `
49
+ import { TimeField } from "@planningcenter/tapestry-react"
50
+
51
+ function Test() {
52
+ return <TimeField state={validateServiceTime(serviceTime) ? undefined : "error"} value={time} onChange={setTime} />
53
+ }
54
+ `.trim()
55
+
56
+ const result = applyTransform(input)
57
+ expect(result).toContain("invalid={!validateServiceTime(serviceTime)}")
58
+ expect(result).not.toContain("state=")
59
+ })
60
+
61
+ it("converts state={x ? null : 'error'} to invalid={!x}", () => {
62
+ const input = `
63
+ import { TimeField } from "@planningcenter/tapestry-react"
64
+
65
+ function Test() {
66
+ return <TimeField state={isValid ? null : "error"} value={time} onChange={setTime} />
67
+ }
68
+ `.trim()
69
+
70
+ const result = applyTransform(input)
71
+ expect(result).toContain("invalid={!isValid}")
72
+ expect(result).not.toContain("state=")
73
+ })
74
+
75
+ it("returns input unchanged when state prop is absent", () => {
76
+ const input = `
77
+ import { TimeField } from "@planningcenter/tapestry-react"
78
+
79
+ function Test() {
80
+ return <TimeField value={time} onChange={setTime} />
81
+ }
82
+ `.trim()
83
+
84
+ const result = applyTransform(input)
85
+ expect(result).toBe(input)
86
+ })
87
+
88
+ it("does not affect other components", () => {
89
+ const input = `
90
+ import { Button } from "@planningcenter/tapestry-react"
91
+
92
+ function Test() {
93
+ return <Button state={hasError ? "error" : undefined}>Click</Button>
94
+ }
95
+ `.trim()
96
+
97
+ const result = applyTransform(input)
98
+ expect(result).toBe(input)
99
+ })
100
+ })
@@ -0,0 +1,11 @@
1
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
2
+ import { ternaryConditionalToPropFactory } from "../../shared/transformFactories/ternaryConditionalToPropFactory"
3
+
4
+ export default ternaryConditionalToPropFactory({
5
+ condition: hasAttribute("state"),
6
+ fromProp: "state",
7
+ matchValue: "error",
8
+ targetComponent: "TimeField",
9
+ targetPackage: "@planningcenter/tapestry-react",
10
+ toProp: "invalid",
11
+ })