@planningcenter/tapestry-migration-cli 3.1.0-rc.9 → 3.1.0

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 (48) hide show
  1. package/dist/tapestry-react-shim.cjs +7 -1
  2. package/package.json +3 -3
  3. package/src/components/input/transformableInput.ts +47 -6
  4. package/src/components/input/transforms/mergeFieldIntoInput.test.ts +78 -0
  5. package/src/components/input/transforms/mergeFieldIntoInput.ts +6 -212
  6. package/src/components/input/transforms/removeDuplicateKeys.test.ts +3 -3
  7. package/src/components/input/transforms/removeTypeInput.ts +3 -3
  8. package/src/components/input/transforms/removeTypeText.ts +2 -3
  9. package/src/components/input/transforms/unsupportedProps.test.ts +20 -20
  10. package/src/components/select/index.ts +58 -0
  11. package/src/components/select/transformableSelect.ts +7 -0
  12. package/src/components/select/transforms/auditSpreadProps.test.ts +103 -0
  13. package/src/components/select/transforms/auditSpreadProps.ts +26 -0
  14. package/src/components/select/transforms/childrenToOptions.test.ts +367 -0
  15. package/src/components/select/transforms/childrenToOptions.ts +295 -0
  16. package/src/components/select/transforms/convertLegacyOptions.test.ts +150 -0
  17. package/src/components/select/transforms/convertLegacyOptions.ts +105 -0
  18. package/src/components/select/transforms/convertStyleProps.test.ts +73 -0
  19. package/src/components/select/transforms/convertStyleProps.ts +12 -0
  20. package/src/components/select/transforms/emptyValueToPlaceholder.test.ts +122 -0
  21. package/src/components/select/transforms/emptyValueToPlaceholder.ts +22 -0
  22. package/src/components/select/transforms/innerRefToRef.test.ts +89 -0
  23. package/src/components/select/transforms/innerRefToRef.ts +18 -0
  24. package/src/components/select/transforms/mapChildrenToOptions.test.ts +521 -0
  25. package/src/components/select/transforms/mapChildrenToOptions.ts +312 -0
  26. package/src/components/select/transforms/mergeFieldIntoSelect.test.ts +506 -0
  27. package/src/components/select/transforms/mergeFieldIntoSelect.ts +7 -0
  28. package/src/components/select/transforms/mergeSelectLabel.test.ts +458 -0
  29. package/src/components/select/transforms/mergeSelectLabel.ts +225 -0
  30. package/src/components/select/transforms/moveSelectImport.test.ts +148 -0
  31. package/src/components/select/transforms/moveSelectImport.ts +14 -0
  32. package/src/components/select/transforms/removeDefaultProps.test.ts +249 -0
  33. package/src/components/select/transforms/removeDefaultProps.ts +112 -0
  34. package/src/components/select/transforms/sizeMapping.test.ts +188 -0
  35. package/src/components/select/transforms/sizeMapping.ts +17 -0
  36. package/src/components/select/transforms/skipMultipleSelect.test.ts +148 -0
  37. package/src/components/select/transforms/skipMultipleSelect.ts +23 -0
  38. package/src/components/select/transforms/stateToInvalid.test.ts +217 -0
  39. package/src/components/select/transforms/stateToInvalid.ts +59 -0
  40. package/src/components/select/transforms/stateToInvalidTernary.test.ts +146 -0
  41. package/src/components/select/transforms/stateToInvalidTernary.ts +13 -0
  42. package/src/components/select/transforms/unsupportedProps.test.ts +252 -0
  43. package/src/components/select/transforms/unsupportedProps.ts +44 -0
  44. package/src/components/shared/helpers/getAttributeExpression.ts +26 -0
  45. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +19 -2
  46. package/src/components/shared/transformFactories/mergeFieldFactory.ts +244 -0
  47. package/src/components/text-area/transforms/mergeFieldIntoTextArea.ts +4 -226
  48. package/src/index.ts +2 -1
