@planningcenter/tapestry-migration-cli 3.2.2-rc.9 → 3.2.3-rc.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.
- package/package.json +3 -3
- package/src/components/shared/helpers/unsupportedPropsHelpers.ts +24 -0
- package/src/components/shared/transformFactories/stylePropTransformFactory.test.ts +330 -0
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +156 -15
- 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.
|
|
3
|
+
"version": "3.2.3-rc.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.2.
|
|
35
|
+
"@planningcenter/tapestry": "^3.2.3-rc.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": "
|
|
55
|
+
"gitHead": "e944c237ec8b2e0318f53f8a8a2a3b31b6e28a81"
|
|
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
|
+
]
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import { stackViewPlugin } from "../../../stubs/stackViewPlugin"
|
|
5
|
+
import { stylePropTransformFactory } from "./stylePropTransformFactory"
|
|
6
|
+
|
|
7
|
+
const j = jscodeshift.withParser("tsx")
|
|
8
|
+
|
|
9
|
+
function applyTransform(
|
|
10
|
+
transform: ReturnType<typeof stylePropTransformFactory>,
|
|
11
|
+
source: string
|
|
12
|
+
): string {
|
|
13
|
+
const fileInfo = { path: "test.tsx", source }
|
|
14
|
+
const result = transform(
|
|
15
|
+
fileInfo,
|
|
16
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
17
|
+
{}
|
|
18
|
+
) as string | null
|
|
19
|
+
return result || source
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("stylePropTransformFactory", () => {
|
|
23
|
+
describe("grid alias remapping", () => {
|
|
24
|
+
const transform = stylePropTransformFactory({
|
|
25
|
+
stylesToRemove: [],
|
|
26
|
+
targetComponent: "Box",
|
|
27
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("remaps column/row string props to gridColumn/gridRow via splitStyles", () => {
|
|
31
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box column="1 / 3" row="2 / 4" />`
|
|
32
|
+
const result = applyTransform(transform, input)
|
|
33
|
+
expect(result).toMatchInlineSnapshot(`
|
|
34
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box
|
|
35
|
+
style={{
|
|
36
|
+
gridColumn: "1 / 3",
|
|
37
|
+
gridRow: "2 / 4"
|
|
38
|
+
}} />"
|
|
39
|
+
`)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("remaps column/row expression props to gridColumn/gridRow", () => {
|
|
43
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box column={1} row={2} />`
|
|
44
|
+
const result = applyTransform(transform, input)
|
|
45
|
+
expect(result).toMatchInlineSnapshot(`
|
|
46
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box
|
|
47
|
+
style={{
|
|
48
|
+
gridColumn: 1,
|
|
49
|
+
gridRow: 2
|
|
50
|
+
}} />"
|
|
51
|
+
`)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("remaps column/row identifier expression props to gridColumn/gridRow", () => {
|
|
55
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box column={col} row={r} />`
|
|
56
|
+
const result = applyTransform(transform, input)
|
|
57
|
+
expect(result).toMatchInlineSnapshot(`
|
|
58
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box
|
|
59
|
+
style={{
|
|
60
|
+
gridColumn: col,
|
|
61
|
+
gridRow: r
|
|
62
|
+
}} />"
|
|
63
|
+
`)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("remaps columnStart/columnEnd/rowStart/rowEnd expression props", () => {
|
|
67
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box columnStart={1} columnEnd={3} rowStart={2} rowEnd={4} />`
|
|
68
|
+
const result = applyTransform(transform, input)
|
|
69
|
+
expect(result).toMatchInlineSnapshot(`
|
|
70
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box
|
|
71
|
+
style={{
|
|
72
|
+
gridColumnStart: 1,
|
|
73
|
+
gridColumnEnd: 3,
|
|
74
|
+
gridRowStart: 2,
|
|
75
|
+
gridRowEnd: 4
|
|
76
|
+
}} />"
|
|
77
|
+
`)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("remaps direction expression prop to flexDirection", () => {
|
|
81
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box direction={dir} />`
|
|
82
|
+
const result = applyTransform(transform, input)
|
|
83
|
+
expect(result).toMatchInlineSnapshot(`
|
|
84
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box style={{
|
|
85
|
+
flexDirection: dir
|
|
86
|
+
}} />"
|
|
87
|
+
`)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Conditional expressions can't be evaluated at codemod time, so they
|
|
91
|
+
// bypass splitStyles and land in directProps as raw source. Without the
|
|
92
|
+
// alias map, the resulting `style` object would contain non-CSS keys
|
|
93
|
+
// like `column` / `row` instead of `gridColumn` / `gridRow`.
|
|
94
|
+
it("remaps column/row ternary expression props to gridColumn/gridRow", () => {
|
|
95
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box column={filter ? '1' : '2'} row={filter ? '2' : undefined} />`
|
|
96
|
+
const result = applyTransform(transform, input)
|
|
97
|
+
expect(result).toMatchInlineSnapshot(`
|
|
98
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box
|
|
99
|
+
style={{
|
|
100
|
+
gridColumn: filter ? '1' : '2',
|
|
101
|
+
gridRow: filter ? '2' : undefined
|
|
102
|
+
}} />"
|
|
103
|
+
`)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe("non-aliased style props", () => {
|
|
108
|
+
const transform = stylePropTransformFactory({
|
|
109
|
+
stylesToRemove: [],
|
|
110
|
+
targetComponent: "Box",
|
|
111
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("preserves the prop name for non-aliased identifier expression props", () => {
|
|
115
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box marginTop={spacing} />`
|
|
116
|
+
const result = applyTransform(transform, input)
|
|
117
|
+
expect(result).toMatchInlineSnapshot(`
|
|
118
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box style={{
|
|
119
|
+
marginTop: spacing
|
|
120
|
+
}} />"
|
|
121
|
+
`)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe("value-transforming aliases", () => {
|
|
126
|
+
const transform = stylePropTransformFactory({
|
|
127
|
+
stylesToRemove: [],
|
|
128
|
+
targetComponent: "Box",
|
|
129
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it("comments out the renamed `wrap` → `flexWrap` and emits a TODO for expression values", () => {
|
|
133
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box wrap={someBool} />`
|
|
134
|
+
const result = applyTransform(transform, input)
|
|
135
|
+
expect(result).toMatchInlineSnapshot(`
|
|
136
|
+
"import { Box } from "@planningcenter/tapestry-react";/* TODO: tapestry-migration (styleProp): migrated from \`wrap\`; value may need manual transformation: flexWrap: someBool */
|
|
137
|
+
<Box />"
|
|
138
|
+
`)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("comments out renamed `grow`/`shrink`/`basis` props with TODOs", () => {
|
|
142
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box grow={g} shrink={s} basis={b} />`
|
|
143
|
+
const result = applyTransform(transform, input)
|
|
144
|
+
expect(result).toMatchInlineSnapshot(`
|
|
145
|
+
"import { Box } from "@planningcenter/tapestry-react";/* TODO: tapestry-migration (styleProp): migrated from \`grow\`; value may need manual transformation: flexGrow: g */
|
|
146
|
+
/* TODO: tapestry-migration (styleProp): migrated from \`shrink\`; value may need manual transformation: flexShrink: s */
|
|
147
|
+
/* TODO: tapestry-migration (styleProp): migrated from \`basis\`; value may need manual transformation: flexBasis: b */
|
|
148
|
+
<Box />"
|
|
149
|
+
`)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it("comments out fanned-out `marginHorizontal` → marginLeft/marginRight with a TODO", () => {
|
|
153
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box marginHorizontal={spacing} />`
|
|
154
|
+
const result = applyTransform(transform, input)
|
|
155
|
+
expect(result).toMatchInlineSnapshot(`
|
|
156
|
+
"import { Box } from "@planningcenter/tapestry-react";/* TODO: tapestry-migration (styleProp): migrated from \`marginHorizontal\`; value may need manual transformation: marginLeft: spacing; marginRight: spacing */
|
|
157
|
+
<Box />"
|
|
158
|
+
`)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it("anchors commented-out renamed-key props inline when a real style key is present", () => {
|
|
162
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box wrap={someBool} marginTop={spacing} />`
|
|
163
|
+
const result = applyTransform(transform, input)
|
|
164
|
+
expect(result).toMatchInlineSnapshot(`
|
|
165
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box
|
|
166
|
+
style={{
|
|
167
|
+
/* TODO: tapestry-migration (styleProp): migrated from \`wrap\`; value may need manual transformation */
|
|
168
|
+
// flexWrap: someBool,
|
|
169
|
+
marginTop: spacing
|
|
170
|
+
}} />"
|
|
171
|
+
`)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it("comments out fanned-out `paddingVertical` → paddingTop/paddingBottom", () => {
|
|
175
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box paddingVertical={p} />`
|
|
176
|
+
const result = applyTransform(transform, input)
|
|
177
|
+
expect(result).toMatchInlineSnapshot(`
|
|
178
|
+
"import { Box } from "@planningcenter/tapestry-react";/* TODO: tapestry-migration (styleProp): migrated from \`paddingVertical\`; value may need manual transformation: paddingTop: p; paddingBottom: p */
|
|
179
|
+
<Box />"
|
|
180
|
+
`)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it("comments out fanned-out `radius` → all four borderRadius corners", () => {
|
|
184
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box radius={r} />`
|
|
185
|
+
const result = applyTransform(transform, input)
|
|
186
|
+
expect(result).toMatchInlineSnapshot(`
|
|
187
|
+
"import { Box } from "@planningcenter/tapestry-react";/* TODO: tapestry-migration (styleProp): migrated from \`radius\`; value may need manual transformation: borderTopLeftRadius: r; borderTopRightRadius: r; borderBottomRightRadius: r; borderBottomLeftRadius: r */
|
|
188
|
+
<Box />"
|
|
189
|
+
`)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("comments out fanned-out `radiusTop` → the two top corners only", () => {
|
|
193
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box radiusTop={rt} />`
|
|
194
|
+
const result = applyTransform(transform, input)
|
|
195
|
+
expect(result).toMatchInlineSnapshot(`
|
|
196
|
+
"import { Box } from "@planningcenter/tapestry-react";/* TODO: tapestry-migration (styleProp): migrated from \`radiusTop\`; value may need manual transformation: borderTopLeftRadius: rt; borderTopRightRadius: rt */
|
|
197
|
+
<Box />"
|
|
198
|
+
`)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// `x`, `y`, `rotate`, `scale`, `elevation` produce CSS by combining
|
|
202
|
+
// multiple values or by theme lookup, so we comment out a draft of the
|
|
203
|
+
// target CSS property under the migrated name.
|
|
204
|
+
it("comments out `x` → `transform` anchored to the next style key", () => {
|
|
205
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box x={dx} marginLeft={ml} />`
|
|
206
|
+
const result = applyTransform(transform, input)
|
|
207
|
+
expect(result).toMatchInlineSnapshot(`
|
|
208
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box
|
|
209
|
+
style={{
|
|
210
|
+
/* TODO: tapestry-migration (styleProp): migrated from \`x\`; value may need manual transformation */
|
|
211
|
+
// transform: dx,
|
|
212
|
+
marginLeft: ml
|
|
213
|
+
}} />"
|
|
214
|
+
`)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it("commented-out-only elements emit JSX-level TODO comments and no style attribute", () => {
|
|
218
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box x={dx} y={dy} rotate={r} elevation={e} />`
|
|
219
|
+
const result = applyTransform(transform, input)
|
|
220
|
+
expect(result).toMatchInlineSnapshot(`
|
|
221
|
+
"import { Box } from "@planningcenter/tapestry-react";/* TODO: tapestry-migration (styleProp): migrated from \`x\`; value may need manual transformation: transform: dx */
|
|
222
|
+
/* TODO: tapestry-migration (styleProp): migrated from \`y\`; value may need manual transformation: transform: dy */
|
|
223
|
+
/* TODO: tapestry-migration (styleProp): migrated from \`rotate\`; value may need manual transformation: transform: r */
|
|
224
|
+
/* TODO: tapestry-migration (styleProp): migrated from \`elevation\`; value may need manual transformation: boxShadow: e */
|
|
225
|
+
<Box />"
|
|
226
|
+
`)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it("primitive `x` literals still go through splitStyles and produce a transform string", () => {
|
|
230
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box x={5} />`
|
|
231
|
+
const result = applyTransform(transform, input)
|
|
232
|
+
expect(result).toMatchInlineSnapshot(`
|
|
233
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box style={{
|
|
234
|
+
transform: "translateX(5) "
|
|
235
|
+
}} />"
|
|
236
|
+
`)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it("does not emit a TODO comment for safely renamed grid aliases", () => {
|
|
240
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box column={col} />`
|
|
241
|
+
const result = applyTransform(transform, input)
|
|
242
|
+
expect(result).toMatchInlineSnapshot(`
|
|
243
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box style={{
|
|
244
|
+
gridColumn: col
|
|
245
|
+
}} />"
|
|
246
|
+
`)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
// Literal values flow through splitStyles, which expands shorthands like
|
|
250
|
+
// `marginHorizontal` into the underlying CSS properties. No TODO needed.
|
|
251
|
+
it("expands `marginHorizontal` string literals via splitStyles without a TODO", () => {
|
|
252
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box marginHorizontal="8px" />`
|
|
253
|
+
const result = applyTransform(transform, input)
|
|
254
|
+
expect(result).toMatchInlineSnapshot(`
|
|
255
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box style={{
|
|
256
|
+
marginRight: "8px",
|
|
257
|
+
marginLeft: "8px"
|
|
258
|
+
}} />"
|
|
259
|
+
`)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it("expands `paddingHorizontal` numeric literals via splitStyles without a TODO", () => {
|
|
263
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box paddingHorizontal={1} />`
|
|
264
|
+
const result = applyTransform(transform, input)
|
|
265
|
+
expect(result).toMatchInlineSnapshot(`
|
|
266
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box style={{
|
|
267
|
+
paddingRight: "8px",
|
|
268
|
+
paddingLeft: "8px"
|
|
269
|
+
}} />"
|
|
270
|
+
`)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it("transforms `wrap`/`grow` boolean literals via splitStyles without a TODO", () => {
|
|
274
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box wrap={true} grow={true} />`
|
|
275
|
+
const result = applyTransform(transform, input)
|
|
276
|
+
expect(result).toMatchInlineSnapshot(`
|
|
277
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box
|
|
278
|
+
style={{
|
|
279
|
+
flexWrap: "wrap",
|
|
280
|
+
flexGrow: 1
|
|
281
|
+
}} />"
|
|
282
|
+
`)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it("expands `radius` shortcut strings via splitStyles without a TODO", () => {
|
|
286
|
+
const input = `import { Box } from "@planningcenter/tapestry-react";<Box radius="pill" />`
|
|
287
|
+
const result = applyTransform(transform, input)
|
|
288
|
+
expect(result).toMatchInlineSnapshot(`
|
|
289
|
+
"import { Box } from "@planningcenter/tapestry-react";<Box style={{
|
|
290
|
+
borderRadius: "10em"
|
|
291
|
+
}} />"
|
|
292
|
+
`)
|
|
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
|
+
})
|
|
330
|
+
})
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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)
|