@reapit/elements 5.0.0-beta.70 → 5.0.0-beta.72

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 (34) hide show
  1. package/bin/elements.cjs +69 -0
  2. package/dist/codemods/at-a-glance-article-card/transform.d.ts +4 -0
  3. package/dist/codemods/at-a-glance-article-card/transform.d.ts.map +1 -0
  4. package/dist/codemods/at-a-glance-article-card/transform.js +223 -0
  5. package/dist/codemods/at-a-glance-article-card/transform.js.map +1 -0
  6. package/dist/codemods/bin.d.ts +2 -0
  7. package/dist/codemods/bin.d.ts.map +1 -0
  8. package/dist/codemods/bin.js +140 -0
  9. package/dist/codemods/bin.js.map +1 -0
  10. package/dist/codemods/codemods.d.ts +29 -0
  11. package/dist/codemods/codemods.d.ts.map +1 -0
  12. package/dist/codemods/codemods.js +32 -0
  13. package/dist/codemods/codemods.js.map +1 -0
  14. package/{codemods → dist/codemods}/manifest.json +1 -1
  15. package/dist/codemods/runner.d.ts +21 -0
  16. package/dist/codemods/runner.d.ts.map +1 -0
  17. package/dist/codemods/runner.js +165 -0
  18. package/dist/codemods/runner.js.map +1 -0
  19. package/package.json +8 -9
  20. package/codemods/__tests__/codemods.test.ts +0 -178
  21. package/codemods/__tests__/generate-manifest.test.ts +0 -240
  22. package/codemods/__tests__/readme-parser.test.ts +0 -218
  23. package/codemods/__tests__/runner.test.ts +0 -530
  24. package/codemods/at-a-glance-article-card/README.md +0 -122
  25. package/codemods/at-a-glance-article-card/__tests__/transform.test.ts +0 -390
  26. package/codemods/at-a-glance-article-card/transform.ts +0 -291
  27. package/codemods/bin.cjs +0 -13
  28. package/codemods/bin.ts +0 -205
  29. package/codemods/codemods.ts +0 -75
  30. package/codemods/generate-manifest.ts +0 -120
  31. package/codemods/manifest.schema.json +0 -39
  32. package/codemods/readme-parser.ts +0 -37
  33. package/codemods/runner.ts +0 -196
  34. package/scripts/cdk/bin/rpt-cdk.cjs +0 -10
