@planningcenter/tapestry-migration-cli 2.3.0-rc.1 → 2.3.0-rc.11

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 (50) hide show
  1. package/dist/tapestry-react-shim.cjs +5065 -0
  2. package/package.json +9 -5
  3. package/src/components/button/index.ts +48 -4
  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/themeVariantToKind.test.ts +401 -0
  20. package/src/components/button/transforms/themeVariantToKind.ts +90 -0
  21. package/src/components/button/transforms/unsupportedProps.ts +73 -0
  22. package/src/components/shared/actions/addAttribute.test.ts +300 -0
  23. package/src/components/shared/actions/addAttribute.ts +65 -0
  24. package/src/components/shared/actions/addComment.test.ts +1 -1
  25. package/src/components/shared/actions/addCommentToAttribute.test.ts +45 -0
  26. package/src/components/shared/actions/addCommentToAttribute.ts +28 -0
  27. package/src/components/shared/actions/addCommentToUnsupportedProps.ts +29 -0
  28. package/src/components/shared/actions/getAttributeValue.test.ts +261 -0
  29. package/src/components/shared/actions/getAttributeValue.ts +15 -0
  30. package/src/components/shared/actions/getSpreadProps.ts +7 -0
  31. package/src/components/shared/actions/hasSpreadProps.ts +7 -0
  32. package/src/components/shared/actions/removeChildren.ts +7 -0
  33. package/src/components/shared/actions/removeDuplicateKeys.test.ts +280 -0
  34. package/src/components/shared/actions/removeDuplicateKeys.ts +45 -0
  35. package/src/components/shared/actions/removeUnusedImport.test.ts +302 -0
  36. package/src/components/shared/actions/removeUnusedImport.ts +81 -0
  37. package/src/components/shared/actions/transformElementName.test.ts +9 -9
  38. package/src/components/shared/actions/transformElementName.ts +13 -16
  39. package/src/components/shared/conditions/hasChildren.ts +5 -0
  40. package/src/components/shared/getJavaScriptTheme.ts +68 -0
  41. package/src/components/shared/jsThemeLoader.ts +85 -0
  42. package/src/components/shared/transformFactories/attributeCombineFactory.test.ts +374 -0
  43. package/src/components/shared/transformFactories/attributeCombineFactory.ts +300 -0
  44. package/src/components/shared/transformFactories/attributeTransformFactory.ts +14 -6
  45. package/src/components/shared/transformFactories/componentTransformFactory.ts +1 -1
  46. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +362 -0
  47. package/src/index.ts +4 -0
  48. package/src/stubs/stackViewPlugin.ts +33 -0
  49. package/src/stubs/tapestry-stub.ts +16 -0
  50. package/src/tapestry-react-shim.ts +7 -0
