@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.
- package/package.json +3 -3
- package/src/components/checkbox/transforms/moveCheckboxImport.test.ts +26 -0
- package/src/components/checkbox/transforms/moveCheckboxImport.ts +1 -0
- package/src/components/input/transforms/moveInputImport.test.ts +26 -0
- package/src/components/input/transforms/moveInputImport.ts +1 -0
- package/src/components/input/transforms/numberFieldRenameToInput.test.ts +51 -0
- package/src/components/input/transforms/numberFieldRenameToInput.ts +1 -0
- package/src/components/radio/transforms/moveRadioImport.test.ts +26 -0
- package/src/components/radio/transforms/moveRadioImport.ts +1 -0
- package/src/components/select/transforms/moveSelectImport.test.ts +24 -0
- package/src/components/select/transforms/moveSelectImport.ts +1 -0
- package/src/components/shared/helpers/unsupportedPropsHelpers.ts +24 -0
- package/src/components/shared/transformFactories/helpers/manageImports.ts +25 -1
- 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/text-area/transforms/moveTextAreaImport.test.ts +26 -0
- package/src/components/text-area/transforms/moveTextAreaImport.ts +1 -0
- 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/components/toggle-switch/transforms/moveToggleSwitchImport.test.ts +28 -0
- package/src/components/toggle-switch/transforms/moveToggleSwitchImport.ts +1 -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
|
|
3
|
+
"version": "3.2.2",
|
|
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
|
|
35
|
+
"@planningcenter/tapestry": "^3.2.2",
|
|
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": "f7d76b11169961788185060bf2555b13bb2dcb2e"
|
|
56
56
|
}
|
|
@@ -114,6 +114,32 @@ function Test() {
|
|
|
114
114
|
})
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
+
describe("import conflict handling", () => {
|
|
118
|
+
it("should use TCheckbox alias when Checkbox is already imported from another package", () => {
|
|
119
|
+
const input = `
|
|
120
|
+
import { Checkbox } from "some-other-library"
|
|
121
|
+
import { Checkbox as ReactCheckbox } from "@planningcenter/tapestry-react"
|
|
122
|
+
|
|
123
|
+
function Test() {
|
|
124
|
+
return (
|
|
125
|
+
<div>
|
|
126
|
+
<Checkbox />
|
|
127
|
+
<ReactCheckbox label="Test" />
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
`.trim()
|
|
132
|
+
|
|
133
|
+
const result = applyTransform(input)
|
|
134
|
+
expect(result).toContain('import { Checkbox } from "some-other-library"')
|
|
135
|
+
expect(result).toContain(
|
|
136
|
+
'import { Checkbox as TCheckbox } from "@planningcenter/tapestry"'
|
|
137
|
+
)
|
|
138
|
+
expect(result).toContain("<TCheckbox")
|
|
139
|
+
expect(result).not.toContain("ReactCheckbox")
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
117
143
|
describe("edge cases", () => {
|
|
118
144
|
it("should handle already migrated imports", () => {
|
|
119
145
|
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: "TCheckbox",
|
|
7
8
|
fromComponent: "Checkbox",
|
|
8
9
|
fromPackage: "@planningcenter/tapestry-react",
|
|
9
10
|
toComponent: "Checkbox",
|
|
@@ -96,6 +96,32 @@ function Test() {
|
|
|
96
96
|
})
|
|
97
97
|
})
|
|
98
98
|
|
|
99
|
+
describe("import conflict handling", () => {
|
|
100
|
+
it("should use TInput alias when Input is already imported from another package", () => {
|
|
101
|
+
const input = `
|
|
102
|
+
import { Input } from "some-other-library"
|
|
103
|
+
import { Input as ReactInput } from "@planningcenter/tapestry-react"
|
|
104
|
+
|
|
105
|
+
function Test() {
|
|
106
|
+
return (
|
|
107
|
+
<div>
|
|
108
|
+
<Input />
|
|
109
|
+
<ReactInput label="Name" />
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
`.trim()
|
|
114
|
+
|
|
115
|
+
const result = applyTransform(input)
|
|
116
|
+
expect(result).toContain('import { Input } from "some-other-library"')
|
|
117
|
+
expect(result).toContain(
|
|
118
|
+
'import { Input as TInput } from "@planningcenter/tapestry"'
|
|
119
|
+
)
|
|
120
|
+
expect(result).toContain("<TInput")
|
|
121
|
+
expect(result).not.toContain("ReactInput")
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
99
125
|
describe("edge cases", () => {
|
|
100
126
|
it("should handle already migrated imports", () => {
|
|
101
127
|
const input = `
|
|
@@ -5,6 +5,7 @@ import { transformableInput } from "../transformableInput"
|
|
|
5
5
|
|
|
6
6
|
const transform: Transform = componentTransformFactory({
|
|
7
7
|
condition: transformableInput,
|
|
8
|
+
conflictAlias: "TInput",
|
|
8
9
|
fromComponent: "Input",
|
|
9
10
|
fromPackage: "@planningcenter/tapestry-react",
|
|
10
11
|
toComponent: "Input",
|
|
@@ -99,6 +99,57 @@ function Test() {
|
|
|
99
99
|
expect(importDeclarationMatches).toHaveLength(1)
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
+
it("uses TInput alias when Input is already imported from another package", () => {
|
|
103
|
+
const input = `
|
|
104
|
+
import { Input } from "some-other-library"
|
|
105
|
+
import { NumberField } from "@planningcenter/tapestry-react"
|
|
106
|
+
|
|
107
|
+
function Test() {
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<Input />
|
|
111
|
+
<NumberField label="Amount" type="number" />
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
`.trim()
|
|
116
|
+
|
|
117
|
+
const result = applyTransform(input)
|
|
118
|
+
expect(result).toContain('import { Input } from "some-other-library"')
|
|
119
|
+
expect(result).toContain(
|
|
120
|
+
'import { Input as TInput } from "@planningcenter/tapestry"'
|
|
121
|
+
)
|
|
122
|
+
expect(result).toContain("<TInput")
|
|
123
|
+
expect(result).not.toContain("NumberField")
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it("adds Input import when fromPackage Input is aliased", () => {
|
|
127
|
+
const input = `
|
|
128
|
+
import { Input as ReactInput, NumberField } from "@planningcenter/tapestry-react"
|
|
129
|
+
|
|
130
|
+
function Test() {
|
|
131
|
+
return (
|
|
132
|
+
<div>
|
|
133
|
+
<ReactInput label="Name" />
|
|
134
|
+
<NumberField label="Amount" type="number" />
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
`.trim()
|
|
139
|
+
|
|
140
|
+
const result = applyTransform(input)
|
|
141
|
+
expect(result).not.toBeNull()
|
|
142
|
+
// The renamed NumberField becomes <Input>, so an Input import is required —
|
|
143
|
+
// the aliased ReactInput in tapestry-react does not put `Input` in scope.
|
|
144
|
+
expect(result).toContain('import { Input } from "@planningcenter/tapestry"')
|
|
145
|
+
expect(result).toContain("<Input")
|
|
146
|
+
expect(result).not.toContain("NumberField")
|
|
147
|
+
// The aliased ReactInput is left untouched by this transform; the moveInputImport
|
|
148
|
+
// transform later in the pipeline will not match it (different local name) and a
|
|
149
|
+
// separate cleanup is responsible for removing unused imports.
|
|
150
|
+
expect(result).toContain("ReactInput")
|
|
151
|
+
})
|
|
152
|
+
|
|
102
153
|
it("merges Input import when file already imports Input from tapestry", () => {
|
|
103
154
|
const input = `
|
|
104
155
|
import { Input } from "@planningcenter/tapestry"
|
|
@@ -2,6 +2,7 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
|
|
|
2
2
|
|
|
3
3
|
export default componentTransformFactory({
|
|
4
4
|
condition: () => true,
|
|
5
|
+
conflictAlias: "TInput",
|
|
5
6
|
fromComponent: "NumberField",
|
|
6
7
|
fromPackage: "@planningcenter/tapestry-react",
|
|
7
8
|
toComponent: "Input",
|
|
@@ -96,6 +96,32 @@ function Test() {
|
|
|
96
96
|
})
|
|
97
97
|
})
|
|
98
98
|
|
|
99
|
+
describe("import conflict handling", () => {
|
|
100
|
+
it("should use TRadio alias when Radio is already imported from another package", () => {
|
|
101
|
+
const input = `
|
|
102
|
+
import { Radio } from "some-other-library"
|
|
103
|
+
import { Radio as ReactRadio } from "@planningcenter/tapestry-react"
|
|
104
|
+
|
|
105
|
+
function Test() {
|
|
106
|
+
return (
|
|
107
|
+
<div>
|
|
108
|
+
<Radio />
|
|
109
|
+
<ReactRadio label="Option" value="a" />
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
`.trim()
|
|
114
|
+
|
|
115
|
+
const result = applyTransform(input)
|
|
116
|
+
expect(result).toContain('import { Radio } from "some-other-library"')
|
|
117
|
+
expect(result).toContain(
|
|
118
|
+
'import { Radio as TRadio } from "@planningcenter/tapestry"'
|
|
119
|
+
)
|
|
120
|
+
expect(result).toContain("<TRadio")
|
|
121
|
+
expect(result).not.toContain("ReactRadio")
|
|
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: "TRadio",
|
|
7
8
|
fromComponent: "Radio",
|
|
8
9
|
fromPackage: "@planningcenter/tapestry-react",
|
|
9
10
|
toComponent: "Radio",
|
|
@@ -145,4 +145,28 @@ function Test() {
|
|
|
145
145
|
expect(result).toContain("disabled")
|
|
146
146
|
expect(result).toContain("onChange={() => {}}")
|
|
147
147
|
})
|
|
148
|
+
|
|
149
|
+
it("should use TSelect alias when Select is already imported from another package", () => {
|
|
150
|
+
const input = `
|
|
151
|
+
import { Select } from "some-other-library"
|
|
152
|
+
import { Select as ReactSelect } from "@planningcenter/tapestry-react"
|
|
153
|
+
|
|
154
|
+
function Test() {
|
|
155
|
+
return (
|
|
156
|
+
<div>
|
|
157
|
+
<Select />
|
|
158
|
+
<ReactSelect label="Test" placeholder="Pick" options={[]} />
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
`.trim()
|
|
163
|
+
|
|
164
|
+
const result = applyTransform(input)
|
|
165
|
+
expect(result).toContain('import { Select } from "some-other-library"')
|
|
166
|
+
expect(result).toContain(
|
|
167
|
+
'import { Select as TSelect } from "@planningcenter/tapestry"'
|
|
168
|
+
)
|
|
169
|
+
expect(result).toContain("<TSelect")
|
|
170
|
+
expect(result).not.toContain("ReactSelect")
|
|
171
|
+
})
|
|
148
172
|
})
|
|
@@ -5,6 +5,7 @@ import { transformableSelect } from "../transformableSelect"
|
|
|
5
5
|
|
|
6
6
|
const transform: Transform = componentTransformFactory({
|
|
7
7
|
condition: transformableSelect,
|
|
8
|
+
conflictAlias: "TSelect",
|
|
8
9
|
fromComponent: "Select",
|
|
9
10
|
fromPackage: "@planningcenter/tapestry-react",
|
|
10
11
|
toComponent: "Select",
|
|
@@ -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
|
+
]
|
|
@@ -151,12 +151,14 @@ export function removeImportFromDeclaration(
|
|
|
151
151
|
export function addImport({
|
|
152
152
|
component,
|
|
153
153
|
conflictAlias,
|
|
154
|
+
fromPackage,
|
|
154
155
|
j,
|
|
155
156
|
pkg,
|
|
156
157
|
source,
|
|
157
158
|
}: {
|
|
158
159
|
component: string
|
|
159
160
|
conflictAlias: string
|
|
161
|
+
fromPackage?: string
|
|
160
162
|
j: JSCodeshift
|
|
161
163
|
pkg: string
|
|
162
164
|
source: Collection
|
|
@@ -166,7 +168,28 @@ export function addImport({
|
|
|
166
168
|
return existingImportName
|
|
167
169
|
}
|
|
168
170
|
|
|
169
|
-
|
|
171
|
+
// Conflict = component imported from a package OTHER than the source/target packages.
|
|
172
|
+
// Imports from fromPackage will be migrated to pkg by another transform in the pipeline,
|
|
173
|
+
// so they don't constitute a conflict that requires aliasing.
|
|
174
|
+
const excludePackages = fromPackage ? [pkg, fromPackage] : [pkg]
|
|
175
|
+
const hasConflict = hasConflictingImport(component, excludePackages, {
|
|
176
|
+
j,
|
|
177
|
+
source,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// If the component is already imported from fromPackage under the same local name
|
|
181
|
+
// (and won't conflict with another package), the pipeline's package-move transform
|
|
182
|
+
// will migrate it later — skip adding a duplicate import here. Aliased imports from
|
|
183
|
+
// fromPackage do not put `component` itself in scope, so they must not trigger this
|
|
184
|
+
// early return.
|
|
185
|
+
const importedFromSourceName = fromPackage
|
|
186
|
+
? getImportName(component, fromPackage, { j, source })
|
|
187
|
+
: null
|
|
188
|
+
const importedFromSource = importedFromSourceName === component
|
|
189
|
+
if (importedFromSource && !hasConflict) {
|
|
190
|
+
return component
|
|
191
|
+
}
|
|
192
|
+
|
|
170
193
|
const finalComponentName = hasConflict ? conflictAlias : component
|
|
171
194
|
const alias =
|
|
172
195
|
finalComponentName !== component ? finalComponentName : undefined
|
|
@@ -240,6 +263,7 @@ export function manageImports(
|
|
|
240
263
|
addImport({
|
|
241
264
|
component: config.toComponent,
|
|
242
265
|
conflictAlias: config.conflictAlias || targetComponentName,
|
|
266
|
+
fromPackage: config.fromPackage,
|
|
243
267
|
j,
|
|
244
268
|
pkg: config.toPackage,
|
|
245
269
|
source,
|
|
@@ -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
|
+
})
|