@@ -2917,6 +2917,7 @@ const tokens = {
2917
2917
  "--t-fill-color-interaction-hover": "hsl(204, 100%, 35%)",
2918
2918
  "--t-fill-color-interaction-active": "hsl(204, 100%, 30%)",
2919
2919
  "--t-fill-color-interaction-disabled": "hsl(0, 0%, 81%)",
2920
+ "--t-fill-color-control-neutral": "hsl(0, 0%, 58%)",
2920
2921
  "--t-fill-color-control-neutral-off": "hsl(0, 0%, 58%)",
2921
2922
  "--t-fill-color-control-neutral-on": "hsl(0, 0%, 24%)",
2922
2923
  "--t-fill-color-control": "hsl(204, 100%, 40%)",
@@ -3232,7 +3233,12 @@ const tokens = {
3232
3233
  "--t-form-font-color-error": "hsl(8, 60%, 45%)",
3233
3234
  "--t-form-font-color-readonly": "hsl(0, 0%, 24%)",
3234
3235
  "--t-form-picker-icon-color": "hsl(0, 0%, 24%)",
3235
- "--t-form-placeholder-color": "hsl(0, 0%, 58%)"
3236
+ "--t-form-placeholder-color": "hsl(0, 0%, 58%)",
3237
+ "--t-form-toggle-color": "hsl(0, 0%, 58%)",
3238
+ "--t-form-toggle-color-disabled": "hsl(0, 0%, 81%)",
3239
+ "--t-form-toggle-color-hover": "hsl(0, 0%, 50%)",
3240
+ "--t-form-toggle-color-on": "hsl(204, 100%, 40%)",
3241
+ "--t-form-toggle-color-on-hover": "hsl(204, 100%, 35%)"
3236
3242
  };
3237
3243
  function token(varName) {
3238
3244
  return `var(${varName})`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "3.1.0-rc.9",
3
+ "version": "3.1.0",
4
4
  "description": "CLI tool for Tapestry migrations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@emotion/react": "^11.14.0",
35
- "@planningcenter/tapestry": "^3.1.0-rc.9",
35
+ "@planningcenter/tapestry": "^3.1.0",
36
36
  "@planningcenter/tapestry-react": "^4.11.5",
37
37
  "@types/jscodeshift": "^17.3.0",
38
38
  "@types/node": "^20.0.0",
@@ -52,5 +52,5 @@
52
52
  "publishConfig": {
53
53
  "access": "public"
54
54
  },
55
- "gitHead": "c3dd0518ee1bbb86ffd8796a6d9604273306a7a0"
55
+ "gitHead": "3d173bf28e92e340ac45b1f08aae5ad64334e94b"
56
56
  }
@@ -1,8 +1,49 @@
1
- import { hasAttribute } from "../shared/conditions/hasAttribute"
2
- import { notCondition } from "../shared/conditions/notCondition"
3
- import { orConditions } from "../shared/conditions/orConditions"
1
+ import { JSXElement } from "jscodeshift"
2
+
3
+ import { ACCEPTED_INPUT_TYPES } from "../shared/helpers/unsupportedPropsHelpers"
4
4
  import { TransformCondition } from "../shared/types"
5
5
 
6
- export const transformableInput: TransformCondition = notCondition(
7
- orConditions(hasAttribute("renderLeft"), hasAttribute("renderRight"))
8
- )
6
+ // Include legacy "input" value so removeTypeInput can strip it
7
+ const acceptedTypes: readonly string[] = [...ACCEPTED_INPUT_TYPES, "input"]
8
+
9
+ function hasUnsupportedType(element: JSXElement): boolean {
10
+ const attributes = element.openingElement.attributes || []
11
+ const typeAttr = attributes.find(
12
+ (attr) =>
13
+ attr.type === "JSXAttribute" &&
14
+ attr.name?.type === "JSXIdentifier" &&
15
+ attr.name.name === "type"
16
+ )
17
+
18
+ if (!typeAttr || typeAttr.type !== "JSXAttribute") {
19
+ return false
20
+ }
21
+
22
+ if (typeAttr.value?.type === "StringLiteral") {
23
+ return !acceptedTypes.includes(typeAttr.value.value)
24
+ }
25
+
26
+ if (typeAttr.value?.type === "JSXExpressionContainer") {
27
+ const { expression } = typeAttr.value
28
+ if (expression.type === "StringLiteral") {
29
+ return !acceptedTypes.includes(expression.value)
30
+ }
31
+ }
32
+
33
+ return true
34
+ }
35
+
36
+ function hasAttribute(element: JSXElement, attributeName: string): boolean {
37
+ const attributes = element.openingElement.attributes || []
38
+ return attributes.some(
39
+ (attr) =>
40
+ attr.type === "JSXAttribute" &&
41
+ attr.name?.type === "JSXIdentifier" &&
42
+ attr.name.name === attributeName
43
+ )
44
+ }
45
+
46
+ export const transformableInput: TransformCondition = (element) =>
47
+ !hasAttribute(element, "renderLeft") &&
48
+ !hasAttribute(element, "renderRight") &&
49
+ !hasUnsupportedType(element)
@@ -305,6 +305,84 @@ function Test() {
305
305
  })
