@planningcenter/tapestry-migration-cli 2.4.0-rc.3 → 2.4.0-rc.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "2.4.0-rc.3",
3
+ "version": "2.4.0-rc.5",
4
4
  "description": "CLI tool for Tapestry migrations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -51,5 +51,5 @@
51
51
  "publishConfig": {
52
52
  "access": "public"
53
53
  },
54
- "gitHead": "4b1aa9118dae6fcb49141a4c0f9d340deaa9cfae"
54
+ "gitHead": "f90b3a7efefc3e67294eb7f0bf44376bae282949"
55
55
  }
@@ -1,7 +1,9 @@
1
1
  import { Transform } from "jscodeshift"
2
2
 
3
+ import convertStyleProps from "./transforms/convertStyleProps"
3
4
  import inlineToKind from "./transforms/inlineToKind"
4
5
  import removeAs from "./transforms/removeAs"
6
+ import reviewStyles from "./transforms/reviewStyles"
5
7
  import targetBlankToExternal from "./transforms/targetBlankToExternal"
6
8
  import toToHref from "./transforms/toToHref"
7
9
 
@@ -14,6 +16,8 @@ const transform: Transform = (fileInfo, api, options) => {
14
16
  toToHref,
15
17
  targetBlankToExternal,
16
18
  removeAs,
19
+ reviewStyles,
20
+ convertStyleProps,
17
21
  ]
18
22
 
