@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.
- package/package.json +3 -3
- package/src/components/shared/helpers/unsupportedPropsHelpers.ts +24 -0
- package/src/components/shared/transformFactories/stylePropTransformFactory.test.ts +36 -0
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +43 -10
- package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +17 -12
- package/src/components/time-field/index.ts +48 -0
- package/src/components/time-field/transforms/auditSpreadProps.test.ts +76 -0
- package/src/components/time-field/transforms/auditSpreadProps.ts +10 -0
- package/src/components/time-field/transforms/convertStyleProps.test.ts +43 -0
- package/src/components/time-field/transforms/convertStyleProps.ts +10 -0
- package/src/components/time-field/transforms/flagMinMax.test.ts +103 -0
- package/src/components/time-field/transforms/flagMinMax.ts +31 -0
- package/src/components/time-field/transforms/mergeFieldIntoTimeField.test.ts +106 -0
- package/src/components/time-field/transforms/mergeFieldIntoTimeField.ts +5 -0
- package/src/components/time-field/transforms/moveTimeFieldImport.test.ts +153 -0
- package/src/components/time-field/transforms/moveTimeFieldImport.ts +14 -0
- package/src/components/time-field/transforms/sizeMapping.test.ts +173 -0
- package/src/components/time-field/transforms/sizeMapping.ts +15 -0
- package/src/components/time-field/transforms/stateToInvalid.test.ts +87 -0
- package/src/components/time-field/transforms/stateToInvalid.ts +56 -0
- package/src/components/time-field/transforms/stateToInvalidTernary.test.ts +100 -0
- package/src/components/time-field/transforms/stateToInvalidTernary.ts +11 -0
- package/src/components/time-field/transforms/tupleToTime.test.ts +182 -0
- package/src/components/time-field/transforms/tupleToTime.ts +107 -0
- package/src/components/time-field/transforms/twelveHourClockToHourCycle.test.ts +117 -0
- package/src/components/time-field/transforms/twelveHourClockToHourCycle.ts +65 -0
- package/src/components/time-field/transforms/unsupportedProps.test.ts +160 -0
- package/src/components/time-field/transforms/unsupportedProps.ts +37 -0
- 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.
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
46
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
})
|