@planningcenter/tapestry-migration-cli 2.3.0-rc.9 → 2.4.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.
Files changed (28) hide show
  1. package/dist/tapestry-react-shim.cjs +1 -1
  2. package/package.json +2 -2
  3. package/src/components/button/index.ts +2 -0
  4. package/src/components/button/transforms/convertStyleProps.test.ts +4 -5
  5. package/src/components/button/transforms/removeTypeButton.test.ts +197 -0
  6. package/src/components/button/transforms/removeTypeButton.ts +15 -0
  7. package/src/components/link/index.ts +32 -0
  8. package/src/components/link/transforms/inlineToKind.test.ts +308 -0
  9. package/src/components/link/transforms/inlineToKind.ts +51 -0
  10. package/src/components/link/transforms/targetBlankToExternal.test.ts +191 -0
  11. package/src/components/link/transforms/targetBlankToExternal.ts +30 -0
  12. package/src/components/link/transforms/toToHref.test.ts +245 -0
  13. package/src/components/link/transforms/toToHref.ts +14 -0
  14. package/src/components/shared/actions/addAttribute.test.ts +108 -0
  15. package/src/components/shared/actions/addAttribute.ts +14 -0
  16. package/src/components/shared/actions/removeAttribute.ts +9 -2
  17. package/src/components/shared/actions/transformElementName.ts +23 -9
  18. package/src/components/shared/transformFactories/attributeTransformFactory.test.ts +83 -0
  19. package/src/components/shared/transformFactories/attributeTransformFactory.ts +21 -14
  20. package/src/components/shared/transformFactories/componentTransformFactory.test.ts +85 -2
  21. package/src/components/shared/transformFactories/componentTransformFactory.ts +41 -22
  22. package/src/components/shared/transformFactories/helpers/findJSXElements.ts +37 -0
  23. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +2 -27
  24. package/src/components/shared/types.ts +19 -1
  25. package/src/index.ts +7 -2
  26. package/src/jscodeshiftRunner.ts +7 -0
  27. package/src/reportGenerator.ts +450 -0
  28. package/src/shared/types.ts +1 -0
@@ -3084,8 +3084,8 @@ const tokens = {
3084
3084
  "--t-fill-color-transparency-dark-070": "hsla(0, 0%, 0%, 0.7)",
3085
3085
  "--t-fill-color-transparency-dark-080": "hsla(0, 0%, 0%, 0.8)",
3086
3086
  "--t-fill-color-transparency-dark-090": "hsla(0, 0%, 0%, 0.9)",
3087
- "--t-surface-color-canvas": "hsl(0, 0%, 98%)",
3088
3087
  "--t-surface-color-card": "hsl(0, 0%, 100%)",
3088
+ "--t-surface-color-canvas": "hsl(0, 0%, 100%)",
3089
3089
  "--t-border-color-default-base": "hsl(0, 0%, 88%)",
3090
3090
  "--t-border-color-default-dark": "hsl(0, 0%, 81%)",
3091
3091
  "--t-border-color-default-darker": "hsl(0, 0%, 68%)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "2.3.0-rc.9",
3
+ "version": "2.4.0-rc.0",
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": "f13a677f55ac79f512edf7ba6da288dcfb53d4d2"
54
+ "gitHead": "cf4bb30dbb35a8834214333d0f9e03b125c4329c"
55
55
  }
@@ -13,6 +13,7 @@ import moveButtonImport from "./transforms/moveButtonImport"
13
13
  import removeAsButton from "./transforms/removeAsButton"
14
14
  import removeDuplicateKeys from "./transforms/removeDuplicateKeys"
15
15
  import removeToTransform from "./transforms/removeTo"
16
+ import removeTypeButton from "./transforms/removeTypeButton"
16
17
  import reviewStyles from "./transforms/reviewStyles"
17
18
  import spinnerToLoadingButton from "./transforms/spinnerToLoadingButton"
18
19
  import themeVariantToKind from "./transforms/themeVariantToKind"
