@planningcenter/tapestry-migration-cli 3.2.3-rc.3 → 3.2.3-rc.5

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/package.json +3 -3
  2. package/src/components/date-picker/index.ts +56 -0
  3. package/src/components/date-picker/transforms/auditSpreadProps.test.ts +97 -0
  4. package/src/components/date-picker/transforms/auditSpreadProps.ts +10 -0
  5. package/src/components/date-picker/transforms/convertStyleProps.test.ts +58 -0
  6. package/src/components/date-picker/transforms/convertStyleProps.ts +10 -0
  7. package/src/components/date-picker/transforms/maxDateToMax.test.ts +87 -0
  8. package/src/components/date-picker/transforms/maxDateToMax.ts +16 -0
  9. package/src/components/date-picker/transforms/mergeFieldIntoDateField.test.ts +240 -0
  10. package/src/components/date-picker/transforms/mergeFieldIntoDateField.ts +5 -0
  11. package/src/components/date-picker/transforms/minDateToMin.test.ts +87 -0
  12. package/src/components/date-picker/transforms/minDateToMin.ts +16 -0
  13. package/src/components/date-picker/transforms/momentToDateString.test.ts +240 -0
  14. package/src/components/date-picker/transforms/momentToDateString.ts +87 -0
  15. package/src/components/date-picker/transforms/moveDatePickerImport.test.ts +157 -0
  16. package/src/components/date-picker/transforms/moveDatePickerImport.ts +14 -0
  17. package/src/components/date-picker/transforms/nativeDateToString.test.ts +220 -0
  18. package/src/components/date-picker/transforms/nativeDateToString.ts +59 -0
  19. package/src/components/date-picker/transforms/removeDuplicateKeys.test.ts +120 -0
  20. package/src/components/date-picker/transforms/removeDuplicateKeys.ts +8 -0
  21. package/src/components/date-picker/transforms/removeFormatValue.test.ts +119 -0
  22. package/src/components/date-picker/transforms/removeFormatValue.ts +22 -0
  23. package/src/components/date-picker/transforms/removePlaceholder.test.ts +117 -0
  24. package/src/components/date-picker/transforms/removePlaceholder.ts +22 -0
  25. package/src/components/date-picker/transforms/sizeMapping.test.ts +173 -0
  26. package/src/components/date-picker/transforms/sizeMapping.ts +15 -0
  27. package/src/components/date-picker/transforms/stateToInvalid.test.ts +109 -0
  28. package/src/components/date-picker/transforms/stateToInvalid.ts +57 -0
  29. package/src/components/date-picker/transforms/stateToInvalidTernary.test.ts +72 -0
  30. package/src/components/date-picker/transforms/stateToInvalidTernary.ts +11 -0
  31. package/src/components/date-picker/transforms/unsupportedProps.test.ts +170 -0
  32. package/src/components/date-picker/transforms/unsupportedProps.ts +37 -0
  33. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +18 -0
  34. package/src/index.ts +1 -0
