@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.
@@ -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
+ };