306
306
  })
307
307
 
308
+ describe("key prop handling", () => {
309
+ it("moves key from Field to Input when Input has no key", () => {
310
+ const input = `
311
+ import { Field, Input } from "@planningcenter/tapestry-react"
312
+
313
+ function Test() {
314
+ return (
315
+ <Box>
316
+ <Field key="item-1" label="Name"><Input /></Field>
317
+ </Box>
318
+ )
319
+ }
320
+ `.trim()
321
+
322
+ const result = applyTransform(input)
323
+ expect(result).not.toBeNull()
324
+ expect(result).not.toContain("<Field")
325
+ expect(result).toContain('key="item-1"')
326
+ expect(result).toContain("<Input")
327
+ })
328
+
329
+ it("keeps Input's key when both Field and Input have key", () => {
330
+ const input = `
331
+ import { Field, Input } from "@planningcenter/tapestry-react"
332
+
333
+ function Test() {
334
+ return (
335
+ <Box>
336
+ <Field key="field-1" label="Name"><Input key="input-1" /></Field>
337
+ </Box>
338
+ )
339
+ }
340
+ `.trim()
341
+
342
+ const result = applyTransform(input)
343
+ expect(result).not.toBeNull()
344
+ expect(result).not.toContain("<Field")
345
+ expect(result).toContain('key="input-1"')
346
+ })
347
+ })
348
+
349
+ describe("spread props on Field", () => {
350
+ it("bails out with TODO comment when Field has spread props", () => {
351
+ const input = `
352
+ import { Field, Input } from "@planningcenter/tapestry-react"
353
+
354
+ function Test() {
355
+ return (
356
+ <Field {...fieldProps}><Input /></Field>
357
+ )
358
+ }
359
+ `.trim()
360
+
361
+ const result = applyTransform(input)
362
+ expect(result).not.toBeNull()
363
+ expect(result).toContain("<Field")
364
+ expect(result).toContain("TODO: tapestry-migration (mergeFieldIntoInput)")
365
+ expect(result).toContain("spread props")
366
+ })
367
+
368
+ it("bails out when Field has spread props mixed with regular props", () => {
369
+ const input = `
370
+ import { Field, Input } from "@planningcenter/tapestry-react"
371
+
372
+ function Test() {
373
+ return (
374
+ <Field label="Name" {...fieldProps}><Input /></Field>
375
+ )
376
+ }
377
+ `.trim()
378
+
379
+ const result = applyTransform(input)
380
+ expect(result).not.toBeNull()
381
+ expect(result).toContain("<Field")
382
+ expect(result).toContain("spread props")
383
+ })
384
+ })
385
+
308
386
  describe("not from tapestry-react", () => {
309
387
  it("returns null when Field is not imported from tapestry-react", () => {
310
388
  const input = `
@@ -1,213 +1,7 @@
1
- import { JSXElement, JSXText, Transform } from "jscodeshift"
1
+ import { mergeFieldFactory } from "../../shared/transformFactories/mergeFieldFactory"
2
+ import { transformableInput } from "../transformableInput"
2
3
 
3
- import { addComment } from "../../shared/actions/addComment"
4
- import { hasAttribute } from "../../shared/conditions/hasAttribute"
5
- import { orConditions } from "../../shared/conditions/orConditions"
6
- import {
7
- getImportName,
8
- removeImportFromDeclaration,
9
- } from "../../shared/transformFactories/helpers/manageImports"
10
-
11
- const SCOPE = "mergeFieldIntoInput"
12
-
13
- const transform: Transform = (fileInfo, api) => {
14
- const j = api.jscodeshift
15
- const source = j(fileInfo.source)
16
-
17
- const fieldLocalName = getImportName(
18
- "Field",
19
- "@planningcenter/tapestry-react",
20
- { j, source }
21
- )
22
- if (!fieldLocalName) return null
23
-
24
- const inputLocalName = getImportName(
25
- "Input",
26
- "@planningcenter/tapestry-react",
27
- { j, source }
28
- )
29
- if (!inputLocalName) return null
30
-
31
- let hasChanges = false
32
- let anyFieldRemoved = false
33
-
34
- source.find(j.JSXElement).forEach((path) => {
35
- const el = path.value
36
- const opening = el.openingElement
37
-
38
- if (opening.name.type !== "JSXIdentifier") return
39
- if (opening.name.name !== fieldLocalName) return
40
-
41
- const elementChildren = (el.children || []).filter(
42
- (child) =>
43
- child.type !== "JSXText" || (child as JSXText).value.trim() !== ""
44
- )
45
-
46
- const inputChildren = elementChildren.filter((child) => {
47
- if (child.type !== "JSXElement") return false
48
- const childOpening = (child as JSXElement).openingElement
49
- return (
50
- childOpening.name.type === "JSXIdentifier" &&
51
- childOpening.name.name === inputLocalName
52
- )
53
- }) as JSXElement[]
54
-
55
- const hasRenderSide = orConditions(
56
- hasAttribute("renderLeft"),
57
- hasAttribute("renderRight")
58
- )
59
-
60
- // Case: exactly 1 child and it is an Input — merge props and unwrap
61
- if (elementChildren.length === 1 && inputChildren.length === 1) {
62
- const inputEl = inputChildren[0]
63
-
64
- // Skip inputs with renderLeft/renderRight — they are not transformable
65
- if (hasRenderSide(inputEl)) return
66
-
67
- const fieldAttrs = opening.attributes || []
68
-
69
- for (const attr of fieldAttrs) {
70
- if (attr.type !== "JSXAttribute") continue
71
- if (attr.name.type !== "JSXIdentifier") continue
72
-
73
- const attrName = attr.name.name
74
- const inputAttrs = inputEl.openingElement.attributes || []
75
-
76
- if (attrName === "label") {
77
- const hasLabel = inputAttrs.some(
78
- (a) =>
79
- a.type === "JSXAttribute" &&
80
- a.name?.type === "JSXIdentifier" &&
81
- a.name.name === "label"
82
- )
83
- if (hasLabel) {
84
- addComment({
85
- element: inputEl,
86
- j,
87
- scope: SCOPE,
88
- source,
89
- text: "Field had label prop but Input already has label. Please migrate manually.",
90
- })
91
- } else {
92
- inputEl.openingElement.attributes.push(attr)
93
- }
94
- } else if (attrName === "feedbackText") {
95
- const hasDescription = inputAttrs.some(
96
- (a) =>
97
- a.type === "JSXAttribute" &&
98
- a.name?.type === "JSXIdentifier" &&
99
- a.name.name === "description"
100
- )
101
- if (hasDescription) {
102
- addComment({
103
- element: inputEl,
104
- j,
105
- scope: SCOPE,
106
- source,
107
- text: "Field had feedbackText prop but Input already has description. Please migrate manually.",
108
- })
109
- } else {
110
- const newAttr = j.jsxAttribute(
111
- j.jsxIdentifier("description"),
112
- attr.value
113
- )
114
- inputEl.openingElement.attributes.push(newAttr)
115
- }
116
- } else if (attrName === "state") {
117
- const hasState = inputAttrs.some(
118
- (a) =>
119
- a.type === "JSXAttribute" &&
120
- a.name?.type === "JSXIdentifier" &&
121
- a.name.name === "state"
122
- )
123
- if (hasState) {
124
- addComment({
125
- element: inputEl,
126
- j,
127
- scope: SCOPE,
128
- source,
129
- text: "Field had state prop but Input already has state. Please migrate manually.",
130
- })
131
- } else {
132
- inputEl.openingElement.attributes.push(attr)
133
- }
134
- } else {
135
- addComment({
136
- element: inputEl,
137
- j,
138
- scope: SCOPE,
139
- source,
140
- text: `Field prop '${attrName}' is not supported by Input. Please migrate manually.`,
141
- })
142
- }
143
- }
144
-
145
- const parent = path.parent?.value
146
- if (parent?.children) {
147
- const idx = parent.children.indexOf(el)
148
- if (idx === -1) return
149
- parent.children.splice(idx, 1, ...(el.children || []))
150
- } else {
151
- // Root JSX (e.g. directly inside return parens) — use path.replace
152
- const nonWsChildren = (el.children || []).filter(
153
- (child) =>
154
- child.type !== "JSXText" || (child as JSXText).value.trim() !== ""
155
- )
156
- if (nonWsChildren.length === 1) {
157
- path.replace(nonWsChildren[0])
158
- } else {
159
- path.replace(
160
- j.jsxFragment(
161
- j.jsxOpeningFragment(),
162
- j.jsxClosingFragment(),
163
- el.children || []
164
- )
165
- )
166
- }
167
- }
168
- hasChanges = true
169
- anyFieldRemoved = true
170
- return
171
- }
172
-
173
- // Case: more than 1 non-whitespace child — comment each Input child, leave Field
174
- if (elementChildren.length > 1) {
175
- for (const child of inputChildren) {
176
- // Skip inputs with renderLeft/renderRight — they are not transformable
177
- if (hasRenderSide(child)) continue
178
- addComment({
179
- element: child,
180
- j,
181
- scope: SCOPE,
182
- source,
183
- text: "Field has multiple children and cannot be auto-merged into Input. Please migrate manually.",
184
- })
185
- hasChanges = true
186
- }
187
- return
188
- }
189
-
190
- // Case: exactly 1 child but not an Input — skip without comment
191
- })
192
-
193
- // Remove Field from imports only if all Field usages were converted
194
- if (anyFieldRemoved) {
195
- const stillUsesField =
196
- source.find(j.JSXOpeningElement, {
197
- name: { name: fieldLocalName },
198
- }).length > 0
199
-
200
- if (!stillUsesField) {
201
- const fieldImports = source.find(j.ImportDeclaration, {
202
- source: { value: "@planningcenter/tapestry-react" },
203
- })
204
- for (let i = 0; i < fieldImports.length; i++) {
205
- removeImportFromDeclaration(fieldImports.at(i), "Field")
206
- }
207
- }
208
- }
209
-
210
- return hasChanges ? source.toSource() : null
211
- }
212
-
213
- export default transform
4
+ export default mergeFieldFactory({
5
+ condition: transformableInput,
6
+ targetComponent: "Input",
7
+ })
@@ -195,7 +195,7 @@ describe("removeDuplicateKeys transform", () => {
195
195
  export function TestComponent() {
196
196
  return (
197
197
  <form>
198
- <Input type="submit" kind="primary" type="button">Submit</Input>
198
+ <Input type="email" kind="primary" type="text">Submit</Input>
199
199
  <Input disabled loading disabled>Loading</Input>
200
200
  <Input size="small">Small</Input>
201
201
  </form>
@@ -208,8 +208,8 @@ describe("removeDuplicateKeys transform", () => {
208
208
  expect(result).not.toBeNull()
209
209
 
210
210
  // First input: should keep first 'type'
211
- expect(result).toContain('type="submit"')
212
- expect(result).not.toContain('type="button"')
211
+ expect(result).toContain('type="email"')
212
+ expect(result).not.toContain('type="text"')
213
213
 
214
214
  expect(result).toContain("<Input disabled loading>")
215
215
 
@@ -7,12 +7,12 @@ import { attributeTransformFactory } from "../../shared/transformFactories/attri
7
7
  import { transformableInput } from "../transformableInput"
8
8
 
9
9
  const transform: Transform = attributeTransformFactory({
10
- condition: (element, context) =>
11
- transformableInput(element, context) &&
10
+ condition: (element) =>
11
+ transformableInput(element) &&
12
12
  orConditions(
13
13
  hasAttributeValue("type", "input"),
14
14
  hasAttributeValue("type", "text")
15
- )(element, context),
15
+ )(element),
16
16
  targetComponent: "Input",
17
17
  targetPackage: "@planningcenter/tapestry-react",
18
18
  transform: (element, { j, source }) =>
@@ -6,9 +6,8 @@ import { attributeTransformFactory } from "../../shared/transformFactories/attri
6
6
  import { transformableInput } from "../transformableInput"
7
7
 
8
8
  const transform: Transform = attributeTransformFactory({
9
- condition: (element, context) =>
10
- transformableInput(element, context) &&
11
- hasAttributeValue("type", "text")(element, context),
9
+ condition: (element) =>
10
+ transformableInput(element) && hasAttributeValue("type", "text")(element),
12
11
  targetComponent: "Input",
13
12
  targetPackage: "@planningcenter/tapestry-react",
14
13
  transform: (element, { j, source }) =>
@@ -276,7 +276,7 @@ function Test() {
276
276
  expect(result).not.toContain("TODO: tapestry-migration")
277
277
  })
278
278
 
279
- it("should flag step on type='range' (not an accepted Input type)", () => {
279
+ it("should not transform Input with type='range' (unsupported type bails)", () => {
280
280
  const input = `
281
281
  import { Input } from "@planningcenter/tapestry-react"
282
282
 
@@ -286,10 +286,10 @@ function Test() {
286
286
  `.trim()
287
287
 
288
288
  const result = applyTransform(input)
289
- expect(result).toContain("TODO: tapestry-migration (step)")
289
+ expect(result).toBe(input)
290
290
  })
291
291
 
292
- it("should flag step on type={'range'} expression container (not an accepted Input type)", () => {
292
+ it("should not transform Input with type={'range'} expression container (unsupported type bails)", () => {
293
293
  const input = `
294
294
  import { Input } from "@planningcenter/tapestry-react"
295
295
 
@@ -299,11 +299,10 @@ function Test() {
299
299
  `.trim()
300
300
 
301
301
  const result = applyTransform(input)
302
- expect(result).toContain("TODO: tapestry-migration (step)")
303
- expect(result).toContain("TODO: tapestry-migration (type)")
302
+ expect(result).toBe(input)
304
303
  })
305
304
 
306
- it("should be permissive with type-specific props when type is dynamic", () => {
305
+ it("should not transform Input with dynamic type in unsupported-types section (bails)", () => {
307
306
  const input = `
308
307
  import { Input } from "@planningcenter/tapestry-react"
309
308
 
@@ -313,11 +312,10 @@ function Test() {
313
312
  `.trim()
314
313
 
315
314
  const result = applyTransform(input)
316
- expect(result).not.toContain("TODO: tapestry-migration (step)")
317
- expect(result).not.toContain("TODO: tapestry-migration (type)")
315
+ expect(result).toBe(input)
318
316
  })
319
317
 
320
- it("should flag accept on type='file' (not an accepted Input type)", () => {
318
+ it("should not transform Input with type='file' (unsupported type bails)", () => {
321
319
  const input = `
322
320
  import { Input } from "@planningcenter/tapestry-react"
323
321
 
@@ -327,7 +325,7 @@ function Test() {
327
325
  `.trim()
328
326
 
329
327
  const result = applyTransform(input)
330
- expect(result).toContain("TODO: tapestry-migration (accept)")
328
+ expect(result).toBe(input)
331
329
  })
332
330
 
333
331
  it("should not flag pattern on type='text'", () => {
@@ -356,7 +354,7 @@ function Test() {
356
354
  expect(result).not.toContain("TODO: tapestry-migration")
357
355
  })
358
356
 
359
- it("should not flag step when type is dynamic", () => {
357
+ it("should not transform Input when type prop is dynamic (bails)", () => {
360
358
  const input = `
361
359
  import { Input } from "@planningcenter/tapestry-react"
362
360
 
@@ -366,7 +364,7 @@ function Test() {
366
364
  `.trim()
367
365
 
368
366
  const result = applyTransform(input)
369
- expect(result).not.toContain("TODO: tapestry-migration")
367
+ expect(result).toBe(input)
370
368
  })
371
369
 
372
370
  it("should flag step without a type attr", () => {
@@ -465,8 +463,10 @@ function Test() {
465
463
  "submit",
466
464
  "reset",
467
465
  "button",
468
- ])("should flag type='%s' as unsupported", (type) => {
469
- const input = `
466
+ ])(
467
+ "should not transform Input with type='%s' (unsupported type bails)",
468
+ (type) => {
469
+ const input = `
470
470
  import { Input } from "@planningcenter/tapestry-react"
471
471
 
472
472
  function Test() {
@@ -474,10 +474,10 @@ function Test() {
474
474
  }
475
475
  `.trim()
476
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
- })
477
+ const result = applyTransform(input)
478
+ expect(result).toBe(input)
479
+ }
480
+ )
481
481
 
482
482
  it("should not flag accepted type values", () => {
483
483
  const types = [
@@ -503,7 +503,7 @@ function Test() {
503
503
  }
504
504
  })
505
505
 
506
- it("should not flag dynamic type values", () => {
506
+ it("should not transform Input when type prop is dynamic (bails)", () => {
507
507
  const input = `
508
508
  import { Input } from "@planningcenter/tapestry-react"
509
509
 
@@ -513,7 +513,7 @@ function Test() {
513
513
  `.trim()
514
514
 
515
515
  const result = applyTransform(input)
516
- expect(result).not.toContain("TODO: tapestry-migration")
516
+ expect(result).toBe(input)
517
517
  })
518
518
  })
519
519
 
@@ -0,0 +1,58 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import auditSpreadProps from "./transforms/auditSpreadProps"
4
+ import childrenToOptions from "./transforms/childrenToOptions"
5
+ import convertLegacyOptions from "./transforms/convertLegacyOptions"
6
+ import convertStyleProps from "./transforms/convertStyleProps"
7
+ import emptyValueToPlaceholder from "./transforms/emptyValueToPlaceholder"
8
+ import innerRefToRef from "./transforms/innerRefToRef"
9
+ import mapChildrenToOptions from "./transforms/mapChildrenToOptions"
10
+ import mergeFieldIntoSelect from "./transforms/mergeFieldIntoSelect"
11
+ import mergeSelectLabel from "./transforms/mergeSelectLabel"
12
+ import moveSelectImport from "./transforms/moveSelectImport"
13
+ import removeDefaultProps from "./transforms/removeDefaultProps"
14
+ import sizeMapping from "./transforms/sizeMapping"
15
+ import skipMultipleSelect from "./transforms/skipMultipleSelect"
16
+ import stateToInvalid from "./transforms/stateToInvalid"
17
+ import stateToInvalidTernary from "./transforms/stateToInvalidTernary"
18
+ import unsupportedProps from "./transforms/unsupportedProps"
19
+
20
+ const transform: Transform = (fileInfo, api, options) => {
21
+ let currentSource = fileInfo.source
22
+ let hasAnyChanges = false
23
+
24
+ const transforms = [
25
+ skipMultipleSelect,
26
+ mergeSelectLabel,
27
+ mergeFieldIntoSelect,
28
+ auditSpreadProps,
29
+ mapChildrenToOptions,
30
+ childrenToOptions,
31
+ convertLegacyOptions,
32
+ emptyValueToPlaceholder,
33
+ innerRefToRef,
34
+ sizeMapping,
35
+ stateToInvalidTernary,
36
+ stateToInvalid,
37
+ removeDefaultProps,
38
+ convertStyleProps,
39
+ unsupportedProps,
40
+ moveSelectImport,
41
+ ]
42
+
43
+ for (const individualTransform of transforms) {
44
+ const result = individualTransform(
45
+ { ...fileInfo, source: currentSource },
46
+ api,
47
+ options
48
+ )
49
+ if (result && result !== currentSource) {
50
+ currentSource = result as string
51
+ hasAnyChanges = true
52
+ }
53
+ }
54
+
55
+ return hasAnyChanges ? currentSource : null
56
+ }
57
+
58
+ export default transform
@@ -0,0 +1,7 @@
1
+ import { hasAttribute } from "../shared/conditions/hasAttribute"
2
+ import { notCondition } from "../shared/conditions/notCondition"
3
+ import { TransformCondition } from "../shared/types"
4
+
5
+ export const transformableSelect: TransformCondition = notCondition(
6
+ hasAttribute("multiple")
7
+ )