@planningcenter/tapestry-migration-cli 2.3.0-rc.6 → 2.3.0-rc.8

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 (41) hide show
  1. package/dist/tapestry-react-shim.cjs +5064 -0
  2. package/package.json +9 -5
  3. package/src/components/button/index.ts +45 -3
  4. package/src/components/button/transforms/auditSpreadProps.test.ts +352 -0
  5. package/src/components/button/transforms/auditSpreadProps.ts +24 -0
  6. package/src/components/button/transforms/childrenToLabel.test.ts +363 -0
  7. package/src/components/button/transforms/childrenToLabel.ts +84 -0
  8. package/src/components/button/transforms/commentOnVisualKindDifference.ts +24 -0
  9. package/src/components/button/transforms/convertStyleProps.test.ts +464 -0
  10. package/src/components/button/transforms/convertStyleProps.ts +16 -0
  11. package/src/components/button/transforms/iconToIconButton.test.ts +377 -0
  12. package/src/components/button/transforms/iconToIconButton.ts +53 -0
  13. package/src/components/button/transforms/removeAsButton.ts +15 -0
  14. package/src/components/button/transforms/removeDuplicateKeys.test.ts +302 -0
  15. package/src/components/button/transforms/removeDuplicateKeys.ts +8 -0
  16. package/src/components/button/transforms/reviewStyles.ts +17 -0
  17. package/src/components/button/transforms/spinnerToLoadingButton.test.ts +165 -0
  18. package/src/components/button/transforms/spinnerToLoadingButton.ts +14 -0
  19. package/src/components/button/transforms/unsupportedProps.ts +73 -0
  20. package/src/components/shared/actions/addCommentToAttribute.test.ts +45 -0
  21. package/src/components/shared/actions/addCommentToAttribute.ts +28 -0
  22. package/src/components/shared/actions/addCommentToUnsupportedProps.ts +29 -0
  23. package/src/components/shared/actions/getSpreadProps.ts +7 -0
  24. package/src/components/shared/actions/hasSpreadProps.ts +7 -0
  25. package/src/components/shared/actions/removeChildren.ts +7 -0
  26. package/src/components/shared/actions/removeDuplicateKeys.test.ts +280 -0
  27. package/src/components/shared/actions/removeDuplicateKeys.ts +45 -0
  28. package/src/components/shared/actions/removeUnusedImport.test.ts +302 -0
  29. package/src/components/shared/actions/removeUnusedImport.ts +81 -0
  30. package/src/components/shared/actions/transformElementName.test.ts +9 -9
  31. package/src/components/shared/actions/transformElementName.ts +13 -16
  32. package/src/components/shared/conditions/hasChildren.ts +5 -0
  33. package/src/components/shared/getJavaScriptTheme.ts +68 -0
  34. package/src/components/shared/jsThemeLoader.ts +85 -0
  35. package/src/components/shared/transformFactories/attributeTransformFactory.ts +14 -6
  36. package/src/components/shared/transformFactories/componentTransformFactory.ts +1 -1
  37. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +362 -0
  38. package/src/index.ts +4 -0
  39. package/src/stubs/stackViewPlugin.ts +33 -0
  40. package/src/stubs/tapestry-stub.ts +16 -0
  41. package/src/tapestry-react-shim.ts +7 -0
