@planningcenter/tapestry-migration-cli 2.3.0-rc.8 → 2.3.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 (25) hide show
  1. package/dist/tapestry-react-shim.cjs +2 -1
  2. package/package.json +2 -2
  3. package/src/components/button/transforms/convertStyleProps.test.ts +4 -5
  4. package/src/components/link/index.ts +32 -0
  5. package/src/components/link/transforms/inlineToKind.test.ts +308 -0
  6. package/src/components/link/transforms/inlineToKind.ts +51 -0
  7. package/src/components/link/transforms/targetBlankToExternal.test.ts +191 -0
  8. package/src/components/link/transforms/targetBlankToExternal.ts +30 -0
  9. package/src/components/link/transforms/toToHref.test.ts +245 -0
  10. package/src/components/link/transforms/toToHref.ts +14 -0
  11. package/src/components/shared/actions/addAttribute.test.ts +108 -0
  12. package/src/components/shared/actions/addAttribute.ts +14 -0
  13. package/src/components/shared/actions/removeAttribute.ts +9 -2
  14. package/src/components/shared/actions/transformElementName.ts +23 -9
  15. package/src/components/shared/transformFactories/attributeTransformFactory.test.ts +83 -0
  16. package/src/components/shared/transformFactories/attributeTransformFactory.ts +21 -14
  17. package/src/components/shared/transformFactories/componentTransformFactory.test.ts +85 -2
  18. package/src/components/shared/transformFactories/componentTransformFactory.ts +41 -22
  19. package/src/components/shared/transformFactories/helpers/findJSXElements.ts +37 -0
  20. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +2 -27
  21. package/src/components/shared/types.ts +19 -1
  22. package/src/index.ts +7 -2
  23. package/src/jscodeshiftRunner.ts +7 -0
  24. package/src/reportGenerator.ts +450 -0
  25. package/src/shared/types.ts +1 -0
@@ -1,7 +1,90 @@
1
+ import jscodeshift from "jscodeshift"
1
2
  import { describe, expect, it } from "vitest"
2
3
 
