@craftpipe/contextpack 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/.contextpackrc.example.json +167 -0
- package/.env.example +5 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- package/.github/pull_request_template.md +9 -0
- package/CODE_OF_CONDUCT.md +40 -0
- package/CONTRIBUTING.md +59 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/SECURITY.md +21 -0
- package/index.js +428 -0
- package/lib/analyzer.js +547 -0
- package/lib/bundler.js +477 -0
- package/lib/config.js +269 -0
- package/lib/license.js +180 -0
- package/lib/premium/config-file.js +917 -0
- package/lib/premium/gate.js +13 -0
- package/lib/premium/html-report.js +1094 -0
- package/lib/premium/index.js +57 -0
- package/lib/premium/watch-mode.js +627 -0
- package/lib/scanner.js +480 -0
- package/lib/tokenizer.js +291 -0
- package/lib/validator.js +561 -0
- package/package.json +12 -0
- package/tests/analyzer.test.mjs +128 -0
- package/tests/bundler.test.mjs +126 -0
- package/tests/config.test.mjs +103 -0
- package/tests/gate.test.mjs +118 -0
- package/tests/index.test.mjs +103 -0
- package/tests/license.test.mjs +97 -0
- package/tests/scanner.test.mjs +110 -0
- package/tests/tokenizer.test.mjs +103 -0
- package/tests/validator.test.mjs +111 -0
- package/vitest.config.mjs +13 -0
package/lib/analyzer.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Supported source file extensions for symbol extraction
|
|
8
|
+
*/
|
|
9
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
10
|
+
'.js', '.mjs', '.cjs', '.jsx',
|
|
11
|
+
'.ts', '.tsx',
|
|
12
|
+
'.py', '.rb', '.go', '.java', '.cs', '.php', '.swift', '.kt',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Regex patterns for extracting symbols from JavaScript/TypeScript source
|
|
17
|
+
*/
|
|
18
|
+
const JS_PATTERNS = {
|
|
19
|
+
functionDecl: /^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/gm,
|
|
20
|
+
arrowFunction: /^(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>/gm,
|
|
21
|
+
classDecl: /^(?:export\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)(?:\s+extends\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s*\{/gm,
|
|
22
|
+
moduleExports: /module\.exports\s*=\s*\{([^}]+)\}/gs,
|
|
23
|
+
moduleExportsSingle: /module\.exports\s*=\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*;/gm,
|
|
24
|
+
namedExport: /^export\s+(?:const|let|var|function|class|async\s+function)\s+([A-Za-z_$][A-Za-z0-9_$]*)/gm,
|
|
25
|
+
exportDefault: /^export\s+default\s+(?:class|function)?\s*([A-Za-z_$][A-Za-z0-9_$]*)?/gm,
|
|
26
|
+
requireImport: /(?:const|let|var)\s+(?:\{[^}]+\}|[A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm,
|
|
27
|
+
esImport: /^import\s+(?:[^'"]+\s+from\s+)?['"]([^'"]+)['"]/gm,
|
|
28
|
+
dynamicImport: /import\s*\(\s*['"]([^'"]+)['"]\s*\)/gm,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Determine if a file is a JavaScript/TypeScript file
|
|
33
|
+
* @param {string} filePath - Path to the file
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
function isJSFile(filePath) {
|
|
37
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
38
|
+
return ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx'].includes(ext);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Determine if a file is a source code file worth analyzing
|
|
43
|
+
* @param {string} filePath - Path to the file
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function isSourceFile(filePath) {
|
|
47
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
48
|
+
return SOURCE_EXTENSIONS.has(ext);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read file content safely, returning null on error
|
|
53
|
+
* @param {string} filePath - Absolute path to file
|
|
54
|
+
* @returns {string|null}
|
|
55
|
+
*/
|
|
56
|
+
function readFileSafe(filePath) {
|
|
57
|
+
try {
|
|
58
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract all unique regex matches for a capture group
|
|
66
|
+
* @param {string} source - Source text
|
|
67
|
+
* @param {RegExp} pattern - Regex with at least one capture group
|
|
68
|
+
* @returns {string[]}
|
|
69
|
+
*/
|
|
70
|
+
function extractMatches(source, pattern) {
|
|
71
|
+
const results = [];
|
|
72
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
73
|
+
let match;
|
|
74
|
+
while ((match = regex.exec(source)) !== null) {
|
|
75
|
+
if (match[1] && match[1].trim()) {
|
|
76
|
+
results.push(match[1].trim());
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extract symbols from JavaScript/TypeScript source
|
|
84
|
+
* @param {string} source - File source code
|
|
85
|
+
* @returns {{ functions: string[], classes: string[], exports: string[] }}
|
|
86
|
+
*/
|
|
87
|
+
function extractJSSymbols(source) {
|
|
88
|
+
const functions = new Set();
|
|
89
|
+
const classes = new Set();
|
|
90
|
+
const exports = new Set();
|
|
91
|
+
|
|
92
|
+
// Named function declarations
|
|
93
|
+
extractMatches(source, JS_PATTERNS.functionDecl).forEach(name => functions.add(name));
|
|
94
|
+
|
|
95
|
+
// Arrow functions assigned to variables
|
|
96
|
+
extractMatches(source, JS_PATTERNS.arrowFunction).forEach(name => functions.add(name));
|
|
97
|
+
|
|
98
|
+
// Class declarations
|
|
99
|
+
extractMatches(source, JS_PATTERNS.classDecl).forEach(name => classes.add(name));
|
|
100
|
+
|
|
101
|
+
// Named ES exports
|
|
102
|
+
extractMatches(source, JS_PATTERNS.namedExport).forEach(name => exports.add(name));
|
|
103
|
+
|
|
104
|
+
// module.exports = { ... }
|
|
105
|
+
const moduleExportsBlock = new RegExp(JS_PATTERNS.moduleExports.source, JS_PATTERNS.moduleExports.flags);
|
|
106
|
+
let blockMatch;
|
|
107
|
+
while ((blockMatch = moduleExportsBlock.exec(source)) !== null) {
|
|
108
|
+
const block = blockMatch[1];
|
|
109
|
+
const keys = block.match(/([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::|,|\n)/g) || [];
|
|
110
|
+
keys.forEach(k => {
|
|
111
|
+
const name = k.replace(/[\s:,\n]/g, '');
|
|
112
|
+
if (name) exports.add(name);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// module.exports = singleName
|
|
117
|
+
extractMatches(source, JS_PATTERNS.moduleExportsSingle).forEach(name => exports.add(name));
|
|
118
|
+
|
|
119
|
+
// export default
|
|
120
|
+
extractMatches(source, JS_PATTERNS.exportDefault).forEach(name => {
|
|
121
|
+
if (name) exports.add(name);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
functions: Array.from(functions),
|
|
126
|
+
classes: Array.from(classes),
|
|
127
|
+
exports: Array.from(exports),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract import/dependency paths from JavaScript/TypeScript source
|
|
133
|
+
* @param {string} source - File source code
|
|
134
|
+
* @param {string} filePath - Path of the file being analyzed (for resolving relative imports)
|
|
135
|
+
* @returns {{ imports: string[], localDeps: string[], externalDeps: string[] }}
|
|
136
|
+
*/
|
|
137
|
+
function extractJSDependencies(source, filePath) {
|
|
138
|
+
const allImports = new Set();
|
|
139
|
+
|
|
140
|
+
extractMatches(source, JS_PATTERNS.requireImport).forEach(dep => allImports.add(dep));
|
|
141
|
+
extractMatches(source, JS_PATTERNS.esImport).forEach(dep => allImports.add(dep));
|
|
142
|
+
extractMatches(source, JS_PATTERNS.dynamicImport).forEach(dep => allImports.add(dep));
|
|
143
|
+
|
|
144
|
+
const localDeps = [];
|
|
145
|
+
const externalDeps = [];
|
|
146
|
+
|
|
147
|
+
for (const imp of allImports) {
|
|
148
|
+
if (imp.startsWith('.') || imp.startsWith('/')) {
|
|
149
|
+
localDeps.push(imp);
|
|
150
|
+
} else {
|
|
151
|
+
// Extract package name (handle scoped packages)
|
|
152
|
+
const parts = imp.split('/');
|
|
153
|
+
const pkgName = imp.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
154
|
+
if (!externalDeps.includes(pkgName)) {
|
|
155
|
+
externalDeps.push(pkgName);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
imports: Array.from(allImports),
|
|
162
|
+
localDeps,
|
|
163
|
+
externalDeps,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generate a brief summary of a file based on its content and symbols
|
|
169
|
+
* @param {string} filePath - Path to the file
|
|
170
|
+
* @param {string} source - File source code
|
|
171
|
+
* @param {object} symbols - Extracted symbols
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
function generateSummary(filePath, source, symbols) {
|
|
175
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
176
|
+
const lines = source.split('\n').length;
|
|
177
|
+
const parts = [];
|
|
178
|
+
|
|
179
|
+
parts.push(`${lines} lines`);
|
|
180
|
+
|
|
181
|
+
if (symbols.classes && symbols.classes.length > 0) {
|
|
182
|
+
parts.push(`classes: ${symbols.classes.join(', ')}`);
|
|
183
|
+
}
|
|
184
|
+
if (symbols.functions && symbols.functions.length > 0) {
|
|
185
|
+
const shown = symbols.functions.slice(0, 5);
|
|
186
|
+
const extra = symbols.functions.length - shown.length;
|
|
187
|
+
parts.push(`functions: ${shown.join(', ')}${extra > 0 ? ` +${extra} more` : ''}`);
|
|
188
|
+
}
|
|
189
|
+
if (symbols.exports && symbols.exports.length > 0) {
|
|
190
|
+
parts.push(`exports: ${symbols.exports.join(', ')}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Try to extract first JSDoc or block comment as description
|
|
194
|
+
const docMatch = source.match(/\/\*\*\s*([\s\S]*?)\s*\*\//);
|
|
195
|
+
if (docMatch) {
|
|
196
|
+
const docText = docMatch[1]
|
|
197
|
+
.replace(/\s*\*\s*/g, ' ')
|
|
198
|
+
.replace(/@\w+[^\n]*/g, '')
|
|
199
|
+
.trim()
|
|
200
|
+
.slice(0, 120);
|
|
201
|
+
if (docText) parts.unshift(docText);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return parts.join('; ');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Resolve a local import path relative to the importing file
|
|
209
|
+
* @param {string} importingFile - Absolute path of the file doing the import
|
|
210
|
+
* @param {string} importPath - The import string (e.g. './utils')
|
|
211
|
+
* @param {string} projectRoot - Root directory of the project
|
|
212
|
+
* @returns {string|null} Resolved relative path from project root, or null
|
|
213
|
+
*/
|
|
214
|
+
function resolveLocalImport(importingFile, importPath, projectRoot) {
|
|
215
|
+
try {
|
|
216
|
+
const dir = path.dirname(importingFile);
|
|
217
|
+
let resolved = path.resolve(dir, importPath);
|
|
218
|
+
|
|
219
|
+
// Try exact path first
|
|
220
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
|
|
221
|
+
return path.relative(projectRoot, resolved).replace(/\\/g, '/');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Try adding common extensions
|
|
225
|
+
const extensions = ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.json'];
|
|
226
|
+
for (const ext of extensions) {
|
|
227
|
+
const candidate = resolved + ext;
|
|
228
|
+
if (fs.existsSync(candidate)) {
|
|
229
|
+
return path.relative(projectRoot, candidate).replace(/\\/g, '/');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Try index files
|
|
234
|
+
for (const ext of extensions) {
|
|
235
|
+
const candidate = path.join(resolved, 'index' + ext);
|
|
236
|
+
if (fs.existsSync(candidate)) {
|
|
237
|
+
return path.relative(projectRoot, candidate).replace(/\\/g, '/');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Analyzes a single file and returns its symbol map, dependencies, and summary.
|
|
249
|
+
* @param {string} filePath - Absolute or relative path to the file
|
|
250
|
+
* @param {object} [options] - Optional configuration
|
|
251
|
+
* @param {string} [options.projectRoot] - Root directory for resolving relative paths
|
|
252
|
+
* @returns {{
|
|
253
|
+
* filePath: string,
|
|
254
|
+
* relativePath: string,
|
|
255
|
+
* extension: string,
|
|
256
|
+
* lines: number,
|
|
257
|
+
* symbols: { functions: string[], classes: string[], exports: string[] },
|
|
258
|
+
* dependencies: { imports: string[], localDeps: string[], externalDeps: string[] },
|
|
259
|
+
* summary: string,
|
|
260
|
+
* error: string|null
|
|
261
|
+
* }}
|
|
262
|
+
*/
|
|
263
|
+
function analyzeFile(filePath, options) {
|
|
264
|
+
const opts = options || {};
|
|
265
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
266
|
+
|
|
267
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
268
|
+
return {
|
|
269
|
+
filePath: '',
|
|
270
|
+
relativePath: '',
|
|
271
|
+
extension: '',
|
|
272
|
+
lines: 0,
|
|
273
|
+
symbols: { functions: [], classes: [], exports: [] },
|
|
274
|
+
dependencies: { imports: [], localDeps: [], externalDeps: [] },
|
|
275
|
+
summary: 'Invalid file path provided',
|
|
276
|
+
error: 'Invalid file path',
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
281
|
+
? filePath
|
|
282
|
+
: path.resolve(projectRoot, filePath);
|
|
283
|
+
|
|
284
|
+
const relativePath = path.relative(projectRoot, absolutePath).replace(/\\/g, '/');
|
|
285
|
+
const extension = path.extname(absolutePath).toLowerCase();
|
|
286
|
+
|
|
287
|
+
const source = readFileSafe(absolutePath);
|
|
288
|
+
|
|
289
|
+
if (source === null) {
|
|
290
|
+
return {
|
|
291
|
+
filePath: absolutePath,
|
|
292
|
+
relativePath,
|
|
293
|
+
extension,
|
|
294
|
+
lines: 0,
|
|
295
|
+
symbols: { functions: [], classes: [], exports: [] },
|
|
296
|
+
dependencies: { imports: [], localDeps: [], externalDeps: [] },
|
|
297
|
+
summary: 'Could not read file',
|
|
298
|
+
error: 'File read error',
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const lines = source.split('\n').length;
|
|
303
|
+
let symbols = { functions: [], classes: [], exports: [] };
|
|
304
|
+
let dependencies = { imports: [], localDeps: [], externalDeps: [] };
|
|
305
|
+
|
|
306
|
+
if (isJSFile(absolutePath)) {
|
|
307
|
+
symbols = extractJSSymbols(source);
|
|
308
|
+
dependencies = extractJSDependencies(source, absolutePath);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const summary = generateSummary(absolutePath, source, symbols);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
filePath: absolutePath,
|
|
315
|
+
relativePath,
|
|
316
|
+
extension,
|
|
317
|
+
lines,
|
|
318
|
+
symbols,
|
|
319
|
+
dependencies,
|
|
320
|
+
summary,
|
|
321
|
+
error: null,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Analyzes all files in a project and returns a structured symbol map and dependency graph.
|
|
327
|
+
* @param {string[]} filePaths - Array of absolute file paths to analyze
|
|
328
|
+
* @param {object} [options] - Optional configuration
|
|
329
|
+
* @param {string} [options.projectRoot] - Root directory of the project
|
|
330
|
+
* @param {boolean} [options.sourceOnly] - If true, skip non-source files
|
|
331
|
+
* @returns {{
|
|
332
|
+
* files: object,
|
|
333
|
+
* symbolMap: object,
|
|
334
|
+
* dependencyGraph: object,
|
|
335
|
+
* stats: { totalFiles: number, analyzedFiles: number, totalSymbols: number, errors: number }
|
|
336
|
+
* }}
|
|
337
|
+
*/
|
|
338
|
+
function analyzeProject(filePaths, options) {
|
|
339
|
+
const opts = options || {};
|
|
340
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
341
|
+
const sourceOnly = opts.sourceOnly !== false;
|
|
342
|
+
|
|
343
|
+
if (!filePaths || !Array.isArray(filePaths)) {
|
|
344
|
+
return {
|
|
345
|
+
files: {},
|
|
346
|
+
symbolMap: {},
|
|
347
|
+
dependencyGraph: {},
|
|
348
|
+
stats: { totalFiles: 0, analyzedFiles: 0, totalSymbols: 0, errors: 0 },
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const files = {};
|
|
353
|
+
const symbolMap = {};
|
|
354
|
+
let errors = 0;
|
|
355
|
+
|
|
356
|
+
const filteredPaths = sourceOnly
|
|
357
|
+
? filePaths.filter(fp => isSourceFile(fp))
|
|
358
|
+
: filePaths;
|
|
359
|
+
|
|
360
|
+
for (const fp of filteredPaths) {
|
|
361
|
+
const result = analyzeFile(fp, { projectRoot });
|
|
362
|
+
|
|
363
|
+
files[result.relativePath] = result;
|
|
364
|
+
|
|
365
|
+
if (result.error) {
|
|
366
|
+
errors++;
|
|
367
|
+
} else {
|
|
368
|
+
// Build symbol map: symbol name -> list of files that define it
|
|
369
|
+
const allSymbols = [
|
|
370
|
+
...result.symbols.functions,
|
|
371
|
+
...result.symbols.classes,
|
|
372
|
+
...result.symbols.exports,
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
for (const sym of allSymbols) {
|
|
376
|
+
if (!symbolMap[sym]) {
|
|
377
|
+
symbolMap[sym] = [];
|
|
378
|
+
}
|
|
379
|
+
if (!symbolMap[sym].includes(result.relativePath)) {
|
|
380
|
+
symbolMap[sym].push(result.relativePath);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const dependencyGraph = buildDependencyGraph(files, { projectRoot });
|
|
387
|
+
|
|
388
|
+
const totalSymbols = Object.keys(symbolMap).length;
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
files,
|
|
392
|
+
symbolMap,
|
|
393
|
+
dependencyGraph,
|
|
394
|
+
stats: {
|
|
395
|
+
totalFiles: filePaths.length,
|
|
396
|
+
analyzedFiles: filteredPaths.length,
|
|
397
|
+
totalSymbols,
|
|
398
|
+
errors,
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Extracts symbols (functions, classes, exports) from a source string or file path.
|
|
405
|
+
* @param {string} input - Either source code string or a file path
|
|
406
|
+
* @param {object} [options] - Options
|
|
407
|
+
* @param {boolean} [options.isPath] - If true, treat input as a file path and read it
|
|
408
|
+
* @param {string} [options.language] - Language hint ('js', 'ts', etc.)
|
|
409
|
+
* @returns {{ functions: string[], classes: string[], exports: string[] }}
|
|
410
|
+
*/
|
|
411
|
+
function extractSymbols(input, options) {
|
|
412
|
+
const opts = options || {};
|
|
413
|
+
|
|
414
|
+
if (!input || typeof input !== 'string') {
|
|
415
|
+
return { functions: [], classes: [], exports: [] };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let source = input;
|
|
419
|
+
let filePath = '';
|
|
420
|
+
|
|
421
|
+
if (opts.isPath) {
|
|
422
|
+
filePath = input;
|
|
423
|
+
source = readFileSafe(input);
|
|
424
|
+
if (source === null) {
|
|
425
|
+
return { functions: [], classes: [], exports: [] };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const language = opts.language || (filePath ? path.extname(filePath).toLowerCase().replace('.', '') : 'js');
|
|
430
|
+
const jsLangs = new Set(['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx']);
|
|
431
|
+
|
|
432
|
+
if (jsLangs.has(language)) {
|
|
433
|
+
return extractJSSymbols(source);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// For other languages, do a best-effort extraction using common patterns
|
|
437
|
+
const functions = [];
|
|
438
|
+
const classes = [];
|
|
439
|
+
const exports = [];
|
|
440
|
+
|
|
441
|
+
// Generic function-like patterns
|
|
442
|
+
const genericFn = /(?:def|func|function|fn|sub|method)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/gm;
|
|
443
|
+
let m;
|
|
444
|
+
while ((m = genericFn.exec(source)) !== null) {
|
|
445
|
+
if (m[1]) functions.push(m[1]);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Generic class patterns
|
|
449
|
+
const genericClass = /class\s+([A-Za-z_][A-Za-z0-9_]*)/gm;
|
|
450
|
+
while ((m = genericClass.exec(source)) !== null) {
|
|
451
|
+
if (m[1]) classes.push(m[1]);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return { functions, classes, exports };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Builds a dependency graph from analyzed file results.
|
|
459
|
+
* @param {object} files - Map of relativePath -> analyzeFile result
|
|
460
|
+
* @param {object} [options] - Options
|
|
461
|
+
* @param {string} [options.projectRoot] - Project root for resolving paths
|
|
462
|
+
* @returns {{
|
|
463
|
+
* nodes: string[],
|
|
464
|
+
* edges: Array<{ from: string, to: string, type: string }>,
|
|
465
|
+
* adjacency: object,
|
|
466
|
+
* reverseAdjacency: object
|
|
467
|
+
* }}
|
|
468
|
+
*/
|
|
469
|
+
function buildDependencyGraph(files, options) {
|
|
470
|
+
const opts = options || {};
|
|
471
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
472
|
+
|
|
473
|
+
if (!files || typeof files !== 'object') {
|
|
474
|
+
return {
|
|
475
|
+
nodes: [],
|
|
476
|
+
edges: [],
|
|
477
|
+
adjacency: {},
|
|
478
|
+
reverseAdjacency: {},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const nodes = Object.keys(files);
|
|
483
|
+
const edges = [];
|
|
484
|
+
const adjacency = {};
|
|
485
|
+
const reverseAdjacency = {};
|
|
486
|
+
|
|
487
|
+
// Initialize adjacency maps
|
|
488
|
+
for (const node of nodes) {
|
|
489
|
+
adjacency[node] = [];
|
|
490
|
+
reverseAdjacency[node] = [];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
for (const [relPath, fileResult] of Object.entries(files)) {
|
|
494
|
+
if (!fileResult || !fileResult.dependencies) continue;
|
|
495
|
+
|
|
496
|
+
const { localDeps } = fileResult.dependencies;
|
|
497
|
+
if (!Array.isArray(localDeps)) continue;
|
|
498
|
+
|
|
499
|
+
const absoluteImportingFile = fileResult.filePath || path.resolve(projectRoot, relPath);
|
|
500
|
+
|
|
501
|
+
for (const dep of localDeps) {
|
|
502
|
+
const resolvedRel = resolveLocalImport(absoluteImportingFile, dep, projectRoot);
|
|
503
|
+
|
|
504
|
+
if (resolvedRel && files[resolvedRel]) {
|
|
505
|
+
// Avoid duplicate edges
|
|
506
|
+
const exists = edges.some(e => e.from === relPath && e.to === resolvedRel);
|
|
507
|
+
if (!exists) {
|
|
508
|
+
edges.push({ from: relPath, to: resolvedRel, type: 'local' });
|
|
509
|
+
|
|
510
|
+
if (!adjacency[relPath]) adjacency[relPath] = [];
|
|
511
|
+
if (!adjacency[relPath].includes(resolvedRel)) {
|
|
512
|
+
adjacency[relPath].push(resolvedRel);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (!reverseAdjacency[resolvedRel]) reverseAdjacency[resolvedRel] = [];
|
|
516
|
+
if (!reverseAdjacency[resolvedRel].includes(relPath)) {
|
|
517
|
+
reverseAdjacency[resolvedRel].push(relPath);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} else if (resolvedRel) {
|
|
521
|
+
// Dependency exists on disk but wasn't in the analyzed set
|
|
522
|
+
const exists = edges.some(e => e.from === relPath && e.to === resolvedRel);
|
|
523
|
+
if (!exists) {
|
|
524
|
+
edges.push({ from: relPath, to: resolvedRel, type: 'external-local' });
|
|
525
|
+
if (!adjacency[relPath]) adjacency[relPath] = [];
|
|
526
|
+
if (!adjacency[relPath].includes(resolvedRel)) {
|
|
527
|
+
adjacency[relPath].push(resolvedRel);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
nodes,
|
|
536
|
+
edges,
|
|
537
|
+
adjacency,
|
|
538
|
+
reverseAdjacency,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
module.exports = {
|
|
543
|
+
analyzeFile,
|
|
544
|
+
analyzeProject,
|
|
545
|
+
extractSymbols,
|
|
546
|
+
buildDependencyGraph,
|
|
547
|
+
};
|