@getmikk/core 1.6.0 → 1.7.0

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.
@@ -0,0 +1,616 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { JavaScriptExtractor } from '../src/parser/javascript/js-extractor'
3
+ import { JavaScriptParser } from '../src/parser/javascript/js-parser'
4
+ import { JavaScriptResolver } from '../src/parser/javascript/js-resolver'
5
+ import { getParser } from '../src/parser/index'
6
+
7
+ // ─── Sample JS source files ───────────────────────────────────────────────────
8
+
9
+ /** Plain CommonJS module — require + module.exports */
10
+ const CJS_MODULE = `
11
+ 'use strict'
12
+
13
+ const crypto = require('crypto')
14
+ const bcrypt = require('bcryptjs')
15
+ const { sign, verify } = require('jsonwebtoken')
16
+ const db = require('./db')
17
+
18
+ /**
19
+ * Hash a plain-text password using bcrypt.
20
+ */
21
+ function hashPassword(password) {
22
+ if (!password) throw new Error('password required')
23
+ return bcrypt.hash(password, 10)
24
+ }
25
+
26
+ /**
27
+ * Verify a JWT token and return the decoded payload.
28
+ */
29
+ const verifyToken = function verifyJwt(token, secret) {
30
+ if (!token) return null
31
+ try {
32
+ return verify(token, secret)
33
+ } catch {
34
+ return null
35
+ }
36
+ }
37
+
38
+ async function getUser(id) {
39
+ if (!id) throw new Error('id required')
40
+ return db.findById(id)
41
+ }
42
+
43
+ module.exports = { hashPassword, verifyToken, getUser }
44
+ `
45
+
46
+ /** ESM module */
47
+ const ESM_MODULE = `
48
+ import path from 'path'
49
+ import { readFile } from 'fs/promises'
50
+ import { formatDate } from './utils/dates.js'
51
+
52
+ export async function loadConfig(configPath) {
53
+ const raw = await readFile(path.resolve(configPath), 'utf-8')
54
+ return JSON.parse(raw)
55
+ }
56
+
57
+ export const formatTimestamp = (ts) => formatDate(new Date(ts))
58
+
59
+ export default function bootstrap(opts = {}) {
60
+ return { ...opts, started: true }
61
+ }
62
+ `
63
+
64
+ /** JSX component file */
65
+ const JSX_COMPONENT = `
66
+ import React from 'react'
67
+
68
+ // UserCard component — displays user info
69
+ function UserCard({ user, onEdit }) {
70
+ if (!user) return null
71
+ return (
72
+ <div className="card">
73
+ <h2>{user.name}</h2>
74
+ </div>
75
+ )
76
+ }
77
+
78
+ const Avatar = ({ src, alt = 'avatar' }) => (
79
+ <img src={src} alt={alt} />
80
+ )
81
+
82
+ export { UserCard, Avatar }
83
+ `
84
+
85
+ /** module.exports = function patterns */
86
+ const MODULE_EXPORTS_FN = `
87
+ /**
88
+ * Handle HTTP login request.
89
+ */
90
+ module.exports = function handleLogin(req, res) {
91
+ if (!req.body.email) {
92
+ return res.status(400).json({ error: 'email required' })
93
+ }
94
+ res.json({ ok: true })
95
+ }
96
+ `
97
+
98
+ /** exports.x = function patterns */
99
+ const EXPORTS_DOT_X = `
100
+ exports.createUser = function(data) {
101
+ if (!data.name) throw new Error('name required')
102
+ return { id: Date.now(), ...data }
103
+ }
104
+
105
+ exports.deleteUser = async (id) => {
106
+ if (!id) throw new Error('id required')
107
+ return true
108
+ }
109
+ `
110
+
111
+ /** module.exports = object with functions */
112
+ const MODULE_EXPORTS_OBJ = `
113
+ function internalHelper(x) { return x * 2 }
114
+
115
+ module.exports = {
116
+ double: internalHelper,
117
+ triple: function(x) { return x * 3 },
118
+ square: (x) => x * x,
119
+ }
120
+ `
121
+
122
+ /** Express route definitions */
123
+ const EXPRESS_ROUTES = `
124
+ const express = require('express')
125
+ const router = express.Router()
126
+
127
+ const { getUser, createUser, deleteUser } = require('./controllers/users')
128
+ const authMiddleware = require('./middleware/auth')
129
+
130
+ router.get('/users', getUser)
131
+ router.post('/users', authMiddleware, createUser)
132
+ router.delete('/users/:id', authMiddleware, deleteUser)
133
+
134
+ module.exports = router
135
+ `
136
+
137
+ /** Edge cases */
138
+ const EDGE_CASES = `
139
+ // Dynamic require with a variable — should NOT be captured as a static import
140
+ const dynamic = require(someVariable)
141
+
142
+ // require.resolve — should NOT be captured (it's a property access on the require object)
143
+ const resolved = require.resolve('./module')
144
+
145
+ // Conditional require
146
+ const isNode = typeof window === 'undefined'
147
+ const platform = isNode ? require('node:os') : null
148
+
149
+ // Nested function in module.exports
150
+ module.exports = {
151
+ outer: function(x) {
152
+ function inner(y) { return y + 1 }
153
+ return inner(x)
154
+ }
155
+ }
156
+
157
+ // module.exports spread — graceful no-crash
158
+ const base = {}
159
+ module.exports = { ...base, extra: 1 }
160
+ `
161
+
162
+ /** Empty file */
163
+ const EMPTY_FILE = ``
164
+
165
+ /** Comments and whitespace only */
166
+ const COMMENTS_ONLY = `
167
+ // This file is intentionally left blank
168
+ /* Another comment block */
169
+ `
170
+
171
+ /** Mixed ESM + CJS (unusual, but Babel-transpiled code can look like this) */
172
+ const MIXED_ESM_CJS = `
173
+ import defaultExport from './base.js'
174
+
175
+ const extra = require('./extra')
176
+
177
+ export function combined() {
178
+ return defaultExport()
179
+ }
180
+
181
+ module.exports.legacy = function() {}
182
+ `
183
+
184
+ // ─── Tests ────────────────────────────────────────────────────────────────────
185
+
186
+ describe('JavaScriptExtractor', () => {
187
+
188
+ describe('CommonJS require() imports', () => {
189
+ const ext = new JavaScriptExtractor('src/auth.js', CJS_MODULE)
190
+
191
+ test('extracts plain require() as default import', () => {
192
+ const imports = ext.extractImports()
193
+ const crypto = imports.find(i => i.source === 'crypto')
194
+ expect(crypto).toBeDefined()
195
+ expect(crypto!.isDefault).toBe(true)
196
+ expect(crypto!.isDynamic).toBe(false)
197
+ })
198
+
199
+ test('extracts destructured require() as named imports', () => {
200
+ const imports = ext.extractImports()
201
+ const jwt = imports.find(i => i.source === 'jsonwebtoken')
202
+ expect(jwt).toBeDefined()
203
+ expect(jwt!.names).toContain('sign')
204
+ expect(jwt!.names).toContain('verify')
205
+ })
206
+
207
+ test('extracts relative require(./db)', () => {
208
+ const imports = ext.extractImports()
209
+ const dbImp = imports.find(i => i.source === './db')
210
+ expect(dbImp).toBeDefined()
211
+ expect(dbImp!.names).toContain('db')
212
+ })
213
+
214
+ test('does NOT capture require(variable) — dynamic require skipped', () => {
215
+ const edgeExt = new JavaScriptExtractor('src/edge.js', EDGE_CASES)
216
+ const imports = edgeExt.extractImports()
217
+ // someVariable is not a StringLiteral — must not appear
218
+ const bad = imports.find(i => i.source === '' || i.source === 'someVariable')
219
+ expect(bad).toBeUndefined()
220
+ })
221
+
222
+ test('does NOT capture require.resolve() as an import', () => {
223
+ const edgeExt = new JavaScriptExtractor('src/edge.js', EDGE_CASES)
224
+ const imports = edgeExt.extractImports()
225
+ // require.resolve('./module') — node.expression is a PropertyAccessExpression,
226
+ // not an Identifier, so it must NOT be captured
227
+ const bad = imports.find(i => i.source === './module')
228
+ expect(bad).toBeUndefined()
229
+ })
230
+ })
231
+
232
+ describe('CommonJS module.exports = { } exports', () => {
233
+ const ext = new JavaScriptExtractor('src/auth.js', CJS_MODULE)
234
+
235
+ test('module.exports = { foo, bar } marks names as exports', () => {
236
+ const exports = ext.extractExports()
237
+ const names = exports.map(e => e.name)
238
+ expect(names).toContain('hashPassword')
239
+ expect(names).toContain('verifyToken')
240
+ expect(names).toContain('getUser')
241
+ })
242
+ })
243
+
244
+ describe('module.exports = function pattern', () => {
245
+ const ext = new JavaScriptExtractor('src/login.js', MODULE_EXPORTS_FN)
246
+
247
+ test('extracts named function expression from module.exports', () => {
248
+ const fns = ext.extractFunctions()
249
+ const fn = fns.find(f => f.name === 'handleLogin')
250
+ expect(fn).toBeDefined()
251
+ expect(fn!.isExported).toBe(true)
252
+ })
253
+
254
+ test('extracted function has correct file and purpose', () => {
255
+ const fns = ext.extractFunctions()
256
+ const fn = fns.find(f => f.name === 'handleLogin')
257
+ expect(fn!.file).toBe('src/login.js')
258
+ expect(fn!.purpose).toMatch(/handle http login/i)
259
+ })
260
+
261
+ test('extracts params from module.exports function', () => {
262
+ const fns = ext.extractFunctions()
263
+ const fn = fns.find(f => f.name === 'handleLogin')
264
+ expect(fn!.params.map(p => p.name)).toEqual(['req', 'res'])
265
+ })
266
+
267
+ test('detects edge cases (early return guard)', () => {
268
+ const fns = ext.extractFunctions()
269
+ const fn = fns.find(f => f.name === 'handleLogin')
270
+ expect(fn!.edgeCasesHandled.length).toBeGreaterThan(0)
271
+ })
272
+ })
273
+
274
+ describe('exports.x = function pattern', () => {
275
+ const ext = new JavaScriptExtractor('src/users.js', EXPORTS_DOT_X)
276
+
277
+ test('extracts function assigned to exports.createUser', () => {
278
+ const fns = ext.extractFunctions()
279
+ const fn = fns.find(f => f.name === 'createUser')
280
+ expect(fn).toBeDefined()
281
+ expect(fn!.isExported).toBe(true)
282
+ })
283
+
284
+ test('extracts async arrow function assigned to exports.deleteUser', () => {
285
+ const fns = ext.extractFunctions()
286
+ const fn = fns.find(f => f.name === 'deleteUser')
287
+ expect(fn).toBeDefined()
288
+ expect(fn!.isAsync).toBe(true)
289
+ })
290
+
291
+ test('exports.x appears in extractExports()', () => {
292
+ const exports = ext.extractExports()
293
+ const names = exports.map(e => e.name)
294
+ expect(names).toContain('createUser')
295
+ expect(names).toContain('deleteUser')
296
+ })
297
+
298
+ test('detects throw as error handling', () => {
299
+ const fns = ext.extractFunctions()
300
+ const fn = fns.find(f => f.name === 'createUser')
301
+ expect(fn!.errorHandling.some(e => e.type === 'throw')).toBe(true)
302
+ })
303
+ })
304
+
305
+ describe('module.exports = { inline functions }', () => {
306
+ test('exports names from object literal', () => {
307
+ const ext = new JavaScriptExtractor('src/math.js', MODULE_EXPORTS_OBJ)
308
+ const exports = ext.extractExports()
309
+ const names = exports.map(e => e.name)
310
+ expect(names).toContain('double')
311
+ expect(names).toContain('triple')
312
+ expect(names).toContain('square')
313
+ })
314
+ })
315
+
316
+ describe('ESM imports and exports', () => {
317
+ const ext = new JavaScriptExtractor('src/loader.js', ESM_MODULE)
318
+
319
+ test('extracts static ESM imports', () => {
320
+ const imports = ext.extractImports()
321
+ const pathImp = imports.find(i => i.source === 'path')
322
+ expect(pathImp).toBeDefined()
323
+ const fsImp = imports.find(i => i.source === 'fs/promises')
324
+ expect(fsImp).toBeDefined()
325
+ expect(fsImp!.names).toContain('readFile')
326
+ })
327
+
328
+ test('extracts named ESM function export', () => {
329
+ const fns = ext.extractFunctions()
330
+ const fn = fns.find(f => f.name === 'loadConfig')
331
+ expect(fn).toBeDefined()
332
+ expect(fn!.isExported).toBe(true)
333
+ expect(fn!.isAsync).toBe(true)
334
+ })
335
+
336
+ test('extracts arrow function export', () => {
337
+ const fns = ext.extractFunctions()
338
+ const fn = fns.find(f => f.name === 'formatTimestamp')
339
+ expect(fn).toBeDefined()
340
+ expect(fn!.isExported).toBe(true)
341
+ })
342
+
343
+ test('extracts export default function', () => {
344
+ const fns = ext.extractFunctions()
345
+ const fn = fns.find(f => f.name === 'bootstrap')
346
+ expect(fn).toBeDefined()
347
+ })
348
+ })
349
+
350
+ describe('JSX components', () => {
351
+ const ext = new JavaScriptExtractor('src/UserCard.jsx', JSX_COMPONENT)
352
+
353
+ test('parses JSX without crashing', () => {
354
+ expect(() => ext.extractFunctions()).not.toThrow()
355
+ })
356
+
357
+ test('extracts function component', () => {
358
+ const fns = ext.extractFunctions()
359
+ const fn = fns.find(f => f.name === 'UserCard')
360
+ expect(fn).toBeDefined()
361
+ expect(fn!.params[0].name).toBe('{ user, onEdit }')
362
+ })
363
+
364
+ test('extracts arrow function component', () => {
365
+ const fns = ext.extractFunctions()
366
+ const fn = fns.find(f => f.name === 'Avatar')
367
+ expect(fn).toBeDefined()
368
+ })
369
+
370
+ test('extracts purpose from comment above JSX component', () => {
371
+ const fns = ext.extractFunctions()
372
+ const fn = fns.find(f => f.name === 'UserCard')
373
+ expect(fn!.purpose).toMatch(/user.*card/i)
374
+ })
375
+
376
+ test('detects early-return edge case (if !user return null)', () => {
377
+ const fns = ext.extractFunctions()
378
+ const fn = fns.find(f => f.name === 'UserCard')
379
+ expect(fn!.edgeCasesHandled.length).toBeGreaterThan(0)
380
+ })
381
+ })
382
+
383
+ describe('async functions', () => {
384
+ test('marks async functions correctly', () => {
385
+ const ext = new JavaScriptExtractor('src/auth.js', CJS_MODULE)
386
+ const fns = ext.extractFunctions()
387
+ const fn = fns.find(f => f.name === 'getUser')
388
+ expect(fn).toBeDefined()
389
+ expect(fn!.isAsync).toBe(true)
390
+ })
391
+ })
392
+
393
+ describe('Express route detection', () => {
394
+ const ext = new JavaScriptExtractor('src/routes.js', EXPRESS_ROUTES)
395
+
396
+ test('detects GET route', () => {
397
+ const routes = ext.extractRoutes()
398
+ const get = routes.find(r => r.method === 'GET' && r.path === '/users')
399
+ expect(get).toBeDefined()
400
+ expect(get!.handler).toBe('getUser')
401
+ })
402
+
403
+ test('detects POST route with middleware', () => {
404
+ const routes = ext.extractRoutes()
405
+ const post = routes.find(r => r.method === 'POST' && r.path === '/users')
406
+ expect(post).toBeDefined()
407
+ expect(post!.middlewares).toContain('authMiddleware')
408
+ expect(post!.handler).toBe('createUser')
409
+ })
410
+
411
+ test('detects DELETE route', () => {
412
+ const routes = ext.extractRoutes()
413
+ const del = routes.find(r => r.method === 'DELETE')
414
+ expect(del).toBeDefined()
415
+ expect(del!.path).toBe('/users/:id')
416
+ })
417
+ })
418
+
419
+ describe('edge cases', () => {
420
+ test('empty file parses without error and returns empty arrays', () => {
421
+ const ext = new JavaScriptExtractor('src/empty.js', EMPTY_FILE)
422
+ expect(ext.extractFunctions()).toEqual([])
423
+ expect(ext.extractImports()).toEqual([])
424
+ expect(ext.extractExports()).toEqual([])
425
+ })
426
+
427
+ test('comments-only file parses without error', () => {
428
+ const ext = new JavaScriptExtractor('src/empty.js', COMMENTS_ONLY)
429
+ expect(() => ext.extractFunctions()).not.toThrow()
430
+ expect(ext.extractFunctions()).toEqual([])
431
+ })
432
+
433
+ test('module.exports spread literal does not crash', () => {
434
+ const ext = new JavaScriptExtractor('src/edge.js', EDGE_CASES)
435
+ expect(() => ext.extractExports()).not.toThrow()
436
+ })
437
+
438
+ test('mixed ESM + CJS file captures both import and require', () => {
439
+ const ext = new JavaScriptExtractor('src/mixed.js', MIXED_ESM_CJS)
440
+ const imports = ext.extractImports()
441
+ const esmImp = imports.find(i => i.source === './base.js')
442
+ const cjsImp = imports.find(i => i.source === './extra')
443
+ expect(esmImp).toBeDefined()
444
+ expect(cjsImp).toBeDefined()
445
+ })
446
+
447
+ test('mixed ESM + CJS: captures both ESM and CJS exports', () => {
448
+ const ext = new JavaScriptExtractor('src/mixed.js', MIXED_ESM_CJS)
449
+ const exports = ext.extractExports()
450
+ const names = exports.map(e => e.name)
451
+ expect(names).toContain('combined') // ESM export
452
+ expect(names).toContain('legacy') // exports.legacy = function()
453
+ })
454
+
455
+ test('no duplicate imports when same source appears in both ESM and CJS', () => {
456
+ // Unlikely but guard: same source in require and import
457
+ const src = `import x from './foo'; const y = require('./foo')`
458
+ const ext = new JavaScriptExtractor('src/dup.js', src)
459
+ const imports = ext.extractImports()
460
+ const fooImports = imports.filter(i => i.source === './foo')
461
+ expect(fooImports.length).toBe(1)
462
+ })
463
+
464
+ test('no duplicate exports when CJS and ESM both declare same name', () => {
465
+ const src = `export function greet() {} \nexports.greet = function() {}`
466
+ const ext = new JavaScriptExtractor('src/dup.js', src)
467
+ const exports = ext.extractExports()
468
+ const greetExports = exports.filter(e => e.name === 'greet')
469
+ expect(greetExports.length).toBe(1)
470
+ })
471
+ })
472
+ })
473
+
474
+ describe('JavaScriptParser', () => {
475
+ const parser = new JavaScriptParser()
476
+
477
+ test('getSupportedExtensions includes .js .mjs .cjs .jsx', () => {
478
+ const exts = parser.getSupportedExtensions()
479
+ expect(exts).toContain('.js')
480
+ expect(exts).toContain('.mjs')
481
+ expect(exts).toContain('.cjs')
482
+ expect(exts).toContain('.jsx')
483
+ })
484
+
485
+ test('parse returns language: javascript', () => {
486
+ const result = parser.parse('src/index.js', CJS_MODULE)
487
+ expect(result.language).toBe('javascript')
488
+ })
489
+
490
+ test('parse includes hash and parsedAt', () => {
491
+ const result = parser.parse('src/index.js', CJS_MODULE)
492
+ expect(typeof result.hash).toBe('string')
493
+ expect(result.hash.length).toBe(64) // SHA-256 hex
494
+ expect(typeof result.parsedAt).toBe('number')
495
+ })
496
+
497
+ test('CJS-exported functions are marked isExported via cross-reference', () => {
498
+ const result = parser.parse('src/auth.js', CJS_MODULE)
499
+ const hashPw = result.functions.find(f => f.name === 'hashPassword')
500
+ expect(hashPw).toBeDefined()
501
+ expect(hashPw!.isExported).toBe(true)
502
+ })
503
+
504
+ test('resolveImports resolves relative paths with .js extension probing', () => {
505
+ const files = [
506
+ parser.parse('src/auth.js', CJS_MODULE),
507
+ parser.parse('src/loader.js', ESM_MODULE),
508
+ ]
509
+ const resolved = parser.resolveImports(files, '/project')
510
+ const authFile = resolved.find(f => f.path === 'src/auth.js')!
511
+ const dbImport = authFile.imports.find(i => i.source === './db')
512
+ expect(dbImport!.resolvedPath).toMatch(/src\/db/)
513
+ expect(dbImport!.resolvedPath).toMatch(/\.js$/)
514
+ })
515
+
516
+ test('resolveImports leaves external packages unresolved (empty resolvedPath)', () => {
517
+ const files = [parser.parse('src/auth.js', CJS_MODULE)]
518
+ const resolved = parser.resolveImports(files, '/project')
519
+ const file = resolved[0]
520
+ const cryptoImp = file.imports.find(i => i.source === 'crypto')
521
+ expect(cryptoImp!.resolvedPath).toBe('')
522
+ })
523
+
524
+ test('parse .jsx file language is javascript', () => {
525
+ const result = parser.parse('src/UserCard.jsx', JSX_COMPONENT)
526
+ expect(result.language).toBe('javascript')
527
+ })
528
+ })
529
+
530
+ describe('JavaScriptResolver', () => {
531
+ const resolver = new JavaScriptResolver('/project')
532
+
533
+ test('resolves relative import with .js extension', () => {
534
+ const imp = { source: './utils', resolvedPath: '', names: [], isDefault: true, isDynamic: false }
535
+ const result = resolver.resolve(imp, 'src/index.js', ['src/utils.js'])
536
+ expect(result.resolvedPath).toBe('src/utils.js')
537
+ })
538
+
539
+ test('resolves relative import with /index.js fallback', () => {
540
+ const imp = { source: './utils', resolvedPath: '', names: [], isDefault: true, isDynamic: false }
541
+ const result = resolver.resolve(imp, 'src/index.js', ['src/utils/index.js'])
542
+ expect(result.resolvedPath).toBe('src/utils/index.js')
543
+ })
544
+
545
+ test('falls back to .ts for mixed TS/JS project', () => {
546
+ const imp = { source: './shared', resolvedPath: '', names: [], isDefault: true, isDynamic: false }
547
+ const result = resolver.resolve(imp, 'src/index.js', ['src/shared.ts'])
548
+ expect(result.resolvedPath).toBe('src/shared.ts')
549
+ })
550
+
551
+ test('leaves external packages unresolved', () => {
552
+ const imp = { source: 'lodash', resolvedPath: '', names: [], isDefault: true, isDynamic: false }
553
+ const result = resolver.resolve(imp, 'src/index.js')
554
+ expect(result.resolvedPath).toBe('')
555
+ })
556
+
557
+ test('resolves with no known files list (defaults to .js suffix)', () => {
558
+ const imp = { source: './foo', resolvedPath: '', names: [], isDefault: true, isDynamic: false }
559
+ const result = resolver.resolve(imp, 'src/index.js', [])
560
+ expect(result.resolvedPath).toMatch(/foo\.js$/)
561
+ })
562
+
563
+ test('source with existing .js extension returned as-is', () => {
564
+ const imp = { source: './utils.js', resolvedPath: '', names: [], isDefault: true, isDynamic: false }
565
+ const result = resolver.resolve(imp, 'src/index.js', ['src/utils.js'])
566
+ expect(result.resolvedPath).toBe('src/utils.js')
567
+ })
568
+
569
+ test('resolves path alias when aliases provided', () => {
570
+ const resolver2 = new JavaScriptResolver('/project', { '@/*': ['src/*'] })
571
+ const imp = { source: '@/utils', resolvedPath: '', names: [], isDefault: false, isDynamic: false }
572
+ const result = resolver2.resolve(imp, 'src/components/Button.js', ['src/utils.js'])
573
+ expect(result.resolvedPath).toBe('src/utils.js')
574
+ })
575
+
576
+ test('resolveAll resolves all imports in a list', () => {
577
+ const imports = [
578
+ { source: './a', resolvedPath: '', names: [], isDefault: false, isDynamic: false },
579
+ { source: 'lodash', resolvedPath: '', names: [], isDefault: false, isDynamic: false },
580
+ ]
581
+ const results = resolver.resolveAll(imports, 'src/index.js', ['src/a.js'])
582
+ expect(results[0].resolvedPath).toBe('src/a.js')
583
+ expect(results[1].resolvedPath).toBe('')
584
+ })
585
+ })
586
+
587
+ describe('getParser — JS extensions', () => {
588
+ test('returns JavaScriptParser for .js', () => {
589
+ const p = getParser('src/index.js')
590
+ expect(p.getSupportedExtensions()).toContain('.js')
591
+ })
592
+
593
+ test('returns JavaScriptParser for .mjs', () => {
594
+ const p = getParser('src/index.mjs')
595
+ expect(p.getSupportedExtensions()).toContain('.mjs')
596
+ })
597
+
598
+ test('returns JavaScriptParser for .cjs', () => {
599
+ const p = getParser('src/index.cjs')
600
+ expect(p.getSupportedExtensions()).toContain('.cjs')
601
+ })
602
+
603
+ test('returns JavaScriptParser for .jsx', () => {
604
+ const p = getParser('src/App.jsx')
605
+ expect(p.getSupportedExtensions()).toContain('.jsx')
606
+ })
607
+
608
+ test('still returns TypeScriptParser for .ts', () => {
609
+ const p = getParser('src/index.ts')
610
+ expect(p.getSupportedExtensions()).toContain('.ts')
611
+ })
612
+
613
+ test('still throws UnsupportedLanguageError for .py', () => {
614
+ expect(() => getParser('src/app.py')).toThrow()
615
+ })
616
+ })