@planningcenter/tapestry-migration-cli 3.2.2-rc.16 → 3.2.2-rc.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +3 -3
  2. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +24 -0
  3. package/src/components/shared/transformFactories/stylePropTransformFactory.test.ts +36 -0
  4. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +43 -10
  5. package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +17 -12
  6. package/src/components/time-field/index.ts +48 -0
  7. package/src/components/time-field/transforms/auditSpreadProps.test.ts +76 -0
  8. package/src/components/time-field/transforms/auditSpreadProps.ts +10 -0
  9. package/src/components/time-field/transforms/convertStyleProps.test.ts +43 -0
  10. package/src/components/time-field/transforms/convertStyleProps.ts +10 -0
  11. package/src/components/time-field/transforms/flagMinMax.test.ts +103 -0
  12. package/src/components/time-field/transforms/flagMinMax.ts +31 -0
  13. package/src/components/time-field/transforms/mergeFieldIntoTimeField.test.ts +106 -0
  14. package/src/components/time-field/transforms/mergeFieldIntoTimeField.ts +5 -0
  15. package/src/components/time-field/transforms/moveTimeFieldImport.test.ts +153 -0
  16. package/src/components/time-field/transforms/moveTimeFieldImport.ts +14 -0
  17. package/src/components/time-field/transforms/sizeMapping.test.ts +173 -0
  18. package/src/components/time-field/transforms/sizeMapping.ts +15 -0
  19. package/src/components/time-field/transforms/stateToInvalid.test.ts +87 -0
  20. package/src/components/time-field/transforms/stateToInvalid.ts +56 -0
  21. package/src/components/time-field/transforms/stateToInvalidTernary.test.ts +100 -0
  22. package/src/components/time-field/transforms/stateToInvalidTernary.ts +11 -0
  23. package/src/components/time-field/transforms/tupleToTime.test.ts +182 -0
  24. package/src/components/time-field/transforms/tupleToTime.ts +107 -0
  25. package/src/components/time-field/transforms/twelveHourClockToHourCycle.test.ts +117 -0
  26. package/src/components/time-field/transforms/twelveHourClockToHourCycle.ts +65 -0
  27. package/src/components/time-field/transforms/unsupportedProps.test.ts +160 -0
  28. package/src/components/time-field/transforms/unsupportedProps.ts +37 -0
  29. package/src/index.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "3.2.2-rc.16",
3
+ "version": "3.2.2-rc.17",
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.2.2-rc.16",
35
+ "@planningcenter/tapestry": "^3.2.2-rc.17",
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": "430054030b6df45cc7af3d09b9810ad2a2b08b0d"
55
+ "gitHead": "36eae38cab706a954312069d744ce5454b08c564"
56
56
  }
@@ -151,3 +151,27 @@ export const TEXTAREA_SUPPORTED_PROPS = [
151
151
  ...TEXTAREA_SPECIFIC_PROPS,
152
152
  ...STYLE_PROP_NAMES_WITHOUT_CSS,
153
153
  ]
154
+
155
+ export const TIME_FIELD_SPECIFIC_PROPS = [
156
+ "defaultValue",
157
+ "description",
158
+ "disabled",
159
+ "forceLeadingZeros",
160
+ "form",
161
+ "hideLabel",
162
+ "hideTimeZone",
163
+ "hourCycle",
164
+ "invalid",
165
+ "max",
166
+ "min",
167
+ "name",
168
+ "onChange",
169
+ "readOnly",
170
+ "required",
171
+ "value",
172
+ ]
173
+
174
+ export const TIME_FIELD_SUPPORTED_PROPS = [
175
+ ...COMMON_PROPS,
176
+ ...TIME_FIELD_SPECIFIC_PROPS,
177
+ ]
@@ -1,6 +1,7 @@
1
1
  import jscodeshift from "jscodeshift"
2
2
  import { describe, expect, it } from "vitest"
3
3
 
4
+ import { stackViewPlugin } from "../../../stubs/stackViewPlugin"
4
5
  import { stylePropTransformFactory } from "./stylePropTransformFactory"
5
6
 
6
7
  const j = jscodeshift.withParser("tsx")