@@ -57,6 +58,7 @@ const transform: Transform = async (fileInfo, api, options) => {
57
58
  removeToTransform,
58
59
  removeDuplicateKeys,
59
60
  removeAsButton,
61
+ removeTypeButton,
60
62
  auditSpreadProps,
61
63
  reviewStyles,
62
64
  convertStyleProps,
@@ -110,9 +110,9 @@ describe("convertStyleProps transform", () => {
110
110
 
111
111
  const result = applyTransform(source)
112
112
 
113
- expect(result).not.toContain('height="40px"')
113
+ expect(result).not.toContain('<Button height="40px"')
114
114
  expect(result).toContain(
115
- 'TODO: tapestry-migration (height): height has been removed as this is covered by default styling: height: "40px"'
115
+ 'TODO: tapestry-migration (styleProp): height has been removed as this is covered by default styling: height="40px"'
116
116
  )
117
117
  expect(result).toContain('kind="primary"')
118
118
  })
@@ -128,7 +128,7 @@ describe("convertStyleProps transform", () => {
128
128
 
129
129
  const result = applyTransform(source)
130
130
 
131
- expect(result).not.toContain("height={buttonHeight}")
131
+ expect(result).not.toContain("<Button height={buttonHeight}")
132
132
  expect(result).toContain("TODO: tapestry-migration")
133
133
  expect(result).toContain("buttonHeight")
134
134
  expect(result).toContain("disabled")
@@ -219,10 +219,9 @@ describe("convertStyleProps transform", () => {
219
219
  expect(result).toContain('marginLeft: "128px"')
220
220
  expect(result).toContain("}}>")
221
221
  expect(result).toContain(
222
- 'TODO: tapestry-migration (height): height has been removed as this is covered by default styling: height: "40px"'
222
+ 'TODO: tapestry-migration (styleProp): height has been removed as this is covered by default styling: height="40px"'
223
223
  )
224
224
  expect(result).not.toContain("marginLeft={16}")
225
- expect(result).not.toContain('height="40px"')
226
225
  expect(result).not.toContain('distribution="center"')
227
226
  expect(result).not.toContain('paddingTop="8px"')
228
227
  expect(result).toContain('kind="primary"')
@@ -0,0 +1,197 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./removeTypeButton"
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("removeTypeButton transform", () => {
22
+ describe("basic transformation", () => {
23
+ it("should remove type='button' from Button", () => {
24
+ const input = `
25
+ import { Button } from "@planningcenter/tapestry-react"
26
+
27
+ function Component() {
28
+ return <Button type="button">Click me</Button>
29
+ }
30
+ `
31
+
32
+ const result = applyTransform(input)
33
+
34
+ expect(result).toContain("<Button>Click me</Button>")
35
+ expect(result).not.toContain('type="button"')
36
+ })
37
+
38
+ it('should remove type="button" from Button', () => {
39
+ const input = `
40
+ import { Button } from "@planningcenter/tapestry-react"
41
+
42
+ function Component() {
43
+ return <Button type="button" onClick={handleClick}>Submit</Button>
44
+ }
45
+ `
46
+
47
+ const result = applyTransform(input)
48
+
49
+ expect(result).toContain("<Button onClick={handleClick}>Submit</Button>")
50
+ expect(result).not.toContain('type="button"')
51
+ })
52
+
53
+ it("should preserve Button without type='button'", () => {
54
+ const input = `
55
+ import { Button } from "@planningcenter/tapestry-react"
56
+
57
+ function Component() {
58
+ return <Button onClick={handleClick}>Click me</Button>
59
+ }
60
+ `
61
+
62
+ const result = applyTransform(input)
63
+
64
+ expect(result).toBeNull()
65
+ })
66
+
67
+ it("should preserve Button with type='submit'", () => {
68
+ const input = `
69
+ import { Button } from "@planningcenter/tapestry-react"
70
+
71
+ function Component() {
72
+ return <Button type="submit">Submit</Button>
73
+ }
74
+ `
75
+
76
+ const result = applyTransform(input)
77
+
78
+ expect(result).toBeNull()
79
+ })
80
+
81
+ it("should preserve Button with type='reset'", () => {
82
+ const input = `
83
+ import { Button } from "@planningcenter/tapestry-react"
84
+
85
+ function Component() {
86
+ return <Button type="reset">Reset</Button>
87
+ }
88
+ `
89
+
90
+ const result = applyTransform(input)
91
+
92
+ expect(result).toBeNull()
93
+ })
94
+ })
95
+
96
+ describe("multiple buttons", () => {
97
+ it("should handle mixed Button usage", () => {
98
+ const input = `
99
+ import { Button } from "@planningcenter/tapestry-react"
100
+
101
+ function Component() {
102
+ return (
103
+ <div>
104
+ <Button type="button">Click me</Button>
105
+ <Button type="submit">Submit</Button>
106
+ <Button type="button" onClick={handleClick}>Action</Button>
107
+ </div>
108
+ )
109
+ }
110
+ `
111
+
112
+ const result = applyTransform(input)
113
+
114
+ expect(result).toContain("<Button>Click me</Button>")
115
+ expect(result).toContain('<Button type="submit">Submit</Button>')
116
+ expect(result).toContain("<Button onClick={handleClick}>Action</Button>")
117
+ expect(result).not.toContain('type="button"')
118
+ })
119
+ })
120
+
121
+ describe("edge cases", () => {
122
+ it("should handle Button with multiple attributes including type='button'", () => {
123
+ const input = `
124
+ import { Button } from "@planningcenter/tapestry-react"
125
+
126
+ function Component() {
127
+ return <Button className="btn" type="button" disabled>Disabled</Button>
128
+ }
129
+ `
130
+
131
+ const result = applyTransform(input)
132
+
133
+ expect(result).toContain(
134
+ '<Button className="btn" disabled>Disabled</Button>'
135
+ )
136
+ expect(result).not.toContain('type="button"')
137
+ })
138
+
139
+ it("should not affect other components with type='button'", () => {
140
+ const input = `
141
+ import { Button } from "@planningcenter/tapestry-react"
142
+
143
+ function Component() {
144
+ return (
145
+ <div>
146
+ <Button type="button">Tapestry Button</Button>
147
+ <button type="button">HTML Button</button>
148
+ </div>
149
+ )
150
+ }
151
+ `
152
+
153
+ const result = applyTransform(input)
154
+
155
+ expect(result).toContain("<Button>Tapestry Button</Button>")
156
+ expect(result).toContain('<button type="button">HTML Button</button>')
157
+ })
158
+
159
+ it("should handle Button with expression type", () => {
160
+ const input = `
161
+ import { Button } from "@planningcenter/tapestry-react"
162
+
163
+ function Component() {
164
+ return <Button type={"button"}>Click me</Button>
165
+ }
166
+ `
167
+
168
+ const result = applyTransform(input)
169
+
170
+ if (result) {
171
+ expect(result).toContain("<Button>Click me</Button>")
172
+ expect(result).not.toContain('type={"button"}')
173
+ } else {
174
+ // If no transformation occurred, the original should still have the expression
175
+ expect(input).toContain('type={"button"}')
176
+ }
177
+ })
178
+ })
179
+
180
+ describe("import handling", () => {
181
+ it("should not affect imports", () => {
182
+ const input = `
183
+ import { Button } from "@planningcenter/tapestry-react"
184
+
185
+ function Component() {
186
+ return <Button type="button">Click me</Button>
187
+ }
188
+ `
189
+
190
+ const result = applyTransform(input)
191
+
192
+ expect(result).toContain(
193
+ 'import { Button } from "@planningcenter/tapestry-react"'
194
+ )
195
+ })
196
+ })
197
+ })
@@ -0,0 +1,15 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { removeAttribute } from "../../shared/actions/removeAttribute"
4
+ import { hasAttributeValue } from "../../shared/conditions/hasAttributeValue"
5
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
6
+
7
+ const transform: Transform = attributeTransformFactory({
8
+ condition: hasAttributeValue("type", "button"),
9
+ targetComponent: "Button",
10
+ targetPackage: "@planningcenter/tapestry-react",
11
+ transform: (element, { j, source }) =>
12
+ removeAttribute("type", { element, j, source }),
13
+ })
14
+
15
+ export default transform
@@ -0,0 +1,32 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import inlineToKind from "./transforms/inlineToKind"
4
+ import targetBlankToExternal from "./transforms/targetBlankToExternal"
5
+ import toToHref from "./transforms/toToHref"
6
+
7
+ const transform: Transform = (fileInfo, api, options) => {
8
+ let currentSource = fileInfo.source
9
+ let hasAnyChanges = false
10
+
11
+ const transforms: Transform[] = [
12
+ inlineToKind,
13
+ toToHref,
14
+ targetBlankToExternal,
15
+ ]
16
+
17
+ for (const individualTransform of transforms) {
18
+ const result = individualTransform(
19
+ { ...fileInfo, source: currentSource },
20
+ api,
21
+ options
22
+ )
23
+ if (result && result !== currentSource) {
24
+ currentSource = result as string
25
+ hasAnyChanges = true
26
+ }
27
+ }
28
+
29
+ return hasAnyChanges ? currentSource : null
30
+ }
31
+
32
+ export default transform
@@ -0,0 +1,308 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./inlineToKind"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string | null {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ return transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ {}
14
+ ) as string | null
15
+ }
16
+
17
+ describe("inlineToKind transform", () => {
18
+ describe("basic transformations", () => {
19
+ it("should transform Link.Inline to Link with kind='inline-text'", () => {
20
+ const input = `
21
+ import { Link } from "@planningcenter/tapestry-react"
22
+
23
+ export default function Test() {
24
+ return <Link.Inline href="/profile">Profile</Link.Inline>
25
+ }
26
+ `.trim()
27
+
28
+ const expected = `
29
+ import { Link } from "@planningcenter/tapestry-react"
30
+
31
+ export default function Test() {
32
+ return <Link href="/profile" kind="inline-text">Profile</Link>;
33
+ }
34
+ `.trim()
35
+
36
+ const result = applyTransform(input)
37
+ expect(result?.trim()).toBe(expected)
38
+ })
39
+
40
+ it("should preserve all existing props when transforming Link.Inline", () => {
41
+ const input = `
42
+ import { Link } from "@planningcenter/tapestry-react"
43
+
44
+ export default function Test() {
45
+ return (
46
+ <Link.Inline
47
+ href="/dashboard"
48
+ className="nav-link"
49
+ target="_blank"
50
+ rel="noopener"
51
+ data-testid="profile-link"
52
+ >
53
+ Dashboard
54
+ </Link.Inline>
55
+ )
56
+ }
57
+ `.trim()
58
+
59
+ const result = applyTransform(input)
60
+ expect(result).toContain('kind="inline-text"')
61
+ expect(result).toContain('href="/dashboard"')
62
+ expect(result).toContain('className="nav-link"')
63
+ expect(result).toContain('target="_blank"')
64
+ expect(result).toContain('rel="noopener"')
65
+ expect(result).toContain('data-testid="profile-link"')
66
+ expect(result).toContain('kind="inline-text"')
67
+ expect(result).toContain("<Link")
68
+ expect(result).not.toContain("Link.Inline")
69
+ })
70
+
71
+ it("should handle self-closing Link.Inline tags", () => {
72
+ const input = `
73
+ import { Link } from "@planningcenter/tapestry-react"
74
+
75
+ export default function Test() {
76
+ return <Link.Inline href="/dashboard" />
77
+ }
78
+ `.trim()
79
+
80
+ const expected = `
81
+ import { Link } from "@planningcenter/tapestry-react"
82
+
83
+ export default function Test() {
84
+ return <Link href="/dashboard" kind="inline-text" />;
85
+ }
86
+ `.trim()
87
+
88
+ const result = applyTransform(input)
89
+ expect(result?.trim()).toBe(expected)
90
+ })
91
+
92
+ it("should handle multiple Link.Inline components", () => {
93
+ const input = `
94
+ import { Link } from "@planningcenter/tapestry-react"
95
+
96
+ export default function Test() {
97
+ return (
98
+ <div>
99
+ <Link.Inline href="/home">Home</Link.Inline>
100
+ <Link.Inline href="/about">About</Link.Inline>
101
+ </div>
102
+ )
103
+ }
104
+ `.trim()
105
+
106
+ const result = applyTransform(input)
107
+ expect(result).toContain(
108
+ '<Link href="/home" kind="inline-text">Home</Link>'
109
+ )
110
+ expect(result).toContain(
111
+ '<Link href="/about" kind="inline-text">About</Link>'
112
+ )
113
+ expect(result).not.toContain("Link.Inline")
114
+ })
115
+ })
116
+
117
+ describe("mixed scenarios", () => {
118
+ it("should only transform Link.Inline and leave regular Link unchanged", () => {
119
+ const input = `
120
+ import { Link } from "@planningcenter/tapestry-react"
121
+
122
+ export default function Test() {
123
+ return (
124
+ <nav>
125
+ <Link href="/home">Home</Link>
126
+ <Link.Inline href="/profile">Profile</Link.Inline>
127
+ <Link as="button" href="/dashboard">Dashboard</Link>
128
+ </nav>
129
+ )
130
+ }
131
+ `.trim()
132
+
133
+ const result = applyTransform(input)
134
+ expect(result).toContain('<Link href="/home">Home</Link>')
135
+ expect(result).toContain(
136
+ '<Link href="/profile" kind="inline-text">Profile</Link>'
137
+ )
138
+ expect(result).toContain(
139
+ '<Link as="button" href="/dashboard">Dashboard</Link>'
140
+ )
141
+ expect(result).not.toContain("Link.Inline")
142
+ })
143
+
144
+ it("should handle aliased Link import", () => {
145
+ const input = `
146
+ import { Link as TapestryLink } from "@planningcenter/tapestry-react"
147
+
148
+ export default function Test() {
149
+ return <TapestryLink.Inline href="/profile">Profile</TapestryLink.Inline>
150
+ }
151
+ `.trim()
152
+
153
+ const result = applyTransform(input)
154
+ expect(result).toContain(
155
+ '<TapestryLink href="/profile" kind="inline-text">Profile</TapestryLink>'
156
+ )
157
+ expect(result).not.toContain("TapestryLink.Inline")
158
+ })
159
+
160
+ it("should handle complex JSX expressions", () => {
161
+ const input = `
162
+ import { Link } from "@planningcenter/tapestry-react"
163
+
164
+ export default function Test({ items, showInline }) {
165
+ return (
166
+ <div>
167
+ {items.map(item => (
168
+ <Link.Inline key={item.id} href={item.url}>{item.name}</Link.Inline>
169
+ ))}
170
+ {showInline && <Link.Inline href="/conditional">Conditional</Link.Inline>}
171
+ </div>
172
+ )
173
+ }
174
+ `.trim()
175
+
176
+ const result = applyTransform(input)
177
+ expect(result).toContain(
178
+ '<Link key={item.id} href={item.url} kind="inline-text">{item.name}</Link>'
179
+ )
180
+ expect(result).toContain(
181
+ '<Link href="/conditional" kind="inline-text">Conditional</Link>'
182
+ )
183
+ expect(result).not.toContain("Link.Inline")
184
+ })
185
+ })
186
+
187
+ describe("edge cases", () => {
188
+ it("should not transform if Link is not imported from tapestry-react", () => {
189
+ const input = `
190
+ import { Link } from "react-router-dom"
191
+
192
+ export default function Test() {
193
+ return <Link.Inline to="/profile">Profile</Link.Inline>
194
+ }
195
+ `.trim()
196
+
197
+ const result = applyTransform(input)
198
+ expect(result).toBe(null) // No changes needed
199
+ })
200
+
201
+ it("should handle Link.Inline with dynamic props", () => {
202
+ const input = `
203
+ import { Link } from "@planningcenter/tapestry-react"
204
+
205
+ export default function Test({ url, isExternal }) {
206
+ return (
207
+ <Link.Inline
208
+ href={url}
209
+ target={isExternal ? "_blank" : "_self"}
210
+ className={\`link-\${isExternal ? 'external' : 'internal'}\`}
211
+ >
212
+ Dynamic Link
213
+ </Link.Inline>
214
+ )
215
+ }
216
+ `.trim()
217
+
218
+ const result = applyTransform(input)
219
+ expect(result).toContain('kind="inline-text"')
220
+ expect(result).toContain("<Link")
221
+ expect(result).toContain("href={url}")
222
+ expect(result).toContain('target={isExternal ? "_blank" : "_self"}')
223
+ expect(result).toContain(
224
+ "className={`link-${isExternal ? 'external' : 'internal'}`}"
225
+ )
226
+ expect(result).not.toContain("Link.Inline")
227
+ })
228
+
229
+ it("should handle nested Link.Inline components", () => {
230
+ const input = `
231
+ import { Link } from "@planningcenter/tapestry-react"
232
+
233
+ export default function Test() {
234
+ return (
235
+ <div>
236
+ <p>
237
+ Visit our <Link.Inline href="/about">about page</Link.Inline> for more info.
238
+ </p>
239
+ <span>
240
+ Or check the <Link.Inline href="/help">help section</Link.Inline>.
241
+ </span>
242
+ </div>
243
+ )
244
+ }
245
+ `.trim()
246
+
247
+ const result = applyTransform(input)
248
+ expect(result).toContain(
249
+ '<Link href="/about" kind="inline-text">about page</Link>'
250
+ )
251
+ expect(result).toContain(
252
+ '<Link href="/help" kind="inline-text">help section</Link>'
253
+ )
254
+ expect(result).not.toContain("Link.Inline")
255
+ })
256
+ })
257
+
258
+ describe("no changes scenarios", () => {
259
+ it("should return null when no Link.Inline components exist", () => {
260
+ const input = `
261
+ import { Link } from "@planningcenter/tapestry-react"
262
+
263
+ export default function Test() {
264
+ return <Link href="/home">Home</Link>
265
+ }
266
+ `.trim()
267
+
268
+ const result = applyTransform(input)
269
+ expect(result).toBe(null)
270
+ })
271
+
272
+ it("should return null when no tapestry-react imports exist", () => {
273
+ const input = `
274
+ import React from "react"
275
+
276
+ export default function Test() {
277
+ return <div>No Links here</div>
278
+ }
279
+ `.trim()
280
+
281
+ const result = applyTransform(input)
282
+ expect(result).toBe(null)
283
+ })
284
+
285
+ it("should return null for empty file", () => {
286
+ const result = applyTransform("")
287
+ expect(result).toBe(null)
288
+ })
289
+
290
+ it("should return null when Link is imported but no Link.Inline used", () => {
291
+ const input = `
292
+ import { Link, Button } from "@planningcenter/tapestry-react"
293
+
294
+ export default function Test() {
295
+ return (
296
+ <div>
297
+ <Link href="/home">Home</Link>
298
+ <Button>Click me</Button>
299
+ </div>
300
+ )
301
+ }
302
+ `.trim()
303
+
304
+ const result = applyTransform(input)
305
+ expect(result).toBe(null)
306
+ })
307
+ })
308
+ })
@@ -0,0 +1,51 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { addAttribute } from "../../shared/actions/addAttribute"
4
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
5
+ import { componentTransformFactory } from "../../shared/transformFactories/componentTransformFactory"
6
+
7
+ // Step 1: Add kind="inline-text" attribute to Link.Inline elements
8
+ const addKindAttribute = attributeTransformFactory({
9
+ condition: () => true, // Add to all Link.Inline elements
10
+ targetComponent: "Link.Inline",
11
+ targetPackage: "@planningcenter/tapestry-react",
12
+ transform: (element, { j }) => {
13
+ addAttribute({
14
+ element,
15
+ j,
16
+ name: "kind",
17
+ value: "inline-text",
18
+ })
19
+ return true
20
+ },
21
+ })
22
+
23
+ // Step 2: Transform Link.Inline to Link (preserving original component name)
24
+ const transformComponent = componentTransformFactory({
25
+ condition: () => true, // Transform all Link.Inline elements
26
+ fromComponent: "Link.Inline",
27
+ fromPackage: "@planningcenter/tapestry-react",
28
+ toComponent: "Link",
29
+ toPackage: "@planningcenter/tapestry-react",
30
+ })
31
+
32
+ // Combined transform that runs both steps
33
+ const transform: Transform = (fileInfo, api, options) => {
34
+ // Step 1: Add kind attribute to Link.Inline elements
35
+ const result1 = addKindAttribute(fileInfo, api, options)
36
+
37
+ if (!result1) {
38
+ return null
39
+ }
40
+
41
+ // Step 2: Transform Link.Inline to Link
42
+ const result2 = transformComponent(
43
+ { ...fileInfo, source: result1 as string },
44
+ api,
45
+ options
46
+ )
47
+
48
+ return result2 || result1
49
+ }
50
+
51
+ export default transform