@@ -0,0 +1,362 @@
1
+ import {
2
+ Collection,
3
+ JSCodeshift,
4
+ JSXAttribute,
5
+ JSXElement,
6
+ Options,
7
+ Transform,
8
+ } from "jscodeshift"
9
+
10
+ import {
11
+ defaultTheme,
12
+ splitStyles,
13
+ stylePropNames,
14
+ } from "../../../../dist/tapestry-react-shim.cjs"
15
+ import { addComment } from "../../shared/actions/addComment"
16
+ import { getAttribute } from "../../shared/actions/getAttribute"
17
+ import { removeAttribute } from "../../shared/actions/removeAttribute"
18
+ import { attributeTransformFactory } from "./attributeTransformFactory"
19
+
20
+ type StylePropMapping = Record<
21
+ string,
22
+ {
23
+ defaultMapping: string
24
+ specialValues?: Record<string, string>
25
+ }
26
+ >
27
+
28
+ // Helper function to extract prop value from JSX attribute
29
+ function extractPropValue(attr: JSXAttribute, j: JSCodeshift) {
30
+ if (!attr.value) {
31
+ // Boolean prop like <Button disabled />
32
+ return true
33
+ } else if (attr.value.type === "StringLiteral") {
34
+ // String literal like color="blue"
35
+ return attr.value.value
36
+ } else if (attr.value.type === "JSXExpressionContainer") {
37
+ // Expression like color={someVar} or color={5}
38
+ const expression = attr.value.expression
39
+ if (expression.type === "StringLiteral") {
40
+ return expression.value
41
+ } else if (expression.type === "NumericLiteral") {
42
+ return expression.value
43
+ } else if (expression.type === "BooleanLiteral") {
44
+ return expression.value
45
+ } else if (expression.type === "Identifier") {
46
+ // Variable reference - we'll use the variable name as a placeholder
47
+ return `{${expression.name}}`
48
+ } else {
49
+ // Complex expression - convert back to string representation
50
+ return `{${j(expression).toSource()}}`
51
+ }
52
+ } else {
53
+ // Fallback for other types
54
+ return j(attr.value).toSource()
55
+ }
56
+ }
57
+
58
+ function processKeepStyleProps({
59
+ attributes,
60
+ j,
61
+ element,
62
+ source,
63
+ }: {
64
+ attributes: JSXAttribute[]
65
+ element: JSXElement
66
+ j: JSCodeshift
67
+ source: Collection
68
+ }): {
69
+ directProps: Record<string, unknown>
70
+ themeProps: Record<string, unknown>
71
+ } {
72
+ const themeProps: Record<string, unknown> = {}
73
+ const directProps: Record<string, unknown> = {}
74
+
75
+ attributes.forEach((attr) => {
76
+ const propName = attr.name.name as string
77
+ const propValue = extractPropValue(attr, j)
78
+
79
+ // If it's a complex expression (starts and ends with braces), handle it directly
80
+ if (
81
+ typeof propValue === "string" &&
82
+ propValue.startsWith("{") &&
83
+ propValue.endsWith("}")
84
+ ) {
85
+ directProps[propName] = propValue
86
+ } else {
87
+ themeProps[propName] = propValue
88
+ }
89
+
90
+ removeAttribute(propName, { element, j, source })
91
+ })
92
+
93
+ return { directProps, themeProps }
94
+ }
95
+
96
+ function processRemoveStyleProps({
97
+ j,
98
+ attributes,
99
+ source,
100
+ element,
101
+ }: {
102
+ attributes: JSXAttribute[]
103
+ element: JSXElement
104
+ j: JSCodeshift
105
+ source: Collection
106
+ }) {
107
+ attributes.forEach((attr) => {
108
+ const propName = attr.name.name as string
109
+ removeAttribute(propName, {
110
+ buildComment: (name, value) =>
111
+ `${name} has been removed as this is covered by default styling: ${name}: ${value}`,
112
+ element,
113
+ j,
114
+ source,
115
+ })
116
+ })
117
+ }
118
+
119
+ // Process props that need style property mappings
120
+ function processStylePropMappings({
121
+ attributes,
122
+ j,
123
+ element,
124
+ source,
125
+ stylePropMapping,
126
+ }: {
127
+ attributes: JSXAttribute[]
128
+ element: JSXElement
129
+ j: JSCodeshift
130
+ source: Collection
131
+ stylePropMapping: StylePropMapping
132
+ }): Record<string, unknown> {
133
+ const styleProps: Record<string, unknown> = {}
134
+
135
+ attributes.forEach((attr) => {
136
+ const propName = attr.name.name as string
137
+ if (propName in stylePropMapping) {
138
+ const propValue = extractPropValue(attr, j)
139
+ const mappingConfig =
140
+ stylePropMapping[propName as keyof typeof stylePropMapping]
141
+
142
+ if (
143
+ mappingConfig.specialValues &&
144
+ typeof propValue === "string" &&
145
+ propValue in mappingConfig.specialValues
146
+ ) {
147
+ const specialAction =
148
+ mappingConfig.specialValues[
149
+ propValue as keyof typeof mappingConfig.specialValues
150
+ ]
151
+
152
+ if (specialAction === "AUDIT") {
153
+ const text = `${propName}="${propValue}" needs manual review for replacement`
154
+ addComment({ element, j, scope: "styleProp", source, text })
155
+ }
156
+ // Future: could support other special actions here
157
+ } else {
158
+ const mappedPropName = mappingConfig.defaultMapping
159
+ styleProps[mappedPropName] = propValue
160
+ }
161
+
162
+ removeAttribute(propName, { element, j, source })
163
+ }
164
+ })
165
+
166
+ return styleProps
167
+ }
168
+
169
+ function applyStylesToComponent({
170
+ j,
171
+ element,
172
+ styles,
173
+ }: {
174
+ element: JSXElement
175
+ j: JSCodeshift
176
+ styles: Record<string, unknown>
177
+ }) {
178
+ const styleAttr = getAttribute({ element, name: "style" })
179
+ const styleValue = j.jsxExpressionContainer(
180
+ j.objectExpression(
181
+ Object.entries(styles).map(([key, value]) => {
182
+ let valueNode
183
+ if (
184
+ typeof value === "string" &&
185
+ value.startsWith("{") &&
186
+ value.endsWith("}")
187
+ ) {
188
+ // This is a complex expression wrapped in braces - parse it as JS
189
+ const expressionCode = value.slice(1, -1) // Remove surrounding braces
190
+ try {
191
+ const parsed = j(expressionCode)
192
+ // Get the first expression from the program body
193
+ const firstStatement = parsed.find(j.Program).get("body", 0).value
194
+ if (firstStatement?.type === "ExpressionStatement") {
195
+ valueNode = firstStatement.expression
196
+ } else {
197
+ valueNode = j.stringLiteral(value)
198
+ }
199
+ } catch {
200
+ // If parsing fails, fall back to string literal
201
+ valueNode = j.stringLiteral(value)
202
+ }
203
+ } else if (typeof value === "string") {
204
+ valueNode = j.stringLiteral(value)
205
+ } else {
206
+ valueNode = j.literal(value as string | number | boolean | null)
207
+ }
208
+
209
+ return j.objectProperty(j.identifier(key), valueNode)
210
+ })
211
+ )
212
+ )
213
+
214
+ if (styleAttr && styleAttr.type === "JSXAttribute") {
215
+ // TODO: ensure this adds to exisiting style object rather than replacing
216
+ styleAttr.value = styleValue
217
+ } else {
218
+ const styleAttr = j.jsxAttribute(j.jsxIdentifier("style"), styleValue)
219
+
220
+ element.openingElement.attributes = element.openingElement.attributes || []
221
+ element.openingElement.attributes.push(styleAttr)
222
+ }
223
+ }
224
+
225
+ export function stylePropTransformFactory(config: {
226
+ plugin?: {
227
+ getStyles: (props: Record<string, unknown>) => Record<string, unknown>
228
+ styleProps: string[]
229
+ }
230
+ stylePropMapping?: StylePropMapping
231
+ stylesToKeep?: string[]
232
+ stylesToRemove: string[]
233
+ targetComponent: string
234
+ targetPackage: string
235
+ }): Transform {
236
+ const {
237
+ stylePropMapping = {} as StylePropMapping,
238
+ stylesToKeep = [],
239
+ stylesToRemove,
240
+ } = config
241
+ const transform: (
242
+ element: JSXElement,
243
+ resources: { j: JSCodeshift; options: Options; source: Collection }
244
+ ) => boolean = (element, { j, options, source }) => {
245
+ const allAttributes = element.openingElement.attributes || []
246
+ let allStyleProps: Record<string, unknown> = {}
247
+ let directStyleProps: Record<string, unknown> = {}
248
+ const attributes = allAttributes.filter((attr) => {
249
+ if (attr.type !== "JSXAttribute") return false
250
+ const name = attr.name?.name as string
251
+ return (
252
+ name &&
253
+ (stylePropNames.includes(name) ||
254
+ name in stylePropMapping ||
255
+ stylesToKeep.includes(name) ||
256
+ stylesToRemove.includes(name) ||
257
+ (config.plugin?.styleProps.includes(name) ?? false))
258
+ )
259
+ }) as JSXAttribute[]
260
+
261
+ const keepAttributes = attributes.filter(
262
+ (attr) =>
263
+ !stylesToRemove.includes(attr.name.name as string) &&
264
+ !((attr.name.name as string) in stylePropMapping)
265
+ )
266
+ const keepResult = processKeepStyleProps({
267
+ attributes: keepAttributes,
268
+ element,
269
+ j,
270
+ source,
271
+ })
272
+ allStyleProps = {
273
+ ...allStyleProps,
274
+ ...keepResult.themeProps,
275
+ }
276
+ directStyleProps = {
277
+ ...directStyleProps,
278
+ ...keepResult.directProps,
279
+ }
280
+
281
+ allStyleProps = {
282
+ ...allStyleProps,
283
+ ...processStylePropMappings({
284
+ attributes,
285
+ element,
286
+ j,
287
+ source,
288
+ stylePropMapping,
289
+ }),
290
+ }
291
+
292
+ const removeAttributes = attributes.filter((attr) =>
293
+ stylesToRemove.includes(attr.name.name as string)
294
+ )
295
+ processRemoveStyleProps({
296
+ attributes: removeAttributes,
297
+ element,
298
+ j,
299
+ source,
300
+ })
301
+
302
+ // Convert collected style props to CSS if any exist
303
+ if (
304
+ Object.keys(allStyleProps).length > 0 ||
305
+ Object.keys(directStyleProps).length > 0
306
+ ) {
307
+ try {
308
+ let styles: Record<string, unknown> = {}
309
+
310
+ // Process theme props through the theme system
311
+ if (Object.keys(allStyleProps).length > 0) {
312
+ const result = splitStyles({
313
+ ...allStyleProps,
314
+ plugin: config.plugin,
315
+ })
316
+ const cssObject = result.css(defaultTheme)
317
+ styles = { ...styles, ...(cssObject[0] || {}) }
318
+ }
319
+
320
+ // Add direct props without theme processing
321
+ styles = { ...styles, ...directStyleProps }
322
+ if (options.verbose) console.log("Final generated styles:", styles)
323
+
324
+ // Apply the styles to the component
325
+ applyStylesToComponent({ element, j, styles })
326
+ } catch (error) {
327
+ console.log("Error processing style props:", error)
328
+ console.log("Style props that caused error:", allStyleProps)
329
+ }
330
+ }
331
+
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
+ return attributes.length + removeAttributes.length > 0
355
+ }
356
+
357
+ return attributeTransformFactory({
358
+ targetComponent: config.targetComponent,
359
+ targetPackage: config.targetPackage,
360
+ transform,
361
+ })
362
+ }
package/src/index.ts CHANGED
@@ -19,6 +19,10 @@ program
19
19
  .option("-f, --fix", "Write the changes")
