@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.
- package/dist/tapestry-react-shim.cjs +2 -1
- package/package.json +2 -2
- package/src/components/button/transforms/convertStyleProps.test.ts +4 -5
- package/src/components/link/index.ts +32 -0
- package/src/components/link/transforms/inlineToKind.test.ts +308 -0
- package/src/components/link/transforms/inlineToKind.ts +51 -0
- package/src/components/link/transforms/targetBlankToExternal.test.ts +191 -0
- package/src/components/link/transforms/targetBlankToExternal.ts +30 -0
- package/src/components/link/transforms/toToHref.test.ts +245 -0
- package/src/components/link/transforms/toToHref.ts +14 -0
- package/src/components/shared/actions/addAttribute.test.ts +108 -0
- package/src/components/shared/actions/addAttribute.ts +14 -0
- package/src/components/shared/actions/removeAttribute.ts +9 -2
- package/src/components/shared/actions/transformElementName.ts +23 -9
- package/src/components/shared/transformFactories/attributeTransformFactory.test.ts +83 -0
- package/src/components/shared/transformFactories/attributeTransformFactory.ts +21 -14
- package/src/components/shared/transformFactories/componentTransformFactory.test.ts +85 -2
- package/src/components/shared/transformFactories/componentTransformFactory.ts +41 -22
- package/src/components/shared/transformFactories/helpers/findJSXElements.ts +37 -0
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +2 -27
- package/src/components/shared/types.ts +19 -1
- package/src/index.ts +7 -2
- package/src/jscodeshiftRunner.ts +7 -0
- package/src/reportGenerator.ts +450 -0
- 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("
|
|
5
|
-
|
|
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
|
|
36
|
-
|
|
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 (!
|
|
47
|
+
if (!sourceComponentLocalName) {
|
|
43
48
|
return null
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
findJSXElements(
|
|
74
|
+
source,
|
|
75
|
+
j,
|
|
76
|
+
config.fromComponent,
|
|
77
|
+
sourceComponentLocalName
|
|
78
|
+
).forEach((path) => {
|
|
79
|
+
const element = path.parent.value
|
|
61
80
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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,
|
|
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}
|
|
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 {
|
|
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
|
-
.
|
|
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}`)
|
package/src/jscodeshiftRunner.ts
CHANGED
|
@@ -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
|
}
|