@@ -291,4 +292,39 @@ describe("stylePropTransformFactory", () => {
291
292
  `)
292
293
  })
293
294
  })
295
+
296
+ describe("non-inlineable plugin output", () => {
297
+ // stackViewPlugin maps `distribution: "fill"` to `{ "& > *": { flex: "1 0 0px" } }`,
298
+ // a nested CSS selector that cannot be represented as a React `style={}` object.
299
+ // The transform must not crash, must not silently drop the source props, and must
300
+ // leave an audit trail for manual review.
301
+ const transform = stylePropTransformFactory({
302
+ plugin: stackViewPlugin,
303
+ stylesToRemove: [],
304
+ targetComponent: "Stack",
305
+ targetPackage: "@planningcenter/tapestry-react",
306
+ })
307
+
308
+ it("preserves source props and emits an audit comment when plugin output includes nested CSS selectors", () => {
309
+ const input = `import { Stack } from "@planningcenter/tapestry-react";<Stack axis="horizontal" spacing={1} distribution="fill" />`
310
+ const result = applyTransform(transform, input)
311
+
312
+ // Source props must remain — they cannot be safely inlined.
313
+ expect(result).toContain('axis="horizontal"')
314
+ expect(result).toContain("spacing={1}")
315
+ expect(result).toContain('distribution="fill"')
316
+ // Must not have produced a broken inline style with a nested object.
317
+ expect(result).not.toContain('"& > *"')
318
+ // Must leave a visible audit trail so the user knows to review.
319
+ expect(result).toMatch(/TODO:\s*tapestry-migration/)
320
+ })
321
+
322
+ it("does not crash on distribution='fill' alongside other plugin props", () => {
323
+ const input = `import { Stack } from "@planningcenter/tapestry-react";<Stack distribution="fill" axis="horizontal" />`
324
+ const result = applyTransform(transform, input)
325
+
326
+ expect(result).toContain('distribution="fill"')
327
+ expect(result).toContain('axis="horizontal"')
328
+ })
329
+ })
294
330
  })
@@ -110,21 +110,19 @@ function reviewBody(entry: ManualReviewEntry) {
110
110
  function processKeepStyleProps({
111
111
  attributes,
112
112
  j,
113
- element,
114
- source,
115
113
  }: {
116
114
  attributes: JSXAttribute[]
117
- element: JSXElement
118
115
  j: JSCodeshift
119
- source: Collection
120
116
  }): {
121
117
  directProps: Record<string, unknown>
122
118
  manualReviewEntries: ManualReviewEntry[]
119
+ processedNames: string[]
123
120
  themeProps: Record<string, unknown>
124
121
  } {
125
122
  const themeProps: Record<string, unknown> = {}
126
123
  const directProps: Record<string, unknown> = {}
127
124
  const manualReviewEntries: ManualReviewEntry[] = []
125
+ const processedNames: string[] = []
128
126
 
129
127
  attributes.forEach((attr) => {
130
128
  const propName = attr.name.name as string
@@ -148,10 +146,19 @@ function processKeepStyleProps({
148
146
  themeProps[propName] = propValue
149
147
  }
150
148
 
151
- removeAttribute(propName, { element, j, source })
149
+ processedNames.push(propName)
152
150
  })
153
151
 
154
- return { directProps, manualReviewEntries, themeProps }
152
+ return { directProps, manualReviewEntries, processedNames, themeProps }
153
+ }
154
+
155
+ function isInlineableStyleValue(value: unknown): boolean {
156
+ return (
157
+ value === null ||
158
+ typeof value === "string" ||
159
+ typeof value === "number" ||
160
+ typeof value === "boolean"
161
+ )
155
162
  }
156
163
 
157
164
  function processRemoveStyleProps({
@@ -395,9 +402,7 @@ export function stylePropTransformFactory(config: {
395
402
  )
396
403
  const keepResult = processKeepStyleProps({
397
404
  attributes: keepAttributes,
398
- element,
399
405
  j,
400
- source,
401
406
  })
402
407
  allStyleProps = {
403
408
  ...allStyleProps,
@@ -450,7 +455,32 @@ export function stylePropTransformFactory(config: {
450
455
  styles = { ...styles, ...directStyleProps }
451
456
  if (options.verbose) console.log("Final generated styles:", styles)
452
457
 
453
- if (Object.keys(styles).length > 0) {
458
+ // Plugins like stackViewPlugin can emit nested CSS selectors (e.g.
459
+ // `{ "& > *": { flex: "1 0 0px" } }` from `distribution: "fill"`)
460
+ // that can't be represented in React's inline `style={}`. Preserve
461
+ // the source props so nothing is silently dropped and flag for review.
462
+ const nonInlineableKeys = Object.entries(styles)
463
+ .filter(([, v]) => !isInlineableStyleValue(v))
464
+ .map(([k]) => k)
465
+ if (nonInlineableKeys.length > 0) {
466
+ addComment({
467
+ element,
468
+ j,
469
+ scope: "styleProp",
470
+ source,
471
+ text: `Could not migrate style props (${keepResult.processedNames.join(
472
+ ", "
473
+ )}) — produced non-inlineable style values (${nonInlineableKeys.join(
474
+ ", "
475
+ )}) that don't fit in inline style (for example, nested selectors). Manual review required.`,
476
+ })
477
+ } else {
478
+ for (const propName of keepResult.processedNames) {
479
+ removeAttribute(propName, { element, j, source })
480
+ }
481
+ }
482
+
483
+ if (nonInlineableKeys.length === 0 && Object.keys(styles).length > 0) {
454
484
  applyStylesToComponent({
455
485
  element,
456
486
  j,
@@ -477,7 +507,10 @@ export function stylePropTransformFactory(config: {
477
507
  }
478
508
  }
479
509
  }
480
- } else if (manualReviewEntries.length > 0) {
510
+ } else if (
511
+ nonInlineableKeys.length === 0 &&
512
+ manualReviewEntries.length > 0
513
+ ) {
481
514
  // No anchoring property in the style block — emit each manual-review
482
515
  // entry as a JSX-level TODO comment instead.
483
516
  for (const entry of manualReviewEntries) {
@@ -35,27 +35,32 @@ export function ternaryConditionalToPropFactory({
35
35
  const expr = attr.value.expression
36
36
  if (expr.type !== "ConditionalExpression") return false
37
37
 
38
- if (
39
- expr.consequent.type !== "StringLiteral" ||
40
- expr.consequent.value !== matchValue
41
- ) {
42
- return false
43
- }
38
+ const isNullish = (node: Expression) =>
39
+ node.type === "NullLiteral" ||
40
+ (node.type === "Identifier" && node.name === "undefined")
44
41
 
45
- const isNullAlternate = expr.alternate.type === "NullLiteral"
46
- const isUndefinedAlternate =
47
- expr.alternate.type === "Identifier" &&
48
- expr.alternate.name === "undefined"
42
+ const isMatchString = (node: Expression) =>
43
+ node.type === "StringLiteral" && node.value === matchValue
49
44
 
50
- if (!isNullAlternate && !isUndefinedAlternate) return false
45
+ const matchInConsequent =
46
+ isMatchString(expr.consequent as Expression) &&
47
+ isNullish(expr.alternate as Expression)
48
+ const matchInAlternate =
49
+ isMatchString(expr.alternate as Expression) &&
50
+ isNullish(expr.consequent as Expression)
51
+
52
+ if (!matchInConsequent && !matchInAlternate) return false
51
53
 
52
54
  const testExpr = expr.test as Expression
55
+ const propValue = matchInConsequent
56
+ ? testExpr
57
+ : j.unaryExpression("!", testExpr)
53
58
 
54
59
  removeAttribute(fromProp, { element, j, source })
55
60
  element.openingElement.attributes.push(
56
61
  j.jsxAttribute(
57
62
  j.jsxIdentifier(toProp),
58
- j.jsxExpressionContainer(testExpr)
63
+ j.jsxExpressionContainer(propValue)
59
64
  )
60
65
  )
61
66
 
@@ -0,0 +1,48 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import auditSpreadProps from "./transforms/auditSpreadProps"
4
+ import convertStyleProps from "./transforms/convertStyleProps"
5
+ import flagMinMax from "./transforms/flagMinMax"
6
+ import mergeFieldIntoTimeField from "./transforms/mergeFieldIntoTimeField"
7
+ import moveTimeFieldImport from "./transforms/moveTimeFieldImport"
8
+ import sizeMapping from "./transforms/sizeMapping"
9
+ import stateToInvalid from "./transforms/stateToInvalid"
10
+ import stateToInvalidTernary from "./transforms/stateToInvalidTernary"
11
+ import tupleToTime from "./transforms/tupleToTime"
12
+ import twelveHourClockToHourCycle from "./transforms/twelveHourClockToHourCycle"
13
+ import unsupportedProps from "./transforms/unsupportedProps"
14
+
15
+ const transform: Transform = (fileInfo, api, options) => {
16
+ let currentSource = fileInfo.source
17
+ let hasAnyChanges = false
18
+
19
+ const transforms = [
20
+ mergeFieldIntoTimeField,
21
+ auditSpreadProps,
22
+ sizeMapping,
23
+ stateToInvalidTernary,
24
+ stateToInvalid,
25
+ twelveHourClockToHourCycle,
26
+ tupleToTime,
27
+ flagMinMax,
28
+ convertStyleProps,
29
+ unsupportedProps,
30
+ moveTimeFieldImport,
31
+ ]
32
+
33
+ for (const individualTransform of transforms) {
34
+ const result = individualTransform(
35
+ { ...fileInfo, source: currentSource },
36
+ api,
37
+ options
38
+ )
39
+ if (result && result !== currentSource) {
40
+ currentSource = result as string
41
+ hasAnyChanges = true
42
+ }
43
+ }
44
+
45
+ return hasAnyChanges ? currentSource : null
46
+ }
47
+
48
+ export default transform
@@ -0,0 +1,76 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./auditSpreadProps"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ const AUDIT_COMMENT =
9
+ "TODO: tapestry-migration (spreadAttribute): Spread props can contain unsupported props, please explore usages and migrate as needed."
10
+
11
+ function applyTransform(source: string) {
12
+ const fileInfo = { path: "test.tsx", source }
13
+ return transform(
14
+ fileInfo,
15
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
16
+ {}
17
+ ) as string | null
18
+ }
19
+
20
+ describe("auditSpreadProps transform", () => {
21
+ it("adds comment to TimeField with spread props", () => {
22
+ const input = `
23
+ import { TimeField } from "@planningcenter/tapestry-react"
24
+
25
+ export default function Test() {
26
+ const props = { onChange: handleChange }
27
+ return <TimeField {...props} value={time} onChange={setTime} />
28
+ }
29
+ `.trim()
30
+
31
+ const result = applyTransform(input)
32
+ expect(result).toContain(AUDIT_COMMENT)
33
+ expect(result).toContain("{...props}")
34
+ })
35
+
36
+ it("returns null when TimeField has no spread props", () => {
37
+ const input = `
38
+ import { TimeField } from "@planningcenter/tapestry-react"
39
+
40
+ export default function Test() {
41
+ return <TimeField value={time} onChange={setTime} />
42
+ }
43
+ `.trim()
44
+
45
+ const result = applyTransform(input)
46
+ expect(result).toBe(null)
47
+ })
48
+
49
+ it("returns null when TimeField is from another package", () => {
50
+ const input = `
51
+ import { TimeField } from "other-library"
52
+
53
+ export default function Test() {
54
+ const props = { onChange: handleChange }
55
+ return <TimeField {...props} value={time} />
56
+ }
57
+ `.trim()
58
+
59
+ const result = applyTransform(input)
60
+ expect(result).toBe(null)
61
+ })
62
+
63
+ it("returns null when TimeField is not imported at all", () => {
64
+ const input = `
65
+ import { Button } from "@planningcenter/tapestry-react"
66
+
67
+ export default function Test() {
68
+ const props = { onClick: handleClick }
69
+ return <Button {...props}>Save</Button>
70
+ }
71
+ `.trim()
72
+
73
+ const result = applyTransform(input)
74
+ expect(result).toBe(null)
75
+ })
76
+ })
@@ -0,0 +1,10 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { commentOnSpreadPropsFactory } from "../../shared/transformFactories/commentOnSpreadPropsFactory"
4
+
5
+ const transform: Transform = commentOnSpreadPropsFactory({
6
+ targetComponent: "TimeField",
7
+ targetPackage: "@planningcenter/tapestry-react",
8
+ })
9
+
10
+ export default transform
@@ -0,0 +1,43 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./convertStyleProps"
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("convertStyleProps transform", () => {
18
+ it("returns null when TimeField has no style props", () => {
19
+ const input = `
20
+ import { TimeField } from "@planningcenter/tapestry-react"
21
+
22
+ function Test() {
23
+ return <TimeField value={time} onChange={setTime} />
24
+ }
25
+ `.trim()
26
+
27
+ const result = applyTransform(input)
28
+ expect(result).toBeNull()
29
+ })
30
+
31
+ it("does not affect TimeField from other packages", () => {
32
+ const input = `
33
+ import { TimeField } from "some-other-lib"
34
+
35
+ function Test() {
36
+ return <TimeField p={2} value={time} onChange={setTime} />
37
+ }
38
+ `.trim()
39
+
40
+ const result = applyTransform(input)
41
+ expect(result).toBeNull()
42
+ })
43
+ })
@@ -0,0 +1,10 @@
1
+ import { stackViewPlugin } from "../../../stubs/stackViewPlugin"
2
+ import { stylePropTransformFactory } from "../../shared/transformFactories/stylePropTransformFactory"
3
+
4
+ export default stylePropTransformFactory({
5
+ plugin: stackViewPlugin,
6
+ stylesToKeep: [],
7
+ stylesToRemove: [],
8
+ targetComponent: "TimeField",
9
+ targetPackage: "@planningcenter/tapestry-react",
10
+ })
@@ -0,0 +1,103 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./flagMinMax"
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("flagMinMax transform", () => {
18
+ it("adds TODO comment to min prop", () => {
19
+ const input = `
20
+ import { TimeField } from "@planningcenter/tapestry-react"
21
+
22
+ function Test() {
23
+ return <TimeField min={9} value={time} onChange={setTime} />
24
+ }
25
+ `.trim()
26
+
27
+ const result = applyTransform(input)
28
+ expect(result).not.toBeNull()
29
+ expect(result).toContain("TODO: tapestry-migration (min)")
30
+ expect(result).toContain("TimeValue")
31
+ expect(result).toContain("min={9}")
32
+ })
33
+
34
+ it("adds TODO comment to max prop", () => {
35
+ const input = `
36
+ import { TimeField } from "@planningcenter/tapestry-react"
37
+
38
+ function Test() {
39
+ return <TimeField max={17} value={time} onChange={setTime} />
40
+ }
41
+ `.trim()
42
+
43
+ const result = applyTransform(input)
44
+ expect(result).not.toBeNull()
45
+ expect(result).toContain("TODO: tapestry-migration (max)")
46
+ expect(result).toContain("TimeValue")
47
+ expect(result).toContain("max={17}")
48
+ })
49
+
50
+ it("flags both min and max when present", () => {
51
+ const input = `
52
+ import { TimeField } from "@planningcenter/tapestry-react"
53
+
54
+ function Test() {
55
+ return <TimeField min={9} max={17} value={time} onChange={setTime} />
56
+ }
57
+ `.trim()
58
+
59
+ const result = applyTransform(input)
60
+ expect(result).not.toBeNull()
61
+ expect(result).toContain("TODO: tapestry-migration (min)")
62
+ expect(result).toContain("TODO: tapestry-migration (max)")
63
+ })
64
+
65
+ it("returns null when neither min nor max is present", () => {
66
+ const input = `
67
+ import { TimeField } from "@planningcenter/tapestry-react"
68
+
69
+ function Test() {
70
+ return <TimeField value={time} onChange={setTime} />
71
+ }
72
+ `.trim()
73
+
74
+ const result = applyTransform(input)
75
+ expect(result).toBeNull()
76
+ })
77
+
78
+ it("does not affect other components", () => {
79
+ const input = `
80
+ import { Input } from "@planningcenter/tapestry-react"
81
+
82
+ function Test() {
83
+ return <Input min={1} max={10} label="Count" />
84
+ }
85
+ `.trim()
86
+
87
+ const result = applyTransform(input)
88
+ expect(result).toBeNull()
89
+ })
90
+
91
+ it("does not affect TimeField from other packages", () => {
92
+ const input = `
93
+ import { TimeField } from "some-other-lib"
94
+
95
+ function Test() {
96
+ return <TimeField min={9} max={17} value={time} onChange={setTime} />
97
+ }
98
+ `.trim()
99
+
100
+ const result = applyTransform(input)
101
+ expect(result).toBeNull()
102
+ })
103
+ })
@@ -0,0 +1,31 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
4
+ import { getAttribute } from "../../shared/actions/getAttribute"
5
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
6
+
7
+ const TIME_PROPS = ["min", "max"]
8
+
9
+ const transform: Transform = attributeTransformFactory({
10
+ targetComponent: "TimeField",
11
+ targetPackage: "@planningcenter/tapestry-react",
12
+ transform: (element, { j }) => {
13
+ let hasChanges = false
14
+
15
+ for (const propName of TIME_PROPS) {
16
+ const attr = getAttribute({ element, name: propName })
17
+ if (!attr) continue
18
+
19
+ addCommentToAttribute({
20
+ attribute: attr,
21
+ j,
22
+ text: `'${propName}' previously accepted an hour number (0–23). TimeField now expects a TimeValue (e.g. new Time(hour, minute)) from @internationalized/date. Migrate this value manually.`,
23
+ })
24
+ hasChanges = true
25
+ }
26
+
27
+ return hasChanges
28
+ },
29
+ })
30
+
31
+ export default transform
@@ -0,0 +1,106 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./mergeFieldIntoTimeField"
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("mergeFieldIntoTimeField transform", () => {
18
+ it("merges Field label into TimeField and unwraps Field", () => {
19
+ const input = `
20
+ import { Field, TimeField } from "@planningcenter/tapestry-react"
21
+
22
+ function Test() {
23
+ return (
24
+ <Field label="Start time"><TimeField value={time} onChange={setTime} /></Field>
25
+ )
26
+ }
27
+ `.trim()
28
+
29
+ const result = applyTransform(input)
30
+ expect(result).not.toBeNull()
31
+ expect(result).not.toContain("<Field")
32
+ expect(result).toContain('label="Start time"')
33
+ expect(result).toContain("<TimeField")
34
+ })
35
+
36
+ it("renames feedbackText to description on TimeField", () => {
37
+ const input = `
38
+ import { Field, TimeField } from "@planningcenter/tapestry-react"
39
+
40
+ function Test() {
41
+ return (
42
+ <Field feedbackText="Required"><TimeField value={time} onChange={setTime} /></Field>
43
+ )
44
+ }
45
+ `.trim()
46
+
47
+ const result = applyTransform(input)
48
+ expect(result).not.toBeNull()
49
+ expect(result).not.toContain("<Field")
50
+ expect(result).not.toContain("feedbackText")
51
+ expect(result).toContain('description="Required"')
52
+ })
53
+
54
+ it("flags helpContent as unsupported and removes Field", () => {
55
+ const input = `
56
+ import { Field, TimeField } from "@planningcenter/tapestry-react"
57
+
58
+ function Test() {
59
+ return (
60
+ <Field helpContent="Help"><TimeField value={time} onChange={setTime} /></Field>
61
+ )
62
+ }
63
+ `.trim()
64
+
65
+ const result = applyTransform(input)
66
+ expect(result).not.toBeNull()
67
+ expect(result).not.toContain("<Field")
68
+ expect(result).toContain(
69
+ "TODO: tapestry-migration (mergeFieldIntoTimeField)"
70
+ )
71
+ expect(result).toContain("helpContent")
72
+ expect(result).toContain("not supported by TimeField")
73
+ })
74
+
75
+ it("returns null when Field is not imported from tapestry-react", () => {
76
+ const input = `
77
+ import { Field } from "some-other-library"
78
+ import { TimeField } from "@planningcenter/tapestry-react"
79
+
80
+ function Test() {
81
+ return (
82
+ <Field label="Time"><TimeField value={time} onChange={setTime} /></Field>
83
+ )
84
+ }
85
+ `.trim()
86
+
87
+ const result = applyTransform(input)
88
+ expect(result).toBeNull()
89
+ })
90
+
91
+ it("returns null when TimeField is not imported from tapestry-react", () => {
92
+ const input = `
93
+ import { Field } from "@planningcenter/tapestry-react"
94
+ import { TimeField } from "some-other-library"
95
+
96
+ function Test() {
97
+ return (
98
+ <Field label="Time"><TimeField value={time} onChange={setTime} /></Field>
99
+ )
100
+ }
101
+ `.trim()
102
+
103
+ const result = applyTransform(input)
104
+ expect(result).toBeNull()
105
+ })
106
+ })
@@ -0,0 +1,5 @@
1
+ import { mergeFieldFactory } from "../../shared/transformFactories/mergeFieldFactory"
2
+
3
+ export default mergeFieldFactory({
4
+ targetComponent: "TimeField",
5
+ })