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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/package.json +3 -3
  2. package/src/components/checkbox/transforms/moveCheckboxImport.test.ts +26 -0
  3. package/src/components/checkbox/transforms/moveCheckboxImport.ts +1 -0
  4. package/src/components/input/transforms/moveInputImport.test.ts +26 -0
  5. package/src/components/input/transforms/moveInputImport.ts +1 -0
  6. package/src/components/input/transforms/numberFieldRenameToInput.test.ts +51 -0
  7. package/src/components/input/transforms/numberFieldRenameToInput.ts +1 -0
  8. package/src/components/radio/transforms/moveRadioImport.test.ts +26 -0
  9. package/src/components/radio/transforms/moveRadioImport.ts +1 -0
  10. package/src/components/select/transforms/moveSelectImport.test.ts +24 -0
  11. package/src/components/select/transforms/moveSelectImport.ts +1 -0
  12. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +24 -0
  13. package/src/components/shared/transformFactories/helpers/manageImports.ts +25 -1
  14. package/src/components/shared/transformFactories/stylePropTransformFactory.test.ts +330 -0
  15. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +156 -15
  16. package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +17 -12
  17. package/src/components/text-area/transforms/moveTextAreaImport.test.ts +26 -0
  18. package/src/components/text-area/transforms/moveTextAreaImport.ts +1 -0
  19. package/src/components/time-field/index.ts +48 -0
  20. package/src/components/time-field/transforms/auditSpreadProps.test.ts +76 -0
  21. package/src/components/time-field/transforms/auditSpreadProps.ts +10 -0
  22. package/src/components/time-field/transforms/convertStyleProps.test.ts +43 -0
  23. package/src/components/time-field/transforms/convertStyleProps.ts +10 -0
  24. package/src/components/time-field/transforms/flagMinMax.test.ts +103 -0
  25. package/src/components/time-field/transforms/flagMinMax.ts +31 -0
  26. package/src/components/time-field/transforms/mergeFieldIntoTimeField.test.ts +106 -0
  27. package/src/components/time-field/transforms/mergeFieldIntoTimeField.ts +5 -0
  28. package/src/components/time-field/transforms/moveTimeFieldImport.test.ts +153 -0
  29. package/src/components/time-field/transforms/moveTimeFieldImport.ts +14 -0
  30. package/src/components/time-field/transforms/sizeMapping.test.ts +173 -0
  31. package/src/components/time-field/transforms/sizeMapping.ts +15 -0
  32. package/src/components/time-field/transforms/stateToInvalid.test.ts +87 -0
  33. package/src/components/time-field/transforms/stateToInvalid.ts +56 -0
  34. package/src/components/time-field/transforms/stateToInvalidTernary.test.ts +100 -0
  35. package/src/components/time-field/transforms/stateToInvalidTernary.ts +11 -0
  36. package/src/components/time-field/transforms/tupleToTime.test.ts +182 -0
  37. package/src/components/time-field/transforms/tupleToTime.ts +107 -0
  38. package/src/components/time-field/transforms/twelveHourClockToHourCycle.test.ts +117 -0
  39. package/src/components/time-field/transforms/twelveHourClockToHourCycle.ts +65 -0
  40. package/src/components/time-field/transforms/unsupportedProps.test.ts +160 -0
  41. package/src/components/time-field/transforms/unsupportedProps.ts +37 -0
  42. package/src/components/toggle-switch/transforms/moveToggleSwitchImport.test.ts +28 -0
  43. package/src/components/toggle-switch/transforms/moveToggleSwitchImport.ts +1 -0
  44. package/src/index.ts +2 -1
@@ -12,7 +12,7 @@ import {
12
12
  splitStyles,
13
13
  stylePropNames,
14
14
  } from "../../../../dist/tapestry-react-shim.cjs"
15
- import { addComment } from "../../shared/actions/addComment"
15
+ import { addComment, formatComment } from "../../shared/actions/addComment"
16
16
  import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
