@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,566 @@
|
|
|
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
|
+
describe("removed InputBox props", () => {
|
|
20
|
+
// renderLeft/renderRight are handled by skipRenderSideProps — unsupportedProps skips those elements
|
|
21
|
+
it("should return null for Input with renderLeft (handled by skipRenderSideProps)", () => {
|
|
22
|
+
const input = `
|
|
23
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
24
|
+
|
|
25
|
+
function Test() {
|
|
26
|
+
return <Input renderLeft={<Icon />} label="Name" />
|
|
27
|
+
}
|
|
28
|
+
`.trim()
|
|
29
|
+
|
|
30
|
+
const result = transform(
|
|
31
|
+
{ path: "test.tsx", source: input },
|
|
32
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
33
|
+
{}
|
|
34
|
+
)
|
|
35
|
+
expect(result).toBeNull()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("should return null for Input with renderRight (handled by skipRenderSideProps)", () => {
|
|
39
|
+
const input = `
|
|
40
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
41
|
+
|
|
42
|
+
function Test() {
|
|
43
|
+
return <Input renderRight={<Icon />} label="Name" />
|
|
44
|
+
}
|
|
45
|
+
`.trim()
|
|
46
|
+
|
|
47
|
+
const result = transform(
|
|
48
|
+
{ path: "test.tsx", source: input },
|
|
49
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
50
|
+
{}
|
|
51
|
+
)
|
|
52
|
+
expect(result).toBeNull()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("should flag isLoading", () => {
|
|
56
|
+
const input = `
|
|
57
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
58
|
+
|
|
59
|
+
function Test() {
|
|
60
|
+
return <Input isLoading label="Name" />
|
|
61
|
+
}
|
|
62
|
+
`.trim()
|
|
63
|
+
|
|
64
|
+
const result = applyTransform(input)
|
|
65
|
+
expect(result).toContain("TODO: tapestry-migration (isLoading)")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("should flag onClear", () => {
|
|
69
|
+
const input = `
|
|
70
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
71
|
+
|
|
72
|
+
function Test() {
|
|
73
|
+
return <Input onClear={handleClear} label="Name" />
|
|
74
|
+
}
|
|
75
|
+
`.trim()
|
|
76
|
+
|
|
77
|
+
const result = applyTransform(input)
|
|
78
|
+
expect(result).toContain("TODO: tapestry-migration (onClear)")
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("should flag naked", () => {
|
|
82
|
+
const input = `
|
|
83
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
84
|
+
|
|
85
|
+
function Test() {
|
|
86
|
+
return <Input naked label="Name" />
|
|
87
|
+
}
|
|
88
|
+
`.trim()
|
|
89
|
+
|
|
90
|
+
const result = applyTransform(input)
|
|
91
|
+
expect(result).toContain("TODO: tapestry-migration (naked)")
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("should flag subdued", () => {
|
|
95
|
+
const input = `
|
|
96
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
97
|
+
|
|
98
|
+
function Test() {
|
|
99
|
+
return <Input subdued label="Name" />
|
|
100
|
+
}
|
|
101
|
+
`.trim()
|
|
102
|
+
|
|
103
|
+
const result = applyTransform(input)
|
|
104
|
+
expect(result).toContain("TODO: tapestry-migration (subdued)")
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("should flag secondaryStroke", () => {
|
|
108
|
+
const input = `
|
|
109
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
110
|
+
|
|
111
|
+
function Test() {
|
|
112
|
+
return <Input secondaryStroke label="Name" />
|
|
113
|
+
}
|
|
114
|
+
`.trim()
|
|
115
|
+
|
|
116
|
+
const result = applyTransform(input)
|
|
117
|
+
expect(result).toContain("TODO: tapestry-migration (secondaryStroke)")
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("should flag evenPadding", () => {
|
|
121
|
+
const input = `
|
|
122
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
123
|
+
|
|
124
|
+
function Test() {
|
|
125
|
+
return <Input evenPadding label="Name" />
|
|
126
|
+
}
|
|
127
|
+
`.trim()
|
|
128
|
+
|
|
129
|
+
const result = applyTransform(input)
|
|
130
|
+
expect(result).toContain("TODO: tapestry-migration (evenPadding)")
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it("should flag focus style object", () => {
|
|
134
|
+
const input = `
|
|
135
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
136
|
+
|
|
137
|
+
function Test() {
|
|
138
|
+
return <Input focus={{ strokeWeight: 2 }} label="Name" />
|
|
139
|
+
}
|
|
140
|
+
`.trim()
|
|
141
|
+
|
|
142
|
+
const result = applyTransform(input)
|
|
143
|
+
expect(result).toContain("TODO: tapestry-migration (focus)")
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it("should flag hover style object", () => {
|
|
147
|
+
const input = `
|
|
148
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
149
|
+
|
|
150
|
+
function Test() {
|
|
151
|
+
return <Input hover={{ strokeWeight: 2 }} label="Name" />
|
|
152
|
+
}
|
|
153
|
+
`.trim()
|
|
154
|
+
|
|
155
|
+
const result = applyTransform(input)
|
|
156
|
+
expect(result).toContain("TODO: tapestry-migration (hover)")
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("should flag autoFocus", () => {
|
|
160
|
+
const input = `
|
|
161
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
162
|
+
|
|
163
|
+
function Test() {
|
|
164
|
+
return <Input autoFocus label="Name" />
|
|
165
|
+
}
|
|
166
|
+
`.trim()
|
|
167
|
+
|
|
168
|
+
const result = applyTransform(input)
|
|
169
|
+
expect(result).toContain("TODO: tapestry-migration (autoFocus)")
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it("should flag readOnlyBackgroundColor", () => {
|
|
173
|
+
const input = `
|
|
174
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
175
|
+
|
|
176
|
+
function Test() {
|
|
177
|
+
return <Input readOnlyBackgroundColor="gray" label="Name" />
|
|
178
|
+
}
|
|
179
|
+
`.trim()
|
|
180
|
+
|
|
181
|
+
const result = applyTransform(input)
|
|
182
|
+
expect(result).toContain(
|
|
183
|
+
"TODO: tapestry-migration (readOnlyBackgroundColor)"
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it("should add extra message for css prop", () => {
|
|
188
|
+
const input = `
|
|
189
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
190
|
+
|
|
191
|
+
function Test() {
|
|
192
|
+
return <Input css={{ color: "red" }} label="Name" />
|
|
193
|
+
}
|
|
194
|
+
`.trim()
|
|
195
|
+
|
|
196
|
+
const result = applyTransform(input)
|
|
197
|
+
expect(result).toContain("TODO: tapestry-migration (css)")
|
|
198
|
+
expect(result).toContain(
|
|
199
|
+
"CSS prop is not supported. Use className or style prop instead."
|
|
200
|
+
)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe("supported props — no comment added", () => {
|
|
205
|
+
it("should not flag supported Input props", () => {
|
|
206
|
+
const input = `
|
|
207
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
208
|
+
|
|
209
|
+
function Test() {
|
|
210
|
+
return (
|
|
211
|
+
<Input
|
|
212
|
+
label="Name"
|
|
213
|
+
description="Helper text"
|
|
214
|
+
invalid={false}
|
|
215
|
+
disabled
|
|
216
|
+
readOnly
|
|
217
|
+
required
|
|
218
|
+
placeholder="Enter name"
|
|
219
|
+
type="email"
|
|
220
|
+
autoWidth
|
|
221
|
+
selectTextOnFocus
|
|
222
|
+
size="md"
|
|
223
|
+
value=""
|
|
224
|
+
onChange={() => {}}
|
|
225
|
+
/>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
`.trim()
|
|
229
|
+
|
|
230
|
+
const result = applyTransform(input)
|
|
231
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it("should not flag standard HTML attributes", () => {
|
|
235
|
+
const input = `
|
|
236
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
237
|
+
|
|
238
|
+
function Test() {
|
|
239
|
+
return (
|
|
240
|
+
<Input
|
|
241
|
+
label="Name"
|
|
242
|
+
id="name-input"
|
|
243
|
+
name="name"
|
|
244
|
+
className="custom"
|
|
245
|
+
style={{ marginTop: 8 }}
|
|
246
|
+
tabIndex={0}
|
|
247
|
+
autoComplete="off"
|
|
248
|
+
defaultValue="foo"
|
|
249
|
+
min={0}
|
|
250
|
+
max={100}
|
|
251
|
+
minLength={2}
|
|
252
|
+
maxLength={50}
|
|
253
|
+
onFocus={() => {}}
|
|
254
|
+
onBlur={() => {}}
|
|
255
|
+
onKeyDown={() => {}}
|
|
256
|
+
onKeyUp={() => {}}
|
|
257
|
+
/>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
`.trim()
|
|
261
|
+
|
|
262
|
+
const result = applyTransform(input)
|
|
263
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it("should not flag step on a numeric input", () => {
|
|
267
|
+
const input = `
|
|
268
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
269
|
+
|
|
270
|
+
function Test() {
|
|
271
|
+
return <Input type="number" label="Amount" min={0} max={100} step={1} />
|
|
272
|
+
}
|
|
273
|
+
`.trim()
|
|
274
|
+
|
|
275
|
+
const result = applyTransform(input)
|
|
276
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it("should flag step on type='range' (not an accepted Input type)", () => {
|
|
280
|
+
const input = `
|
|
281
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
282
|
+
|
|
283
|
+
function Test() {
|
|
284
|
+
return <Input type="range" step={0.5} label="Amount" />
|
|
285
|
+
}
|
|
286
|
+
`.trim()
|
|
287
|
+
|
|
288
|
+
const result = applyTransform(input)
|
|
289
|
+
expect(result).toContain("TODO: tapestry-migration (step)")
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it("should flag step on type={'range'} expression container (not an accepted Input type)", () => {
|
|
293
|
+
const input = `
|
|
294
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
295
|
+
|
|
296
|
+
function Test() {
|
|
297
|
+
return <Input type={"range"} step={0.5} label="Amount" />
|
|
298
|
+
}
|
|
299
|
+
`.trim()
|
|
300
|
+
|
|
301
|
+
const result = applyTransform(input)
|
|
302
|
+
expect(result).toContain("TODO: tapestry-migration (step)")
|
|
303
|
+
expect(result).toContain("TODO: tapestry-migration (type)")
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it("should be permissive with type-specific props when type is dynamic", () => {
|
|
307
|
+
const input = `
|
|
308
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
309
|
+
|
|
310
|
+
function Test() {
|
|
311
|
+
return <Input type={inputType} step={0.5} label="Amount" />
|
|
312
|
+
}
|
|
313
|
+
`.trim()
|
|
314
|
+
|
|
315
|
+
const result = applyTransform(input)
|
|
316
|
+
expect(result).not.toContain("TODO: tapestry-migration (step)")
|
|
317
|
+
expect(result).not.toContain("TODO: tapestry-migration (type)")
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it("should flag accept on type='file' (not an accepted Input type)", () => {
|
|
321
|
+
const input = `
|
|
322
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
323
|
+
|
|
324
|
+
function Test() {
|
|
325
|
+
return <Input type="file" accept=".pdf" label="File" />
|
|
326
|
+
}
|
|
327
|
+
`.trim()
|
|
328
|
+
|
|
329
|
+
const result = applyTransform(input)
|
|
330
|
+
expect(result).toContain("TODO: tapestry-migration (accept)")
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it("should not flag pattern on type='text'", () => {
|
|
334
|
+
const input = `
|
|
335
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
336
|
+
|
|
337
|
+
function Test() {
|
|
338
|
+
return <Input type="text" pattern="[0-9]+" label="Number" />
|
|
339
|
+
}
|
|
340
|
+
`.trim()
|
|
341
|
+
|
|
342
|
+
const result = applyTransform(input)
|
|
343
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it("should not flag multiple on type='email'", () => {
|
|
347
|
+
const input = `
|
|
348
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
349
|
+
|
|
350
|
+
function Test() {
|
|
351
|
+
return <Input type="email" multiple label="Emails" />
|
|
352
|
+
}
|
|
353
|
+
`.trim()
|
|
354
|
+
|
|
355
|
+
const result = applyTransform(input)
|
|
356
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it("should not flag step when type is dynamic", () => {
|
|
360
|
+
const input = `
|
|
361
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
362
|
+
|
|
363
|
+
function Test() {
|
|
364
|
+
return <Input type={inputType} step={1} label="Amount" />
|
|
365
|
+
}
|
|
366
|
+
`.trim()
|
|
367
|
+
|
|
368
|
+
const result = applyTransform(input)
|
|
369
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it("should flag step without a type attr", () => {
|
|
373
|
+
const input = `
|
|
374
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
375
|
+
|
|
376
|
+
function Test() {
|
|
377
|
+
return <Input step={1} label="Amount" />
|
|
378
|
+
}
|
|
379
|
+
`.trim()
|
|
380
|
+
|
|
381
|
+
const result = applyTransform(input)
|
|
382
|
+
expect(result).toContain("TODO: tapestry-migration (step)")
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it("should flag step on type='text'", () => {
|
|
386
|
+
const input = `
|
|
387
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
388
|
+
|
|
389
|
+
function Test() {
|
|
390
|
+
return <Input type="text" step={1} label="Amount" />
|
|
391
|
+
}
|
|
392
|
+
`.trim()
|
|
393
|
+
|
|
394
|
+
const result = applyTransform(input)
|
|
395
|
+
expect(result).toContain("TODO: tapestry-migration (step)")
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it("should flag accept without a type attr", () => {
|
|
399
|
+
const input = `
|
|
400
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
401
|
+
|
|
402
|
+
function Test() {
|
|
403
|
+
return <Input accept=".pdf" label="File" />
|
|
404
|
+
}
|
|
405
|
+
`.trim()
|
|
406
|
+
|
|
407
|
+
const result = applyTransform(input)
|
|
408
|
+
expect(result).toContain("TODO: tapestry-migration (accept)")
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it("should not flag aria-* props", () => {
|
|
412
|
+
const input = `
|
|
413
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
414
|
+
|
|
415
|
+
function Test() {
|
|
416
|
+
return (
|
|
417
|
+
<Input
|
|
418
|
+
aria-label="Name"
|
|
419
|
+
aria-describedby="desc"
|
|
420
|
+
aria-labelledby="label"
|
|
421
|
+
label="Name"
|
|
422
|
+
/>
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
`.trim()
|
|
426
|
+
|
|
427
|
+
const result = applyTransform(input)
|
|
428
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it("should not flag data-* props", () => {
|
|
432
|
+
const input = `
|
|
433
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
434
|
+
|
|
435
|
+
function Test() {
|
|
436
|
+
return (
|
|
437
|
+
<Input
|
|
438
|
+
data-testid="name-input"
|
|
439
|
+
data-cy="name-field"
|
|
440
|
+
label="Name"
|
|
441
|
+
/>
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
`.trim()
|
|
445
|
+
|
|
446
|
+
const result = applyTransform(input)
|
|
447
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
448
|
+
})
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
describe("unsupported type values", () => {
|
|
452
|
+
it.each([
|
|
453
|
+
"range",
|
|
454
|
+
"date",
|
|
455
|
+
"time",
|
|
456
|
+
"datetime-local",
|
|
457
|
+
"week",
|
|
458
|
+
"month",
|
|
459
|
+
"file",
|
|
460
|
+
"color",
|
|
461
|
+
"checkbox",
|
|
462
|
+
"radio",
|
|
463
|
+
"hidden",
|
|
464
|
+
"image",
|
|
465
|
+
"submit",
|
|
466
|
+
"reset",
|
|
467
|
+
"button",
|
|
468
|
+
])("should flag type='%s' as unsupported", (type) => {
|
|
469
|
+
const input = `
|
|
470
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
471
|
+
|
|
472
|
+
function Test() {
|
|
473
|
+
return <Input type="${type}" label="Field" />
|
|
474
|
+
}
|
|
475
|
+
`.trim()
|
|
476
|
+
|
|
477
|
+
const result = applyTransform(input)
|
|
478
|
+
expect(result).toContain("TODO: tapestry-migration (type)")
|
|
479
|
+
expect(result).toContain("is not a supported Input type")
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it("should not flag accepted type values", () => {
|
|
483
|
+
const types = [
|
|
484
|
+
"email",
|
|
485
|
+
"number",
|
|
486
|
+
"password",
|
|
487
|
+
"search",
|
|
488
|
+
"tel",
|
|
489
|
+
"text",
|
|
490
|
+
"url",
|
|
491
|
+
]
|
|
492
|
+
for (const type of types) {
|
|
493
|
+
const input = `
|
|
494
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
495
|
+
|
|
496
|
+
function Test() {
|
|
497
|
+
return <Input type="${type}" label="Field" />
|
|
498
|
+
}
|
|
499
|
+
`.trim()
|
|
500
|
+
|
|
501
|
+
const result = applyTransform(input)
|
|
502
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
503
|
+
}
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
it("should not flag dynamic type values", () => {
|
|
507
|
+
const input = `
|
|
508
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
509
|
+
|
|
510
|
+
function Test() {
|
|
511
|
+
return <Input type={inputType} label="Field" />
|
|
512
|
+
}
|
|
513
|
+
`.trim()
|
|
514
|
+
|
|
515
|
+
const result = applyTransform(input)
|
|
516
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
describe("edge cases", () => {
|
|
521
|
+
it("should not affect Input with only supported props", () => {
|
|
522
|
+
const input = `
|
|
523
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
524
|
+
|
|
525
|
+
function Test() {
|
|
526
|
+
return <Input label="Name" />
|
|
527
|
+
}
|
|
528
|
+
`.trim()
|
|
529
|
+
|
|
530
|
+
const result = applyTransform(input)
|
|
531
|
+
expect(result).toBe(input)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
it("should not affect other components", () => {
|
|
535
|
+
const input = `
|
|
536
|
+
import { Input, Button } from "@planningcenter/tapestry-react"
|
|
537
|
+
|
|
538
|
+
function Test() {
|
|
539
|
+
return (
|
|
540
|
+
<div>
|
|
541
|
+
<Button naked>Naked button</Button>
|
|
542
|
+
<Input label="Name" />
|
|
543
|
+
</div>
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
`.trim()
|
|
547
|
+
|
|
548
|
+
const result = applyTransform(input)
|
|
549
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
550
|
+
expect(result).toContain("naked")
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it("should not transform if not imported from @planningcenter/tapestry-react", () => {
|
|
554
|
+
const input = `
|
|
555
|
+
import { Input } from "other-library"
|
|
556
|
+
|
|
557
|
+
function Test() {
|
|
558
|
+
return <Input renderLeft={<Icon />} label="Name" />
|
|
559
|
+
}
|
|
560
|
+
`.trim()
|
|
561
|
+
|
|
562
|
+
const result = applyTransform(input)
|
|
563
|
+
expect(result).toBe(input)
|
|
564
|
+
})
|
|
565
|
+
})
|
|
566
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { JSXAttribute, Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addCommentToUnsupportedProps } from "../../shared/actions/addCommentToUnsupportedProps"
|
|
4
|
+
import { getAttribute } from "../../shared/actions/getAttribute"
|
|
5
|
+
import { getAttributeValue } from "../../shared/actions/getAttributeValue"
|
|
6
|
+
import {
|
|
7
|
+
ACCEPTED_INPUT_TYPES,
|
|
8
|
+
INPUT_SUPPORTED_PROPS,
|
|
9
|
+
TYPE_SPECIFIC_PROPS,
|
|
10
|
+
} from "../../shared/helpers/unsupportedPropsHelpers"
|
|
11
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
12
|
+
import { transformableInput } from "../transformableInput"
|
|
13
|
+
|
|
14
|
+
const transform: Transform = attributeTransformFactory({
|
|
15
|
+
condition: transformableInput,
|
|
16
|
+
targetComponent: "Input",
|
|
17
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
18
|
+
transform: (element, { j }) => {
|
|
19
|
+
const attrs = element.openingElement.attributes || []
|
|
20
|
+
|
|
21
|
+
const typeAttr = getAttribute({ element, name: "type" })
|
|
22
|
+
|
|
23
|
+
const typeValue = typeAttr
|
|
24
|
+
? getAttributeValue({ attribute: typeAttr, j })
|
|
25
|
+
: null
|
|
26
|
+
|
|
27
|
+
// getAttributeValue resolves both type="text" and type={"text"} to "text",
|
|
28
|
+
// but returns source-code representations for truly dynamic expressions
|
|
29
|
+
// (e.g. type={someVar} → "{someVar}"). We need to distinguish static
|
|
30
|
+
// string values from dynamic ones so we can flag unsupported types without
|
|
31
|
+
// false-positiving on variables, and be permissive with type-specific props
|
|
32
|
+
// when the type can't be determined statically.
|
|
33
|
+
const isStaticType =
|
|
34
|
+
typeAttr?.value?.type === "StringLiteral" ||
|
|
35
|
+
(typeAttr?.value?.type === "JSXExpressionContainer" &&
|
|
36
|
+
typeAttr.value.expression.type === "StringLiteral")
|
|
37
|
+
|
|
38
|
+
const hasUnsupportedTypeValue =
|
|
39
|
+
isStaticType &&
|
|
40
|
+
typeValue !== null &&
|
|
41
|
+
!ACCEPTED_INPUT_TYPES.includes(
|
|
42
|
+
typeValue as (typeof ACCEPTED_INPUT_TYPES)[number]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const typeAllowedProps: string[] =
|
|
46
|
+
typeAttr === undefined
|
|
47
|
+
? []
|
|
48
|
+
: isStaticType
|
|
49
|
+
? (TYPE_SPECIFIC_PROPS[typeValue!] ?? [])
|
|
50
|
+
: Object.values(TYPE_SPECIFIC_PROPS).flat()
|
|
51
|
+
|
|
52
|
+
const UNSUPPORTED_PROPS = attrs
|
|
53
|
+
.filter(
|
|
54
|
+
(attr) =>
|
|
55
|
+
attr.type === "JSXAttribute" &&
|
|
56
|
+
!INPUT_SUPPORTED_PROPS.includes(attr.name.name as string) &&
|
|
57
|
+
!typeAllowedProps.includes(attr.name.name as string) &&
|
|
58
|
+
!(attr.name.name as string).startsWith("aria-") &&
|
|
59
|
+
!(attr.name.name as string).startsWith("data-")
|
|
60
|
+
)
|
|
61
|
+
.map((attr) => (attr as JSXAttribute).name.name as string)
|
|
62
|
+
|
|
63
|
+
if (hasUnsupportedTypeValue) {
|
|
64
|
+
UNSUPPORTED_PROPS.push("type")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return addCommentToUnsupportedProps({
|
|
68
|
+
element,
|
|
69
|
+
j,
|
|
70
|
+
messageSuffix: (prop) => {
|
|
71
|
+
if (prop === "css") {
|
|
72
|
+
return "\n * CSS prop is not supported. Use className or style prop instead.\n"
|
|
73
|
+
}
|
|
74
|
+
if (prop === "type") {
|
|
75
|
+
return `\n * '${typeValue}' is not a supported Input type. Accepted types: ${ACCEPTED_INPUT_TYPES.join(", ")}.\n`
|
|
76
|
+
}
|
|
77
|
+
return ""
|
|
78
|
+
},
|
|
79
|
+
props: UNSUPPORTED_PROPS,
|
|
80
|
+
})
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
export default transform
|
|
@@ -49,3 +49,55 @@ export const CHECKBOX_RADIO_SUPPORTED_PROPS = [
|
|
|
49
49
|
...CHECKBOX_RADIO_SHARED_PROPS,
|
|
50
50
|
...STYLE_PROP_NAMES_WITHOUT_CSS,
|
|
51
51
|
]
|
|
52
|
+
|
|
53
|
+
export const INPUT_SPECIFIC_PROPS = [
|
|
54
|
+
"autoComplete",
|
|
55
|
+
"autoWidth",
|
|
56
|
+
"defaultValue",
|
|
57
|
+
"description",
|
|
58
|
+
"disabled",
|
|
59
|
+
"form",
|
|
60
|
+
"hideLabel",
|
|
61
|
+
"invalid",
|
|
62
|
+
"max",
|
|
63
|
+
"maxLength",
|
|
64
|
+
"min",
|
|
65
|
+
"minLength",
|
|
66
|
+
"name",
|
|
67
|
+
"onChange",
|
|
68
|
+
"placeholder",
|
|
69
|
+
"readOnly",
|
|
70
|
+
"required",
|
|
71
|
+
"selectTextOnFocus",
|
|
72
|
+
"spellCheck",
|
|
73
|
+
"type",
|
|
74
|
+
"value",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
export const ACCEPTED_INPUT_TYPES = [
|
|
78
|
+
"email",
|
|
79
|
+
"number",
|
|
80
|
+
"password",
|
|
81
|
+
"search",
|
|
82
|
+
"tel",
|
|
83
|
+
"text",
|
|
84
|
+
"url",
|
|
85
|
+
] as const
|
|
86
|
+
|
|
87
|
+
// Per https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#attributes
|
|
88
|
+
// Only covers the types accepted by the Tapestry Input component.
|
|
89
|
+
export const TYPE_SPECIFIC_PROPS: Record<string, string[]> = {
|
|
90
|
+
email: ["dirname", "list", "multiple", "pattern"],
|
|
91
|
+
number: ["inputMode", "list", "step"],
|
|
92
|
+
password: ["pattern"],
|
|
93
|
+
search: ["dirname", "inputMode", "list", "pattern"],
|
|
94
|
+
tel: ["dirname", "inputMode", "list", "pattern"],
|
|
95
|
+
text: ["dirname", "inputMode", "list", "pattern"],
|
|
96
|
+
url: ["dirname", "inputMode", "list", "pattern"],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const INPUT_SUPPORTED_PROPS = [
|
|
100
|
+
...COMMON_PROPS,
|
|
101
|
+
...INPUT_SPECIFIC_PROPS,
|
|
102
|
+
...STYLE_PROP_NAMES_WITHOUT_CSS,
|
|
103
|
+
]
|