@anirudw/repolens 0.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.
@@ -0,0 +1,199 @@
1
+ import type { ParserStrategy, ExtractedDependency, ParsedFile } from "../types.js";
2
+
3
+ type TreeSitterParser = {
4
+ parse(content: string): { rootNode: TreeNode };
5
+ setLanguage(lang: unknown): void;
6
+ };
7
+
8
+ let TreeSitterParser: new () => TreeSitterParser;
9
+ let JavaScript: unknown;
10
+
11
+ async function ensureLoaded(): Promise<void> {
12
+ if (!TreeSitterParser) {
13
+ const [ts, js] = await Promise.all([
14
+ import("tree-sitter"),
15
+ import("tree-sitter-javascript"),
16
+ ]);
17
+ TreeSitterParser = ts.default as new () => TreeSitterParser;
18
+ JavaScript = js.default;
19
+ }
20
+ }
21
+
22
+ type TreeNode = {
23
+ type: string;
24
+ text: string;
25
+ startPosition: { row: number; column: number };
26
+ endPosition: { row: number; column: number };
27
+ children: TreeNode[];
28
+ childForFieldName(name: string): TreeNode | null;
29
+ };
30
+
31
+ export class JavaScriptParser implements ParserStrategy {
32
+ private parser: TreeSitterParser | null = null;
33
+
34
+ private async ensureParser(): Promise<TreeSitterParser> {
35
+ if (!this.parser) {
36
+ await ensureLoaded();
37
+ this.parser = new TreeSitterParser();
38
+ this.parser.setLanguage(JavaScript);
39
+ }
40
+ return this.parser;
41
+ }
42
+
43
+ async parse(filePath: string, content: string): Promise<ParsedFile> {
44
+ const parser = await this.ensureParser();
45
+ const tree = parser.parse(content);
46
+ const rootNode = tree.rootNode as TreeNode;
47
+ const dependencies: ExtractedDependency[] = [];
48
+ const imports: Set<string> = new Set();
49
+ const sourceModules: string[] = [];
50
+
51
+ this.extractImports(rootNode, dependencies, imports, sourceModules);
52
+
53
+ const heuristics: Record<string, boolean> = {};
54
+ const allImports = [...imports, ...sourceModules].map((s) => s.toLowerCase());
55
+ heuristics.isReact = allImports.some((i) => i === "react" || i === "@types/react");
56
+ heuristics.isReactNative = allImports.some((i) => i === "react-native");
57
+ heuristics.isNodejs = allImports.some((i) => i.startsWith("node:"));
58
+ heuristics.hasDefaultExport = this.hasDefaultExport(rootNode);
59
+ heuristics.hasNamedExports = this.hasNamedExports(rootNode);
60
+
61
+ return {
62
+ id: filePath,
63
+ relativePath: filePath.split("/").pop() ?? filePath,
64
+ language: "javascript",
65
+ dependencies,
66
+ metadata: {
67
+ sizeBytes: Buffer.byteLength(content, "utf-8"),
68
+ heuristics,
69
+ },
70
+ };
71
+ }
72
+
73
+ private extractImports(
74
+ node: TreeNode,
75
+ dependencies: ExtractedDependency[],
76
+ imports: Set<string>,
77
+ sourceModules: string[]
78
+ ): void {
79
+ if (node.type === "import_statement") {
80
+ this.extractImportStatement(node, dependencies, imports, sourceModules);
81
+ } else if (node.type === "call_expression" && this.isRequireCall(node)) {
82
+ this.extractRequireCall(node, dependencies, sourceModules);
83
+ }
84
+
85
+ for (const child of node.children) {
86
+ this.extractImports(child, dependencies, imports, sourceModules);
87
+ }
88
+ }
89
+
90
+ private extractImportStatement(
91
+ node: TreeNode,
92
+ dependencies: ExtractedDependency[],
93
+ imports: Set<string>,
94
+ sourceModules: string[]
95
+ ): void {
96
+ const sourceNode = node.childForFieldName("source");
97
+ if (sourceNode && sourceNode.type === "string") {
98
+ const rawSpecifier = sourceNode.text.slice(1, -1);
99
+ sourceModules.push(rawSpecifier);
100
+ dependencies.push({
101
+ rawSpecifier,
102
+ type: "import",
103
+ location: {
104
+ line: node.startPosition.row + 1,
105
+ column: node.startPosition.column + 1,
106
+ },
107
+ resolvedPath: rawSpecifier,
108
+ });
109
+ }
110
+
111
+ for (const child of node.children) {
112
+ if (child.type === "import_specifier") {
113
+ const nameNode = child.childForFieldName("name");
114
+ if (nameNode) {
115
+ imports.add(nameNode.text);
116
+ }
117
+ } else if (child.type === "import_default_specifier") {
118
+ const nameNode = child.childForFieldName("name");
119
+ if (nameNode) {
120
+ imports.add(nameNode.text);
121
+ }
122
+ } else if (child.type === "import_namespace_specifier") {
123
+ const nameNode = child.childForFieldName("name");
124
+ if (nameNode) {
125
+ imports.add(nameNode.text);
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ private extractRequireCall(
132
+ node: TreeNode,
133
+ dependencies: ExtractedDependency[],
134
+ sourceModules: string[]
135
+ ): void {
136
+ const args = node.childForFieldName("arguments");
137
+ if (args) {
138
+ for (const arg of args.children) {
139
+ if (arg.type === "string") {
140
+ const rawSpecifier = arg.text.slice(1, -1);
141
+ sourceModules.push(rawSpecifier);
142
+ dependencies.push({
143
+ rawSpecifier,
144
+ type: "require",
145
+ location: {
146
+ line: node.startPosition.row + 1,
147
+ column: node.startPosition.column + 1,
148
+ },
149
+ resolvedPath: rawSpecifier,
150
+ });
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ private isRequireCall(node: TreeNode): boolean {
157
+ const functionExpr = node.childForFieldName("function");
158
+ if (!functionExpr) return false;
159
+
160
+ if (functionExpr.type === "identifier" && functionExpr.text === "require") {
161
+ return true;
162
+ }
163
+
164
+ if (functionExpr.type === "member_expression") {
165
+ const object = functionExpr.childForFieldName("object");
166
+ const property = functionExpr.childForFieldName("property");
167
+ if (object?.text === "require" && property?.text === "resolve") {
168
+ return true;
169
+ }
170
+ }
171
+
172
+ return false;
173
+ }
174
+
175
+ private hasDefaultExport(node: TreeNode): boolean {
176
+ if (node.type === "export_statement") {
177
+ return node.children.some((c) => c.type === "default");
178
+ }
179
+ for (const child of node.children) {
180
+ if (this.hasDefaultExport(child)) return true;
181
+ }
182
+ return false;
183
+ }
184
+
185
+ private hasNamedExports(node: TreeNode): boolean {
186
+ if (node.type === "export_clause") return true;
187
+ if (node.type === "export_statement") {
188
+ const hasDefault = node.children.some((c) => c.type === "default");
189
+ if (hasDefault) return false;
190
+ return node.children.some(
191
+ (c) => c.type === "variable_declaration" || c.type === "function_declaration" || c.type === "class_declaration"
192
+ );
193
+ }
194
+ for (const child of node.children) {
195
+ if (this.hasNamedExports(child)) return true;
196
+ }
197
+ return false;
198
+ }
199
+ }
@@ -0,0 +1,83 @@
1
+ import { remark } from "remark";
2
+ import { visit } from "unist-util-visit";
3
+ import type { Node } from "unist";
4
+ import type { ParserStrategy, ExtractedDependency, ParsedFile } from "../types.js";
5
+
6
+ interface LinkNode extends Node {
7
+ type: string;
8
+ url?: string;
9
+ children?: Node[];
10
+ position?: {
11
+ start: { line: number; column: number };
12
+ end: { line: number; column: number };
13
+ };
14
+ }
15
+
16
+ export class MarkdownParser implements ParserStrategy {
17
+ private processor = remark();
18
+
19
+ async parse(filePath: string, content: string): Promise<ParsedFile> {
20
+ const tree = this.processor.parse(content);
21
+ const dependencies: ExtractedDependency[] = [];
22
+
23
+ visit(tree, "link", (node: LinkNode) => {
24
+ if (node.url) {
25
+ dependencies.push({
26
+ rawSpecifier: node.url,
27
+ type: "markdown-link",
28
+ location: {
29
+ line: node.position?.start.line ?? 1,
30
+ column: node.position?.start.column ?? 1,
31
+ },
32
+ resolvedPath: this.resolvePath(node.url),
33
+ });
34
+ }
35
+ });
36
+
37
+ const wikilinkRegex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
38
+ let match;
39
+ while ((match = wikilinkRegex.exec(content)) !== null) {
40
+ const lineInfo = this.getLineNumber(content, match.index);
41
+ dependencies.push({
42
+ rawSpecifier: match[1],
43
+ type: "wikilink",
44
+ location: {
45
+ line: lineInfo.line,
46
+ column: lineInfo.column,
47
+ },
48
+ resolvedPath: this.resolvePath(match[1]),
49
+ });
50
+ }
51
+
52
+ return {
53
+ id: filePath,
54
+ relativePath: filePath.split("/").pop() ?? filePath,
55
+ language: "markdown",
56
+ dependencies,
57
+ metadata: {
58
+ sizeBytes: Buffer.byteLength(content, "utf-8"),
59
+ heuristics: {
60
+ hasWikilinks: dependencies.some((d) => d.type === "wikilink"),
61
+ hasExternalLinks: dependencies.some((d) =>
62
+ d.rawSpecifier.startsWith("http")
63
+ ),
64
+ },
65
+ },
66
+ };
67
+ }
68
+
69
+ private getLineNumber(content: string, index: number): { line: number; column: number } {
70
+ const lines = content.substring(0, index).split("\n");
71
+ return {
72
+ line: lines.length,
73
+ column: (lines[lines.length - 1]?.length ?? 0) + 1,
74
+ };
75
+ }
76
+
77
+ private resolvePath(specifier: string): string | null {
78
+ if (specifier.startsWith("http") || specifier.startsWith("//")) {
79
+ return null;
80
+ }
81
+ return specifier;
82
+ }
83
+ }
@@ -0,0 +1,184 @@
1
+ import type { ParserStrategy, ExtractedDependency, ParsedFile } from "../types.js";
2
+
3
+ type TreeSitterParser = {
4
+ parse(content: string): { rootNode: TreeNode };
5
+ setLanguage(lang: unknown): void;
6
+ };
7
+
8
+ let TreeSitterParser: new () => TreeSitterParser;
9
+ let Python: unknown;
10
+
11
+ async function ensureLoaded(): Promise<void> {
12
+ if (!TreeSitterParser) {
13
+ const [ts, py] = await Promise.all([
14
+ import("tree-sitter"),
15
+ import("tree-sitter-python"),
16
+ ]);
17
+ TreeSitterParser = ts.default as new () => TreeSitterParser;
18
+ Python = py.default;
19
+ }
20
+ }
21
+
22
+ type TreeNode = {
23
+ type: string;
24
+ text: string;
25
+ startPosition: { row: number; column: number };
26
+ endPosition: { row: number; column: number };
27
+ children: TreeNode[];
28
+ childForFieldName(name: string): TreeNode | null;
29
+ };
30
+
31
+ export class PythonParser implements ParserStrategy {
32
+ private parser: TreeSitterParser | null = null;
33
+
34
+ private async ensureParser(): Promise<TreeSitterParser> {
35
+ if (!this.parser) {
36
+ await ensureLoaded();
37
+ this.parser = new TreeSitterParser();
38
+ this.parser.setLanguage(Python);
39
+ }
40
+ return this.parser;
41
+ }
42
+
43
+ async parse(filePath: string, content: string): Promise<ParsedFile> {
44
+ const parser = await this.ensureParser();
45
+ const tree = parser.parse(content);
46
+ const rootNode = tree.rootNode as TreeNode;
47
+ const dependencies: ExtractedDependency[] = [];
48
+ const heuristics: Record<string, boolean> = {};
49
+
50
+ this.extractImports(rootNode, dependencies);
51
+ heuristics.hasEntrypoint = this.detectMainEntrypoint(rootNode);
52
+
53
+ return {
54
+ id: filePath,
55
+ relativePath: filePath.split("/").pop() ?? filePath,
56
+ language: "python",
57
+ dependencies,
58
+ metadata: {
59
+ sizeBytes: Buffer.byteLength(content, "utf-8"),
60
+ heuristics,
61
+ },
62
+ };
63
+ }
64
+
65
+ private extractImports(node: TreeNode, dependencies: ExtractedDependency[]): void {
66
+ if (node.type === "import_statement") {
67
+ for (const child of node.children) {
68
+ if (child.type === "dotted_name") {
69
+ dependencies.push({
70
+ rawSpecifier: child.text,
71
+ type: "import",
72
+ location: {
73
+ line: node.startPosition.row + 1,
74
+ column: node.startPosition.column + 1,
75
+ },
76
+ resolvedPath: child.text,
77
+ });
78
+ } else if (child.type === "aliased_name") {
79
+ const dottedName = child.childForFieldName("name");
80
+ if (dottedName) {
81
+ dependencies.push({
82
+ rawSpecifier: dottedName.text,
83
+ type: "import",
84
+ location: {
85
+ line: node.startPosition.row + 1,
86
+ column: node.startPosition.column + 1,
87
+ },
88
+ resolvedPath: dottedName.text,
89
+ });
90
+ }
91
+ }
92
+ }
93
+ } else if (node.type === "import_from_statement") {
94
+ let moduleName = "";
95
+ const importedNames: string[] = [];
96
+ let foundImportKeyword = false;
97
+
98
+ for (const child of node.children) {
99
+ if (child.type === "dotted_name") {
100
+ if (!foundImportKeyword) {
101
+ moduleName = child.text;
102
+ } else {
103
+ importedNames.push(child.text);
104
+ }
105
+ } else if (child.type === "identifier") {
106
+ if (foundImportKeyword) {
107
+ importedNames.push(child.text);
108
+ }
109
+ } else if (child.type === "import") {
110
+ foundImportKeyword = true;
111
+ } else if (child.type === "wildcard_import") {
112
+ importedNames.push("*");
113
+ }
114
+ }
115
+
116
+ if (importedNames.length === 0 && moduleName) {
117
+ dependencies.push({
118
+ rawSpecifier: moduleName,
119
+ type: "import",
120
+ location: {
121
+ line: node.startPosition.row + 1,
122
+ column: node.startPosition.column + 1,
123
+ },
124
+ resolvedPath: moduleName,
125
+ });
126
+ } else {
127
+ for (const name of importedNames) {
128
+ dependencies.push({
129
+ rawSpecifier: `${moduleName}.${name}`,
130
+ type: "import",
131
+ location: {
132
+ line: node.startPosition.row + 1,
133
+ column: node.startPosition.column + 1,
134
+ },
135
+ resolvedPath: `${moduleName}.${name}`,
136
+ });
137
+ }
138
+ }
139
+ }
140
+
141
+ for (const child of node.children) {
142
+ this.extractImports(child, dependencies);
143
+ }
144
+ }
145
+
146
+ private detectMainEntrypoint(node: TreeNode): boolean {
147
+ if (node.type === "if_statement") {
148
+ const condition = node.childForFieldName("condition");
149
+ if (condition && this.isNameEqualsMainCheck(condition)) {
150
+ return true;
151
+ }
152
+ }
153
+
154
+ for (const child of node.children) {
155
+ if (this.detectMainEntrypoint(child)) {
156
+ return true;
157
+ }
158
+ }
159
+
160
+ return false;
161
+ }
162
+
163
+ private isNameEqualsMainCheck(node: TreeNode): boolean {
164
+ if (node.type === "comparison_operator") {
165
+ let hasName = false;
166
+ let hasMainString = false;
167
+
168
+ for (const child of node.children) {
169
+ if (child.type === "identifier" && child.text === "__name__") {
170
+ hasName = true;
171
+ } else if (child.type === "string") {
172
+ const innerText = child.text.replace(/['"]/g, "");
173
+ if (innerText === "__main__") {
174
+ hasMainString = true;
175
+ }
176
+ }
177
+ }
178
+
179
+ return hasName && hasMainString;
180
+ }
181
+
182
+ return false;
183
+ }
184
+ }
@@ -0,0 +1,18 @@
1
+ export interface ExtractedDependency {
2
+ rawSpecifier: string;
3
+ type: 'import' | 'require' | 'export' | 'markdown-link' | 'wikilink';
4
+ location: { line: number; column: number };
5
+ resolvedPath: string | null;
6
+ }
7
+
8
+ export interface ParsedFile {
9
+ id: string;
10
+ relativePath: string;
11
+ language: string;
12
+ dependencies: ExtractedDependency[];
13
+ metadata: { sizeBytes: number; heuristics: Record<string, boolean> };
14
+ }
15
+
16
+ export interface ParserStrategy {
17
+ parse(filePath: string, content: string): Promise<ParsedFile>;
18
+ }
@@ -0,0 +1,56 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import type { Graph } from "../../graph/network.js";
3
+ import type { ParsedFile } from "../../parser/types.js";
4
+
5
+ export interface JsonNode {
6
+ id: string;
7
+ relativePath: string;
8
+ language: string;
9
+ sizeBytes: number;
10
+ heuristics: Record<string, boolean>;
11
+ }
12
+
13
+ export interface JsonEdge {
14
+ source: string;
15
+ target: string;
16
+ type: string;
17
+ }
18
+
19
+ export interface GraphJson {
20
+ nodes: JsonNode[];
21
+ edges: JsonEdge[];
22
+ }
23
+
24
+ export async function exportGraphToJson(
25
+ graph: Graph,
26
+ parsedFiles: ParsedFile[],
27
+ outputPath: string
28
+ ): Promise<void> {
29
+ const fileMap = new Map<string, ParsedFile>();
30
+ for (const file of parsedFiles) {
31
+ fileMap.set(file.id, file);
32
+ }
33
+
34
+ const nodes: JsonNode[] = [];
35
+ for (const [nodeId] of graph.getNodes()) {
36
+ const parsedFile = fileMap.get(nodeId);
37
+ if (parsedFile) {
38
+ nodes.push({
39
+ id: parsedFile.id,
40
+ relativePath: parsedFile.relativePath,
41
+ language: parsedFile.language,
42
+ sizeBytes: parsedFile.metadata.sizeBytes,
43
+ heuristics: parsedFile.metadata.heuristics,
44
+ });
45
+ }
46
+ }
47
+
48
+ const edges: JsonEdge[] = graph.getEdges().map((edge) => ({
49
+ source: edge.source,
50
+ target: edge.target,
51
+ type: edge.type,
52
+ }));
53
+
54
+ const output: GraphJson = { nodes, edges };
55
+ await writeFile(outputPath, JSON.stringify(output, null, 2));
56
+ }
@@ -0,0 +1,42 @@
1
+ export enum SupportedLanguage {
2
+ JavaScript = "javascript",
3
+ TypeScript = "typescript",
4
+ Python = "python",
5
+ Java = "java",
6
+ Markdown = "markdown",
7
+ Unknown = "unknown",
8
+ }
9
+
10
+ export const LANGUAGE_EXTENSIONS: Record<SupportedLanguage, string[]> = {
11
+ [SupportedLanguage.JavaScript]: [".js", ".jsx", ".mjs", ".cjs"],
12
+ [SupportedLanguage.TypeScript]: [".ts", ".tsx", ".mts", ".cts"],
13
+ [SupportedLanguage.Python]: [".py"],
14
+ [SupportedLanguage.Java]: [".java"],
15
+ [SupportedLanguage.Markdown]: [".md", ".markdown"],
16
+ [SupportedLanguage.Unknown]: [],
17
+ };
18
+
19
+ export const EXTENSION_TO_LANGUAGE: Map<string, SupportedLanguage> = new Map();
20
+
21
+ for (const [lang, extensions] of Object.entries(LANGUAGE_EXTENSIONS)) {
22
+ if (lang !== SupportedLanguage.Unknown) {
23
+ for (const ext of extensions) {
24
+ EXTENSION_TO_LANGUAGE.set(ext, lang as SupportedLanguage);
25
+ }
26
+ }
27
+ }
28
+
29
+ export function getLanguageFromPath(filePath: string): SupportedLanguage {
30
+ const ext = getExtension(filePath);
31
+ return EXTENSION_TO_LANGUAGE.get(ext) ?? SupportedLanguage.Unknown;
32
+ }
33
+
34
+ export function getExtension(filePath: string): string {
35
+ const lastDot = filePath.lastIndexOf(".");
36
+ if (lastDot === -1) return "";
37
+ return filePath.slice(lastDot);
38
+ }
39
+
40
+ export function isTargetLanguage(filePath: string): boolean {
41
+ return getLanguageFromPath(filePath) !== SupportedLanguage.Unknown;
42
+ }
@@ -0,0 +1,58 @@
1
+ import { readFileSync } from "node:fs";
2
+ import ignore, { Ignore } from "ignore";
3
+
4
+ export const DEFAULT_IGNORES = [
5
+ "node_modules",
6
+ "bower_components",
7
+ "dist",
8
+ "build",
9
+ "out",
10
+ "target",
11
+ ".git",
12
+ ".svn",
13
+ ".hg",
14
+ ".DS_Store",
15
+ "Thumbs.db",
16
+ "*.pyc",
17
+ "__pycache__",
18
+ ".pytest_cache",
19
+ ".mypy_cache",
20
+ ".next",
21
+ ".nuxt",
22
+ ".cache",
23
+ ".parcel-cache",
24
+ "coverage",
25
+ ".nyc_output",
26
+ ".turbo",
27
+ ];
28
+
29
+ export function createIgnoreRules(rootDir: string): Ignore {
30
+ const ig = ignore();
31
+
32
+ for (const pattern of DEFAULT_IGNORES) {
33
+ ig.add(pattern);
34
+ }
35
+
36
+ const gitignorePath = `${rootDir}/.gitignore`;
37
+ try {
38
+ const content = readFileSync(gitignorePath, "utf-8");
39
+ const rules = content
40
+ .split("\n")
41
+ .map((line) => line.trim())
42
+ .filter((line) => line && !line.startsWith("#"));
43
+ ig.add(rules);
44
+ } catch {
45
+ // .gitignore doesn't exist, continue with defaults
46
+ }
47
+
48
+ return ig;
49
+ }
50
+
51
+ export function shouldIgnore(
52
+ ig: Ignore,
53
+ relativePath: string,
54
+ isDirectory: boolean
55
+ ): boolean {
56
+ const pathWithTrailingSlash = isDirectory ? `${relativePath}/` : relativePath;
57
+ return ig.test(pathWithTrailingSlash).ignored;
58
+ }
@@ -0,0 +1,9 @@
1
+ export { scanDirectory, type ScanResult, type ScannedFile } from "./walker.js";
2
+ export {
3
+ SupportedLanguage,
4
+ LANGUAGE_EXTENSIONS,
5
+ getLanguageFromPath,
6
+ getExtension,
7
+ isTargetLanguage,
8
+ } from "./file-types.js";
9
+ export { createIgnoreRules, shouldIgnore, DEFAULT_IGNORES } from "./ignore.js";