19
23
  for (const individualTransform of transforms) {
@@ -0,0 +1,239 @@
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, options = {}) {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ return transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ options
14
+ ) as string | null
15
+ }
16
+
17
+ describe("convertStyleProps transform", () => {
18
+ describe("visible prop - handled by theme system", () => {
19
+ it("should convert visible={false} to display: none like Button", () => {
20
+ const source = `
21
+ import { Link } from "@planningcenter/tapestry-react"
22
+
23
+ export function TestComponent() {
24
+ return <Link visible={false} href="/hidden">Hidden Link</Link>
25
+ }
26
+ `
27
+
28
+ const result = applyTransform(source)
29
+
30
+ expect(result).toContain("style={{")
31
+ expect(result).toContain('display: "none"')
32
+ expect(result).not.toContain("visible={false}")
33
+ })
34
+
35
+ it("should remove visible={true} with no style changes (true is default)", () => {
36
+ const source = `
37
+ import { Link } from "@planningcenter/tapestry-react"
38
+
39
+ export function TestComponent() {
40
+ return <Link visible={true} href="/visible">Visible Link</Link>
41
+ }
42
+ `
43
+
44
+ const result = applyTransform(source)
45
+
46
+ // visible={true} is default behavior, so it should be removed with no style added
47
+ expect(result).not.toContain("visible={true}")
48
+ expect(result).not.toContain("style={{") // No style should be added
49
+ expect(result).toContain('<Link href="/visible">Visible Link</Link>')
50
+ })
51
+ })
52
+
53
+ describe("supported props - preserved like Button", () => {
54
+ it("should preserve size prop as-is like Button does", () => {
55
+ const source = `
56
+ import { Link } from "@planningcenter/tapestry-react"
57
+
58
+ export function TestComponent() {
59
+ return <Link size="lg" href="/sized">Sized Link</Link>
60
+ }
61
+ `
62
+
63
+ const result = applyTransform(source)
64
+
65
+ // size should be preserved as a supported prop, not converted to CSS
66
+ expect(result).toBeNull() // No transformation needed
67
+ })
68
+
69
+ it("should preserve all valid Link size values", () => {
70
+ const sizes = ["xs", "sm", "md", "lg", "xl"]
71
+
72
+ sizes.forEach((size) => {
73
+ const source = `
74
+ import { Link } from "@planningcenter/tapestry-react"
75
+
76
+ export function TestComponent() {
77
+ return <Link size="${size}" href="/test">Test Link</Link>
78
+ }
79
+ `
80
+
81
+ const result = applyTransform(source)
82
+
83
+ // No transformation should occur for valid size props
84
+ expect(result).toBeNull()
85
+ })
86
+ })
87
+
88
+ // Note: underline and truncate props will be handled by future custom transforms
89
+ // since they require boolean-to-string conversion that stylePropMapping doesn't support
90
+ })
91
+
92
+ describe("standard style props", () => {
93
+ it("should convert standard style props to style object", () => {
94
+ const source = `
95
+ import { Link } from "@planningcenter/tapestry-react"
96
+
97
+ export function TestComponent() {
98
+ return (
99
+ <Link
100
+ marginBottom={8}
101
+ color="blue"
102
+ fontSize="14px"
103
+ href="/styled"
104
+ >
105
+ Styled Link
106
+ </Link>
107
+ )
108
+ }
109
+ `
110
+
111
+ const result = applyTransform(source)
112
+
113
+ expect(result).toContain("style={{")
114
+ expect(result).toContain('color: "blue"')
115
+ expect(result).toContain('fontSize: "14px"')
116
+ expect(result).not.toContain("marginBottom={8}")
117
+ expect(result).not.toContain('color="blue"')
118
+ expect(result).not.toContain('fontSize="14px"')
119
+ })
120
+ })
121
+
122
+ describe("mixed props", () => {
123
+ it("should handle combination of visible and standard style props", () => {
124
+ const source = `
125
+ import { Link } from "@planningcenter/tapestry-react"
126
+
127
+ export function TestComponent() {
128
+ return (
129
+ <Link
130
+ visible={false}
131
+ marginLeft={16}
132
+ color="red"
133
+ href="/mixed"
134
+ >
135
+ Mixed Props
136
+ </Link>
137
+ )
138
+ }
139
+ `
140
+
141
+ const result = applyTransform(source)
142
+
143
+ expect(result).toContain("style={{")
144
+ expect(result).toContain('display: "none"') // from visible={false}
145
+ expect(result).toContain('color: "red"') // standard style prop
146
+ expect(result).not.toContain("visible={false}")
147
+ expect(result).not.toContain("marginLeft={16}")
148
+ })
149
+
150
+ it("should handle existing style prop and merge with converted props", () => {
151
+ const source = `
152
+ import { Link } from "@planningcenter/tapestry-react"
153
+
154
+ export function TestComponent() {
155
+ return (
156
+ <Link
157
+ style={{ padding: "10px", border: "1px solid gray" }}
158
+ visible={false}
159
+ marginTop={8}
160
+ href="/existing"
161
+ >
162
+ Existing Style
163
+ </Link>
164
+ )
165
+ }
166
+ `
167
+
168
+ const result = applyTransform(source)
169
+
170
+ expect(result).toContain("style={{")
171
+ expect(result).toContain('display: "none"') // from visible={false}
172
+ expect(result).not.toContain("visible={false}")
173
+ expect(result).not.toContain("marginTop={8}")
174
+ })
175
+ })
176
+
177
+ describe("no style props", () => {
178
+ it("should return null when no style props are present", () => {
179
+ const source = `
180
+ import { Link } from "@planningcenter/tapestry-react"
181
+
182
+ export function TestComponent() {
183
+ return <Link href="/plain" external>Plain Link</Link>
184
+ }
185
+ `
186
+
187
+ const result = applyTransform(source)
188
+
189
+ expect(result).toBeNull()
190
+ })
191
+ })
192
+
193
+ describe("variable expressions", () => {
194
+ it("should handle variable expressions in style props", () => {
195
+ const source = `
196
+ import { Link } from "@planningcenter/tapestry-react"
197
+
198
+ export function TestComponent() {
199
+ const isVisible = true
200
+ const linkColor = "purple"
201
+ return (
202
+ <Link
203
+ visible={isVisible}
204
+ color={linkColor}
205
+ href="/variables"
206
+ >
207
+ Variable Link
208
+ </Link>
209
+ )
210
+ }
211
+ `
212
+
213
+ const result = applyTransform(source)
214
+
215
+ expect(result).toContain("style={{")
216
+ expect(result).toContain("color: linkColor")
217
+ expect(result).not.toContain("visible={isVisible}")
218
+ expect(result).not.toContain("color={linkColor}")
219
+ })
220
+ })
221
+
222
+ describe("import handling", () => {
223
+ it("should not affect imports", () => {
224
+ const source = `
225
+ import { Link } from "@planningcenter/tapestry-react"
226
+
227
+ export function TestComponent() {
228
+ return <Link visible={false} href="/test">Test</Link>
229
+ }
230
+ `
231
+
232
+ const result = applyTransform(source)
233
+
234
+ expect(result).toContain(
235
+ 'import { Link } from "@planningcenter/tapestry-react"'
236
+ )
237
+ })
238
+ })
239
+ })
@@ -0,0 +1,8 @@
1
+ import { stylePropTransformFactory } from "../../shared/transformFactories/stylePropTransformFactory"
2
+
3
+ export default stylePropTransformFactory({
4
+ stylesToKeep: ["visible"],
5
+ stylesToRemove: [],
6
+ targetComponent: "Link",
7
+ targetPackage: "@planningcenter/tapestry-react",
8
+ })
@@ -0,0 +1,172 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./reviewStyles"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ // Helper to run transform and get result
9
+ function applyTransform(source: string): string | null {
10
+ const fileInfo = { path: "test.tsx", source }
11
+ const api = {
12
+ j,
13
+ jscodeshift: j,
14
+ report: () => {},
15
+ stats: () => {},
16
+ }
17
+ const result = transform(fileInfo, api, {})
18
+ return result as string | null
19
+ }
20
+
21
+ describe("reviewStyles transform", () => {
22
+ describe("basic transformation", () => {
23
+ it("should add comment to Link with style attribute", () => {
24
+ const input = `
25
+ import { Link } from "@planningcenter/tapestry-react"
26
+
27
+ function Component() {
28
+ return <Link style={{ color: "blue" }} href="/home">Home</Link>
29
+ }
30
+ `
31
+
32
+ const result = applyTransform(input)
33
+
34
+ expect(result).toContain("TODO: tapestry-migration")
35
+ expect(result).toContain(
36
+ "review custom styles - Link styles may need to be updated for new design system"
37
+ )
38
+ expect(result).toContain('style={{ color: "blue" }}')
39
+ })
40
+
41
+ it("should handle Link with complex style object", () => {
42
+ const input = `
43
+ import { Link } from "@planningcenter/tapestry-react"
44
+
45
+ function Component() {
46
+ return (
47
+ <Link
48
+ style={{
49
+ color: "red",
50
+ fontSize: "14px",
51
+ textDecoration: "none"
52
+ }}
53
+ href="/about"
54
+ >
55
+ About
56
+ </Link>
57
+ )
58
+ }
59
+ `
60
+
61
+ const result = applyTransform(input)
62
+
63
+ expect(result).toContain("TODO: tapestry-migration")
64
+ expect(result).toContain("review custom styles")
65
+ expect(result).toContain('color: "red"')
66
+ expect(result).toContain('fontSize: "14px"')
67
+ expect(result).toContain('textDecoration: "none"')
68
+ })
69
+
70
+ it("should preserve Link without style attribute", () => {
71
+ const input = `
72
+ import { Link } from "@planningcenter/tapestry-react"
73
+
74
+ function Component() {
75
+ return <Link href="/home">Home</Link>
76
+ }
77
+ `
78
+
79
+ const result = applyTransform(input)
80
+
81
+ expect(result).toBeNull()
82
+ })
83
+ })
84
+
85
+ describe("multiple links", () => {
86
+ it("should handle mixed Link usage", () => {
87
+ const input = `
88
+ import { Link } from "@planningcenter/tapestry-react"
89
+
90
+ function Component() {
91
+ return (
92
+ <div>
93
+ <Link style={{ color: "blue" }} href="/home">Home</Link>
94
+ <Link href="/about">About</Link>
95
+ <Link style={{ fontSize: "16px" }} href="/contact">Contact</Link>
96
+ </div>
97
+ )
98
+ }
99
+ `
100
+
101
+ const result = applyTransform(input)
102
+
103
+ expect(result).toContain("TODO: tapestry-migration")
104
+ expect(result).toContain("review custom styles")
105
+ // Should have comments for both styled links
106
+ const commentCount = (result?.match(/TODO: tapestry-migration/g) || [])
107
+ .length
108
+ expect(commentCount).toBe(2)
109
+ })
110
+ })
111
+
112
+ describe("edge cases", () => {
113
+ it("should handle Link with style expression", () => {
114
+ const input = `
115
+ import { Link } from "@planningcenter/tapestry-react"
116
+
117
+ function Component() {
118
+ const linkStyles = { color: "green" }
119
+ return <Link style={linkStyles} href="/dynamic">Dynamic</Link>
120
+ }
121
+ `
122
+
123
+ const result = applyTransform(input)
124
+
125
+ expect(result).toContain("TODO: tapestry-migration")
126
+ expect(result).toContain("review custom styles")
127
+ expect(result).toContain("style={linkStyles}")
128
+ })
129
+
130
+ it("should not affect other components with style", () => {
131
+ const input = `
132
+ import { Link } from "@planningcenter/tapestry-react"
133
+
134
+ function Component() {
135
+ return (
136
+ <div>
137
+ <Link style={{ color: "blue" }} href="/home">Link</Link>
138
+ <div style={{ padding: "10px" }}>Div</div>
139
+ </div>
140
+ )
141
+ }
142
+ `
143
+
144
+ const result = applyTransform(input)
145
+
146
+ expect(result).toContain("TODO: tapestry-migration")
147
+ // Should only have one comment (for Link, not div)
148
+ const commentCount = (result?.match(/TODO: tapestry-migration/g) || [])
149
+ .length
150
+ expect(commentCount).toBe(1)
151
+ expect(result).toContain('<div style={{ padding: "10px" }}>Div</div>')
152
+ })
153
+ })
154
+
155
+ describe("import handling", () => {
156
+ it("should not affect imports", () => {
157
+ const input = `
158
+ import { Link } from "@planningcenter/tapestry-react"
159
+
160
+ function Component() {
161
+ return <Link style={{ color: "blue" }} href="/home">Home</Link>
162
+ }
163
+ `
164
+
165
+ const result = applyTransform(input)
166
+
167
+ expect(result).toContain(
168
+ 'import { Link } from "@planningcenter/tapestry-react"'
169
+ )
170
+ })
171
+ })
172
+ })
@@ -0,0 +1,17 @@
1
+ import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
2
+ import { getAttribute } from "../../shared/actions/getAttribute"
3
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
4
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
5
+
6
+ const COMMENT = `review custom styles - Link styles may need to be updated for new design system.`
7
+
8
+ export default attributeTransformFactory({
9
+ condition: hasAttribute("style"),
10
+ targetComponent: "Link",
11
+ targetPackage: "@planningcenter/tapestry-react",
12
+ transform: (element, { j }) => {
13
+ const attribute = getAttribute({ element, name: "style" })!
14
+ addCommentToAttribute({ attribute, j, text: COMMENT })
15
+ return true
16
+ },
17
+ })
@@ -132,6 +132,20 @@ export default function Test() {
132
132
  expect(result).toContain('href="/page3"') // Should remain unchanged
133
133
  })
