@planningcenter/tapestry-migration-cli 2.4.1 → 2.5.0-qa-459.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 (36) hide show
  1. package/README.md +10 -10
  2. package/dist/tapestry-react-shim.cjs +1 -0
  3. package/package.json +2 -2
  4. package/src/components/button/transforms/childrenToLabel.test.ts +18 -2
  5. package/src/components/button/transforms/childrenToLabel.ts +13 -1
  6. package/src/components/button/transforms/iconLeftToPrefix.ts +2 -2
  7. package/src/components/button/transforms/iconRightToSuffix.ts +2 -2
  8. package/src/components/button/transforms/iconToIconButton.ts +13 -1
  9. package/src/components/button/transforms/innerRefToRef.ts +2 -2
  10. package/src/components/button/transforms/spinnerToLoadingButton.ts +2 -2
  11. package/src/components/button/transforms/titleToLabel.ts +2 -2
  12. package/src/components/checkbox/transforms/childrenToLabel.test.ts +18 -2
  13. package/src/components/checkbox/transforms/childrenToLabel.ts +13 -1
  14. package/src/components/checkbox/transforms/innerRefToRef.test.ts +20 -2
  15. package/src/components/checkbox/transforms/innerRefToRef.ts +2 -2
  16. package/src/components/link/transforms/childrenToLabel.test.ts +18 -2
  17. package/src/components/link/transforms/childrenToLabel.ts +13 -1
  18. package/src/components/link/transforms/innerRefToRef.test.ts +19 -2
  19. package/src/components/link/transforms/innerRefToRef.ts +2 -2
  20. package/src/components/link/transforms/removeAs.test.ts +18 -2
  21. package/src/components/link/transforms/removeAs.ts +13 -2
  22. package/src/components/link/transforms/removeInlineProp.test.ts +20 -2
  23. package/src/components/link/transforms/removeInlineProp.ts +14 -1
  24. package/src/components/link/transforms/targetBlankToExternal.test.ts +18 -2
  25. package/src/components/link/transforms/targetBlankToExternal.ts +13 -4
  26. package/src/components/link/transforms/toToHref.test.ts +19 -2
  27. package/src/components/link/transforms/toToHref.ts +2 -2
  28. package/src/components/shared/actions/addComment.ts +10 -3
  29. package/src/components/shared/actions/addCommentToAttribute.ts +3 -1
  30. package/src/components/shared/actions/transformAttributeName.ts +16 -2
  31. package/src/components/shared/transformFactories/attributeCombineFactory.ts +33 -1
  32. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +20 -0
  33. package/src/index.ts +3 -3
  34. package/src/jscodeshiftRunner.ts +1 -1
  35. package/src/reportGenerator.ts +69 -5
  36. package/src/shared/types.ts +1 -1
package/README.md CHANGED
@@ -14,25 +14,25 @@ yarn add --dev @planningcenter/tapestry-migration-cli
14
14
 
15
15
  For optimal results, migrate components in this order:
16
16
 
17
- 1. **Button components first**: `npx @planningcenter/tapestry-migration-cli run button -p ./src/components --fix`
18
- 2. **Link components second**: `npx @planningcenter/tapestry-migration-cli run link -p ./src/components --fix`
17
+ 1. **Button components first**: `npx @planningcenter/tapestry-migration-cli run button -p ./src/components`
18
+ 2. **Link components second**: `npx @planningcenter/tapestry-migration-cli run link -p ./src/components`
19
19
 
20
20
  This order ensures that Button components with navigation props are properly handled before Link components are migrated.
21
21
 
22
22
  ### Basic Commands
23
23
 
24
24
  ```bash
25
- # Run a dry-run migration (preview changes)
25
+ # Apply the migration changes (default behavior)
26
26
  npx @planningcenter/tapestry-migration-cli run button -p ./src/components
27
27
 
28
- # Apply the migration changes
29
- npx @planningcenter/tapestry-migration-cli run button -p ./src/components --fix
28
+ # Preview changes without writing (dry run)
29
+ npx @planningcenter/tapestry-migration-cli run button -p ./src/components --dry-run
30
30
 
31
31
  # Run with verbose output
32
- npx @planningcenter/tapestry-migration-cli run button -p ./src/components --fix --verbose
32
+ npx @planningcenter/tapestry-migration-cli run button -p ./src/components --verbose
33
33
 
34
34
  # Generate a migration report
35
- npx @planningcenter/tapestry-migration-cli run button -p ./src/components --fix --report-path ./migration-report.md
35
+ npx @planningcenter/tapestry-migration-cli run button -p ./src/components --report-path ./migration-report.md
36
36
  ```
37
37
 
38
38
  ## Available Components
@@ -46,7 +46,7 @@ npx @planningcenter/tapestry-migration-cli run button -p ./src/components --fix
46
46
 
47
47
  ## Optional Arguments
48
48
 
49
- - `-f, --fix` - Write changes to files (without this flag, it's a dry run)
49
+ - `-d, --dry-run` - Preview changes without writing to files (default: writes changes)
50
50
  - `-v, --verbose` - Show detailed output
51
51
  - `-j, --js-theme <path>` - Path to JavaScript theme file
52
52
  - `-r, --report-path <path>` - Path for migration report (default: MIGRATION_REPORT.md)
@@ -101,6 +101,6 @@ yarn add --dev @planningcenter/tapestry-migration-cli
101
101
  }
102
102
 
103
103
  # Then run (following recommended order)
