@planningcenter/tapestry-migration-cli 3.2.2-rc.8 → 3.2.2

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 (44) hide show
  1. package/package.json +3 -3
  2. package/src/components/checkbox/transforms/moveCheckboxImport.test.ts +26 -0
  3. package/src/components/checkbox/transforms/moveCheckboxImport.ts +1 -0
  4. package/src/components/input/transforms/moveInputImport.test.ts +26 -0
  5. package/src/components/input/transforms/moveInputImport.ts +1 -0
  6. package/src/components/input/transforms/numberFieldRenameToInput.test.ts +51 -0
  7. package/src/components/input/transforms/numberFieldRenameToInput.ts +1 -0
  8. package/src/components/radio/transforms/moveRadioImport.test.ts +26 -0
  9. package/src/components/radio/transforms/moveRadioImport.ts +1 -0
  10. package/src/components/select/transforms/moveSelectImport.test.ts +24 -0
  11. package/src/components/select/transforms/moveSelectImport.ts +1 -0
  12. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +24 -0
  13. package/src/components/shared/transformFactories/helpers/manageImports.ts +25 -1
  14. package/src/components/shared/transformFactories/stylePropTransformFactory.test.ts +330 -0
  15. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +156 -15
  16. package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +17 -12
  17. package/src/components/text-area/transforms/moveTextAreaImport.test.ts +26 -0
  18. package/src/components/text-area/transforms/moveTextAreaImport.ts +1 -0
  19. package/src/components/time-field/index.ts +48 -0
  20. package/src/components/time-field/transforms/auditSpreadProps.test.ts +76 -0
  21. package/src/components/time-field/transforms/auditSpreadProps.ts +10 -0
  22. package/src/components/time-field/transforms/convertStyleProps.test.ts +43 -0
  23. package/src/components/time-field/transforms/convertStyleProps.ts +10 -0
  24. package/src/components/time-field/transforms/flagMinMax.test.ts +103 -0
  25. package/src/components/time-field/transforms/flagMinMax.ts +31 -0
  26. package/src/components/time-field/transforms/mergeFieldIntoTimeField.test.ts +106 -0
  27. package/src/components/time-field/transforms/mergeFieldIntoTimeField.ts +5 -0
  28. package/src/components/time-field/transforms/moveTimeFieldImport.test.ts +153 -0
  29. package/src/components/time-field/transforms/moveTimeFieldImport.ts +14 -0
  30. package/src/components/time-field/transforms/sizeMapping.test.ts +173 -0
  31. package/src/components/time-field/transforms/sizeMapping.ts +15 -0
  32. package/src/components/time-field/transforms/stateToInvalid.test.ts +87 -0
  33. package/src/components/time-field/transforms/stateToInvalid.ts +56 -0
  34. package/src/components/time-field/transforms/stateToInvalidTernary.test.ts +100 -0
  35. package/src/components/time-field/transforms/stateToInvalidTernary.ts +11 -0
  36. package/src/components/time-field/transforms/tupleToTime.test.ts +182 -0
  37. package/src/components/time-field/transforms/tupleToTime.ts +107 -0
  38. package/src/components/time-field/transforms/twelveHourClockToHourCycle.test.ts +117 -0
  39. package/src/components/time-field/transforms/twelveHourClockToHourCycle.ts +65 -0
  40. package/src/components/time-field/transforms/unsupportedProps.test.ts +160 -0
  41. package/src/components/time-field/transforms/unsupportedProps.ts +37 -0
  42. package/src/components/toggle-switch/transforms/moveToggleSwitchImport.test.ts +28 -0
  43. package/src/components/toggle-switch/transforms/moveToggleSwitchImport.ts +1 -0
  44. package/src/index.ts +2 -1
@@ -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
+ })
@@ -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