20
20
  .option("-p, --path <path>", "The path to the folder/file to migrate")
21
21
  .option("-v, --verbose", "Verbose output")
22
+ .option(
23
+ "-j, --js-theme <path>",
24
+ "Path to JavaScript theme file containing design tokens"
25
+ )
22
26
  .action((componentName, options) => {
23
27
  console.log("Hello from Tapestry Migration CLI! 🎨")
24
28
  console.log(`Component: ${componentName}`)
@@ -0,0 +1,33 @@
1
+ // copied from packages/tapestry-react/src/StackView/StackView.tsx in tapestry-react
2
+
3
+ function spacingValue(value: string | number): string {
4
+ return typeof value === "number" ? value * 8 + "px" : value
5
+ }
6
+
7
+ export const stackViewPlugin = {
8
+ getStyles({
9
+ inline,
10
+ alignment,
11
+ axis,
12
+ distribution,
13
+ spacing,
14
+ ...styles
15
+ }: Record<string, unknown>) {
16
+ if (axis && styles.flexDirection === undefined) {
17
+ styles.flexDirection = axis === "horizontal" ? "row" : "column"
18
+ }
19
+ if (alignment && styles.alignItems === undefined) {
20
+ styles.alignItems = alignment
21
+ }
22
+ if (distribution === "fill") {
23
+ styles["& > *"] = { flex: "1 0 0px" }
24
+ } else if (styles.justifyContent === undefined) {
25
+ styles.justifyContent = distribution
26
+ }
27
+ if (typeof spacing === "number" || typeof spacing === "string") {
28
+ styles.gap = spacingValue(spacing)
29
+ }
30
+ return styles
31
+ },
32
+ styleProps: ["alignment", "axis", "distribution", "spacing"],
33
+ }
@@ -0,0 +1,16 @@
1
+ export { Button } from "../../../../node_modules/@planningcenter/tapestry/dist/components/button/Button"
2
+ export { DropdownButton } from "../../../../node_modules/@planningcenter/tapestry/dist/components/button/DropdownButton"
3
+ export { DropdownIconButton } from "../../../../node_modules/@planningcenter/tapestry/dist/components/button/DropdownIconButton"
4
+ export { IconButton } from "../../../../node_modules/@planningcenter/tapestry/dist/components/button/IconButton"
5
+ export { LoadingButton } from "../../../../node_modules/@planningcenter/tapestry/dist/components/button/LoadingButton"
6
+ export { PageHeaderActionsDropdownButton } from "../../../../node_modules/@planningcenter/tapestry/dist/components/button/PageHeaderActionsDropdownButton"
7
+ export { IconLink } from "../../../../node_modules/@planningcenter/tapestry/dist/components/link/IconLink"
8
+ export { Link } from "../../../../node_modules/@planningcenter/tapestry/dist/components/link/Link"
9
+ export { PageHeader } from "../../../../node_modules/@planningcenter/tapestry/dist/components/page-header/PageHeader"
10
+ export { Sidenav } from "../../../../node_modules/@planningcenter/tapestry/dist/components/sidenav/Sidenav"
11
+ export { SidenavItem } from "../../../../node_modules/@planningcenter/tapestry/dist/components/sidenav/SidenavItem"
12
+ export { SidenavSection } from "../../../../node_modules/@planningcenter/tapestry/dist/components/sidenav/SidenavSection"
13
+ export {
14
+ computedToken,
15
+ token,
16
+ } from "../../../../node_modules/@planningcenter/tapestry/dist/jsTokens"
@@ -0,0 +1,7 @@
1
+ // Import the real utilities from tapestry-react (Stencil parts will be stubbed by Vite aliases)
2
+ // Use absolute file paths since the package doesn't export subpaths
3
+ import defaultTheme from "../../../node_modules/@planningcenter/tapestry-react/dist/system/default-theme.js"
4
+ import splitStyles from "../../../node_modules/@planningcenter/tapestry-react/dist/system/split-styles.js"
5
+ import { stylePropNames } from "../../../node_modules/@planningcenter/tapestry-react/dist/system/style-names.js"
6
+
7
+ export { defaultTheme, splitStyles, stylePropNames }