@reapit/elements 5.0.0-beta.71 → 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 +3 -5
- 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 +6 -5
- 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.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
|
@@ -1,530 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { findFiles, matchesPatterns, run } from '../runner'
|
|
5
|
-
import type { Transform } from '../runner'
|
|
6
|
-
|
|
7
|
-
let testDir: string
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
// Create a temporary test directory
|
|
11
|
-
testDir = join(tmpdir(), `runner-test-${Date.now()}`)
|
|
12
|
-
mkdirSync(testDir, { recursive: true })
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
// Clean up test directory
|
|
17
|
-
if (existsSync(testDir)) {
|
|
18
|
-
rmSync(testDir, { recursive: true, force: true })
|
|
19
|
-
}
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
describe('matchesPatterns', () => {
|
|
23
|
-
test('matches files with *.ext pattern', () => {
|
|
24
|
-
expect(matchesPatterns('file.ts', ['*.ts'])).toBe(true)
|
|
25
|
-
expect(matchesPatterns('file.tsx', ['*.tsx'])).toBe(true)
|
|
26
|
-
expect(matchesPatterns('file.js', ['*.js'])).toBe(true)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
test('does not match files with wrong extension', () => {
|
|
30
|
-
expect(matchesPatterns('file.ts', ['*.tsx'])).toBe(false)
|
|
31
|
-
expect(matchesPatterns('file.js', ['*.ts'])).toBe(false)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('matches with multiple patterns', () => {
|
|
35
|
-
const patterns = ['*.ts', '*.tsx']
|
|
36
|
-
|
|
37
|
-
expect(matchesPatterns('file.ts', patterns)).toBe(true)
|
|
38
|
-
expect(matchesPatterns('file.tsx', patterns)).toBe(true)
|
|
39
|
-
expect(matchesPatterns('file.js', patterns)).toBe(false)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('matches exact filename', () => {
|
|
43
|
-
expect(matchesPatterns('specific-file.ts', ['specific-file.ts'])).toBe(true)
|
|
44
|
-
expect(matchesPatterns('other-file.ts', ['specific-file.ts'])).toBe(false)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('matches with wildcard in middle of pattern', () => {
|
|
48
|
-
expect(matchesPatterns('test.spec.ts', ['*.spec.ts'])).toBe(true)
|
|
49
|
-
expect(matchesPatterns('file.test.ts', ['*.spec.ts'])).toBe(false)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
test('rejects patterns that are too long (ReDoS protection)', () => {
|
|
53
|
-
const longPattern = '*'.repeat(200)
|
|
54
|
-
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
55
|
-
|
|
56
|
-
const result = matchesPatterns('file.ts', [longPattern])
|
|
57
|
-
|
|
58
|
-
expect(result).toBe(false)
|
|
59
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('potentially unsafe pattern'))
|
|
60
|
-
|
|
61
|
-
consoleSpy.mockRestore()
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test('rejects patterns with excessive wildcards (ReDoS protection)', () => {
|
|
65
|
-
const pattern = '*.*.*.*.*.*.ts'
|
|
66
|
-
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
67
|
-
|
|
68
|
-
const result = matchesPatterns('file.ts', [pattern])
|
|
69
|
-
|
|
70
|
-
expect(result).toBe(false)
|
|
71
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('potentially unsafe pattern'))
|
|
72
|
-
|
|
73
|
-
consoleSpy.mockRestore()
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
test('rejects filenames that are too long', () => {
|
|
77
|
-
const longFilename = 'a'.repeat(600) + '.ts'
|
|
78
|
-
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
79
|
-
|
|
80
|
-
const result = matchesPatterns(longFilename, ['*test*.ts'])
|
|
81
|
-
|
|
82
|
-
expect(result).toBe(false)
|
|
83
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Filename too long'))
|
|
84
|
-
|
|
85
|
-
consoleSpy.mockRestore()
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
test('matches common file extensions', () => {
|
|
89
|
-
expect(matchesPatterns('Component.tsx', ['*.tsx', '*.ts', '*.jsx', '*.js'])).toBe(true)
|
|
90
|
-
expect(matchesPatterns('utils.ts', ['*.tsx', '*.ts', '*.jsx', '*.js'])).toBe(true)
|
|
91
|
-
expect(matchesPatterns('App.jsx', ['*.tsx', '*.ts', '*.jsx', '*.js'])).toBe(true)
|
|
92
|
-
expect(matchesPatterns('index.js', ['*.tsx', '*.ts', '*.jsx', '*.js'])).toBe(true)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test('returns false for empty pattern array', () => {
|
|
96
|
-
expect(matchesPatterns('file.ts', [])).toBe(false)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('matches first matching pattern', () => {
|
|
100
|
-
const patterns = ['*.js', '*.ts', '*.tsx']
|
|
101
|
-
|
|
102
|
-
expect(matchesPatterns('file.ts', patterns)).toBe(true)
|
|
103
|
-
})
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
describe('findFiles', () => {
|
|
107
|
-
test('finds files matching patterns in directory', () => {
|
|
108
|
-
writeFileSync(join(testDir, 'file1.ts'), 'content')
|
|
109
|
-
writeFileSync(join(testDir, 'file2.tsx'), 'content')
|
|
110
|
-
writeFileSync(join(testDir, 'file3.js'), 'content')
|
|
111
|
-
|
|
112
|
-
const files = findFiles(testDir, ['*.ts', '*.tsx'])
|
|
113
|
-
|
|
114
|
-
expect(files).toHaveLength(2)
|
|
115
|
-
expect(files).toContainEqual(join(testDir, 'file1.ts'))
|
|
116
|
-
expect(files).toContainEqual(join(testDir, 'file2.tsx'))
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
test('recursively finds files in subdirectories', () => {
|
|
120
|
-
const subDir = join(testDir, 'subdir')
|
|
121
|
-
mkdirSync(subDir)
|
|
122
|
-
|
|
123
|
-
writeFileSync(join(testDir, 'root.ts'), 'content')
|
|
124
|
-
writeFileSync(join(subDir, 'nested.ts'), 'content')
|
|
125
|
-
|
|
126
|
-
const files = findFiles(testDir, ['*.ts'])
|
|
127
|
-
|
|
128
|
-
expect(files).toHaveLength(2)
|
|
129
|
-
expect(files).toContainEqual(join(testDir, 'root.ts'))
|
|
130
|
-
expect(files).toContainEqual(join(subDir, 'nested.ts'))
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
test('skips node_modules directory', () => {
|
|
134
|
-
const nodeModules = join(testDir, 'node_modules')
|
|
135
|
-
mkdirSync(nodeModules)
|
|
136
|
-
|
|
137
|
-
writeFileSync(join(testDir, 'app.ts'), 'content')
|
|
138
|
-
writeFileSync(join(nodeModules, 'package.ts'), 'content')
|
|
139
|
-
|
|
140
|
-
const files = findFiles(testDir, ['*.ts'])
|
|
141
|
-
|
|
142
|
-
expect(files).toHaveLength(1)
|
|
143
|
-
expect(files).toContainEqual(join(testDir, 'app.ts'))
|
|
144
|
-
expect(files).not.toContainEqual(join(nodeModules, 'package.ts'))
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
test('skips dist directory', () => {
|
|
148
|
-
const dist = join(testDir, 'dist')
|
|
149
|
-
mkdirSync(dist)
|
|
150
|
-
|
|
151
|
-
writeFileSync(join(testDir, 'source.ts'), 'content')
|
|
152
|
-
writeFileSync(join(dist, 'compiled.js'), 'content')
|
|
153
|
-
|
|
154
|
-
const files = findFiles(testDir, ['*.ts', '*.js'])
|
|
155
|
-
|
|
156
|
-
expect(files).toHaveLength(1)
|
|
157
|
-
expect(files).toContainEqual(join(testDir, 'source.ts'))
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
test('returns empty array when no matching files exist', () => {
|
|
161
|
-
writeFileSync(join(testDir, 'file.txt'), 'content')
|
|
162
|
-
writeFileSync(join(testDir, 'file.md'), 'content')
|
|
163
|
-
|
|
164
|
-
const files = findFiles(testDir, ['*.ts'])
|
|
165
|
-
|
|
166
|
-
expect(files).toEqual([])
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
test('finds files in deeply nested directories', () => {
|
|
170
|
-
const level1 = join(testDir, 'level1')
|
|
171
|
-
const level2 = join(level1, 'level2')
|
|
172
|
-
const level3 = join(level2, 'level3')
|
|
173
|
-
|
|
174
|
-
mkdirSync(level3, { recursive: true })
|
|
175
|
-
|
|
176
|
-
writeFileSync(join(level3, 'deep.ts'), 'content')
|
|
177
|
-
|
|
178
|
-
const files = findFiles(testDir, ['*.ts'])
|
|
179
|
-
|
|
180
|
-
expect(files).toHaveLength(1)
|
|
181
|
-
expect(files).toContainEqual(join(level3, 'deep.ts'))
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
test('handles multiple file types in nested structure', () => {
|
|
185
|
-
const src = join(testDir, 'src')
|
|
186
|
-
const components = join(src, 'components')
|
|
187
|
-
const utils = join(src, 'utils')
|
|
188
|
-
|
|
189
|
-
mkdirSync(components, { recursive: true })
|
|
190
|
-
mkdirSync(utils, { recursive: true })
|
|
191
|
-
|
|
192
|
-
writeFileSync(join(components, 'Button.tsx'), 'content')
|
|
193
|
-
writeFileSync(join(components, 'Input.tsx'), 'content')
|
|
194
|
-
writeFileSync(join(utils, 'helpers.ts'), 'content')
|
|
195
|
-
writeFileSync(join(src, 'index.ts'), 'content')
|
|
196
|
-
|
|
197
|
-
const files = findFiles(testDir, ['*.ts', '*.tsx'])
|
|
198
|
-
|
|
199
|
-
expect(files).toHaveLength(4)
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
test('uses accumulator parameter correctly', () => {
|
|
203
|
-
writeFileSync(join(testDir, 'file1.ts'), 'content')
|
|
204
|
-
writeFileSync(join(testDir, 'file2.ts'), 'content')
|
|
205
|
-
|
|
206
|
-
const existingResults = ['/existing/file.ts']
|
|
207
|
-
const files = findFiles(testDir, ['*.ts'], existingResults)
|
|
208
|
-
|
|
209
|
-
expect(files).toHaveLength(3)
|
|
210
|
-
expect(files).toContain('/existing/file.ts')
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
test('prevents path traversal attacks', () => {
|
|
214
|
-
// This test ensures that symlinks or malicious paths don't escape the base directory
|
|
215
|
-
writeFileSync(join(testDir, 'safe.ts'), 'content')
|
|
216
|
-
|
|
217
|
-
const files = findFiles(testDir, ['*.ts'])
|
|
218
|
-
|
|
219
|
-
// All returned files should be within testDir
|
|
220
|
-
files.forEach((file) => {
|
|
221
|
-
expect(file.startsWith(testDir)).toBe(true)
|
|
222
|
-
})
|
|
223
|
-
})
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
describe('run', () => {
|
|
227
|
-
let originalCwd: string
|
|
228
|
-
let originalExit: typeof process.exit
|
|
229
|
-
let exitCode: number | null
|
|
230
|
-
let consoleOutput: string[]
|
|
231
|
-
|
|
232
|
-
beforeEach(() => {
|
|
233
|
-
originalCwd = process.cwd()
|
|
234
|
-
originalExit = process.exit
|
|
235
|
-
exitCode = null
|
|
236
|
-
consoleOutput = []
|
|
237
|
-
|
|
238
|
-
// Mock process.exit to capture exit codes
|
|
239
|
-
process.exit = ((code?: number) => {
|
|
240
|
-
exitCode = code ?? 0
|
|
241
|
-
throw new Error(`process.exit(${code})`)
|
|
242
|
-
}) as typeof process.exit
|
|
243
|
-
|
|
244
|
-
// Mock console methods
|
|
245
|
-
vi.spyOn(console, 'log').mockImplementation((msg) => {
|
|
246
|
-
consoleOutput.push(String(msg))
|
|
247
|
-
})
|
|
248
|
-
vi.spyOn(console, 'error').mockImplementation((msg) => {
|
|
249
|
-
consoleOutput.push(String(msg))
|
|
250
|
-
})
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
afterEach(() => {
|
|
254
|
-
process.exit = originalExit
|
|
255
|
-
process.chdir(originalCwd)
|
|
256
|
-
vi.restoreAllMocks()
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
test('exits with help when no args provided', async () => {
|
|
260
|
-
const transform: Transform = (source) => source
|
|
261
|
-
|
|
262
|
-
await expect(
|
|
263
|
-
run({
|
|
264
|
-
transform,
|
|
265
|
-
codemodName: 'test-codemod',
|
|
266
|
-
args: [],
|
|
267
|
-
}),
|
|
268
|
-
).rejects.toThrow('process.exit(0)')
|
|
269
|
-
|
|
270
|
-
expect(exitCode).toBe(0)
|
|
271
|
-
expect(consoleOutput.join('\n')).toContain('Usage:')
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
test('exits with help when --help flag provided', async () => {
|
|
275
|
-
const transform: Transform = (source) => source
|
|
276
|
-
|
|
277
|
-
await expect(
|
|
278
|
-
run({
|
|
279
|
-
transform,
|
|
280
|
-
codemodName: 'test-codemod',
|
|
281
|
-
args: ['--help'],
|
|
282
|
-
}),
|
|
283
|
-
).rejects.toThrow('process.exit(0)')
|
|
284
|
-
|
|
285
|
-
expect(exitCode).toBe(0)
|
|
286
|
-
expect(consoleOutput.join('\n')).toContain('Usage:')
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
test('transforms files in directory', async () => {
|
|
290
|
-
process.chdir(testDir)
|
|
291
|
-
|
|
292
|
-
const srcDir = join(testDir, 'src')
|
|
293
|
-
mkdirSync(srcDir)
|
|
294
|
-
|
|
295
|
-
writeFileSync(join(srcDir, 'file1.ts'), 'OLD_CODE')
|
|
296
|
-
writeFileSync(join(srcDir, 'file2.ts'), 'OLD_CODE')
|
|
297
|
-
|
|
298
|
-
const transform: Transform = (source) => source.replace('OLD_CODE', 'NEW_CODE')
|
|
299
|
-
|
|
300
|
-
await run({
|
|
301
|
-
transform,
|
|
302
|
-
codemodName: 'test-codemod',
|
|
303
|
-
args: ['src'],
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
const file1Content = readFileSync(join(srcDir, 'file1.ts'), 'utf-8')
|
|
307
|
-
const file2Content = readFileSync(join(srcDir, 'file2.ts'), 'utf-8')
|
|
308
|
-
|
|
309
|
-
expect(file1Content).toBe('NEW_CODE')
|
|
310
|
-
expect(file2Content).toBe('NEW_CODE')
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
test('performs dry run without modifying files', async () => {
|
|
314
|
-
process.chdir(testDir)
|
|
315
|
-
|
|
316
|
-
const srcDir = join(testDir, 'src')
|
|
317
|
-
mkdirSync(srcDir)
|
|
318
|
-
|
|
319
|
-
writeFileSync(join(srcDir, 'file.ts'), 'OLD_CODE')
|
|
320
|
-
|
|
321
|
-
const transform: Transform = (source) => source.replace('OLD_CODE', 'NEW_CODE')
|
|
322
|
-
|
|
323
|
-
await run({
|
|
324
|
-
transform,
|
|
325
|
-
codemodName: 'test-codemod',
|
|
326
|
-
args: ['src', '--dry-run'],
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
const fileContent = readFileSync(join(srcDir, 'file.ts'), 'utf-8')
|
|
330
|
-
|
|
331
|
-
expect(fileContent).toBe('OLD_CODE')
|
|
332
|
-
expect(consoleOutput.join('\n')).toContain('Would transform')
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
test('respects custom file extensions', async () => {
|
|
336
|
-
process.chdir(testDir)
|
|
337
|
-
|
|
338
|
-
const srcDir = join(testDir, 'src')
|
|
339
|
-
mkdirSync(srcDir)
|
|
340
|
-
|
|
341
|
-
writeFileSync(join(srcDir, 'file.tsx'), 'OLD_CODE')
|
|
342
|
-
writeFileSync(join(srcDir, 'file.ts'), 'OLD_CODE')
|
|
343
|
-
writeFileSync(join(srcDir, 'file.js'), 'OLD_CODE')
|
|
344
|
-
|
|
345
|
-
const transform: Transform = (source) => source.replace('OLD_CODE', 'NEW_CODE')
|
|
346
|
-
|
|
347
|
-
await run({
|
|
348
|
-
transform,
|
|
349
|
-
codemodName: 'test-codemod',
|
|
350
|
-
args: ['src', '--ext', '.tsx,.jsx'],
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
const tsxContent = readFileSync(join(srcDir, 'file.tsx'), 'utf-8')
|
|
354
|
-
const tsContent = readFileSync(join(srcDir, 'file.ts'), 'utf-8')
|
|
355
|
-
|
|
356
|
-
expect(tsxContent).toBe('NEW_CODE')
|
|
357
|
-
expect(tsContent).toBe('OLD_CODE') // Not transformed
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
test('exits with error when directory not found', async () => {
|
|
361
|
-
process.chdir(testDir)
|
|
362
|
-
|
|
363
|
-
const transform: Transform = (source) => source
|
|
364
|
-
|
|
365
|
-
await expect(
|
|
366
|
-
run({
|
|
367
|
-
transform,
|
|
368
|
-
codemodName: 'test-codemod',
|
|
369
|
-
args: ['nonexistent'],
|
|
370
|
-
}),
|
|
371
|
-
).rejects.toThrow('process.exit(1)')
|
|
372
|
-
|
|
373
|
-
expect(exitCode).toBe(1)
|
|
374
|
-
expect(consoleOutput.join('\n')).toContain('Directory not found')
|
|
375
|
-
})
|
|
376
|
-
|
|
377
|
-
test('exits with error when no directory provided', async () => {
|
|
378
|
-
process.chdir(testDir)
|
|
379
|
-
|
|
380
|
-
const transform: Transform = (source) => source
|
|
381
|
-
|
|
382
|
-
await expect(
|
|
383
|
-
run({
|
|
384
|
-
transform,
|
|
385
|
-
codemodName: 'test-codemod',
|
|
386
|
-
args: ['--dry-run'],
|
|
387
|
-
}),
|
|
388
|
-
).rejects.toThrow('process.exit(1)')
|
|
389
|
-
|
|
390
|
-
expect(exitCode).toBe(1)
|
|
391
|
-
expect(consoleOutput.join('\n')).toContain('No directory provided')
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
test('prevents directory traversal attacks', async () => {
|
|
395
|
-
process.chdir(testDir)
|
|
396
|
-
|
|
397
|
-
const transform: Transform = (source) => source
|
|
398
|
-
|
|
399
|
-
await expect(
|
|
400
|
-
run({
|
|
401
|
-
transform,
|
|
402
|
-
codemodName: 'test-codemod',
|
|
403
|
-
args: ['../../../etc'],
|
|
404
|
-
}),
|
|
405
|
-
).rejects.toThrow('process.exit(1)')
|
|
406
|
-
|
|
407
|
-
expect(exitCode).toBe(1)
|
|
408
|
-
expect(consoleOutput.join('\n')).toContain('outside the current working directory')
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
test('only transforms files that change', async () => {
|
|
412
|
-
process.chdir(testDir)
|
|
413
|
-
|
|
414
|
-
const srcDir = join(testDir, 'src')
|
|
415
|
-
mkdirSync(srcDir)
|
|
416
|
-
|
|
417
|
-
writeFileSync(join(srcDir, 'changed.ts'), 'OLD_CODE')
|
|
418
|
-
writeFileSync(join(srcDir, 'unchanged.ts'), 'KEEP_CODE')
|
|
419
|
-
|
|
420
|
-
const transform: Transform = (source) => source.replace('OLD_CODE', 'NEW_CODE')
|
|
421
|
-
|
|
422
|
-
await run({
|
|
423
|
-
transform,
|
|
424
|
-
codemodName: 'test-codemod',
|
|
425
|
-
args: ['src'],
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
const output = consoleOutput.join('\n')
|
|
429
|
-
expect(output).toContain('Transformed: src/changed.ts')
|
|
430
|
-
expect(output).not.toContain('unchanged.ts')
|
|
431
|
-
expect(output).toContain('Transformed 1 file(s)')
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
test('handles transform errors gracefully', async () => {
|
|
435
|
-
process.chdir(testDir)
|
|
436
|
-
|
|
437
|
-
const srcDir = join(testDir, 'src')
|
|
438
|
-
mkdirSync(srcDir)
|
|
439
|
-
|
|
440
|
-
writeFileSync(join(srcDir, 'file1.ts'), 'content')
|
|
441
|
-
writeFileSync(join(srcDir, 'file2.ts'), 'content')
|
|
442
|
-
|
|
443
|
-
const transform: Transform = (source, filePath) => {
|
|
444
|
-
if (filePath.includes('file1')) {
|
|
445
|
-
throw new Error('Transform failed')
|
|
446
|
-
}
|
|
447
|
-
return source.replace('content', 'transformed')
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
await run({
|
|
451
|
-
transform,
|
|
452
|
-
codemodName: 'test-codemod',
|
|
453
|
-
args: ['src'],
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
const output = consoleOutput.join('\n')
|
|
457
|
-
expect(output).toContain('Error processing')
|
|
458
|
-
expect(output).toContain('Transform failed')
|
|
459
|
-
|
|
460
|
-
// Second file should still be transformed
|
|
461
|
-
const file2Content = readFileSync(join(srcDir, 'file2.ts'), 'utf-8')
|
|
462
|
-
expect(file2Content).toBe('transformed')
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
test('reports correct file count when no files match', async () => {
|
|
466
|
-
process.chdir(testDir)
|
|
467
|
-
|
|
468
|
-
const srcDir = join(testDir, 'src')
|
|
469
|
-
mkdirSync(srcDir)
|
|
470
|
-
|
|
471
|
-
writeFileSync(join(srcDir, 'file.txt'), 'content')
|
|
472
|
-
|
|
473
|
-
const transform: Transform = (source) => source
|
|
474
|
-
|
|
475
|
-
await expect(
|
|
476
|
-
run({
|
|
477
|
-
transform,
|
|
478
|
-
codemodName: 'test-codemod',
|
|
479
|
-
args: ['src'],
|
|
480
|
-
}),
|
|
481
|
-
).rejects.toThrow('process.exit(0)')
|
|
482
|
-
|
|
483
|
-
expect(consoleOutput.join('\n')).toContain('No matching files found')
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
test('handles -d shorthand for dry-run', async () => {
|
|
487
|
-
process.chdir(testDir)
|
|
488
|
-
|
|
489
|
-
const srcDir = join(testDir, 'src')
|
|
490
|
-
mkdirSync(srcDir)
|
|
491
|
-
|
|
492
|
-
writeFileSync(join(srcDir, 'file.ts'), 'OLD_CODE')
|
|
493
|
-
|
|
494
|
-
const transform: Transform = (source) => source.replace('OLD_CODE', 'NEW_CODE')
|
|
495
|
-
|
|
496
|
-
await run({
|
|
497
|
-
transform,
|
|
498
|
-
codemodName: 'test-codemod',
|
|
499
|
-
args: ['src', '-d'],
|
|
500
|
-
})
|
|
501
|
-
|
|
502
|
-
const fileContent = readFileSync(join(srcDir, 'file.ts'), 'utf-8')
|
|
503
|
-
|
|
504
|
-
expect(fileContent).toBe('OLD_CODE')
|
|
505
|
-
expect(consoleOutput.join('\n')).toContain('dry run')
|
|
506
|
-
})
|
|
507
|
-
|
|
508
|
-
test('passes file path to transform function', async () => {
|
|
509
|
-
process.chdir(testDir)
|
|
510
|
-
|
|
511
|
-
const srcDir = join(testDir, 'src')
|
|
512
|
-
mkdirSync(srcDir)
|
|
513
|
-
|
|
514
|
-
writeFileSync(join(srcDir, 'file.ts'), 'content')
|
|
515
|
-
|
|
516
|
-
let receivedPath = ''
|
|
517
|
-
const transform: Transform = (source, filePath) => {
|
|
518
|
-
receivedPath = filePath
|
|
519
|
-
return source
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
await run({
|
|
523
|
-
transform,
|
|
524
|
-
codemodName: 'test-codemod',
|
|
525
|
-
args: ['src'],
|
|
526
|
-
})
|
|
527
|
-
|
|
528
|
-
expect(receivedPath).toContain('file.ts')
|
|
529
|
-
})
|
|
530
|
-
})
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Migrates AtAGlance.Card to AtAGlance.ArticleCard
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# AtAGlance ArticleCard Codemod
|
|
6
|
-
|
|
7
|
-
Migrates old `AtAGlance.Card` usage to the new `AtAGlance.ArticleCard` component.
|
|
8
|
-
|
|
9
|
-
## Usage
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
# List available codemods
|
|
13
|
-
yarn dlx @reapit/elements@beta codemod list
|
|
14
|
-
|
|
15
|
-
# Show detailed info about this codemod
|
|
16
|
-
yarn dlx @reapit/elements@beta codemod info at-a-glance-article-card
|
|
17
|
-
|
|
18
|
-
# Run on a directory
|
|
19
|
-
yarn dlx @reapit/elements@beta codemod apply at-a-glance-article-card src/
|
|
20
|
-
|
|
21
|
-
# Preview changes without writing files
|
|
22
|
-
yarn dlx @reapit/elements@beta codemod apply at-a-glance-article-card src/ --dry-run
|
|
23
|
-
|
|
24
|
-
# Specify file extensions
|
|
25
|
-
yarn dlx @reapit/elements@beta codemod apply at-a-glance-article-card src/ --ext .tsx,.jsx
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### Facade Package Support
|
|
29
|
-
|
|
30
|
-
If your project re-exports `@reapit/elements` through an internal facade package, use the `--facade-package` flag:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
yarn dlx @reapit/elements@beta codemod apply at-a-glance-article-card src/ --facade-package @company/ui-components
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
The codemod uses **prefix matching**, so specifying a base package will match all its subpaths:
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
# This will match:
|
|
40
|
-
# - @company/design-system/elements
|
|
41
|
-
# - @company/design-system/core
|
|
42
|
-
# - @company/design-system/utils
|
|
43
|
-
# - etc.
|
|
44
|
-
yarn dlx @reapit/elements@beta codemod apply at-a-glance-article-card src/ --facade-package @company/design-system
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
If you have multiple unrelated facade packages, run the codemod once for each package:
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
yarn dlx @reapit/elements@beta codemod apply at-a-glance-article-card src/ --facade-package @company/ui
|
|
51
|
-
yarn dlx @reapit/elements@beta codemod apply at-a-glance-article-card src/ --facade-package @another/design-lib
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
**Example with facade package:**
|
|
55
|
-
|
|
56
|
-
```tsx
|
|
57
|
-
// Before (with facade package @habio/design-system)
|
|
58
|
-
import { AtAGlanceCard } from '@habio/design-system/elements'
|
|
59
|
-
;<AtAGlanceCard displayValue="42" label="Total" />
|
|
60
|
-
|
|
61
|
-
// After running with --facade-package @habio/design-system
|
|
62
|
-
import { AtAGlance } from '@habio/design-system/elements'
|
|
63
|
-
;<AtAGlance.ArticleCard displayValue="42" label="Total" />
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Background
|
|
67
|
-
|
|
68
|
-
The `AtAGlance.Card` component API has changed:
|
|
69
|
-
|
|
70
|
-
- **Old API**: `AtAGlance.Card` accepted props like `displayValue`, `label`, `description`, and `icon` directly
|
|
71
|
-
- **New API**: `AtAGlance.Card` is now a primitive for custom layouts using the `grid` prop and subcomponents (`CardIcon`, `CardLabel`, `CardDescription`, `CardValue`)
|
|
72
|
-
|
|
73
|
-
For standard layouts, use the new element-specific card components:
|
|
74
|
-
|
|
75
|
-
- `AtAGlance.ArticleCard` - static, non-interactive cards
|
|
76
|
-
- `AtAGlance.AnchorCard` - navigable link cards
|
|
77
|
-
- `AtAGlance.ButtonCard` - interactive button cards
|
|
78
|
-
|
|
79
|
-
## Transformations
|
|
80
|
-
|
|
81
|
-
Both namespaced and direct component usage are converted to the namespaced `AtAGlance.ArticleCard`:
|
|
82
|
-
|
|
83
|
-
| Before | After |
|
|
84
|
-
| --------------------------------------------------- | ---------------------------------------------------------- |
|
|
85
|
-
| `<AtAGlance.Card displayValue="42" label="Total"/>` | `<AtAGlance.ArticleCard displayValue="42" label="Total"/>` |
|
|
86
|
-
| `<AtAGlanceCard displayValue="42" label="Total"/>` | `<AtAGlance.ArticleCard displayValue="42" label="Total"/>` |
|
|
87
|
-
| `<AtAGlance.Card grid="..." />` | No change (already using new primitive API) |
|
|
88
|
-
| `<AtAGlanceCard grid="..." />` | No change (already using new primitive API) |
|
|
89
|
-
| `<AtAGlance.Card>{children}</AtAGlance.Card>` | No change (already using new primitive API) |
|
|
90
|
-
| `<AtAGlanceCard>{children}</AtAGlanceCard>` | No change (already using new primitive API) |
|
|
91
|
-
| `<AtAGlance.AnchorCard {...} />` | No change (API unchanged) |
|
|
92
|
-
| `<AtAGlanceAnchorCard {...} />` | No change (API unchanged) |
|
|
93
|
-
| `<AtAGlance.ButtonCard {...} />` | No change (API unchanged) |
|
|
94
|
-
| `<AtAGlanceButtonCard {...} />` | No change (API unchanged) |
|
|
95
|
-
|
|
96
|
-
## Import Handling
|
|
97
|
-
|
|
98
|
-
The codemod removes `AtAGlanceCard` from imports when it is no longer used and adds the `AtAGlance` import. This works for direct `@reapit/elements` imports and facade packages (when specified via `--facade-package`):
|
|
99
|
-
|
|
100
|
-
```tsx
|
|
101
|
-
// Before
|
|
102
|
-
import { AtAGlanceCard } from '@reapit/elements'
|
|
103
|
-
;<AtAGlanceCard displayValue="42" label="Total" />
|
|
104
|
-
|
|
105
|
-
// After
|
|
106
|
-
import { AtAGlance } from '@reapit/elements'
|
|
107
|
-
;<AtAGlance.ArticleCard displayValue="42" label="Total" />
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
When `AtAGlanceCard` is still used with the new primitive API, the import is kept:
|
|
111
|
-
|
|
112
|
-
```tsx
|
|
113
|
-
// Before
|
|
114
|
-
import { AtAGlanceCard } from '@reapit/elements'
|
|
115
|
-
<AtAGlanceCard displayValue="42" label="Old" />
|
|
116
|
-
<AtAGlanceCard grid="auto"><span>New</span></AtAGlanceCard>
|
|
117
|
-
|
|
118
|
-
// After
|
|
119
|
-
import { AtAGlanceCard } from '@reapit/elements'
|
|
120
|
-
<AtAGlance.ArticleCard displayValue="42" label="Old" />
|
|
121
|
-
<AtAGlanceCard grid="auto"><span>New</span></AtAGlanceCard>
|
|
122
|
-
```
|