@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.
- package/package.json +3 -3
- package/src/components/shared/helpers/unsupportedPropsHelpers.ts +24 -0
- package/src/components/shared/transformFactories/stylePropTransformFactory.test.ts +36 -0
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +43 -10
- package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +17 -12
- package/src/components/time-field/index.ts +48 -0
- package/src/components/time-field/transforms/auditSpreadProps.test.ts +76 -0
- package/src/components/time-field/transforms/auditSpreadProps.ts +10 -0
- package/src/components/time-field/transforms/convertStyleProps.test.ts +43 -0
- package/src/components/time-field/transforms/convertStyleProps.ts +10 -0
- package/src/components/time-field/transforms/flagMinMax.test.ts +103 -0
- package/src/components/time-field/transforms/flagMinMax.ts +31 -0
- package/src/components/time-field/transforms/mergeFieldIntoTimeField.test.ts +106 -0
- package/src/components/time-field/transforms/mergeFieldIntoTimeField.ts +5 -0
- package/src/components/time-field/transforms/moveTimeFieldImport.test.ts +153 -0
- package/src/components/time-field/transforms/moveTimeFieldImport.ts +14 -0
- package/src/components/time-field/transforms/sizeMapping.test.ts +173 -0
- package/src/components/time-field/transforms/sizeMapping.ts +15 -0
- package/src/components/time-field/transforms/stateToInvalid.test.ts +87 -0
- package/src/components/time-field/transforms/stateToInvalid.ts +56 -0
- package/src/components/time-field/transforms/stateToInvalidTernary.test.ts +100 -0
- package/src/components/time-field/transforms/stateToInvalidTernary.ts +11 -0
- package/src/components/time-field/transforms/tupleToTime.test.ts +182 -0
- package/src/components/time-field/transforms/tupleToTime.ts +107 -0
- package/src/components/time-field/transforms/twelveHourClockToHourCycle.test.ts +117 -0
- package/src/components/time-field/transforms/twelveHourClockToHourCycle.ts +65 -0
- package/src/components/time-field/transforms/unsupportedProps.test.ts +160 -0
- package/src/components/time-field/transforms/unsupportedProps.ts +37 -0
- package/src/index.ts +2 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./tupleToTime"
|
|
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("tupleToTime transform", () => {
|
|
18
|
+
describe("literal tuple conversion", () => {
|
|
19
|
+
it("converts value={[9, 30]} to value={new Time(9, 30)}", () => {
|
|
20
|
+
const input = `
|
|
21
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
function Test() {
|
|
24
|
+
return <TimeField value={[9, 30]} onChange={setTime} />
|
|
25
|
+
}
|
|
26
|
+
`.trim()
|
|
27
|
+
|
|
28
|
+
const result = applyTransform(input)
|
|
29
|
+
expect(result).not.toBeNull()
|
|
30
|
+
expect(result).toContain("new Time(9, 30)")
|
|
31
|
+
expect(result).not.toContain("[9, 30]")
|
|
32
|
+
expect(result).toContain("CHANGED: tapestry-migration")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("converts defaultValue={[14, 0]} to new Time", () => {
|
|
36
|
+
const input = `
|
|
37
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
38
|
+
|
|
39
|
+
function Test() {
|
|
40
|
+
return <TimeField defaultValue={[14, 0]} onChange={setTime} />
|
|
41
|
+
}
|
|
42
|
+
`.trim()
|
|
43
|
+
|
|
44
|
+
const result = applyTransform(input)
|
|
45
|
+
expect(result).not.toBeNull()
|
|
46
|
+
expect(result).toContain("new Time(14, 0)")
|
|
47
|
+
expect(result).not.toContain("[14, 0]")
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("imports Time from @internationalized/date when converting", () => {
|
|
51
|
+
const input = `
|
|
52
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
53
|
+
|
|
54
|
+
function Test() {
|
|
55
|
+
return <TimeField value={[9, 30]} onChange={setTime} />
|
|
56
|
+
}
|
|
57
|
+
`.trim()
|
|
58
|
+
|
|
59
|
+
const result = applyTransform(input)
|
|
60
|
+
expect(result).not.toBeNull()
|
|
61
|
+
expect(result).toContain('import { Time } from "@internationalized/date"')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("does not duplicate Time import if already present", () => {
|
|
65
|
+
const input = `
|
|
66
|
+
import { Time } from "@internationalized/date"
|
|
67
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
68
|
+
|
|
69
|
+
function Test() {
|
|
70
|
+
return <TimeField value={[9, 30]} onChange={setTime} />
|
|
71
|
+
}
|
|
72
|
+
`.trim()
|
|
73
|
+
|
|
74
|
+
const result = applyTransform(input)
|
|
75
|
+
expect(result).not.toBeNull()
|
|
76
|
+
const matches = result!.match(/from ["']@internationalized\/date["']/g)
|
|
77
|
+
expect(matches).toHaveLength(1)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("converts both value and defaultValue tuples in the same element", () => {
|
|
81
|
+
const input = `
|
|
82
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
83
|
+
|
|
84
|
+
function Test() {
|
|
85
|
+
return <TimeField value={[9, 30]} defaultValue={[8, 0]} onChange={setTime} />
|
|
86
|
+
}
|
|
87
|
+
`.trim()
|
|
88
|
+
|
|
89
|
+
const result = applyTransform(input)
|
|
90
|
+
expect(result).not.toBeNull()
|
|
91
|
+
expect(result).toContain("new Time(9, 30)")
|
|
92
|
+
expect(result).toContain("new Time(8, 0)")
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe("non-tuple expressions", () => {
|
|
97
|
+
it("adds TODO comment for variable references", () => {
|
|
98
|
+
const input = `
|
|
99
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
100
|
+
|
|
101
|
+
function Test() {
|
|
102
|
+
return <TimeField value={currentTime} onChange={setTime} />
|
|
103
|
+
}
|
|
104
|
+
`.trim()
|
|
105
|
+
|
|
106
|
+
const result = applyTransform(input)
|
|
107
|
+
expect(result).not.toBeNull()
|
|
108
|
+
expect(result).toContain("TODO: tapestry-migration")
|
|
109
|
+
expect(result).toContain("TimeValue")
|
|
110
|
+
expect(result).toContain("currentTime")
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("adds TODO for tuples with non-numeric elements", () => {
|
|
114
|
+
const input = `
|
|
115
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
116
|
+
|
|
117
|
+
function Test() {
|
|
118
|
+
return <TimeField value={[hours, minutes]} onChange={setTime} />
|
|
119
|
+
}
|
|
120
|
+
`.trim()
|
|
121
|
+
|
|
122
|
+
const result = applyTransform(input)
|
|
123
|
+
expect(result).not.toBeNull()
|
|
124
|
+
expect(result).toContain("TODO: tapestry-migration")
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("does not add Time import when only TODO comments are added", () => {
|
|
128
|
+
const input = `
|
|
129
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
130
|
+
|
|
131
|
+
function Test() {
|
|
132
|
+
return <TimeField value={currentTime} onChange={setTime} />
|
|
133
|
+
}
|
|
134
|
+
`.trim()
|
|
135
|
+
|
|
136
|
+
const result = applyTransform(input)
|
|
137
|
+
expect(result).not.toBeNull()
|
|
138
|
+
expect(result).not.toContain('from "@internationalized/date"')
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe("edge cases", () => {
|
|
143
|
+
it("returns null when TimeField has no value/defaultValue", () => {
|
|
144
|
+
const input = `
|
|
145
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
146
|
+
|
|
147
|
+
function Test() {
|
|
148
|
+
return <TimeField onChange={setTime} />
|
|
149
|
+
}
|
|
150
|
+
`.trim()
|
|
151
|
+
|
|
152
|
+
const result = applyTransform(input)
|
|
153
|
+
expect(result).toBeNull()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it("does not affect TimeField from other packages", () => {
|
|
157
|
+
const input = `
|
|
158
|
+
import { TimeField } from "some-other-lib"
|
|
159
|
+
|
|
160
|
+
function Test() {
|
|
161
|
+
return <TimeField value={[9, 30]} onChange={setTime} />
|
|
162
|
+
}
|
|
163
|
+
`.trim()
|
|
164
|
+
|
|
165
|
+
const result = applyTransform(input)
|
|
166
|
+
expect(result).toBeNull()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it("does not affect other components", () => {
|
|
170
|
+
const input = `
|
|
171
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
172
|
+
|
|
173
|
+
function Test() {
|
|
174
|
+
return <Input value={[9, 30]} label="Name" />
|
|
175
|
+
}
|
|
176
|
+
`.trim()
|
|
177
|
+
|
|
178
|
+
const result = applyTransform(input)
|
|
179
|
+
expect(result).toBeNull()
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ArrayExpression, Expression, Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
|
|
4
|
+
import { getAttribute } from "../../shared/actions/getAttribute"
|
|
5
|
+
import { addImport } from "../../shared/transformFactories/helpers/manageImports"
|
|
6
|
+
|
|
7
|
+
const TUPLE_PROPS = ["value", "defaultValue"]
|
|
8
|
+
|
|
9
|
+
function isNumberLike(expr: Expression): boolean {
|
|
10
|
+
if (expr.type === "NumericLiteral") return true
|
|
11
|
+
if (
|
|
12
|
+
expr.type === "UnaryExpression" &&
|
|
13
|
+
expr.operator === "-" &&
|
|
14
|
+
expr.argument.type === "NumericLiteral"
|
|
15
|
+
) {
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isHourMinuteTuple(expr: Expression): expr is ArrayExpression {
|
|
22
|
+
if (expr.type !== "ArrayExpression") return false
|
|
23
|
+
if (expr.elements.length !== 2) return false
|
|
24
|
+
return expr.elements.every(
|
|
25
|
+
(el) => el !== null && el.type !== "SpreadElement" && isNumberLike(el)
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const transform: Transform = (fileInfo, api) => {
|
|
30
|
+
const j = api.jscodeshift
|
|
31
|
+
const source = j(fileInfo.source)
|
|
32
|
+
let hasChanges = false
|
|
33
|
+
let needsTimeImport = false
|
|
34
|
+
|
|
35
|
+
// Find local name for TimeField imported from tapestry-react
|
|
36
|
+
let timeFieldLocalName: string | null = null
|
|
37
|
+
source
|
|
38
|
+
.find(j.ImportDeclaration, {
|
|
39
|
+
source: { value: "@planningcenter/tapestry-react" },
|
|
40
|
+
})
|
|
41
|
+
.forEach((path) => {
|
|
42
|
+
const specs = path.value.specifiers || []
|
|
43
|
+
specs.forEach((spec) => {
|
|
44
|
+
if (
|
|
45
|
+
spec.type === "ImportSpecifier" &&
|
|
46
|
+
spec.imported?.name === "TimeField"
|
|
47
|
+
) {
|
|
48
|
+
timeFieldLocalName = (spec.local?.name as string) || "TimeField"
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
if (!timeFieldLocalName) return null
|
|
54
|
+
|
|
55
|
+
source.find(j.JSXOpeningElement).forEach((path) => {
|
|
56
|
+
const opening = path.value
|
|
57
|
+
if (opening.name.type !== "JSXIdentifier") return
|
|
58
|
+
if (opening.name.name !== timeFieldLocalName) return
|
|
59
|
+
|
|
60
|
+
const element = path.parent.value
|
|
61
|
+
for (const propName of TUPLE_PROPS) {
|
|
62
|
+
const attr = getAttribute({ element, name: propName })
|
|
63
|
+
if (!attr?.value) continue
|
|
64
|
+
if (attr.value.type !== "JSXExpressionContainer") continue
|
|
65
|
+
|
|
66
|
+
const expr = attr.value.expression
|
|
67
|
+
if (expr.type === "JSXEmptyExpression") continue
|
|
68
|
+
|
|
69
|
+
if (isHourMinuteTuple(expr as Expression)) {
|
|
70
|
+
const arr = expr as ArrayExpression
|
|
71
|
+
const [hourEl, minuteEl] = arr.elements as Expression[]
|
|
72
|
+
attr.value = j.jsxExpressionContainer(
|
|
73
|
+
j.newExpression(j.identifier("Time"), [hourEl, minuteEl])
|
|
74
|
+
)
|
|
75
|
+
addCommentToAttribute({
|
|
76
|
+
attribute: attr,
|
|
77
|
+
commentKind: "change",
|
|
78
|
+
j,
|
|
79
|
+
text: "Converted [hours, minutes] tuple to new Time(hours, minutes) — TimeField now uses TimeValue from @internationalized/date.",
|
|
80
|
+
})
|
|
81
|
+
hasChanges = true
|
|
82
|
+
needsTimeImport = true
|
|
83
|
+
} else {
|
|
84
|
+
addCommentToAttribute({
|
|
85
|
+
attribute: attr,
|
|
86
|
+
j,
|
|
87
|
+
text: "TimeField now expects a TimeValue (e.g. new Time(hours, minutes)) from @internationalized/date instead of a [hours, minutes] tuple. Migrate this value manually.",
|
|
88
|
+
})
|
|
89
|
+
hasChanges = true
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
if (needsTimeImport) {
|
|
95
|
+
addImport({
|
|
96
|
+
component: "Time",
|
|
97
|
+
conflictAlias: "Time",
|
|
98
|
+
j,
|
|
99
|
+
pkg: "@internationalized/date",
|
|
100
|
+
source,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return hasChanges ? source.toSource() : null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default transform
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./twelveHourClockToHourCycle"
|
|
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("twelveHourClockToHourCycle transform", () => {
|
|
18
|
+
it("converts shorthand twelveHourClock to hourCycle={12}", () => {
|
|
19
|
+
const input = `
|
|
20
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
21
|
+
|
|
22
|
+
function Test() {
|
|
23
|
+
return <TimeField twelveHourClock value={time} onChange={setTime} />
|
|
24
|
+
}
|
|
25
|
+
`.trim()
|
|
26
|
+
|
|
27
|
+
const result = applyTransform(input)
|
|
28
|
+
expect(result).not.toBeNull()
|
|
29
|
+
expect(result).toContain("hourCycle={12}")
|
|
30
|
+
expect(result).not.toContain("twelveHourClock")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("converts twelveHourClock={true} to hourCycle={12}", () => {
|
|
34
|
+
const input = `
|
|
35
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
36
|
+
|
|
37
|
+
function Test() {
|
|
38
|
+
return <TimeField twelveHourClock={true} value={time} onChange={setTime} />
|
|
39
|
+
}
|
|
40
|
+
`.trim()
|
|
41
|
+
|
|
42
|
+
const result = applyTransform(input)
|
|
43
|
+
expect(result).not.toBeNull()
|
|
44
|
+
expect(result).toContain("hourCycle={12}")
|
|
45
|
+
expect(result).not.toContain("twelveHourClock")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("converts twelveHourClock={false} to hourCycle={24}", () => {
|
|
49
|
+
const input = `
|
|
50
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
51
|
+
|
|
52
|
+
function Test() {
|
|
53
|
+
return <TimeField twelveHourClock={false} value={time} onChange={setTime} />
|
|
54
|
+
}
|
|
55
|
+
`.trim()
|
|
56
|
+
|
|
57
|
+
const result = applyTransform(input)
|
|
58
|
+
expect(result).not.toBeNull()
|
|
59
|
+
expect(result).toContain("hourCycle={24}")
|
|
60
|
+
expect(result).not.toContain("twelveHourClock")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("converts dynamic expression to hourCycle ternary with change comment", () => {
|
|
64
|
+
const input = `
|
|
65
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
66
|
+
|
|
67
|
+
function Test() {
|
|
68
|
+
return <TimeField twelveHourClock={use12} value={time} onChange={setTime} />
|
|
69
|
+
}
|
|
70
|
+
`.trim()
|
|
71
|
+
|
|
72
|
+
const result = applyTransform(input)
|
|
73
|
+
expect(result).not.toBeNull()
|
|
74
|
+
expect(result).toContain("hourCycle={use12 ? 12 : 24}")
|
|
75
|
+
expect(result).toContain("CHANGED: tapestry-migration")
|
|
76
|
+
expect(result).not.toContain("twelveHourClock=")
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("returns null when TimeField has no twelveHourClock prop", () => {
|
|
80
|
+
const input = `
|
|
81
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
82
|
+
|
|
83
|
+
function Test() {
|
|
84
|
+
return <TimeField value={time} onChange={setTime} />
|
|
85
|
+
}
|
|
86
|
+
`.trim()
|
|
87
|
+
|
|
88
|
+
const result = applyTransform(input)
|
|
89
|
+
expect(result).toBeNull()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("does not affect other components", () => {
|
|
93
|
+
const input = `
|
|
94
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
95
|
+
|
|
96
|
+
function Test() {
|
|
97
|
+
return <Input twelveHourClock value="abc" />
|
|
98
|
+
}
|
|
99
|
+
`.trim()
|
|
100
|
+
|
|
101
|
+
const result = applyTransform(input)
|
|
102
|
+
expect(result).toBeNull()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("does not affect TimeField imported from a different package", () => {
|
|
106
|
+
const input = `
|
|
107
|
+
import { TimeField } from "some-other-lib"
|
|
108
|
+
|
|
109
|
+
function Test() {
|
|
110
|
+
return <TimeField twelveHourClock value={time} onChange={setTime} />
|
|
111
|
+
}
|
|
112
|
+
`.trim()
|
|
113
|
+
|
|
114
|
+
const result = applyTransform(input)
|
|
115
|
+
expect(result).toBeNull()
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
|
|
4
|
+
import { getAttribute } from "../../shared/actions/getAttribute"
|
|
5
|
+
import { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
6
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
7
|
+
|
|
8
|
+
const transform: Transform = attributeTransformFactory({
|
|
9
|
+
condition: hasAttribute("twelveHourClock"),
|
|
10
|
+
targetComponent: "TimeField",
|
|
11
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
12
|
+
transform: (element, { j }) => {
|
|
13
|
+
const attr = getAttribute({ element, name: "twelveHourClock" })
|
|
14
|
+
if (!attr) return false
|
|
15
|
+
|
|
16
|
+
// Shorthand `twelveHourClock` (no value) — boolean true → hourCycle={12}
|
|
17
|
+
if (attr.value === null || attr.value === undefined) {
|
|
18
|
+
attr.name.name = "hourCycle"
|
|
19
|
+
attr.value = j.jsxExpressionContainer(j.numericLiteral(12))
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (attr.value.type === "JSXExpressionContainer") {
|
|
24
|
+
const expr = attr.value.expression
|
|
25
|
+
|
|
26
|
+
if (expr.type === "BooleanLiteral") {
|
|
27
|
+
attr.name.name = "hourCycle"
|
|
28
|
+
attr.value = j.jsxExpressionContainer(
|
|
29
|
+
j.numericLiteral(expr.value ? 12 : 24)
|
|
30
|
+
)
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Dynamic expression — convert to ternary `hourCycle={x ? 12 : 24}`
|
|
35
|
+
if (expr.type !== "JSXEmptyExpression") {
|
|
36
|
+
attr.name.name = "hourCycle"
|
|
37
|
+
attr.value = j.jsxExpressionContainer(
|
|
38
|
+
j.conditionalExpression(
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
expr as any,
|
|
41
|
+
j.numericLiteral(12),
|
|
42
|
+
j.numericLiteral(24)
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
addCommentToAttribute({
|
|
46
|
+
attribute: attr,
|
|
47
|
+
commentKind: "change",
|
|
48
|
+
j,
|
|
49
|
+
text: "Converted 'twelveHourClock' to 'hourCycle' (12 or 24). Verify the conditional logic is correct. NOTE: i18n is built-in to Tapestry.",
|
|
50
|
+
})
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fallback: leave attribute, add TODO
|
|
56
|
+
addCommentToAttribute({
|
|
57
|
+
attribute: attr,
|
|
58
|
+
j,
|
|
59
|
+
text: "'twelveHourClock' has been replaced by 'hourCycle' (12 or 24). Migrate this prop manually. NOTE: i18n is built-in to Tapestry.",
|
|
60
|
+
})
|
|
61
|
+
return true
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export default transform
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./unsupportedProps"
|
|
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("unsupportedProps transform", () => {
|
|
19
|
+
it("flags ignoredKeys as unsupported", () => {
|
|
20
|
+
const input = `
|
|
21
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
function Test() {
|
|
24
|
+
return <TimeField ignoredKeys={["ArrowUp"]} value={time} onChange={setTime} />
|
|
25
|
+
}
|
|
26
|
+
`.trim()
|
|
27
|
+
|
|
28
|
+
const result = applyTransform(input)
|
|
29
|
+
expect(result).toContain("TODO: tapestry-migration")
|
|
30
|
+
expect(result).toContain("ignoredKeys")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("flags interval as unsupported", () => {
|
|
34
|
+
const input = `
|
|
35
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
36
|
+
|
|
37
|
+
function Test() {
|
|
38
|
+
return <TimeField interval={15} value={time} onChange={setTime} />
|
|
39
|
+
}
|
|
40
|
+
`.trim()
|
|
41
|
+
|
|
42
|
+
const result = applyTransform(input)
|
|
43
|
+
expect(result).toContain("TODO: tapestry-migration")
|
|
44
|
+
expect(result).toContain("interval")
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("flags isIOS as unsupported", () => {
|
|
48
|
+
const input = `
|
|
49
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
50
|
+
|
|
51
|
+
function Test() {
|
|
52
|
+
return <TimeField isIOS value={time} onChange={setTime} />
|
|
53
|
+
}
|
|
54
|
+
`.trim()
|
|
55
|
+
|
|
56
|
+
const result = applyTransform(input)
|
|
57
|
+
expect(result).toContain("TODO: tapestry-migration")
|
|
58
|
+
expect(result).toContain("isIOS")
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("flags controlled as unsupported", () => {
|
|
62
|
+
const input = `
|
|
63
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
64
|
+
|
|
65
|
+
function Test() {
|
|
66
|
+
return <TimeField controlled value={time} onChange={setTime} />
|
|
67
|
+
}
|
|
68
|
+
`.trim()
|
|
69
|
+
|
|
70
|
+
const result = applyTransform(input)
|
|
71
|
+
expect(result).toContain("TODO: tapestry-migration")
|
|
72
|
+
expect(result).toContain("controlled")
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("flags placeholder as unsupported", () => {
|
|
76
|
+
const input = `
|
|
77
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
78
|
+
|
|
79
|
+
function Test() {
|
|
80
|
+
return <TimeField placeholder="Enter time" value={time} onChange={setTime} />
|
|
81
|
+
}
|
|
82
|
+
`.trim()
|
|
83
|
+
|
|
84
|
+
const result = applyTransform(input)
|
|
85
|
+
expect(result).toContain("TODO: tapestry-migration")
|
|
86
|
+
expect(result).toContain("placeholder")
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("does not flag supported props", () => {
|
|
90
|
+
const input = `
|
|
91
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
92
|
+
|
|
93
|
+
function Test() {
|
|
94
|
+
return (
|
|
95
|
+
<TimeField
|
|
96
|
+
value={time}
|
|
97
|
+
onChange={setTime}
|
|
98
|
+
disabled
|
|
99
|
+
required
|
|
100
|
+
size="md"
|
|
101
|
+
label="Start time"
|
|
102
|
+
description="Pick a time"
|
|
103
|
+
invalid
|
|
104
|
+
hourCycle={12}
|
|
105
|
+
hideLabel
|
|
106
|
+
hideTimeZone
|
|
107
|
+
forceLeadingZeros
|
|
108
|
+
readOnly
|
|
109
|
+
name="start"
|
|
110
|
+
className="my-time"
|
|
111
|
+
id="start-time"
|
|
112
|
+
/>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
`.trim()
|
|
116
|
+
|
|
117
|
+
const result = applyTransform(input)
|
|
118
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("allows aria-* and data-* attributes", () => {
|
|
122
|
+
const input = `
|
|
123
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
124
|
+
|
|
125
|
+
function Test() {
|
|
126
|
+
return <TimeField aria-describedby="help" data-testid="time" value={time} onChange={setTime} />
|
|
127
|
+
}
|
|
128
|
+
`.trim()
|
|
129
|
+
|
|
130
|
+
const result = applyTransform(input)
|
|
131
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("flags css prop with specific message", () => {
|
|
135
|
+
const input = `
|
|
136
|
+
import { TimeField } from "@planningcenter/tapestry-react"
|
|
137
|
+
|
|
138
|
+
function Test() {
|
|
139
|
+
return <TimeField css={{ color: "red" }} value={time} onChange={setTime} />
|
|
140
|
+
}
|
|
141
|
+
`.trim()
|
|
142
|
+
|
|
143
|
+
const result = applyTransform(input)
|
|
144
|
+
expect(result).toContain("TODO: tapestry-migration")
|
|
145
|
+
expect(result).toContain("CSS prop is not supported")
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("does not affect other components", () => {
|
|
149
|
+
const input = `
|
|
150
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
151
|
+
|
|
152
|
+
function Test() {
|
|
153
|
+
return <Button ignoredKeys={["x"]}>Click</Button>
|
|
154
|
+
}
|
|
155
|
+
`.trim()
|
|
156
|
+
|
|
157
|
+
const result = applyTransform(input)
|
|
158
|
+
expect(result).toBe(input)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { JSXAttribute, Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addCommentToUnsupportedProps } from "../../shared/actions/addCommentToUnsupportedProps"
|
|
4
|
+
import { TIME_FIELD_SUPPORTED_PROPS } from "../../shared/helpers/unsupportedPropsHelpers"
|
|
5
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
6
|
+
|
|
7
|
+
const transform: Transform = attributeTransformFactory({
|
|
8
|
+
targetComponent: "TimeField",
|
|
9
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
10
|
+
transform: (element, { j }) => {
|
|
11
|
+
const attrs = element.openingElement.attributes || []
|
|
12
|
+
|
|
13
|
+
const UNSUPPORTED_PROPS = attrs
|
|
14
|
+
.filter(
|
|
15
|
+
(attr) =>
|
|
16
|
+
attr.type === "JSXAttribute" &&
|
|
17
|
+
!TIME_FIELD_SUPPORTED_PROPS.includes(attr.name.name as string) &&
|
|
18
|
+
!(attr.name.name as string).startsWith("aria-") &&
|
|
19
|
+
!(attr.name.name as string).startsWith("data-")
|
|
20
|
+
)
|
|
21
|
+
.map((attr) => (attr as JSXAttribute).name.name as string)
|
|
22
|
+
|
|
23
|
+
return addCommentToUnsupportedProps({
|
|
24
|
+
element,
|
|
25
|
+
j,
|
|
26
|
+
messageSuffix: (prop) => {
|
|
27
|
+
if (prop === "css") {
|
|
28
|
+
return "\n * CSS prop is not supported. Use className or style prop instead.\n"
|
|
29
|
+
}
|
|
30
|
+
return ""
|
|
31
|
+
},
|
|
32
|
+
props: UNSUPPORTED_PROPS,
|
|
33
|
+
})
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export default transform
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ const COMPONENTS_SET = new Set([
|
|
|
19
19
|
"radio",
|
|
20
20
|
"select",
|
|
21
21
|
"text-area",
|
|
22
|
+
"time-field",
|
|
22
23
|
"toggle-switch",
|
|
23
24
|
])
|
|
24
25
|
|
|
@@ -27,7 +28,7 @@ program
|
|
|
27
28
|
.description("Run a migration of a component from Tapestry React to Tapestry")
|
|
28
29
|
.argument(
|
|
29
30
|
"<component-name>",
|
|
30
|
-
"The name of the component to migrate (button, checkbox, input, link, radio, select, text-area, toggle-switch)"
|
|
31
|
+
"The name of the component to migrate (button, checkbox, input, link, radio, select, text-area, time-field, toggle-switch)"
|
|
31
32
|
)
|
|
32
33
|
.requiredOption(
|
|
33
34
|
"-p, --path <path>",
|