@planningcenter/tapestry-migration-cli 2.2.0-rc.1 → 2.2.0-rc.3

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.2.0-rc.1",
3
+ "version": "2.2.0-rc.3",
4
4
  "description": "CLI tool for Tapestry migrations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -47,5 +47,5 @@
47
47
  "publishConfig": {
48
48
  "access": "public"
49
49
  },
50
- "gitHead": "8cd78a562ebd134c299ef2feb1f7e52c5d3e7896"
50
+ "gitHead": "dfc70b40ec42c964fe0744a0d982757e695e41ba"
51
51
  }
@@ -2,13 +2,19 @@ import { Transform } from "jscodeshift"
2
2
 
3
3
  import linkToButton from "./transforms/linkToButton"
4
4
  import moveButtonImport from "./transforms/moveButtonImport"
5
+ import removeToTransform from "./transforms/removeTo"
5
6
  import titleToLabel from "./transforms/titleToLabel"
6
7
 
7
8
  const transform: Transform = (fileInfo, api, options) => {
8
9
  let currentSource = fileInfo.source
9
10
  let hasAnyChanges = false
10
11
 
11
- const transforms: Transform[] = [linkToButton, titleToLabel, moveButtonImport]
12
+ const transforms: Transform[] = [
13
+ linkToButton,
14
+ titleToLabel,
15
+ removeToTransform,
16
+ moveButtonImport,
17
+ ]
12
18
 
13
19
  for (const individualTransform of transforms) {
14
20
  const result = individualTransform(
@@ -0,0 +1,370 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./removeTo"
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("removeTo transform", () => {
18
+ describe("basic transformations", () => {
19
+ it("should remove 'to' attribute with empty string value", () => {
20
+ const input = `
21
+ import { Button } from "@planningcenter/tapestry-react"
22
+
23
+ export default function Test() {
24
+ return <Button to="">Save</Button>
25
+ }
26
+ `.trim()
27
+
28
+ const expected = `
29
+ import { Button } from "@planningcenter/tapestry-react"
30
+
31
+ export default function Test() {
32
+ return <Button>Save</Button>;
33
+ }
34
+ `.trim()
35
+
36
+ const result = applyTransform(input)
37
+ expect(result?.trim()).toBe(expected)
38
+ })
39
+
40
+ it("should remove 'to' attribute with hash value", () => {
41
+ const input = `
42
+ import { Button } from "@planningcenter/tapestry-react"
43
+
44
+ export default function Test() {
45
+ return <Button to="#">Save</Button>
46
+ }
47
+ `.trim()
48
+
49
+ const result = applyTransform(input)
50
+ expect(result).toContain("<Button>Save</Button>")
51
+ expect(result).not.toContain('to="#"')
52
+ })
53
+
54
+ it("should remove 'to' attribute with expression containing empty string", () => {
55
+ const input = `
56
+ import { Button } from "@planningcenter/tapestry-react"
57
+
58
+ export default function Test() {
59
+ return <Button to={""}>Save</Button>
60
+ }
61
+ `.trim()
62
+
63
+ const result = applyTransform(input)
64
+ expect(result).toContain("<Button>Save</Button>")
65
+ expect(result).not.toContain('to={""}')
66
+ })
67
+
68
+ it("should remove 'to' attribute with expression containing hash", () => {
69
+ const input = `
70
+ import { Button } from "@planningcenter/tapestry-react"
71
+
72
+ export default function Test() {
73
+ return <Button to={"#"}>Save</Button>
74
+ }
75
+ `.trim()
76
+
77
+ const result = applyTransform(input)
78
+ expect(result).toContain("<Button>Save</Button>")
79
+ expect(result).not.toContain('to={"#"}')
80
+ })
81
+ })
82
+
83
+ describe("comment generation for non-empty values", () => {
84
+ it("should remove 'to' attribute with string value and add TODO comment", () => {
85
+ const input = `
86
+ import { Button } from "@planningcenter/tapestry-react"
87
+
88
+ export default function Test() {
89
+ return <Button to="/users">Save</Button>
90
+ }
91
+ `.trim()
92
+
93
+ const result = applyTransform(input)
94
+ expect(result).toContain("<Button>Save</Button>")
95
+ expect(result).not.toContain('Button to="/users"')
96
+ expect(result).toContain("TODO: tapestry-migration")
97
+ expect(result).toContain('removed: to="/users"')
98
+ })
99
+
100
+ it("should remove 'to' attribute with expression value and add TODO comment", () => {
101
+ const input = `
102
+ import { Button } from "@planningcenter/tapestry-react"
103
+
104
+ export default function Test() {
105
+ return <Button to={"/profile"}>Save</Button>
106
+ }
107
+ `.trim()
108
+
109
+ const result = applyTransform(input)
110
+ expect(result).toContain("<Button>Save</Button>")
111
+ expect(result).not.toContain('to={"/profile"}')
112
+ expect(result).toContain("TODO: tapestry-migration")
113
+ expect(result).toContain('removed: to="/profile"')
114
+ })
115
+
116
+ it("should handle complex string values in comments", () => {
117
+ const input = `
118
+ import { Button } from "@planningcenter/tapestry-react"
119
+
120
+ export default function Test() {
121
+ return <Button to="/users?tab=active&sort=name">View Users</Button>
122
+ }
123
+ `.trim()
124
+
125
+ const result = applyTransform(input)
126
+ expect(result).toContain("<Button>View Users</Button>")
127
+ expect(result).toContain("TODO: tapestry-migration")
128
+ expect(result).toContain('removed: to="/users?tab=active&sort=name"')
129
+ })
130
+ })
131
+
132
+ describe("multiple Button components", () => {
133
+ it("should handle multiple Button components with different 'to' values", () => {
134
+ const input = `
135
+ import { Button } from "@planningcenter/tapestry-react"
136
+
137
+ export default function Test() {
138
+ return (
139
+ <div>
140
+ <Button to="">Empty</Button>
141
+ <Button to="/users">Users</Button>
142
+ <Button to="#">Hash</Button>
143
+ </div>
144
+ )
145
+ }
146
+ `.trim()
147
+
148
+ const result = applyTransform(input)
149
+ expect(result).toContain("<Button>Empty</Button>")
150
+ expect(result).toContain("<Button>Hash</Button>")
151
+ expect(result).toContain("<Button>Users</Button>")
152
+ expect(result).not.toContain('Button to=""')
153
+ expect(result).not.toContain('Button to="#"')
154
+ expect(result).not.toContain('Button to="/users"')
155
+ expect(result).toContain("TODO: tapestry-migration")
156
+ expect(result).toContain('removed: to="/users"')
157
+ })
158
+
159
+ it("should preserve other attributes", () => {
160
+ const input = `
161
+ import { Button } from "@planningcenter/tapestry-react"
162
+
163
+ export default function Test() {
164
+ return <Button to="/test" variant="primary" onClick={handleClick} disabled>Save</Button>
165
+ }
166
+ `.trim()
167
+
168
+ const result = applyTransform(input)
169
+ expect(result).toContain('variant="primary"')
170
+ expect(result).toContain("onClick={handleClick}")
171
+ expect(result).toContain("disabled")
172
+ expect(result).not.toContain('Button to="/test"')
173
+ expect(result).toContain("TODO: tapestry-migration")
174
+ expect(result).toContain('removed: to="/test"')
175
+ })
176
+ })
177
+
178
+ describe("edge cases", () => {
179
+ it("should not transform Button without 'to' attribute", () => {
180
+ const input = `
181
+ import { Button } from "@planningcenter/tapestry-react"
182
+
183
+ export default function Test() {
184
+ return <Button variant="primary">Save</Button>
185
+ }
186
+ `.trim()
187
+
188
+ const result = applyTransform(input)
189
+ expect(result).toBe(null)
190
+ })
191
+
192
+ it("should not transform if Button is not imported from tapestry-react", () => {
193
+ const input = `
194
+ import { Button } from "@planningcenter/tapestry"
195
+
196
+ export default function Test() {
197
+ return <Button to="/test">Save</Button>
198
+ }
199
+ `.trim()
200
+
201
+ const result = applyTransform(input)
202
+ expect(result).toBe(null)
203
+ })
204
+
205
+ it("should handle Button with aliased import", () => {
206
+ const input = `
207
+ import { Button as TapestryButton } from "@planningcenter/tapestry-react"
208
+
209
+ export default function Test() {
210
+ return <TapestryButton to="/users">Save</TapestryButton>
211
+ }
212
+ `.trim()
213
+
214
+ const result = applyTransform(input)
215
+ expect(result).toContain("<TapestryButton>Save</TapestryButton>")
216
+ expect(result).not.toContain('TapestryButton to="/users"')
217
+ expect(result).toContain("TODO: tapestry-migration")
218
+ expect(result).toContain('removed: to="/users"')
219
+ })
220
+
221
+ it("should handle self-closing Button components", () => {
222
+ const input = `
223
+ import { Button } from "@planningcenter/tapestry-react"
224
+
225
+ export default function Test() {
226
+ return <Button to="/test" />
227
+ }
228
+ `.trim()
229
+
230
+ const result = applyTransform(input)
231
+ expect(result).toContain("<Button />")
232
+ expect(result).not.toContain('Button to="/test"')
233
+ expect(result).toContain("TODO: tapestry-migration")
234
+ expect(result).toContain('removed: to="/test"')
235
+ })
236
+
237
+ it("should handle Button components with complex children", () => {
238
+ const input = `
239
+ import { Button } from "@planningcenter/tapestry-react"
240
+
241
+ export default function Test() {
242
+ return (
243
+ <Button to="/dashboard">
244
+ <span>Go to</span>
245
+ <strong>Dashboard</strong>
246
+ </Button>
247
+ )
248
+ }
249
+ `.trim()
250
+
251
+ const result = applyTransform(input)
252
+ expect(result).toContain("<Button>")
253
+ expect(result).toContain("<span>Go to</span>")
254
+ expect(result).toContain("<strong>Dashboard</strong>")
255
+ expect(result).toContain("</Button>")
256
+ expect(result).not.toContain('Button to="/dashboard"')
257
+ expect(result).toContain("TODO: tapestry-migration")
258
+ expect(result).toContain('removed: to="/dashboard"')
259
+ })
260
+ })
261
+
262
+ describe("no changes scenarios", () => {
263
+ it("should return null when no Button import exists", () => {
264
+ const input = `
265
+ import React from "react"
266
+
267
+ export default function Test() {
268
+ return <div>Hello</div>
269
+ }
270
+ `.trim()
271
+
272
+ const result = applyTransform(input)
273
+ expect(result).toBe(null)
274
+ })
275
+
276
+ it("should return null when no Button components with 'to' exist", () => {
277
+ const input = `
278
+ import { Button, Link } from "@planningcenter/tapestry-react"
279
+
280
+ export default function Test() {
281
+ return (
282
+ <div>
283
+ <Button>Save</Button>
284
+ <Link to="/users">Users</Link>
285
+ </div>
286
+ )
287
+ }
288
+ `.trim()
289
+
290
+ const result = applyTransform(input)
291
+ expect(result).toBe(null)
292
+ })
293
+
294
+ it("should return null for empty file", () => {
295
+ const result = applyTransform("")
296
+ expect(result).toBe(null)
297
+ })
298
+ })
299
+
300
+ describe("complex scenarios", () => {
301
+ it("should handle mixed imports and multiple components", () => {
302
+ const input = `
303
+ import React from "react"
304
+ import { Button, Link } from "@planningcenter/tapestry-react"
305
+ import { Input } from "@planningcenter/tapestry"
306
+
307
+ export default function Test() {
308
+ return (
309
+ <form>
310
+ <Input name="email" />
311
+ <Button to="/submit" type="submit">Submit</Button>
312
+ <Link to="/cancel">Cancel</Link>
313
+ <Button to="">Reset</Button>
314
+ </form>
315
+ )
316
+ }
317
+ `.trim()
318
+
319
+ const result = applyTransform(input)
320
+ expect(result).toContain('<Button type="submit">Submit</Button>')
321
+ expect(result).toContain("<Button>Reset</Button>")
322
+ expect(result).toContain('<Link to="/cancel">Cancel</Link>') // Link should be unchanged
323
+ expect(result).not.toContain('Button to="/submit"')
324
+ expect(result).not.toContain('Button to=""')
325
+ expect(result).toContain(
326
+ '/* TODO: tapestry-migration (to): prop is not supported. If needed, use Link instead - removed: to="/submit" */'
327
+ )
328
+ })
329
+
330
+ it("should handle Button usage in JSX expressions", () => {
331
+ const input = `
332
+ import { Button } from "@planningcenter/tapestry-react"
333
+
334
+ export default function Test({ showButton, items }) {
335
+ return (
336
+ <div>
337
+ {showButton && <Button to="/conditional">Conditional</Button>}
338
+ {items.map(item => <Button key={item.id} to={item.url}>{item.name}</Button>)}
339
+ </div>
340
+ )
341
+ }
342
+ `.trim()
343
+
344
+ const result = applyTransform(input)
345
+ expect(result).toContain("<Button>Conditional</Button>")
346
+ expect(result).toContain("<Button key={item.id}>{item.name}</Button>")
347
+ expect(result).not.toContain('Button to="/conditional"')
348
+ expect(result).not.toContain("} to={item.url}")
349
+ expect(result).toContain("TODO: tapestry-migration")
350
+ expect(result).toContain('removed: to="/conditional"')
351
+ })
352
+ })
353
+
354
+ describe("comment placement and format", () => {
355
+ it("should add comments with correct format", () => {
356
+ const input = `
357
+ import { Button } from "@planningcenter/tapestry-react"
358
+
359
+ export default function Test() {
360
+ return <Button href="/test">Save</Button>
361
+ }
362
+ `.trim()
363
+
364
+ const result = applyTransform(input)
365
+ expect(result).toContain(
366
+ '/* TODO: tapestry-migration (href): prop is not supported. If needed, use Link instead - removed: href="/test" */'
367
+ )
368
+ })
369
+ })
370
+ })
@@ -0,0 +1,25 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { removeAttribute } from "../../shared/actions/removeAttribute"
4
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
5
+ import { orConditions } from "../../shared/conditions/orConditions"
6
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
7
+
8
+ function buildComment(propName: string, formattedValue: string) {
9
+ if (formattedValue === '""' || formattedValue === '"#"') return
10
+ return `prop is not supported. If needed, use Link instead - removed: ${propName}=${formattedValue}`
11
+ }
12
+
13
+ const transform: Transform = attributeTransformFactory({
14
+ condition: orConditions(hasAttribute("to"), hasAttribute("href")),
15
+ targetComponent: "Button",
16
+ targetPackage: "@planningcenter/tapestry-react",
17
+ transform: (element, { j, source }) => {
18
+ const options = { buildComment, element, j, source }
19
+ const toRemoved = removeAttribute("to", options)
20
+ const hrefRemoved = removeAttribute("href", options)
21
+ return toRemoved || hrefRemoved
22
+ },
23
+ })
24
+
25
+ export default transform
@@ -0,0 +1,243 @@
1
+ import jscodeshift, { JSXElement, JSXIdentifier } from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import { addComment } from "./addComment"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function createElementFromCode(code: string): JSXElement {
9
+ const source = j(`<div>${code}</div>`)
10
+ return source.find(j.JSXElement).at(0).get().value.children?.[0] as JSXElement
11
+ }
12
+
13
+ function createTopLevelElementFromCode(code: string): JSXElement {
14
+ const source = j(code)
15
+ return source.find(j.JSXElement).at(0).get().value as JSXElement
16
+ }
17
+
18
+ describe("addComment", () => {
19
+ it("should add comment to element", () => {
20
+ const element = createElementFromCode("<Button>Save</Button>")
21
+ const commentText = "TODO: This needs to be updated"
22
+
23
+ addComment({ element, j, scope: "test", source: j(""), text: commentText })
24
+
25
+ expect(element.comments).toHaveLength(1)
26
+ expect(element.comments?.[0].type).toBe("CommentBlock")
27
+ expect(element.comments?.[0].value).toBe(
28
+ ` TODO: tapestry-migration (test): ${commentText} `
29
+ )
30
+ })
31
+
32
+ it("should add multiple comments to element", () => {
33
+ const element = createElementFromCode("<Button>Save</Button>")
34
+ const firstComment = "TODO: First comment"
35
+ const secondComment = "TODO: Second comment"
36
+
37
+ addComment({ element, j, scope: "test", source: j(""), text: firstComment })
38
+ addComment({
39
+ element,
40
+ j,
41
+ scope: "test",
42
+ source: j(""),
43
+ text: secondComment,
44
+ })
45
+
46
+ expect(element.comments).toHaveLength(2)
47
+ expect(element.comments?.[0].value).toBe(
48
+ ` TODO: tapestry-migration (test): ${firstComment} `
49
+ )
50
+ expect(element.comments?.[1].value).toBe(
51
+ ` TODO: tapestry-migration (test): ${secondComment} `
52
+ )
53
+ })
54
+
55
+ it("should render comment above top-level element", () => {
56
+ const element = createTopLevelElementFromCode("<Button>Save</Button>")
57
+ const commentText = "TODO: Update this button"
58
+
59
+ addComment({ element, j, scope: "test", source: j(""), text: commentText })
60
+
61
+ const source = j.withParser("tsx")("").find(j.Program).get()
62
+ source.value.body = [j.expressionStatement(element)]
63
+ const result = j(source).toSource()
64
+
65
+ expect(result).toContain(
66
+ `/* TODO: tapestry-migration (test): ${commentText} */`
67
+ )
68
+ expect(result).toContain("<Button>Save</Button>")
69
+ })
70
+
71
+ it("should render comment above nested element (current JSCodeshift limitation)", () => {
72
+ const outerCode = `
73
+ <div>
74
+ <Button>Save</Button>
75
+ </div>
76
+ `
77
+ const source = j(outerCode)
78
+ const buttonElement = source
79
+ .find(j.JSXElement)
80
+ .filter(
81
+ (path) =>
82
+ (path.value.openingElement.name as JSXIdentifier)?.name === "Button"
83
+ )
84
+ .at(0)
85
+ .get().value as JSXElement
86
+
87
+ const commentText = "TODO: Update this nested button"
88
+ addComment({
89
+ element: buttonElement,
90
+ j,
91
+ scope: "test",
92
+ source,
93
+ text: commentText,
94
+ })
95
+
96
+ const result = source.toSource()
97
+
98
+ // Current JSCodeshift limitation: comments are rendered as regular block comments
99
+ // This is a known issue - comments in JSX context should ideally be {/* */}
100
+ expect(result).toContain(
101
+ `/* TODO: tapestry-migration (test): ${commentText} */`
102
+ )
103
+ expect(result).toContain("<Button>Save</Button>")
104
+ })
105
+
106
+ it("should handle empty comment text", () => {
107
+ const element = createElementFromCode("<Button>Save</Button>")
108
+ const commentText = ""
109
+
110
+ addComment({ element, j, scope: "test", source: j(""), text: commentText })
111
+
112
+ expect(element.comments).toHaveLength(1)
113
+ expect(element.comments?.[0].value).toBe(
114
+ ` TODO: tapestry-migration (test): ${commentText} `
115
+ )
116
+ })
117
+
118
+ it("should handle comment text with special characters", () => {
119
+ const element = createElementFromCode("<Button>Save</Button>")
120
+ const commentText = "TODO: Handle 'quotes' and \"double quotes\" and <tags>"
121
+
122
+ addComment({ element, j, scope: "test", source: j(""), text: commentText })
123
+
124
+ expect(element.comments).toHaveLength(1)
125
+ expect(element.comments?.[0].value).toBe(
126
+ ` TODO: tapestry-migration (test): ${commentText} `
127
+ )
128
+ })
129
+
130
+ it("should handle multiline comment text", () => {
131
+ const element = createElementFromCode("<Button>Save</Button>")
132
+ const commentText =
133
+ "TODO: This is a long comment\nthat spans multiple lines"
134
+
135
+ addComment({ element, j, scope: "test", source: j(""), text: commentText })
136
+
137
+ expect(element.comments).toHaveLength(1)
138
+ expect(element.comments?.[0].value).toBe(
139
+ ` TODO: tapestry-migration (test): ${commentText} `
140
+ )
141
+ })
142
+
143
+ describe("JSX context formatting (current limitations)", () => {
144
+ it("should format comments in JSX children context (JSCodeshift limitation)", () => {
145
+ const outerCode = `
146
+ <div>
147
+ <span>Before</span>
148
+ <Button>Save</Button>
149
+ <span>After</span>
150
+ </div>
151
+ `
152
+ const source = j(outerCode)
153
+ const buttonElement = source
154
+ .find(j.JSXElement)
155
+ .filter(
156
+ (path) =>
157
+ (path.value.openingElement.name as JSXIdentifier)?.name === "Button"
158
+ )
159
+ .at(0)
160
+ .get().value as JSXElement
161
+
162
+ addComment({
163
+ element: buttonElement,
164
+ j,
165
+ scope: "test",
166
+ source,
167
+ text: "Fix this button",
168
+ })
169
+
170
+ const result = source.toSource()
171
+
172
+ expect(result).toContain(
173
+ "/* TODO: tapestry-migration (test): Fix this button */"
174
+ )
175
+ })
176
+
177
+ it("should format comments in JSX attribute context (JSCodeshift limitation)", () => {
178
+ const outerCode = `
179
+ <div>
180
+ <Button iconLeft={<Icon name="star" />}>Save</Button>
181
+ </div>
182
+ `
183
+ const source = j(outerCode)
184
+ const iconElement = source
185
+ .find(j.JSXElement)
186
+ .filter(
187
+ (path) =>
188
+ (path.value.openingElement.name as JSXIdentifier)?.name === "Icon"
189
+ )
190
+ .at(0)
191
+ .get().value as JSXElement
192
+
193
+ addComment({
194
+ element: iconElement,
195
+ j,
196
+ scope: "test",
197
+ source,
198
+ text: "Update icon",
199
+ })
200
+
201
+ const result = source.toSource()
202
+
203
+ expect(result).toContain(
204
+ "/* TODO: tapestry-migration (test): Update icon */"
205
+ )
206
+ })
207
+
208
+ it("should format comments for deeply nested elements with JSX expression", () => {
209
+ const outerCode = `
210
+ <div>
211
+ <section>
212
+ <article>
213
+ <Button>Deep Button</Button>
214
+ </article>
215
+ </section>
216
+ </div>
217
+ `
218
+ const source = j(outerCode)
219
+ const buttonElement = source
220
+ .find(j.JSXElement)
221
+ .filter(
222
+ (path) =>
223
+ (path.value.openingElement.name as JSXIdentifier)?.name === "Button"
224
+ )
225
+ .at(0)
226
+ .get().value as JSXElement
227
+
228
+ addComment({
229
+ element: buttonElement,
230
+ j,
231
+ scope: "test",
232
+ source,
233
+ text: "Deeply nested comment",
234
+ })
235
+
236
+ const result = source.toSource()
237
+
238
+ expect(result.replace(/\n/gm, "")).toMatch(
239
+ /\{\/\*\sTODO:\stapestry-migration\s\(test\):\sDeeply\snested\scomment\s\*\/\s*}/
240
+ )
241
+ })
242
+ })
243
+ })
@@ -0,0 +1,69 @@
1
+ import { ASTPath, Collection, JSCodeshift, JSXElement } from "jscodeshift"
2
+
3
+ /**
4
+ * Adds a comment to a JSX element.
5
+ *
6
+ * For JSX contexts, creates a proper JSX expression comment by embedding
7
+ * the comment in a JSX expression container that renders properly.
8
+ */
9
+ export function addComment({
10
+ text,
11
+ element,
12
+ j,
13
+ source,
14
+ scope,
15
+ }: {
16
+ element: JSXElement
17
+ j: JSCodeshift
18
+ scope: string
19
+ source: Collection
20
+ text: string
21
+ }) {
22
+ const commentText = formatComment(text, scope)
23
+ if (tryInsertJSXComment(source, element, commentText, j)) {
24
+ return
25
+ }
26
+
27
+ const comment = j.commentBlock(commentText, true, false)
28
+
29
+ if (element.comments) {
30
+ element.comments.push(comment)
31
+ } else {
32
+ element.comments = [comment]
33
+ }
34
+ }
35
+
36
+ function createJSXExpressionComment(text: string, j: JSCodeshift) {
37
+ const comment = j.commentBlock(text, true, false)
38
+ const emptyExpr = j.jsxEmptyExpression()
39
+ emptyExpr.comments = [comment]
40
+ return j.jsxExpressionContainer(emptyExpr)
41
+ }
42
+
43
+ function tryInsertJSXComment(
44
+ source: Collection,
45
+ element: JSXElement,
46
+ text: string,
47
+ j: JSCodeshift
48
+ ): boolean {
49
+ let found = false
50
+
51
+ source.find(j.JSXElement).forEach((path: ASTPath<JSXElement>) => {
52
+ if (path.value === element && path.parent && path.parent.value.children) {
53
+ const parent = path.parent.value
54
+ const childIndex = parent.children.indexOf(element)
55
+
56
+ if (childIndex >= 0) {
57
+ const jsxComment = createJSXExpressionComment(text, j)
58
+ parent.children.splice(childIndex, 0, jsxComment)
59
+ found = true
60
+ }
61
+ }
62
+ })
63
+
64
+ return found
65
+ }
66
+
67
+ export function formatComment(text: string, scope: string): string {
68
+ return ` TODO: tapestry-migration (${scope}): ${text} `
69
+ }
@@ -0,0 +1,202 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import { getPrintableAttributeValue } from "./getPrintableAttributeValue"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ // Helper to create JSX attributes for testing
9
+ function createAttribute(code: string) {
10
+ const source = j(`<div ${code} />`)
11
+ const element = source.find(j.JSXElement).at(0)
12
+ const attributes = element.get().value.openingElement.attributes
13
+ return attributes?.[0] || null
14
+ }
15
+
16
+ describe("getPrintableAttributeValue", () => {
17
+ describe("null/undefined handling", () => {
18
+ it("should return null for null attribute", () => {
19
+ const result = getPrintableAttributeValue(null, j)
20
+ expect(result).toBe(null)
21
+ })
22
+
23
+ it("should return null for undefined attribute", () => {
24
+ const result = getPrintableAttributeValue(undefined, j)
25
+ expect(result).toBe(null)
26
+ })
27
+
28
+ it("should return null for attribute with no value", () => {
29
+ const attribute = createAttribute("disabled")
30
+ const result = getPrintableAttributeValue(attribute, j)
31
+ expect(result).toBe(null)
32
+ })
33
+ })
34
+
35
+ describe("string literal values", () => {
36
+ it("should format empty string literal", () => {
37
+ const attribute = createAttribute('to=""')
38
+ const result = getPrintableAttributeValue(attribute, j)
39
+ expect(result).toBe('""')
40
+ })
41
+
42
+ it("should format simple string literal", () => {
43
+ const attribute = createAttribute('to="/users"')
44
+ const result = getPrintableAttributeValue(attribute, j)
45
+ expect(result).toBe('"/users"')
46
+ })
47
+
48
+ it("should format hash string literal", () => {
49
+ const attribute = createAttribute('to="#"')
50
+ const result = getPrintableAttributeValue(attribute, j)
51
+ expect(result).toBe('"#"')
52
+ })
53
+
54
+ it("should handle string with special characters", () => {
55
+ const attribute = createAttribute('to="/users?tab=active&sort=name"')
56
+ const result = getPrintableAttributeValue(attribute, j)
57
+ expect(result).toBe('"/users?tab=active&sort=name"')
58
+ })
59
+
60
+ it("should handle string with quotes inside", () => {
61
+ const attribute = createAttribute(
62
+ "aria-label='Click \"here\" to continue'"
63
+ )
64
+ const result = getPrintableAttributeValue(attribute, j)
65
+ expect(result).toBe('"Click "here" to continue"')
66
+ })
67
+ })
68
+
69
+ describe("JSX expression containers with string literals", () => {
70
+ it("should normalize empty string expression to string literal format", () => {
71
+ const attribute = createAttribute('to={""}')
72
+ const result = getPrintableAttributeValue(attribute, j)
73
+ expect(result).toBe('""')
74
+ })
75
+
76
+ it("should normalize simple string expression to string literal format", () => {
77
+ const attribute = createAttribute('to={"/profile"}')
78
+ const result = getPrintableAttributeValue(attribute, j)
79
+ expect(result).toBe('"/profile"')
80
+ })
81
+
82
+ it("should normalize hash string expression to string literal format", () => {
83
+ const attribute = createAttribute('to={"#"}')
84
+ const result = getPrintableAttributeValue(attribute, j)
85
+ expect(result).toBe('"#"')
86
+ })
87
+
88
+ it("should handle complex string in expression", () => {
89
+ const attribute = createAttribute(
90
+ 'to={"/dashboard?user=123&tab=settings"}'
91
+ )
92
+ const result = getPrintableAttributeValue(attribute, j)
93
+ expect(result).toBe('"/dashboard?user=123&tab=settings"')
94
+ })
95
+ })
96
+
97
+ describe("JSX expression containers with complex expressions", () => {
98
+ it("should return full source for variable expression", () => {
99
+ const attribute = createAttribute("to={url}")
100
+ const result = getPrintableAttributeValue(attribute, j)
101
+ expect(result).toBe("{url}")
102
+ })
103
+
104
+ it("should return full source for template literal expression", () => {
105
+ const attribute = createAttribute("to={`/users/${userId}`}")
106
+ const result = getPrintableAttributeValue(attribute, j)
107
+ expect(result).toBe("{`/users/${userId}`}")
108
+ })
109
+
110
+ it("should return full source for function call expression", () => {
111
+ const attribute = createAttribute("to={getUrl()}")
112
+ const result = getPrintableAttributeValue(attribute, j)
113
+ expect(result).toBe("{getUrl()}")
114
+ })
115
+
116
+ it("should return full source for conditional expression", () => {
117
+ const attribute = createAttribute(
118
+ 'to={isActive ? "/dashboard" : "/login"}'
119
+ )
120
+ const result = getPrintableAttributeValue(attribute, j)
121
+ expect(result).toBe('{isActive ? "/dashboard" : "/login"}')
122
+ })
123
+
124
+ it("should return full source for object property access", () => {
125
+ const attribute = createAttribute("to={config.baseUrl}")
126
+ const result = getPrintableAttributeValue(attribute, j)
127
+ expect(result).toBe("{config.baseUrl}")
128
+ })
129
+
130
+ it("should return full source for array access", () => {
131
+ const attribute = createAttribute("to={routes[0]}")
132
+ const result = getPrintableAttributeValue(attribute, j)
133
+ expect(result).toBe("{routes[0]}")
134
+ })
135
+
136
+ it("should return full source for complex nested expression", () => {
137
+ const attribute = createAttribute(
138
+ "onClick={() => navigate(getUrl(userId))}"
139
+ )
140
+ const result = getPrintableAttributeValue(attribute, j)
141
+ expect(result).toBe("{() => navigate(getUrl(userId))}")
142
+ })
143
+ })
144
+
145
+ describe("edge cases", () => {
146
+ it("should handle boolean attribute (no value)", () => {
147
+ const attribute = createAttribute("disabled")
148
+ const result = getPrintableAttributeValue(attribute, j)
149
+ expect(result).toBe(null)
150
+ })
151
+
152
+ it("should handle numeric literal in expression", () => {
153
+ const attribute = createAttribute("tabIndex={0}")
154
+ const result = getPrintableAttributeValue(attribute, j)
155
+ expect(result).toBe("{0}")
156
+ })
157
+
158
+ it("should handle boolean literal in expression", () => {
159
+ const attribute = createAttribute("disabled={true}")
160
+ const result = getPrintableAttributeValue(attribute, j)
161
+ expect(result).toBe("{true}")
162
+ })
163
+
164
+ it("should handle null literal in expression", () => {
165
+ const attribute = createAttribute("value={null}")
166
+ const result = getPrintableAttributeValue(attribute, j)
167
+ expect(result).toBe("{null}")
168
+ })
169
+
170
+ it("should handle undefined literal in expression", () => {
171
+ const attribute = createAttribute("value={undefined}")
172
+ const result = getPrintableAttributeValue(attribute, j)
173
+ expect(result).toBe("{undefined}")
174
+ })
175
+ })
176
+
177
+ describe("string escaping and special characters", () => {
178
+ it("should handle strings with newlines", () => {
179
+ const attribute = createAttribute('to={"line1\\nline2"}')
180
+ const result = getPrintableAttributeValue(attribute, j)
181
+ expect(result).toBe('"line1\nline2"')
182
+ })
183
+
184
+ it("should handle strings with tabs", () => {
185
+ const attribute = createAttribute('to={"col1\\tcol2"}')
186
+ const result = getPrintableAttributeValue(attribute, j)
187
+ expect(result).toBe('"col1\tcol2"')
188
+ })
189
+
190
+ it("should handle strings with backslashes", () => {
191
+ const attribute = createAttribute('to={"C:\\\\Users\\\\file.txt"}')
192
+ const result = getPrintableAttributeValue(attribute, j)
193
+ expect(result).toBe('"C:\\Users\\file.txt"')
194
+ })
195
+
196
+ it("should handle unicode characters", () => {
197
+ const attribute = createAttribute('to={"🚀 rocket"}')
198
+ const result = getPrintableAttributeValue(attribute, j)
199
+ expect(result).toBe('"🚀 rocket"')
200
+ })
201
+ })
202
+ })
@@ -0,0 +1,23 @@
1
+ import { JSCodeshift, JSXAttribute } from "jscodeshift"
2
+
3
+ export function getPrintableAttributeValue(
4
+ attribute: JSXAttribute | null | undefined,
5
+ j: JSCodeshift
6
+ ): string | null {
7
+ if (!attribute) return null
8
+
9
+ const value = attribute.value
10
+
11
+ if (!value) return null
12
+ if (value.type === "StringLiteral") return `"${value.value}"`
13
+ if (value.type === "JSXExpressionContainer") {
14
+ // Check if the expression is a simple string literal
15
+ if (value.expression.type === "StringLiteral") {
16
+ return `"${value.expression.value}"`
17
+ }
18
+ // For complex expressions, return the full source
19
+ return j(value).toSource()
20
+ }
21
+
22
+ return null
23
+ }
@@ -0,0 +1,343 @@
1
+ import jscodeshift, { JSXAttribute } from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import { removeAttribute } from "./removeAttribute"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ // Helper to create JSX elements for testing
9
+ function createElement(code: string) {
10
+ const source = j(`<div>${code}</div>`)
11
+ const element = source.find(j.JSXElement).at(0).get().value.children?.[0]
12
+ return element
13
+ }
14
+
15
+ describe("removeAttribute", () => {
16
+ describe("basic attribute removal", () => {
17
+ it("should remove existing string attribute", () => {
18
+ const element = createElement('<Button to="/users">Save</Button>')
19
+ const result = removeAttribute("to", { element, j, source: j("") })
20
+
21
+ expect(result).toBe(true)
22
+ expect(element.openingElement.attributes).toHaveLength(0)
23
+ })
24
+
25
+ it("should remove existing expression attribute", () => {
26
+ const element = createElement("<Button to={url}>Save</Button>")
27
+ const result = removeAttribute("to", { element, j, source: j("") })
28
+
29
+ expect(result).toBe(true)
30
+ expect(element.openingElement.attributes).toHaveLength(0)
31
+ })
32
+
33
+ it("should remove boolean attribute", () => {
34
+ const element = createElement("<Button disabled>Save</Button>")
35
+ const result = removeAttribute("disabled", { element, j, source: j("") })
36
+
37
+ expect(result).toBe(true)
38
+ expect(element.openingElement.attributes).toHaveLength(0)
39
+ })
40
+
41
+ it("should return false when attribute does not exist", () => {
42
+ const element = createElement('<Button variant="primary">Save</Button>')
43
+ const result = removeAttribute("to", { element, j, source: j("") })
44
+
45
+ expect(result).toBe(false)
46
+ expect(element.openingElement.attributes).toHaveLength(1)
47
+ })
48
+
49
+ it("should return false when element has no attributes", () => {
50
+ const element = createElement("<Button>Save</Button>")
51
+ const result = removeAttribute("to", { element, j, source: j("") })
52
+
53
+ expect(result).toBe(false)
54
+ })
55
+ })
56
+
57
+ describe("partial attribute removal", () => {
58
+ it("should remove target attribute while preserving others", () => {
59
+ const element = createElement(
60
+ '<Button to="/users" variant="primary" disabled>Save</Button>'
61
+ )
62
+ const result = removeAttribute("to", { element, j, source: j("") })
63
+
64
+ expect(result).toBe(true)
65
+ expect(element.openingElement.attributes).toHaveLength(2)
66
+
67
+ // Check that other attributes remain
68
+ const remainingAttrs = element.openingElement.attributes || []
69
+ const variantAttr = remainingAttrs.find(
70
+ (attr: JSXAttribute) =>
71
+ attr.type === "JSXAttribute" && attr.name?.name === "variant"
72
+ )
73
+ const disabledAttr = remainingAttrs.find(
74
+ (attr: JSXAttribute) =>
75
+ attr.type === "JSXAttribute" && attr.name?.name === "disabled"
76
+ )
77
+
78
+ expect(variantAttr).toBeTruthy()
79
+ expect(disabledAttr).toBeTruthy()
80
+ })
81
+
82
+ it("should handle removal of first attribute", () => {
83
+ const element = createElement(
84
+ '<Button to="/users" variant="primary">Save</Button>'
85
+ )
86
+ const result = removeAttribute("to", { element, j, source: j("") })
87
+
88
+ expect(result).toBe(true)
89
+ expect(element.openingElement.attributes).toHaveLength(1)
90
+ expect(
91
+ (element.openingElement.attributes?.[0] as JSXAttribute)?.name?.name
92
+ ).toBe("variant")
93
+ })
94
+
95
+ it("should handle removal of last attribute", () => {
96
+ const element = createElement(
97
+ '<Button variant="primary" to="/users">Save</Button>'
98
+ )
99
+ const result = removeAttribute("to", { element, j, source: j("") })
100
+
101
+ expect(result).toBe(true)
102
+ expect(element.openingElement.attributes).toHaveLength(1)
103
+ expect(
104
+ (element.openingElement.attributes?.[0] as JSXAttribute)?.name?.name
105
+ ).toBe("variant")
106
+ })
107
+
108
+ it("should handle removal of middle attribute", () => {
109
+ const element = createElement(
110
+ '<Button variant="primary" to="/users" disabled>Save</Button>'
111
+ )
112
+ const result = removeAttribute("to", { element, j, source: j("") })
113
+
114
+ expect(result).toBe(true)
115
+ expect(element.openingElement.attributes).toHaveLength(2)
116
+
117
+ const names = element.openingElement.attributes?.map(
118
+ (attr: JSXAttribute) =>
119
+ attr.type === "JSXAttribute" ? attr.name?.name : null
120
+ )
121
+ expect(names).toEqual(["variant", "disabled"])
122
+ })
123
+ })
124
+
125
+ describe("comment generation", () => {
126
+ it("should add comment when buildComment function is provided", () => {
127
+ const element = createElement('<Button to="/users">Save</Button>')
128
+ const buildComment = (propName: string, formattedValue: string) =>
129
+ `Removed ${propName}=${formattedValue}`
130
+
131
+ const result = removeAttribute("to", {
132
+ buildComment,
133
+ element,
134
+ j,
135
+ source: j(""),
136
+ })
137
+
138
+ expect(result).toBe(true)
139
+ expect(element.comments).toBeDefined()
140
+ expect(element.comments).toHaveLength(1)
141
+ expect(element.comments?.[0].value).toContain('Removed to="/users"')
142
+ })
143
+
144
+ it("should add multiple comments when called multiple times", () => {
145
+ const element = createElement(
146
+ '<Button to="/users" href="/users">Save</Button>'
147
+ )
148
+ const buildComment = (propName: string, formattedValue: string) =>
149
+ `Removed ${propName}=${formattedValue}`
150
+
151
+ removeAttribute("to", { buildComment, element, j, source: j("") })
152
+ removeAttribute("href", { buildComment, element, j, source: j("") })
153
+
154
+ expect(element.comments).toHaveLength(2)
155
+ expect(element.comments?.[0].value).toContain('Removed to="/users"')
156
+ expect(element.comments?.[1].value).toContain('Removed href="/users"')
157
+ })
158
+
159
+ it("should not add comment when buildComment returns undefined", () => {
160
+ const element = createElement('<Button to="">Save</Button>')
161
+ const buildComment = (propName: string, formattedValue: string) => {
162
+ if (formattedValue === '""') return undefined
163
+ return `Removed ${propName}=${formattedValue}`
164
+ }
165
+
166
+ const result = removeAttribute("to", {
167
+ buildComment,
168
+ element,
169
+ j,
170
+ source: j(""),
171
+ })
172
+
173
+ expect(result).toBe(true)
174
+ expect(element.comments).toBeUndefined()
175
+ })
176
+
177
+ it("should not add comment when buildComment returns empty string", () => {
178
+ const element = createElement('<Button to="">Save</Button>')
179
+ const buildComment = () => ""
180
+
181
+ const result = removeAttribute("to", {
182
+ buildComment,
183
+ element,
184
+ j,
185
+ source: j(""),
186
+ })
187
+
188
+ expect(result).toBe(true)
189
+ expect(element.comments).toBeUndefined()
190
+ })
191
+
192
+ it("should handle comment generation with expression attributes", () => {
193
+ const element = createElement("<Button to={url}>Save</Button>")
194
+ const buildComment = (propName: string, formattedValue: string) =>
195
+ `Removed ${propName}=${formattedValue}`
196
+
197
+ const result = removeAttribute("to", {
198
+ buildComment,
199
+ element,
200
+ j,
201
+ source: j(""),
202
+ })
203
+
204
+ expect(result).toBe(true)
205
+ expect(element.comments?.[0].value).toContain("Removed to={url}")
206
+ })
207
+
208
+ it("should handle comment generation with boolean attributes", () => {
209
+ const element = createElement("<Button disabled>Save</Button>")
210
+ const buildComment = (propName: string, formattedValue: string) =>
211
+ `Removed ${propName}=${formattedValue || "true"}`
212
+
213
+ const result = removeAttribute("disabled", {
214
+ buildComment,
215
+ element,
216
+ j,
217
+ source: j(""),
218
+ })
219
+
220
+ expect(result).toBe(true)
221
+ expect(element.comments?.[0].value).toContain("Removed disabled=true")
222
+ })
223
+ })
224
+
225
+ describe("edge cases", () => {
226
+ it("should handle element with null attributes", () => {
227
+ const element = createElement("<Button>Save</Button>")
228
+ element.openingElement.attributes = null
229
+
230
+ const result = removeAttribute("to", { element, j, source: j("") })
231
+
232
+ expect(result).toBe(false)
233
+ })
234
+
235
+ it("should handle element with undefined attributes", () => {
236
+ const element = createElement("<Button>Save</Button>")
237
+ element.openingElement.attributes = undefined
238
+
239
+ const result = removeAttribute("to", { element, j, source: j("") })
240
+
241
+ expect(result).toBe(false)
242
+ })
243
+
244
+ it("should handle JSX spread attributes", () => {
245
+ const element = createElement(
246
+ '<Button {...props} to="/users">Save</Button>'
247
+ )
248
+ const result = removeAttribute("to", { element, j, source: j("") })
249
+
250
+ expect(result).toBe(true)
251
+ expect(element.openingElement.attributes).toHaveLength(1)
252
+ expect(element.openingElement.attributes?.[0].type).toBe(
253
+ "JSXSpreadAttribute"
254
+ )
255
+ })
256
+
257
+ it("should not remove JSX spread attributes when looking for named attribute", () => {
258
+ const element = createElement("<Button {...props}>Save</Button>")
259
+ const result = removeAttribute("to", { element, j, source: j("") })
260
+
261
+ expect(result).toBe(false)
262
+ expect(element.openingElement.attributes).toHaveLength(1)
263
+ })
264
+
265
+ it("should handle self-closing elements", () => {
266
+ const element = createElement('<Button to="/users" />')
267
+ const result = removeAttribute("to", { element, j, source: j("") })
268
+
269
+ expect(result).toBe(true)
270
+ expect(element.openingElement.attributes).toHaveLength(0)
271
+ })
272
+
273
+ it("should work with different JSX element types", () => {
274
+ const element = createElement('<Link to="/users">Go</Link>')
275
+ const result = removeAttribute("to", { element, j, source: j("") })
276
+
277
+ expect(result).toBe(true)
278
+ expect(element.openingElement.attributes).toHaveLength(0)
279
+ })
280
+ })
281
+
282
+ describe("complex attribute values", () => {
283
+ it("should handle complex object expression", () => {
284
+ const element = createElement(
285
+ '<Button style={{ color: "red", fontSize: 14 }}>Save</Button>'
286
+ )
287
+ const buildComment = (propName: string, formattedValue: string) =>
288
+ `Removed ${propName}=${formattedValue}`
289
+
290
+ const result = removeAttribute("style", {
291
+ buildComment,
292
+ element,
293
+ j,
294
+ source: j(""),
295
+ })
296
+
297
+ expect(result).toBe(true)
298
+ expect(element.comments?.[0].value).toContain("Removed style={{")
299
+ expect(element.comments?.[0].value).toContain('color: "red"')
300
+ expect(element.comments?.[0].value).toContain("fontSize: 14")
301
+ })
302
+
303
+ it("should handle arrow function expression", () => {
304
+ const element = createElement(
305
+ "<Button onClick={() => doSomething()}>Save</Button>"
306
+ )
307
+ const buildComment = (propName: string, formattedValue: string) =>
308
+ `Removed ${propName}=${formattedValue}`
309
+
310
+ const result = removeAttribute("onClick", {
311
+ buildComment,
312
+ element,
313
+ j,
314
+ source: j(""),
315
+ })
316
+
317
+ expect(result).toBe(true)
318
+ expect(element.comments?.[0].value).toContain(
319
+ "Removed onClick={() => doSomething()}"
320
+ )
321
+ })
322
+
323
+ it("should handle template literal expression", () => {
324
+ const element = createElement(
325
+ "<Button to={`/users/${userId}`}>Save</Button>"
326
+ )
327
+ const buildComment = (propName: string, formattedValue: string) =>
328
+ `Removed ${propName}=${formattedValue}`
329
+
330
+ const result = removeAttribute("to", {
331
+ buildComment,
332
+ element,
333
+ j,
334
+ source: j(""),
335
+ })
336
+
337
+ expect(result).toBe(true)
338
+ expect(element.comments?.[0].value).toContain(
339
+ "Removed to={`/users/${userId}`}"
340
+ )
341
+ })
342
+ })
343
+ })
@@ -0,0 +1,37 @@
1
+ import { Collection, JSCodeshift, JSXElement } from "jscodeshift"
2
+
3
+ import { findAttribute } from "../findAttribute"
4
+ import { addComment } from "./addComment"
5
+ import { getPrintableAttributeValue } from "./getPrintableAttributeValue"
6
+
7
+ type RemoveAttributeConfig = {
8
+ /** Function to build comment text given the prop name and formatted value */
9
+ buildComment?: (propName: string, formattedValue: string) => string | void
10
+ element: JSXElement
11
+ j: JSCodeshift
12
+ source: Collection
13
+ }
14
+
15
+ export function removeAttribute(
16
+ name: string,
17
+ { element, buildComment, j, source }: RemoveAttributeConfig
18
+ ): boolean {
19
+ const attributes = element.openingElement.attributes || []
20
+ const attribute = findAttribute(attributes, name)
21
+
22
+ if (!attribute) return false
23
+
24
+ const index = attributes?.indexOf(attribute)
25
+
26
+ if (buildComment) {
27
+ const printableValue = getPrintableAttributeValue(attribute, j)
28
+ const commentText = buildComment(name, printableValue || "")
29
+
30
+ if (commentText) {
31
+ addComment({ element, j, scope: name, source, text: commentText })
32
+ }
33
+ }
34
+
35
+ attributes.splice(index, 1)
36
+ return true
37
+ }
@@ -1,4 +1,4 @@
1
- import { JSCodeshift, JSXElement, Transform } from "jscodeshift"
1
+ import { Collection, JSCodeshift, JSXElement, Transform } from "jscodeshift"
2
2
 
3
3
  import { TransformCondition } from "../types"
4
4
  import { getImportName } from "./helpers/manageImports"
@@ -14,7 +14,10 @@ export function attributeTransformFactory(config: {
14
14
  /** Package the target component is imported from */
15
15
  targetPackage: string
16
16
  /** Function that performs the actual attribute transformation */
17
- transform: (element: JSXElement, j: JSCodeshift) => boolean
17
+ transform: (
18
+ element: JSXElement,
19
+ resources: { j: JSCodeshift; source: Collection }
20
+ ) => boolean
18
21
  }): Transform {
19
22
  return (fileInfo, api) => {
20
23
  const j = api.jscodeshift
@@ -40,7 +43,7 @@ export function attributeTransformFactory(config: {
40
43
  const element = path.parent.value
41
44
 
42
45
  if (config.condition(element)) {
43
- if (config.transform(element, j)) {
46
+ if (config.transform(element, { j, source })) {
44
47
  hasChanges = true
45
48
  }
46
49
  }