@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
|
@@ -0,0 +1,1094 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { requirePro } = require('../premium/gate');
|
|
6
|
+
const { scanProject } = require('../scanner');
|
|
7
|
+
const { analyzeProject } = require('../analyzer');
|
|
8
|
+
const { createBundle } = require('../bundler');
|
|
9
|
+
const { estimateTokens, calculateBundleSize } = require('../tokenizer');
|
|
10
|
+
const { validateBundle } = require('../validator');
|
|
11
|
+
const { loadConfig, mergeWithFlags } = require('../config');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Escape HTML special characters
|
|
15
|
+
* @param {string} str
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
function escapeHtml(str) {
|
|
19
|
+
if (str === null || str === undefined) return '';
|
|
20
|
+
return String(str)
|
|
21
|
+
.replace(/&/g, '&')
|
|
22
|
+
.replace(/</g, '<')
|
|
23
|
+
.replace(/>/g, '>')
|
|
24
|
+
.replace(/"/g, '"')
|
|
25
|
+
.replace(/'/g, ''');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Format a byte count into a human-readable string
|
|
30
|
+
* @param {number} bytes
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function formatBytes(bytes) {
|
|
34
|
+
if (!bytes || bytes === 0) return '0 B';
|
|
35
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
36
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
37
|
+
return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + units[i];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format a number with thousands separators
|
|
42
|
+
* @param {number} n
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
function formatNumber(n) {
|
|
46
|
+
if (n === null || n === undefined) return '0';
|
|
47
|
+
return Number(n).toLocaleString();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Derive a simple language label from a file extension
|
|
52
|
+
* @param {string} filePath
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
function langFromPath(filePath) {
|
|
56
|
+
const ext = path.extname(filePath || '').toLowerCase();
|
|
57
|
+
const map = {
|
|
58
|
+
'.js': 'JavaScript',
|
|
59
|
+
'.mjs': 'JavaScript',
|
|
60
|
+
'.cjs': 'JavaScript',
|
|
61
|
+
'.jsx': 'JSX',
|
|
62
|
+
'.ts': 'TypeScript',
|
|
63
|
+
'.tsx': 'TSX',
|
|
64
|
+
'.json': 'JSON',
|
|
65
|
+
'.md': 'Markdown',
|
|
66
|
+
'.py': 'Python',
|
|
67
|
+
'.rb': 'Ruby',
|
|
68
|
+
'.go': 'Go',
|
|
69
|
+
'.java': 'Java',
|
|
70
|
+
'.cs': 'C#',
|
|
71
|
+
'.php': 'PHP',
|
|
72
|
+
'.swift': 'Swift',
|
|
73
|
+
'.kt': 'Kotlin',
|
|
74
|
+
};
|
|
75
|
+
return map[ext] || ext.replace('.', '').toUpperCase() || 'Unknown';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build a colour class name for a language badge
|
|
80
|
+
* @param {string} lang
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function langColour(lang) {
|
|
84
|
+
const map = {
|
|
85
|
+
JavaScript: 'badge-js',
|
|
86
|
+
JSX: 'badge-jsx',
|
|
87
|
+
TypeScript: 'badge-ts',
|
|
88
|
+
TSX: 'badge-tsx',
|
|
89
|
+
JSON: 'badge-json',
|
|
90
|
+
Markdown: 'badge-md',
|
|
91
|
+
Python: 'badge-py',
|
|
92
|
+
Go: 'badge-go',
|
|
93
|
+
Java: 'badge-java',
|
|
94
|
+
};
|
|
95
|
+
return map[lang] || 'badge-default';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Render the summary cards section
|
|
100
|
+
* @param {object} stats
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
function renderSummaryCards(stats) {
|
|
104
|
+
const cards = [
|
|
105
|
+
{ label: 'Total Files', value: formatNumber(stats.totalFiles), icon: '📄', cls: 'card-blue' },
|
|
106
|
+
{ label: 'Total Symbols', value: formatNumber(stats.totalSymbols), icon: '🔗', cls: 'card-purple' },
|
|
107
|
+
{ label: 'Estimated Tokens', value: formatNumber(stats.estimatedTokens), icon: '🧰', cls: 'card-green' },
|
|
108
|
+
{ label: 'Bundle Size', value: formatBytes(stats.bundleSize), icon: '📦', cls: 'card-orange' },
|
|
109
|
+
{ label: 'Dependencies', value: formatNumber(stats.totalDeps), icon: '🔁', cls: 'card-teal' },
|
|
110
|
+
{ label: 'Validation Issues', value: formatNumber(stats.validationIssues), icon: '⚠️', cls: stats.validationIssues > 0 ? 'card-red' : 'card-grey' },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const cardHtml = cards.map(c => `
|
|
114
|
+
<div class="summary-card ${escapeHtml(c.cls)}">
|
|
115
|
+
<div class="card-icon">${c.icon}</div>
|
|
116
|
+
<div class="card-value">${escapeHtml(c.value)}</div>
|
|
117
|
+
<div class="card-label">${escapeHtml(c.label)}</div>
|
|
118
|
+
</div>`).join('');
|
|
119
|
+
|
|
120
|
+
return `<section class="summary-grid">${cardHtml}</section>`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Render the file table section
|
|
125
|
+
* @param {Array} files
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
function renderFileTable(files) {
|
|
129
|
+
if (!files || files.length === 0) {
|
|
130
|
+
return '<section class="section"><h2>Files</h2><p class="empty">No files found.</p></section>';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const rows = files.map((f, idx) => {
|
|
134
|
+
const lang = langFromPath(f.path || f.filePath || '');
|
|
135
|
+
const symbols = Array.isArray(f.symbols) ? f.symbols.length : 0;
|
|
136
|
+
const size = formatBytes(f.size || 0);
|
|
137
|
+
const tokens = formatNumber(f.tokens || estimateTokens(f.summary || ''));
|
|
138
|
+
const filePath = escapeHtml(f.path || f.filePath || '');
|
|
139
|
+
const summary = escapeHtml((f.summary || '').substring(0, 120) + ((f.summary || '').length > 120 ? '…' : ''));
|
|
140
|
+
|
|
141
|
+
return `
|
|
142
|
+
<tr class="${idx % 2 === 0 ? 'row-even' : 'row-odd'}" data-lang="${escapeHtml(lang)}">
|
|
143
|
+
<td class="td-path" title="${filePath}">
|
|
144
|
+
<span class="badge ${escapeHtml(langColour(lang))}">${escapeHtml(lang)}</span>
|
|
145
|
+
<span class="file-path">${filePath}</span>
|
|
146
|
+
</td>
|
|
147
|
+
<td class="td-num">${escapeHtml(size)}</td>
|
|
148
|
+
<td class="td-num">${escapeHtml(tokens)}</td>
|
|
149
|
+
<td class="td-num">${symbols}</td>
|
|
150
|
+
<td class="td-summary">${summary}</td>
|
|
151
|
+
</tr>`;
|
|
152
|
+
}).join('');
|
|
153
|
+
|
|
154
|
+
return `
|
|
155
|
+
<section class="section">
|
|
156
|
+
<h2>📄 Files <span class="count-badge">${files.length}</span></h2>
|
|
157
|
+
<div class="table-controls">
|
|
158
|
+
<input type="text" id="file-search" placeholder="Filter files…" class="search-input" oninput="filterTable(this,'file-table')" />
|
|
159
|
+
<select id="lang-filter" class="lang-select" onchange="filterByLang(this,'file-table')">
|
|
160
|
+
<option value="">All Languages</option>
|
|
161
|
+
</select>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="table-wrap">
|
|
164
|
+
<table id="file-table" class="data-table">
|
|
165
|
+
<thead>
|
|
166
|
+
<tr>
|
|
167
|
+
<th class="th-sortable" onclick="sortTable('file-table',0)">File ↕</th>
|
|
168
|
+
<th class="th-sortable th-num" onclick="sortTable('file-table',1)">Size ↕</th>
|
|
169
|
+
<th class="th-sortable th-num" onclick="sortTable('file-table',2)">Tokens ↕</th>
|
|
170
|
+
<th class="th-sortable th-num" onclick="sortTable('file-table',3)">Symbols ↕</th>
|
|
171
|
+
<th>Summary</th>
|
|
172
|
+
</tr>
|
|
173
|
+
</thead>
|
|
174
|
+
<tbody>${rows}</tbody>
|
|
175
|
+
</table>
|
|
176
|
+
</div>
|
|
177
|
+
</section>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Render the symbol index section
|
|
182
|
+
* @param {object} symbolIndex
|
|
183
|
+
* @returns {string}
|
|
184
|
+
*/
|
|
185
|
+
function renderSymbolIndex(symbolIndex) {
|
|
186
|
+
if (!symbolIndex || Object.keys(symbolIndex).length === 0) {
|
|
187
|
+
return '<section class="section"><h2>Symbol Index</h2><p class="empty">No symbols extracted.</p></section>';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const entries = Object.entries(symbolIndex);
|
|
191
|
+
const rows = entries.map(([name, info], idx) => {
|
|
192
|
+
const filePath = escapeHtml(info.file || info.filePath || '');
|
|
193
|
+
const kind = escapeHtml(info.kind || info.type || 'symbol');
|
|
194
|
+
const kindCls = 'kind-' + kind.toLowerCase().replace(/[^a-z]/g, '');
|
|
195
|
+
return `
|
|
196
|
+
<tr class="${idx % 2 === 0 ? 'row-even' : 'row-odd'}">
|
|
197
|
+
<td class="td-symbol"><code>${escapeHtml(name)}</code></td>
|
|
198
|
+
<td><span class="kind-badge ${kindCls}">${kind}</span></td>
|
|
199
|
+
<td class="td-path" title="${filePath}">${filePath}</td>
|
|
200
|
+
</tr>`;
|
|
201
|
+
}).join('');
|
|
202
|
+
|
|
203
|
+
return `
|
|
204
|
+
<section class="section">
|
|
205
|
+
<h2>🔗 Symbol Index <span class="count-badge">${entries.length}</span></h2>
|
|
206
|
+
<div class="table-controls">
|
|
207
|
+
<input type="text" id="sym-search" placeholder="Filter symbols…" class="search-input" oninput="filterTable(this,'sym-table')" />
|
|
208
|
+
</div>
|
|
209
|
+
<div class="table-wrap">
|
|
210
|
+
<table id="sym-table" class="data-table">
|
|
211
|
+
<thead>
|
|
212
|
+
<tr>
|
|
213
|
+
<th class="th-sortable" onclick="sortTable('sym-table',0)">Symbol ↕</th>
|
|
214
|
+
<th class="th-sortable" onclick="sortTable('sym-table',1)">Kind ↕</th>
|
|
215
|
+
<th class="th-sortable" onclick="sortTable('sym-table',2)">File ↕</th>
|
|
216
|
+
</tr>
|
|
217
|
+
</thead>
|
|
218
|
+
<tbody>${rows}</tbody>
|
|
219
|
+
</table>
|
|
220
|
+
</div>
|
|
221
|
+
</section>`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Render the dependency map section
|
|
226
|
+
* @param {object} depMap
|
|
227
|
+
* @returns {string}
|
|
228
|
+
*/
|
|
229
|
+
function renderDependencyMap(depMap) {
|
|
230
|
+
if (!depMap || Object.keys(depMap).length === 0) {
|
|
231
|
+
return '<section class="section"><h2>Dependency Map</h2><p class="empty">No dependencies found.</p></section>';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const entries = Object.entries(depMap);
|
|
235
|
+
const rows = entries.map(([file, deps], idx) => {
|
|
236
|
+
const depList = Array.isArray(deps) ? deps : [];
|
|
237
|
+
const depBadges = depList.map(d => `<span class="dep-badge">${escapeHtml(d)}</span>`).join(' ');
|
|
238
|
+
return `
|
|
239
|
+
<tr class="${idx % 2 === 0 ? 'row-even' : 'row-odd'}">
|
|
240
|
+
<td class="td-path" title="${escapeHtml(file)}">${escapeHtml(file)}</td>
|
|
241
|
+
<td class="td-num">${depList.length}</td>
|
|
242
|
+
<td class="td-deps">${depBadges || '<span class="none-label">none</span>'}</td>
|
|
243
|
+
</tr>`;
|
|
244
|
+
}).join('');
|
|
245
|
+
|
|
246
|
+
return `
|
|
247
|
+
<section class="section">
|
|
248
|
+
<h2>🔁 Dependency Map <span class="count-badge">${entries.length}</span></h2>
|
|
249
|
+
<div class="table-controls">
|
|
250
|
+
<input type="text" id="dep-search" placeholder="Filter files…" class="search-input" oninput="filterTable(this,'dep-table')" />
|
|
251
|
+
</div>
|
|
252
|
+
<div class="table-wrap">
|
|
253
|
+
<table id="dep-table" class="data-table">
|
|
254
|
+
<thead>
|
|
255
|
+
<tr>
|
|
256
|
+
<th class="th-sortable" onclick="sortTable('dep-table',0)">File ↕</th>
|
|
257
|
+
<th class="th-sortable th-num" onclick="sortTable('dep-table',1)">Count ↕</th>
|
|
258
|
+
<th>Imports</th>
|
|
259
|
+
</tr>
|
|
260
|
+
</thead>
|
|
261
|
+
<tbody>${rows}</tbody>
|
|
262
|
+
</table>
|
|
263
|
+
</div>
|
|
264
|
+
</section>`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Render the validation report section
|
|
269
|
+
* @param {object} validation
|
|
270
|
+
* @returns {string}
|
|
271
|
+
*/
|
|
272
|
+
function renderValidation(validation) {
|
|
273
|
+
if (!validation) {
|
|
274
|
+
return '<section class="section"><h2>Validation</h2><p class="empty">No validation data.</p></section>';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const errors = Array.isArray(validation.errors) ? validation.errors : [];
|
|
278
|
+
const warnings = Array.isArray(validation.warnings) ? validation.warnings : [];
|
|
279
|
+
const info = Array.isArray(validation.info) ? validation.info : [];
|
|
280
|
+
|
|
281
|
+
const renderItems = (items, cls, icon) => {
|
|
282
|
+
if (items.length === 0) return '';
|
|
283
|
+
return items.map(item => {
|
|
284
|
+
const msg = escapeHtml(typeof item === 'string' ? item : (item.message || JSON.stringify(item)));
|
|
285
|
+
return `<div class="validation-item ${cls}">${icon} ${msg}</div>`;
|
|
286
|
+
}).join('');
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const statusCls = errors.length > 0 ? 'status-error' : warnings.length > 0 ? 'status-warn' : 'status-ok';
|
|
290
|
+
const statusLabel = errors.length > 0 ? 'Failed' : warnings.length > 0 ? 'Warnings' : 'Passed';
|
|
291
|
+
|
|
292
|
+
return `
|
|
293
|
+
<section class="section">
|
|
294
|
+
<h2>⚠️ Validation <span class="status-badge ${statusCls}">${statusLabel}</span></h2>
|
|
295
|
+
<div class="validation-summary">
|
|
296
|
+
<span class="val-count error-count">❌ ${errors.length} error${errors.length !== 1 ? 's' : ''}</span>
|
|
297
|
+
<span class="val-count warn-count">⚠️ ${warnings.length} warning${warnings.length !== 1 ? 's' : ''}</span>
|
|
298
|
+
<span class="val-count info-count">ℹ️ ${info.length} info</span>
|
|
299
|
+
</div>
|
|
300
|
+
<div class="validation-list">
|
|
301
|
+
${renderItems(errors, 'item-error', '❌')}
|
|
302
|
+
${renderItems(warnings, 'item-warn', '⚠️')}
|
|
303
|
+
${renderItems(info, 'item-info', 'ℹ️')}
|
|
304
|
+
${errors.length === 0 && warnings.length === 0 && info.length === 0
|
|
305
|
+
? '<div class="validation-item item-ok">✅ Bundle passed all validation checks.</div>'
|
|
306
|
+
: ''}
|
|
307
|
+
</div>
|
|
308
|
+
</section>`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Render the language breakdown chart (pure CSS bar chart)
|
|
313
|
+
* @param {Array} files
|
|
314
|
+
* @returns {string}
|
|
315
|
+
*/
|
|
316
|
+
function renderLanguageBreakdown(files) {
|
|
317
|
+
if (!files || files.length === 0) return '';
|
|
318
|
+
|
|
319
|
+
const counts = {};
|
|
320
|
+
files.forEach(f => {
|
|
321
|
+
const lang = langFromPath(f.path || f.filePath || '');
|
|
322
|
+
counts[lang] = (counts[lang] || 0) + 1;
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const total = files.length;
|
|
326
|
+
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
|
327
|
+
const maxCount = sorted[0] ? sorted[0][1] : 1;
|
|
328
|
+
|
|
329
|
+
const bars = sorted.map(([lang, count]) => {
|
|
330
|
+
const pct = ((count / total) * 100).toFixed(1);
|
|
331
|
+
const barWidth = ((count / maxCount) * 100).toFixed(1);
|
|
332
|
+
const cls = langColour(lang);
|
|
333
|
+
return `
|
|
334
|
+
<div class="bar-row">
|
|
335
|
+
<div class="bar-label">
|
|
336
|
+
<span class="badge ${escapeHtml(cls)}">${escapeHtml(lang)}</span>
|
|
337
|
+
</div>
|
|
338
|
+
<div class="bar-track">
|
|
339
|
+
<div class="bar-fill ${escapeHtml(cls)}-fill" style="width:${barWidth}%"></div>
|
|
340
|
+
</div>
|
|
341
|
+
<div class="bar-stat">${count} <span class="bar-pct">(${pct}%)</span></div>
|
|
342
|
+
</div>`;
|
|
343
|
+
}).join('');
|
|
344
|
+
|
|
345
|
+
return `
|
|
346
|
+
<section class="section">
|
|
347
|
+
<h2>📊 Language Breakdown</h2>
|
|
348
|
+
<div class="bar-chart">${bars}</div>
|
|
349
|
+
</section>`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Render the top symbols by file (most symbols per file)
|
|
354
|
+
* @param {Array} files
|
|
355
|
+
* @returns {string}
|
|
356
|
+
*/
|
|
357
|
+
function renderTopFiles(files) {
|
|
358
|
+
if (!files || files.length === 0) return '';
|
|
359
|
+
|
|
360
|
+
const sorted = files
|
|
361
|
+
.filter(f => Array.isArray(f.symbols) && f.symbols.length > 0)
|
|
362
|
+
.sort((a, b) => b.symbols.length - a.symbols.length)
|
|
363
|
+
.slice(0, 10);
|
|
364
|
+
|
|
365
|
+
if (sorted.length === 0) return '';
|
|
366
|
+
|
|
367
|
+
const maxSymbols = sorted[0].symbols.length;
|
|
368
|
+
|
|
369
|
+
const rows = sorted.map((f, idx) => {
|
|
370
|
+
const filePath = escapeHtml(f.path || f.filePath || '');
|
|
371
|
+
const count = f.symbols.length;
|
|
372
|
+
const barWidth = ((count / maxSymbols) * 100).toFixed(1);
|
|
373
|
+
return `
|
|
374
|
+
<tr class="${idx % 2 === 0 ? 'row-even' : 'row-odd'}">
|
|
375
|
+
<td class="td-rank">${idx + 1}</td>
|
|
376
|
+
<td class="td-path" title="${filePath}">${filePath}</td>
|
|
377
|
+
<td class="td-bar">
|
|
378
|
+
<div class="inline-bar">
|
|
379
|
+
<div class="inline-bar-fill" style="width:${barWidth}%"></div>
|
|
380
|
+
</div>
|
|
381
|
+
</td>
|
|
382
|
+
<td class="td-num">${count}</td>
|
|
383
|
+
</tr>`;
|
|
384
|
+
}).join('');
|
|
385
|
+
|
|
386
|
+
return `
|
|
387
|
+
<section class="section">
|
|
388
|
+
<h2>🏆 Top Files by Symbol Count</h2>
|
|
389
|
+
<div class="table-wrap">
|
|
390
|
+
<table class="data-table">
|
|
391
|
+
<thead>
|
|
392
|
+
<tr>
|
|
393
|
+
<th class="th-num">#</th>
|
|
394
|
+
<th>File</th>
|
|
395
|
+
<th>Distribution</th>
|
|
396
|
+
<th class="th-num">Symbols</th>
|
|
397
|
+
</tr>
|
|
398
|
+
</thead>
|
|
399
|
+
<tbody>${rows}</tbody>
|
|
400
|
+
</table>
|
|
401
|
+
</div>
|
|
402
|
+
</section>`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Build the complete CSS stylesheet string
|
|
407
|
+
* @returns {string}
|
|
408
|
+
*/
|
|
409
|
+
function buildCSS() {
|
|
410
|
+
return `
|
|
411
|
+
:root {
|
|
412
|
+
--bg: #0f1117;
|
|
413
|
+
--surface: #1a1d27;
|
|
414
|
+
--surface2: #22263a;
|
|
415
|
+
--border: #2e3250;
|
|
416
|
+
--text: #e2e8f0;
|
|
417
|
+
--text-muted: #8892a4;
|
|
418
|
+
--accent: #6c63ff;
|
|
419
|
+
--accent2: #00d4aa;
|
|
420
|
+
--blue: #3b82f6;
|
|
421
|
+
--purple: #8b5cf6;
|
|
422
|
+
--green: #10b981;
|
|
423
|
+
--orange: #f59e0b;
|
|
424
|
+
--teal: #06b6d4;
|
|
425
|
+
--red: #ef4444;
|
|
426
|
+
--grey: #6b7280;
|
|
427
|
+
--radius: 10px;
|
|
428
|
+
--shadow: 0 4px 24px rgba(0,0,0,0.4);
|
|
429
|
+
}
|
|
430
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
431
|
+
body {
|
|
432
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
433
|
+
background: var(--bg);
|
|
434
|
+
color: var(--text);
|
|
435
|
+
font-size: 14px;
|
|
436
|
+
line-height: 1.6;
|
|
437
|
+
min-height: 100vh;
|
|
438
|
+
}
|
|
439
|
+
a { color: var(--accent); text-decoration: none; }
|
|
440
|
+
a:hover { text-decoration: underline; }
|
|
441
|
+
code {
|
|
442
|
+
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace;
|
|
443
|
+
font-size: 12px;
|
|
444
|
+
background: var(--surface2);
|
|
445
|
+
padding: 1px 5px;
|
|
446
|
+
border-radius: 4px;
|
|
447
|
+
color: var(--accent2);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* Header */
|
|
451
|
+
.header {
|
|
452
|
+
background: linear-gradient(135deg, #1a1d27 0%, #12152a 100%);
|
|
453
|
+
border-bottom: 1px solid var(--border);
|
|
454
|
+
padding: 28px 40px 24px;
|
|
455
|
+
display: flex;
|
|
456
|
+
align-items: center;
|
|
457
|
+
justify-content: space-between;
|
|
458
|
+
flex-wrap: wrap;
|
|
459
|
+
gap: 16px;
|
|
460
|
+
}
|
|
461
|
+
.header-brand { display: flex; align-items: center; gap: 14px; }
|
|
462
|
+
.header-logo {
|
|
463
|
+
width: 44px; height: 44px;
|
|
464
|
+
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
465
|
+
border-radius: 10px;
|
|
466
|
+
display: flex; align-items: center; justify-content: center;
|
|
467
|
+
font-size: 22px;
|
|
468
|
+
}
|
|
469
|
+
.header-title { font-size: 22px; font-weight: 700; letter-spacing: -0.3px; }
|
|
470
|
+
.header-subtitle { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
|
|
471
|
+
.header-meta { text-align: right; font-size: 12px; color: var(--text-muted); }
|
|
472
|
+
.header-meta strong { color: var(--text); display: block; font-size: 13px; }
|
|
473
|
+
.pro-badge {
|
|
474
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
475
|
+
background: linear-gradient(135deg, #f59e0b, #ef4444);
|
|
476
|
+
color: #fff; font-size: 10px; font-weight: 700;
|
|
477
|
+
padding: 3px 8px; border-radius: 20px; letter-spacing: 0.5px;
|
|
478
|
+
text-transform: uppercase; margin-top: 6px;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/* Nav tabs */
|
|
482
|
+
.nav-tabs {
|
|
483
|
+
background: var(--surface);
|
|
484
|
+
border-bottom: 1px solid var(--border);
|
|
485
|
+
padding: 0 40px;
|
|
486
|
+
display: flex; gap: 4px;
|
|
487
|
+
overflow-x: auto;
|
|
488
|
+
}
|
|
489
|
+
.nav-tab {
|
|
490
|
+
padding: 12px 18px;
|
|
491
|
+
cursor: pointer;
|
|
492
|
+
font-size: 13px;
|
|
493
|
+
font-weight: 500;
|
|
494
|
+
color: var(--text-muted);
|
|
495
|
+
border-bottom: 2px solid transparent;
|
|
496
|
+
white-space: nowrap;
|
|
497
|
+
transition: color 0.15s, border-color 0.15s;
|
|
498
|
+
background: none; border-top: none; border-left: none; border-right: none;
|
|
499
|
+
}
|
|
500
|
+
.nav-tab:hover { color: var(--text); }
|
|
501
|
+
.nav-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
502
|
+
|
|
503
|
+
/* Main content */
|
|
504
|
+
.main { padding: 32px 40px; max-width: 1400px; margin: 0 auto; }
|
|
505
|
+
.tab-panel { display: none; }
|
|
506
|
+
.tab-panel.active { display: block; }
|
|
507
|
+
|
|
508
|
+
/* Summary grid */
|
|
509
|
+
.summary-grid {
|
|
510
|
+
display: grid;
|
|
511
|
+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
512
|
+
gap: 16px;
|
|
513
|
+
margin-bottom: 32px;
|
|
514
|
+
}
|
|
515
|
+
.summary-card {
|
|
516
|
+
background: var(--surface);
|
|
517
|
+
border: 1px solid var(--border);
|
|
518
|
+
border-radius: var(--radius);
|
|
519
|
+
padding: 20px 16px;
|
|
520
|
+
text-align: center;
|
|
521
|
+
transition: transform 0.15s, box-shadow 0.15s;
|
|
522
|
+
position: relative;
|
|
523
|
+
overflow: hidden;
|
|
524
|
+
}
|
|
525
|
+
.summary-card::before {
|
|
526
|
+
content: '';
|
|
527
|
+
position: absolute; top: 0; left: 0; right: 0; height: 3px;
|
|
528
|
+
}
|
|
529
|
+
.summary-card:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
|
|
530
|
+
.card-blue::before { background: var(--blue); }
|
|
531
|
+
.card-purple::before { background: var(--purple); }
|
|
532
|
+
.card-green::before { background: var(--green); }
|
|
533
|
+
.card-orange::before { background: var(--orange); }
|
|
534
|
+
.card-teal::before { background: var(--teal); }
|
|
535
|
+
.card-red::before { background: var(--red); }
|
|
536
|
+
.card-grey::before { background: var(--grey); }
|
|
537
|
+
.card-icon { font-size: 24px; margin-bottom: 8px; }
|
|
538
|
+
.card-value { font-size: 26px; font-weight: 700; letter-spacing: -0.5px; }
|
|
539
|
+
.card-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 4px; }
|
|
540
|
+
|
|
541
|
+
/* Sections */
|
|
542
|
+
.section { margin-bottom: 36px; }
|
|
543
|
+
.section h2 {
|
|
544
|
+
font-size: 17px; font-weight: 600;
|
|
545
|
+
margin-bottom: 16px;
|
|
546
|
+
display: flex; align-items: center; gap: 10px;
|
|
547
|
+
padding-bottom: 10px;
|
|
548
|
+
border-bottom: 1px solid var(--border);
|
|
549
|
+
}
|
|
550
|
+
.count-badge {
|
|
551
|
+
font-size: 11px; font-weight: 600;
|
|
552
|
+
background: var(--surface2);
|
|
553
|
+
border: 1px solid var(--border);
|
|
554
|
+
color: var(--text-muted);
|
|
555
|
+
padding: 2px 8px; border-radius: 20px;
|
|
556
|
+
}
|
|
557
|
+
.empty { color: var(--text-muted); font-style: italic; padding: 16px 0; }
|
|
558
|
+
|
|
559
|
+
/* Table controls */
|
|
560
|
+
.table-controls {
|
|
561
|
+
display: flex; gap: 10px; margin-bottom: 12px; flex-wrap: wrap;
|
|
562
|
+
}
|
|
563
|
+
.search-input, .lang-select {
|
|
564
|
+
background: var(--surface);
|
|
565
|
+
border: 1px solid var(--border);
|
|
566
|
+
color: var(--text);
|
|
567
|
+
padding: 7px 12px;
|
|
568
|
+
border-radius: 6px;
|
|
569
|
+
font-size: 13px;
|
|
570
|
+
outline: none;
|
|
571
|
+
transition: border-color 0.15s;
|
|
572
|
+
}
|
|
573
|
+
.search-input { min-width: 220px; }
|
|
574
|
+
.search-input:focus, .lang-select:focus { border-color: var(--accent); }
|
|
575
|
+
.lang-select { cursor: pointer; }
|
|
576
|
+
|
|
577
|
+
/* Tables */
|
|
578
|
+
.table-wrap { overflow-x: auto; border-radius: var(--radius); border: 1px solid var(--border); }
|
|
579
|
+
.data-table { width: 100%; border-collapse: collapse; }
|
|
580
|
+
.data-table thead { background: var(--surface2); }
|
|
581
|
+
.data-table th {
|
|
582
|
+
padding: 10px 14px;
|
|
583
|
+
text-align: left;
|
|
584
|
+
font-size: 11px;
|
|
585
|
+
font-weight: 600;
|
|
586
|
+
text-transform: uppercase;
|
|
587
|
+
letter-spacing: 0.5px;
|
|
588
|
+
color: var(--text-muted);
|
|
589
|
+
white-space: nowrap;
|
|
590
|
+
}
|
|
591
|
+
.th-sortable { cursor: pointer; user-select: none; }
|
|
592
|
+
.th-sortable:hover { color: var(--text); }
|
|
593
|
+
.th-num { text-align: right; }
|
|
594
|
+
.data-table td { padding: 9px 14px; border-top: 1px solid var(--border); vertical-align: middle; }
|
|
595
|
+
.row-even { background: var(--surface); }
|
|
596
|
+
.row-odd { background: var(--bg); }
|
|
597
|
+
.data-table tr:hover td { background: var(--surface2); }
|
|
598
|
+
.td-num { text-align: right; font-variant-numeric: tabular-nums; color: var(--text-muted); }
|
|
599
|
+
.td-path {
|
|
600
|
+
font-family: 'JetBrains Mono', Consolas, monospace;
|
|
601
|
+
font-size: 12px;
|
|
602
|
+
max-width: 320px;
|
|
603
|
+
overflow: hidden;
|
|
604
|
+
text-overflow: ellipsis;
|
|
605
|
+
white-space: nowrap;
|
|
606
|
+
}
|
|
607
|
+
.td-summary { color: var(--text-muted); font-size: 12px; max-width: 400px; }
|
|
608
|
+
.td-symbol { font-family: 'JetBrains Mono', Consolas, monospace; font-size: 12px; }
|
|
609
|
+
.td-deps { max-width: 500px; }
|
|
610
|
+
.td-rank { text-align: center; color: var(--text-muted); font-weight: 600; }
|
|
611
|
+
.td-bar { min-width: 120px; }
|
|
612
|
+
.file-path { margin-left: 6px; }
|
|
613
|
+
|
|
614
|
+
/* Badges */
|
|
615
|
+
.badge {
|
|
616
|
+
display: inline-block;
|
|
617
|
+
font-size: 10px; font-weight: 600;
|
|
618
|
+
padding: 2px 7px; border-radius: 4px;
|
|
619
|
+
text-transform: uppercase; letter-spacing: 0.3px;
|
|
620
|
+
white-space: nowrap;
|
|
621
|
+
}
|
|
622
|
+
.badge-js { background: #f7df1e22; color: #f7df1e; border: 1px solid #f7df1e44; }
|
|
623
|
+
.badge-jsx { background: #61dafb22; color: #61dafb; border: 1px solid #61dafb44; }
|
|
624
|
+
.badge-ts { background: #3178c622; color: #3b82f6; border: 1px solid #3178c644; }
|
|
625
|
+
.badge-tsx { background: #3178c622; color: #06b6d4; border: 1px solid #06b6d444; }
|
|
626
|
+
.badge-json { background: #10b98122; color: #10b981; border: 1px solid #10b98144; }
|
|
627
|
+
.badge-md { background: #8b5cf622; color: #8b5cf6; border: 1px solid #8b5cf644; }
|
|
628
|
+
.badge-py { background: #3b82f622; color: #60a5fa; border: 1px solid #3b82f644; }
|
|
629
|
+
.badge-go { background: #06b6d422; color: #06b6d4; border: 1px solid #06b6d444; }
|
|
630
|
+
.badge-java { background: #f59e0b22; color: #f59e0b; border: 1px solid #f59e0b44; }
|
|
631
|
+
.badge-default { background: #6b728022; color: #9ca3af; border: 1px solid #6b728044; }
|
|
632
|
+
|
|
633
|
+
/* Kind badges */
|
|
634
|
+
.kind-badge {
|
|
635
|
+
display: inline-block;
|
|
636
|
+
font-size: 10px; font-weight: 600;
|
|
637
|
+
padding: 2px 7px; border-radius: 4px;
|
|
638
|
+
text-transform: capitalize;
|
|
639
|
+
}
|
|
640
|
+
.kind-function { background: #3b82f622; color: #60a5fa; border: 1px solid #3b82f644; }
|
|
641
|
+
.kind-class { background: #8b5cf622; color: #a78bfa; border: 1px solid #8b5cf644; }
|
|
642
|
+
.kind-export { background: #10b98122; color: #34d399; border: 1px solid #10b98144; }
|
|
643
|
+
.kind-import { background: #f59e0b22; color: #fbbf24; border: 1px solid #f59e0b44; }
|
|
644
|
+
.kind-arrowfunction { background: #06b6d422; color: #22d3ee; border: 1px solid #06b6d444; }
|
|
645
|
+
.kind-default { background: #6b728022; color: #9ca3af; border: 1px solid #6b728044; }
|
|
646
|
+
|
|
647
|
+
/* Dep badges */
|
|
648
|
+
.dep-badge {
|
|
649
|
+
display: inline-block;
|
|
650
|
+
font-size: 11px;
|
|
651
|
+
font-family: 'JetBrains Mono', Consolas, monospace;
|
|
652
|
+
background: var(--surface2);
|
|
653
|
+
border: 1px solid var(--border);
|
|
654
|
+
color: var(--text-muted);
|
|
655
|
+
padding: 1px 6px; border-radius: 4px;
|
|
656
|
+
margin: 2px 2px;
|
|
657
|
+
}
|
|
658
|
+
.none-label { color: var(--text-muted); font-style: italic; font-size: 12px; }
|
|
659
|
+
|
|
660
|
+
/* Validation */
|
|
661
|
+
.validation-summary {
|
|
662
|
+
display: flex; gap: 20px; margin-bottom: 14px; flex-wrap: wrap;
|
|
663
|
+
}
|
|
664
|
+
.val-count { font-size: 13px; font-weight: 600; }
|
|
665
|
+
.error-count { color: var(--red); }
|
|
666
|
+
.warn-count { color: var(--orange); }
|
|
667
|
+
.info-count { color: var(--blue); }
|
|
668
|
+
.validation-list { display: flex; flex-direction: column; gap: 6px; }
|
|
669
|
+
.validation-item {
|
|
670
|
+
padding: 10px 14px;
|
|
671
|
+
border-radius: 6px;
|
|
672
|
+
font-size: 13px;
|
|
673
|
+
border-left: 3px solid transparent;
|
|
674
|
+
}
|
|
675
|
+
.item-error { background: #ef444411; border-left-color: var(--red); color: #fca5a5; }
|
|
676
|
+
.item-warn { background: #f59e0b11; border-left-color: var(--orange); color: #fcd34d; }
|
|
677
|
+
.item-info { background: #3b82f611; border-left-color: var(--blue); color: #93c5fd; }
|
|
678
|
+
.item-ok { background: #10b98111; border-left-color: var(--green); color: #6ee7b7; }
|
|
679
|
+
.status-badge {
|
|
680
|
+
font-size: 11px; font-weight: 700;
|
|
681
|
+
padding: 3px 10px; border-radius: 20px;
|
|
682
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
683
|
+
}
|
|
684
|
+
.status-ok { background: #10b98122; color: #34d399; border: 1px solid #10b98144; }
|
|
685
|
+
.status-warn { background: #f59e0b22; color: #fbbf24; border: 1px solid #f59e0b44; }
|
|
686
|
+
.status-error { background: #ef444422; color: #f87171; border: 1px solid #ef444444; }
|
|
687
|
+
|
|
688
|
+
/* Bar chart */
|
|
689
|
+
.bar-chart { display: flex; flex-direction: column; gap: 10px; }
|
|
690
|
+
.bar-row { display: flex; align-items: center; gap: 12px; }
|
|
691
|
+
.bar-label { min-width: 110px; }
|
|
692
|
+
.bar-track {
|
|
693
|
+
flex: 1; height: 10px;
|
|
694
|
+
background: var(--surface2);
|
|
695
|
+
border-radius: 5px; overflow: hidden;
|
|
696
|
+
}
|
|
697
|
+
.bar-fill {
|
|
698
|
+
height: 100%; border-radius: 5px;
|
|
699
|
+
transition: width 0.6s ease;
|
|
700
|
+
}
|
|
701
|
+
.badge-js-fill { background: #f7df1e; }
|
|
702
|
+
.badge-jsx-fill { background: #61dafb; }
|
|
703
|
+
.badge-ts-fill { background: #3b82f6; }
|
|
704
|
+
.badge-tsx-fill { background: #06b6d4; }
|
|
705
|
+
.badge-json-fill { background: #10b981; }
|
|
706
|
+
.badge-md-fill { background: #8b5cf6; }
|
|
707
|
+
.badge-py-fill { background: #60a5fa; }
|
|
708
|
+
.badge-go-fill { background: #06b6d4; }
|
|
709
|
+
.badge-java-fill { background: #f59e0b; }
|
|
710
|
+
.badge-default-fill { background: #6b7280; }
|
|
711
|
+
.bar-stat { min-width: 80px; text-align: right; font-size: 12px; color: var(--text-muted); }
|
|
712
|
+
.bar-pct { color: var(--text-muted); }
|
|
713
|
+
|
|
714
|
+
/* Inline bar */
|
|
715
|
+
.inline-bar {
|
|
716
|
+
height: 8px; background: var(--surface2);
|
|
717
|
+
border-radius: 4px; overflow: hidden; min-width: 80px;
|
|
718
|
+
}
|
|
719
|
+
.inline-bar-fill {
|
|
720
|
+
height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent2));
|
|
721
|
+
border-radius: 4px;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/* Footer */
|
|
725
|
+
.footer {
|
|
726
|
+
text-align: center;
|
|
727
|
+
padding: 24px 40px;
|
|
728
|
+
border-top: 1px solid var(--border);
|
|
729
|
+
color: var(--text-muted);
|
|
730
|
+
font-size: 12px;
|
|
731
|
+
margin-top: 40px;
|
|
732
|
+
}
|
|
733
|
+
.footer a { color: var(--accent); }
|
|
734
|
+
|
|
735
|
+
/* Responsive */
|
|
736
|
+
@media (max-width: 768px) {
|
|
737
|
+
.header { padding: 20px; }
|
|
738
|
+
.main { padding: 20px; }
|
|
739
|
+
.nav-tabs { padding: 0 20px; }
|
|
740
|
+
.summary-grid { grid-template-columns: repeat(2, 1fr); }
|
|
741
|
+
.td-summary { display: none; }
|
|
742
|
+
}
|
|
743
|
+
@media (max-width: 480px) {
|
|
744
|
+
.summary-grid { grid-template-columns: 1fr 1fr; }
|
|
745
|
+
.header-meta { display: none; }
|
|
746
|
+
}
|
|
747
|
+
`;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Build the JavaScript string for interactivity
|
|
752
|
+
* @returns {string}
|
|
753
|
+
*/
|
|
754
|
+
function buildJS() {
|
|
755
|
+
return `
|
|
756
|
+
(function() {
|
|
757
|
+
// Tab switching
|
|
758
|
+
var tabs = document.querySelectorAll('.nav-tab');
|
|
759
|
+
var panels = document.querySelectorAll('.tab-panel');
|
|
760
|
+
tabs.forEach(function(tab) {
|
|
761
|
+
tab.addEventListener('click', function() {
|
|
762
|
+
var target = tab.getAttribute('data-tab');
|
|
763
|
+
tabs.forEach(function(t) { t.classList.remove('active'); });
|
|
764
|
+
panels.forEach(function(p) { p.classList.remove('active'); });
|
|
765
|
+
tab.classList.add('active');
|
|
766
|
+
var panel = document.getElementById('panel-' + target);
|
|
767
|
+
if (panel) panel.classList.add('active');
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// Populate language filter
|
|
772
|
+
var langFilter = document.getElementById('lang-filter');
|
|
773
|
+
if (langFilter) {
|
|
774
|
+
var rows = document.querySelectorAll('#file-table tbody tr');
|
|
775
|
+
var langs = {};
|
|
776
|
+
rows.forEach(function(r) { var l = r.getAttribute('data-lang'); if (l) langs[l] = true; });
|
|
777
|
+
Object.keys(langs).sort().forEach(function(l) {
|
|
778
|
+
var opt = document.createElement('option');
|
|
779
|
+
opt.value = l; opt.textContent = l;
|
|
780
|
+
langFilter.appendChild(opt);
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Table filter by text
|
|
785
|
+
window.filterTable = function(input, tableId) {
|
|
786
|
+
var q = input.value.toLowerCase();
|
|
787
|
+
var rows = document.querySelectorAll('#' + tableId + ' tbody tr');
|
|
788
|
+
rows.forEach(function(r) {
|
|
789
|
+
r.style.display = r.textContent.toLowerCase().includes(q) ? '' : 'none';
|
|
790
|
+
});
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// Filter by language
|
|
794
|
+
window.filterByLang = function(select, tableId) {
|
|
795
|
+
var lang = select.value;
|
|
796
|
+
var rows = document.querySelectorAll('#' + tableId + ' tbody tr');
|
|
797
|
+
rows.forEach(function(r) {
|
|
798
|
+
r.style.display = (!lang || r.getAttribute('data-lang') === lang) ? '' : 'none';
|
|
799
|
+
});
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
// Table sort
|
|
803
|
+
window.sortTable = function(tableId, colIdx) {
|
|
804
|
+
var table = document.getElementById(tableId);
|
|
805
|
+
if (!table) return;
|
|
806
|
+
var tbody = table.querySelector('tbody');
|
|
807
|
+
var rows = Array.from(tbody.querySelectorAll('tr'));
|
|
808
|
+
var asc = table.getAttribute('data-sort-col') === String(colIdx) &&
|
|
809
|
+
table.getAttribute('data-sort-dir') === 'asc';
|
|
810
|
+
rows.sort(function(a, b) {
|
|
811
|
+
var aText = (a.cells[colIdx] ? a.cells[colIdx].textContent : '').trim();
|
|
812
|
+
var bText = (b.cells[colIdx] ? b.cells[colIdx].textContent : '').trim();
|
|
813
|
+
var aNum = parseFloat(aText.replace(/[^0-9.-]/g, ''));
|
|
814
|
+
var bNum = parseFloat(bText.replace(/[^0-9.-]/g, ''));
|
|
815
|
+
if (!isNaN(aNum) && !isNaN(bNum)) {
|
|
816
|
+
return asc ? aNum - bNum : bNum - aNum;
|
|
817
|
+
}
|
|
818
|
+
return asc ? aText.localeCompare(bText) : bText.localeCompare(aText);
|
|
819
|
+
});
|
|
820
|
+
table.setAttribute('data-sort-col', String(colIdx));
|
|
821
|
+
table.setAttribute('data-sort-dir', asc ? 'desc' : 'asc');
|
|
822
|
+
rows.forEach(function(r) { tbody.appendChild(r); });
|
|
823
|
+
};
|
|
824
|
+
})();
|
|
825
|
+
`;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Assemble the full HTML document
|
|
830
|
+
* @param {object} opts
|
|
831
|
+
* @param {object} opts.bundle
|
|
832
|
+
* @param {object} opts.validation
|
|
833
|
+
* @param {object} opts.stats
|
|
834
|
+
* @param {string} opts.projectName
|
|
835
|
+
* @param {string} opts.generatedAt
|
|
836
|
+
* @returns {string}
|
|
837
|
+
*/
|
|
838
|
+
function buildHtmlDocument(opts) {
|
|
839
|
+
const { bundle, validation, stats, projectName, generatedAt } = opts || {};
|
|
840
|
+
|
|
841
|
+
const files = (bundle && bundle.files) ? bundle.files : [];
|
|
842
|
+
const symbolIndex = (bundle && bundle.symbolIndex) ? bundle.symbolIndex : {};
|
|
843
|
+
const depMap = (bundle && bundle.dependencyMap) ? bundle.dependencyMap : {};
|
|
844
|
+
|
|
845
|
+
const overviewHtml = `
|
|
846
|
+
${renderSummaryCards(stats)}
|
|
847
|
+
${renderLanguageBreakdown(files)}
|
|
848
|
+
${renderTopFiles(files)}
|
|
849
|
+
`;
|
|
850
|
+
|
|
851
|
+
const tabs = [
|
|
852
|
+
{ id: 'overview', label: '🏠 Overview' },
|
|
853
|
+
{ id: 'files', label: '📄 Files' },
|
|
854
|
+
{ id: 'symbols', label: '🔗 Symbols' },
|
|
855
|
+
{ id: 'deps', label: '🔁 Dependencies' },
|
|
856
|
+
{ id: 'validation', label: '⚠️ Validation' },
|
|
857
|
+
];
|
|
858
|
+
|
|
859
|
+
const navHtml = tabs.map((t, i) =>
|
|
860
|
+
`<button class="nav-tab${i === 0 ? ' active' : ''}" data-tab="${t.id}">${t.label}</button>`
|
|
861
|
+
).join('');
|
|
862
|
+
|
|
863
|
+
const panelsHtml = `
|
|
864
|
+
<div id="panel-overview" class="tab-panel active">${overviewHtml}</div>
|
|
865
|
+
<div id="panel-files" class="tab-panel">${renderFileTable(files)}</div>
|
|
866
|
+
<div id="panel-symbols" class="tab-panel">${renderSymbolIndex(symbolIndex)}</div>
|
|
867
|
+
<div id="panel-deps" class="tab-panel">${renderDependencyMap(depMap)}</div>
|
|
868
|
+
<div id="panel-validation" class="tab-panel">${renderValidation(validation)}</div>
|
|
869
|
+
`;
|
|
870
|
+
|
|
871
|
+
const safeProject = escapeHtml(projectName || 'Project');
|
|
872
|
+
const safeDate = escapeHtml(generatedAt || new Date().toISOString());
|
|
873
|
+
|
|
874
|
+
return `<!DOCTYPE html>
|
|
875
|
+
<html lang="en">
|
|
876
|
+
<head>
|
|
877
|
+
<meta charset="UTF-8" />
|
|
878
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
879
|
+
<title>ContextPack Report — ${safeProject}</title>
|
|
880
|
+
<style>${buildCSS()}</style>
|
|
881
|
+
</head>
|
|
882
|
+
<body>
|
|
883
|
+
<header class="header">
|
|
884
|
+
<div class="header-brand">
|
|
885
|
+
<div class="header-logo">📦</div>
|
|
886
|
+
<div>
|
|
887
|
+
<div class="header-title">ContextPack</div>
|
|
888
|
+
<div class="header-subtitle">Codebase Analysis Report</div>
|
|
889
|
+
<div class="pro-badge">⭐ Pro</div>
|
|
890
|
+
</div>
|
|
891
|
+
</div>
|
|
892
|
+
<div class="header-meta">
|
|
893
|
+
<strong>${safeProject}</strong>
|
|
894
|
+
Generated ${safeDate}
|
|
895
|
+
</div>
|
|
896
|
+
</header>
|
|
897
|
+
<nav class="nav-tabs">${navHtml}</nav>
|
|
898
|
+
<main class="main">${panelsHtml}</main>
|
|
899
|
+
<footer class="footer">
|
|
900
|
+
Generated by <a href="https://craftpipe.gumroad.com" target="_blank">ContextPack Pro</a>
|
|
901
|
+
— AI-ready codebase bundles
|
|
902
|
+
</footer>
|
|
903
|
+
<script>${buildJS()}</script>
|
|
904
|
+
</body>
|
|
905
|
+
</html>`;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Compute aggregate statistics from bundle and validation data
|
|
910
|
+
* @param {object} bundle
|
|
911
|
+
* @param {object} validation
|
|
912
|
+
* @param {string} rawJson
|
|
913
|
+
* @returns {object}
|
|
914
|
+
*/
|
|
915
|
+
function computeStats(bundle, validation, rawJson) {
|
|
916
|
+
const files = (bundle && bundle.files) ? bundle.files : [];
|
|
917
|
+
const symbolIndex = (bundle && bundle.symbolIndex) ? bundle.symbolIndex : {};
|
|
918
|
+
const depMap = (bundle && bundle.dependencyMap) ? bundle.dependencyMap : {};
|
|
919
|
+
|
|
920
|
+
const totalFiles = files.length;
|
|
921
|
+
const totalSymbols = Object.keys(symbolIndex).length;
|
|
922
|
+
const totalDeps = Object.keys(depMap).length;
|
|
923
|
+
|
|
924
|
+
let estimatedTokens = 0;
|
|
925
|
+
try {
|
|
926
|
+
estimatedTokens = calculateBundleSize(bundle) || estimateTokens(rawJson || '');
|
|
927
|
+
} catch (_) {
|
|
928
|
+
estimatedTokens = estimateTokens(rawJson || '');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const bundleSize = Buffer.byteLength(rawJson || '', 'utf8');
|
|
932
|
+
|
|
933
|
+
const validationErrors = validation && Array.isArray(validation.errors) ? validation.errors : [];
|
|
934
|
+
const validationWarnings = validation && Array.isArray(validation.warnings) ? validation.warnings : [];
|
|
935
|
+
const validationIssues = validationErrors.length + validationWarnings.length;
|
|
936
|
+
|
|
937
|
+
return { totalFiles, totalSymbols, totalDeps, estimatedTokens, bundleSize, validationIssues };
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Generate an HTML report for a ContextPack bundle.
|
|
942
|
+
*
|
|
943
|
+
* @param {object} opts
|
|
944
|
+
* @param {string} [opts.rootDir] - Root directory to scan (default: '.')
|
|
945
|
+
* @param {string} [opts.output] - Output HTML file path (default: 'contextpack-report.html')
|
|
946
|
+
* @param {string} [opts.configPath] - Path to .contextpackrc.json
|
|
947
|
+
* @param {object} [opts.flags] - Additional CLI flags to merge with config
|
|
948
|
+
* @param {boolean} [opts.open] - Attempt to open the report in a browser after generation
|
|
949
|
+
* @param {object} [opts.bundle] - Pre-built bundle (skips scan/analyze/bundle pipeline)
|
|
950
|
+
* @param {object} [opts.validation] - Pre-built validation result
|
|
951
|
+
* @param {boolean} [opts.verbose] - Verbose logging
|
|
952
|
+
* @returns {Promise<string>} - Resolves with the output file path
|
|
953
|
+
*/
|
|
954
|
+
async function generateHtmlReport(opts) {
|
|
955
|
+
const {
|
|
956
|
+
rootDir = '.',
|
|
957
|
+
output = 'contextpack-report.html',
|
|
958
|
+
configPath,
|
|
959
|
+
flags = {},
|
|
960
|
+
open: shouldOpen = false,
|
|
961
|
+
bundle: prebuiltBundle,
|
|
962
|
+
validation: prebuiltValidation,
|
|
963
|
+
verbose = false,
|
|
964
|
+
} = opts || {};
|
|
965
|
+
|
|
966
|
+
requirePro('HTML Report');
|
|
967
|
+
|
|
968
|
+
const log = verbose ? (...args) => console.log('[html-report]', ...args) : () => {};
|
|
969
|
+
|
|
970
|
+
// ── 1. Load config ──────────────────────────────────────────────────────────
|
|
971
|
+
let config = {};
|
|
972
|
+
try {
|
|
973
|
+
const raw = loadConfig(configPath || null);
|
|
974
|
+
config = mergeWithFlags(raw, { rootDir, ...flags });
|
|
975
|
+
log('Config loaded');
|
|
976
|
+
} catch (err) {
|
|
977
|
+
log('Config load failed, using defaults:', err.message);
|
|
978
|
+
config = mergeWithFlags({}, { rootDir, ...flags });
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// ── 2. Run pipeline (or use pre-built data) ─────────────────────────────────
|
|
982
|
+
let bundle = prebuiltBundle || null;
|
|
983
|
+
let validation = prebuiltValidation || null;
|
|
984
|
+
|
|
985
|
+
if (!bundle) {
|
|
986
|
+
log('Scanning project…');
|
|
987
|
+
let fileTree;
|
|
988
|
+
try {
|
|
989
|
+
fileTree = await scanProject(config);
|
|
990
|
+
log(`Scanned ${(fileTree && fileTree.files ? fileTree.files.length : 0)} files`);
|
|
991
|
+
} catch (err) {
|
|
992
|
+
throw new Error('[html-report] Scan failed: ' + err.message);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
log('Analyzing project…');
|
|
996
|
+
let analysis;
|
|
997
|
+
try {
|
|
998
|
+
analysis = await analyzeProject(fileTree, config);
|
|
999
|
+
log('Analysis complete');
|
|
1000
|
+
} catch (err) {
|
|
1001
|
+
throw new Error('[html-report] Analysis failed: ' + err.message);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
log('Building bundle…');
|
|
1005
|
+
try {
|
|
1006
|
+
bundle = createBundle(fileTree, analysis, config);
|
|
1007
|
+
log('Bundle created');
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
throw new Error('[html-report] Bundle creation failed: ' + err.message);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (!validation) {
|
|
1014
|
+
log('Validating bundle…');
|
|
1015
|
+
try {
|
|
1016
|
+
validation = validateBundle(bundle, config);
|
|
1017
|
+
log('Validation complete');
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
log('Validation error (non-fatal):', err.message);
|
|
1020
|
+
validation = { errors: [], warnings: [{ message: 'Validation step failed: ' + err.message }], info: [] };
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// ── 3. Compute stats ────────────────────────────────────────────────────────
|
|
1025
|
+
let rawJson = '';
|
|
1026
|
+
try {
|
|
1027
|
+
rawJson = JSON.stringify(bundle);
|
|
1028
|
+
} catch (_) {
|
|
1029
|
+
rawJson = '';
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const stats = computeStats(bundle, validation, rawJson);
|
|
1033
|
+
log('Stats:', stats);
|
|
1034
|
+
|
|
1035
|
+
// ── 4. Determine project name ───────────────────────────────────────────────
|
|
1036
|
+
let projectName = path.basename(path.resolve(rootDir));
|
|
1037
|
+
try {
|
|
1038
|
+
const pkgPath = path.join(path.resolve(rootDir), 'package.json');
|
|
1039
|
+
if (fs.existsSync(pkgPath)) {
|
|
1040
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
1041
|
+
if (pkg && pkg.name) projectName = pkg.name;
|
|
1042
|
+
}
|
|
1043
|
+
} catch (_) {}
|
|
1044
|
+
|
|
1045
|
+
// ── 5. Build HTML ───────────────────────────────────────────────────────────
|
|
1046
|
+
log('Building HTML document…');
|
|
1047
|
+
const generatedAt = new Date().toLocaleString('en-US', {
|
|
1048
|
+
year: 'numeric', month: 'short', day: 'numeric',
|
|
1049
|
+
hour: '2-digit', minute: '2-digit', timeZoneName: 'short',
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
let html;
|
|
1053
|
+
try {
|
|
1054
|
+
html = buildHtmlDocument({ bundle, validation, stats, projectName, generatedAt });
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
throw new Error('[html-report] HTML generation failed: ' + err.message);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// ── 6. Write output file ────────────────────────────────────────────────────
|
|
1060
|
+
const outputPath = path.resolve(output);
|
|
1061
|
+
try {
|
|
1062
|
+
const dir = path.dirname(outputPath);
|
|
1063
|
+
if (!fs.existsSync(dir)) {
|
|
1064
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1065
|
+
}
|
|
1066
|
+
fs.writeFileSync(outputPath, html, 'utf8');
|
|
1067
|
+
log('Report written to', outputPath);
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
throw new Error('[html-report] Failed to write output file: ' + err.message);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// ── 7. Optionally open in browser ───────────────────────────────────────────
|
|
1073
|
+
if (shouldOpen) {
|
|
1074
|
+
try {
|
|
1075
|
+
const { execSync } = require('child_process');
|
|
1076
|
+
const cmd = process.platform === 'win32'
|
|
1077
|
+
? `start "" "${outputPath}"`
|
|
1078
|
+
: process.platform === 'darwin'
|
|
1079
|
+
? `open "${outputPath}"`
|
|
1080
|
+
: `xdg-open "${outputPath}"`;
|
|
1081
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
1082
|
+
log('Opened report in browser');
|
|
1083
|
+
} catch (_) {
|
|
1084
|
+
log('Could not open browser automatically');
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
console.log(`\n ✅ HTML report generated: ${outputPath}`);
|
|
1089
|
+
console.log(` Files: ${stats.totalFiles} | Symbols: ${stats.totalSymbols} | Tokens: ${stats.estimatedTokens.toLocaleString()}\n`);
|
|
1090
|
+
|
|
1091
|
+
return outputPath;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
module.exports = { generateHtmlReport };
|