104
- yarn migrate run button -p app/javascript/components --fix
105
- yarn migrate run link -p app/javascript/components --fix
104
+ yarn migrate run button -p app/javascript/components
105
+ yarn migrate run link -p app/javascript/components
106
106
  ```
@@ -2571,6 +2571,7 @@ const COMPONENT_KIND_CLASS_MAP = {
2571
2571
  "ghost-interaction": "tds-btn--ghost-interaction",
2572
2572
  "inline-text": "tds-btn--inline-text",
2573
2573
  neutral: "tds-btn--neutral",
2574
+ "neutral-inline": "tds-btn--neutral-inline",
2574
2575
  pill: "tds-btn--pill",
2575
2576
  primary: "tds-btn--interaction",
2576
2577
  "primary-page-header": "tds-btn--primary-page-header",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "2.4.1",
3
+ "version": "2.5.0-qa-459.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": "465fa2ffd3103c546f66e368859061daf3c7b6c1"
54
+ "gitHead": "c09a62a934592af9a96f33a9fff32ece2e33aed6"
55
55
  }
@@ -5,12 +5,12 @@ import transform from "./childrenToLabel"
5
5
 
6
6
  const j = jscodeshift.withParser("tsx")
7
7
 
8
- function applyTransform(source: string): string | null {
8
+ function applyTransform(source: string, verbose = false): string | null {
9
9
  const fileInfo = { path: "test.tsx", source }
10
10
  return transform(
11
11
  fileInfo,
12
12
  { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
- {}
13
+ { verbose }
14
14
  ) as string | null
15
15
  }
16
16
 
@@ -360,5 +360,21 @@ function Test() {
360
360
  "take time to find the right text for the component"
361
361
  )
362
362
  })
363
+
364
+ it("should add CHANGED comment when verbose is enabled", () => {
365
+ const input = `
366
+ import { Button } from "@planningcenter/tapestry-react"
367
+
368
+ function Test() {
369
+ return <Button>Save</Button>
370
+ }
371
+ `.trim()
372
+
373
+ const result = applyTransform(input, true)
374
+ expect(result).toContain('<Button label="Save" />')
375
+ expect(result).toContain(
376
+ "CHANGED: tapestry-migration (children): children converted to label prop"
377
+ )
378
+ })
363
379
  })
364
380
  })
@@ -15,7 +15,7 @@ const transform: Transform = attributeTransformFactory({
15
15
  condition: hasChildren,
16
16
  targetComponent: "Button",
17
17
  targetPackage: "@planningcenter/tapestry-react",
18
- transform: (element, { j, source }) => {
18
+ transform: (element, { j, options, source }) => {
19
19
  if (hasAttribute("label")(element)) {
20
20
  addComment({
21
21
  element,
@@ -32,6 +32,18 @@ const transform: Transform = attributeTransformFactory({
32
32
  if (isSimpleText && textContent) {
33
33
  addAttribute({ element, j, name: "label", value: textContent })
34
34
  removeChildren(element)
35
+
36
+ if (options?.verbose) {
37
+ addComment({
38
+ commentKind: "change",
39
+ element,
40
+ j,
41
+ scope: "children",
42
+ source,
43
+ text: buildComment("children converted to label prop", false),
44
+ })
45
+ }
46
+
35
47
  return true
36
48
  } else if (!isSimpleText) {
37
49
  addComment({
@@ -8,7 +8,7 @@ const transform = attributeTransformFactory({
8
8
  condition: hasAttribute("iconLeft"),
9
9
  targetComponent: "Button",
10
10
  targetPackage: "@planningcenter/tapestry-react",
11
- transform: (element, { j, source }) => {
11
+ transform: (element, { j, options, source }) => {
12
12
  const name = addImport({
13
13
  component: "Icon",
14
14
  conflictAlias: "TRIcon",
@@ -25,7 +25,7 @@ const transform = attributeTransformFactory({
25
25
  })
26
26
  if (!updatedElement) return false
27
27
 
28
- transformAttributeName("iconLeft", "prefix", { element })
28
+ transformAttributeName("iconLeft", "prefix", { element, j, options })
29
29
  return true
30
30
  },
31
31
  })
@@ -8,7 +8,7 @@ const transform = attributeTransformFactory({
8
8
  condition: hasAttribute("iconRight"),
9
9
  targetComponent: "Button",
10
10
  targetPackage: "@planningcenter/tapestry-react",
11
- transform: (element, { j, source }) => {
11
+ transform: (element, { j, options, source }) => {
12
12
  const name = addImport({
13
13
  component: "Icon",
14
14
  conflictAlias: "TRIcon",
@@ -25,7 +25,7 @@ const transform = attributeTransformFactory({
25
25
  })
26
26
  if (!updatedElement) return false
27
27
 
28
- transformAttributeName("iconRight", "suffix", { element })
28
+ transformAttributeName("iconRight", "suffix", { element, j, options })
29
29
  return true
30
30
  },
31
31
  })
@@ -1,5 +1,6 @@
1
1
  import { JSXIdentifier } from "jscodeshift"
2
2
 
3
+ import { addComment } from "../../shared/actions/addComment"
3
4
  import { convertAttributeFromObjectToJSXElement } from "../../shared/actions/convertAttributeFromObjectToJSXElement"
4
5
  import { removeUnusedImport } from "../../shared/actions/removeUnusedImport"
5
6
  import { hasAttribute } from "../../shared/conditions/hasAttribute"
@@ -10,7 +11,7 @@ const transform = attributeTransformFactory({
10
11
  condition: hasAttribute("icon"),
11
12
  targetComponent: "Button",
12
13
  targetPackage: "@planningcenter/tapestry-react",
13
- transform: (element, { j, source }) => {
14
+ transform: (element, { j, options, source }) => {
14
15
  const name = addImport({
15
16
  component: "Icon",
16
17
  conflictAlias: "TRIcon",
@@ -46,6 +47,17 @@ const transform = attributeTransformFactory({
46
47
  source,
47
48
  })
48
49
 
50
+ if (options?.verbose) {
51
+ addComment({
52
+ commentKind: "change",
53
+ element,
54
+ j,
55
+ scope: "component",
56
+ source,
57
+ text: `converted Button with icon prop to IconButton`,
58
+ })
59
+ }
60
+
49
61
  return true
50
62
  },
51
63
  })
@@ -6,8 +6,8 @@ const transform = attributeTransformFactory({
6
6
  condition: hasAttribute("innerRef"),
7
7
  targetComponent: "Button",
8
8
  targetPackage: "@planningcenter/tapestry-react",
9
- transform: (element) => {
10
- return transformAttributeName("innerRef", "ref", { element })
9
+ transform: (element, { j, options }) => {
10
+ return transformAttributeName("innerRef", "ref", { element, j, options })
11
11
  },
12
12
  })
13
13
 
@@ -6,8 +6,8 @@ export default attributeTransformFactory({
6
6
  condition: hasAttribute("spinner"),
7
7
  targetComponent: "Button",
8
8
  targetPackage: "@planningcenter/tapestry-react",
9
- transform: (element) => {
10
- transformAttributeName("spinner", "loading", { element })
9
+ transform: (element, { j, options }) => {
10
+ transformAttributeName("spinner", "loading", { element, j, options })
11
11
 
12
12
  return true
13
13
  },
@@ -8,11 +8,11 @@ const transform: Transform = attributeTransformFactory({
8
8
  condition: hasAttribute("title"),
9
9
  targetComponent: "Button",
10
10
  targetPackage: "@planningcenter/tapestry-react",
11
- transform: (element: JSXElement) =>
11
+ transform: (element: JSXElement, { j, options }) =>
12
12
  transformAttributeName(
13
13
  "title",
14
14
  () => (hasAttribute("icon")(element) ? "aria-label" : "label"),
15
- { element }
15
+ { element, j, options }
16
16
  ),
17
17
  })
18
18
 
@@ -5,12 +5,12 @@ import transform from "./childrenToLabel"
5
5
 
6
6
  const j = jscodeshift.withParser("tsx")
7
7
 
8
- function applyTransform(source: string): string {
8
+ function applyTransform(source: string, verbose = false): string {
9
9
  const fileInfo = { path: "test.tsx", source }
10
10
  const result = transform(
11
11
  fileInfo,
12
12
  { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
- {}
13
+ { verbose }
14
14
  ) as string | null
15
15
  return result || source
16
16
  }
@@ -142,5 +142,21 @@ function Test() {
142
142
  '<Checkbox checked disabled label="Accept terms" />'
143
143
  )
144
144
  })
145
+
146
+ it("should add CHANGED comment when verbose is enabled", () => {
147
+ const input = `
148
+ import { Checkbox } from "@planningcenter/tapestry-react"
149
+
150
+ function Test() {
151
+ return <Checkbox>Accept terms</Checkbox>
152
+ }
153
+ `.trim()
154
+
155
+ const result = applyTransform(input, true)
156
+ expect(result).toContain('<Checkbox label="Accept terms" />')
157
+ expect(result).toContain(
158
+ "CHANGED: tapestry-migration (children): children converted to label prop"
159
+ )
160
+ })
145
161
  })
146
162
  })
@@ -15,7 +15,7 @@ const transform: Transform = attributeTransformFactory({
15
15
  condition: hasChildren,
16
16
  targetComponent: "Checkbox",
17
17
  targetPackage: "@planningcenter/tapestry-react",
18
- transform: (element, { j, source }) => {
18
+ transform: (element, { j, options, source }) => {
19
19
  if (hasAttribute("label")(element)) {
20
20
  addComment({
21
21
  element,
@@ -32,6 +32,18 @@ const transform: Transform = attributeTransformFactory({
32
32
  if (isSimpleText && textContent) {
33
33
  addAttribute({ element, j, name: "label", value: textContent })
34
34
  removeChildren(element)
35
+
36
+ if (options?.verbose) {
37
+ addComment({
38
+ commentKind: "change",
39
+ element,
40
+ j,
41
+ scope: "children",
42
+ source,
43
+ text: buildComment("children converted to label prop", false),
44
+ })
45
+ }
46
+
35
47
  return true
36
48
  } else if (!isSimpleText) {
37
49
  addComment({
@@ -5,12 +5,12 @@ import transform from "./innerRefToRef"
5
5
 
6
6
  const j = jscodeshift.withParser("tsx")
7
7
 
8
- function applyTransform(source: string): string {
8
+ function applyTransform(source: string, verbose = false): string {
9
9
  const fileInfo = { path: "test.tsx", source }
10
10
  const result = transform(
11
11
  fileInfo,
12
12
  { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
- {}
13
+ { verbose }
14
14
  ) as string | null
15
15
  return result || source
16
16
  }
@@ -157,5 +157,23 @@ function Test() {
157
157
  expect(result).toContain("disabled")
158
158
  expect(result).not.toContain("innerRef=")
159
159
  })
160
+
161
+ it("should add CHANGED comment when verbose is enabled", () => {
162
+ const input = `
163
+ import { Checkbox } from "@planningcenter/tapestry-react"
164
+
165
+ function Test() {
166
+ const checkboxRef = React.useRef()
167
+ return <Checkbox innerRef={checkboxRef} label="Test" />
168
+ }
169
+ `.trim()
170
+
171
+ const result = applyTransform(input, true)
172
+ expect(result).toContain("ref={checkboxRef}")
173
+ expect(result).not.toContain("innerRef=")
174
+ expect(result).toContain(
175
+ "CHANGED: tapestry-migration (ref): renamed from innerRef"
176
+ )
177
+ })
160
178
  })
161
179
  })
@@ -6,8 +6,8 @@ const transform = attributeTransformFactory({
6
6
  condition: hasAttribute("innerRef"),
7
7
  targetComponent: "Checkbox",
8
8
  targetPackage: "@planningcenter/tapestry-react",
9
- transform: (element) => {
10
- return transformAttributeName("innerRef", "ref", { element })
9
+ transform: (element, { j, options }) => {
10
+ return transformAttributeName("innerRef", "ref", { element, j, options })
11
11
  },
12
12
  })
13
13
 
@@ -5,12 +5,12 @@ import transform from "./childrenToLabel"
5
5
 
6
6
  const j = jscodeshift.withParser("tsx")
7
7
 
8
- function applyTransform(source: string): string | null {
8
+ function applyTransform(source: string, verbose = false): string | null {
9
9
  const fileInfo = { path: "test.tsx", source }
10
10
  return transform(
11
11
  fileInfo,
12
12
  { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
- {}
13
+ { verbose }
14
14
  ) as string | null
15
15
  }
16
16
 
@@ -327,5 +327,21 @@ function Test() {
327
327
  "If icons are used in the component, you can use prefix and suffix to correctly display those icons"
328
328
  )
329
329
  })
330
+
331
+ it("should add CHANGED comment when verbose is enabled", () => {
332
+ const input = `
333
+ import { Link } from "@planningcenter/tapestry-react"
334
+
335
+ function Test() {
336
+ return <Link href="/home">Home</Link>
337
+ }
338
+ `.trim()
339
+
340
+ const result = applyTransform(input, true)
341
+ expect(result).toContain('<Link href="/home" label="Home" />')
342
+ expect(result).toContain(
343
+ "CHANGED: tapestry-migration (children): children converted to label prop"
344
+ )
345
+ })
330
346
  })
331
347
  })
@@ -15,7 +15,7 @@ const transform: Transform = attributeTransformFactory({
15
15
  condition: hasChildren,
16
16
  targetComponent: "Link",
17
17
  targetPackage: "@planningcenter/tapestry-react",
18
- transform: (element, { j, source }) => {
18
+ transform: (element, { j, options, source }) => {
19
19
  if (hasAttribute("label")(element)) {
20
20
  addComment({
21
21
  element,
@@ -32,6 +32,18 @@ const transform: Transform = attributeTransformFactory({
32
32
  if (isSimpleText && textContent) {
33
33
  addAttribute({ element, j, name: "label", value: textContent })
34
34
  removeChildren(element)
35
+
36
+ if (options?.verbose) {
37
+ addComment({
38
+ commentKind: "change",
39
+ element,
40
+ j,
41
+ scope: "children",
42
+ source,
43
+ text: buildComment("children converted to label prop", false),
44
+ })
45
+ }
46
+
35
47
  return true
36
48
  } else if (!isSimpleText) {
37
49
  addComment({
@@ -5,12 +5,12 @@ import transform from "./innerRefToRef"
5
5
 
6
6
  const j = jscodeshift.withParser("tsx")
7
7
 
8
- function applyTransform(source: string): string | null {
8
+ function applyTransform(source: string, verbose = false): string | null {
9
9
  const fileInfo = { path: "test.tsx", source }
10
10
  return transform(
11
11
  fileInfo,
12
12
  { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
- {}
13
+ { verbose }
14
14
  ) as string | null
15
15
  }
16
16
 
@@ -166,5 +166,22 @@ export default function Test() {
166
166
  const result = applyTransform(input)
167
167
  expect(result).toBe(expected)
168
168
  })
169
+
170
+ it("should add CHANGED comment when verbose is enabled", () => {
171
+ const input = `
172
+ import { Link } from "@planningcenter/tapestry-react"
173
+
174
+ export default function Test() {
175
+ return <Link innerRef={linkRef}>Go somewhere</Link>
176
+ }
177
+ `.trim()
178
+
179
+ const result = applyTransform(input, true)
180
+ expect(result).toContain("ref={linkRef}")
181
+ expect(result).not.toContain("innerRef=")
182
+ expect(result).toContain(
183
+ "CHANGED: tapestry-migration (ref): renamed from innerRef"
184
+ )
185
+ })
169
186
  })
170
187
  })
@@ -6,8 +6,8 @@ const transform = attributeTransformFactory({
6
6
  condition: hasAttribute("innerRef"),
7
7
  targetComponent: "Link",
8
8
  targetPackage: "@planningcenter/tapestry-react",
9
- transform: (element) => {
10
- return transformAttributeName("innerRef", "ref", { element })
9
+ transform: (element, { j, options }) => {
10
+ return transformAttributeName("innerRef", "ref", { element, j, options })
11
11
  },
12
12
  })
13
13
 
@@ -5,11 +5,11 @@ import removeAs from "./removeAs"
5
5
 
6
6
  const j = jscodeshift.withParser("tsx")
7
7
 
8
- function applyTransform(source: string): string | null {
8
+ function applyTransform(source: string, verbose = false): string | null {
9
9
  return removeAs(
10
10
  { path: "test.tsx", source },
11
11
  { j, jscodeshift: j, report: () => {}, stats: () => {} },
12
- {}
12
+ { verbose }
13
13
  ) as string | null
14
14
  }
15
15
 
@@ -188,5 +188,21 @@ export default function Test() {
188
188
  expect(result).toContain('href="/external"')
189
189
  expect(result).not.toContain('as="a"')
190
190
  })
191
+
192
+ it("should add CHANGED comment when verbose is enabled", () => {
193
+ const input = `
194
+ import { Link } from "@planningcenter/tapestry-react"
195
+
196
+ export default function Test() {
197
+ return <Link href="/home" as="a">Home</Link>
198
+ }
199
+ `.trim()
200
+
201
+ const result = applyTransform(input, true)
202
+ expect(result).not.toContain('as="a"')
203
+ expect(result).toContain(
204
+ "CHANGED: tapestry-migration (as): as='a' prop removed"
205
+ )
206
+ })
191
207
  })
192
208
  })
@@ -1,3 +1,4 @@
1
+ import { addComment } from "../../shared/actions/addComment"
1
2
  import { removeAttribute } from "../../shared/actions/removeAttribute"
2
3
  import { hasAttributeValue } from "../../shared/conditions/hasAttributeValue"
3
4
  import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
@@ -6,10 +7,20 @@ const transform = attributeTransformFactory({
6
7
  condition: hasAttributeValue("as", "a"),
7
8
  targetComponent: "Link",
8
9
  targetPackage: "@planningcenter/tapestry-react",
9
- transform: (element, { j, source }) => {
10
- // Remove as attribute
10
+ transform: (element, { j, options, source }) => {
11
11
  removeAttribute("as", { element, j, source })
12
12
 
13
+ if (options?.verbose) {
14
+ addComment({
15
+ commentKind: "change",
16
+ element,
17
+ j,
18
+ scope: "as",
19
+ source,
20
+ text: "as='a' prop removed",
21
+ })
22
+ }
23
+
13
24
  return true
14
25
  },
15
26
  })
@@ -5,12 +5,12 @@ import transform from "./removeInlineProp"
5
5
 
6
6
  const j = jscodeshift.withParser("tsx")
7
7
 
8
- function applyTransform(source: string): string | null {
8
+ function applyTransform(source: string, verbose = false): string | null {
9
9
  const fileInfo = { path: "test.tsx", source }
10
10
  return transform(
11
11
  fileInfo,
12
12
  { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
- {}
13
+ { verbose }
14
14
  ) as string | null
15
15
  }
16
16
 
@@ -291,5 +291,23 @@ export default function Test() {
291
291
  const result = applyTransform(input)
292
292
  expect(result).toBe(null)
293
293
  })
294
+
295
+ it("should add CHANGED comment when verbose is enabled", () => {
296
+ const input = `
297
+ import { Link } from "@planningcenter/tapestry-react"
298
+
299
+ export default function Test() {
300
+ return <Link href="/profile" inline>Profile</Link>
301
+ }
302
+ `.trim()
303
+
304
+ const result = applyTransform(input, true)
305
+ expect(result).not.toContain(" inline>")
306
+ expect(result).not.toContain(' inline"')
307
+ expect(result).not.toContain(" inline=")
308
+ expect(result).toContain(
309
+ "CHANGED: tapestry-migration (inline): inline prop removed"
310
+ )
311
+ })
294
312
  })
295
313
  })
@@ -1,3 +1,4 @@
1
+ import { addComment } from "../../shared/actions/addComment"
1
2
  import { removeAttribute } from "../../shared/actions/removeAttribute"
2
3
  import { hasAttribute } from "../../shared/conditions/hasAttribute"
3
4
  import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
@@ -6,8 +7,20 @@ const transform = attributeTransformFactory({
6
7
  condition: hasAttribute("inline"),
7
8
  targetComponent: "Link",
8
9
  targetPackage: "@planningcenter/tapestry-react",
9
- transform: (element, { j, source }) => {
10
+ transform: (element, { j, options, source }) => {
10
11
  removeAttribute("inline", { element, j, source })
12
+
13
+ if (options?.verbose) {
14
+ addComment({
15
+ commentKind: "change",
16
+ element,
17
+ j,
18
+ scope: "inline",
19
+ source,
20
+ text: "inline prop removed",
21
+ })
22
+ }
23
+
11
24
  return true
12
25
  },
13
26
  })
@@ -5,11 +5,11 @@ import targetBlankToExternal from "./targetBlankToExternal"
5
5
 
6
6
  const j = jscodeshift.withParser("tsx")
7
7
 
8
- function applyTransform(source: string): string | null {
8
+ function applyTransform(source: string, verbose = false): string | null {
9
9
  return targetBlankToExternal(
10
10
  { path: "test.tsx", source },
11
11
  { j, jscodeshift: j, report: () => {}, stats: () => {} },
12
- {}
12
+ { verbose }
13
13
  ) as string | null
14
14
  }
15
15
 
@@ -201,5 +201,21 @@ export default function Test() {
201
201
  expect(result).toContain("external")
202
202
  expect(result).not.toContain('target="_blank"')
203
203
  })
204
+
205
+ it("should add CHANGED comment when verbose is enabled", () => {
206
+ const input = `
207
+ import { Link } from "@planningcenter/tapestry-react"
208
+
209
+ export default function Test() {
210
+ return <Link href="/external" target="_blank">External Link</Link>
211
+ }
212
+ `.trim()
213
+
214
+ const result = applyTransform(input, true)
215
+ expect(result).toContain("external")
216
+ expect(result).toContain(
217
+ "CHANGED: tapestry-migration (external): target='_blank' converted to external prop"
218
+ )
219
+ })
204
220
  })
205
221
  })
@@ -1,4 +1,5 @@
1
1
  import { addAttribute } from "../../shared/actions/addAttribute"
2
+ import { addComment } from "../../shared/actions/addComment"
2
3
  import { removeAttribute } from "../../shared/actions/removeAttribute"
3
4
  import { hasAttributeValue } from "../../shared/conditions/hasAttributeValue"
4
5
  import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
@@ -7,14 +8,11 @@ const transform = attributeTransformFactory({
7
8
  condition: hasAttributeValue("target", "_blank"),
8
9
  targetComponent: "Link",
9
10
  targetPackage: "@planningcenter/tapestry-react",
10
- transform: (element, { j, source }) => {
11
- // Remove target attribute
11
+ transform: (element, { j, options, source }) => {
12
12
  removeAttribute("target", { element, j, source })
13
13
 
14
- // Remove rel attribute if it exists
15
14
  removeAttribute("rel", { element, j, source })
16
15
 
17
- // Add external attribute as shorthand boolean
18
16
  addAttribute({
19
17
  booleanAsShorthand: true,
20
18
  element,
@@ -23,6 +21,17 @@ const transform = attributeTransformFactory({
23
21
  value: true, // This will create just "external" not "external={true}"
24
22
  })
25
23
 
24
+ if (options?.verbose) {
25
+ addComment({
26
+ commentKind: "change",
27
+ element,
28
+ j,
29
+ scope: "external",
30
+ source,
31
+ text: "target='_blank' converted to external prop",
32
+ })
33
+ }
34
+
26
35
  return true
27
36
  },
28
37
  })
@@ -5,12 +5,12 @@ import transform from "./toToHref"
5
5
 
6
6
  const j = jscodeshift.withParser("tsx")
7
7
 
8
- function applyTransform(source: string): string | null {
8
+ function applyTransform(source: string, verbose = false): string | null {
9
9
  const fileInfo = { path: "test.tsx", source }
10
10
  return transform(
11
11
  fileInfo,
12
12
  { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
- {}
13
+ { verbose }
14
14
  ) as string | null
15
15
  }
16
16
 
@@ -208,6 +208,23 @@ export default function Test({ items }) {
208
208
  expect(result).not.toContain("to={item.url}")
209
209
  expect(result).not.toContain('to="/conditional"')
210
210
  })
211
+
212
+ it("should add CHANGED comment when verbose is enabled", () => {
213
+ const input = `
214
+ import { Link } from "@planningcenter/tapestry-react"
215
+
216
+ export default function Test() {
217
+ return <Link to="/dashboard">Dashboard</Link>
218
+ }
219
+ `.trim()
220
+
221
+ const result = applyTransform(input, true)
222
+ expect(result).toContain('href="/dashboard"')
223
+ expect(result).not.toContain('to="/dashboard"')
224
+ expect(result).toContain(
225
+ "CHANGED: tapestry-migration (href): renamed from to"
226
+ )
227
+ })
211
228
  })
212
229
 
213
230
  describe("no changes scenarios", () => {
@@ -6,8 +6,8 @@ const transform = attributeTransformFactory({
6
6
  condition: hasAttribute("to"),
7
7
  targetComponent: "Link",
8
8
  targetPackage: "@planningcenter/tapestry-react",
9
- transform: (element) => {
10
- return transformAttributeName("to", "href", { element })
9
+ transform: (element, { j, options }) => {
10
+ return transformAttributeName("to", "href", { element, j, options })
11
11
  },
12
12
  })
13
13
 
@@ -12,14 +12,16 @@ export function addComment({
12
12
  j,
13
13
  source,
14
14
  scope,
15
+ commentKind = "todo",
15
16
  }: {
17
+ commentKind?: "todo" | "change"
16
18
  element: JSXElement
17
19
  j: JSCodeshift
18
20
  scope: string
19
21
  source: Collection
20
22
  text: string
21
23
  }) {
22
- const commentText = formatComment(text, scope)
24
+ const commentText = formatComment(text, scope, commentKind)
23
25
  if (tryInsertJSXComment(source, element, commentText, j)) {
24
26
  return
25
27
  }
@@ -64,6 +66,11 @@ function tryInsertJSXComment(
64
66
  return found
65
67
  }
66
68
 
67
- export function formatComment(text: string, scope: string): string {
68
- return ` TODO: tapestry-migration (${scope}): ${text} `
69
+ export function formatComment(
70
+ text: string,
71
+ scope: string,
72
+ commentKind: "todo" | "change" = "todo"
73
+ ): string {
74
+ const prefix = commentKind === "change" ? "CHANGED" : "TODO"
75
+ return ` ${prefix}: tapestry-migration (${scope}): ${text} `
69
76
  }
@@ -6,8 +6,10 @@ export function addCommentToAttribute({
6
6
  text,
7
7
  attribute,
8
8
  j,
9
+ commentKind = "todo",
9
10
  }: {
10
11
  attribute: JSXAttribute | JSXSpreadAttribute
12
+ commentKind?: "todo" | "change"
11
13
  j: JSCodeshift
12
14
  text: string
13
15
  }) {
@@ -15,7 +17,7 @@ export function addCommentToAttribute({
15
17
  ((attribute.type === "JSXAttribute" && attribute.name.name) as string) ||
16
18
  "spreadAttribute"
17
19
  const comment = j.commentBlock(
18
- formatComment(text, attributeName),
20
+ formatComment(text, attributeName, commentKind),
19
21
  true,
20
22
  false
21
23
  )
@@ -1,11 +1,16 @@
1
- import { JSXElement } from "jscodeshift"
1
+ import { JSCodeshift, JSXElement, Options } from "jscodeshift"
2
2
 
3
3
  import { findAttribute } from "../findAttribute"
4
+ import { addCommentToAttribute } from "./addCommentToAttribute"
4
5
 
5
6
  export function transformAttributeName(
6
7
  name: string,
7
8
  nameTransform: string | ((element: JSXElement) => string),
8
- { element }: { element: JSXElement }
9
+ {
10
+ element,
11
+ j,
12
+ options,
13
+ }: { element: JSXElement; j?: JSCodeshift; options?: Options }
9
14
  ): boolean {
10
15
  if (!nameTransform) return false
11
16
  const attributes = element.openingElement.attributes || []
@@ -16,5 +21,14 @@ export function transformAttributeName(
16
21
  typeof nameTransform === "string" ? nameTransform : nameTransform(element)
17
22
  attribute.name.name = resolvedName
18
23
 
24
+ if (options?.verbose && j && attribute.type === "JSXAttribute") {
25
+ addCommentToAttribute({
26
+ attribute,
27
+ commentKind: "change",
28
+ j,
29
+ text: `renamed from ${name}`,
30
+ })
31
+ }
32
+
19
33
  return true
20
34
  }
@@ -3,10 +3,12 @@ import {
3
3
  JSCodeshift,
4
4
  JSXAttribute,
5
5
  JSXElement,
6
+ Options,
6
7
  Transform,
7
8
  } from "jscodeshift"
8
9
 
9
10
  import { addAttribute, Conditional } from "../actions/addAttribute"
11
+ import { addCommentToAttribute } from "../actions/addCommentToAttribute"
10
12
  import { getAttribute } from "../actions/getAttribute"
11
13
  import { removeAttribute } from "../actions/removeAttribute"
12
14
  import { hasAttribute } from "../conditions/hasAttribute"
@@ -263,7 +265,11 @@ export function attributeCombineFactory({
263
265
  targetPackage: packageName,
264
266
  transform: (
265
267
  element: JSXElement,
266
- { j, source }: { j: JSCodeshift; source: Collection }
268
+ {
269
+ j,
270
+ options,
271
+ source,
272
+ }: { j: JSCodeshift; options: Options; source: Collection }
267
273
  ) => {
268
274
  const attributes: AttributeWithName[] = sourceAttributes.map(
269
275
  (name: string) => ({
@@ -294,6 +300,32 @@ export function attributeCombineFactory({
294
300
  }
295
301
  addAttribute({ element, j, name: targetAttribute, value: mappingResult })
296
302
 
303
+ if (options?.verbose) {
304
+ const addedAttr = getAttribute({ element, name: targetAttribute })
305
+ if (addedAttr && addedAttr.type === "JSXAttribute") {
306
+ const v1 = getAttributeStringValue(
307
+ attributes[0].attribute,
308
+ defaults[sourceAttributes[0]] || "",
309
+ valueNormalizers[sourceAttributes[0]]
310
+ )
311
+ const v2 = getAttributeStringValue(
312
+ attributes[1].attribute,
313
+ defaults[sourceAttributes[1]] || "",
314
+ valueNormalizers[sourceAttributes[1]]
315
+ )
316
+ const fromParts = [
317
+ `${sourceAttributes[0]}=${v1 ?? "<unsupported>"}`,
318
+ `${sourceAttributes[1]}=${v2 ?? "<unsupported>"}`,
319
+ ]
320
+ addCommentToAttribute({
321
+ attribute: addedAttr,
322
+ commentKind: "change",
323
+ j,
324
+ text: `derived from ${fromParts.join(" ")}`,
325
+ })
326
+ }
327
+ }
328
+
297
329
  return true
298
330
  },
299
331
  })
@@ -13,6 +13,7 @@ import {
13
13
  stylePropNames,
14
14
  } from "../../../../dist/tapestry-react-shim.cjs"
15
15
  import { addComment } from "../../shared/actions/addComment"
16
+ import { addCommentToAttribute } from "../../shared/actions/addCommentToAttribute"
16
17
  import { getAttribute } from "../../shared/actions/getAttribute"
17
18
  import { removeAttribute } from "../../shared/actions/removeAttribute"
18
19
  import { attributeTransformFactory } from "./attributeTransformFactory"
@@ -323,6 +324,25 @@ export function stylePropTransformFactory(config: {
323
324
  // Only apply styles if there are actual CSS properties to add
324
325
  if (Object.keys(styles).length > 0) {
325
326
  applyStylesToComponent({ element, j, styles })
327
+
328
+ if (options.verbose) {
329
+ const styleAttr = getAttribute({ element, name: "style" })
330
+ if (styleAttr && styleAttr.type === "JSXAttribute") {
331
+ const folded = Object.keys(allStyleProps)
332
+ const direct = Object.keys(directStyleProps)
333
+ const parts = [] as string[]
334
+ if (folded.length) parts.push(`props: ${folded.join(", ")}`)
335
+ if (direct.length) parts.push(`direct: ${direct.join(", ")}`)
336
+ if (parts.length) {
337
+ addCommentToAttribute({
338
+ attribute: styleAttr,
339
+ commentKind: "change",
340
+ j,
341
+ text: `migrated style from ${parts.join("; ")}`,
342
+ })
343
+ }
344
+ }
345
+ }
326
346
  }
327
347
  } catch (error) {
328
348
  console.log("Error processing style props:", error)
package/src/index.ts CHANGED
@@ -23,7 +23,7 @@ program
23
23
  "-p, --path <path>",
24
24
  "REQUIRED: The path to the folder/file to migrate"
25
25
  )
26
- .option("-f, --fix", "Write the changes to files (default: dry run)")
26
+ .option("-d, --dry-run", "Preview changes without writing to files")
27
27
  .option("-v, --verbose", "Verbose output")
28
28
  .option(
29
29
  "-j, --js-theme <path>",
@@ -31,7 +31,7 @@ program
31
31
  )
32
32
  .option(
33
33
  "-r, --report-path <path>",
34
- "Path for the migration report. Only runs with --fix and does not create a report if no additional changes are required.",
34
+ "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.",
35
35
  "MIGRATION_REPORT.md"
36
36
  )
37
37
  .action((componentName, options) => {
@@ -41,7 +41,7 @@ program
41
41
  console.log(`🎨 Migrating ${componentName} components...`)
42
42
  console.log(`📁 Target: ${options.path}`)
43
43
  console.log(
44
- `🔧 Mode: ${options.fix ? "Apply changes" : "Dry run (preview only)"}`
44
+ `🔧 Mode: ${options.dryRun ? "Dry run (preview only)" : "Apply changes"}`
45
45
  )
46
46
  runTransforms(key, options)
47
47
  } else {
@@ -34,7 +34,7 @@ export function runTransforms(key: string, options: Options): void {
34
34
  "-t",
35
35
  transformPath,
36
36
  targetPath,
37
- options.fix ? "" : "--dry",
37
+ options.dryRun ? "--dry" : "",
38
38
  options.verbose ? "--verbose=2" : "",
39
39
  "--parser=tsx",
40
40
  ]
@@ -12,6 +12,7 @@ interface CommentData {
12
12
  line: number
13
13
  scope: string
14
14
  text: string
15
+ type: "todo" | "changed"
15
16
  }
16
17
 
17
18
  interface CommentStats {
@@ -20,6 +21,7 @@ interface CommentStats {
20
21
  files: string[]
21
22
  normalizedText: string
22
23
  scope: string
24
+ type: "todo" | "changed"
23
25
  weight: number
24
26
  }
25
27
 
@@ -60,14 +62,17 @@ function extractCommentsFromFile(filePath: string): CommentData[] {
60
62
  // Match both JSX and regular comment formats:
61
63
  // {/* TODO: tapestry-migration (scope): text */}
62
64
  // /* TODO: tapestry-migration (scope): text */
65
+ // {/* CHANGED: tapestry-migration (scope): text */}
66
+ // /* CHANGED: tapestry-migration (scope): text */
63
67
  // Use lazy matching with [\s\S]*? to capture everything including * until we hit the closing */
64
68
  const commentRegex =
65
- /(?:\{\/\*|\/\*)\s*TODO:\s*tapestry-migration\s*\(([^)]+)\):\s*([\s\S]*?)\s*\*\/\}?/g
69
+ /(?:\{\/\*|\/\*)\s*(TODO|CHANGED):\s*tapestry-migration\s*\(([^)]+)\):\s*([\s\S]*?)\s*\*\/\}?/g
66
70
 
67
71
  let match
68
72
  while ((match = commentRegex.exec(content)) !== null) {
69
- const scope = match[1].trim()
70
- const text = match[2].trim()
73
+ const commentType = match[1].trim().toLowerCase() as "todo" | "changed"
74
+ const scope = match[2].trim()
75
+ const text = match[3].trim()
71
76
 
72
77
  // Calculate line number
73
78
  const beforeMatch = content.substring(0, match.index)
@@ -78,6 +83,7 @@ function extractCommentsFromFile(filePath: string): CommentData[] {
78
83
  line,
79
84
  scope,
80
85
  text,
86
+ type: commentType,
81
87
  })
82
88
  }
83
89
 
@@ -228,7 +234,7 @@ function aggregateComments(
228
234
 
229
235
  for (const comment of comments) {
230
236
  const normalizedText = normalizeCommentText(comment.text)
231
- const key = `${comment.scope}|||${normalizedText}`
237
+ const key = `${comment.type}|||${comment.scope}|||${normalizedText}`
232
238
 
233
239
  if (statsMap.has(key)) {
234
240
  const stats = statsMap.get(key)!
@@ -248,6 +254,7 @@ function aggregateComments(
248
254
  files: [comment.file],
249
255
  normalizedText,
250
256
  scope: comment.scope,
257
+ type: comment.type,
251
258
  weight,
252
259
  })
253
260
  }
@@ -314,6 +321,19 @@ function generateMarkdownReport(
314
321
  lines.push(`- **Total Comments:** ${totalComments}`)
315
322
  lines.push(`- **Unique Comment Types:** ${uniqueCommentTypes}`)
316
323
  lines.push(`- **Affected Files:** ${affectedFiles}`)
324
+
325
+ // Add breakdown by comment type
326
+ const todoComments = allComments.filter((c) => c.type === "todo")
327
+ const changedComments = allComments.filter((c) => c.type === "changed")
328
+ lines.push("")
329
+ lines.push("### Comment Breakdown")
330
+ lines.push("")
331
+ lines.push(
332
+ `- **TODO Comments:** ${todoComments.length} (requires manual attention)`
333
+ )
334
+ lines.push(
335
+ `- **CHANGED Comments:** ${changedComments.length} (automatic transformations)`
336
+ )
317
337
  lines.push("")
318
338
  lines.push("### Effort by Difficulty")
319
339
  lines.push("")
@@ -343,6 +363,48 @@ function generateMarkdownReport(
343
363
  lines.push(`- **${difficulty}:** ${data.count} occurrences`)
344
364
  }
345
365
 
366
+ lines.push("")
367
+ lines.push("## Automatic Changes")
368
+ lines.push("")
369
+ lines.push(
370
+ "The following changes were applied automatically during migration:"
371
+ )
372
+ lines.push("")
373
+
374
+ const changedStats = stats.filter((s) => s.type === "changed")
375
+
376
+ if (changedStats.length > 0) {
377
+ const changedByScope = new Map<string, CommentStats[]>()
378
+ for (const stat of changedStats) {
379
+ if (!changedByScope.has(stat.scope)) {
380
+ changedByScope.set(stat.scope, [])
381
+ }
382
+ changedByScope.get(stat.scope)!.push(stat)
383
+ }
384
+
385
+ for (const [scope, scopeStats] of Array.from(changedByScope.entries()).sort(
386
+ (a, b) => {
387
+ const totalA = a[1].reduce((sum, s) => sum + s.count, 0)
388
+ const totalB = b[1].reduce((sum, s) => sum + s.count, 0)
389
+ return totalB - totalA
390
+ }
391
+ )) {
392
+ lines.push(`### ${scope}`)
393
+ lines.push("")
394
+
395
+ for (const stat of scopeStats) {
396
+ lines.push(
397
+ `- **${stat.normalizedText}** (${stat.count} occurrence${stat.count > 1 ? "s" : ""})`
398
+ )
399
+ }
400
+
401
+ lines.push("")
402
+ }
403
+ } else {
404
+ lines.push("_No automatic changes were applied._")
405
+ lines.push("")
406
+ }
407
+
346
408
  lines.push("")
347
409
  lines.push("## Comments by Effort (sorted by total effort)")
348
410
  lines.push("")
@@ -373,7 +435,9 @@ function generateMarkdownReport(
373
435
  lines.push("")
374
436
 
375
437
  for (const stat of scopeStats) {
376
- lines.push(`#### ${stat.normalizedText}`)
438
+ const typeLabel = stat.type === "changed" ? "CHANGED" : "TODO"
439
+ const typeEmoji = stat.type === "changed" ? "✅" : "⚠️"
440
+ lines.push(`#### ${typeEmoji} [${typeLabel}] ${stat.normalizedText}`)
377
441
  lines.push("")
378
442
  lines.push(`- **Occurrences:** ${stat.count}`)
379
443
  lines.push(`- **Files affected:** ${stat.files.length}`)
@@ -1,5 +1,5 @@
1
1
  export interface Options {
2
- fix?: boolean
2
+ dryRun?: boolean
3
3
  jsTheme?: string
4
4
  path: string
5
5
  reportPath?: string