@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.
- package/dist/tapestry-react-shim.cjs +5065 -0
- package/package.json +9 -5
- package/src/components/button/index.ts +48 -4
- package/src/components/button/transforms/auditSpreadProps.test.ts +352 -0
- package/src/components/button/transforms/auditSpreadProps.ts +24 -0
- package/src/components/button/transforms/childrenToLabel.test.ts +363 -0
- package/src/components/button/transforms/childrenToLabel.ts +84 -0
- package/src/components/button/transforms/commentOnVisualKindDifference.ts +24 -0
- package/src/components/button/transforms/convertStyleProps.test.ts +464 -0
- package/src/components/button/transforms/convertStyleProps.ts +16 -0
- package/src/components/button/transforms/iconToIconButton.test.ts +377 -0
- package/src/components/button/transforms/iconToIconButton.ts +53 -0
- package/src/components/button/transforms/removeAsButton.ts +15 -0
- package/src/components/button/transforms/removeDuplicateKeys.test.ts +302 -0
- package/src/components/button/transforms/removeDuplicateKeys.ts +8 -0
- package/src/components/button/transforms/reviewStyles.ts +17 -0
- package/src/components/button/transforms/spinnerToLoadingButton.test.ts +165 -0
- package/src/components/button/transforms/spinnerToLoadingButton.ts +14 -0
- package/src/components/button/transforms/themeVariantToKind.test.ts +401 -0
- package/src/components/button/transforms/themeVariantToKind.ts +90 -0
- package/src/components/button/transforms/unsupportedProps.ts +73 -0
- package/src/components/shared/actions/addAttribute.test.ts +300 -0
- package/src/components/shared/actions/addAttribute.ts +65 -0
- package/src/components/shared/actions/addComment.test.ts +1 -1
- package/src/components/shared/actions/addCommentToAttribute.test.ts +45 -0
- package/src/components/shared/actions/addCommentToAttribute.ts +28 -0
- package/src/components/shared/actions/addCommentToUnsupportedProps.ts +29 -0
- package/src/components/shared/actions/getAttributeValue.test.ts +261 -0
- package/src/components/shared/actions/getAttributeValue.ts +15 -0
- package/src/components/shared/actions/getSpreadProps.ts +7 -0
- package/src/components/shared/actions/hasSpreadProps.ts +7 -0
- package/src/components/shared/actions/removeChildren.ts +7 -0
- package/src/components/shared/actions/removeDuplicateKeys.test.ts +280 -0
- package/src/components/shared/actions/removeDuplicateKeys.ts +45 -0
- package/src/components/shared/actions/removeUnusedImport.test.ts +302 -0
- package/src/components/shared/actions/removeUnusedImport.ts +81 -0
- package/src/components/shared/actions/transformElementName.test.ts +9 -9
- package/src/components/shared/actions/transformElementName.ts +13 -16
- package/src/components/shared/conditions/hasChildren.ts +5 -0
- package/src/components/shared/getJavaScriptTheme.ts +68 -0
- package/src/components/shared/jsThemeLoader.ts +85 -0
- package/src/components/shared/transformFactories/attributeCombineFactory.test.ts +374 -0
- package/src/components/shared/transformFactories/attributeCombineFactory.ts +300 -0
- package/src/components/shared/transformFactories/attributeTransformFactory.ts +14 -6
- package/src/components/shared/transformFactories/componentTransformFactory.ts +1 -1
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +362 -0
- package/src/index.ts +4 -0
- package/src/stubs/stackViewPlugin.ts +33 -0
- package/src/stubs/tapestry-stub.ts +16 -0
- 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
|
|
12
|
+
const element = source.find(j.JSXElement).at(0).get().value
|
|
13
13
|
|
|
14
|
-
const result = transformElementName(
|
|
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
|
|
23
|
+
const element = source.find(j.JSXElement).at(0).get().value
|
|
24
24
|
|
|
25
|
-
const result = transformElementName(
|
|
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
|
|
36
|
+
const element = source.find(j.JSXElement).at(0).get().value
|
|
37
37
|
|
|
38
|
-
const result = transformElementName(
|
|
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
|
|
47
|
+
const element = source.find(j.JSXElement).at(0).get().value
|
|
48
48
|
|
|
49
|
-
const result = transformElementName(
|
|
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 {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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,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
|
+
}
|