@@ -0,0 +1,16 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { transformAttributeName } from "../../shared/actions/transformAttributeName"
4
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
5
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
6
+
7
+ const transform: Transform = attributeTransformFactory({
8
+ condition: hasAttribute("minDate"),
9
+ targetComponent: "DateField",
10
+ targetPackage: "@planningcenter/tapestry-react",
11
+ transform: (element, { j, options }) => {
12
+ return transformAttributeName("minDate", "min", { element, j, options })
13
+ },
14
+ })
15
+
16
+ export default transform
@@ -0,0 +1,240 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./momentToDateString"
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("momentToDateString transform", () => {
18
+ describe("minDate with moment().toDate()", () => {
19
+ it("converts moment(starts).add(1, 'day').toDate() to .format()", () => {
20
+ const input = `
21
+ import { DateField } from "@planningcenter/tapestry-react"
22
+
23
+ function Test() {
24
+ return <DateField minDate={moment(starts).add(1, 'day').toDate()} value={date} onChange={setDate} />
25
+ }
26
+ `.trim()
27
+
28
+ const result = applyTransform(input)
29
+ expect(result).not.toBeNull()
30
+ expect(result).toContain('.format("YYYY-MM-DD")')
31
+ expect(result).not.toContain(".toDate()}")
32
+ expect(result).toContain("moment(starts).add(1, 'day')")
33
+ })
34
+
35
+ it("converts moment().add(-10, 'years').toDate() to .format()", () => {
36
+ const input = `
37
+ import { DateField } from "@planningcenter/tapestry-react"
38
+
39
+ function Test() {
40
+ return <DateField minDate={moment().add(-10, 'years').toDate()} value={date} onChange={setDate} />
41
+ }
42
+ `.trim()
43
+
44
+ const result = applyTransform(input)
45
+ expect(result).not.toBeNull()
46
+ expect(result).toContain('.format("YYYY-MM-DD")')
47
+ expect(result).not.toContain(".toDate()}")
48
+ })
49
+
50
+ it("converts simple moment().toDate() to .format()", () => {
51
+ const input = `
52
+ import { DateField } from "@planningcenter/tapestry-react"
53
+
54
+ function Test() {
55
+ return <DateField minDate={moment().toDate()} value={date} onChange={setDate} />
56
+ }
57
+ `.trim()
58
+
59
+ const result = applyTransform(input)
60
+ expect(result).not.toBeNull()
61
+ expect(result).toContain('moment().format("YYYY-MM-DD")')
62
+ expect(result).not.toContain(".toDate()}")
63
+ })
64
+
65
+ it("adds a change comment noting the conversion", () => {
66
+ const input = `
67
+ import { DateField } from "@planningcenter/tapestry-react"
68
+
69
+ function Test() {
70
+ return <DateField minDate={moment().toDate()} value={date} onChange={setDate} />
71
+ }
72
+ `.trim()
73
+
74
+ const result = applyTransform(input)
75
+ expect(result).not.toBeNull()
76
+ expect(result).toContain("Converted .toDate()")
77
+ expect(result).toContain("ISO date strings")
78
+ })
79
+ })
80
+
81
+ describe("maxDate with moment().toDate()", () => {
82
+ it("converts maxDate moment chain to .format()", () => {
83
+ const input = `
84
+ import { DateField } from "@planningcenter/tapestry-react"
85
+
86
+ function Test() {
87
+ return <DateField maxDate={moment(ends).subtract(1, 'day').toDate()} value={date} onChange={setDate} />
88
+ }
89
+ `.trim()
90
+
91
+ const result = applyTransform(input)
92
+ expect(result).not.toBeNull()
93
+ expect(result).toContain('.format("YYYY-MM-DD")')
94
+ expect(result).not.toContain(".toDate()}")
95
+ })
96
+ })
97
+
98
+ describe("value with moment().toDate()", () => {
99
+ it("converts value moment chain to .format()", () => {
100
+ const input = `
101
+ import { DateField } from "@planningcenter/tapestry-react"
102
+
103
+ function Test() {
104
+ return <DateField value={moment(selectedDate).toDate()} onChange={setDate} />
105
+ }
106
+ `.trim()
107
+
108
+ const result = applyTransform(input)
109
+ expect(result).not.toBeNull()
110
+ expect(result).toContain('.format("YYYY-MM-DD")')
111
+ expect(result).not.toContain(".toDate()}")
112
+ })
113
+ })
114
+
115
+ describe("multiple props with moment", () => {
116
+ it("converts both minDate and maxDate moment chains", () => {
117
+ const input = `
118
+ import { DateField } from "@planningcenter/tapestry-react"
119
+
120
+ function Test() {
121
+ return (
122
+ <DateField
123
+ minDate={moment().add(-10, 'years').toDate()}
124
+ maxDate={moment().toDate()}
125
+ value={date}
126
+ onChange={setDate}
127
+ />
128
+ )
129
+ }
130
+ `.trim()
131
+
132
+ const result = applyTransform(input)
133
+ expect(result).not.toBeNull()
134
+ expect(result).not.toContain(".toDate()}")
135
+ const formatMatches = result!.match(/\)\.format\("YYYY-MM-DD"\)\}/g)
136
+ expect(formatMatches).toHaveLength(2)
137
+ })
138
+ })
139
+
140
+ describe("moment without .toDate()", () => {
141
+ it("adds TODO comment when moment is used without .toDate()", () => {
142
+ const input = `
143
+ import { DateField } from "@planningcenter/tapestry-react"
144
+
145
+ function Test() {
146
+ return <DateField minDate={moment(starts)} value={date} onChange={setDate} />
147
+ }
148
+ `.trim()
149
+
150
+ const result = applyTransform(input)
151
+ expect(result).not.toBeNull()
152
+ expect(result).toContain("TODO: tapestry-migration")
153
+ expect(result).toContain("manual conversion")
154
+ })
155
+
156
+ it("adds TODO for moment variable reference in value", () => {
157
+ const input = `
158
+ import { DateField } from "@planningcenter/tapestry-react"
159
+
160
+ function Test() {
161
+ const d = moment()
162
+ return <DateField value={moment(d)} onChange={setDate} />
163
+ }
164
+ `.trim()
165
+
166
+ const result = applyTransform(input)
167
+ expect(result).not.toBeNull()
168
+ expect(result).toContain("TODO: tapestry-migration")
169
+ })
170
+ })
171
+
172
+ describe("non-date values", () => {
173
+ it("does not transform string values", () => {
174
+ const input = `
175
+ import { DateField } from "@planningcenter/tapestry-react"
176
+
177
+ function Test() {
178
+ return <DateField value="2024-01-01" onChange={setDate} />
179
+ }
180
+ `.trim()
181
+
182
+ const result = applyTransform(input)
183
+ expect(result).toBeNull()
184
+ })
185
+
186
+ it("does not transform variable references", () => {
187
+ const input = `
188
+ import { DateField } from "@planningcenter/tapestry-react"
189
+
190
+ function Test() {
191
+ return <DateField minDate={startDate} value={date} onChange={setDate} />
192
+ }
193
+ `.trim()
194
+
195
+ const result = applyTransform(input)
196
+ expect(result).toBeNull()
197
+ })
198
+ })
199
+
200
+ describe("edge cases", () => {
201
+ it("does not affect other components", () => {
202
+ const input = `
203
+ import { Input } from "@planningcenter/tapestry-react"
204
+
205
+ function Test() {
206
+ return <Input value={moment().toDate()} label="Name" />
207
+ }
208
+ `.trim()
209
+
210
+ const result = applyTransform(input)
211
+ expect(result).toBeNull()
212
+ })
213
+
214
+ it("does not transform non-date props that use moment", () => {
215
+ const input = `
216
+ import { DateField } from "@planningcenter/tapestry-react"
217
+
218
+ function Test() {
219
+ return <DateField onChange={moment().toDate()} value={date} />
220
+ }
221
+ `.trim()
222
+
223
+ const result = applyTransform(input)
224
+ expect(result).toBeNull()
225
+ })
226
+
227
+ it("does not transform DateField from other packages", () => {
228
+ const input = `
229
+ import { DateField } from "other-library"
230
+
231
+ function Test() {
232
+ return <DateField minDate={moment().toDate()} value={date} onChange={setDate} />
233
+ }
234
+ `.trim()
235
+
236
+ const result = applyTransform(input)
237
+ expect(result).toBeNull()
238
+ })
239
+ })
240
+ })
@@ -0,0 +1,87 @@
1
+ import {
2
+ CallExpression,
3
+ Expression,
4
+ MemberExpression,
5
+ Transform,
6
+ } from "jscodeshift"
7
+
8
+ import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
9
+ import { getAttribute } from "../../shared/actions/getAttribute"
10
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
11
+
12
+ const DATE_PROPS = ["minDate", "maxDate", "value"]
13
+
14
+ /**
15
+ * Checks if an expression is a call to `.toDate()` at the end of a chain
16
+ */
17
+ function isToDateCall(expr: Expression): expr is CallExpression {
18
+ return (
19
+ expr.type === "CallExpression" &&
20
+ expr.callee.type === "MemberExpression" &&
21
+ expr.callee.property.type === "Identifier" &&
22
+ expr.callee.property.name === "toDate" &&
23
+ expr.arguments.length === 0
24
+ )
25
+ }
26
+
27
+ /**
28
+ * Checks whether an expression references `moment` anywhere in its call chain
29
+ */
30
+ function usesMoment(expr: Expression): boolean {
31
+ if (expr.type === "Identifier" && expr.name === "moment") return true
32
+ if (expr.type === "CallExpression") {
33
+ if (usesMoment(expr.callee as Expression)) return true
34
+ return expr.arguments.some((arg) =>
35
+ arg.type !== "SpreadElement" ? usesMoment(arg as Expression) : false
36
+ )
37
+ }
38
+ if (expr.type === "MemberExpression") {
39
+ return usesMoment(expr.object as Expression)
40
+ }
41
+ return false
42
+ }
43
+
44
+ const transform: Transform = attributeTransformFactory({
45
+ targetComponent: "DateField",
46
+ targetPackage: "@planningcenter/tapestry-react",
47
+ transform: (element, { j }) => {
48
+ let hasChanges = false
49
+
50
+ for (const propName of DATE_PROPS) {
51
+ const attr = getAttribute({ element, name: propName })
52
+ if (!attr?.value) continue
53
+ if (attr.value.type !== "JSXExpressionContainer") continue
54
+
55
+ const expr = attr.value.expression
56
+ if (expr.type === "JSXEmptyExpression") continue
57
+
58
+ if (!usesMoment(expr as Expression)) continue
59
+
60
+ if (isToDateCall(expr as Expression)) {
61
+ const callExpr = expr as CallExpression
62
+ const memberExpr = callExpr.callee as MemberExpression
63
+ memberExpr.property = j.identifier("format")
64
+ callExpr.arguments = [j.stringLiteral("YYYY-MM-DD")]
65
+
66
+ addCommentToAttribute({
67
+ attribute: attr,
68
+ commentKind: "change",
69
+ j,
70
+ text: `Converted .toDate() to .format("YYYY-MM-DD") — DatePicker accepts ISO date strings instead of Date objects. Verify the format is correct.`,
71
+ })
72
+ hasChanges = true
73
+ } else {
74
+ addCommentToAttribute({
75
+ attribute: attr,
76
+ j,
77
+ text: `This prop uses moment and may need manual conversion. DatePicker accepts DateValue or ISO date strings (YYYY-MM-DD) instead of Date objects.`,
78
+ })
79
+ hasChanges = true
80
+ }
81
+ }
82
+
83
+ return hasChanges
84
+ },
85
+ })
86
+
87
+ export default transform
@@ -0,0 +1,157 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./moveDatePickerImport"
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("moveDatePickerImport transform", () => {
19
+ describe("import migration", () => {
20
+ it("should rename DateField to DatePicker and change import", () => {
21
+ const input = `
22
+ import { DateField } from "@planningcenter/tapestry-react"
23
+
24
+ function Test() {
25
+ return <DateField value={date} onChange={setDate} />
26
+ }
27
+ `.trim()
28
+
29
+ const result = applyTransform(input)
30
+ expect(result).toContain(
31
+ 'import { DatePicker } from "@planningcenter/tapestry"'
32
+ )
33
+ expect(result).not.toContain("@planningcenter/tapestry-react")
34
+ expect(result).toContain("<DatePicker")
35
+ expect(result).not.toContain("<DateField")
36
+ })
37
+
38
+ it("should only move DateField, leaving other imports in place", () => {
39
+ const input = `
40
+ import { Button } from "@planningcenter/tapestry-react"
41
+ import { DateField } from "@planningcenter/tapestry-react"
42
+
43
+ function Test() {
44
+ return (
45
+ <div>
46
+ <Button>Click</Button>
47
+ <DateField value={date} onChange={setDate} />
48
+ </div>
49
+ )
50
+ }
51
+ `.trim()
52
+
53
+ const result = applyTransform(input)
54
+ expect(result).toContain(
55
+ 'import { Button } from "@planningcenter/tapestry-react"'
56
+ )
57
+ expect(result).toContain(
58
+ 'import { DatePicker } from "@planningcenter/tapestry"'
59
+ )
60
+ expect(result).toContain("<DatePicker")
61
+ })
62
+ })
63
+
64
+ describe("import conflict handling", () => {
65
+ it("should use TdsDatePicker alias when DatePicker is already imported from another package", () => {
66
+ const input = `
67
+ import { DatePicker } from "some-other-library"
68
+ import { DateField } from "@planningcenter/tapestry-react"
69
+
70
+ function Test() {
71
+ return (
72
+ <div>
73
+ <DatePicker />
74
+ <DateField value={date} onChange={setDate} />
75
+ </div>
76
+ )
77
+ }
78
+ `.trim()
79
+
80
+ const result = applyTransform(input)
81
+ expect(result).toContain(
82
+ 'import { DatePicker } from "some-other-library"'
83
+ )
84
+ expect(result).toContain(
85
+ 'import { DatePicker as TdsDatePicker } from "@planningcenter/tapestry"'
86
+ )
87
+ expect(result).toContain("<TdsDatePicker")
88
+ expect(result).not.toContain("<DateField")
89
+ })
90
+ })
91
+
92
+ describe("edge cases", () => {
93
+ it("should not affect other components", () => {
94
+ const input = `
95
+ import { Button } from "@planningcenter/tapestry-react"
96
+
97
+ function Test() {
98
+ return <Button>Click me</Button>
99
+ }
100
+ `.trim()
101
+
102
+ const result = applyTransform(input)
103
+ expect(result).toContain(
104
+ 'import { Button } from "@planningcenter/tapestry-react"'
105
+ )
106
+ })
107
+
108
+ it("should handle no imports", () => {
109
+ const input = `
110
+ function Test() {
111
+ return <div>No imports</div>
112
+ }
113
+ `.trim()
114
+
115
+ const result = applyTransform(input)
116
+ expect(result).toBe(input)
117
+ })
118
+
119
+ it("should preserve all attributes", () => {
120
+ const input = `
121
+ import { DateField } from "@planningcenter/tapestry-react"
122
+
123
+ function Test() {
124
+ return (
125
+ <DateField
126
+ value={date}
127
+ onChange={setDate}
128
+ disabled
129
+ />
130
+ )
131
+ }
132
+ `.trim()
133
+
134
+ const result = applyTransform(input)
135
+ expect(result).toContain(
136
+ 'import { DatePicker } from "@planningcenter/tapestry"'
137
+ )
138
+ expect(result).toContain("value={date}")
139
+ expect(result).toContain("onChange={setDate}")
140
+ expect(result).toContain("disabled")
141
+ })
142
+
143
+ it("should handle alias import", () => {
144
+ const input = `
145
+ import { DateField as TapestryDateField } from "@planningcenter/tapestry-react"
146
+
147
+ function Test() {
148
+ return <TapestryDateField value={date} onChange={setDate} />
149
+ }
150
+ `.trim()
151
+
152
+ const result = applyTransform(input)
153
+ expect(result).toContain("@planningcenter/tapestry")
154
+ expect(result).not.toContain("@planningcenter/tapestry-react")
155
+ })
156
+ })
157
+ })
@@ -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: "TdsDatePicker",
8
+ fromComponent: "DateField",
9
+ fromPackage: "@planningcenter/tapestry-react",
10
+ toComponent: "DatePicker",
11
+ toPackage: "@planningcenter/tapestry",
12
+ })
13
+
14
+ export default transform