preflight-mcp 0.1.2 → 0.1.3
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/README.md +35 -142
- package/README.zh-CN.md +141 -124
- package/dist/ast/treeSitter.js +588 -0
- package/dist/bundle/analysis.js +47 -0
- package/dist/bundle/context7.js +65 -36
- package/dist/bundle/facts.js +829 -0
- package/dist/bundle/githubArchive.js +49 -28
- package/dist/bundle/overview.js +226 -48
- package/dist/bundle/service.js +27 -126
- package/dist/config.js +29 -3
- package/dist/context7/client.js +5 -2
- package/dist/evidence/dependencyGraph.js +826 -0
- package/dist/http/server.js +109 -0
- package/dist/search/sqliteFts.js +150 -10
- package/dist/server.js +84 -295
- package/dist/trace/service.js +108 -0
- package/dist/trace/store.js +170 -0
- package/package.json +4 -2
- package/dist/bundle/deepwiki.js +0 -206
package/dist/bundle/facts.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { extractModuleSyntaxWasm } from '../ast/treeSitter.js';
|
|
3
4
|
/**
|
|
4
5
|
* Detect programming languages from file extensions
|
|
5
6
|
*/
|
|
@@ -265,6 +266,15 @@ export async function extractBundleFacts(params) {
|
|
|
265
266
|
const dependencies = await extractDependencies(allFiles, params.bundleRoot);
|
|
266
267
|
const fileStructure = analyzeFileStructure(allFiles);
|
|
267
268
|
const frameworks = detectFrameworks(dependencies, allFiles);
|
|
269
|
+
// Phase 2: Module analysis (optional, more expensive)
|
|
270
|
+
let modules;
|
|
271
|
+
let patterns;
|
|
272
|
+
let techStack;
|
|
273
|
+
if (params.enablePhase2) {
|
|
274
|
+
modules = await analyzeModules(allFiles);
|
|
275
|
+
patterns = detectArchitecturePatterns(allFiles, modules);
|
|
276
|
+
techStack = analyzeTechStack(languages, dependencies, frameworks);
|
|
277
|
+
}
|
|
268
278
|
return {
|
|
269
279
|
version: '1.0',
|
|
270
280
|
timestamp: new Date().toISOString(),
|
|
@@ -273,6 +283,9 @@ export async function extractBundleFacts(params) {
|
|
|
273
283
|
dependencies,
|
|
274
284
|
fileStructure,
|
|
275
285
|
frameworks,
|
|
286
|
+
modules,
|
|
287
|
+
patterns,
|
|
288
|
+
techStack,
|
|
276
289
|
};
|
|
277
290
|
}
|
|
278
291
|
/**
|
|
@@ -294,3 +307,819 @@ export async function readFacts(factsPath) {
|
|
|
294
307
|
return null;
|
|
295
308
|
}
|
|
296
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Phase 2: Extract exports from a code file using regex
|
|
312
|
+
*/
|
|
313
|
+
function extractExports(content, filePath) {
|
|
314
|
+
const exports = [];
|
|
315
|
+
const lines = content.split('\n');
|
|
316
|
+
// Detect file language
|
|
317
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
318
|
+
const isTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext);
|
|
319
|
+
const isPython = ext === '.py';
|
|
320
|
+
const isGo = ext === '.go';
|
|
321
|
+
if (isTS) {
|
|
322
|
+
// TypeScript/JavaScript export patterns
|
|
323
|
+
for (const line of lines) {
|
|
324
|
+
// export function/class/const/let/var/type/interface
|
|
325
|
+
const match1 = line.match(/^\s*export\s+(?:async\s+)?(?:function|class|const|let|var|type|interface|enum)\s+([a-zA-Z_$][\w$]*)/);
|
|
326
|
+
if (match1?.[1]) {
|
|
327
|
+
exports.push(match1[1]);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
// export { xxx, yyy }
|
|
331
|
+
const match2 = line.match(/^\s*export\s*\{\s*([^}]+)\s*\}/);
|
|
332
|
+
if (match2?.[1]) {
|
|
333
|
+
const names = match2[1].split(',').map(n => {
|
|
334
|
+
const parts = n.trim().split(/\s+as\s+/);
|
|
335
|
+
return parts[parts.length - 1]?.trim() || '';
|
|
336
|
+
}).filter(Boolean);
|
|
337
|
+
exports.push(...names);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
// export default
|
|
341
|
+
if (line.match(/^\s*export\s+default\s+/)) {
|
|
342
|
+
exports.push('default');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
else if (isPython) {
|
|
347
|
+
// Python: __all__ = [...]
|
|
348
|
+
const allMatch = content.match(/__all__\s*=\s*\[([^\]]+)\]/);
|
|
349
|
+
if (allMatch?.[1]) {
|
|
350
|
+
const names = allMatch[1].split(',').map(n => n.trim().replace(/["']/g, '')).filter(Boolean);
|
|
351
|
+
exports.push(...names);
|
|
352
|
+
}
|
|
353
|
+
// Top-level functions and classes (heuristic)
|
|
354
|
+
for (const line of lines) {
|
|
355
|
+
const funcMatch = line.match(/^def\s+([a-zA-Z_][\w]*)/);
|
|
356
|
+
if (funcMatch?.[1] && !funcMatch[1].startsWith('_')) {
|
|
357
|
+
exports.push(funcMatch[1]);
|
|
358
|
+
}
|
|
359
|
+
const classMatch = line.match(/^class\s+([a-zA-Z_][\w]*)/);
|
|
360
|
+
if (classMatch?.[1] && !classMatch[1].startsWith('_')) {
|
|
361
|
+
exports.push(classMatch[1]);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else if (isGo) {
|
|
366
|
+
// Go: public functions/types (start with uppercase)
|
|
367
|
+
for (const line of lines) {
|
|
368
|
+
const funcMatch = line.match(/^func\s+([A-Z][\w]*)/);
|
|
369
|
+
if (funcMatch?.[1]) {
|
|
370
|
+
exports.push(funcMatch[1]);
|
|
371
|
+
}
|
|
372
|
+
const typeMatch = line.match(/^type\s+([A-Z][\w]*)/);
|
|
373
|
+
if (typeMatch?.[1]) {
|
|
374
|
+
exports.push(typeMatch[1]);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return [...new Set(exports)]; // Remove duplicates
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Phase 2: Extract imports from a code file using regex
|
|
382
|
+
*/
|
|
383
|
+
function extractImports(content, filePath) {
|
|
384
|
+
const imports = [];
|
|
385
|
+
const lines = content.split('\n');
|
|
386
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
387
|
+
const isTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext);
|
|
388
|
+
const isPython = ext === '.py';
|
|
389
|
+
const isGo = ext === '.go';
|
|
390
|
+
if (isTS) {
|
|
391
|
+
// import ... from 'xxx'
|
|
392
|
+
for (const line of lines) {
|
|
393
|
+
const match1 = line.match(/from\s+['"]([^'"]+)['"]/);
|
|
394
|
+
if (match1?.[1]) {
|
|
395
|
+
imports.push(match1[1]);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
// import 'xxx' or import('xxx')
|
|
399
|
+
const match2 = line.match(/import\s*\(?\s*['"]([^'"]+)['"]/);
|
|
400
|
+
if (match2?.[1]) {
|
|
401
|
+
imports.push(match2[1]);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
// require('xxx')
|
|
405
|
+
const match3 = line.match(/require\s*\(\s*['"]([^'"]+)['"]/);
|
|
406
|
+
if (match3?.[1]) {
|
|
407
|
+
imports.push(match3[1]);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else if (isPython) {
|
|
412
|
+
// import xxx or from xxx import
|
|
413
|
+
for (const line of lines) {
|
|
414
|
+
const match1 = line.match(/^\s*import\s+([a-zA-Z_][\w.]*)/);
|
|
415
|
+
if (match1?.[1]) {
|
|
416
|
+
imports.push(match1[1].split('.')[0]);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const match2 = line.match(/^\s*from\s+([a-zA-Z_][\w.]*)\s+import/);
|
|
420
|
+
if (match2?.[1]) {
|
|
421
|
+
imports.push(match2[1].split('.')[0]);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else if (isGo) {
|
|
426
|
+
// Go: import statements
|
|
427
|
+
const importBlock = content.match(/import\s*\(([^)]+)\)/);
|
|
428
|
+
if (importBlock?.[1]) {
|
|
429
|
+
const lines = importBlock[1].split('\n');
|
|
430
|
+
for (const line of lines) {
|
|
431
|
+
const match = line.match(/["']([^"']+)["']/);
|
|
432
|
+
if (match?.[1]) {
|
|
433
|
+
imports.push(match[1]);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Single import
|
|
438
|
+
for (const line of lines) {
|
|
439
|
+
const match = line.match(/^\s*import\s+["']([^"']+)["']/);
|
|
440
|
+
if (match?.[1]) {
|
|
441
|
+
imports.push(match[1]);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return [...new Set(imports)]; // Remove duplicates
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Phase 2: Determine module role based on path and usage
|
|
449
|
+
*/
|
|
450
|
+
function determineModuleRole(file, importedBy) {
|
|
451
|
+
const p = file.repoRelativePath.toLowerCase();
|
|
452
|
+
// Test files
|
|
453
|
+
if (p.includes('/test/') ||
|
|
454
|
+
p.includes('/tests/') ||
|
|
455
|
+
p.includes('/__tests__/') ||
|
|
456
|
+
p.includes('.test.') ||
|
|
457
|
+
p.includes('.spec.')) {
|
|
458
|
+
return 'test';
|
|
459
|
+
}
|
|
460
|
+
// Config files
|
|
461
|
+
if (p.includes('config') ||
|
|
462
|
+
p.endsWith('.config.ts') ||
|
|
463
|
+
p.endsWith('.config.js') ||
|
|
464
|
+
p.includes('/scripts/')) {
|
|
465
|
+
return 'config';
|
|
466
|
+
}
|
|
467
|
+
// Example files
|
|
468
|
+
if (p.includes('/example') || p.includes('/demo')) {
|
|
469
|
+
return 'example';
|
|
470
|
+
}
|
|
471
|
+
// Core: imported by multiple modules (2+)
|
|
472
|
+
if (importedBy.size >= 2) {
|
|
473
|
+
return 'core';
|
|
474
|
+
}
|
|
475
|
+
// Utility: in utils/helpers directory or imported by 1-2 modules
|
|
476
|
+
if (p.includes('/util') || p.includes('/helper') || importedBy.size > 0) {
|
|
477
|
+
return 'utility';
|
|
478
|
+
}
|
|
479
|
+
return 'unknown';
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Phase 2: Calculate module complexity
|
|
483
|
+
*/
|
|
484
|
+
function calculateComplexity(loc, importCount) {
|
|
485
|
+
// Simple heuristic based on LOC and import count
|
|
486
|
+
const score = loc / 100 + importCount / 5;
|
|
487
|
+
if (score < 2)
|
|
488
|
+
return 'low';
|
|
489
|
+
if (score < 5)
|
|
490
|
+
return 'medium';
|
|
491
|
+
return 'high';
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Phase 2: Analyze modules in the repository
|
|
495
|
+
*/
|
|
496
|
+
async function analyzeModules(files) {
|
|
497
|
+
const modules = [];
|
|
498
|
+
const eligibleExtensions = new Set([
|
|
499
|
+
'.ts',
|
|
500
|
+
'.tsx',
|
|
501
|
+
'.js',
|
|
502
|
+
'.jsx',
|
|
503
|
+
'.mjs',
|
|
504
|
+
'.cjs',
|
|
505
|
+
'.py',
|
|
506
|
+
'.go',
|
|
507
|
+
'.java',
|
|
508
|
+
'.rs',
|
|
509
|
+
]);
|
|
510
|
+
const fileKey = (f) => `${f.repoId}:${f.repoRelativePath}`;
|
|
511
|
+
const buildSuffixIndex = (keyByRelPath) => {
|
|
512
|
+
const index = new Map();
|
|
513
|
+
const add = (suffix, key) => {
|
|
514
|
+
const existing = index.get(suffix);
|
|
515
|
+
if (existing === undefined) {
|
|
516
|
+
index.set(suffix, key);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (existing !== key) {
|
|
520
|
+
index.set(suffix, null);
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
for (const [relPath, key] of keyByRelPath.entries()) {
|
|
524
|
+
const parts = relPath.split('/').filter(Boolean);
|
|
525
|
+
for (let i = 0; i < parts.length; i++) {
|
|
526
|
+
add(parts.slice(i).join('/'), key);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return index;
|
|
530
|
+
};
|
|
531
|
+
// Pre-pass: build per-repo lookup tables so we can resolve local imports deterministically.
|
|
532
|
+
const repoIndexes = new Map();
|
|
533
|
+
for (const file of files) {
|
|
534
|
+
if (file.kind !== 'code')
|
|
535
|
+
continue;
|
|
536
|
+
const ext = path.extname(file.repoRelativePath).toLowerCase();
|
|
537
|
+
if (!eligibleExtensions.has(ext))
|
|
538
|
+
continue;
|
|
539
|
+
let idx = repoIndexes.get(file.repoId);
|
|
540
|
+
if (!idx) {
|
|
541
|
+
idx = {
|
|
542
|
+
keyByRelPath: new Map(),
|
|
543
|
+
suffixIndex: new Map(),
|
|
544
|
+
goModules: [],
|
|
545
|
+
goRepByDir: new Map(),
|
|
546
|
+
rustCrateRootDirs: [],
|
|
547
|
+
rustCrateRootFiles: new Set(),
|
|
548
|
+
};
|
|
549
|
+
repoIndexes.set(file.repoId, idx);
|
|
550
|
+
}
|
|
551
|
+
idx.keyByRelPath.set(file.repoRelativePath, fileKey(file));
|
|
552
|
+
}
|
|
553
|
+
for (const idx of repoIndexes.values()) {
|
|
554
|
+
idx.suffixIndex = buildSuffixIndex(idx.keyByRelPath);
|
|
555
|
+
}
|
|
556
|
+
const normalizeDir = (d) => (d === '.' ? '' : d);
|
|
557
|
+
// Go: build package directory representative map + parse go.mod module paths.
|
|
558
|
+
const goModFilesByRepo = new Map();
|
|
559
|
+
for (const file of files) {
|
|
560
|
+
if (file.repoRelativePath === 'go.mod' || file.repoRelativePath.endsWith('/go.mod')) {
|
|
561
|
+
const list = goModFilesByRepo.get(file.repoId) ?? [];
|
|
562
|
+
list.push(file);
|
|
563
|
+
goModFilesByRepo.set(file.repoId, list);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const parseGoModulePath = (content) => {
|
|
567
|
+
for (const line of content.split('\n')) {
|
|
568
|
+
const t = line.trim();
|
|
569
|
+
if (!t || t.startsWith('//'))
|
|
570
|
+
continue;
|
|
571
|
+
const m = t.match(/^module\s+(\S+)/);
|
|
572
|
+
if (m?.[1])
|
|
573
|
+
return m[1];
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
};
|
|
577
|
+
for (const [repoId, idx] of Array.from(repoIndexes.entries()).sort(([a], [b]) => a.localeCompare(b))) {
|
|
578
|
+
// go package dir -> representative file
|
|
579
|
+
const goFilesByDir = new Map();
|
|
580
|
+
for (const relPath of idx.keyByRelPath.keys()) {
|
|
581
|
+
if (!relPath.endsWith('.go'))
|
|
582
|
+
continue;
|
|
583
|
+
const dir = normalizeDir(path.posix.dirname(relPath));
|
|
584
|
+
const list = goFilesByDir.get(dir) ?? [];
|
|
585
|
+
list.push(relPath);
|
|
586
|
+
goFilesByDir.set(dir, list);
|
|
587
|
+
}
|
|
588
|
+
for (const [dir, relPaths] of goFilesByDir.entries()) {
|
|
589
|
+
relPaths.sort();
|
|
590
|
+
const preferred = relPaths.find((p) => !p.endsWith('_test.go')) ?? relPaths[0];
|
|
591
|
+
if (preferred) {
|
|
592
|
+
const key = idx.keyByRelPath.get(preferred);
|
|
593
|
+
if (key)
|
|
594
|
+
idx.goRepByDir.set(dir, key);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// go.mod module path(s)
|
|
598
|
+
const goMods = (goModFilesByRepo.get(repoId) ?? []).slice().sort((a, b) => a.repoRelativePath.localeCompare(b.repoRelativePath));
|
|
599
|
+
for (const goMod of goMods) {
|
|
600
|
+
try {
|
|
601
|
+
const raw = await fs.readFile(goMod.bundleNormAbsPath, 'utf8');
|
|
602
|
+
const content = raw.replace(/\r\n/g, '\n');
|
|
603
|
+
const modulePath = parseGoModulePath(content);
|
|
604
|
+
if (!modulePath)
|
|
605
|
+
continue;
|
|
606
|
+
const moduleRootDir = normalizeDir(path.posix.dirname(goMod.repoRelativePath));
|
|
607
|
+
idx.goModules.push({ moduleRootDir, modulePath });
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
// ignore
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
idx.goModules.sort((a, b) => {
|
|
614
|
+
const len = b.moduleRootDir.length - a.moduleRootDir.length;
|
|
615
|
+
if (len !== 0)
|
|
616
|
+
return len;
|
|
617
|
+
return a.moduleRootDir.localeCompare(b.moduleRootDir) || a.modulePath.localeCompare(b.modulePath);
|
|
618
|
+
});
|
|
619
|
+
// Rust: detect crate roots (lib/main + bin/examples/benches/tests entrypoints)
|
|
620
|
+
const crateRootDirs = new Set();
|
|
621
|
+
const isCrateRootFile = (relPath) => {
|
|
622
|
+
if (!relPath.endsWith('.rs'))
|
|
623
|
+
return false;
|
|
624
|
+
const base = path.posix.basename(relPath);
|
|
625
|
+
if (base === 'lib.rs' || base === 'main.rs')
|
|
626
|
+
return true;
|
|
627
|
+
if (base === 'mod.rs')
|
|
628
|
+
return false;
|
|
629
|
+
const dir = path.posix.dirname(relPath);
|
|
630
|
+
const isEntryDir = dir === 'src/bin' ||
|
|
631
|
+
dir.endsWith('/src/bin') ||
|
|
632
|
+
dir === 'examples' ||
|
|
633
|
+
dir.endsWith('/examples') ||
|
|
634
|
+
dir === 'benches' ||
|
|
635
|
+
dir.endsWith('/benches') ||
|
|
636
|
+
dir === 'tests' ||
|
|
637
|
+
dir.endsWith('/tests');
|
|
638
|
+
return isEntryDir;
|
|
639
|
+
};
|
|
640
|
+
for (const relPath of idx.keyByRelPath.keys()) {
|
|
641
|
+
if (!isCrateRootFile(relPath))
|
|
642
|
+
continue;
|
|
643
|
+
idx.rustCrateRootFiles.add(relPath);
|
|
644
|
+
crateRootDirs.add(normalizeDir(path.posix.dirname(relPath)));
|
|
645
|
+
}
|
|
646
|
+
idx.rustCrateRootDirs = Array.from(crateRootDirs)
|
|
647
|
+
.sort((a, b) => {
|
|
648
|
+
const len = b.length - a.length;
|
|
649
|
+
if (len !== 0)
|
|
650
|
+
return len;
|
|
651
|
+
return a.localeCompare(b);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const isJsLike = (ext) => ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext);
|
|
655
|
+
const resolveJsLocalImportRelPath = (params) => {
|
|
656
|
+
const cleaned = params.specifier.split(/[?#]/, 1)[0] ?? '';
|
|
657
|
+
if (!cleaned || (!cleaned.startsWith('.') && !cleaned.startsWith('/')))
|
|
658
|
+
return null;
|
|
659
|
+
const base = cleaned.startsWith('/')
|
|
660
|
+
? path.posix.normalize(cleaned.slice(1))
|
|
661
|
+
: path.posix.normalize(path.posix.join(path.posix.dirname(params.importerRelPath), cleaned));
|
|
662
|
+
const addIfExists = (cand, out) => {
|
|
663
|
+
if (params.keyByRelPath.has(cand))
|
|
664
|
+
out.push(cand);
|
|
665
|
+
};
|
|
666
|
+
const candidates = [];
|
|
667
|
+
const ext = path.posix.extname(base).toLowerCase();
|
|
668
|
+
if (ext) {
|
|
669
|
+
addIfExists(base, candidates);
|
|
670
|
+
// TS projects often import './x.js' but source is './x.ts'
|
|
671
|
+
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
|
|
672
|
+
const stem = base.slice(0, -ext.length);
|
|
673
|
+
addIfExists(`${stem}.ts`, candidates);
|
|
674
|
+
addIfExists(`${stem}.tsx`, candidates);
|
|
675
|
+
addIfExists(`${stem}.jsx`, candidates);
|
|
676
|
+
}
|
|
677
|
+
if (ext === '.jsx') {
|
|
678
|
+
const stem = base.slice(0, -ext.length);
|
|
679
|
+
addIfExists(`${stem}.tsx`, candidates);
|
|
680
|
+
addIfExists(`${stem}.ts`, candidates);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
const exts = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
685
|
+
for (const e of exts)
|
|
686
|
+
addIfExists(`${base}${e}`, candidates);
|
|
687
|
+
for (const e of exts)
|
|
688
|
+
addIfExists(path.posix.join(base, `index${e}`), candidates);
|
|
689
|
+
}
|
|
690
|
+
return candidates[0] ?? null;
|
|
691
|
+
};
|
|
692
|
+
const resolvePythonLocalImportKey = (params) => {
|
|
693
|
+
const cleaned = params.specifier.split(/[?#]/, 1)[0]?.trim() ?? '';
|
|
694
|
+
if (!cleaned)
|
|
695
|
+
return null;
|
|
696
|
+
// Relative imports: .foo / ..foo.bar
|
|
697
|
+
if (cleaned.startsWith('.')) {
|
|
698
|
+
const m = cleaned.match(/^(\.+)(.*)$/);
|
|
699
|
+
if (!m)
|
|
700
|
+
return null;
|
|
701
|
+
const dotCount = m[1]?.length ?? 0;
|
|
702
|
+
const rest = (m[2] ?? '').replace(/^\.+/, '');
|
|
703
|
+
let baseDir = normalizeDir(path.posix.dirname(params.importerRelPath));
|
|
704
|
+
for (let i = 1; i < dotCount; i++) {
|
|
705
|
+
baseDir = normalizeDir(path.posix.dirname(baseDir));
|
|
706
|
+
}
|
|
707
|
+
const restPath = rest ? rest.replace(/\./g, '/') : '';
|
|
708
|
+
const candidates = [];
|
|
709
|
+
if (restPath) {
|
|
710
|
+
candidates.push(normalizeDir(path.posix.join(baseDir, `${restPath}.py`)));
|
|
711
|
+
candidates.push(normalizeDir(path.posix.join(baseDir, restPath, '__init__.py')));
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
candidates.push(normalizeDir(path.posix.join(baseDir, '__init__.py')));
|
|
715
|
+
}
|
|
716
|
+
for (const cand of candidates) {
|
|
717
|
+
const direct = params.keyByRelPath.get(cand);
|
|
718
|
+
if (direct)
|
|
719
|
+
return direct;
|
|
720
|
+
}
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
// If the import is already a path, try to match directly.
|
|
724
|
+
if (cleaned.startsWith('/')) {
|
|
725
|
+
const asPath = cleaned.slice(1);
|
|
726
|
+
const direct = params.keyByRelPath.get(asPath);
|
|
727
|
+
if (direct)
|
|
728
|
+
return direct;
|
|
729
|
+
}
|
|
730
|
+
// Dotted module name -> file path suffix (best-effort, unique-match only).
|
|
731
|
+
const modulePath = cleaned.replace(/\./g, '/');
|
|
732
|
+
const candFile = `${modulePath}.py`;
|
|
733
|
+
const candInit = path.posix.join(modulePath, '__init__.py');
|
|
734
|
+
const directFile = params.keyByRelPath.get(candFile);
|
|
735
|
+
if (directFile)
|
|
736
|
+
return directFile;
|
|
737
|
+
const directInit = params.keyByRelPath.get(candInit);
|
|
738
|
+
if (directInit)
|
|
739
|
+
return directInit;
|
|
740
|
+
const viaSuffixFile = params.suffixIndex.get(candFile);
|
|
741
|
+
if (typeof viaSuffixFile === 'string')
|
|
742
|
+
return viaSuffixFile;
|
|
743
|
+
const viaSuffixInit = params.suffixIndex.get(candInit);
|
|
744
|
+
if (typeof viaSuffixInit === 'string')
|
|
745
|
+
return viaSuffixInit;
|
|
746
|
+
return null;
|
|
747
|
+
};
|
|
748
|
+
const resolveJavaLocalImportKey = (params) => {
|
|
749
|
+
const cleaned = params.specifier.split(/[?#]/, 1)[0] ?? '';
|
|
750
|
+
if (!cleaned || cleaned.endsWith('.*'))
|
|
751
|
+
return null;
|
|
752
|
+
const cand = `${cleaned.replace(/\./g, '/')}.java`;
|
|
753
|
+
const direct = params.keyByRelPath.get(cand);
|
|
754
|
+
if (direct)
|
|
755
|
+
return direct;
|
|
756
|
+
const viaSuffix = params.suffixIndex.get(cand);
|
|
757
|
+
if (typeof viaSuffix === 'string')
|
|
758
|
+
return viaSuffix;
|
|
759
|
+
return null;
|
|
760
|
+
};
|
|
761
|
+
const findGoModuleForFile = (fileRelPath, modules) => {
|
|
762
|
+
for (const m of modules) {
|
|
763
|
+
if (!m.moduleRootDir)
|
|
764
|
+
return m;
|
|
765
|
+
if (fileRelPath.startsWith(`${m.moduleRootDir}/`))
|
|
766
|
+
return m;
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
};
|
|
770
|
+
const isGoModuleLocalImport = (file, specifier) => {
|
|
771
|
+
const idx = repoIndexes.get(file.repoId);
|
|
772
|
+
if (!idx)
|
|
773
|
+
return false;
|
|
774
|
+
const mod = findGoModuleForFile(file.repoRelativePath, idx.goModules);
|
|
775
|
+
if (!mod)
|
|
776
|
+
return false;
|
|
777
|
+
const cleaned = specifier.split(/[?#]/, 1)[0]?.trim() ?? '';
|
|
778
|
+
return cleaned === mod.modulePath || cleaned.startsWith(`${mod.modulePath}/`);
|
|
779
|
+
};
|
|
780
|
+
const resolveGoLocalImportKey = (params) => {
|
|
781
|
+
const cleaned = params.specifier.split(/[?#]/, 1)[0]?.trim() ?? '';
|
|
782
|
+
if (!cleaned)
|
|
783
|
+
return null;
|
|
784
|
+
const mod = findGoModuleForFile(params.importerRelPath, params.idx.goModules);
|
|
785
|
+
if (!mod)
|
|
786
|
+
return null;
|
|
787
|
+
if (cleaned !== mod.modulePath && !cleaned.startsWith(`${mod.modulePath}/`))
|
|
788
|
+
return null;
|
|
789
|
+
const sub = cleaned === mod.modulePath ? '' : cleaned.slice(mod.modulePath.length + 1);
|
|
790
|
+
const targetDir = normalizeDir(path.posix.join(mod.moduleRootDir, sub));
|
|
791
|
+
return params.idx.goRepByDir.get(targetDir) ?? null;
|
|
792
|
+
};
|
|
793
|
+
const findRustCrateRootDir = (fileRelPath, crateRootDirs) => {
|
|
794
|
+
for (const dir of crateRootDirs) {
|
|
795
|
+
if (!dir)
|
|
796
|
+
return '';
|
|
797
|
+
if (fileRelPath.startsWith(`${dir}/`))
|
|
798
|
+
return dir;
|
|
799
|
+
}
|
|
800
|
+
return null;
|
|
801
|
+
};
|
|
802
|
+
const moduleDirForRustFile = (fileRelPath, crateRootFiles) => {
|
|
803
|
+
const dir = normalizeDir(path.posix.dirname(fileRelPath));
|
|
804
|
+
if (crateRootFiles.has(fileRelPath))
|
|
805
|
+
return dir;
|
|
806
|
+
const base = path.posix.basename(fileRelPath);
|
|
807
|
+
if (base === 'mod.rs')
|
|
808
|
+
return dir;
|
|
809
|
+
const stem = path.posix.basename(fileRelPath, '.rs');
|
|
810
|
+
return normalizeDir(path.posix.join(dir, stem));
|
|
811
|
+
};
|
|
812
|
+
const resolveRustLocalImportKey = (params) => {
|
|
813
|
+
let cleaned = (params.specifier.split(/[?#]/, 1)[0] ?? '').trim();
|
|
814
|
+
cleaned = cleaned.replace(/;$/, '');
|
|
815
|
+
cleaned = cleaned.replace(/^::+/, '');
|
|
816
|
+
if (!cleaned)
|
|
817
|
+
return null;
|
|
818
|
+
const rawSegs = cleaned.split('::').filter(Boolean);
|
|
819
|
+
const segs = [];
|
|
820
|
+
for (const seg of rawSegs) {
|
|
821
|
+
const m = seg.match(/^[A-Za-z_][A-Za-z0-9_]*/);
|
|
822
|
+
if (!m?.[0])
|
|
823
|
+
break;
|
|
824
|
+
segs.push(m[0]);
|
|
825
|
+
}
|
|
826
|
+
if (segs.length === 0)
|
|
827
|
+
return null;
|
|
828
|
+
let baseDir;
|
|
829
|
+
let i = 0;
|
|
830
|
+
if (segs[0] === 'crate') {
|
|
831
|
+
const crateRoot = findRustCrateRootDir(params.importerRelPath, params.idx.rustCrateRootDirs);
|
|
832
|
+
if (crateRoot === null)
|
|
833
|
+
return null;
|
|
834
|
+
baseDir = crateRoot;
|
|
835
|
+
i = 1;
|
|
836
|
+
}
|
|
837
|
+
else if (segs[0] === 'self') {
|
|
838
|
+
baseDir = moduleDirForRustFile(params.importerRelPath, params.idx.rustCrateRootFiles);
|
|
839
|
+
i = 1;
|
|
840
|
+
}
|
|
841
|
+
else if (segs[0] === 'super') {
|
|
842
|
+
baseDir = moduleDirForRustFile(params.importerRelPath, params.idx.rustCrateRootFiles);
|
|
843
|
+
while (i < segs.length && segs[i] === 'super') {
|
|
844
|
+
baseDir = normalizeDir(path.posix.dirname(baseDir));
|
|
845
|
+
i++;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
if (i >= segs.length)
|
|
852
|
+
return null;
|
|
853
|
+
let curDir = baseDir;
|
|
854
|
+
let lastResolvedRelPath = null;
|
|
855
|
+
for (let j = i; j < segs.length; j++) {
|
|
856
|
+
const name = segs[j];
|
|
857
|
+
const cand1 = path.posix.join(curDir, `${name}.rs`);
|
|
858
|
+
if (params.idx.keyByRelPath.has(cand1)) {
|
|
859
|
+
lastResolvedRelPath = cand1;
|
|
860
|
+
curDir = moduleDirForRustFile(cand1, params.idx.rustCrateRootFiles);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
const cand2 = path.posix.join(curDir, name, 'mod.rs');
|
|
864
|
+
if (params.idx.keyByRelPath.has(cand2)) {
|
|
865
|
+
lastResolvedRelPath = cand2;
|
|
866
|
+
curDir = moduleDirForRustFile(cand2, params.idx.rustCrateRootFiles);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
if (!lastResolvedRelPath)
|
|
872
|
+
return null;
|
|
873
|
+
return params.idx.keyByRelPath.get(lastResolvedRelPath) ?? null;
|
|
874
|
+
};
|
|
875
|
+
const resolveLocalImportKey = (file, specifier) => {
|
|
876
|
+
const idx = repoIndexes.get(file.repoId);
|
|
877
|
+
if (!idx)
|
|
878
|
+
return null;
|
|
879
|
+
const ext = path.extname(file.repoRelativePath).toLowerCase();
|
|
880
|
+
if (isJsLike(ext)) {
|
|
881
|
+
const rel = resolveJsLocalImportRelPath({
|
|
882
|
+
importerRelPath: file.repoRelativePath,
|
|
883
|
+
specifier,
|
|
884
|
+
keyByRelPath: idx.keyByRelPath,
|
|
885
|
+
});
|
|
886
|
+
if (!rel)
|
|
887
|
+
return null;
|
|
888
|
+
return idx.keyByRelPath.get(rel) ?? null;
|
|
889
|
+
}
|
|
890
|
+
if (ext === '.py') {
|
|
891
|
+
return resolvePythonLocalImportKey({
|
|
892
|
+
importerRelPath: file.repoRelativePath,
|
|
893
|
+
specifier,
|
|
894
|
+
suffixIndex: idx.suffixIndex,
|
|
895
|
+
keyByRelPath: idx.keyByRelPath,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
if (ext === '.java') {
|
|
899
|
+
return resolveJavaLocalImportKey({
|
|
900
|
+
specifier,
|
|
901
|
+
suffixIndex: idx.suffixIndex,
|
|
902
|
+
keyByRelPath: idx.keyByRelPath,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
if (ext === '.go') {
|
|
906
|
+
return resolveGoLocalImportKey({ importerRelPath: file.repoRelativePath, specifier, idx });
|
|
907
|
+
}
|
|
908
|
+
if (ext === '.rs') {
|
|
909
|
+
return resolveRustLocalImportKey({ importerRelPath: file.repoRelativePath, specifier, idx });
|
|
910
|
+
}
|
|
911
|
+
return null;
|
|
912
|
+
};
|
|
913
|
+
const isExternalImportForStandalone = (file, specifier) => {
|
|
914
|
+
const ext = path.extname(file.repoRelativePath).toLowerCase();
|
|
915
|
+
const cleaned = specifier.split(/[?#]/, 1)[0] ?? '';
|
|
916
|
+
// If we can confidently resolve it to a repo file, it's internal.
|
|
917
|
+
if (resolveLocalImportKey(file, cleaned))
|
|
918
|
+
return false;
|
|
919
|
+
// Otherwise, fall back to language syntax heuristics.
|
|
920
|
+
if (isJsLike(ext)) {
|
|
921
|
+
return !(cleaned.startsWith('.') || cleaned.startsWith('/'));
|
|
922
|
+
}
|
|
923
|
+
if (ext === '.go') {
|
|
924
|
+
// In-module Go imports are internal even though they are not relative paths.
|
|
925
|
+
if (isGoModuleLocalImport(file, cleaned))
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
if (ext === '.rs') {
|
|
929
|
+
// Rust intra-crate paths.
|
|
930
|
+
if (cleaned.startsWith('crate::') ||
|
|
931
|
+
cleaned.startsWith('self::') ||
|
|
932
|
+
cleaned.startsWith('super::') ||
|
|
933
|
+
cleaned.startsWith('::crate::') ||
|
|
934
|
+
cleaned.startsWith('::self::') ||
|
|
935
|
+
cleaned.startsWith('::super::')) {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
// For other languages, only treat explicit relative paths as internal.
|
|
940
|
+
if (cleaned.startsWith('.') || cleaned.startsWith('/'))
|
|
941
|
+
return false;
|
|
942
|
+
return true;
|
|
943
|
+
};
|
|
944
|
+
const importGraph = new Map(); // fileKey -> imported fileKeys
|
|
945
|
+
const reverseImportGraph = new Map(); // fileKey -> fileKeys that import it
|
|
946
|
+
// First pass: extract exports and imports
|
|
947
|
+
const fileData = new Map(); // fileKey -> data
|
|
948
|
+
for (const file of files) {
|
|
949
|
+
if (file.kind !== 'code')
|
|
950
|
+
continue;
|
|
951
|
+
const ext = path.extname(file.repoRelativePath).toLowerCase();
|
|
952
|
+
if (!eligibleExtensions.has(ext))
|
|
953
|
+
continue;
|
|
954
|
+
const key = fileKey(file);
|
|
955
|
+
try {
|
|
956
|
+
const raw = await fs.readFile(file.bundleNormAbsPath, 'utf8');
|
|
957
|
+
const content = raw.replace(/\r\n/g, '\n');
|
|
958
|
+
let exports = [];
|
|
959
|
+
let imports = [];
|
|
960
|
+
try {
|
|
961
|
+
const parsed = await extractModuleSyntaxWasm(file.repoRelativePath, content);
|
|
962
|
+
if (parsed) {
|
|
963
|
+
exports = Array.from(new Set(parsed.exports)).sort();
|
|
964
|
+
imports = Array.from(new Set(parsed.imports.map((i) => i.module))).sort();
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
exports = extractExports(content, file.repoRelativePath).sort();
|
|
968
|
+
imports = extractImports(content, file.repoRelativePath).sort();
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
catch {
|
|
972
|
+
// Keep Phase2 analysis robust: fall back to regex if parsing fails.
|
|
973
|
+
exports = extractExports(content, file.repoRelativePath).sort();
|
|
974
|
+
imports = extractImports(content, file.repoRelativePath).sort();
|
|
975
|
+
}
|
|
976
|
+
const loc = content
|
|
977
|
+
.split('\n')
|
|
978
|
+
.filter((l) => l.trim() && !l.trim().startsWith('//')).length;
|
|
979
|
+
fileData.set(key, { exports, imports, content, loc });
|
|
980
|
+
// Build local-import graph (resolved to known repo files)
|
|
981
|
+
const localImportKeys = new Set();
|
|
982
|
+
for (const imp of imports) {
|
|
983
|
+
const targetKey = resolveLocalImportKey(file, imp);
|
|
984
|
+
if (targetKey)
|
|
985
|
+
localImportKeys.add(targetKey);
|
|
986
|
+
}
|
|
987
|
+
importGraph.set(key, localImportKeys);
|
|
988
|
+
}
|
|
989
|
+
catch {
|
|
990
|
+
// Skip files that can't be read
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
// Build reverse import graph
|
|
994
|
+
for (const [fromKey, toKeys] of importGraph.entries()) {
|
|
995
|
+
for (const toKey of toKeys) {
|
|
996
|
+
if (!reverseImportGraph.has(toKey)) {
|
|
997
|
+
reverseImportGraph.set(toKey, new Set());
|
|
998
|
+
}
|
|
999
|
+
reverseImportGraph.get(toKey).add(fromKey);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
// Second pass: create ModuleInfo
|
|
1003
|
+
for (const file of files) {
|
|
1004
|
+
if (file.kind !== 'code')
|
|
1005
|
+
continue;
|
|
1006
|
+
const ext = path.extname(file.repoRelativePath).toLowerCase();
|
|
1007
|
+
if (!eligibleExtensions.has(ext))
|
|
1008
|
+
continue;
|
|
1009
|
+
const key = fileKey(file);
|
|
1010
|
+
const data = fileData.get(key);
|
|
1011
|
+
if (!data)
|
|
1012
|
+
continue;
|
|
1013
|
+
const importedBy = reverseImportGraph.get(key) || new Set();
|
|
1014
|
+
const role = determineModuleRole(file, importedBy);
|
|
1015
|
+
const complexity = calculateComplexity(data.loc, data.imports.length);
|
|
1016
|
+
const externalImportCount = data.imports.filter((imp) => isExternalImportForStandalone(file, imp)).length;
|
|
1017
|
+
const standalone = externalImportCount <= 3; // Few external deps
|
|
1018
|
+
modules.push({
|
|
1019
|
+
path: file.bundleNormRelativePath,
|
|
1020
|
+
exports: data.exports,
|
|
1021
|
+
imports: data.imports,
|
|
1022
|
+
role,
|
|
1023
|
+
standalone,
|
|
1024
|
+
complexity,
|
|
1025
|
+
loc: data.loc,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
return modules;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Phase 2: Detect architecture patterns
|
|
1032
|
+
*/
|
|
1033
|
+
function detectArchitecturePatterns(files, modules) {
|
|
1034
|
+
const patterns = [];
|
|
1035
|
+
const paths = files.map(f => f.repoRelativePath.toLowerCase());
|
|
1036
|
+
const pathSet = new Set(paths);
|
|
1037
|
+
// MVC pattern
|
|
1038
|
+
if (paths.some(p => p.includes('/model')) &&
|
|
1039
|
+
paths.some(p => p.includes('/view')) &&
|
|
1040
|
+
paths.some(p => p.includes('/controller'))) {
|
|
1041
|
+
patterns.push('MVC');
|
|
1042
|
+
}
|
|
1043
|
+
// Plugin architecture
|
|
1044
|
+
if (paths.some(p => p.includes('/plugin')) || paths.some(p => p.includes('/extension'))) {
|
|
1045
|
+
patterns.push('Plugin Architecture');
|
|
1046
|
+
}
|
|
1047
|
+
// Event-driven
|
|
1048
|
+
const hasEvents = modules.some(m => m.exports.some(e => e.toLowerCase().includes('event') || e.toLowerCase().includes('emitter')));
|
|
1049
|
+
if (hasEvents) {
|
|
1050
|
+
patterns.push('Event-Driven');
|
|
1051
|
+
}
|
|
1052
|
+
// Monorepo
|
|
1053
|
+
if (pathSet.has('packages') || pathSet.has('apps') || paths.filter(p => p === 'package.json').length > 1) {
|
|
1054
|
+
patterns.push('Monorepo');
|
|
1055
|
+
}
|
|
1056
|
+
// Layered architecture
|
|
1057
|
+
if (paths.some(p => p.includes('/service')) &&
|
|
1058
|
+
paths.some(p => p.includes('/repository')) ||
|
|
1059
|
+
paths.some(p => p.includes('/dao'))) {
|
|
1060
|
+
patterns.push('Layered Architecture');
|
|
1061
|
+
}
|
|
1062
|
+
// Microservices indicators
|
|
1063
|
+
if (pathSet.has('docker-compose.yml') || paths.filter(p => p.includes('/service/')).length > 3) {
|
|
1064
|
+
patterns.push('Microservices');
|
|
1065
|
+
}
|
|
1066
|
+
// CLI
|
|
1067
|
+
if (paths.some(p => p.includes('/cli/') || p.includes('/command'))) {
|
|
1068
|
+
patterns.push('CLI');
|
|
1069
|
+
}
|
|
1070
|
+
return patterns;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Phase 2: Analyze technology stack
|
|
1074
|
+
*/
|
|
1075
|
+
function analyzeTechStack(languages, dependencies, frameworks) {
|
|
1076
|
+
const primaryLang = languages[0]?.language || 'Unknown';
|
|
1077
|
+
let runtime;
|
|
1078
|
+
let packageManager;
|
|
1079
|
+
const buildTools = [];
|
|
1080
|
+
const testFrameworks = [];
|
|
1081
|
+
// Detect runtime
|
|
1082
|
+
if (primaryLang === 'TypeScript' || primaryLang === 'JavaScript') {
|
|
1083
|
+
runtime = 'Node.js';
|
|
1084
|
+
}
|
|
1085
|
+
else if (primaryLang === 'Python') {
|
|
1086
|
+
runtime = 'Python';
|
|
1087
|
+
}
|
|
1088
|
+
else if (primaryLang === 'Go') {
|
|
1089
|
+
runtime = 'Go';
|
|
1090
|
+
}
|
|
1091
|
+
// Package manager
|
|
1092
|
+
packageManager = dependencies.manager !== 'unknown' ? dependencies.manager : undefined;
|
|
1093
|
+
// Build tools
|
|
1094
|
+
const allDeps = [...dependencies.runtime, ...dependencies.dev].map(d => d.name.toLowerCase());
|
|
1095
|
+
if (primaryLang === 'TypeScript')
|
|
1096
|
+
buildTools.push('TypeScript');
|
|
1097
|
+
if (allDeps.includes('webpack'))
|
|
1098
|
+
buildTools.push('Webpack');
|
|
1099
|
+
if (allDeps.includes('vite'))
|
|
1100
|
+
buildTools.push('Vite');
|
|
1101
|
+
if (allDeps.includes('rollup'))
|
|
1102
|
+
buildTools.push('Rollup');
|
|
1103
|
+
if (allDeps.includes('esbuild'))
|
|
1104
|
+
buildTools.push('esbuild');
|
|
1105
|
+
if (allDeps.includes('babel'))
|
|
1106
|
+
buildTools.push('Babel');
|
|
1107
|
+
// Test frameworks
|
|
1108
|
+
if (allDeps.includes('jest'))
|
|
1109
|
+
testFrameworks.push('Jest');
|
|
1110
|
+
if (allDeps.includes('vitest'))
|
|
1111
|
+
testFrameworks.push('Vitest');
|
|
1112
|
+
if (allDeps.includes('mocha'))
|
|
1113
|
+
testFrameworks.push('Mocha');
|
|
1114
|
+
if (allDeps.includes('pytest'))
|
|
1115
|
+
testFrameworks.push('Pytest');
|
|
1116
|
+
if (allDeps.includes('unittest'))
|
|
1117
|
+
testFrameworks.push('unittest');
|
|
1118
|
+
return {
|
|
1119
|
+
language: primaryLang,
|
|
1120
|
+
runtime,
|
|
1121
|
+
packageManager,
|
|
1122
|
+
buildTools: buildTools.length > 0 ? buildTools : undefined,
|
|
1123
|
+
testFrameworks: testFrameworks.length > 0 ? testFrameworks : undefined,
|
|
1124
|
+
};
|
|
1125
|
+
}
|