code-survey 1.0.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/LICENSE +21 -0
- package/README.md +191 -0
- package/dist/bin/code-survey.js +1620 -0
- package/dist/cache/cache.d.ts +9 -0
- package/dist/cli/cli-parser.d.ts +24 -0
- package/dist/cli/token-estimator.d.ts +4 -0
- package/dist/cli/watcher.d.ts +2 -0
- package/dist/config/config.d.ts +2 -0
- package/dist/git/git.d.ts +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +941 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/package-parser.d.ts +2 -0
- package/dist/parser.d.ts +7 -0
- package/dist/parsers/base.d.ts +9 -0
- package/dist/parsers/csharp.d.ts +5 -0
- package/dist/parsers/go.d.ts +5 -0
- package/dist/parsers/java.d.ts +5 -0
- package/dist/parsers/javascript.d.ts +5 -0
- package/dist/parsers/python.d.ts +5 -0
- package/dist/parsers/rust.d.ts +5 -0
- package/dist/parsers/typescript.d.ts +5 -0
- package/dist/resolver.d.ts +5 -0
- package/dist/scanner.d.ts +7 -0
- package/dist/types.d.ts +74 -0
- package/dist/wasm/wasm-loader.d.ts +1 -0
- package/dist/writer.d.ts +21 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as fs6 from "node:fs";
|
|
3
|
+
import * as path8 from "node:path";
|
|
4
|
+
import * as os2 from "node:os";
|
|
5
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
// src/scanner.ts
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import * as crypto from "node:crypto";
|
|
11
|
+
var DEFAULT_IGNORED_DIRS = new Set([
|
|
12
|
+
".git",
|
|
13
|
+
".code-survey",
|
|
14
|
+
"node_modules",
|
|
15
|
+
".venv",
|
|
16
|
+
"venv",
|
|
17
|
+
"dist",
|
|
18
|
+
"bin",
|
|
19
|
+
"build",
|
|
20
|
+
"target",
|
|
21
|
+
"obj"
|
|
22
|
+
]);
|
|
23
|
+
function calculateHash(filePath) {
|
|
24
|
+
const content = fs.readFileSync(filePath);
|
|
25
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
26
|
+
}
|
|
27
|
+
function scanDirectory(rootDir, customExcludes = [], maxDepth) {
|
|
28
|
+
const absoluteRoot = path.resolve(rootDir);
|
|
29
|
+
const excludeSet = new Set(customExcludes);
|
|
30
|
+
const results = [];
|
|
31
|
+
function walk(currentDir, currentDepth = 0) {
|
|
32
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const entryName = entry.name;
|
|
35
|
+
const fullPath = path.join(currentDir, entryName);
|
|
36
|
+
const relativePath = path.relative(absoluteRoot, fullPath);
|
|
37
|
+
if (DEFAULT_IGNORED_DIRS.has(entryName) || excludeSet.has(entryName) || excludeSet.has(relativePath)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
if (maxDepth === undefined || currentDepth < maxDepth) {
|
|
42
|
+
walk(fullPath, currentDepth + 1);
|
|
43
|
+
}
|
|
44
|
+
} else if (entry.isFile()) {
|
|
45
|
+
const hash = calculateHash(fullPath);
|
|
46
|
+
results.push({
|
|
47
|
+
relativePath,
|
|
48
|
+
absolutePath: fullPath,
|
|
49
|
+
hash
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
walk(absoluteRoot);
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/parser.ts
|
|
59
|
+
import * as path3 from "node:path";
|
|
60
|
+
|
|
61
|
+
// src/wasm/wasm-loader.ts
|
|
62
|
+
import * as fs2 from "node:fs";
|
|
63
|
+
import * as path2 from "node:path";
|
|
64
|
+
import * as os from "node:os";
|
|
65
|
+
import { fileURLToPath } from "node:url";
|
|
66
|
+
var WASM_FILE_MAP = {
|
|
67
|
+
typescript: "tree-sitter-typescript.wasm",
|
|
68
|
+
javascript: "tree-sitter-javascript.wasm",
|
|
69
|
+
python: "tree-sitter-python.wasm",
|
|
70
|
+
go: "tree-sitter-go.wasm",
|
|
71
|
+
java: "tree-sitter-java.wasm",
|
|
72
|
+
csharp: "tree-sitter-c_sharp.wasm",
|
|
73
|
+
rust: "tree-sitter-rust.wasm"
|
|
74
|
+
};
|
|
75
|
+
var CDN_BASE_URL = "https://cdn.jsdelivr.net/npm/tree-sitter-wasms@0.1.13/out";
|
|
76
|
+
async function getWasmPath(language) {
|
|
77
|
+
const fileName = WASM_FILE_MAP[language];
|
|
78
|
+
if (!fileName) {
|
|
79
|
+
throw new Error(`Unsupported tree-sitter language: ${language}`);
|
|
80
|
+
}
|
|
81
|
+
const currentDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
82
|
+
const nodeModulesPath = path2.resolve(currentDir, "..", "..", "node_modules", "tree-sitter-wasms", "out", fileName);
|
|
83
|
+
if (fs2.existsSync(nodeModulesPath)) {
|
|
84
|
+
return nodeModulesPath;
|
|
85
|
+
}
|
|
86
|
+
const localPath = path2.resolve(process.cwd(), "node_modules", "tree-sitter-wasms", "out", fileName);
|
|
87
|
+
if (fs2.existsSync(localPath)) {
|
|
88
|
+
return localPath;
|
|
89
|
+
}
|
|
90
|
+
const cacheDir = path2.resolve(os.homedir(), ".code-survey", "grammars");
|
|
91
|
+
const cachePath = path2.resolve(cacheDir, fileName);
|
|
92
|
+
if (fs2.existsSync(cachePath)) {
|
|
93
|
+
return cachePath;
|
|
94
|
+
}
|
|
95
|
+
if (!fs2.existsSync(cacheDir)) {
|
|
96
|
+
fs2.mkdirSync(cacheDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
const url = `${CDN_BASE_URL}/${fileName}`;
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(url);
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(`Failed to fetch WASM from CDN: ${response.statusText}`);
|
|
103
|
+
}
|
|
104
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
105
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
106
|
+
fs2.writeFileSync(cachePath, buffer);
|
|
107
|
+
return cachePath;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
throw new Error(`Could not resolve or download WASM grammar for ${language}: ${err.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/parser.ts
|
|
114
|
+
class CodebaseParser {
|
|
115
|
+
parsers = {};
|
|
116
|
+
async initialize(fileExtensions) {
|
|
117
|
+
const extensions = fileExtensions ? new Set(fileExtensions) : null;
|
|
118
|
+
const loadParser = async (exts, importPath, className, wasmName) => {
|
|
119
|
+
if (extensions) {
|
|
120
|
+
const needsLoad = exts.some((ext) => extensions.has(ext));
|
|
121
|
+
if (!needsLoad)
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const module = await import(importPath);
|
|
126
|
+
const constructor = module[className];
|
|
127
|
+
const parser = new constructor;
|
|
128
|
+
await parser.initialize(await getWasmPath(wasmName));
|
|
129
|
+
for (const ext of exts) {
|
|
130
|
+
this.parsers[ext] = parser;
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error(`Failed to initialize parser for ${wasmName}: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
await loadParser([".ts", ".tsx"], "./parsers/typescript.ts", "TypeScriptParser", "typescript");
|
|
137
|
+
await loadParser([".js", ".jsx"], "./parsers/javascript.ts", "JavaScriptParser", "javascript");
|
|
138
|
+
await loadParser([".py"], "./parsers/python.ts", "PythonParser", "python");
|
|
139
|
+
await loadParser([".go"], "./parsers/go.ts", "GoParser", "go");
|
|
140
|
+
await loadParser([".java"], "./parsers/java.ts", "JavaParser", "java");
|
|
141
|
+
await loadParser([".cs"], "./parsers/csharp.ts", "CSharpParser", "csharp");
|
|
142
|
+
await loadParser([".rs"], "./parsers/rust.ts", "RustParser", "rust");
|
|
143
|
+
}
|
|
144
|
+
getParserForFile(filePath) {
|
|
145
|
+
const ext = path3.extname(filePath);
|
|
146
|
+
return this.parsers[ext] || null;
|
|
147
|
+
}
|
|
148
|
+
parseFile(filePath, code, options) {
|
|
149
|
+
const parser = this.getParserForFile(filePath);
|
|
150
|
+
if (!parser) {
|
|
151
|
+
return { imports: [], exports: [], symbols: [] };
|
|
152
|
+
}
|
|
153
|
+
return parser.parse(code, options);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/package-parser.ts
|
|
158
|
+
import * as fs3 from "node:fs";
|
|
159
|
+
import * as path4 from "node:path";
|
|
160
|
+
function parsePackageDependencies(dir) {
|
|
161
|
+
const deps = {};
|
|
162
|
+
const pkgJsonPath = path4.join(dir, "package.json");
|
|
163
|
+
if (fs3.existsSync(pkgJsonPath)) {
|
|
164
|
+
try {
|
|
165
|
+
const content = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf-8"));
|
|
166
|
+
deps.npm = {
|
|
167
|
+
...content.dependencies || {},
|
|
168
|
+
...content.devDependencies || {}
|
|
169
|
+
};
|
|
170
|
+
} catch {}
|
|
171
|
+
}
|
|
172
|
+
const goModPath = path4.join(dir, "go.mod");
|
|
173
|
+
if (fs3.existsSync(goModPath)) {
|
|
174
|
+
try {
|
|
175
|
+
const content = fs3.readFileSync(goModPath, "utf-8");
|
|
176
|
+
const goDeps = {};
|
|
177
|
+
const requireBlockRegex = /require\s*\(([\s\S]*?)\)/g;
|
|
178
|
+
let match;
|
|
179
|
+
while ((match = requireBlockRegex.exec(content)) !== null) {
|
|
180
|
+
const lines = match[1].split(`
|
|
181
|
+
`);
|
|
182
|
+
for (const line of lines) {
|
|
183
|
+
const parts = line.trim().split(/\s+/);
|
|
184
|
+
if (parts.length >= 2 && !parts[0].startsWith("//")) {
|
|
185
|
+
goDeps[parts[0]] = parts[1];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const singleRequireRegex = /^\s*require\s+([a-zA-Z0-9.\-_\/]+)\s+([^\s]+)/gm;
|
|
190
|
+
while ((match = singleRequireRegex.exec(content)) !== null) {
|
|
191
|
+
goDeps[match[1]] = match[2];
|
|
192
|
+
}
|
|
193
|
+
if (Object.keys(goDeps).length > 0) {
|
|
194
|
+
deps.go = goDeps;
|
|
195
|
+
}
|
|
196
|
+
} catch {}
|
|
197
|
+
}
|
|
198
|
+
const pomPath = path4.join(dir, "pom.xml");
|
|
199
|
+
if (fs3.existsSync(pomPath)) {
|
|
200
|
+
try {
|
|
201
|
+
const content = fs3.readFileSync(pomPath, "utf-8");
|
|
202
|
+
const mavenDeps = {};
|
|
203
|
+
const depRegex = /<dependency>([\s\S]*?)<\/dependency>/g;
|
|
204
|
+
let match;
|
|
205
|
+
while ((match = depRegex.exec(content)) !== null) {
|
|
206
|
+
const block = match[1];
|
|
207
|
+
const groupId = block.match(/<groupId>(.*?)<\/groupId>/)?.[1]?.trim();
|
|
208
|
+
const artifactId = block.match(/<artifactId>(.*?)<\/artifactId>/)?.[1]?.trim();
|
|
209
|
+
const version = block.match(/<version>(.*?)<\/version>/)?.[1]?.trim();
|
|
210
|
+
if (groupId && artifactId && version) {
|
|
211
|
+
mavenDeps[`${groupId}:${artifactId}`] = version;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (Object.keys(mavenDeps).length > 0) {
|
|
215
|
+
deps.maven = mavenDeps;
|
|
216
|
+
}
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
const files = fs3.readdirSync(dir);
|
|
220
|
+
const csprojFile = files.find((f) => f.endsWith(".csproj"));
|
|
221
|
+
if (csprojFile) {
|
|
222
|
+
try {
|
|
223
|
+
const content = fs3.readFileSync(path4.join(dir, csprojFile), "utf-8");
|
|
224
|
+
const nugetDeps = {};
|
|
225
|
+
const packageRefRegex = /<PackageReference\s+([\s\S]*?)\/>/g;
|
|
226
|
+
let match;
|
|
227
|
+
while ((match = packageRefRegex.exec(content)) !== null) {
|
|
228
|
+
const attrs = match[1];
|
|
229
|
+
const include = attrs.match(/Include="([^"]+)"/)?.[1];
|
|
230
|
+
const version = attrs.match(/Version="([^"]+)"/)?.[1];
|
|
231
|
+
if (include && version) {
|
|
232
|
+
nugetDeps[include] = version;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (Object.keys(nugetDeps).length > 0) {
|
|
236
|
+
deps.nuget = nugetDeps;
|
|
237
|
+
}
|
|
238
|
+
} catch {}
|
|
239
|
+
}
|
|
240
|
+
const reqPath = path4.join(dir, "requirements.txt");
|
|
241
|
+
if (fs3.existsSync(reqPath)) {
|
|
242
|
+
try {
|
|
243
|
+
const lines = fs3.readFileSync(reqPath, "utf-8").split(`
|
|
244
|
+
`);
|
|
245
|
+
const pipDeps = {};
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
const trimmed = line.trim();
|
|
248
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
249
|
+
const match = trimmed.match(/^([^>=<#\s]+)\s*(>=|<=|==|!=|<|>)\s*([^\s]+)/);
|
|
250
|
+
if (match) {
|
|
251
|
+
pipDeps[match[1]] = match[2] + match[3];
|
|
252
|
+
} else {
|
|
253
|
+
pipDeps[trimmed] = "latest";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (Object.keys(pipDeps).length > 0) {
|
|
258
|
+
deps.pip = pipDeps;
|
|
259
|
+
}
|
|
260
|
+
} catch {}
|
|
261
|
+
}
|
|
262
|
+
return deps;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/resolver.ts
|
|
266
|
+
import * as path5 from "node:path";
|
|
267
|
+
function resolveImports(files, namespaces, packageDeps) {
|
|
268
|
+
const externalImports = {};
|
|
269
|
+
const internalLinks = {};
|
|
270
|
+
const filePaths = Object.keys(files);
|
|
271
|
+
const addExternalImport = (pkg, file) => {
|
|
272
|
+
if (!externalImports[pkg])
|
|
273
|
+
externalImports[pkg] = [];
|
|
274
|
+
if (!externalImports[pkg].includes(file)) {
|
|
275
|
+
externalImports[pkg].push(file);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const addInternalLink = (targetFile, symbol, sourceFile) => {
|
|
279
|
+
const key = symbol ? `${targetFile}:${symbol}` : targetFile;
|
|
280
|
+
if (!internalLinks[key])
|
|
281
|
+
internalLinks[key] = [];
|
|
282
|
+
if (!internalLinks[key].includes(sourceFile)) {
|
|
283
|
+
internalLinks[key].push(sourceFile);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const npmPackages = new Set(Object.keys(packageDeps.npm || {}));
|
|
287
|
+
const pipPackages = new Set(Object.keys(packageDeps.pip || {}));
|
|
288
|
+
const nugetPackages = new Set(Object.keys(packageDeps.nuget || {}));
|
|
289
|
+
const mavenPackages = new Set(Object.keys(packageDeps.maven || {}));
|
|
290
|
+
const goPackages = new Set(Object.keys(packageDeps.go || {}));
|
|
291
|
+
function isExternal(source) {
|
|
292
|
+
return npmPackages.has(source) || pipPackages.has(source) || nugetPackages.has(source) || mavenPackages.has(source) || goPackages.has(source);
|
|
293
|
+
}
|
|
294
|
+
for (const sourceFile of filePaths) {
|
|
295
|
+
const entry = files[sourceFile];
|
|
296
|
+
const sourceDir = path5.dirname(sourceFile);
|
|
297
|
+
for (const imp of entry.imports) {
|
|
298
|
+
const { source, symbols } = imp;
|
|
299
|
+
if (isExternal(source)) {
|
|
300
|
+
addExternalImport(source, sourceFile);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (source.startsWith(".")) {
|
|
304
|
+
const resolvedBase = path5.normalize(path5.join(sourceDir, source));
|
|
305
|
+
const candidates = [
|
|
306
|
+
resolvedBase,
|
|
307
|
+
resolvedBase + ".ts",
|
|
308
|
+
resolvedBase + ".tsx",
|
|
309
|
+
resolvedBase + ".js",
|
|
310
|
+
resolvedBase + ".jsx",
|
|
311
|
+
resolvedBase + ".d.ts",
|
|
312
|
+
path5.join(resolvedBase, "index.ts"),
|
|
313
|
+
path5.join(resolvedBase, "index.tsx"),
|
|
314
|
+
path5.join(resolvedBase, "index.js")
|
|
315
|
+
];
|
|
316
|
+
let resolvedFile = null;
|
|
317
|
+
for (const cand of candidates) {
|
|
318
|
+
const normalizedCand = cand.replace(/\\/g, "/");
|
|
319
|
+
if (files[normalizedCand]) {
|
|
320
|
+
resolvedFile = normalizedCand;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (resolvedFile) {
|
|
325
|
+
if (symbols.length === 0) {
|
|
326
|
+
addInternalLink(resolvedFile, null, sourceFile);
|
|
327
|
+
} else {
|
|
328
|
+
for (const sym of symbols) {
|
|
329
|
+
addInternalLink(resolvedFile, sym, sourceFile);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const isPythonDotted = sourceFile.endsWith(".py") && !source.includes("/") && (source.includes(".") || source.startsWith("."));
|
|
336
|
+
if (isPythonDotted) {
|
|
337
|
+
let normalizedSource = source;
|
|
338
|
+
if (source.startsWith(".")) {
|
|
339
|
+
const dots = source.match(/^\.+/)?.[0] || "";
|
|
340
|
+
const remainder = source.substring(dots.length);
|
|
341
|
+
const dotCount = dots.length;
|
|
342
|
+
let targetDir = sourceDir;
|
|
343
|
+
for (let i = 1;i < dotCount; i++) {
|
|
344
|
+
targetDir = path5.dirname(targetDir);
|
|
345
|
+
}
|
|
346
|
+
normalizedSource = remainder ? path5.join(targetDir, remainder.replace(/\./g, "/")) : targetDir;
|
|
347
|
+
} else {
|
|
348
|
+
normalizedSource = source.replace(/\./g, "/");
|
|
349
|
+
}
|
|
350
|
+
const candidate = (normalizedSource + ".py").replace(/\\/g, "/");
|
|
351
|
+
if (files[candidate]) {
|
|
352
|
+
if (symbols.length === 0) {
|
|
353
|
+
addInternalLink(candidate, null, sourceFile);
|
|
354
|
+
} else {
|
|
355
|
+
for (const sym of symbols) {
|
|
356
|
+
addInternalLink(candidate, sym, sourceFile);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (namespaces[source]) {
|
|
363
|
+
const ns = namespaces[source];
|
|
364
|
+
for (const targetFile of ns.files) {
|
|
365
|
+
if (symbols.length === 0) {
|
|
366
|
+
addInternalLink(targetFile, null, sourceFile);
|
|
367
|
+
} else {
|
|
368
|
+
for (const sym of symbols) {
|
|
369
|
+
const targetEntry = files[targetFile];
|
|
370
|
+
if (targetEntry && targetEntry.exports.some((e) => e.name === sym)) {
|
|
371
|
+
addInternalLink(targetFile, sym, sourceFile);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (source.includes(".")) {
|
|
379
|
+
const lastDot = source.lastIndexOf(".");
|
|
380
|
+
const nsName = source.substring(0, lastDot);
|
|
381
|
+
const className = source.substring(lastDot + 1);
|
|
382
|
+
if (namespaces[nsName]) {
|
|
383
|
+
const ns = namespaces[nsName];
|
|
384
|
+
for (const targetFile of ns.files) {
|
|
385
|
+
const targetEntry = files[targetFile];
|
|
386
|
+
if (targetEntry && targetEntry.exports.some((e) => e.name === className)) {
|
|
387
|
+
addInternalLink(targetFile, className, sourceFile);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const simpleCandidate = source.replace(/\\/g, "/");
|
|
394
|
+
if (files[simpleCandidate]) {
|
|
395
|
+
addInternalLink(simpleCandidate, null, sourceFile);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return { externalImports, internalLinks };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/writer.ts
|
|
403
|
+
import { stringify } from "yaml";
|
|
404
|
+
import * as fs4 from "node:fs";
|
|
405
|
+
import * as path6 from "node:path";
|
|
406
|
+
function sortObjectKeys(obj) {
|
|
407
|
+
if (obj === null || typeof obj !== "object") {
|
|
408
|
+
return obj;
|
|
409
|
+
}
|
|
410
|
+
if (Array.isArray(obj)) {
|
|
411
|
+
return obj.map(sortObjectKeys);
|
|
412
|
+
}
|
|
413
|
+
const sortedObj = {};
|
|
414
|
+
const keys = Object.keys(obj).sort();
|
|
415
|
+
for (const key of keys) {
|
|
416
|
+
sortedObj[key] = sortObjectKeys(obj[key]);
|
|
417
|
+
}
|
|
418
|
+
return sortedObj;
|
|
419
|
+
}
|
|
420
|
+
function sortCodeSurveyData(data) {
|
|
421
|
+
const clone = JSON.parse(JSON.stringify(data));
|
|
422
|
+
if (clone.project?.languages) {
|
|
423
|
+
clone.project.languages.sort();
|
|
424
|
+
}
|
|
425
|
+
if (clone.files) {
|
|
426
|
+
for (const fileKey of Object.keys(clone.files)) {
|
|
427
|
+
const fileEntry = clone.files[fileKey];
|
|
428
|
+
if (fileEntry.imports) {
|
|
429
|
+
fileEntry.imports.sort((a, b) => a.source.localeCompare(b.source));
|
|
430
|
+
for (const imp of fileEntry.imports) {
|
|
431
|
+
if (imp.symbols)
|
|
432
|
+
imp.symbols.sort();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (fileEntry.exports) {
|
|
436
|
+
fileEntry.exports.sort((a, b) => a.name.localeCompare(b.name));
|
|
437
|
+
}
|
|
438
|
+
if (fileEntry.symbols) {
|
|
439
|
+
fileEntry.symbols.sort((a, b) => a.name.localeCompare(b.name));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (clone.namespaces) {
|
|
444
|
+
for (const nsKey of Object.keys(clone.namespaces)) {
|
|
445
|
+
const nsEntry = clone.namespaces[nsKey];
|
|
446
|
+
if (nsEntry.files)
|
|
447
|
+
nsEntry.files.sort();
|
|
448
|
+
if (nsEntry.exports)
|
|
449
|
+
nsEntry.exports.sort();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (clone.externalImports) {
|
|
453
|
+
for (const extKey of Object.keys(clone.externalImports)) {
|
|
454
|
+
clone.externalImports[extKey].sort();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (clone.internalLinks) {
|
|
458
|
+
for (const linkKey of Object.keys(clone.internalLinks)) {
|
|
459
|
+
clone.internalLinks[linkKey].sort();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return sortObjectKeys(clone);
|
|
463
|
+
}
|
|
464
|
+
function generateDeterministicJson(data) {
|
|
465
|
+
const sortedData = sortCodeSurveyData(data);
|
|
466
|
+
return JSON.stringify(sortedData, null, 2);
|
|
467
|
+
}
|
|
468
|
+
function generateDeterministicYaml(data) {
|
|
469
|
+
const sortedData = sortCodeSurveyData(data);
|
|
470
|
+
return stringify(sortedData, { sortMapEntries: true });
|
|
471
|
+
}
|
|
472
|
+
function generateDeterministicMermaid(data) {
|
|
473
|
+
const sortedData = sortCodeSurveyData(data);
|
|
474
|
+
const lines = ["graph TD"];
|
|
475
|
+
const fileKeys = Object.keys(sortedData.files).sort();
|
|
476
|
+
if (fileKeys.length === 0) {
|
|
477
|
+
return `graph TD
|
|
478
|
+
`;
|
|
479
|
+
}
|
|
480
|
+
const fileToId = {};
|
|
481
|
+
fileKeys.forEach((fileKey, index) => {
|
|
482
|
+
fileToId[fileKey] = `node_${index}`;
|
|
483
|
+
lines.push(` node_${index}["${fileKey}"]`);
|
|
484
|
+
});
|
|
485
|
+
const linkSet = new Set;
|
|
486
|
+
const linkKeys = Object.keys(sortedData.internalLinks).sort();
|
|
487
|
+
for (const linkKey of linkKeys) {
|
|
488
|
+
const sourceFile = linkKey.split(":")[0];
|
|
489
|
+
const sourceId = fileToId[sourceFile];
|
|
490
|
+
if (!sourceId)
|
|
491
|
+
continue;
|
|
492
|
+
const targets = sortedData.internalLinks[linkKey];
|
|
493
|
+
for (const targetFile of targets) {
|
|
494
|
+
const targetId = fileToId[targetFile];
|
|
495
|
+
if (targetId && sourceId !== targetId) {
|
|
496
|
+
const edge = ` ${targetId} --> ${sourceId}`;
|
|
497
|
+
linkSet.add(edge);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const sortedEdges = Array.from(linkSet).sort();
|
|
502
|
+
lines.push(...sortedEdges);
|
|
503
|
+
return lines.join(`
|
|
504
|
+
`) + `
|
|
505
|
+
`;
|
|
506
|
+
}
|
|
507
|
+
function generateProjectMarkdown(data) {
|
|
508
|
+
return `# Project: ${data.project.name}
|
|
509
|
+
Languages: ${data.project.languages.join(", ")}
|
|
510
|
+
`;
|
|
511
|
+
}
|
|
512
|
+
function generateDependenciesMarkdown(data) {
|
|
513
|
+
const lines = [];
|
|
514
|
+
const depKeys = Object.keys(data.packageDependencies).sort();
|
|
515
|
+
if (depKeys.length > 0) {
|
|
516
|
+
lines.push("## Dependencies");
|
|
517
|
+
for (const key of depKeys) {
|
|
518
|
+
const deps = data.packageDependencies[key];
|
|
519
|
+
const items = Object.keys(deps).sort().map((d) => `${d} (${deps[d]})`);
|
|
520
|
+
if (items.length > 0) {
|
|
521
|
+
lines.push(`- **${key}**: ${items.join(", ")}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return lines.length > 0 ? lines.join(`
|
|
526
|
+
`) + `
|
|
527
|
+
` : "";
|
|
528
|
+
}
|
|
529
|
+
function generateFilesMarkdown(data, options) {
|
|
530
|
+
const lines = [];
|
|
531
|
+
const fileKeys = Object.keys(data.files).sort();
|
|
532
|
+
if (fileKeys.length > 0) {
|
|
533
|
+
lines.push("## Files");
|
|
534
|
+
for (const fileKey of fileKeys) {
|
|
535
|
+
const fileEntry = data.files[fileKey];
|
|
536
|
+
lines.push(`
|
|
537
|
+
### \`${fileKey}\``);
|
|
538
|
+
lines.push(`Hash: ${fileEntry.hash}`);
|
|
539
|
+
if (fileEntry.imports.length > 0) {
|
|
540
|
+
lines.push("- **Imports**:");
|
|
541
|
+
for (const imp of fileEntry.imports) {
|
|
542
|
+
const syms = imp.symbols.length > 0 ? `: ${imp.symbols.join(", ")}` : "";
|
|
543
|
+
lines.push(` - \`${imp.source}\`${syms}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (fileEntry.exports.length > 0) {
|
|
547
|
+
lines.push("- **Exports**:");
|
|
548
|
+
for (const exp of fileEntry.exports) {
|
|
549
|
+
const docStr = exp.doc ? ` - ${exp.doc}` : "";
|
|
550
|
+
const dispName = exp.signature ? `${exp.name}${exp.signature}` : exp.name;
|
|
551
|
+
lines.push(` - \`${dispName}\` (${exp.type}, ${exp.location})${docStr}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (fileEntry.symbols.length > 0) {
|
|
555
|
+
lines.push("- **Symbols**:");
|
|
556
|
+
for (const sym of fileEntry.symbols) {
|
|
557
|
+
const docStr = sym.doc ? ` - ${sym.doc}` : "";
|
|
558
|
+
const dispName = sym.signature ? `${sym.name}${sym.signature}` : sym.name;
|
|
559
|
+
lines.push(` - \`${dispName}\` (${sym.type}, ${sym.location})${docStr}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (options?.includeToc) {
|
|
563
|
+
lines.push(`
|
|
564
|
+
[Back to Table of Contents](#table-of-contents)`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return lines.length > 0 ? lines.join(`
|
|
569
|
+
`) + `
|
|
570
|
+
` : "";
|
|
571
|
+
}
|
|
572
|
+
function generateNamespacesMarkdown(data) {
|
|
573
|
+
const lines = [];
|
|
574
|
+
const nsKeys = Object.keys(data.namespaces).sort();
|
|
575
|
+
if (nsKeys.length > 0) {
|
|
576
|
+
lines.push("## Namespaces");
|
|
577
|
+
for (const nsKey of nsKeys) {
|
|
578
|
+
const nsEntry = data.namespaces[nsKey];
|
|
579
|
+
lines.push(`
|
|
580
|
+
### ${nsKey}`);
|
|
581
|
+
lines.push(`- **Files**: ${nsEntry.files.join(", ")}`);
|
|
582
|
+
lines.push(`- **Exports**: ${nsEntry.exports.join(", ")}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return lines.length > 0 ? lines.join(`
|
|
586
|
+
`) + `
|
|
587
|
+
` : "";
|
|
588
|
+
}
|
|
589
|
+
function generateLinksMarkdown(data) {
|
|
590
|
+
const lines = [];
|
|
591
|
+
const linkKeys = Object.keys(data.internalLinks).sort();
|
|
592
|
+
if (linkKeys.length > 0) {
|
|
593
|
+
lines.push("## Links");
|
|
594
|
+
for (const linkKey of linkKeys) {
|
|
595
|
+
const targets = data.internalLinks[linkKey];
|
|
596
|
+
lines.push(`- \`${linkKey}\` -> ${targets.map((t) => `\`${t}\``).join(", ")}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return lines.length > 0 ? lines.join(`
|
|
600
|
+
`) + `
|
|
601
|
+
` : "";
|
|
602
|
+
}
|
|
603
|
+
function generateDeterministicMarkdown(data, options) {
|
|
604
|
+
const sortedData = sortCodeSurveyData(data);
|
|
605
|
+
const parts = [];
|
|
606
|
+
parts.push(generateProjectMarkdown(sortedData));
|
|
607
|
+
if (options?.includeToc) {
|
|
608
|
+
const tocLines = [];
|
|
609
|
+
tocLines.push("## Table of Contents");
|
|
610
|
+
tocLines.push("- [Dependencies](#dependencies)");
|
|
611
|
+
tocLines.push("- [Files](#files)");
|
|
612
|
+
const fileKeys = Object.keys(sortedData.files).sort();
|
|
613
|
+
for (const fileKey of fileKeys) {
|
|
614
|
+
const anchor = fileKey.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
615
|
+
tocLines.push(` - [${fileKey}](#${anchor})`);
|
|
616
|
+
}
|
|
617
|
+
const nsKeys = Object.keys(sortedData.namespaces).sort();
|
|
618
|
+
if (nsKeys.length > 0) {
|
|
619
|
+
tocLines.push("- [Namespaces](#namespaces)");
|
|
620
|
+
}
|
|
621
|
+
const linkKeys = Object.keys(sortedData.internalLinks).sort();
|
|
622
|
+
if (linkKeys.length > 0) {
|
|
623
|
+
tocLines.push("- [Links](#links)");
|
|
624
|
+
}
|
|
625
|
+
parts.push(tocLines.join(`
|
|
626
|
+
`) + `
|
|
627
|
+
`);
|
|
628
|
+
}
|
|
629
|
+
const depsMd = generateDependenciesMarkdown(sortedData);
|
|
630
|
+
if (depsMd)
|
|
631
|
+
parts.push(depsMd);
|
|
632
|
+
const filesMd = generateFilesMarkdown(sortedData, options);
|
|
633
|
+
if (filesMd)
|
|
634
|
+
parts.push(filesMd);
|
|
635
|
+
const nsMd = generateNamespacesMarkdown(sortedData);
|
|
636
|
+
if (nsMd)
|
|
637
|
+
parts.push(nsMd);
|
|
638
|
+
const linksMd = generateLinksMarkdown(sortedData);
|
|
639
|
+
if (linksMd)
|
|
640
|
+
parts.push(linksMd);
|
|
641
|
+
return parts.join(`
|
|
642
|
+
`);
|
|
643
|
+
}
|
|
644
|
+
function writeSurveyOutput(schemaObj, options) {
|
|
645
|
+
if (!options.output)
|
|
646
|
+
return;
|
|
647
|
+
let format = options.format;
|
|
648
|
+
if (!format) {
|
|
649
|
+
const ext = path6.extname(options.output).toLowerCase();
|
|
650
|
+
if (ext === ".yaml" || ext === ".yml") {
|
|
651
|
+
format = "yaml";
|
|
652
|
+
} else if (ext === ".md" || ext === ".markdown") {
|
|
653
|
+
format = "markdown";
|
|
654
|
+
} else if (ext === ".mermaid" || ext === ".mmd") {
|
|
655
|
+
format = "mermaid";
|
|
656
|
+
} else {
|
|
657
|
+
format = "json";
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (options.split) {
|
|
661
|
+
if (!fs4.existsSync(options.output)) {
|
|
662
|
+
fs4.mkdirSync(options.output, { recursive: true });
|
|
663
|
+
}
|
|
664
|
+
const ext = format === "yaml" ? ".yaml" : format === "markdown" ? ".md" : format === "mermaid" ? ".mermaid" : ".json";
|
|
665
|
+
if (format === "json") {
|
|
666
|
+
fs4.writeFileSync(path6.join(options.output, `project${ext}`), generateDeterministicJson({ version: schemaObj.version, project: schemaObj.project }), "utf-8");
|
|
667
|
+
fs4.writeFileSync(path6.join(options.output, `dependencies${ext}`), generateDeterministicJson({ packageDependencies: schemaObj.packageDependencies }), "utf-8");
|
|
668
|
+
fs4.writeFileSync(path6.join(options.output, `files${ext}`), generateDeterministicJson({ files: schemaObj.files }), "utf-8");
|
|
669
|
+
fs4.writeFileSync(path6.join(options.output, `namespaces${ext}`), generateDeterministicJson({ namespaces: schemaObj.namespaces }), "utf-8");
|
|
670
|
+
fs4.writeFileSync(path6.join(options.output, `links${ext}`), generateDeterministicJson({ externalImports: schemaObj.externalImports, internalLinks: schemaObj.internalLinks }), "utf-8");
|
|
671
|
+
} else if (format === "yaml") {
|
|
672
|
+
fs4.writeFileSync(path6.join(options.output, `project${ext}`), generateDeterministicYaml({ version: schemaObj.version, project: schemaObj.project }), "utf-8");
|
|
673
|
+
fs4.writeFileSync(path6.join(options.output, `dependencies${ext}`), generateDeterministicYaml({ packageDependencies: schemaObj.packageDependencies }), "utf-8");
|
|
674
|
+
fs4.writeFileSync(path6.join(options.output, `files${ext}`), generateDeterministicYaml({ files: schemaObj.files }), "utf-8");
|
|
675
|
+
fs4.writeFileSync(path6.join(options.output, `namespaces${ext}`), generateDeterministicYaml({ namespaces: schemaObj.namespaces }), "utf-8");
|
|
676
|
+
fs4.writeFileSync(path6.join(options.output, `links${ext}`), generateDeterministicYaml({ externalImports: schemaObj.externalImports, internalLinks: schemaObj.internalLinks }), "utf-8");
|
|
677
|
+
} else if (format === "markdown") {
|
|
678
|
+
fs4.writeFileSync(path6.join(options.output, `project${ext}`), generateProjectMarkdown(schemaObj), "utf-8");
|
|
679
|
+
fs4.writeFileSync(path6.join(options.output, `dependencies${ext}`), generateDependenciesMarkdown(schemaObj), "utf-8");
|
|
680
|
+
fs4.writeFileSync(path6.join(options.output, `files${ext}`), generateFilesMarkdown(schemaObj), "utf-8");
|
|
681
|
+
fs4.writeFileSync(path6.join(options.output, `namespaces${ext}`), generateNamespacesMarkdown(schemaObj), "utf-8");
|
|
682
|
+
fs4.writeFileSync(path6.join(options.output, `links${ext}`), generateLinksMarkdown(schemaObj), "utf-8");
|
|
683
|
+
} else if (format === "mermaid") {
|
|
684
|
+
fs4.writeFileSync(path6.join(options.output, `links${ext}`), generateDeterministicMermaid(schemaObj), "utf-8");
|
|
685
|
+
}
|
|
686
|
+
} else {
|
|
687
|
+
const outputStr = format === "yaml" ? generateDeterministicYaml(schemaObj) : format === "markdown" ? generateDeterministicMarkdown(schemaObj, { includeToc: options.includeToc }) : format === "mermaid" ? generateDeterministicMermaid(schemaObj) : generateDeterministicJson(schemaObj);
|
|
688
|
+
const outputDir = path6.dirname(options.output);
|
|
689
|
+
if (!fs4.existsSync(outputDir)) {
|
|
690
|
+
fs4.mkdirSync(outputDir, { recursive: true });
|
|
691
|
+
}
|
|
692
|
+
fs4.writeFileSync(options.output, outputStr, "utf-8");
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// src/cache/cache.ts
|
|
697
|
+
import * as fs5 from "node:fs";
|
|
698
|
+
import * as path7 from "node:path";
|
|
699
|
+
import * as crypto2 from "node:crypto";
|
|
700
|
+
function loadCache(rootDir, options) {
|
|
701
|
+
const optionsHash = crypto2.createHash("md5").update(JSON.stringify({
|
|
702
|
+
includeInternalVars: !!options.includeInternalVars,
|
|
703
|
+
includeDocs: !!options.includeDocs,
|
|
704
|
+
includeSignatures: !!options.includeSignatures
|
|
705
|
+
})).digest("hex");
|
|
706
|
+
const cacheDir = path7.join(rootDir, ".code-survey");
|
|
707
|
+
const cacheFile = path7.join(cacheDir, "cache.json");
|
|
708
|
+
let cache = {
|
|
709
|
+
optionsHash,
|
|
710
|
+
files: {}
|
|
711
|
+
};
|
|
712
|
+
if (fs5.existsSync(cacheFile)) {
|
|
713
|
+
try {
|
|
714
|
+
const rawCache = JSON.parse(fs5.readFileSync(cacheFile, "utf-8"));
|
|
715
|
+
if (rawCache && rawCache.optionsHash === optionsHash && rawCache.files) {
|
|
716
|
+
cache = rawCache;
|
|
717
|
+
}
|
|
718
|
+
} catch {}
|
|
719
|
+
}
|
|
720
|
+
return { cache, cacheDir, cacheFile };
|
|
721
|
+
}
|
|
722
|
+
function saveCache(cacheDir, cacheFile, cache) {
|
|
723
|
+
try {
|
|
724
|
+
if (!fs5.existsSync(cacheDir)) {
|
|
725
|
+
fs5.mkdirSync(cacheDir, { recursive: true });
|
|
726
|
+
}
|
|
727
|
+
fs5.writeFileSync(cacheFile, JSON.stringify(cache, null, 2), "utf-8");
|
|
728
|
+
} catch {}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/git/git.ts
|
|
732
|
+
import { execSync } from "node:child_process";
|
|
733
|
+
function getGitChangedFiles(rootDir, diffTarget) {
|
|
734
|
+
const changed = new Set;
|
|
735
|
+
try {
|
|
736
|
+
const target = typeof diffTarget === "string" ? diffTarget : "HEAD";
|
|
737
|
+
const diffOutput = execSync(`git diff --name-only ${target}`, { cwd: rootDir, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
|
|
738
|
+
for (const line of diffOutput.split(`
|
|
739
|
+
`)) {
|
|
740
|
+
const trimmed = line.trim();
|
|
741
|
+
if (trimmed)
|
|
742
|
+
changed.add(trimmed);
|
|
743
|
+
}
|
|
744
|
+
const statusOutput = execSync("git status --porcelain", { cwd: rootDir, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
|
|
745
|
+
for (const line of statusOutput.split(`
|
|
746
|
+
`)) {
|
|
747
|
+
const trimmed = line.trim();
|
|
748
|
+
if (trimmed) {
|
|
749
|
+
const filePath = trimmed.substring(3).trim();
|
|
750
|
+
if (filePath)
|
|
751
|
+
changed.add(filePath);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
} catch {}
|
|
755
|
+
return changed;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/index.ts
|
|
759
|
+
async function createCodeSurvey(options) {
|
|
760
|
+
let rootDir = path8.resolve(options.root);
|
|
761
|
+
let tempDir = null;
|
|
762
|
+
try {
|
|
763
|
+
if (options.remote) {
|
|
764
|
+
tempDir = fs6.mkdtempSync(path8.join(os2.tmpdir(), "code-survey-remote-"));
|
|
765
|
+
const refArg = options.ref ? `--branch ${options.ref} ` : "";
|
|
766
|
+
execSync2(`git clone --depth 1 ${refArg}${options.remote} "${tempDir}"`, { stdio: ["ignore", "ignore", "pipe"] });
|
|
767
|
+
rootDir = tempDir;
|
|
768
|
+
}
|
|
769
|
+
const ignoreFile = path8.join(rootDir, ".codesurveyignore");
|
|
770
|
+
const ignoreExcludes = [];
|
|
771
|
+
if (fs6.existsSync(ignoreFile)) {
|
|
772
|
+
const content = fs6.readFileSync(ignoreFile, "utf-8");
|
|
773
|
+
for (const line of content.split(`
|
|
774
|
+
`)) {
|
|
775
|
+
const trimmed = line.trim();
|
|
776
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
777
|
+
ignoreExcludes.push(trimmed);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const excludes = [...options.excludes || [], ...ignoreExcludes];
|
|
782
|
+
const scannedFiles = scanDirectory(rootDir, excludes, options.maxDepth);
|
|
783
|
+
let filesToProcess = scannedFiles;
|
|
784
|
+
if (options.diff) {
|
|
785
|
+
const changedFiles = getGitChangedFiles(rootDir, options.diff);
|
|
786
|
+
filesToProcess = scannedFiles.filter((f) => changedFiles.has(f.relativePath));
|
|
787
|
+
}
|
|
788
|
+
const symbolsCount = {
|
|
789
|
+
class: 0,
|
|
790
|
+
interface: 0,
|
|
791
|
+
struct: 0,
|
|
792
|
+
method: 0,
|
|
793
|
+
function: 0,
|
|
794
|
+
variable: 0,
|
|
795
|
+
type: 0
|
|
796
|
+
};
|
|
797
|
+
const parser = new CodebaseParser;
|
|
798
|
+
const extensionsToInitialize = new Set(filesToProcess.map((f) => path8.extname(f.relativePath)));
|
|
799
|
+
await parser.initialize(extensionsToInitialize);
|
|
800
|
+
const files = {};
|
|
801
|
+
const namespaces = {};
|
|
802
|
+
const detectedLanguages = new Set;
|
|
803
|
+
const { cache, cacheDir, cacheFile } = loadCache(rootDir, options);
|
|
804
|
+
const supportedExtensions = new Set([
|
|
805
|
+
".ts",
|
|
806
|
+
".tsx",
|
|
807
|
+
".js",
|
|
808
|
+
".jsx",
|
|
809
|
+
".py",
|
|
810
|
+
".go",
|
|
811
|
+
".java",
|
|
812
|
+
".cs",
|
|
813
|
+
".rs"
|
|
814
|
+
]);
|
|
815
|
+
for (const file of filesToProcess) {
|
|
816
|
+
const ext = path8.extname(file.relativePath);
|
|
817
|
+
if (!supportedExtensions.has(ext)) {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
let lang = "";
|
|
821
|
+
if (ext === ".ts" || ext === ".tsx") {
|
|
822
|
+
lang = "typescript";
|
|
823
|
+
} else if (ext === ".js" || ext === ".jsx") {
|
|
824
|
+
lang = "javascript";
|
|
825
|
+
} else if (ext === ".py") {
|
|
826
|
+
lang = "python";
|
|
827
|
+
} else if (ext === ".go") {
|
|
828
|
+
lang = "go";
|
|
829
|
+
} else if (ext === ".java") {
|
|
830
|
+
lang = "java";
|
|
831
|
+
} else if (ext === ".cs") {
|
|
832
|
+
lang = "csharp";
|
|
833
|
+
} else if (ext === ".rs") {
|
|
834
|
+
lang = "rust";
|
|
835
|
+
}
|
|
836
|
+
if (lang) {
|
|
837
|
+
detectedLanguages.add(lang);
|
|
838
|
+
}
|
|
839
|
+
try {
|
|
840
|
+
let parseResult;
|
|
841
|
+
const cached = cache.files[file.relativePath];
|
|
842
|
+
if (cached && cached.hash === file.hash) {
|
|
843
|
+
parseResult = cached;
|
|
844
|
+
} else {
|
|
845
|
+
const code = fs6.readFileSync(file.absolutePath, "utf-8");
|
|
846
|
+
parseResult = parser.parseFile(file.relativePath, code, {
|
|
847
|
+
includeInternalVars: options.includeInternalVars,
|
|
848
|
+
includeDocs: options.includeDocs,
|
|
849
|
+
includeSignatures: options.includeSignatures
|
|
850
|
+
});
|
|
851
|
+
cache.files[file.relativePath] = {
|
|
852
|
+
hash: file.hash,
|
|
853
|
+
imports: parseResult.imports,
|
|
854
|
+
exports: parseResult.exports,
|
|
855
|
+
symbols: parseResult.symbols,
|
|
856
|
+
namespace: parseResult.namespace
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
const filterSymbols = (list) => {
|
|
860
|
+
if (!options.symbolsFilter)
|
|
861
|
+
return list;
|
|
862
|
+
return list.filter((s) => options.symbolsFilter.includes(s.type));
|
|
863
|
+
};
|
|
864
|
+
const filteredExports = filterSymbols(parseResult.exports);
|
|
865
|
+
const filteredSymbols = filterSymbols(parseResult.symbols);
|
|
866
|
+
for (const exp of filteredExports) {
|
|
867
|
+
if (exp.type in symbolsCount) {
|
|
868
|
+
symbolsCount[exp.type]++;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
for (const sym of filteredSymbols) {
|
|
872
|
+
if (sym.type in symbolsCount) {
|
|
873
|
+
symbolsCount[sym.type]++;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
files[file.relativePath] = {
|
|
877
|
+
hash: file.hash,
|
|
878
|
+
imports: parseResult.imports,
|
|
879
|
+
exports: filteredExports,
|
|
880
|
+
symbols: filteredSymbols
|
|
881
|
+
};
|
|
882
|
+
if (parseResult.namespace) {
|
|
883
|
+
const ns = parseResult.namespace;
|
|
884
|
+
if (!namespaces[ns]) {
|
|
885
|
+
namespaces[ns] = { files: [], exports: [] };
|
|
886
|
+
}
|
|
887
|
+
if (!namespaces[ns].files.includes(file.relativePath)) {
|
|
888
|
+
namespaces[ns].files.push(file.relativePath);
|
|
889
|
+
}
|
|
890
|
+
for (const exp of filteredExports) {
|
|
891
|
+
if (!namespaces[ns].exports.includes(exp.name)) {
|
|
892
|
+
namespaces[ns].exports.push(exp.name);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
} catch {}
|
|
897
|
+
}
|
|
898
|
+
if (!options.remote) {
|
|
899
|
+
saveCache(cacheDir, cacheFile, cache);
|
|
900
|
+
}
|
|
901
|
+
const packageDependencies = parsePackageDependencies(rootDir);
|
|
902
|
+
const { externalImports, internalLinks } = resolveImports(files, namespaces, packageDependencies);
|
|
903
|
+
let projectName = path8.basename(rootDir) || "project";
|
|
904
|
+
if (options.remote) {
|
|
905
|
+
const parts = options.remote.split("/");
|
|
906
|
+
const last = parts[parts.length - 1];
|
|
907
|
+
if (last) {
|
|
908
|
+
projectName = last.replace(/\.git$/, "");
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const schemaObj = {
|
|
912
|
+
version: "1.0.0",
|
|
913
|
+
$schema: "https://raw.githubusercontent.com/username/code-survey/main/schema.json",
|
|
914
|
+
project: {
|
|
915
|
+
name: projectName,
|
|
916
|
+
languages: Array.from(detectedLanguages),
|
|
917
|
+
root: "."
|
|
918
|
+
},
|
|
919
|
+
packageDependencies,
|
|
920
|
+
files,
|
|
921
|
+
namespaces,
|
|
922
|
+
externalImports,
|
|
923
|
+
internalLinks
|
|
924
|
+
};
|
|
925
|
+
writeSurveyOutput(schemaObj, options);
|
|
926
|
+
return {
|
|
927
|
+
filesCount: Object.keys(files).length,
|
|
928
|
+
symbolsCount,
|
|
929
|
+
data: schemaObj
|
|
930
|
+
};
|
|
931
|
+
} finally {
|
|
932
|
+
if (tempDir && fs6.existsSync(tempDir)) {
|
|
933
|
+
try {
|
|
934
|
+
fs6.rmSync(tempDir, { recursive: true, force: true });
|
|
935
|
+
} catch {}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
export {
|
|
940
|
+
createCodeSurvey
|
|
941
|
+
};
|