@brunobrise/xfeat 1.0.5 → 1.1.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.
- package/README.md +1 -0
- package/index.js +177 -130
- package/index.test.js +71 -53
- package/package.json +2 -1
package/README.md
CHANGED
package/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require("path");
|
|
|
4
4
|
const fg = require("fast-glob");
|
|
5
5
|
const ignore = require("ignore");
|
|
6
6
|
const { Parser, Language } = require("web-tree-sitter");
|
|
7
|
+
const { Listr } = require("listr2");
|
|
7
8
|
require("dotenv").config({ path: path.join(process.cwd(), ".env") });
|
|
8
9
|
const { Anthropic } = require("@anthropic-ai/sdk");
|
|
9
10
|
const anthropic = new Anthropic({
|
|
@@ -587,43 +588,6 @@ async function main() {
|
|
|
587
588
|
return;
|
|
588
589
|
}
|
|
589
590
|
|
|
590
|
-
// 1. Extract structural data
|
|
591
|
-
const structuralData = [];
|
|
592
|
-
for (const file of targetFiles) {
|
|
593
|
-
// Only analyze files from the codebase (skip our own index.js if running locally)
|
|
594
|
-
if (file.endsWith("index.js") && __dirname === targetDir) continue;
|
|
595
|
-
|
|
596
|
-
console.log(`Analyzing file structure: ${file}...`);
|
|
597
|
-
const data = await extractStructure(file);
|
|
598
|
-
|
|
599
|
-
if (
|
|
600
|
-
data &&
|
|
601
|
-
(data.classes.length > 0 ||
|
|
602
|
-
data.functions.length > 0 ||
|
|
603
|
-
data.exports.length > 0)
|
|
604
|
-
) {
|
|
605
|
-
structuralData.push({
|
|
606
|
-
path: path.relative(targetDir, file),
|
|
607
|
-
...data,
|
|
608
|
-
});
|
|
609
|
-
} else {
|
|
610
|
-
// Graceful Fallback for missing AST or empty extractions (LLM must read whole file)
|
|
611
|
-
structuralData.push({
|
|
612
|
-
path: path.relative(targetDir, file),
|
|
613
|
-
classes: [],
|
|
614
|
-
functions: [],
|
|
615
|
-
exports: [],
|
|
616
|
-
imports: [],
|
|
617
|
-
note: "AST parsing unavailable. You MUST use view_file to extract features.",
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
if (structuralData.length === 0) {
|
|
623
|
-
console.log("No valid structural data found.");
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
591
|
// --- CACHE INITIALIZATION ---
|
|
628
592
|
const folderName = path.basename(path.resolve(targetDir));
|
|
629
593
|
const outputPath = path.join(process.cwd(), `${folderName}-features.md`);
|
|
@@ -656,8 +620,6 @@ async function main() {
|
|
|
656
620
|
await fs.writeFile(cachePath, JSON.stringify(cache, null, 2), "utf8");
|
|
657
621
|
};
|
|
658
622
|
|
|
659
|
-
// --- PIPELINE EXECUTION ---
|
|
660
|
-
|
|
661
623
|
if (!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
662
624
|
console.error(
|
|
663
625
|
"\n❌ ERROR: Missing ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable.",
|
|
@@ -667,107 +629,192 @@ async function main() {
|
|
|
667
629
|
|
|
668
630
|
const CONCURRENCY_LIMIT = parseInt(process.env.CONCURRENCY_LIMIT || "5", 10);
|
|
669
631
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
632
|
+
const tasks = new Listr(
|
|
633
|
+
[
|
|
634
|
+
{
|
|
635
|
+
title: "Analyzing File Structure",
|
|
636
|
+
task: async (ctx, task) => {
|
|
637
|
+
const structuralData = [];
|
|
638
|
+
let completed = 0;
|
|
639
|
+
const total = targetFiles.length;
|
|
640
|
+
|
|
641
|
+
for (const file of targetFiles) {
|
|
642
|
+
if (file.endsWith("index.js") && __dirname === targetDir) {
|
|
643
|
+
completed++;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
681
646
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
647
|
+
task.output = `${path.relative(targetDir, file)} (${completed}/${total})`;
|
|
648
|
+
const data = await extractStructure(file);
|
|
649
|
+
|
|
650
|
+
if (
|
|
651
|
+
data &&
|
|
652
|
+
(data.classes.length > 0 ||
|
|
653
|
+
data.functions.length > 0 ||
|
|
654
|
+
data.exports.length > 0)
|
|
655
|
+
) {
|
|
656
|
+
structuralData.push({
|
|
657
|
+
path: path.relative(targetDir, file),
|
|
658
|
+
...data,
|
|
659
|
+
});
|
|
660
|
+
} else {
|
|
661
|
+
structuralData.push({
|
|
662
|
+
path: path.relative(targetDir, file),
|
|
663
|
+
classes: [],
|
|
664
|
+
functions: [],
|
|
665
|
+
exports: [],
|
|
666
|
+
imports: [],
|
|
667
|
+
note: "AST parsing unavailable. You MUST use view_file to extract features.",
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
completed++;
|
|
671
|
+
}
|
|
703
672
|
|
|
704
|
-
|
|
673
|
+
if (structuralData.length === 0) {
|
|
674
|
+
throw new Error("No valid structural data found.");
|
|
675
|
+
}
|
|
676
|
+
ctx.structuralData = structuralData;
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
title: "STAGE 1: Micro Analysis (File Level)",
|
|
681
|
+
task: async (ctx, task) => {
|
|
682
|
+
let completed = 0;
|
|
683
|
+
const total = ctx.structuralData.length;
|
|
684
|
+
task.output = `Completed 0/${total} files`;
|
|
685
|
+
|
|
686
|
+
const fileAnalysesRaw = await pMap(
|
|
687
|
+
ctx.structuralData,
|
|
688
|
+
async (data) => {
|
|
689
|
+
if (cache.fileAnalyses[data.path]) {
|
|
690
|
+
completed++;
|
|
691
|
+
task.output = `Completed ${completed}/${total} files`;
|
|
692
|
+
return cache.fileAnalyses[data.path];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
const featuresMd = await extractFeaturesWithClaude(
|
|
697
|
+
data,
|
|
698
|
+
targetDir,
|
|
699
|
+
);
|
|
700
|
+
const result = {
|
|
701
|
+
path: data.path,
|
|
702
|
+
dir: path.dirname(data.path),
|
|
703
|
+
features: featuresMd,
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// Save to cache immediately
|
|
707
|
+
cache.fileAnalyses[data.path] = result;
|
|
708
|
+
await saveCache();
|
|
709
|
+
|
|
710
|
+
completed++;
|
|
711
|
+
task.output = `Completed ${completed}/${total} files`;
|
|
712
|
+
return result;
|
|
713
|
+
} catch (e) {
|
|
714
|
+
completed++;
|
|
715
|
+
task.output = `Failed to analyze ${data.path}`;
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
CONCURRENCY_LIMIT,
|
|
720
|
+
);
|
|
705
721
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
722
|
+
ctx.fileAnalyses = fileAnalysesRaw.filter(Boolean);
|
|
723
|
+
},
|
|
724
|
+
options: { bottomBar: Infinity },
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
title: "STAGE 2: Macro Analysis (Component Level)",
|
|
728
|
+
task: async (ctx, task) => {
|
|
729
|
+
const directories = {};
|
|
730
|
+
for (const file of ctx.fileAnalyses) {
|
|
731
|
+
if (!directories[file.dir]) directories[file.dir] = [];
|
|
732
|
+
directories[file.dir].push(file);
|
|
733
|
+
}
|
|
713
734
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
735
|
+
const componentSummaries = {};
|
|
736
|
+
const dirEntries = Object.entries(directories);
|
|
737
|
+
let completed = 0;
|
|
738
|
+
const total = dirEntries.length;
|
|
739
|
+
task.output = `Completed 0/${total} components`;
|
|
740
|
+
|
|
741
|
+
await pMap(
|
|
742
|
+
dirEntries,
|
|
743
|
+
async ([dir, files]) => {
|
|
744
|
+
if (cache.componentSummaries[dir]) {
|
|
745
|
+
componentSummaries[dir] = cache.componentSummaries[dir];
|
|
746
|
+
completed++;
|
|
747
|
+
task.output = `Completed ${completed}/${total} components`;
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
try {
|
|
752
|
+
const summary = await extractComponentSummary(dir, files);
|
|
753
|
+
componentSummaries[dir] = summary;
|
|
754
|
+
|
|
755
|
+
cache.componentSummaries[dir] = summary;
|
|
756
|
+
await saveCache();
|
|
757
|
+
|
|
758
|
+
completed++;
|
|
759
|
+
task.output = `Completed ${completed}/${total} components`;
|
|
760
|
+
} catch (e) {
|
|
761
|
+
completed++;
|
|
762
|
+
task.output = `Failed to synthesize component ${dir}`;
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
CONCURRENCY_LIMIT,
|
|
766
|
+
);
|
|
767
|
+
ctx.componentSummaries = componentSummaries;
|
|
768
|
+
},
|
|
769
|
+
options: { bottomBar: Infinity },
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
title: "STAGE 3: Global Architecture Mapping",
|
|
773
|
+
task: async (ctx, task) => {
|
|
774
|
+
let globalArchitecture = cache.globalArchitecture;
|
|
775
|
+
if (!globalArchitecture) {
|
|
776
|
+
globalArchitecture = await extractGlobalArchitecture(
|
|
777
|
+
ctx.componentSummaries,
|
|
778
|
+
);
|
|
779
|
+
cache.globalArchitecture = globalArchitecture;
|
|
780
|
+
await saveCache();
|
|
781
|
+
}
|
|
782
|
+
ctx.globalArchitecture = globalArchitecture;
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
title: "Final Assembly",
|
|
787
|
+
task: async (ctx, task) => {
|
|
788
|
+
let finalDocument = `# Codebase Architecture & Feature Map\n\n`;
|
|
789
|
+
finalDocument += `${ctx.globalArchitecture}\n\n`;
|
|
790
|
+
finalDocument += `---\n\n## Component Breakdown\n\n`;
|
|
791
|
+
|
|
792
|
+
for (const [dir, summary] of Object.entries(ctx.componentSummaries)) {
|
|
793
|
+
finalDocument += `### Directory: \`${dir}\`\n${summary}\n\n`;
|
|
794
|
+
}
|
|
726
795
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
796
|
+
finalDocument += `---\n\n## File-Level Details\n\n`;
|
|
797
|
+
for (const file of ctx.fileAnalyses) {
|
|
798
|
+
finalDocument += `#### \`${file.path}\`\n${file.features}\n\n`;
|
|
799
|
+
}
|
|
731
800
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
|
|
801
|
+
await fs.writeFile(outputPath, finalDocument, "utf8");
|
|
802
|
+
task.title = `Codebase mapping complete! Saved to ${outputPath}`;
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
],
|
|
806
|
+
{
|
|
807
|
+
rendererOptions: {
|
|
808
|
+
collapseSubtasks: false,
|
|
809
|
+
},
|
|
738
810
|
},
|
|
739
|
-
CONCURRENCY_LIMIT,
|
|
740
811
|
);
|
|
741
812
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
console.
|
|
746
|
-
} else {
|
|
747
|
-
console.log(`[Global] Synthesizing final architecture...`);
|
|
748
|
-
globalArchitecture = await extractGlobalArchitecture(componentSummaries);
|
|
749
|
-
cache.globalArchitecture = globalArchitecture;
|
|
750
|
-
await saveCache();
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// --- FINAL ASSEMBLY ---
|
|
754
|
-
console.log(`\nWriting final report to ${outputPath}...`);
|
|
755
|
-
|
|
756
|
-
let finalDocument = `# Codebase Architecture & Feature Map\n\n`;
|
|
757
|
-
finalDocument += `${globalArchitecture}\n\n`;
|
|
758
|
-
finalDocument += `---\n\n## Component Breakdown\n\n`;
|
|
759
|
-
|
|
760
|
-
for (const [dir, summary] of Object.entries(componentSummaries)) {
|
|
761
|
-
finalDocument += `### Directory: \`${dir}\`\n${summary}\n\n`;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
finalDocument += `---\n\n## File-Level Details\n\n`;
|
|
765
|
-
for (const file of fileAnalyses) {
|
|
766
|
-
finalDocument += `#### \`${file.path}\`\n${file.features}\n\n`;
|
|
813
|
+
try {
|
|
814
|
+
await tasks.run();
|
|
815
|
+
} catch (err) {
|
|
816
|
+
console.error("An error occurred during execution:", err.message);
|
|
767
817
|
}
|
|
768
|
-
|
|
769
|
-
await fs.writeFile(outputPath, finalDocument, "utf8");
|
|
770
|
-
console.log(`\n✅ Codebase mapping complete! Saved to ${outputPath}`);
|
|
771
818
|
}
|
|
772
819
|
|
|
773
820
|
if (require.main === module) {
|
package/index.test.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
const path = require(
|
|
2
|
-
const fs = require(
|
|
3
|
-
const {
|
|
4
|
-
getIgnores,
|
|
5
|
-
initTreeSitter,
|
|
6
|
-
extractStructure
|
|
7
|
-
} = require('./index');
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs/promises");
|
|
3
|
+
const { getIgnores, initTreeSitter, extractStructure } = require("./index");
|
|
8
4
|
|
|
9
|
-
describe(
|
|
10
|
-
const testDir = path.join(__dirname,
|
|
5
|
+
describe("Code Features Extractor Unit Tests", () => {
|
|
6
|
+
const testDir = path.join(__dirname, "__test_workspace__");
|
|
11
7
|
|
|
12
8
|
beforeAll(async () => {
|
|
13
9
|
// Create a temporary workspace for testing
|
|
@@ -15,24 +11,24 @@ describe('Code Features Extractor Unit Tests', () => {
|
|
|
15
11
|
|
|
16
12
|
// Create a fake .gitignore
|
|
17
13
|
await fs.writeFile(
|
|
18
|
-
path.join(testDir,
|
|
19
|
-
|
|
14
|
+
path.join(testDir, ".gitignore"),
|
|
15
|
+
"ignored_folder/\n*.log\n",
|
|
20
16
|
);
|
|
21
17
|
|
|
22
18
|
// Create some fake code files
|
|
23
19
|
await fs.writeFile(
|
|
24
|
-
path.join(testDir,
|
|
20
|
+
path.join(testDir, "sample.js"),
|
|
25
21
|
`
|
|
26
22
|
import { someLib } from 'some-lib';
|
|
27
23
|
export class MyClass {
|
|
28
24
|
myMethod() {}
|
|
29
25
|
}
|
|
30
26
|
export function myFunction() {}
|
|
31
|
-
|
|
27
|
+
`,
|
|
32
28
|
);
|
|
33
29
|
|
|
34
30
|
await fs.writeFile(
|
|
35
|
-
path.join(testDir,
|
|
31
|
+
path.join(testDir, "sample.py"),
|
|
36
32
|
`
|
|
37
33
|
from math import sqrt
|
|
38
34
|
class PythonClass:
|
|
@@ -40,14 +36,25 @@ class PythonClass:
|
|
|
40
36
|
pass
|
|
41
37
|
def py_function():
|
|
42
38
|
pass
|
|
43
|
-
|
|
39
|
+
`,
|
|
44
40
|
);
|
|
45
41
|
|
|
46
42
|
await fs.writeFile(
|
|
47
|
-
path.join(testDir,
|
|
48
|
-
|
|
43
|
+
path.join(testDir, "sample.rs"),
|
|
44
|
+
`
|
|
45
|
+
use std::collections::HashMap;
|
|
46
|
+
pub struct MyRustStruct {
|
|
47
|
+
field: i32,
|
|
48
|
+
}
|
|
49
|
+
impl MyRustStruct {
|
|
50
|
+
pub fn rs_method(&self) {}
|
|
51
|
+
}
|
|
52
|
+
pub fn rs_function() {}
|
|
53
|
+
`,
|
|
49
54
|
);
|
|
50
55
|
|
|
56
|
+
await fs.writeFile(path.join(testDir, "unsupported.txt"), "Hello world");
|
|
57
|
+
|
|
51
58
|
// Initialize TreeSitter before testing extraction
|
|
52
59
|
await initTreeSitter();
|
|
53
60
|
});
|
|
@@ -57,71 +64,82 @@ def py_function():
|
|
|
57
64
|
await fs.rm(testDir, { recursive: true, force: true });
|
|
58
65
|
});
|
|
59
66
|
|
|
60
|
-
describe(
|
|
61
|
-
it(
|
|
67
|
+
describe("getIgnores", () => {
|
|
68
|
+
it("should parse .gitignore and include defaults", async () => {
|
|
62
69
|
const ig = await getIgnores(testDir);
|
|
63
|
-
|
|
70
|
+
|
|
64
71
|
// Defaults
|
|
65
|
-
expect(ig.ignores(
|
|
66
|
-
expect(ig.ignores(
|
|
67
|
-
|
|
72
|
+
expect(ig.ignores("node_modules")).toBe(true);
|
|
73
|
+
expect(ig.ignores(".git")).toBe(true);
|
|
74
|
+
|
|
68
75
|
// Custom rules
|
|
69
|
-
expect(ig.ignores(
|
|
70
|
-
expect(ig.ignores(
|
|
71
|
-
|
|
76
|
+
expect(ig.ignores("ignored_folder/file.js")).toBe(true);
|
|
77
|
+
expect(ig.ignores("test.log")).toBe(true);
|
|
78
|
+
|
|
72
79
|
// Non-ignored
|
|
73
|
-
expect(ig.ignores(
|
|
74
|
-
expect(ig.ignores(
|
|
80
|
+
expect(ig.ignores("src/main.js")).toBe(false);
|
|
81
|
+
expect(ig.ignores("index.js")).toBe(false);
|
|
75
82
|
});
|
|
76
83
|
|
|
77
|
-
it(
|
|
78
|
-
const emptyDir = path.join(__dirname,
|
|
84
|
+
it("should handle missing .gitignore by returning defaults", async () => {
|
|
85
|
+
const emptyDir = path.join(__dirname, "__empty_test_dir__");
|
|
79
86
|
await fs.mkdir(emptyDir, { recursive: true });
|
|
80
87
|
|
|
81
88
|
const ig = await getIgnores(emptyDir);
|
|
82
|
-
expect(ig.ignores(
|
|
83
|
-
expect(ig.ignores(
|
|
84
|
-
expect(ig.ignores(
|
|
89
|
+
expect(ig.ignores("node_modules")).toBe(true);
|
|
90
|
+
expect(ig.ignores(".git")).toBe(true);
|
|
91
|
+
expect(ig.ignores("test.log")).toBe(false); // custom rule should be false
|
|
85
92
|
|
|
86
93
|
await fs.rm(emptyDir, { recursive: true, force: true });
|
|
87
94
|
});
|
|
88
95
|
});
|
|
89
96
|
|
|
90
|
-
describe(
|
|
91
|
-
it(
|
|
92
|
-
const jsFilePath = path.join(testDir,
|
|
97
|
+
describe("extractStructure", () => {
|
|
98
|
+
it("should extract AST structure for JavaScript files", async () => {
|
|
99
|
+
const jsFilePath = path.join(testDir, "sample.js");
|
|
93
100
|
const structure = await extractStructure(jsFilePath);
|
|
94
|
-
|
|
101
|
+
|
|
95
102
|
expect(structure).not.toBeNull();
|
|
96
103
|
expect(structure.file).toBe(jsFilePath);
|
|
97
|
-
expect(structure.classes).toContain(
|
|
98
|
-
expect(structure.functions).toContain(
|
|
99
|
-
expect(structure.functions).toContain(
|
|
100
|
-
expect(structure.exports).toContain(
|
|
101
|
-
expect(structure.exports).toContain(
|
|
104
|
+
expect(structure.classes).toContain("MyClass");
|
|
105
|
+
expect(structure.functions).toContain("myMethod");
|
|
106
|
+
expect(structure.functions).toContain("myFunction");
|
|
107
|
+
expect(structure.exports).toContain("MyClass");
|
|
108
|
+
expect(structure.exports).toContain("myFunction");
|
|
102
109
|
expect(structure.imports).toContain("'some-lib'");
|
|
103
110
|
});
|
|
104
111
|
|
|
105
|
-
it(
|
|
106
|
-
const pyFilePath = path.join(testDir,
|
|
112
|
+
it("should extract AST structure for Python files", async () => {
|
|
113
|
+
const pyFilePath = path.join(testDir, "sample.py");
|
|
107
114
|
const structure = await extractStructure(pyFilePath);
|
|
108
|
-
|
|
115
|
+
|
|
109
116
|
expect(structure).not.toBeNull();
|
|
110
117
|
expect(structure.file).toBe(pyFilePath);
|
|
111
|
-
expect(structure.classes).toContain(
|
|
112
|
-
expect(structure.functions).toContain(
|
|
113
|
-
expect(structure.functions).toContain(
|
|
114
|
-
expect(structure.imports).toContain(
|
|
118
|
+
expect(structure.classes).toContain("PythonClass");
|
|
119
|
+
expect(structure.functions).toContain("py_method");
|
|
120
|
+
expect(structure.functions).toContain("py_function");
|
|
121
|
+
expect(structure.imports).toContain("math");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should extract AST structure for Rust files", async () => {
|
|
125
|
+
const rsFilePath = path.join(testDir, "sample.rs");
|
|
126
|
+
const structure = await extractStructure(rsFilePath);
|
|
127
|
+
|
|
128
|
+
expect(structure).not.toBeNull();
|
|
129
|
+
expect(structure.file).toBe(rsFilePath);
|
|
130
|
+
expect(structure.classes).toContain("MyRustStruct");
|
|
131
|
+
expect(structure.functions).toContain("rs_method");
|
|
132
|
+
expect(structure.functions).toContain("rs_function");
|
|
115
133
|
});
|
|
116
134
|
|
|
117
|
-
it(
|
|
118
|
-
const txtFilePath = path.join(testDir,
|
|
135
|
+
it("should return null for unsupported file extensions", async () => {
|
|
136
|
+
const txtFilePath = path.join(testDir, "unsupported.txt");
|
|
119
137
|
const structure = await extractStructure(txtFilePath);
|
|
120
138
|
expect(structure).toBeNull();
|
|
121
139
|
});
|
|
122
140
|
|
|
123
|
-
it(
|
|
124
|
-
const nonexistentPath = path.join(testDir,
|
|
141
|
+
it("should return null (graceful failure) when parsing a nonexistent file", async () => {
|
|
142
|
+
const nonexistentPath = path.join(testDir, "does-not-exist.js");
|
|
125
143
|
const structure = await extractStructure(nonexistentPath);
|
|
126
144
|
expect(structure).toBeNull();
|
|
127
145
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brunobrise/xfeat",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"dotenv": "^17.3.1",
|
|
38
38
|
"fast-glob": "^3.3.3",
|
|
39
39
|
"ignore": "^7.0.5",
|
|
40
|
+
"listr2": "^6.6.1",
|
|
40
41
|
"map-stream": "^0.0.7",
|
|
41
42
|
"tree-sitter-bash": "^0.25.1",
|
|
42
43
|
"tree-sitter-css": "^0.25.0",
|