@planningcenter/tapestry-migration-cli 3.2.3-rc.8 → 3.3.0-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 +4 -6
- package/src/components/preflight/index.ts +90 -0
- package/src/components/preflight/transforms/commentOnAsProp.ts +38 -0
- package/src/components/preflight/transforms/convertStyleProps.test.ts +333 -0
- package/src/components/preflight/transforms/convertStyleProps.ts +29 -0
- package/src/index.ts +34 -0
- package/src/reportGenerator.ts +2 -0
package/package.json
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/tapestry-migration-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0-rc.0",
|
|
4
4
|
"description": "CLI tool for Tapestry migrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
|
-
"bin":
|
|
8
|
-
"tapestry-migration-cli": "./src/index.ts"
|
|
9
|
-
},
|
|
7
|
+
"bin": "./src/index.ts",
|
|
10
8
|
"scripts": {
|
|
11
9
|
"build": "vite build",
|
|
12
10
|
"dev": "tsc --watch",
|
|
@@ -32,7 +30,7 @@
|
|
|
32
30
|
},
|
|
33
31
|
"devDependencies": {
|
|
34
32
|
"@emotion/react": "^11.14.0",
|
|
35
|
-
"@planningcenter/tapestry": "^3.
|
|
33
|
+
"@planningcenter/tapestry": "^3.3.0-rc.0",
|
|
36
34
|
"@planningcenter/tapestry-react": "^4.11.5",
|
|
37
35
|
"@types/jscodeshift": "^17.3.0",
|
|
38
36
|
"@types/node": "^20.0.0",
|
|
@@ -52,5 +50,5 @@
|
|
|
52
50
|
"publishConfig": {
|
|
53
51
|
"access": "public"
|
|
54
52
|
},
|
|
55
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "7fd3d76244befe3a32ba0a79ff512f7c31db40b2"
|
|
56
54
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { createAsPropCommentTransform } from "./transforms/commentOnAsProp"
|
|
4
|
+
import { createStylePropsCleanupTransform } from "./transforms/convertStyleProps"
|
|
5
|
+
|
|
6
|
+
const TAPESTRY_REACT_PACKAGE = "@planningcenter/tapestry-react"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Collect every top-level named import from @planningcenter/tapestry-react
|
|
10
|
+
* (e.g. `import { Stack, Box as MyBox } from "@planningcenter/tapestry-react"`
|
|
11
|
+
* yields ["Stack", "Box"]). We target the *imported* name, not the local
|
|
12
|
+
* alias, because `attributeTransformFactory` already resolves aliases via
|
|
13
|
+
* `getImportName`.
|
|
14
|
+
*
|
|
15
|
+
* Member-expression targets like `<Link.Inline>` are intentionally out of
|
|
16
|
+
* scope — they're handled by the dedicated `run link` migration.
|
|
17
|
+
*/
|
|
18
|
+
function collectTapestryReactImports(
|
|
19
|
+
fileInfo: Parameters<Transform>[0],
|
|
20
|
+
api: Parameters<Transform>[1]
|
|
21
|
+
): string[] {
|
|
22
|
+
const j = api.jscodeshift
|
|
23
|
+
const source = j(fileInfo.source)
|
|
24
|
+
const names = new Set<string>()
|
|
25
|
+
|
|
26
|
+
source
|
|
27
|
+
.find(j.ImportDeclaration, {
|
|
28
|
+
source: { value: TAPESTRY_REACT_PACKAGE },
|
|
29
|
+
})
|
|
30
|
+
.forEach((path) => {
|
|
31
|
+
path.node.specifiers?.forEach((spec) => {
|
|
32
|
+
if (
|
|
33
|
+
spec.type === "ImportSpecifier" &&
|
|
34
|
+
spec.imported.type === "Identifier"
|
|
35
|
+
) {
|
|
36
|
+
names.add(spec.imported.name)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return [...names]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The `preflight` orchestrator: a cross-component assessment pass for
|
|
46
|
+
* tapestry-react. It applies each registered transform to every imported
|
|
47
|
+
* tapestry-react component in each file so the resulting report can scope
|
|
48
|
+
* the migration. Current transforms:
|
|
49
|
+
* - shorthand style-prop conversion (`createStylePropsCleanupTransform`)
|
|
50
|
+
* - `as={...}` TODO annotation (`createAsPropCommentTransform`) — flags call
|
|
51
|
+
* sites because `@planningcenter/tapestry` does not support the `as` prop
|
|
52
|
+
*
|
|
53
|
+
* Note: applying this pass produces inline `style={{}}` objects, which is
|
|
54
|
+
* rarely the desired end state and can negatively impact rendering
|
|
55
|
+
* performance. Run with `--dry-run` for assessment and use the per-component
|
|
56
|
+
* migrations (e.g. `run button`) for the real conversion.
|
|
57
|
+
*/
|
|
58
|
+
const transform: Transform = (fileInfo, api, options) => {
|
|
59
|
+
const componentNames = collectTapestryReactImports(fileInfo, api)
|
|
60
|
+
if (componentNames.length === 0) {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let currentSource = fileInfo.source
|
|
65
|
+
let hasAnyChanges = false
|
|
66
|
+
|
|
67
|
+
const transformBuilders = [
|
|
68
|
+
createStylePropsCleanupTransform,
|
|
69
|
+
createAsPropCommentTransform,
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
for (const componentName of componentNames) {
|
|
73
|
+
for (const build of transformBuilders) {
|
|
74
|
+
const individualTransform = build(componentName)
|
|
75
|
+
const result = individualTransform(
|
|
76
|
+
{ ...fileInfo, source: currentSource },
|
|
77
|
+
api,
|
|
78
|
+
options
|
|
79
|
+
)
|
|
80
|
+
if (result && result !== currentSource) {
|
|
81
|
+
currentSource = result as string
|
|
82
|
+
hasAnyChanges = true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return hasAnyChanges ? currentSource : null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default transform
|
|
@@ -0,0 +1,38 @@
|
|
|
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 COMMENT =
|
|
8
|
+
"The `as` prop is not supported in @planningcenter/tapestry. Review this usage and migrate to a supported API before upgrading."
|
|
9
|
+
|
|
10
|
+
const ANNOTATION_MARKER = "TODO: tapestry-migration (as):"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Builds a transform that flags `as={...}` usage on a single tapestry-react
|
|
14
|
+
* component with a TODO comment. `@planningcenter/tapestry` does not support
|
|
15
|
+
* the `as` prop, so these call sites need manual review before upgrading.
|
|
16
|
+
*
|
|
17
|
+
* The transform is idempotent: re-running it on an already-annotated file
|
|
18
|
+
* does not stack duplicate comments.
|
|
19
|
+
*/
|
|
20
|
+
export function createAsPropCommentTransform(componentName: string): Transform {
|
|
21
|
+
return attributeTransformFactory({
|
|
22
|
+
condition: (element) => Boolean(getAttribute({ element, name: "as" })),
|
|
23
|
+
targetComponent: componentName,
|
|
24
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
25
|
+
transform: (element, { j }) => {
|
|
26
|
+
const attribute = getAttribute({ element, name: "as" })
|
|
27
|
+
if (!attribute) return false
|
|
28
|
+
|
|
29
|
+
const alreadyAnnotated = attribute.comments?.some((comment) =>
|
|
30
|
+
comment.value.includes(ANNOTATION_MARKER)
|
|
31
|
+
)
|
|
32
|
+
if (alreadyAnnotated) return false
|
|
33
|
+
|
|
34
|
+
addCommentToAttribute({ attribute, j, text: COMMENT })
|
|
35
|
+
return true
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import preflight from "../index"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function applyPreflight(source: string, options = {}) {
|
|
9
|
+
const fileInfo = { path: "test.tsx", source }
|
|
10
|
+
const result = preflight(
|
|
11
|
+
fileInfo,
|
|
12
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
13
|
+
options
|
|
14
|
+
) as string | null
|
|
15
|
+
return result || source
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("preflight: convertStyleProps across all tapestry-react components", () => {
|
|
19
|
+
describe("discovery", () => {
|
|
20
|
+
it("no-ops when the file has no @planningcenter/tapestry-react imports", () => {
|
|
21
|
+
const source = `
|
|
22
|
+
import { Something } from "some-other-package"
|
|
23
|
+
|
|
24
|
+
export function TestComponent() {
|
|
25
|
+
return <Something padding={16} />
|
|
26
|
+
}
|
|
27
|
+
`
|
|
28
|
+
|
|
29
|
+
const result = applyPreflight(source)
|
|
30
|
+
|
|
31
|
+
// Style props on non-tapestry-react components are left alone.
|
|
32
|
+
expect(result).toContain("padding={16}")
|
|
33
|
+
expect(result).not.toContain("style={{")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("leaves a same-named import from a different package untouched", () => {
|
|
37
|
+
const source = `
|
|
38
|
+
import { Stack } from "some-other-ui-lib"
|
|
39
|
+
|
|
40
|
+
export function TestComponent() {
|
|
41
|
+
return <Stack padding={16} margin={8} />
|
|
42
|
+
}
|
|
43
|
+
`
|
|
44
|
+
|
|
45
|
+
const result = applyPreflight(source)
|
|
46
|
+
|
|
47
|
+
expect(result).toContain("padding={16}")
|
|
48
|
+
expect(result).toContain("margin={8}")
|
|
49
|
+
expect(result).not.toContain("style={{")
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe("unmigrated components (no dedicated per-component migration)", () => {
|
|
54
|
+
it("merges shorthand style props into style={{}} on Stack", () => {
|
|
55
|
+
const source = `
|
|
56
|
+
import { Stack } from "@planningcenter/tapestry-react"
|
|
57
|
+
|
|
58
|
+
export function TestComponent() {
|
|
59
|
+
return <Stack padding={16} margin={8} />
|
|
60
|
+
}
|
|
61
|
+
`
|
|
62
|
+
|
|
63
|
+
const result = applyPreflight(source)
|
|
64
|
+
|
|
65
|
+
expect(result).toMatchInlineSnapshot(`
|
|
66
|
+
"
|
|
67
|
+
import { Stack } from "@planningcenter/tapestry-react"
|
|
68
|
+
|
|
69
|
+
export function TestComponent() {
|
|
70
|
+
return (
|
|
71
|
+
<Stack
|
|
72
|
+
style={{
|
|
73
|
+
paddingTop: "128px",
|
|
74
|
+
paddingRight: "128px",
|
|
75
|
+
paddingBottom: "128px",
|
|
76
|
+
paddingLeft: "128px",
|
|
77
|
+
marginTop: "64px",
|
|
78
|
+
marginRight: "64px",
|
|
79
|
+
marginBottom: "64px",
|
|
80
|
+
marginLeft: "64px"
|
|
81
|
+
}} />
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
"
|
|
85
|
+
`)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("preserves the visible prop by routing it through style", () => {
|
|
89
|
+
const source = `
|
|
90
|
+
import { Box } from "@planningcenter/tapestry-react"
|
|
91
|
+
|
|
92
|
+
export function TestComponent() {
|
|
93
|
+
return <Box visible={false} padding={8} />
|
|
94
|
+
}
|
|
95
|
+
`
|
|
96
|
+
|
|
97
|
+
const result = applyPreflight(source)
|
|
98
|
+
|
|
99
|
+
expect(result).toContain("style={{")
|
|
100
|
+
expect(result).toContain('display: "none"')
|
|
101
|
+
expect(result).not.toContain("visible={false}")
|
|
102
|
+
expect(result).not.toContain("padding={8}")
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("leaves non-style props (like children and id) alone", () => {
|
|
106
|
+
const source = `
|
|
107
|
+
import { Stack } from "@planningcenter/tapestry-react"
|
|
108
|
+
|
|
109
|
+
export function TestComponent() {
|
|
110
|
+
return <Stack id="main" padding={16}>hello</Stack>
|
|
111
|
+
}
|
|
112
|
+
`
|
|
113
|
+
|
|
114
|
+
const result = applyPreflight(source)
|
|
115
|
+
|
|
116
|
+
expect(result).toContain('id="main"')
|
|
117
|
+
expect(result).toContain(">hello</Stack>")
|
|
118
|
+
expect(result).not.toContain("padding={16}")
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe("multiple components in one file", () => {
|
|
123
|
+
it("applies style-prop cleanup independently to each imported component", () => {
|
|
124
|
+
const source = `
|
|
125
|
+
import { Stack, Box } from "@planningcenter/tapestry-react"
|
|
126
|
+
|
|
127
|
+
export function TestComponent() {
|
|
128
|
+
return (
|
|
129
|
+
<Stack padding={16}>
|
|
130
|
+
<Box margin={8} />
|
|
131
|
+
</Stack>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
`
|
|
135
|
+
|
|
136
|
+
const result = applyPreflight(source)
|
|
137
|
+
|
|
138
|
+
expect(result).not.toContain("padding={16}")
|
|
139
|
+
expect(result).not.toContain("margin={8}")
|
|
140
|
+
expect(result.match(/style=\{\{/g)?.length ?? 0).toBeGreaterThanOrEqual(2)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe("aliased imports", () => {
|
|
145
|
+
it("handles local aliases via the factory's getImportName resolution", () => {
|
|
146
|
+
const source = `
|
|
147
|
+
import { Stack as MyStack } from "@planningcenter/tapestry-react"
|
|
148
|
+
|
|
149
|
+
export function TestComponent() {
|
|
150
|
+
return <MyStack padding={16} />
|
|
151
|
+
}
|
|
152
|
+
`
|
|
153
|
+
|
|
154
|
+
const result = applyPreflight(source)
|
|
155
|
+
|
|
156
|
+
expect(result).not.toContain("padding={16}")
|
|
157
|
+
expect(result).toContain("style={{")
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe("components that already have dedicated migrations", () => {
|
|
162
|
+
it("still applies the generic cleanup to Checkbox (user chose to include all components)", () => {
|
|
163
|
+
const source = `
|
|
164
|
+
import { Checkbox } from "@planningcenter/tapestry-react"
|
|
165
|
+
|
|
166
|
+
export function TestComponent() {
|
|
167
|
+
return <Checkbox visible={false} marginTop={16} label="Test" />
|
|
168
|
+
}
|
|
169
|
+
`
|
|
170
|
+
|
|
171
|
+
const result = applyPreflight(source)
|
|
172
|
+
|
|
173
|
+
expect(result).toContain("style={{")
|
|
174
|
+
expect(result).toContain('display: "none"')
|
|
175
|
+
expect(result).not.toContain("visible={false}")
|
|
176
|
+
expect(result).not.toContain("marginTop={16}")
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe("import preservation", () => {
|
|
181
|
+
it("does not modify the import declaration", () => {
|
|
182
|
+
const source = `
|
|
183
|
+
import { Stack } from "@planningcenter/tapestry-react"
|
|
184
|
+
|
|
185
|
+
export function TestComponent() {
|
|
186
|
+
return <Stack padding={16} />
|
|
187
|
+
}
|
|
188
|
+
`
|
|
189
|
+
|
|
190
|
+
const result = applyPreflight(source)
|
|
191
|
+
|
|
192
|
+
expect(result).toContain(
|
|
193
|
+
'import { Stack } from "@planningcenter/tapestry-react"'
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe("as prop TODO annotation", () => {
|
|
199
|
+
it("adds a TODO comment when a tapestry-react component uses as={string}", () => {
|
|
200
|
+
const source = `
|
|
201
|
+
import { Box } from "@planningcenter/tapestry-react"
|
|
202
|
+
|
|
203
|
+
export function TestComponent() {
|
|
204
|
+
return <Box as="section" />
|
|
205
|
+
}
|
|
206
|
+
`
|
|
207
|
+
|
|
208
|
+
const result = applyPreflight(source)
|
|
209
|
+
|
|
210
|
+
expect(result).toMatch(/TODO: tapestry-migration \(as\):/)
|
|
211
|
+
expect(result).toContain("not supported in @planningcenter/tapestry")
|
|
212
|
+
// The `as` prop itself must be preserved — only a comment is added.
|
|
213
|
+
expect(result).toContain('as="section"')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it("adds a TODO comment when as={} references a component", () => {
|
|
217
|
+
const source = `
|
|
218
|
+
import { Box } from "@planningcenter/tapestry-react"
|
|
219
|
+
import { MyWrapper } from "./MyWrapper"
|
|
220
|
+
|
|
221
|
+
export function TestComponent() {
|
|
222
|
+
return <Box as={MyWrapper} />
|
|
223
|
+
}
|
|
224
|
+
`
|
|
225
|
+
|
|
226
|
+
const result = applyPreflight(source)
|
|
227
|
+
|
|
228
|
+
expect(result).toMatch(/TODO: tapestry-migration \(as\):/)
|
|
229
|
+
expect(result).toContain("as={MyWrapper}")
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it("leaves components without an as prop untouched by the as-prop transform", () => {
|
|
233
|
+
const source = `
|
|
234
|
+
import { Box } from "@planningcenter/tapestry-react"
|
|
235
|
+
|
|
236
|
+
export function TestComponent() {
|
|
237
|
+
return <Box id="plain" />
|
|
238
|
+
}
|
|
239
|
+
`
|
|
240
|
+
|
|
241
|
+
const result = applyPreflight(source)
|
|
242
|
+
|
|
243
|
+
expect(result).not.toMatch(/TODO: tapestry-migration \(as\):/)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it("does not flag as={} on non-tapestry-react components", () => {
|
|
247
|
+
const source = `
|
|
248
|
+
import { Box } from "some-other-ui-lib"
|
|
249
|
+
|
|
250
|
+
export function TestComponent() {
|
|
251
|
+
return <Box as="section" />
|
|
252
|
+
}
|
|
253
|
+
`
|
|
254
|
+
|
|
255
|
+
const result = applyPreflight(source)
|
|
256
|
+
|
|
257
|
+
expect(result).not.toMatch(/TODO: tapestry-migration \(as\):/)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it("flags as={} across multiple different tapestry-react components", () => {
|
|
261
|
+
const source = `
|
|
262
|
+
import { Stack, Box } from "@planningcenter/tapestry-react"
|
|
263
|
+
|
|
264
|
+
export function TestComponent() {
|
|
265
|
+
return (
|
|
266
|
+
<Stack as="main">
|
|
267
|
+
<Box as="aside" />
|
|
268
|
+
</Stack>
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
`
|
|
272
|
+
|
|
273
|
+
const result = applyPreflight(source)
|
|
274
|
+
|
|
275
|
+
const matches = result.match(/TODO: tapestry-migration \(as\):/g) ?? []
|
|
276
|
+
expect(matches.length).toBe(2)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it("is idempotent — a second run does not stack duplicate TODO comments", () => {
|
|
280
|
+
const source = `
|
|
281
|
+
import { Box } from "@planningcenter/tapestry-react"
|
|
282
|
+
|
|
283
|
+
export function TestComponent() {
|
|
284
|
+
return <Box as="section" />
|
|
285
|
+
}
|
|
286
|
+
`
|
|
287
|
+
|
|
288
|
+
const firstPass = applyPreflight(source)
|
|
289
|
+
const secondPass = applyPreflight(firstPass)
|
|
290
|
+
|
|
291
|
+
const firstCount =
|
|
292
|
+
firstPass.match(/TODO: tapestry-migration \(as\):/g)?.length ?? 0
|
|
293
|
+
const secondCount =
|
|
294
|
+
secondPass.match(/TODO: tapestry-migration \(as\):/g)?.length ?? 0
|
|
295
|
+
|
|
296
|
+
expect(firstCount).toBe(1)
|
|
297
|
+
expect(secondCount).toBe(1)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it("handles aliased imports", () => {
|
|
301
|
+
const source = `
|
|
302
|
+
import { Box as MyBox } from "@planningcenter/tapestry-react"
|
|
303
|
+
|
|
304
|
+
export function TestComponent() {
|
|
305
|
+
return <MyBox as="section" />
|
|
306
|
+
}
|
|
307
|
+
`
|
|
308
|
+
|
|
309
|
+
const result = applyPreflight(source)
|
|
310
|
+
|
|
311
|
+
expect(result).toMatch(/TODO: tapestry-migration \(as\):/)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it("still applies style-prop cleanup on the same element that has an as prop", () => {
|
|
315
|
+
const source = `
|
|
316
|
+
import { Box } from "@planningcenter/tapestry-react"
|
|
317
|
+
|
|
318
|
+
export function TestComponent() {
|
|
319
|
+
return <Box as="section" padding={16} />
|
|
320
|
+
}
|
|
321
|
+
`
|
|
322
|
+
|
|
323
|
+
const result = applyPreflight(source)
|
|
324
|
+
|
|
325
|
+
// as prop flagged with TODO
|
|
326
|
+
expect(result).toMatch(/TODO: tapestry-migration \(as\):/)
|
|
327
|
+
expect(result).toContain('as="section"')
|
|
328
|
+
// shorthand style prop still migrated
|
|
329
|
+
expect(result).toContain("style={{")
|
|
330
|
+
expect(result).not.toContain("padding={16}")
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { stackViewPlugin } from "../../../stubs/stackViewPlugin"
|
|
4
|
+
import { stylePropTransformFactory } from "../../shared/transformFactories/stylePropTransformFactory"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds a conservative, cross-component style-prop cleanup transform for a
|
|
8
|
+
* single tapestry-react component. The orchestrator calls this once per
|
|
9
|
+
* discovered named import from `@planningcenter/tapestry-react`.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately minimal compared to per-component migrations:
|
|
12
|
+
* - Keeps only `visible` (universally kept by all existing migrations).
|
|
13
|
+
* - Uses `stackViewPlugin` — broadly correct for 7 of 8 existing components.
|
|
14
|
+
* Link's text-plugin props stay with `run link`.
|
|
15
|
+
* - No `stylesToRemove` or `stylePropMapping` — those are genuinely
|
|
16
|
+
* component-specific and belong in dedicated migrations.
|
|
17
|
+
*/
|
|
18
|
+
export function createStylePropsCleanupTransform(
|
|
19
|
+
componentName: string
|
|
20
|
+
): Transform {
|
|
21
|
+
return stylePropTransformFactory({
|
|
22
|
+
plugin: stackViewPlugin,
|
|
23
|
+
stylePropMapping: {},
|
|
24
|
+
stylesToKeep: ["visible"],
|
|
25
|
+
stylesToRemove: [],
|
|
26
|
+
targetComponent: componentName,
|
|
27
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
28
|
+
})
|
|
29
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,40 @@ program
|
|
|
70
70
|
}
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
+
program
|
|
74
|
+
.command("preflight")
|
|
75
|
+
.description(
|
|
76
|
+
"Assess migration readiness across @planningcenter/tapestry-react usage. Surfaces shorthand style props and unsupported `as` prop usage so you can plan the work. Intended for assessment — running with changes applied converts shorthand style props to inline `style={{}}` objects, which is rarely the desired end state and can negatively impact rendering performance. Use the per-component migrations (e.g. `run button`) for the real conversion."
|
|
77
|
+
)
|
|
78
|
+
.requiredOption(
|
|
79
|
+
"-p, --path <path>",
|
|
80
|
+
"REQUIRED: The path to the folder/file to assess"
|
|
81
|
+
)
|
|
82
|
+
.option("-d, --dry-run", "Preview changes without writing to files")
|
|
83
|
+
.option("-v, --verbose", "Verbose output")
|
|
84
|
+
.option(
|
|
85
|
+
"-j, --js-theme <path>",
|
|
86
|
+
"Path to JavaScript theme file containing design tokens"
|
|
87
|
+
)
|
|
88
|
+
.option(
|
|
89
|
+
"-r, --report-path <path>",
|
|
90
|
+
"Path for the migration report. Creates a report when running with changes applied and does not create a report if no additional changes are required.",
|
|
91
|
+
"MIGRATION_REPORT.md"
|
|
92
|
+
)
|
|
93
|
+
.action((options) => {
|
|
94
|
+
console.log("🛫 Running tapestry-react preflight assessment...")
|
|
95
|
+
console.log(`📁 Target: ${options.path}`)
|
|
96
|
+
console.log(
|
|
97
|
+
`🔧 Mode: ${options.dryRun ? "Dry run (preview only)" : "Apply changes"}`
|
|
98
|
+
)
|
|
99
|
+
if (!options.dryRun) {
|
|
100
|
+
console.log(
|
|
101
|
+
"⚠️ Preflight is intended for assessment. Applying changes will convert shorthand style props to inline `style={{}}` objects, which is rarely the desired end state and can hurt rendering performance. Prefer `--dry-run` with `--report-path`."
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
runTransforms("preflight", options)
|
|
105
|
+
})
|
|
106
|
+
|
|
73
107
|
program
|
|
74
108
|
.command("help")
|
|
75
109
|
.description("Show help information")
|
package/src/reportGenerator.ts
CHANGED
|
@@ -30,6 +30,8 @@ export const GLOBAL_MESSAGES = {
|
|
|
30
30
|
"Buttons inside of Tapestry Group components are not supported and should not be migrated at this time.",
|
|
31
31
|
checkbox:
|
|
32
32
|
"Checkbox sizes have adjusted. Verify visual appearance as sizes may differ slightly.",
|
|
33
|
+
preflight:
|
|
34
|
+
"Preflight is an assessment pass. It surfaces shorthand style props and `as={...}` usage on tapestry-react components so you can scope the migration. Applying its changes converts shorthand style props into inline `style={{}}` objects — that is rarely the desired end state and can negatively impact rendering performance. Prefer running with `--dry-run` and `--report-path`, then use the per-component migrations (e.g. `run button`) for the real tapestry-react → tapestry conversion.",
|
|
33
35
|
radio:
|
|
34
36
|
"Radio sizes have adjusted. Verify visual appearance as sizes may differ slightly.",
|
|
35
37
|
"toggle-switch":
|