@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/lib/bundler.js ADDED
@@ -0,0 +1,477 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { analyzeProject, buildDependencyGraph } = require('./analyzer');
5
+ const { scanProject } = require('./scanner');
6
+
7
+ /**
8
+ * Creates a compact context bundle from scanner and analyzer output
9
+ * @param {string} projectDir - Root directory of the project to bundle
10
+ * @param {object} options - Optional configuration options
11
+ * @param {string[]} [options.include] - Glob patterns to include
12
+ * @param {string[]} [options.exclude] - Glob patterns to exclude
13
+ * @param {boolean} [options.includeDependencyMap] - Whether to include dependency map
14
+ * @param {boolean} [options.includeSymbolIndex] - Whether to include symbol index
15
+ * @param {number} [options.maxFileSummaryLength] - Max characters per file summary
16
+ * @returns {object} Bundle object with fileSummaries, symbolIndex, dependencyMap, metadata
17
+ */
18
+ function createBundle(projectDir, options) {
19
+ const opts = options || {};
20
+ const {
21
+ include,
22
+ exclude,
23
+ includeDependencyMap = true,
24
+ includeSymbolIndex = true,
25
+ maxFileSummaryLength = 500,
26
+ } = opts;
27
+
28
+ if (!projectDir || typeof projectDir !== 'string') {
29
+ return {
30
+ metadata: {
31
+ error: 'Invalid projectDir provided',
32
+ createdAt: new Date().toISOString(),
33
+ projectDir: projectDir || null,
34
+ fileCount: 0,
35
+ totalSize: 0,
36
+ },
37
+ fileSummaries: [],
38
+ symbolIndex: {},
39
+ dependencyMap: {},
40
+ };
41
+ }
42
+
43
+ let scanResult;
44
+ try {
45
+ scanResult = scanProject(projectDir, { include, exclude });
46
+ } catch (err) {
47
+ return {
48
+ metadata: {
49
+ error: `Scan failed: ${err.message}`,
50
+ createdAt: new Date().toISOString(),
51
+ projectDir,
52
+ fileCount: 0,
53
+ totalSize: 0,
54
+ },
55
+ fileSummaries: [],
56
+ symbolIndex: {},
57
+ dependencyMap: {},
58
+ };
59
+ }
60
+
61
+ const files = (scanResult && Array.isArray(scanResult.files)) ? scanResult.files : [];
62
+
63
+ let analysisResult;
64
+ try {
65
+ analysisResult = analyzeProject(projectDir, { include, exclude });
66
+ } catch (err) {
67
+ analysisResult = { symbolMap: {}, dependencies: {} };
68
+ }
69
+
70
+ const symbolMap = (analysisResult && analysisResult.symbolMap) ? analysisResult.symbolMap : {};
71
+ const dependencies = (analysisResult && analysisResult.dependencies) ? analysisResult.dependencies : {};
72
+
73
+ let dependencyGraph = {};
74
+ if (includeDependencyMap) {
75
+ try {
76
+ dependencyGraph = buildDependencyGraph(projectDir, { include, exclude }) || {};
77
+ } catch (err) {
78
+ dependencyGraph = dependencies || {};
79
+ }
80
+ }
81
+
82
+ const fileSummaries = buildFileSummaries(files, symbolMap, projectDir, maxFileSummaryLength);
83
+ const symbolIndex = includeSymbolIndex ? buildSymbolIndex(symbolMap, projectDir) : {};
84
+ const dependencyMap = includeDependencyMap ? buildDependencyMap(dependencyGraph, projectDir) : {};
85
+
86
+ const totalSize = files.reduce((sum, f) => sum + ((f && f.size) ? f.size : 0), 0);
87
+
88
+ return {
89
+ metadata: {
90
+ createdAt: new Date().toISOString(),
91
+ projectDir,
92
+ fileCount: files.length,
93
+ totalSize,
94
+ version: '1.0.0',
95
+ },
96
+ fileSummaries,
97
+ symbolIndex,
98
+ dependencyMap,
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Build file summaries from scan results and symbol map
104
+ * @param {object[]} files - Array of file objects from scanner
105
+ * @param {object} symbolMap - Symbol map from analyzer
106
+ * @param {string} projectDir - Root project directory
107
+ * @param {number} maxLength - Max summary length
108
+ * @returns {object[]} Array of file summary objects
109
+ */
110
+ function buildFileSummaries(files, symbolMap, projectDir, maxLength) {
111
+ if (!Array.isArray(files)) return [];
112
+
113
+ return files.map(function (file) {
114
+ if (!file) return null;
115
+
116
+ const filePath = file.path || file.absolutePath || '';
117
+ const relPath = projectDir ? path.relative(projectDir, filePath).replace(/\\/g, '/') : filePath;
118
+ const ext = path.extname(filePath).toLowerCase();
119
+
120
+ const fileSymbols = symbolMap[filePath] || symbolMap[relPath] || null;
121
+
122
+ const symbols = fileSymbols ? extractSymbolNames(fileSymbols) : [];
123
+ const imports = fileSymbols && fileSymbols.imports ? fileSymbols.imports : [];
124
+ const exports = fileSymbols && fileSymbols.exports ? fileSymbols.exports : [];
125
+
126
+ let summary = buildSummaryText(relPath, ext, symbols, exports, imports);
127
+ if (summary.length > maxLength) {
128
+ summary = summary.slice(0, maxLength - 3) + '...';
129
+ }
130
+
131
+ return {
132
+ path: relPath,
133
+ absolutePath: filePath,
134
+ size: file.size || 0,
135
+ extension: ext,
136
+ summary,
137
+ symbols,
138
+ exports,
139
+ imports,
140
+ };
141
+ }).filter(Boolean);
142
+ }
143
+
144
+ /**
145
+ * Extract symbol names from a file's symbol data
146
+ * @param {object} fileSymbols - Symbol data for a file
147
+ * @returns {string[]} Array of symbol names
148
+ */
149
+ function extractSymbolNames(fileSymbols) {
150
+ if (!fileSymbols) return [];
151
+
152
+ const names = new Set();
153
+
154
+ if (Array.isArray(fileSymbols.functions)) {
155
+ fileSymbols.functions.forEach(function (fn) {
156
+ if (fn && fn.name) names.add(fn.name);
157
+ else if (typeof fn === 'string') names.add(fn);
158
+ });
159
+ }
160
+
161
+ if (Array.isArray(fileSymbols.classes)) {
162
+ fileSymbols.classes.forEach(function (cls) {
163
+ if (cls && cls.name) names.add(cls.name);
164
+ else if (typeof cls === 'string') names.add(cls);
165
+ });
166
+ }
167
+
168
+ if (Array.isArray(fileSymbols.symbols)) {
169
+ fileSymbols.symbols.forEach(function (sym) {
170
+ if (sym && sym.name) names.add(sym.name);
171
+ else if (typeof sym === 'string') names.add(sym);
172
+ });
173
+ }
174
+
175
+ return Array.from(names);
176
+ }
177
+
178
+ /**
179
+ * Build a human-readable summary text for a file
180
+ * @param {string} relPath - Relative file path
181
+ * @param {string} ext - File extension
182
+ * @param {string[]} symbols - Symbol names
183
+ * @param {string[]} exports - Export names
184
+ * @param {string[]} imports - Import sources
185
+ * @returns {string} Summary text
186
+ */
187
+ function buildSummaryText(relPath, ext, symbols, exports, imports) {
188
+ const parts = [];
189
+
190
+ parts.push('File: ' + relPath);
191
+
192
+ if (ext) {
193
+ parts.push('Type: ' + ext.replace('.', '').toUpperCase());
194
+ }
195
+
196
+ if (symbols && symbols.length > 0) {
197
+ parts.push('Symbols: ' + symbols.slice(0, 10).join(', ') + (symbols.length > 10 ? ' ...' : ''));
198
+ }
199
+
200
+ if (exports && exports.length > 0) {
201
+ const exportNames = exports.map(function (e) {
202
+ return (e && typeof e === 'object') ? (e.name || e.value || JSON.stringify(e)) : String(e);
203
+ });
204
+ parts.push('Exports: ' + exportNames.slice(0, 10).join(', ') + (exportNames.length > 10 ? ' ...' : ''));
205
+ }
206
+
207
+ if (imports && imports.length > 0) {
208
+ const importSources = imports.map(function (imp) {
209
+ return (imp && typeof imp === 'object') ? (imp.source || imp.from || imp.module || JSON.stringify(imp)) : String(imp);
210
+ });
211
+ parts.push('Imports: ' + importSources.slice(0, 5).join(', ') + (importSources.length > 5 ? ' ...' : ''));
212
+ }
213
+
214
+ return parts.join(' | ');
215
+ }
216
+
217
+ /**
218
+ * Build a symbol index mapping symbol names to file paths
219
+ * @param {object} symbolMap - Symbol map from analyzer
220
+ * @param {string} projectDir - Root project directory
221
+ * @returns {object} Symbol index object
222
+ */
223
+ function buildSymbolIndex(symbolMap, projectDir) {
224
+ if (!symbolMap || typeof symbolMap !== 'object') return {};
225
+
226
+ const index = {};
227
+
228
+ Object.keys(symbolMap).forEach(function (filePath) {
229
+ const fileSymbols = symbolMap[filePath];
230
+ if (!fileSymbols) return;
231
+
232
+ const relPath = projectDir
233
+ ? path.relative(projectDir, filePath).replace(/\\/g, '/')
234
+ : filePath;
235
+
236
+ const symbolNames = extractSymbolNames(fileSymbols);
237
+
238
+ symbolNames.forEach(function (name) {
239
+ if (!name) return;
240
+ if (!index[name]) {
241
+ index[name] = [];
242
+ }
243
+ if (!index[name].includes(relPath)) {
244
+ index[name].push(relPath);
245
+ }
246
+ });
247
+
248
+ if (Array.isArray(fileSymbols.exports)) {
249
+ fileSymbols.exports.forEach(function (exp) {
250
+ const expName = (exp && typeof exp === 'object') ? (exp.name || exp.value) : exp;
251
+ if (!expName || typeof expName !== 'string') return;
252
+ if (!index[expName]) {
253
+ index[expName] = [];
254
+ }
255
+ if (!index[expName].includes(relPath)) {
256
+ index[expName].push(relPath);
257
+ }
258
+ });
259
+ }
260
+ });
261
+
262
+ return index;
263
+ }
264
+
265
+ /**
266
+ * Build a dependency map from the dependency graph
267
+ * @param {object} dependencyGraph - Dependency graph from analyzer
268
+ * @param {string} projectDir - Root project directory
269
+ * @returns {object} Dependency map with relative paths
270
+ */
271
+ function buildDependencyMap(dependencyGraph, projectDir) {
272
+ if (!dependencyGraph || typeof dependencyGraph !== 'object') return {};
273
+
274
+ const map = {};
275
+
276
+ Object.keys(dependencyGraph).forEach(function (filePath) {
277
+ const deps = dependencyGraph[filePath];
278
+ if (!deps) return;
279
+
280
+ const relPath = projectDir
281
+ ? path.relative(projectDir, filePath).replace(/\\/g, '/')
282
+ : filePath;
283
+
284
+ const relDeps = Array.isArray(deps)
285
+ ? deps.map(function (dep) {
286
+ if (!dep || typeof dep !== 'string') return dep;
287
+ if (dep.startsWith('.') || path.isAbsolute(dep)) {
288
+ try {
289
+ return projectDir
290
+ ? path.relative(projectDir, dep).replace(/\\/g, '/')
291
+ : dep;
292
+ } catch (e) {
293
+ return dep;
294
+ }
295
+ }
296
+ return dep;
297
+ }).filter(Boolean)
298
+ : [];
299
+
300
+ map[relPath] = relDeps;
301
+ });
302
+
303
+ return map;
304
+ }
305
+
306
+ /**
307
+ * Formats a bundle as a JSON string
308
+ * @param {object} bundle - Bundle object from createBundle
309
+ * @param {object} options - Formatting options
310
+ * @param {boolean} [options.pretty] - Whether to pretty-print the JSON (default: true)
311
+ * @returns {string} JSON string representation of the bundle
312
+ */
313
+ function formatAsJSON(bundle, options) {
314
+ const opts = options || {};
315
+ const pretty = opts.pretty !== false;
316
+
317
+ if (!bundle || typeof bundle !== 'object') {
318
+ return pretty ? JSON.stringify({ error: 'Invalid bundle' }, null, 2) : JSON.stringify({ error: 'Invalid bundle' });
319
+ }
320
+
321
+ try {
322
+ return pretty ? JSON.stringify(bundle, null, 2) : JSON.stringify(bundle);
323
+ } catch (err) {
324
+ const fallback = { error: 'Failed to serialize bundle: ' + err.message };
325
+ return pretty ? JSON.stringify(fallback, null, 2) : JSON.stringify(fallback);
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Formats a bundle as a Markdown document
331
+ * @param {object} bundle - Bundle object from createBundle
332
+ * @param {object} options - Formatting options
333
+ * @param {string} [options.title] - Document title (default: 'ContextPack Bundle')
334
+ * @param {boolean} [options.includeSymbolIndex] - Whether to include symbol index section
335
+ * @param {boolean} [options.includeDependencyMap] - Whether to include dependency map section
336
+ * @returns {string} Markdown string representation of the bundle
337
+ */
338
+ function formatAsMarkdown(bundle, options) {
339
+ const opts = options || {};
340
+ const {
341
+ title = 'ContextPack Bundle',
342
+ includeSymbolIndex = true,
343
+ includeDependencyMap = true,
344
+ } = opts;
345
+
346
+ if (!bundle || typeof bundle !== 'object') {
347
+ return '# Error\n\nInvalid bundle provided.\n';
348
+ }
349
+
350
+ const lines = [];
351
+
352
+ lines.push('# ' + title);
353
+ lines.push('');
354
+
355
+ const meta = bundle.metadata || {};
356
+ lines.push('## Metadata');
357
+ lines.push('');
358
+ lines.push('| Property | Value |');
359
+ lines.push('|----------|-------|');
360
+ lines.push('| Created | ' + (meta.createdAt || 'N/A') + ' |');
361
+ lines.push('| Project | ' + (meta.projectDir || 'N/A') + ' |');
362
+ lines.push('| Files | ' + (meta.fileCount != null ? meta.fileCount : 'N/A') + ' |');
363
+ lines.push('| Total Size | ' + formatBytes(meta.totalSize || 0) + ' |');
364
+ if (meta.version) {
365
+ lines.push('| Version | ' + meta.version + ' |');
366
+ }
367
+ if (meta.error) {
368
+ lines.push('| Error | ' + meta.error + ' |');
369
+ }
370
+ lines.push('');
371
+
372
+ const fileSummaries = Array.isArray(bundle.fileSummaries) ? bundle.fileSummaries : [];
373
+ lines.push('## File Summaries');
374
+ lines.push('');
375
+
376
+ if (fileSummaries.length === 0) {
377
+ lines.push('_No files found._');
378
+ lines.push('');
379
+ } else {
380
+ fileSummaries.forEach(function (file) {
381
+ if (!file) return;
382
+ lines.push('### `' + (file.path || 'unknown') + '`');
383
+ lines.push('');
384
+ lines.push('- **Size:** ' + formatBytes(file.size || 0));
385
+ lines.push('- **Type:** ' + ((file.extension || '').replace('.', '').toUpperCase() || 'Unknown'));
386
+
387
+ if (file.symbols && file.symbols.length > 0) {
388
+ lines.push('- **Symbols:** ' + file.symbols.join(', '));
389
+ }
390
+
391
+ if (file.exports && file.exports.length > 0) {
392
+ const exportNames = file.exports.map(function (e) {
393
+ return (e && typeof e === 'object') ? (e.name || e.value || JSON.stringify(e)) : String(e);
394
+ });
395
+ lines.push('- **Exports:** ' + exportNames.join(', '));
396
+ }
397
+
398
+ if (file.imports && file.imports.length > 0) {
399
+ const importSources = file.imports.map(function (imp) {
400
+ return (imp && typeof imp === 'object') ? (imp.source || imp.from || imp.module || JSON.stringify(imp)) : String(imp);
401
+ });
402
+ lines.push('- **Imports:** ' + importSources.join(', '));
403
+ }
404
+
405
+ if (file.summary) {
406
+ lines.push('');
407
+ lines.push('> ' + file.summary.replace(/\n/g, ' '));
408
+ }
409
+
410
+ lines.push('');
411
+ });
412
+ }
413
+
414
+ if (includeSymbolIndex) {
415
+ const symbolIndex = (bundle.symbolIndex && typeof bundle.symbolIndex === 'object') ? bundle.symbolIndex : {};
416
+ const symbolKeys = Object.keys(symbolIndex);
417
+
418
+ lines.push('## Symbol Index');
419
+ lines.push('');
420
+
421
+ if (symbolKeys.length === 0) {
422
+ lines.push('_No symbols found._');
423
+ lines.push('');
424
+ } else {
425
+ lines.push('| Symbol | Defined In |');
426
+ lines.push('|--------|-----------|');
427
+ symbolKeys.sort().forEach(function (sym) {
428
+ const locations = Array.isArray(symbolIndex[sym]) ? symbolIndex[sym] : [symbolIndex[sym]];
429
+ lines.push('| `' + sym + '` | ' + locations.map(function (l) { return '`' + l + '`'; }).join(', ') + ' |');
430
+ });
431
+ lines.push('');
432
+ }
433
+ }
434
+
435
+ if (includeDependencyMap) {
436
+ const dependencyMap = (bundle.dependencyMap && typeof bundle.dependencyMap === 'object') ? bundle.dependencyMap : {};
437
+ const depKeys = Object.keys(dependencyMap);
438
+
439
+ lines.push('## Dependency Map');
440
+ lines.push('');
441
+
442
+ if (depKeys.length === 0) {
443
+ lines.push('_No dependencies found._');
444
+ lines.push('');
445
+ } else {
446
+ depKeys.sort().forEach(function (file) {
447
+ const deps = Array.isArray(dependencyMap[file]) ? dependencyMap[file] : [];
448
+ lines.push('**`' + file + '`**');
449
+ if (deps.length === 0) {
450
+ lines.push('- _(no dependencies)_');
451
+ } else {
452
+ deps.forEach(function (dep) {
453
+ lines.push('- `' + dep + '`');
454
+ });
455
+ }
456
+ lines.push('');
457
+ });
458
+ }
459
+ }
460
+
461
+ return lines.join('\n');
462
+ }
463
+
464
+ /**
465
+ * Format bytes into a human-readable string
466
+ * @param {number} bytes - Number of bytes
467
+ * @returns {string} Human-readable size string
468
+ */
469
+ function formatBytes(bytes) {
470
+ if (!bytes || bytes === 0) return '0 B';
471
+ const units = ['B', 'KB', 'MB', 'GB'];
472
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
473
+ const val = (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1);
474
+ return val + ' ' + (units[i] || 'B');
475
+ }
476
+
477
+ module.exports = { createBundle, formatAsJSON, formatAsMarkdown };