@planningcenter/tapestry-migration-cli 3.1.0-rc.6 → 3.1.0-rc.7
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/button/transforms/convertStyleProps.test.ts +97 -0
- package/src/components/button/transforms/removeTypeButton.test.ts +0 -1
- package/src/components/checkbox/transforms/moveCheckboxImport.test.ts +3 -0
- package/src/components/input/index.ts +66 -0
- package/src/components/input/transformableInput.ts +8 -0
- package/src/components/input/transforms/auditSpreadProps.test.ts +192 -0
- package/src/components/input/transforms/auditSpreadProps.ts +26 -0
- package/src/components/input/transforms/autoWidthTransform.test.ts +172 -0
- package/src/components/input/transforms/autoWidthTransform.ts +41 -0
- package/src/components/input/transforms/convertStyleProps.test.ts +128 -0
- package/src/components/input/transforms/convertStyleProps.ts +12 -0
- package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.test.ts +186 -0
- package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.ts +27 -0
- package/src/components/input/transforms/inputLabelToLabelProp.test.ts +319 -0
- package/src/components/input/transforms/inputLabelToLabelProp.ts +203 -0
- package/src/components/input/transforms/mergeFieldIntoInput.test.ts +391 -0
- package/src/components/input/transforms/mergeFieldIntoInput.ts +213 -0
- package/src/components/input/transforms/mergeInputLabel.test.ts +458 -0
- package/src/components/input/transforms/mergeInputLabel.ts +204 -0
- package/src/components/input/transforms/moveInputImport.test.ts +166 -0
- package/src/components/input/transforms/moveInputImport.ts +14 -0
- package/src/components/input/transforms/numberFieldAddTypeNumber.test.ts +92 -0
- package/src/components/input/transforms/numberFieldAddTypeNumber.ts +14 -0
- package/src/components/input/transforms/numberFieldRenameToInput.test.ts +126 -0
- package/src/components/input/transforms/numberFieldRenameToInput.ts +9 -0
- package/src/components/input/transforms/removeAsInput.test.ts +139 -0
- package/src/components/input/transforms/removeAsInput.ts +20 -0
- package/src/components/input/transforms/removeDuplicateKeys.test.ts +302 -0
- package/src/components/input/transforms/removeDuplicateKeys.ts +10 -0
- package/src/components/input/transforms/removeInputBox.test.ts +352 -0
- package/src/components/input/transforms/removeInputBox.ts +109 -0
- package/src/components/input/transforms/removeRedundantAriaLabel.test.ts +128 -0
- package/src/components/input/transforms/removeRedundantAriaLabel.ts +21 -0
- package/src/components/input/transforms/removeTypeText.test.ts +160 -0
- package/src/components/input/transforms/removeTypeText.ts +18 -0
- package/src/components/input/transforms/sizeMapping.test.ts +198 -0
- package/src/components/input/transforms/sizeMapping.ts +17 -0
- package/src/components/input/transforms/skipRenderSideProps.test.ts +236 -0
- package/src/components/input/transforms/skipRenderSideProps.ts +27 -0
- package/src/components/input/transforms/stateToInvalid.test.ts +208 -0
- package/src/components/input/transforms/stateToInvalid.ts +59 -0
- package/src/components/input/transforms/stateToInvalidTernary.test.ts +159 -0
- package/src/components/input/transforms/stateToInvalidTernary.ts +13 -0
- package/src/components/input/transforms/unsupportedProps.test.ts +566 -0
- package/src/components/input/transforms/unsupportedProps.ts +84 -0
- package/src/components/link/transforms/reviewStyles.test.ts +0 -1
- package/src/components/shared/helpers/unsupportedPropsHelpers.ts +52 -0
- package/src/components/shared/transformFactories/helpers/manageImports.ts +14 -12
- package/src/components/shared/transformFactories/sizeMappingFactory.ts +9 -2
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +54 -16
- package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +65 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { JSXElement, 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 { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
7
|
+
import { orConditions } from "../../shared/conditions/orConditions"
|
|
8
|
+
import { extractTextContent } from "../../shared/helpers/childrenToLabelHelpers"
|
|
9
|
+
import { getImportName } from "../../shared/transformFactories/helpers/manageImports"
|
|
10
|
+
|
|
11
|
+
const transform: Transform = (fileInfo, api) => {
|
|
12
|
+
const j = api.jscodeshift
|
|
13
|
+
const source = j(fileInfo.source)
|
|
14
|
+
|
|
15
|
+
const inputLocalName = getImportName(
|
|
16
|
+
"Input",
|
|
17
|
+
"@planningcenter/tapestry-react",
|
|
18
|
+
{
|
|
19
|
+
j,
|
|
20
|
+
source,
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
if (!inputLocalName) return null
|
|
24
|
+
|
|
25
|
+
const boxLocalName = getImportName("Box", "@planningcenter/tapestry-react", {
|
|
26
|
+
j,
|
|
27
|
+
source,
|
|
28
|
+
})
|
|
29
|
+
const stackViewLocalName = getImportName(
|
|
30
|
+
"StackView",
|
|
31
|
+
"@planningcenter/tapestry-react",
|
|
32
|
+
{
|
|
33
|
+
j,
|
|
34
|
+
source,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if (!boxLocalName && !stackViewLocalName) return null
|
|
39
|
+
|
|
40
|
+
let hasChanges = false
|
|
41
|
+
|
|
42
|
+
source.find(j.JSXElement).forEach((path) => {
|
|
43
|
+
const parentEl = path.value
|
|
44
|
+
const openingElement = parentEl.openingElement
|
|
45
|
+
|
|
46
|
+
if (openingElement.name.type !== "JSXIdentifier") return
|
|
47
|
+
const parentName = openingElement.name.name
|
|
48
|
+
const isBox = boxLocalName && parentName === boxLocalName
|
|
49
|
+
const isStackView = stackViewLocalName && parentName === stackViewLocalName
|
|
50
|
+
if (!isBox && !isStackView) return
|
|
51
|
+
|
|
52
|
+
if (isStackView) {
|
|
53
|
+
const axisAttr = getAttribute({ element: parentEl, name: "axis" })
|
|
54
|
+
if (axisAttr) {
|
|
55
|
+
const axisValue = axisAttr.value
|
|
56
|
+
if (
|
|
57
|
+
axisValue &&
|
|
58
|
+
((axisValue.type === "StringLiteral" &&
|
|
59
|
+
axisValue.value === "horizontal") ||
|
|
60
|
+
(axisValue.type === "JSXExpressionContainer" &&
|
|
61
|
+
axisValue.expression.type === "StringLiteral" &&
|
|
62
|
+
axisValue.expression.value === "horizontal"))
|
|
63
|
+
) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const children = parentEl.children || []
|
|
70
|
+
let i = 0
|
|
71
|
+
|
|
72
|
+
while (i < children.length) {
|
|
73
|
+
const child = children[i]
|
|
74
|
+
|
|
75
|
+
if (child.type !== "JSXElement") {
|
|
76
|
+
i++
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const childOpening = child.openingElement
|
|
81
|
+
const childName = childOpening.name
|
|
82
|
+
|
|
83
|
+
if (childName.type !== "JSXMemberExpression") {
|
|
84
|
+
i++
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
const objectPart = childName.object
|
|
88
|
+
if (
|
|
89
|
+
objectPart.type !== "JSXIdentifier" ||
|
|
90
|
+
objectPart.name !== inputLocalName
|
|
91
|
+
) {
|
|
92
|
+
i++
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if (childName.property.name !== "InputLabel") {
|
|
96
|
+
i++
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Find the next JSXElement sibling (skip JSXText whitespace nodes)
|
|
101
|
+
let nextIdx = i + 1
|
|
102
|
+
while (
|
|
103
|
+
nextIdx < children.length &&
|
|
104
|
+
children[nextIdx].type === "JSXText"
|
|
105
|
+
) {
|
|
106
|
+
nextIdx++
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
nextIdx >= children.length ||
|
|
111
|
+
children[nextIdx].type !== "JSXElement"
|
|
112
|
+
) {
|
|
113
|
+
i++
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const inputEl = children[nextIdx] as JSXElement
|
|
118
|
+
const inputOpening = inputEl.openingElement
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
inputOpening.name.type !== "JSXIdentifier" ||
|
|
122
|
+
inputOpening.name.name !== inputLocalName
|
|
123
|
+
) {
|
|
124
|
+
i++
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Skip inputs with renderLeft/renderRight — they are not transformable
|
|
129
|
+
const hasRenderSide = orConditions(
|
|
130
|
+
hasAttribute("renderLeft"),
|
|
131
|
+
hasAttribute("renderRight")
|
|
132
|
+
)
|
|
133
|
+
if (hasRenderSide(inputEl)) {
|
|
134
|
+
i++
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { isSimpleText, textContent } = extractTextContent(
|
|
139
|
+
child.children || []
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
// Collect non-controls attrs from InputLabel to warn about
|
|
143
|
+
const inputLabelAttrs = childOpening.attributes || []
|
|
144
|
+
const removedProps: string[] = []
|
|
145
|
+
for (const attr of inputLabelAttrs) {
|
|
146
|
+
if (
|
|
147
|
+
attr.type === "JSXAttribute" &&
|
|
148
|
+
attr.name.type === "JSXIdentifier" &&
|
|
149
|
+
attr.name.name !== "controls"
|
|
150
|
+
) {
|
|
151
|
+
removedProps.push(attr.name.name)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
children.splice(i, nextIdx - i)
|
|
156
|
+
|
|
157
|
+
hasChanges = true
|
|
158
|
+
|
|
159
|
+
if (!isSimpleText) {
|
|
160
|
+
addComment({
|
|
161
|
+
element: inputEl,
|
|
162
|
+
j,
|
|
163
|
+
scope: "mergeInputLabel",
|
|
164
|
+
source,
|
|
165
|
+
text: "InputLabel children are complex and cannot be auto-converted to a label prop. Please migrate manually.",
|
|
166
|
+
})
|
|
167
|
+
} else {
|
|
168
|
+
const existingLabel = getAttribute({ element: inputEl, name: "label" })
|
|
169
|
+
if (existingLabel) {
|
|
170
|
+
addComment({
|
|
171
|
+
element: inputEl,
|
|
172
|
+
j,
|
|
173
|
+
scope: "mergeInputLabel",
|
|
174
|
+
source,
|
|
175
|
+
text: `Input already has a label prop. The InputLabel had text: "${textContent}". Please review manually.`,
|
|
176
|
+
})
|
|
177
|
+
} else {
|
|
178
|
+
addAttribute({
|
|
179
|
+
element: inputEl,
|
|
180
|
+
j,
|
|
181
|
+
name: "label",
|
|
182
|
+
value: textContent,
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (removedProps.length > 0) {
|
|
188
|
+
addComment({
|
|
189
|
+
element: inputEl,
|
|
190
|
+
j,
|
|
191
|
+
scope: "mergeInputLabel",
|
|
192
|
+
source,
|
|
193
|
+
text: `The following props from Input.InputLabel were removed: ${removedProps.join(", ")}. Please review manually.`,
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
i++
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
return hasChanges ? source.toSource() : null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export default transform
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./moveInputImport"
|
|
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("moveInputImport transform", () => {
|
|
19
|
+
describe("import migration", () => {
|
|
20
|
+
it("should change import from tapestry-react to tapestry", () => {
|
|
21
|
+
const input = `
|
|
22
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
23
|
+
|
|
24
|
+
function Test() {
|
|
25
|
+
return <Input label="Name" />
|
|
26
|
+
}
|
|
27
|
+
`.trim()
|
|
28
|
+
|
|
29
|
+
const result = applyTransform(input)
|
|
30
|
+
expect(result).toContain(
|
|
31
|
+
'import { Input } from "@planningcenter/tapestry"'
|
|
32
|
+
)
|
|
33
|
+
expect(result).not.toContain("@planningcenter/tapestry-react")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("should only move Input, leaving other imports in place", () => {
|
|
37
|
+
const input = `
|
|
38
|
+
import { Button, Input } from "@planningcenter/tapestry-react"
|
|
39
|
+
|
|
40
|
+
function Test() {
|
|
41
|
+
return (
|
|
42
|
+
<div>
|
|
43
|
+
<Button>Click</Button>
|
|
44
|
+
<Input label="Name" />
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
`.trim()
|
|
49
|
+
|
|
50
|
+
const result = applyTransform(input)
|
|
51
|
+
expect(result).toContain(
|
|
52
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
53
|
+
)
|
|
54
|
+
expect(result).toContain(
|
|
55
|
+
'import { Input } from "@planningcenter/tapestry"'
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("should handle Input as sole import in declaration", () => {
|
|
60
|
+
const input = `
|
|
61
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
62
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
63
|
+
|
|
64
|
+
function Test() {
|
|
65
|
+
return (
|
|
66
|
+
<div>
|
|
67
|
+
<Button>Click</Button>
|
|
68
|
+
<Input label="Name" />
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
`.trim()
|
|
73
|
+
|
|
74
|
+
const result = applyTransform(input)
|
|
75
|
+
expect(result).toContain(
|
|
76
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
77
|
+
)
|
|
78
|
+
expect(result).toContain(
|
|
79
|
+
'import { Input } from "@planningcenter/tapestry"'
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it("should not affect other components", () => {
|
|
84
|
+
const input = `
|
|
85
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
86
|
+
|
|
87
|
+
function Test() {
|
|
88
|
+
return <Button>Click me</Button>
|
|
89
|
+
}
|
|
90
|
+
`.trim()
|
|
91
|
+
|
|
92
|
+
const result = applyTransform(input)
|
|
93
|
+
expect(result).toContain(
|
|
94
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe("edge cases", () => {
|
|
100
|
+
it("should handle already migrated imports", () => {
|
|
101
|
+
const input = `
|
|
102
|
+
import { Input } from "@planningcenter/tapestry"
|
|
103
|
+
|
|
104
|
+
function Test() {
|
|
105
|
+
return <Input label="Name" />
|
|
106
|
+
}
|
|
107
|
+
`.trim()
|
|
108
|
+
|
|
109
|
+
const result = applyTransform(input)
|
|
110
|
+
expect(result).toContain(
|
|
111
|
+
'import { Input } from "@planningcenter/tapestry"'
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("should handle no imports", () => {
|
|
116
|
+
const input = `
|
|
117
|
+
function Test() {
|
|
118
|
+
return <div>No imports</div>
|
|
119
|
+
}
|
|
120
|
+
`.trim()
|
|
121
|
+
|
|
122
|
+
const result = applyTransform(input)
|
|
123
|
+
expect(result).toBe(input)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it("should preserve all attributes", () => {
|
|
127
|
+
const input = `
|
|
128
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
129
|
+
|
|
130
|
+
function Test() {
|
|
131
|
+
return (
|
|
132
|
+
<Input
|
|
133
|
+
label="Name"
|
|
134
|
+
placeholder="Enter name"
|
|
135
|
+
disabled
|
|
136
|
+
onChange={() => {}}
|
|
137
|
+
/>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
`.trim()
|
|
141
|
+
|
|
142
|
+
const result = applyTransform(input)
|
|
143
|
+
expect(result).toContain(
|
|
144
|
+
'import { Input } from "@planningcenter/tapestry"'
|
|
145
|
+
)
|
|
146
|
+
expect(result).toContain('label="Name"')
|
|
147
|
+
expect(result).toContain('placeholder="Enter name"')
|
|
148
|
+
expect(result).toContain("disabled")
|
|
149
|
+
expect(result).toContain("onChange={() => {}}")
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it("should handle alias import", () => {
|
|
153
|
+
const input = `
|
|
154
|
+
import { Input as TapestryInput } from "@planningcenter/tapestry-react"
|
|
155
|
+
|
|
156
|
+
function Test() {
|
|
157
|
+
return <TapestryInput label="Name" />
|
|
158
|
+
}
|
|
159
|
+
`.trim()
|
|
160
|
+
|
|
161
|
+
const result = applyTransform(input)
|
|
162
|
+
expect(result).toContain("@planningcenter/tapestry")
|
|
163
|
+
expect(result).not.toContain("@planningcenter/tapestry-react")
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { componentTransformFactory } from "../../shared/transformFactories/componentTransformFactory"
|
|
4
|
+
import { transformableInput } from "../transformableInput"
|
|
5
|
+
|
|
6
|
+
const transform: Transform = componentTransformFactory({
|
|
7
|
+
condition: transformableInput,
|
|
8
|
+
fromComponent: "Input",
|
|
9
|
+
fromPackage: "@planningcenter/tapestry-react",
|
|
10
|
+
toComponent: "Input",
|
|
11
|
+
toPackage: "@planningcenter/tapestry",
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export default transform
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./numberFieldAddTypeNumber"
|
|
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("numberFieldAddTypeNumber transform", () => {
|
|
18
|
+
it("adds type='number' to NumberField", () => {
|
|
19
|
+
const input = `
|
|
20
|
+
import { NumberField } from "@planningcenter/tapestry-react"
|
|
21
|
+
|
|
22
|
+
function Test() {
|
|
23
|
+
return <NumberField label="Amount" />
|
|
24
|
+
}
|
|
25
|
+
`.trim()
|
|
26
|
+
|
|
27
|
+
const result = applyTransform(input)
|
|
28
|
+
expect(result).not.toBeNull()
|
|
29
|
+
expect(result).toContain('type="number"')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("preserves other props when adding type", () => {
|
|
33
|
+
const input = `
|
|
34
|
+
import { NumberField } from "@planningcenter/tapestry-react"
|
|
35
|
+
|
|
36
|
+
function Test() {
|
|
37
|
+
return <NumberField label="Amount" value={42} onChange={handleChange} />
|
|
38
|
+
}
|
|
39
|
+
`.trim()
|
|
40
|
+
|
|
41
|
+
const result = applyTransform(input)
|
|
42
|
+
expect(result).toContain('type="number"')
|
|
43
|
+
expect(result).toContain('label="Amount"')
|
|
44
|
+
expect(result).toContain("value={42}")
|
|
45
|
+
expect(result).toContain("onChange={handleChange}")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("does not add type if already present", () => {
|
|
49
|
+
const input = `
|
|
50
|
+
import { NumberField } from "@planningcenter/tapestry-react"
|
|
51
|
+
|
|
52
|
+
function Test() {
|
|
53
|
+
return <NumberField label="Amount" type="number" />
|
|
54
|
+
}
|
|
55
|
+
`.trim()
|
|
56
|
+
|
|
57
|
+
const result = applyTransform(input)
|
|
58
|
+
expect(result).toBeNull()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("returns null when not imported from @planningcenter/tapestry-react", () => {
|
|
62
|
+
const input = `
|
|
63
|
+
import { NumberField } from "other-library"
|
|
64
|
+
|
|
65
|
+
function Test() {
|
|
66
|
+
return <NumberField label="Amount" />
|
|
67
|
+
}
|
|
68
|
+
`.trim()
|
|
69
|
+
|
|
70
|
+
expect(applyTransform(input)).toBeNull()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("handles multiple NumberField elements", () => {
|
|
74
|
+
const input = `
|
|
75
|
+
import { NumberField } from "@planningcenter/tapestry-react"
|
|
76
|
+
|
|
77
|
+
function Test() {
|
|
78
|
+
return (
|
|
79
|
+
<div>
|
|
80
|
+
<NumberField label="Qty" />
|
|
81
|
+
<NumberField label="Price" />
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
`.trim()
|
|
86
|
+
|
|
87
|
+
const result = applyTransform(input)
|
|
88
|
+
expect(result).not.toBeNull()
|
|
89
|
+
const matches = result!.match(/type="number"/g)
|
|
90
|
+
expect(matches).toHaveLength(2)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { addAttribute } from "../../shared/actions/addAttribute"
|
|
2
|
+
import { getAttribute } from "../../shared/actions/getAttribute"
|
|
3
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
4
|
+
|
|
5
|
+
export default attributeTransformFactory({
|
|
6
|
+
targetComponent: "NumberField",
|
|
7
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
8
|
+
transform: (element, { j }) => {
|
|
9
|
+
if (getAttribute({ element, name: "type" })) return false
|
|
10
|
+
|
|
11
|
+
addAttribute({ element, j, name: "type", value: "number" })
|
|
12
|
+
return true
|
|
13
|
+
},
|
|
14
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./numberFieldRenameToInput"
|
|
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("numberFieldRenameToInput transform", () => {
|
|
18
|
+
it("renames NumberField to Input", () => {
|
|
19
|
+
const input = `
|
|
20
|
+
import { NumberField } from "@planningcenter/tapestry-react"
|
|
21
|
+
|
|
22
|
+
function Test() {
|
|
23
|
+
return <NumberField label="Amount" type="number" />
|
|
24
|
+
}
|
|
25
|
+
`.trim()
|
|
26
|
+
|
|
27
|
+
const result = applyTransform(input)
|
|
28
|
+
expect(result).not.toBeNull()
|
|
29
|
+
expect(result).toContain("<Input")
|
|
30
|
+
expect(result).not.toContain("NumberField")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("updates import from tapestry-react to tapestry", () => {
|
|
34
|
+
const input = `
|
|
35
|
+
import { NumberField } from "@planningcenter/tapestry-react"
|
|
36
|
+
|
|
37
|
+
function Test() {
|
|
38
|
+
return <NumberField label="Amount" type="number" />
|
|
39
|
+
}
|
|
40
|
+
`.trim()
|
|
41
|
+
|
|
42
|
+
const result = applyTransform(input)
|
|
43
|
+
expect(result).toContain('@planningcenter/tapestry"')
|
|
44
|
+
expect(result).not.toContain("@planningcenter/tapestry-react")
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("returns null when not imported from @planningcenter/tapestry-react", () => {
|
|
48
|
+
const input = `
|
|
49
|
+
import { NumberField } from "other-library"
|
|
50
|
+
|
|
51
|
+
function Test() {
|
|
52
|
+
return <NumberField label="Amount" />
|
|
53
|
+
}
|
|
54
|
+
`.trim()
|
|
55
|
+
|
|
56
|
+
expect(applyTransform(input)).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("handles multiple NumberField elements", () => {
|
|
60
|
+
const input = `
|
|
61
|
+
import { NumberField } from "@planningcenter/tapestry-react"
|
|
62
|
+
|
|
63
|
+
function Test() {
|
|
64
|
+
return (
|
|
65
|
+
<div>
|
|
66
|
+
<NumberField label="Qty" type="number" />
|
|
67
|
+
<NumberField label="Price" type="number" />
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
`.trim()
|
|
72
|
+
|
|
73
|
+
const result = applyTransform(input)
|
|
74
|
+
expect(result).not.toBeNull()
|
|
75
|
+
expect(result).not.toContain("NumberField")
|
|
76
|
+
const inputMatches = result!.match(/<Input/g)
|
|
77
|
+
expect(inputMatches).toHaveLength(2)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("does not create duplicate Input identifier when file already imports Input from tapestry-react", () => {
|
|
81
|
+
const input = `
|
|
82
|
+
import { Input, NumberField } from "@planningcenter/tapestry-react"
|
|
83
|
+
|
|
84
|
+
function Test() {
|
|
85
|
+
return (
|
|
86
|
+
<div>
|
|
87
|
+
<Input label="Name" />
|
|
88
|
+
<NumberField label="Amount" type="number" />
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
`.trim()
|
|
93
|
+
|
|
94
|
+
const result = applyTransform(input)
|
|
95
|
+
expect(result).not.toBeNull()
|
|
96
|
+
expect(result).not.toContain("NumberField")
|
|
97
|
+
// Should not produce two separate Input declarations (would cause parse error)
|
|
98
|
+
const importDeclarationMatches = result!.match(/import.*Input/g)
|
|
99
|
+
expect(importDeclarationMatches).toHaveLength(1)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it("merges Input import when file already imports Input from tapestry", () => {
|
|
103
|
+
const input = `
|
|
104
|
+
import { Input } from "@planningcenter/tapestry"
|
|
105
|
+
import { NumberField } from "@planningcenter/tapestry-react"
|
|
106
|
+
|
|
107
|
+
function Test() {
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<Input label="Name" />
|
|
111
|
+
<NumberField label="Amount" type="number" />
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
`.trim()
|
|
116
|
+
|
|
117
|
+
const result = applyTransform(input)
|
|
118
|
+
expect(result).not.toBeNull()
|
|
119
|
+
expect(result).not.toContain("NumberField")
|
|
120
|
+
// Should not produce two separate Input imports
|
|
121
|
+
const importMatches = result!.match(
|
|
122
|
+
/import.*Input.*from.*@planningcenter\/tapestry/g
|
|
123
|
+
)
|
|
124
|
+
expect(importMatches).toHaveLength(1)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { componentTransformFactory } from "../../shared/transformFactories/componentTransformFactory"
|
|
2
|
+
|
|
3
|
+
export default componentTransformFactory({
|
|
4
|
+
condition: () => true,
|
|
5
|
+
fromComponent: "NumberField",
|
|
6
|
+
fromPackage: "@planningcenter/tapestry-react",
|
|
7
|
+
toComponent: "Input",
|
|
8
|
+
toPackage: "@planningcenter/tapestry",
|
|
9
|
+
})
|