@@ -0,0 +1,302 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import { removeUnusedImport } from "./removeUnusedImport"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ describe("removeUnusedImport", () => {
9
+ describe("single component removal", () => {
10
+ it("should remove unused import completely when it's the only specifier", () => {
11
+ const source = j(`
12
+ import { Button } from "@planningcenter/tapestry-react"
13
+
14
+ export function TestComponent() {
15
+ return <div>No Button used</div>
16
+ }
17
+ `)
18
+
19
+ const result = removeUnusedImport({
20
+ componentName: "Button",
21
+ j,
22
+ packageName: "@planningcenter/tapestry-react",
23
+ source,
24
+ })
25
+
26
+ expect(result).toBe(true)
27
+ expect(source.toSource()).not.toContain("import { Button }")
28
+ expect(source.toSource()).not.toContain("@planningcenter/tapestry-react")
29
+ })
30
+
31
+ it("should remove only the unused specifier from multi-component import", () => {
32
+ const source = j(`
33
+ import { Button, Icon, Input } from "@planningcenter/tapestry-react"
34
+
35
+ export function TestComponent() {
36
+ return (
37
+ <div>
38
+ <Icon name="add" />
39
+ <Input placeholder="Enter text" />
40
+ </div>
41
+ )
42
+ }
43
+ `)
44
+
45
+ const result = removeUnusedImport({
46
+ componentName: "Button",
47
+ j,
48
+ packageName: "@planningcenter/tapestry-react",
49
+ source,
50
+ })
51
+
52
+ expect(result).toBe(true)
53
+ const output = source.toSource()
54
+ expect(output).toContain("import { Icon, Input }")
55
+ expect(output).not.toContain("Button")
56
+ expect(output).toContain('<Icon name="add" />')
57
+ expect(output).toContain('<Input placeholder="Enter text" />')
58
+ })
59
+
60
+ it("should not remove import if component is still used in JSX", () => {
61
+ const source = j(`
62
+ import { Button } from "@planningcenter/tapestry-react"
63
+
64
+ export function TestComponent() {
65
+ return <Button kind="primary">Click me</Button>
66
+ }
67
+ `)
68
+
69
+ const result = removeUnusedImport({
70
+ componentName: "Button",
71
+ j,
72
+ packageName: "@planningcenter/tapestry-react",
73
+ source,
74
+ })
75
+
76
+ expect(result).toBe(false)
77
+ expect(source.toSource()).toContain("import { Button }")
78
+ expect(source.toSource()).toContain(
79
+ '<Button kind="primary">Click me</Button>'
80
+ )
81
+ })
82
+
83
+ it("should not remove import if component is used as a variable reference", () => {
84
+ const source = j(`
85
+ import { Button } from "@planningcenter/tapestry-react"
86
+
87
+ export function TestComponent() {
88
+ const MyButton = Button
89
+ return <MyButton>Click me</MyButton>
90
+ }
91
+ `)
92
+
93
+ const result = removeUnusedImport({
94
+ componentName: "Button",
95
+ j,
96
+ packageName: "@planningcenter/tapestry-react",
97
+ source,
98
+ })
99
+
100
+ expect(result).toBe(false)
101
+ expect(source.toSource()).toContain("import { Button }")
102
+ expect(source.toSource()).toContain("const MyButton = Button")
103
+ })
104
+
105
+ it("should handle aliased imports correctly", () => {
106
+ const source = j(`
107
+ import { Button as TapestryButton } from "@planningcenter/tapestry-react"
108
+
109
+ export function TestComponent() {
110
+ return <div>No button used</div>
111
+ }
112
+ `)
113
+
114
+ const result = removeUnusedImport({
115
+ componentName: "Button",
116
+ j,
117
+ packageName: "@planningcenter/tapestry-react",
118
+ source,
119
+ })
120
+
121
+ expect(result).toBe(true)
122
+ expect(source.toSource()).not.toContain(
123
+ "import { Button as TapestryButton }"
124
+ )
125
+ })
126
+
127
+ it("should not remove aliased import if alias is still used", () => {
128
+ const source = j(`
129
+ import { Button as TapestryButton } from "@planningcenter/tapestry-react"
130
+
131
+ export function TestComponent() {
132
+ return <TapestryButton>Click me</TapestryButton>
133
+ }
134
+ `)
135
+
136
+ const result = removeUnusedImport({
137
+ componentName: "Button",
138
+ j,
139
+ packageName: "@planningcenter/tapestry-react",
140
+ source,
141
+ })
142
+
143
+ expect(result).toBe(false)
144
+ expect(source.toSource()).toContain("import { Button as TapestryButton }")
145
+ expect(source.toSource()).toContain(
146
+ "<TapestryButton>Click me</TapestryButton>"
147
+ )
148
+ })
149
+ })
150
+
151
+ describe("edge cases", () => {
152
+ it("should return false when import declaration doesn't exist", () => {
153
+ const source = j(`
154
+ export function TestComponent() {
155
+ return <div>No imports</div>
156
+ }
157
+ `)
158
+
159
+ const result = removeUnusedImport({
160
+ componentName: "Button",
161
+ j,
162
+ packageName: "@planningcenter/tapestry-react",
163
+ source,
164
+ })
165
+
166
+ expect(result).toBe(false)
167
+ })
168
+
169
+ it("should return false when component isn't in the specified package", () => {
170
+ const source = j(`
171
+ import { Button } from "some-other-library"
172
+
173
+ export function TestComponent() {
174
+ return <div>No tapestry button</div>
175
+ }
176
+ `)
177
+
178
+ const result = removeUnusedImport({
179
+ componentName: "Button",
180
+ j,
181
+ packageName: "@planningcenter/tapestry-react",
182
+ source,
183
+ })
184
+
185
+ expect(result).toBe(false)
186
+ expect(source.toSource()).toContain(
187
+ 'import { Button } from "some-other-library"'
188
+ )
189
+ })
190
+
191
+ it("should return false when component isn't in the import specifiers", () => {
192
+ const source = j(`
193
+ import { Icon, Input } from "@planningcenter/tapestry-react"
194
+
195
+ export function TestComponent() {
196
+ return <Icon name="add" />
197
+ }
198
+ `)
199
+
200
+ const result = removeUnusedImport({
201
+ componentName: "Button",
202
+ j,
203
+ packageName: "@planningcenter/tapestry-react",
204
+ source,
205
+ })
206
+
207
+ expect(result).toBe(false)
208
+ expect(source.toSource()).toContain("import { Icon, Input }")
209
+ })
210
+
211
+ it("should handle self-closing JSX elements", () => {
212
+ const source = j(`
213
+ import { Icon } from "@planningcenter/tapestry-react"
214
+
215
+ export function TestComponent() {
216
+ return <Icon name="add" />
217
+ }
218
+ `)
219
+
220
+ const result = removeUnusedImport({
221
+ componentName: "Icon",
222
+ j,
223
+ packageName: "@planningcenter/tapestry-react",
224
+ source,
225
+ })
226
+
227
+ expect(result).toBe(false) // Should not remove because Icon is used
228
+ expect(source.toSource()).toContain("import { Icon }")
229
+ })
230
+
231
+ it("should handle component usage in nested JSX", () => {
232
+ const source = j(`
233
+ import { Button } from "@planningcenter/tapestry-react"
234
+
235
+ export function TestComponent() {
236
+ return (
237
+ <div>
238
+ <section>
239
+ <Button>Nested Button</Button>
240
+ </section>
241
+ </div>
242
+ )
243
+ }
244
+ `)
245
+
246
+ const result = removeUnusedImport({
247
+ componentName: "Button",
248
+ j,
249
+ packageName: "@planningcenter/tapestry-react",
250
+ source,
251
+ })
252
+
253
+ expect(result).toBe(false) // Should not remove because Button is used
254
+ expect(source.toSource()).toContain("import { Button }")
255
+ expect(source.toSource()).toContain("<Button>Nested Button</Button>")
256
+ })
257
+ })
258
+
259
+ describe("complex usage patterns", () => {
260
+ it("should handle component used in function calls", () => {
261
+ const source = j(`
262
+ import { Button } from "@planningcenter/tapestry-react"
263
+
264
+ export function TestComponent() {
265
+ const components = [Button]
266
+ return <div>{components.map(C => <C key="test">Test</C>)}</div>
267
+ }
268
+ `)
269
+
270
+ const result = removeUnusedImport({
271
+ componentName: "Button",
272
+ j,
273
+ packageName: "@planningcenter/tapestry-react",
274
+ source,
275
+ })
276
+
277
+ expect(result).toBe(false) // Should not remove because Button is referenced
278
+ expect(source.toSource()).toContain("import { Button }")
279
+ expect(source.toSource()).toContain("const components = [Button]")
280
+ })
281
+
282
+ it("should handle component used in conditional rendering", () => {
283
+ const source = j(`
284
+ import { Button } from "@planningcenter/tapestry-react"
285
+
286
+ export function TestComponent({ showButton }) {
287
+ return showButton ? <Button>Show</Button> : null
288
+ }
289
+ `)
290
+
291
+ const result = removeUnusedImport({
292
+ componentName: "Button",
293
+ j,
294
+ packageName: "@planningcenter/tapestry-react",
295
+ source,
296
+ })
297
+
298
+ expect(result).toBe(false) // Should not remove because Button is used
299
+ expect(source.toSource()).toContain("import { Button }")
300
+ })
301
+ })
302
+ })
@@ -0,0 +1,81 @@
1
+ import { Collection, JSCodeshift } from "jscodeshift"
2
+
3
+ export function removeUnusedImport({
4
+ componentName,
5
+ packageName,
6
+ j,
7
+ source,
8
+ }: {
9
+ componentName: string
10
+ j: JSCodeshift
11
+ packageName: string
12
+ source: Collection
13
+ }): boolean {
14
+ const importDeclaration = source
15
+ .find(j.ImportDeclaration)
16
+ .filter((path) => path.value.source.value === packageName)
17
+ .at(0)
18
+ if (!importDeclaration.length) return false
19
+
20
+ const importDeclPath = importDeclaration.get()
21
+ const specifiers = importDeclPath.value.specifiers || []
22
+ const targetSpecifier = specifiers.find(
23
+ (spec: { imported: { name: string }; type: string }) => {
24
+ if (spec.type === "ImportSpecifier") {
25
+ return spec.imported.name === componentName
26
+ }
27
+ if (spec.type === "ImportDefaultSpecifier") {
28
+ return componentName === "default"
29
+ }
30
+ return false
31
+ }
32
+ )
33
+ if (!targetSpecifier) return false
34
+
35
+ const localName = targetSpecifier.local?.name || componentName
36
+ const isUsed = checkComponentUsage(localName, j, source)
37
+ if (isUsed) return false
38
+
39
+ const remainingSpecifiers = specifiers.filter(
40
+ (spec: { imported: { name: string }; type: string }) =>
41
+ spec !== targetSpecifier
42
+ )
43
+
44
+ if (remainingSpecifiers.length === 0) {
45
+ importDeclPath.prune()
46
+ } else {
47
+ importDeclPath.value.specifiers = remainingSpecifiers
48
+ }
49
+
50
+ return true
51
+ }
52
+
53
+ function checkComponentUsage(
54
+ localName: string,
55
+ j: JSCodeshift,
56
+ source: Collection
57
+ ): boolean {
58
+ const jsxUsage = source.find(j.JSXElement).filter((path) => {
59
+ const openingName = path.value.openingElement.name
60
+ if (openingName.type === "JSXIdentifier")
61
+ return openingName.name === localName
62
+ return false
63
+ })
64
+
65
+ if (jsxUsage.length > 0) return true
66
+
67
+ const identifierUsage = source.find(j.Identifier).filter((path) => {
68
+ const parent = path.parent?.value
69
+ if (
70
+ parent?.type === "ImportSpecifier" ||
71
+ parent?.type === "ImportDefaultSpecifier" ||
72
+ parent?.type === "JSXOpeningElement" ||
73
+ parent?.type === "JSXClosingElement"
74
+ )
75
+ return false
76
+
77
+ return path.value.name === localName
78
+ })
79
+
80
+ return identifierUsage.length > 0
81
+ }
@@ -9,9 +9,9 @@ describe("transformElementName", () => {
9
9
  it("should transform JSX element name", () => {
10
10
  const code = `<Button>Click me</Button>`
11
11
  const source = j(code)
12
- const elementPath = source.find(j.JSXOpeningElement).at(0)
12
+ const element = source.find(j.JSXElement).at(0).get().value
13
13
 
14
- const result = transformElementName(elementPath.get(), "Link")
14
+ const result = transformElementName({ element, name: "Link" })
15
15
 
16
16
  expect(result).toBe(true)
17
17
  expect(source.toSource()).toContain("<Link>Click me</Link>")
@@ -20,9 +20,9 @@ describe("transformElementName", () => {
20
20
  it("should transform both opening and closing tags", () => {
21
21
  const code = `<Button className="test">Content</Button>`
22
22
  const source = j(code)
23
- const elementPath = source.find(j.JSXOpeningElement).at(0)
23
+ const element = source.find(j.JSXElement).at(0).get().value
24
24
 
25
- const result = transformElementName(elementPath.get(), "Link")
25
+ const result = transformElementName({ element, name: "Link" })
26
26
 
27
27
  expect(result).toBe(true)
28
28
  const output = source.toSource()
@@ -33,9 +33,9 @@ describe("transformElementName", () => {
33
33
  it("should handle self-closing tags", () => {
34
34
  const code = `<Button />`
35
35
  const source = j(code)
36
- const elementPath = source.find(j.JSXOpeningElement).at(0)
36
+ const element = source.find(j.JSXElement).at(0).get().value
37
37
 
38
- const result = transformElementName(elementPath.get(), "Link")
38
+ const result = transformElementName({ element, name: "Link" })
39
39
 
40
40
  expect(result).toBe(true)
41
41
  expect(source.toSource()).toContain("<Link />")
@@ -44,16 +44,16 @@ describe("transformElementName", () => {
44
44
  it("should preserve all attributes", () => {
45
45
  const code = `<Button className="test" onClick={handler} disabled>Content</Button>`
46
46
  const source = j(code)
47
- const elementPath = source.find(j.JSXOpeningElement).at(0)
47
+ const element = source.find(j.JSXElement).at(0).get().value
48
48
 
49
- const result = transformElementName(elementPath.get(), "OtherButton")
49
+ const result = transformElementName({ element, name: "OtherButton" })
50
50
 
51
- expect(result).toBe(true)
52
51
  const output = source.toSource()
53
52
  expect(output).toContain('className="test"')
54
53
  expect(output).toContain("onClick={handler}")
55
54
  expect(output).toContain("disabled")
56
55
  expect(output).toContain("<OtherButton")
57
56
  expect(output).toContain("</OtherButton>")
57
+ expect(result).toBe(true)
58
58
  })
59
59
  })
@@ -1,24 +1,21 @@
1
- import { ASTPath, JSXOpeningElement } from "jscodeshift"
1
+ import { JSXElement } from "jscodeshift"
2
2
 
3
3
  /**
4
4
  * Transforms JSX element names (both opening and closing tags)
5
5
  */
6
- export function transformElementName(
7
- elementPath: ASTPath<JSXOpeningElement>,
8
- newName: string
9
- ): boolean {
10
- if (elementPath.value.name.type === "JSXIdentifier") {
11
- elementPath.value.name.name = newName
6
+ export function transformElementName({
7
+ element,
8
+ name,
9
+ }: {
10
+ element: JSXElement
11
+ name: string
12
+ }): boolean {
13
+ if (element?.openingElement?.name?.type === "JSXIdentifier") {
14
+ element.openingElement.name.name = name
12
15
 
13
- // Update closing tag if it exists
14
- const parent = elementPath.parent
15
- if (
16
- parent &&
17
- parent.value.type === "JSXElement" &&
18
- parent.value.closingElement
19
- ) {
20
- if (parent.value.closingElement.name.type === "JSXIdentifier") {
21
- parent.value.closingElement.name.name = newName
16
+ if (element.closingElement) {
17
+ if (element.closingElement.name.type === "JSXIdentifier") {
18
+ element.closingElement.name.name = name
22
19
  }
23
20
  }
24
21
  return true
@@ -0,0 +1,5 @@
1
+ import { JSXElement } from "jscodeshift"
2
+
3
+ export function hasChildren(element: JSXElement): boolean {
4
+ return !!element.children && element.children.length > 0
5
+ }
@@ -0,0 +1,68 @@
1
+ import { JavaScriptTheme, loadJavaScriptTheme } from "./jsThemeLoader"
2
+
3
+ // Cache to avoid reloading the same theme multiple times during a transform run
4
+ const themeCache = new Map<string, JavaScriptTheme>()
5
+
6
+ /**
7
+ * Get JavaScript theme from transform options
8
+ * This function can be used by individual transforms to access the loaded theme
9
+ */
10
+ export async function getJavaScriptTheme(
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ options: any
13
+ ): Promise<JavaScriptTheme | null> {
14
+ // Check if jsThemePath is provided in options
15
+ const jsThemePath = options.jsThemePath
16
+
17
+ if (!jsThemePath) {
18
+ return null
19
+ }
20
+
21
+ // Check cache first
22
+ if (themeCache.has(jsThemePath)) {
23
+ return themeCache.get(jsThemePath)!
24
+ }
25
+
26
+ try {
27
+ // Load the theme
28
+ const theme = await loadJavaScriptTheme(jsThemePath)
29
+
30
+ // Cache the result
31
+ themeCache.set(jsThemePath, theme)
32
+
33
+ return theme
34
+ } catch (error) {
35
+ console.error(
36
+ `❌ Failed to load JS theme: ${error instanceof Error ? error.message : "Unknown error"}`
37
+ )
38
+ return null
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Clear the theme cache (useful for testing)
44
+ */
45
+ export function clearThemeCache(): void {
46
+ themeCache.clear()
47
+ }
48
+
49
+ /**
50
+ * Helper function to access nested theme values safely
51
+ * Example: getThemeValue(theme, 'colors.primary') returns '#007bff'
52
+ */
53
+ export function getThemeValue(theme: JavaScriptTheme, path: string) {
54
+ return path.split(".").reduce((obj, key) => {
55
+ return obj && typeof obj === "object" ? obj[key] : undefined
56
+ }, theme)
57
+ }
58
+
59
+ /**
60
+ * Example usage in a transform:
61
+ *
62
+ * const theme = await getJavaScriptTheme(options)
63
+ * if (theme) {
64
+ * const primaryColor = getThemeValue(theme, 'colors.primary')
65
+ * const mediumSpacing = getThemeValue(theme, 'spacing.md')
66
+ * // Use theme values for transformations
67
+ * }
68
+ */
@@ -0,0 +1,85 @@
1
+ import { existsSync } from "fs"
2
+ import { isAbsolute, resolve } from "path"
3
+ import { pathToFileURL } from "url"
4
+
5
+ /**
6
+ * Interface for JavaScript theme object - flexible structure
7
+ * This can contain any design tokens: colors, spacing, typography, etc.
8
+ */
9
+ export interface JavaScriptTheme {
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ [key: string]: any
12
+ }
13
+
14
+ /**
15
+ * Load a JavaScript theme file using dynamic imports with proper module resolution
16
+ */
17
+ export async function loadJavaScriptTheme(
18
+ themePath: string,
19
+ basePath?: string
20
+ ): Promise<JavaScriptTheme> {
21
+ // Resolve the absolute path
22
+ let resolvedPath: string
23
+
24
+ if (isAbsolute(themePath)) {
25
+ resolvedPath = themePath
26
+ } else {
27
+ // Resolve relative to basePath (usually the current working directory)
28
+ const base = basePath || process.cwd()
29
+ resolvedPath = resolve(base, themePath)
30
+ }
31
+
32
+ // Check if file exists
33
+ if (!existsSync(resolvedPath)) {
34
+ throw new Error(`JavaScript theme file not found: ${resolvedPath}`)
35
+ }
36
+
37
+ try {
38
+ // Convert file path to file URL for proper dynamic import
39
+ const fileUrl = pathToFileURL(resolvedPath).href
40
+
41
+ // Dynamic import the JavaScript file
42
+ const themeModule = await import(fileUrl)
43
+
44
+ // Handle different export patterns
45
+ let themeObject: JavaScriptTheme
46
+
47
+ if (themeModule.default) {
48
+ // ES6 default export or CommonJS module.exports
49
+ themeObject = themeModule.default
50
+ } else if (themeModule.theme) {
51
+ // Named export 'theme'
52
+ themeObject = themeModule.theme
53
+ } else if (Object.keys(themeModule).length > 0) {
54
+ // Named exports - use the entire module
55
+ themeObject = themeModule
56
+ } else {
57
+ throw new Error(
58
+ 'No theme object found in module. Expected default export, named "theme" export, or named exports.'
59
+ )
60
+ }
61
+
62
+ // Validate that we got an object
63
+ if (typeof themeObject !== "object" || themeObject === null) {
64
+ throw new Error("Theme must export an object")
65
+ }
66
+
67
+ return themeObject
68
+ } catch (error) {
69
+ if (error instanceof Error) {
70
+ throw new Error(`Failed to load JavaScript theme: ${error.message}`)
71
+ }
72
+ throw new Error("Failed to load JavaScript theme: Unknown error")
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Load JavaScript theme synchronously for use in transforms
78
+ * This is a wrapper that handles the async loading in the main transform
79
+ */
80
+ export function loadJavaScriptThemeSync(
81
+ themePath: string,
82
+ basePath?: string
83
+ ): Promise<JavaScriptTheme> {
84
+ return loadJavaScriptTheme(themePath, basePath)
85
+ }