@@ -1,390 +0,0 @@
1
- import transform from '../transform'
2
-
3
- describe('namespaced component usage (AtAGlance.Card)', () => {
4
- test('renames AtAGlance.Card to AtAGlance.ArticleCard when using old props-based API', () => {
5
- const input = '<AtAGlance.Card displayValue="42" label="Total" />'
6
- const output = transform(input)
7
- expect(output).toBe('<AtAGlance.ArticleCard displayValue="42" label="Total" />')
8
- })
9
-
10
- test('preserves all props when renaming AtAGlance.Card to AtAGlance.ArticleCard', () => {
11
- const input = '<AtAGlance.Card displayValue="42" label="Total" description="Last 30 days" layout="horizontal" />'
12
- const output = transform(input)
13
- expect(output).toBe(
14
- '<AtAGlance.ArticleCard displayValue="42" label="Total" description="Last 30 days" layout="horizontal" />',
15
- )
16
- })
17
-
18
- test('does not rename AtAGlance.Card when using grid prop', () => {
19
- const input =
20
- '<AtAGlance.Card grid="auto / 1fr 1fr"><AtAGlance.CardLabel>Label</AtAGlance.CardLabel></AtAGlance.Card>'
21
- const output = transform(input)
22
- expect(output).toBe(input)
23
- })
24
-
25
- test('does not rename AtAGlance.Card when it has children', () => {
26
- const input = '<AtAGlance.Card><AtAGlance.CardLabel>Label</AtAGlance.CardLabel></AtAGlance.Card>'
27
- const output = transform(input)
28
- expect(output).toBe(input)
29
- })
30
-
31
- test('does not modify AtAGlance.AnchorCard', () => {
32
- const input = '<AtAGlance.AnchorCard href="/dashboard" displayValue="42" label="Total" />'
33
- const output = transform(input)
34
- expect(output).toBe(input)
35
- })
36
-
37
- test('does not modify AtAGlance.ButtonCard', () => {
38
- const input = '<AtAGlance.ButtonCard onClick={handleClick} displayValue="42" label="Total" />'
39
- const output = transform(input)
40
- expect(output).toBe(input)
41
- })
42
-
43
- test('does not modify AtAGlance.ArticleCard', () => {
44
- const input = '<AtAGlance.ArticleCard displayValue="42" label="Total" />'
45
- const output = transform(input)
46
- expect(output).toBe(input)
47
- })
48
-
49
- test('renames multiple AtAGlance.Card instances', () => {
50
- const input =
51
- '<><AtAGlance.Card displayValue="10" label="First" /><AtAGlance.Card displayValue="20" label="Second" /></>'
52
- const output = transform(input)
53
- expect(output).toBe(
54
- '<><AtAGlance.ArticleCard displayValue="10" label="First" /><AtAGlance.ArticleCard displayValue="20" label="Second" /></>',
55
- )
56
- })
57
-
58
- test('renames old API usage while preserving new API usage in the same file', () => {
59
- const input =
60
- '<><AtAGlance.Card displayValue="42" label="Old" /><AtAGlance.Card grid="auto"><span>New</span></AtAGlance.Card></>'
61
- const output = transform(input)
62
- expect(output).toBe(
63
- '<><AtAGlance.ArticleCard displayValue="42" label="Old" /><AtAGlance.Card grid="auto"><span>New</span></AtAGlance.Card></>',
64
- )
65
- })
66
-
67
- test('does not modify non-AtAGlance Card components', () => {
68
- const input = '<><AtAGlance.Card displayValue="42" label="Total" /><Card title="Other">Content</Card></>'
69
- const output = transform(input)
70
- expect(output).toContain('<Card title="Other">Content</Card>')
71
- })
72
- })
73
-
74
- describe('direct component usage (AtAGlanceCard)', () => {
75
- test('converts AtAGlanceCard to AtAGlance.ArticleCard when using old props-based API', () => {
76
- const input = '<AtAGlanceCard displayValue="42" label="Total" />'
77
- const output = transform(input)
78
- expect(output).toBe('<AtAGlance.ArticleCard displayValue="42" label="Total" />')
79
- })
80
-
81
- test('does not rename AtAGlanceCard when using grid prop', () => {
82
- const input = '<AtAGlanceCard grid="auto / 1fr 1fr"><AtAGlanceCardLabel>Label</AtAGlanceCardLabel></AtAGlanceCard>'
83
- const output = transform(input)
84
- expect(output).toBe(input)
85
- })
86
-
87
- test('does not rename AtAGlanceCard when it has children', () => {
88
- const input = '<AtAGlanceCard><AtAGlanceCardLabel>Label</AtAGlanceCardLabel></AtAGlanceCard>'
89
- const output = transform(input)
90
- expect(output).toBe(input)
91
- })
92
-
93
- test('does not modify AtAGlanceAnchorCard', () => {
94
- const input = '<AtAGlanceAnchorCard href="/dashboard" displayValue="42" label="Total" />'
95
- const output = transform(input)
96
- expect(output).toBe(input)
97
- })
98
-
99
- test('does not modify AtAGlanceButtonCard', () => {
100
- const input = '<AtAGlanceButtonCard onClick={handleClick} displayValue="42" label="Total" />'
101
- const output = transform(input)
102
- expect(output).toBe(input)
103
- })
104
-
105
- test('converts both namespaced and direct usage to AtAGlance.ArticleCard', () => {
106
- const input =
107
- '<><AtAGlance.Card displayValue="42" label="Namespaced" /><AtAGlanceCard displayValue="100" label="Direct" /></>'
108
- const output = transform(input)
109
- expect(output).toBe(
110
- '<><AtAGlance.ArticleCard displayValue="42" label="Namespaced" /><AtAGlance.ArticleCard displayValue="100" label="Direct" /></>',
111
- )
112
- })
113
- })
114
-
115
- describe('import handling', () => {
116
- test('removes AtAGlanceCard from imports when no longer used', () => {
117
- const input = `import { AtAGlanceCard } from '@reapit/elements'
118
-
119
- function Component() {
120
- return <AtAGlanceCard displayValue="42" label="Total" />
121
- }`
122
- const output = transform(input)
123
- expect(output).not.toContain('AtAGlanceCard')
124
- expect(output).toContain('<AtAGlance.ArticleCard')
125
- })
126
-
127
- test('keeps AtAGlanceCard import when still used with new primitive API', () => {
128
- const input = `import { AtAGlanceCard } from '@reapit/elements'
129
-
130
- function Component() {
131
- return <AtAGlanceCard grid="auto"><span>Content</span></AtAGlanceCard>
132
- }`
133
- const output = transform(input)
134
- expect(output).toContain('import { AtAGlanceCard }')
135
- })
136
-
137
- test('removes only AtAGlanceCard from imports while keeping other imports', () => {
138
- const input = `import { AtAGlanceCard, AtAGlance } from '@reapit/elements'
139
-
140
- function Component() {
141
- return <AtAGlanceCard displayValue="42" label="Total" />
142
- }`
143
- const output = transform(input)
144
- expect(output).toContain('AtAGlance')
145
- expect(output).not.toMatch(/\bAtAGlanceCard\b/)
146
- })
147
-
148
- test('adds AtAGlance import when converting AtAGlanceCard and no AtAGlance import exists', () => {
149
- const input = `import { AtAGlanceCard } from '@reapit/elements'
150
-
151
- function Component() {
152
- return <AtAGlanceCard displayValue="42" label="Total" />
153
- }`
154
- const output = transform(input)
155
- expect(output).toContain('import { AtAGlance }')
156
- expect(output).not.toContain('AtAGlanceCard')
157
- })
158
-
159
- test('removes entire import declaration when no named imports remain', () => {
160
- const input = `import { AtAGlanceCard } from '@reapit/elements'
161
-
162
- function Component() {
163
- return <AtAGlanceCard displayValue="42" label="Total" />
164
- }`
165
- const output = transform(input)
166
- expect(output).not.toContain('import { }')
167
- expect(output).toContain('import { AtAGlance }')
168
- })
169
-
170
- test('handles subpath imports correctly', () => {
171
- const input = `import { AtAGlance } from '@reapit/elements/core/at-a-glance'
172
-
173
- function Component() {
174
- return <AtAGlance.Card displayValue="42" label="Total" />
175
- }`
176
- const output = transform(input)
177
- expect(output).toContain("import { AtAGlance } from '@reapit/elements/core/at-a-glance'")
178
- expect(output).toContain('<AtAGlance.ArticleCard')
179
- })
180
-
181
- test('handles aliased AtAGlanceCard imports', () => {
182
- const input = `import { AtAGlanceCard as Card } from '@reapit/elements'
183
-
184
- function Component() {
185
- return <Card displayValue="42" label="Total" />
186
- }`
187
- const output = transform(input)
188
- expect(output).toContain('<AtAGlance.ArticleCard')
189
- expect(output).not.toContain('<Card')
190
- expect(output).toContain('import { AtAGlance }')
191
- })
192
-
193
- test('does not add duplicate AtAGlance imports when multiple import declarations exist', () => {
194
- const input = `import { Button } from '@reapit/elements'
195
- import { AtAGlanceCard } from '@reapit/elements'
196
-
197
- function Component() {
198
- return <AtAGlanceCard displayValue="42" label="Total" />
199
- }`
200
- const output = transform(input)
201
- const atAGlanceMatches = output.match(/\bAtAGlance\b/g) || []
202
- // Should have AtAGlance in import (once) and in JSX (once)
203
- expect(atAGlanceMatches.length).toBe(2)
204
- })
205
- })
206
-
207
- describe('facade package support', () => {
208
- test('transforms components imported from a facade package', () => {
209
- const input = `import { AtAGlanceCard } from '@company/ui-components'
210
-
211
- function Component() {
212
- return <AtAGlanceCard displayValue="42" label="Total" />
213
- }`
214
- const output = transform(input, 'file.tsx', {
215
- facadePackage: '@company/ui-components',
216
- })
217
- expect(output).toContain('import { AtAGlance }')
218
- expect(output).toContain('<AtAGlance.ArticleCard')
219
- expect(output).not.toContain('AtAGlanceCard')
220
- })
221
-
222
- test('removes AtAGlanceCard import and adds AtAGlance import for facade packages', () => {
223
- const input = `import { AtAGlanceCard } from '@company/ui-components'
224
-
225
- function Component() {
226
- return <AtAGlanceCard displayValue="42" label="Total" />
227
- }`
228
- const output = transform(input, 'file.tsx', {
229
- facadePackage: '@company/ui-components',
230
- })
231
- expect(output).toContain("import { AtAGlance } from '@company/ui-components'")
232
- expect(output).not.toContain('AtAGlanceCard')
233
- })
234
-
235
- test('does not transform non-facade packages', () => {
236
- const input = `import { AtAGlanceCard } from '@other/package'
237
-
238
- function Component() {
239
- return <AtAGlanceCard displayValue="42" label="Total" />
240
- }`
241
- const output = transform(input, 'file.tsx', {
242
- facadePackage: '@company/ui-components',
243
- })
244
- expect(output).toBe(input) // No transformation
245
- })
246
-
247
- test('works without facade package parameter (backward compatible)', () => {
248
- const input = `import { AtAGlanceCard } from '@reapit/elements'
249
-
250
- function Component() {
251
- return <AtAGlanceCard displayValue="42" label="Total" />
252
- }`
253
- const output = transform(input)
254
- expect(output).toContain('<AtAGlance.ArticleCard')
255
- })
256
-
257
- test('handles facade package alongside direct @reapit/elements imports', () => {
258
- const input = `import { Button } from '@reapit/elements'
259
- import { AtAGlanceCard } from '@company/ui-components'
260
-
261
- function Component() {
262
- return <AtAGlanceCard displayValue="42" label="Total" />
263
- }`
264
- const output = transform(input, 'file.tsx', {
265
- facadePackage: '@company/ui-components',
266
- })
267
- expect(output).toContain("import { AtAGlance } from '@company/ui-components'")
268
- expect(output).toContain('<AtAGlance.ArticleCard')
269
- expect(output).toContain("import { Button } from '@reapit/elements'")
270
- })
271
-
272
- test('transforms namespaced AtAGlance.Card with facade package', () => {
273
- const input = `import { AtAGlance } from '@company/ui-components'
274
-
275
- function Component() {
276
- return <AtAGlance.Card displayValue="42" label="Total" />
277
- }`
278
- const output = transform(input, 'file.tsx', {
279
- facadePackage: '@company/ui-components',
280
- })
281
- expect(output).toContain('<AtAGlance.ArticleCard')
282
- expect(output).toContain("import { AtAGlance } from '@company/ui-components'")
283
- })
284
-
285
- test('handles aliased imports from facade packages', () => {
286
- const input = `import { AtAGlanceCard as Card } from '@company/ui-components'
287
-
288
- function Component() {
289
- return <Card displayValue="42" label="Total" />
290
- }`
291
- const output = transform(input, 'file.tsx', {
292
- facadePackage: '@company/ui-components',
293
- })
294
- expect(output).toContain('<AtAGlance.ArticleCard')
295
- expect(output).not.toContain('<Card')
296
- expect(output).toContain('import { AtAGlance }')
297
- })
298
-
299
- test('does not transform AtAGlanceCard with new API from facade package', () => {
300
- const input = `import { AtAGlanceCard } from '@company/ui-components'
301
-
302
- function Component() {
303
- return <AtAGlanceCard grid="auto"><span>Content</span></AtAGlanceCard>
304
- }`
305
- const output = transform(input, 'file.tsx', {
306
- facadePackage: '@company/ui-components',
307
- })
308
- expect(output).toBe(input) // No transformation for new API
309
- })
310
-
311
- test('transforms multiple uses of AtAGlanceCard from facade package', () => {
312
- const input = `import { AtAGlanceCard } from '@company/ui-components'
313
-
314
- function Component() {
315
- return (
316
- <>
317
- <AtAGlanceCard displayValue="10" label="First" />
318
- <AtAGlanceCard displayValue="20" label="Second" />
319
- </>
320
- )
321
- }`
322
- const output = transform(input, 'file.tsx', {
323
- facadePackage: '@company/ui-components',
324
- })
325
- expect(output).toContain('<AtAGlance.ArticleCard displayValue="10" label="First" />')
326
- expect(output).toContain('<AtAGlance.ArticleCard displayValue="20" label="Second" />')
327
- expect(output).not.toContain('AtAGlanceCard')
328
- })
329
-
330
- test('transforms imports from facade package subpaths using prefix matching', () => {
331
- const input = `import { AtAGlanceCard } from '@company/design-system/elements'
332
-
333
- function Component() {
334
- return <AtAGlanceCard displayValue="42" label="Total" />
335
- }`
336
- const output = transform(input, 'file.tsx', {
337
- facadePackage: '@company/design-system',
338
- })
339
- expect(output).toContain("import { AtAGlance } from '@company/design-system/elements'")
340
- expect(output).toContain('<AtAGlance.ArticleCard')
341
- expect(output).not.toContain('AtAGlanceCard')
342
- })
343
-
344
- test('transforms imports from multiple subpaths of the same facade package', () => {
345
- const input = `import { Button } from '@company/design-system/core'
346
- import { AtAGlanceCard } from '@company/design-system/elements'
347
-
348
- function Component() {
349
- return (
350
- <>
351
- <Button>Click</Button>
352
- <AtAGlanceCard displayValue="42" label="Total" />
353
- </>
354
- )
355
- }`
356
- const output = transform(input, 'file.tsx', {
357
- facadePackage: '@company/design-system',
358
- })
359
- expect(output).toContain("import { AtAGlance } from '@company/design-system/elements'")
360
- expect(output).toContain('<AtAGlance.ArticleCard')
361
- expect(output).toContain("import { Button } from '@company/design-system/core'")
362
- expect(output).not.toContain('AtAGlanceCard')
363
- })
364
-
365
- test('does not transform packages that start with similar prefix but are different', () => {
366
- const input = `import { AtAGlanceCard } from '@company/design-system-v2/elements'
367
-
368
- function Component() {
369
- return <AtAGlanceCard displayValue="42" label="Total" />
370
- }`
371
- const output = transform(input, 'file.tsx', {
372
- facadePackage: '@company/design-system',
373
- })
374
- expect(output).toBe(input) // Should not transform - different package
375
- })
376
- })
377
-
378
- describe('default @reapit/elements prefix matching', () => {
379
- test('transforms imports from @reapit/elements subpaths without facade package', () => {
380
- const input = `import { AtAGlanceCard } from '@reapit/elements/core/at-a-glance'
381
-
382
- function Component() {
383
- return <AtAGlanceCard displayValue="42" label="Total" />
384
- }`
385
- const output = transform(input)
386
- expect(output).toContain("import { AtAGlance } from '@reapit/elements/core/at-a-glance'")
387
- expect(output).toContain('<AtAGlance.ArticleCard')
388
- expect(output).not.toContain('AtAGlanceCard')
389
- })
390
- })
@@ -1,291 +0,0 @@
1
- import { Project, SyntaxKind, JsxOpeningElement, JsxSelfClosingElement, SourceFile } from 'ts-morph'
2
-
3
- /**
4
- * Codemod to migrate AtAGlance.Card to the new AtAGlance.ArticleCard.
5
- *
6
- * The old AtAGlance.Card component accepted props like `displayValue`, `label`,
7
- * `description`, and `icon` directly. The new API separates concerns:
8
- *
9
- * - `AtAGlance.Card` is now a primitive for custom layouts using `grid` prop and
10
- * subcomponents (Icon, Label, Description, Value)
11
- * - `AtAGlance.ArticleCard` is the new high-level component for static article cards
12
- *
13
- * Transformations:
14
- * - AtAGlance.Card (with displayValue/label props) -> AtAGlance.ArticleCard
15
- * - AtAGlanceCard (with displayValue/label props) -> AtAGlance.ArticleCard
16
- * - AtAGlance.Card (with children/grid) -> No change (already using new API)
17
- * - AtAGlanceCard (with children/grid) -> No change (already using new API)
18
- * - AtAGlance.AnchorCard / AtAGlanceAnchorCard -> No change (API unchanged)
19
- * - AtAGlance.ButtonCard / AtAGlanceButtonCard -> No change (API unchanged)
20
- */
21
-
22
- type JsxElementWithTag = JsxOpeningElement | JsxSelfClosingElement
23
-
24
- /**
25
- * Checks if a module specifier matches a package name.
26
- * Handles both exact matches and subpath imports.
27
- * @example
28
- * matchesPackage('@company/ui', '@company/ui') // true
29
- * matchesPackage('@company/ui/elements', '@company/ui') // true
30
- * matchesPackage('@company/ui-v2', '@company/ui') // false
31
- */
32
- function matchesPackage(moduleSpecifier: string, packageName: string): boolean {
33
- return moduleSpecifier === packageName || moduleSpecifier.startsWith(packageName + '/')
34
- }
35
-
36
- /**
37
- * Checks if a module specifier is an import from @reapit/elements or a facade package.
38
- */
39
- function isElementsImport(moduleSpecifier: string, facadePackage?: string): boolean {
40
- return (
41
- matchesPackage(moduleSpecifier, '@reapit/elements') ||
42
- (facadePackage !== undefined && matchesPackage(moduleSpecifier, facadePackage))
43
- )
44
- }
45
-
46
- function getTagName(element: JsxElementWithTag): string {
47
- return element.getTagNameNode().getText()
48
- }
49
-
50
- function isNamespacedComponent(element: JsxElementWithTag, componentName: string): boolean {
51
- return getTagName(element) === `AtAGlance.${componentName}`
52
- }
53
-
54
- function hasProp(element: JsxElementWithTag, propName: string): boolean {
55
- const attributes = element.getAttributes()
56
- return attributes.some((attr) => {
57
- if (attr.getKind() !== SyntaxKind.JsxAttribute) {
58
- return false
59
- }
60
- const jsxAttr = attr.asKindOrThrow(SyntaxKind.JsxAttribute)
61
- const nameNode = jsxAttr.getNameNode()
62
- return nameNode.getText() === propName
63
- })
64
- }
65
-
66
- function hasChildren(element: JsxElementWithTag): boolean {
67
- // Self-closing elements have no children
68
- if (element.getKind() === SyntaxKind.JsxSelfClosingElement) {
69
- return false
70
- }
71
-
72
- // For opening elements, check the parent JsxElement's children
73
- const parent = element.getParent()
74
- if (parent?.getKind() === SyntaxKind.JsxElement) {
75
- const jsxElement = parent.asKindOrThrow(SyntaxKind.JsxElement)
76
- const children = jsxElement.getJsxChildren()
77
- // Filter out whitespace-only text
78
- return children.some((child) => {
79
- if (child.getKind() === SyntaxKind.JsxText) {
80
- return child.getText().trim().length > 0
81
- }
82
- return true
83
- })
84
- }
85
-
86
- return false
87
- }
88
-
89
- function isUsingOldApi(element: JsxElementWithTag): boolean {
90
- // New API uses subcomponents as children or grid prop
91
- if (hasChildren(element) || hasProp(element, 'grid')) {
92
- return false
93
- }
94
- // Old API has displayValue or label props, or is completely empty (no props, no children)
95
- // Empty cards should be migrated to ArticleCard (will cause TS errors, but that's expected)
96
- return true
97
- }
98
-
99
- function renameTagTo(element: JsxElementWithTag, newTagName: string): void {
100
- const tagNameNode = element.getTagNameNode()
101
- tagNameNode.replaceWithText(newTagName)
102
- }
103
-
104
- function transformJsxElements(sourceFile: SourceFile, atAGlanceCardAliases: Set<string>): void {
105
- // Two-pass transformation approach:
106
- // Pass 1: Transform namespaced components (AtAGlance.Card -> AtAGlance.ArticleCard)
107
- // Pass 2: Transform direct imports (AtAGlanceCard -> AtAGlance.ArticleCard)
108
- //
109
- // We cannot do this in a single pass because mutating the AST while iterating
110
- // over it can cause nodes to be invalidated or missed. By collecting all elements
111
- // first, then mutating, we avoid iterator invalidation issues.
112
-
113
- // Pass 1: Process namespaced AtAGlance.Card components
114
- const selfClosingElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)
115
- const openingElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
116
-
117
- // Transform namespaced AtAGlance.Card to AtAGlance.ArticleCard when using old API
118
- for (const element of [...selfClosingElements, ...openingElements]) {
119
- if (isNamespacedComponent(element, 'Card') && isUsingOldApi(element)) {
120
- renameTagTo(element, 'AtAGlance.ArticleCard')
121
-
122
- // Also rename closing tag for non-self-closing elements
123
- if (element.getKind() === SyntaxKind.JsxOpeningElement) {
124
- const parent = element.getParent()
125
- if (parent?.getKind() === SyntaxKind.JsxElement) {
126
- const jsxElement = parent.asKindOrThrow(SyntaxKind.JsxElement)
127
- const closingElement = jsxElement.getClosingElement()
128
- if (closingElement) {
129
- closingElement.getTagNameNode().replaceWithText('AtAGlance.ArticleCard')
130
- }
131
- }
132
- }
133
- }
134
- }
135
-
136
- // Pass 2: Re-fetch elements after Pass 1 mutations, then transform direct imports
137
- const selfClosingElements2 = sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)
138
- const openingElements2 = sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
139
-
140
- // Transform direct AtAGlanceCard (or its aliases) to AtAGlance.ArticleCard when using old API
141
- for (const element of [...selfClosingElements2, ...openingElements2]) {
142
- const tagName = getTagName(element)
143
- if (atAGlanceCardAliases.has(tagName) && isUsingOldApi(element)) {
144
- renameTagTo(element, 'AtAGlance.ArticleCard')
145
-
146
- // Also rename closing tag for non-self-closing elements
147
- if (element.getKind() === SyntaxKind.JsxOpeningElement) {
148
- const parent = element.getParent()
149
- if (parent?.getKind() === SyntaxKind.JsxElement) {
150
- const jsxElement = parent.asKindOrThrow(SyntaxKind.JsxElement)
151
- const closingElement = jsxElement.getClosingElement()
152
- if (closingElement) {
153
- closingElement.getTagNameNode().replaceWithText('AtAGlance.ArticleCard')
154
- }
155
- }
156
- }
157
- }
158
- }
159
- }
160
-
161
- function getAtAGlanceCardAliases(sourceFile: SourceFile, facadePackage?: string): Set<string> {
162
- const aliases = new Set<string>()
163
-
164
- for (const importDecl of sourceFile.getImportDeclarations()) {
165
- const moduleSpecifier = importDecl.getModuleSpecifierValue()
166
-
167
- if (!isElementsImport(moduleSpecifier, facadePackage)) continue
168
-
169
- for (const namedImport of importDecl.getNamedImports()) {
170
- if (namedImport.getName() === 'AtAGlanceCard') {
171
- // Get the alias if it exists, otherwise use the original name
172
- // Only add the alias (or original name) that's actually used in the file
173
- const alias = namedImport.getAliasNode()?.getText()
174
- aliases.add(alias ?? 'AtAGlanceCard')
175
- }
176
- }
177
- }
178
-
179
- // Add default only if file has NO imports at all (handles test snippets without imports)
180
- if (aliases.size === 0 && sourceFile.getImportDeclarations().length === 0) {
181
- aliases.add('AtAGlanceCard')
182
- }
183
-
184
- return aliases
185
- }
186
-
187
- function isAtAGlanceCardStillUsed(sourceFile: SourceFile, aliases: Set<string>): boolean {
188
- const selfClosingElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)
189
- const openingElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
190
-
191
- return [...selfClosingElements, ...openingElements].some((element) => {
192
- const tagName = getTagName(element)
193
- return aliases.has(tagName)
194
- })
195
- }
196
-
197
- function hasAtAGlanceImport(sourceFile: SourceFile, facadePackage?: string): boolean {
198
- return sourceFile.getImportDeclarations().some((importDecl) => {
199
- const moduleSpecifier = importDecl.getModuleSpecifierValue()
200
-
201
- if (!isElementsImport(moduleSpecifier, facadePackage)) return false
202
-
203
- return importDecl.getNamedImports().some((namedImport) => namedImport.getName() === 'AtAGlance')
204
- })
205
- }
206
-
207
- function usesAtAGlanceNamespace(sourceFile: SourceFile): boolean {
208
- const selfClosingElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)
209
- const openingElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
210
-
211
- return [...selfClosingElements, ...openingElements].some((element) => getTagName(element).startsWith('AtAGlance.'))
212
- }
213
-
214
- function updateImports(sourceFile: SourceFile, atAGlanceCardAliases: Set<string>, facadePackage?: string): void {
215
- const importDeclarations = sourceFile.getImportDeclarations()
216
- const atAGlanceCardStillUsed = isAtAGlanceCardStillUsed(sourceFile, atAGlanceCardAliases)
217
- const needsAtAGlanceImport = usesAtAGlanceNamespace(sourceFile) && !hasAtAGlanceImport(sourceFile, facadePackage)
218
- let importDeclWhereAtAGlanceCardWasRemoved: (typeof importDeclarations)[0] | null = null
219
-
220
- // First pass: Remove AtAGlanceCard imports and track where it was removed
221
- for (const importDecl of importDeclarations) {
222
- const moduleSpecifier = importDecl.getModuleSpecifierValue()
223
-
224
- if (!isElementsImport(moduleSpecifier, facadePackage)) continue
225
-
226
- if (!atAGlanceCardStillUsed) {
227
- const namedImports = importDecl.getNamedImports()
228
-
229
- for (const namedImport of namedImports) {
230
- if (namedImport.getName() === 'AtAGlanceCard') {
231
- namedImport.remove()
232
- importDeclWhereAtAGlanceCardWasRemoved = importDecl
233
- }
234
- }
235
- }
236
- }
237
-
238
- // Second pass: Add AtAGlance import to the same declaration where AtAGlanceCard was removed
239
- // Do this BEFORE removing empty imports to avoid accessing removed nodes
240
- if (needsAtAGlanceImport && importDeclWhereAtAGlanceCardWasRemoved) {
241
- importDeclWhereAtAGlanceCardWasRemoved.addNamedImport('AtAGlance')
242
- } else if (needsAtAGlanceImport) {
243
- // If we didn't find where AtAGlanceCard was removed, add to first elements import
244
- for (const importDecl of importDeclarations) {
245
- const moduleSpecifier = importDecl.getModuleSpecifierValue()
246
-
247
- if (isElementsImport(moduleSpecifier, facadePackage)) {
248
- importDecl.addNamedImport('AtAGlance')
249
- break
250
- }
251
- }
252
- }
253
-
254
- // Third pass: Remove empty import declarations
255
- for (const importDecl of importDeclarations) {
256
- const moduleSpecifier = importDecl.getModuleSpecifierValue()
257
-
258
- if (!isElementsImport(moduleSpecifier, facadePackage)) continue
259
-
260
- if (
261
- importDecl.getNamedImports().length === 0 &&
262
- !importDecl.getDefaultImport() &&
263
- !importDecl.getNamespaceImport()
264
- ) {
265
- importDecl.remove()
266
- }
267
- }
268
- }
269
-
270
- export default function transform(
271
- source: string,
272
- filePath: string = 'file.tsx',
273
- options?: { facadePackage?: string },
274
- ): string {
275
- const project = new Project({
276
- useInMemoryFileSystem: true,
277
- compilerOptions: {
278
- jsx: 2, // JsxEmit.React
279
- },
280
- })
281
-
282
- const sourceFile = project.createSourceFile(filePath, source)
283
-
284
- // Get aliases before transforming (e.g., import { AtAGlanceCard as Card })
285
- const atAGlanceCardAliases = getAtAGlanceCardAliases(sourceFile, options?.facadePackage)
286
-
287
- transformJsxElements(sourceFile, atAGlanceCardAliases)
288
- updateImports(sourceFile, atAGlanceCardAliases, options?.facadePackage)
289
-
290
- return sourceFile.getFullText()
291
- }