@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.
- package/bin/elements.cjs +69 -0
- package/dist/codemods/at-a-glance-article-card/transform.d.ts +4 -0
- package/dist/codemods/at-a-glance-article-card/transform.d.ts.map +1 -0
- package/dist/codemods/at-a-glance-article-card/transform.js +223 -0
- package/dist/codemods/at-a-glance-article-card/transform.js.map +1 -0
- package/dist/codemods/bin.d.ts +2 -0
- package/dist/codemods/bin.d.ts.map +1 -0
- package/dist/codemods/bin.js +140 -0
- package/dist/codemods/bin.js.map +1 -0
- package/dist/codemods/codemods.d.ts +29 -0
- package/dist/codemods/codemods.d.ts.map +1 -0
- package/dist/codemods/codemods.js +32 -0
- package/dist/codemods/codemods.js.map +1 -0
- package/{codemods → dist/codemods}/manifest.json +1 -1
- package/dist/codemods/runner.d.ts +21 -0
- package/dist/codemods/runner.d.ts.map +1 -0
- package/dist/codemods/runner.js +165 -0
- package/dist/codemods/runner.js.map +1 -0
- package/package.json +8 -9
- package/codemods/__tests__/codemods.test.ts +0 -178
- package/codemods/__tests__/generate-manifest.test.ts +0 -240
- package/codemods/__tests__/readme-parser.test.ts +0 -218
- package/codemods/__tests__/runner.test.ts +0 -530
- package/codemods/at-a-glance-article-card/README.md +0 -122
- package/codemods/at-a-glance-article-card/__tests__/transform.test.ts +0 -390
- package/codemods/at-a-glance-article-card/transform.ts +0 -291
- package/codemods/bin.cjs +0 -13
- package/codemods/bin.ts +0 -205
- package/codemods/codemods.ts +0 -75
- package/codemods/generate-manifest.ts +0 -120
- package/codemods/manifest.schema.json +0 -39
- package/codemods/readme-parser.ts +0 -37
- package/codemods/runner.ts +0 -196
- 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
|
-
}
|