134
134
 
135
+ it("should handle target={'_blank'} format", () => {
136
+ const input = `
137
+ import { Link } from "@planningcenter/tapestry-react"
138
+
139
+ export default function Test() {
140
+ return <Link href="/external" target={'_blank'}>External Link</Link>
141
+ }
142
+ `.trim()
143
+
144
+ const result = applyTransform(input)
145
+ expect(result).toContain("external")
146
+ expect(result).not.toContain("target={'_blank'}")
147
+ })
148
+
135
149
  it("should handle dynamic target values", () => {
136
150
  const input = `
137
151
  import { Link } from "@planningcenter/tapestry-react"
@@ -32,7 +32,28 @@ describe("hasAttributeValue", () => {
32
32
  expect(condition(element)).toBe(false)
33
33
  })
34
34
 
35
- it("should return false for expression values", () => {
35
+ it("should return true for expression with string literal", () => {
36
+ const condition = hasAttributeValue("target", "_blank")
37
+ const element = createJSXElement(" target={'_blank'}")
38
+
39
+ expect(condition(element)).toBe(true)
40
+ })
41
+
42
+ it("should return true for expression with double quotes", () => {
43
+ const condition = hasAttributeValue("target", "_blank")
44
+ const element = createJSXElement(' target={"_blank"}')
45
+
46
+ expect(condition(element)).toBe(true)
47
+ })
48
+
49
+ it("should return false for expression with different value", () => {
50
+ const condition = hasAttributeValue("target", "_blank")
51
+ const element = createJSXElement(" target={'_self'}")
52
+
53
+ expect(condition(element)).toBe(false)
54
+ })
55
+
56
+ it("should return false for complex expression values", () => {
36
57
  const condition = hasAttributeValue("onClick", "handleClick")
37
58
  const element = createJSXElement(" onClick={handleClick}")
38
59
 
@@ -4,6 +4,7 @@ import { TransformCondition } from "../types"
4
4
 
5
5
  /**
6
6
  * Helper function to create a condition that checks for an attribute with a specific value
7
+ * Handles both string literals and expressions with string literals
7
8
  */
8
9
  export function hasAttributeValue(
9
10
  attributeName: string,
@@ -11,13 +12,30 @@ export function hasAttributeValue(
11
12
  ): TransformCondition {
12
13
  return (element: JSXElement) => {
13
14
  const attributes = element.openingElement.attributes || []
14
- return attributes.some(
15
- (attr) =>
15
+ return attributes.some((attr) => {
16
+ const hasAttribute =
16
17
  attr.type === "JSXAttribute" &&
17
18
  attr.name?.type === "JSXIdentifier" &&
18
- attr.name.name === attributeName &&
19
- attr.value?.type === "StringLiteral" &&
20
- attr.value.value === value
21
- )
19
+ attr.name.name === attributeName
20
+
21
+ if (!hasAttribute) {
22
+ return false
23
+ }
24
+
25
+ // Handle string literal: attribute="value"
26
+ if (attr.value?.type === "StringLiteral") {
27
+ return attr.value.value === value
28
+ }
29
+
30
+ // Handle expression: attribute={"value"} or attribute={'value'}
31
+ if (attr.value?.type === "JSXExpressionContainer") {
32
+ const { expression } = attr.value
33
+ if (expression.type === "StringLiteral") {
34
+ return expression.value === value
35
+ }
36
+ }
37
+
38
+ return false
39
+ })
22
40
  }
23
41
  }
@@ -319,7 +319,10 @@ export function stylePropTransformFactory(config: {
319
319
  styles = { ...styles, ...directStyleProps }
320
320
  if (options.verbose) console.log("Final generated styles:", styles)
321
321
 
322
- applyStylesToComponent({ element, j, styles })
322
+ // Only apply styles if there are actual CSS properties to add
323
+ if (Object.keys(styles).length > 0) {
324
+ applyStylesToComponent({ element, j, styles })
325
+ }
323
326
  } catch (error) {
324
327
  console.log("Error processing style props:", error)
325
328
  console.log("Style props that caused error:", allStyleProps)