17
17
  import { getAttribute } from "../../shared/actions/getAttribute"
18
18
  import { removeAttribute } from "../../shared/actions/removeAttribute"
@@ -27,6 +27,49 @@ type StylePropMapping = Record<
27
27
  }
28
28
  >
29
29
 
30
+ // JSX expression values bypass splitStyles, so we mirror its plugin behavior
31
+ // for props that bypass it. Pure key renames pass through live; everything
32
+ // else gets commented out under the css target(s) so the developer can verify
33
+ // the value before enabling.
34
+ const liveAliases: Record<string, string> = {
35
+ column: "gridColumn",
36
+ columnEnd: "gridColumnEnd",
37
+ columnStart: "gridColumnStart",
38
+ direction: "flexDirection",
39
+ row: "gridRow",
40
+ rowEnd: "gridRowEnd",
41
+ rowStart: "gridRowStart",
42
+ }
43
+
44
+ const commentedAliases: Record<string, string[]> = {
45
+ basis: ["flexBasis"],
46
+ elevation: ["boxShadow"],
47
+ grow: ["flexGrow"],
48
+ marginHorizontal: ["marginLeft", "marginRight"],
49
+ marginVertical: ["marginTop", "marginBottom"],
50
+ paddingHorizontal: ["paddingLeft", "paddingRight"],
51
+ paddingVertical: ["paddingTop", "paddingBottom"],
52
+ radius: [
53
+ "borderTopLeftRadius",
54
+ "borderTopRightRadius",
55
+ "borderBottomRightRadius",
56
+ "borderBottomLeftRadius",
57
+ ],
58
+ radiusBottom: ["borderBottomRightRadius", "borderBottomLeftRadius"],
59
+ radiusLeft: ["borderTopLeftRadius", "borderBottomLeftRadius"],
60
+ radiusRight: ["borderTopRightRadius", "borderBottomRightRadius"],
61
+ radiusTop: ["borderTopLeftRadius", "borderTopRightRadius"],
62
+ rotate: ["transform"],
63
+ scale: ["transform"],
64
+ shrink: ["flexShrink"],
65
+ strokeAlign: ["strokeAlign"],
66
+ strokeWeight: ["strokeWeight"],
67
+ uppercase: ["textTransform"],
68
+ wrap: ["flexWrap"],
69
+ x: ["transform"],
70
+ y: ["transform"],
71
+ }
72
+
30
73
  function extractPropValue(attr: JSXAttribute, j: JSCodeshift) {
31
74
  if (!attr.value) {
32
75
  return true
@@ -50,22 +93,36 @@ function extractPropValue(attr: JSXAttribute, j: JSCodeshift) {
50
93
  }
51
94
  }
52
95
 
96
+ type ManualReviewEntry = {
97
+ cssTargets: string[]
98
+ originalProp: string
99
+ originalValue: string
100
+ }
101
+
102
+ function reviewHeading(entry: ManualReviewEntry) {
103
+ return `migrated from \`${entry.originalProp}\`; value may need manual transformation`
104
+ }
105
+
106
+ function reviewBody(entry: ManualReviewEntry) {
107
+ return entry.cssTargets.map((key) => `${key}: ${entry.originalValue}`)
108
+ }
109
+
53
110
  function processKeepStyleProps({
54
111
  attributes,
55
112
  j,
56
- element,
57
- source,
58
113
  }: {
59
114
  attributes: JSXAttribute[]
60
- element: JSXElement
61
115
  j: JSCodeshift
62
- source: Collection
63
116
  }): {
64
117
  directProps: Record<string, unknown>
118
+ manualReviewEntries: ManualReviewEntry[]
119
+ processedNames: string[]
65
120
  themeProps: Record<string, unknown>
66
121
  } {
67
122
  const themeProps: Record<string, unknown> = {}
68
123
  const directProps: Record<string, unknown> = {}
124
+ const manualReviewEntries: ManualReviewEntry[] = []
125
+ const processedNames: string[] = []
69
126
 
70
127
  attributes.forEach((attr) => {
71
128
  const propName = attr.name.name as string
@@ -76,15 +133,32 @@ function processKeepStyleProps({
76
133
  propValue.startsWith("{") &&
77
134
  propValue.endsWith("}")
78
135
  ) {
79
- directProps[propName] = propValue
136
+ if (propName in commentedAliases) {
137
+ manualReviewEntries.push({
138
+ cssTargets: commentedAliases[propName],
139
+ originalProp: propName,
140
+ originalValue: propValue.slice(1, -1),
141
+ })
142
+ } else {
143
+ directProps[liveAliases[propName] ?? propName] = propValue
144
+ }
80
145
  } else {
81
146
  themeProps[propName] = propValue
82
147
  }
83
148
 
84
- removeAttribute(propName, { element, j, source })
149
+ processedNames.push(propName)
85
150
  })
86
151
 
87
- return { directProps, 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
+ )
88
162
  }
89
163
 
90
164
  function processRemoveStyleProps({
@@ -160,20 +234,36 @@ function processStylePropMappings({
160
234
  return styleProps
161
235
  }
162
236
 
237
+ function buildManualReviewComments(
238
+ entries: ManualReviewEntry[],
239
+ j: JSCodeshift
240
+ ) {
241
+ return entries.flatMap((entry) => [
242
+ j.commentBlock(
243
+ formatComment(reviewHeading(entry), "styleProp"),
244
+ true,
245
+ false
246
+ ),
247
+ ...reviewBody(entry).map((line) => j.commentLine(` ${line},`)),
248
+ ])
249
+ }
250
+
163
251
  function applyStylesToComponent({
164
252
  j,
165
253
  element,
166
254
  styles,
255
+ manualReviewEntries = [],
167
256
  }: {
168
257
  element: JSXElement
169
258
  j: JSCodeshift
259
+ manualReviewEntries?: ManualReviewEntry[]
170
260
  source: Collection
171
261
  styles: Record<string, unknown>
172
262
  }) {
173
263
  const styleAttr = getAttribute({ element, name: "style" })
174
264
  const styleValue = j.jsxExpressionContainer(
175
265
  j.objectExpression(
176
- Object.entries(styles).map(([key, value]) => {
266
+ Object.entries(styles).map(([key, value], index) => {
177
267
  let valueNode
178
268
  if (
179
269
  typeof value === "string" &&
@@ -198,7 +288,11 @@ function applyStylesToComponent({
198
288
  valueNode = j.literal(value as string | number | boolean | null)
199
289
  }
200
290
 
201
- return j.objectProperty(j.identifier(key), valueNode)
291
+ const property = j.objectProperty(j.identifier(key), valueNode)
292
+ if (index === 0 && manualReviewEntries.length > 0) {
293
+ property.comments = buildManualReviewComments(manualReviewEntries, j)
294
+ }
295
+ return property
202
296
  })
203
297
  )
204
298
  )
@@ -286,6 +380,7 @@ export function stylePropTransformFactory(config: {
286
380
  const allAttributes = element.openingElement.attributes || []
287
381
  let allStyleProps: Record<string, unknown> = {}
288
382
  let directStyleProps: Record<string, unknown> = {}
383
+ const manualReviewEntries: ManualReviewEntry[] = []
289
384
  const attributes = allAttributes.filter((attr) => {
290
385
  if (attr.type !== "JSXAttribute") return false
291
386
  const name = attr.name?.name as string
@@ -307,9 +402,7 @@ export function stylePropTransformFactory(config: {
307
402
  )
308
403
  const keepResult = processKeepStyleProps({
309
404
  attributes: keepAttributes,
310
- element,
311
405
  j,
312
- source,
313
406
  })
314
407
  allStyleProps = {
315
408
  ...allStyleProps,
@@ -319,6 +412,7 @@ export function stylePropTransformFactory(config: {
319
412
  ...directStyleProps,
320
413
  ...keepResult.directProps,
321
414
  }
415
+ manualReviewEntries.push(...keepResult.manualReviewEntries)
322
416
 
323
417
  allStyleProps = {
324
418
  ...allStyleProps,
@@ -343,7 +437,8 @@ export function stylePropTransformFactory(config: {
343
437
 
344
438
  if (
345
439
  Object.keys(allStyleProps).length > 0 ||
346
- Object.keys(directStyleProps).length > 0
440
+ Object.keys(directStyleProps).length > 0 ||
441
+ manualReviewEntries.length > 0
347
442
  ) {
348
443
  try {
349
444
  let styles: Record<string, unknown> = {}
@@ -360,8 +455,39 @@ export function stylePropTransformFactory(config: {
360
455
  styles = { ...styles, ...directStyleProps }
361
456
  if (options.verbose) console.log("Final generated styles:", styles)
362
457
 
363
- if (Object.keys(styles).length > 0) {
364
- applyStylesToComponent({ element, j, source, styles })
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) {
484
+ applyStylesToComponent({
485
+ element,
486
+ j,
487
+ manualReviewEntries,
488
+ source,
489
+ styles,
490
+ })
365
491
 
366
492
  if (options.verbose) {
367
493
  const styleAttr = getAttribute({ element, name: "style" })
@@ -381,6 +507,21 @@ export function stylePropTransformFactory(config: {
381
507
  }
382
508
  }
383
509
  }
510
+ } else if (
511
+ nonInlineableKeys.length === 0 &&
512
+ manualReviewEntries.length > 0
513
+ ) {
514
+ // No anchoring property in the style block — emit each manual-review
515
+ // entry as a JSX-level TODO comment instead.
516
+ for (const entry of manualReviewEntries) {
517
+ addComment({
518
+ element,
519
+ j,
520
+ scope: "styleProp",
521
+ source,
522
+ text: `${reviewHeading(entry)}: ${reviewBody(entry).join("; ")}`,
523
+ })
524
+ }
384
525
  }
385
526
  } catch (error) {
386
527
  console.log("Error processing style props:", error)
@@ -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
 
@@ -96,6 +96,32 @@ function Test() {
96
96
  })
97
97
  })
98
98
 
99
+ describe("import conflict handling", () => {
100
+ it("should use TTextArea alias when TextArea is already imported from another package", () => {
101
+ const input = `
102
+ import { TextArea } from "some-other-library"
103
+ import { TextArea as ReactTextArea } from "@planningcenter/tapestry-react"
104
+
105
+ function Test() {
106
+ return (
107
+ <div>
108
+ <TextArea />
109
+ <ReactTextArea label="Notes" />
110
+ </div>
111
+ )
112
+ }
113
+ `.trim()
114
+
115
+ const result = applyTransform(input)
116
+ expect(result).toContain('import { TextArea } from "some-other-library"')
117
+ expect(result).toContain(
118
+ 'import { TextArea as TTextArea } from "@planningcenter/tapestry"'
119
+ )
120
+ expect(result).toContain("<TTextArea")
121
+ expect(result).not.toContain("ReactTextArea")
122
+ })
123
+ })
124
+
99
125
  describe("edge cases", () => {
100
126
  it("should handle already migrated imports", () => {
101
127
  const input = `
@@ -4,6 +4,7 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
4
4
 
5
5
  const transform: Transform = componentTransformFactory({
6
6
  condition: () => true,
7
+ conflictAlias: "TTextArea",
7
8
  fromComponent: "TextArea",
8
9
  fromPackage: "@planningcenter/tapestry-react",
9
10
  toComponent: "TextArea",
@@ -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
+ })