4
+ import { componentTransformFactory } from "./componentTransformFactory"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string | null {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ return componentTransformFactory({
11
+ condition: () => true,
12
+ fromComponent: "Link.Inline",
13
+ fromPackage: "@planningcenter/tapestry-react",
14
+ toComponent: "Link",
15
+ toPackage: "@planningcenter/tapestry-react",
16
+ })(fileInfo, { j, jscodeshift: j, report: () => {}, stats: () => {} }, {}) as
17
+ | string
18
+ | null
19
+ }
20
+
3
21
  describe("componentTransformFactory", () => {
4
- it("is a placeholder test", () => {
5
- expect(true).toBe(true)
22
+ it("should transform Link.Inline to Link", () => {
23
+ const input = `
24
+ import { Link } from "@planningcenter/tapestry-react"
25
+
26
+ export default function Test() {
27
+ return <Link.Inline>Test</Link.Inline>
28
+ }
29
+ `.trim()
30
+
31
+ const result = applyTransform(input)
32
+ expect(result).toContain("<Link>Test</Link>")
33
+ expect(result).not.toContain("<Link.Inline>")
34
+ })
35
+
36
+ it("should handle multiple Link.Inline elements", () => {
37
+ const input = `
38
+ import { Link } from "@planningcenter/tapestry-react"
39
+
40
+ export default function Test() {
41
+ return (
42
+ <div>
43
+ <Link.Inline>First</Link.Inline>
44
+ <Link.Inline>Second</Link.Inline>
45
+ </div>
46
+ )
47
+ }
48
+ `.trim()
49
+
50
+ const result = applyTransform(input)
51
+ expect(result).toContain("<Link>First</Link>")
52
+ expect(result).toContain("<Link>Second</Link>")
53
+ expect(result).not.toContain("<Link.Inline>")
54
+ })
55
+
56
+ it("should not transform other member expressions", () => {
57
+ const input = `
58
+ import { Link } from "@planningcenter/tapestry-react"
59
+
60
+ export default function Test() {
61
+ return (
62
+ <div>
63
+ <Link.Inline>Inline</Link.Inline>
64
+ <Link.Primary>Primary</Link.Primary>
65
+ </div>
66
+ )
67
+ }
68
+ `.trim()
69
+
70
+ const result = applyTransform(input)
71
+ expect(result).toContain("<Link>Inline</Link>")
72
+ expect(result).toContain("<Link.Primary>Primary</Link.Primary>")
73
+ expect(result).not.toContain("<Link.Inline>")
74
+ })
75
+
76
+ it("should preserve original component name when removing member expressions", () => {
77
+ const input = `
78
+ import { Link as TapestryLink } from "@planningcenter/tapestry-react"
79
+
80
+ export default function Test() {
81
+ return <TapestryLink.Inline>Test</TapestryLink.Inline>
82
+ }
83
+ `.trim()
84
+
85
+ const result = applyTransform(input)
86
+ expect(result).toContain("<TapestryLink>Test</TapestryLink>")
87
+ expect(result).not.toContain("<TapestryLink.Inline>")
88
+ expect(result).not.toContain("<Link>")
6
89
  })
7
90
  })
@@ -2,6 +2,7 @@ import { Transform } from "jscodeshift"
2
2
 
3
3
  import { transformElementName } from "../actions/transformElementName"
4
4
  import { TransformCondition } from "../types"
5
+ import { findJSXElements } from "./helpers/findJSXElements"
5
6
  import {
6
7
  getImportName,
7
8
  hasConflictingImport,
@@ -17,7 +18,7 @@ export function componentTransformFactory(config: {
17
18
  condition: TransformCondition
18
19
  /** Optional alias to use if target component conflicts with existing imports */
19
20
  conflictAlias?: string
20
- /** The source component name to transform from */
21
+ /** The source component name to transform from (supports member expressions like "Link.Inline") */
21
22
  fromComponent: string
22
23
  /** The package to import the source component from */
23
24
  fromPackage: string
@@ -31,44 +32,62 @@ export function componentTransformFactory(config: {
31
32
  const source = j(fileInfo.source)
32
33
  let hasChanges = false
33
34
 
35
+ // Parse the fromComponent to handle member expressions
36
+ const [fromComponentName, fromComponentExpression] =
37
+ config.fromComponent.split(".")
38
+
34
39
  // Get the local name of the source component
35
- const sourceComponentName = getImportName(
36
- config.fromComponent,
40
+ const sourceComponentLocalName = getImportName(
41
+ fromComponentName,
37
42
  config.fromPackage,
38
43
  { j, source }
39
44
  )
40
45
 
41
46
  // Only proceed if source component is imported
42
- if (!sourceComponentName) {
47
+ if (!sourceComponentLocalName) {
43
48
  return null
44
49
  }
45
50
 
46
- const hasConflict = hasConflictingImport(
47
- config.toComponent,
48
- [config.fromPackage, config.toPackage],
49
- { j, source }
50
- )
51
+ // Determine the target component name
52
+ // If we're removing a member expression (same base name), preserve the original component name
53
+ const isRemovingMemberExpression =
54
+ fromComponentExpression && fromComponentName === config.toComponent
51
55
 
52
- const targetComponentName = hasConflict
53
- ? config.conflictAlias || `T${config.toComponent}`
54
- : config.toComponent
56
+ let targetComponentName: string
57
+ if (isRemovingMemberExpression) {
58
+ // Preserve the original component name (e.g., TapestryLink.Inline → TapestryLink)
59
+ targetComponentName = sourceComponentLocalName
60
+ } else {
61
+ // Use the specified toComponent (e.g., Button → TapestryButton)
62
+ const hasConflict = hasConflictingImport(
63
+ config.toComponent,
64
+ [config.fromPackage, config.toPackage],
65
+ { j, source }
66
+ )
67
+ targetComponentName = hasConflict
68
+ ? config.conflictAlias || `T${config.toComponent}`
69
+ : config.toComponent
70
+ }
55
71
 
56
72
  // Transform matching JSX elements
57
- source
58
- .find(j.JSXOpeningElement, { name: { name: sourceComponentName } })
59
- .forEach((path) => {
60
- const element = path.parent.value
73
+ findJSXElements(
74
+ source,
75
+ j,
76
+ config.fromComponent,
77
+ sourceComponentLocalName
78
+ ).forEach((path) => {
79
+ const element = path.parent.value
61
80
 
62
- if (config.condition(element)) {
63
- if (transformElementName({ element, name: targetComponentName })) {
64
- hasChanges = true
65
- }
81
+ if (config.condition(element)) {
82
+ if (transformElementName({ element, name: targetComponentName })) {
83
+ hasChanges = true
66
84
  }
67
- })
85
+ }
86
+ })
68
87
 
69
88
  if (hasChanges) {
70
89
  // Handle import management
71
- manageImports(source, config, sourceComponentName, targetComponentName, j)
90
+ manageImports(source, config, fromComponentName, targetComponentName, j)
72
91
  }
73
92
 
74
93
  return hasChanges ? source.toSource() : null
@@ -0,0 +1,37 @@
1
+ import { Collection, JSCodeshift, JSXElement } from "jscodeshift"
2
+
3
+ /**
4
+ * Helper function to find JSX elements, supporting both regular components and member expressions
5
+ */
6
+ export function findJSXElements(
7
+ source: Collection,
8
+ j: JSCodeshift,
9
+ componentName: string,
10
+ localComponentName: string
11
+ ) {
12
+ // Parse the component name to handle member expressions
13
+ const [, memberExpression] = componentName.split(".")
14
+
15
+ if (memberExpression) {
16
+ // Find member expressions (e.g., Link.Inline)
17
+ return source
18
+ .find(j.JSXOpeningElement, {
19
+ name: {
20
+ object: { name: localComponentName },
21
+ type: "JSXMemberExpression",
22
+ },
23
+ })
24
+ .filter((path) => {
25
+ const element = path.parent.value as JSXElement
26
+ return (
27
+ element.openingElement.name.type === "JSXMemberExpression" &&
28
+ element.openingElement.name.property.name === memberExpression
29
+ )
30
+ })
31
+ } else {
32
+ // Find regular components (e.g., Link)
33
+ return source.find(j.JSXOpeningElement, {
34
+ name: { name: localComponentName },
35
+ })
36
+ }
37
+ }
@@ -108,7 +108,8 @@ function processRemoveStyleProps({
108
108
  const propName = attr.name.name as string
109
109
  removeAttribute(propName, {
110
110
  buildComment: (name, value) =>
111
- `${name} has been removed as this is covered by default styling: ${name}: ${value}`,
111
+ `${name} has been removed as this is covered by default styling: ${name}=${value}`,
112
+ commentScope: "styleProp",
112
113
  element,
113
114
  j,
114
115
  source,
@@ -299,7 +300,6 @@ export function stylePropTransformFactory(config: {
299
300
  source,
300
301
  })
301
302
 
302
- // Convert collected style props to CSS if any exist
303
303
  if (
304
304
  Object.keys(allStyleProps).length > 0 ||
305
305
  Object.keys(directStyleProps).length > 0
@@ -307,7 +307,6 @@ export function stylePropTransformFactory(config: {
307
307
  try {
308
308
  let styles: Record<string, unknown> = {}
309
309
 
310
- // Process theme props through the theme system
311
310
  if (Object.keys(allStyleProps).length > 0) {
312
311
  const result = splitStyles({
313
312
  ...allStyleProps,
@@ -317,11 +316,9 @@ export function stylePropTransformFactory(config: {
317
316
  styles = { ...styles, ...(cssObject[0] || {}) }
318
317
  }
319
318
 
320
- // Add direct props without theme processing
321
319
  styles = { ...styles, ...directStyleProps }
322
320
  if (options.verbose) console.log("Final generated styles:", styles)
323
321
 
324
- // Apply the styles to the component
325
322
  applyStylesToComponent({ element, j, styles })
326
323
  } catch (error) {
327
324
  console.log("Error processing style props:", error)
@@ -329,28 +326,6 @@ export function stylePropTransformFactory(config: {
329
326
  }
330
327
  }
331
328
 
332
- // Add new attributes from replacements and mappings
333
- // const allNewAttributes = [...mappingResult.newAttributes]
334
- // if (allNewAttributes.length > 0) {
335
- // path.value.attributes = path.value.attributes || []
336
- // path.value.attributes.push(...allNewAttributes)
337
- // hasChanges = true
338
- // }
339
-
340
- // Check for unhandled style props before removing processed attributes
341
- // const mappingPropNames = Object.keys(STYLE_PROP_MAPPINGS)
342
- // processUnhandledProps(
343
- // j,
344
- // path,
345
- // attributes,
346
- // KEEP_STYLE_PROPS,
347
- // REMOVE_STYLE_PROPS,
348
- // mappingPropNames,
349
- // stylePropNames,
350
- // options,
351
- // fileInfo
352
- // )
353
-
354
329
  return attributes.length + removeAttributes.length > 0
355
330
  }
356
331
 
@@ -1,6 +1,24 @@
1
- import { JSXElement } from "jscodeshift"
1
+ import {
2
+ ASTPath,
3
+ Collection,
4
+ JSCodeshift,
5
+ JSXElement,
6
+ JSXOpeningElement,
7
+ } from "jscodeshift"
2
8
 
3
9
  /**
4
10
  * Condition function that determines whether a JSX element should be transformed
5
11
  */
6
12
  export type TransformCondition = (element: JSXElement) => boolean
13
+
14
+ /**
15
+ * Transform function for JSX elements
16
+ */
17
+ export type TransformFunction = (
18
+ element: JSXElement,
19
+ resources: {
20
+ j: JSCodeshift
21
+ path: ASTPath<JSXOpeningElement>
22
+ source: Collection
23
+ }
24
+ ) => boolean
package/src/index.ts CHANGED
@@ -10,19 +10,24 @@ program
10
10
  .name("tapestry-migration-cli")
11
11
  .description("CLI tool for Tapestry migrations")
12
12
 
13
- const COMPONENTS = ["button"]
13
+ const COMPONENTS = ["button", "link"]
14
14
 
15
15
  program
16
16
  .command("run")
17
17
  .description("Run a migration of a component from Tapestry React to Tapestry")
18
18
  .argument("<component-name>", "The name of the component to migrate")
19
19
  .option("-f, --fix", "Write the changes")
20
- .option("-p, --path <path>", "The path to the folder/file to migrate")
20
+ .requiredOption("-p, --path <path>", "The path to the folder/file to migrate")
21
21
  .option("-v, --verbose", "Verbose output")
22
22
  .option(
23
23
  "-j, --js-theme <path>",
24
24
  "Path to JavaScript theme file containing design tokens"
25
25
  )
26
+ .option(
27
+ "-r, --report-path <path>",
28
+ "Path for the migration report. Only runs with --fix and does not create a report if no additional changes are required.",
29
+ "MIGRATION_REPORT.md"
30
+ )
26
31
  .action((componentName, options) => {
27
32
  console.log("Hello from Tapestry Migration CLI! 🎨")
28
33
  console.log(`Component: ${componentName}`)
@@ -3,6 +3,7 @@ import { existsSync } from "fs"
3
3
  import { dirname, resolve } from "path"
4
4
  import { fileURLToPath } from "url"
5
5
 
6
+ import { generateMigrationReport, WEIGHT_CONFIGS } from "./reportGenerator"
6
7
  import { Options } from "./shared/types"
7
8
 
8
9
  const __filename = fileURLToPath(import.meta.url)
@@ -43,6 +44,12 @@ export function runTransforms(key: string, options: Options): void {
43
44
  env: process.env,
44
45
  stdio: "inherit",
45
46
  })
47
+
48
+ generateMigrationReport({
49
+ outputPath: options.reportPath,
50
+ targetPath,
51
+ weightConfig: WEIGHT_CONFIGS[key] || { default: 1 },
52
+ })
46
53
  } catch (error) {
47
54
  console.error("❌ Transform failed:", error)
48
55
  }