@wtdlee/repomap 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/analyzers/index.js +1 -1
- package/dist/{chunk-YKPXOHWZ.js → chunk-54DEMJO2.js} +204 -14
- package/dist/{chunk-LHP2OKKA.js → chunk-DSYVU23K.js} +233 -11
- package/dist/{chunk-ATRSGO6O.js → chunk-JBPSEUZS.js} +169 -6
- package/dist/chunk-O2SKKQVN.js +3 -0
- package/dist/cli.js +15 -15
- package/dist/{env-detector-RVGPBVNJ.js → env-detector-BIWJ7OYF.js} +1 -1
- package/dist/generators/assets/docs.css +40 -3
- package/dist/generators/assets/page-map.css +24 -0
- package/dist/generators/assets/rails-map.css +49 -0
- package/dist/generators/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/page-map-generator-SL3H6CKF.js +1 -0
- package/dist/{rails-57MNOGHR.js → rails-3HNUFTQV.js} +1 -1
- package/dist/rails-map-generator-GV4ZIVJJ.js +1 -0
- package/dist/server/index.js +1 -1
- package/dist/types.d.ts +4 -0
- package/package.json +16 -7
- package/dist/chunk-IGRCLYVZ.js +0 -451
- package/dist/chunk-ZWRDP37E.js +0 -1
- package/dist/page-map-generator-LTVRHSDC.js +0 -1
- package/dist/rails-map-generator-JNU5QHX4.js +0 -1
|
@@ -1,5 +1,58 @@
|
|
|
1
|
-
import {k}from'./chunk-H7VVRHQZ.js';import*as
|
|
2
|
-
|
|
1
|
+
import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as m from'path';var c=`
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0;overflow:hidden" aria-hidden="true" focusable="false">
|
|
3
|
+
<symbol id="icon-zoom-in" viewBox="0 0 24 24">
|
|
4
|
+
<circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"/>
|
|
5
|
+
<line x1="11" y1="8" x2="11" y2="14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
6
|
+
<line x1="8" y1="11" x2="14" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
7
|
+
<line x1="20" y1="20" x2="16.65" y2="16.65" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
8
|
+
</symbol>
|
|
9
|
+
|
|
10
|
+
<symbol id="icon-zoom-out" viewBox="0 0 24 24">
|
|
11
|
+
<circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"/>
|
|
12
|
+
<line x1="8" y1="11" x2="14" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
13
|
+
<line x1="20" y1="20" x2="16.65" y2="16.65" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
14
|
+
</symbol>
|
|
15
|
+
|
|
16
|
+
<symbol id="icon-reset" viewBox="0 0 24 24">
|
|
17
|
+
<path d="M3 12a9 9 0 1 0 3-6.7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
18
|
+
<polyline points="3 4 3 10 9 10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
19
|
+
</symbol>
|
|
20
|
+
|
|
21
|
+
<symbol id="icon-fullscreen" viewBox="0 0 24 24">
|
|
22
|
+
<polyline points="15 3 21 3 21 9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
23
|
+
<polyline points="9 21 3 21 3 15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
24
|
+
<line x1="21" y1="3" x2="14" y2="10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
25
|
+
<line x1="3" y1="21" x2="10" y2="14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
26
|
+
</symbol>
|
|
27
|
+
|
|
28
|
+
<symbol id="icon-copy" viewBox="0 0 24 24">
|
|
29
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2"/>
|
|
30
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
31
|
+
</symbol>
|
|
32
|
+
|
|
33
|
+
<symbol id="icon-download" viewBox="0 0 24 24">
|
|
34
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
35
|
+
<polyline points="7 10 12 15 17 10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
36
|
+
<line x1="12" y1="15" x2="12" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
37
|
+
</symbol>
|
|
38
|
+
|
|
39
|
+
<symbol id="icon-image" viewBox="0 0 24 24">
|
|
40
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2"/>
|
|
41
|
+
<circle cx="8.5" cy="8.5" r="1.5" fill="none" stroke="currentColor" stroke-width="2"/>
|
|
42
|
+
<polyline points="21 15 16 10 5 21" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
43
|
+
</symbol>
|
|
44
|
+
|
|
45
|
+
<symbol id="icon-x" viewBox="0 0 24 24">
|
|
46
|
+
<line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
47
|
+
<line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
48
|
+
</symbol>
|
|
49
|
+
|
|
50
|
+
<symbol id="icon-check" viewBox="0 0 24 24">
|
|
51
|
+
<polyline points="20 6 9 17 4 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
52
|
+
</symbol>
|
|
53
|
+
</svg>
|
|
54
|
+
`.trim();var i=class{constructor(t){this.rootPath=t;}result=null;async generate(t={}){if(!this.rootPath)throw new Error("Root path required for analysis");let{title:e="Rails Application Map"}=t;this.result=await k(this.rootPath);let s=this.generateHTML(e);return t.outputPath&&(d.writeFileSync(t.outputPath,s),console.log(`
|
|
55
|
+
\u{1F4C4} Generated: ${t.outputPath}`)),s}generateFromResult(t,e="Rails Application Map"){return this.result=t,this.generateHTML(e)}generateHTML(t){if(!this.result)throw new Error("Analysis not run");let{routes:e,controllers:s,models:a,grpc:l,summary:o}=this.result;return `<!DOCTYPE html>
|
|
3
56
|
<html lang="en">
|
|
4
57
|
<head>
|
|
5
58
|
<meta charset="UTF-8">
|
|
@@ -13,6 +66,7 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as c from'fs';import*as d from'path';
|
|
|
13
66
|
<link rel="stylesheet" href="/rails-map.css">
|
|
14
67
|
</head>
|
|
15
68
|
<body>
|
|
69
|
+
${c}
|
|
16
70
|
<header>
|
|
17
71
|
<h1>\u{1F6E4}\uFE0F ${t}</h1>
|
|
18
72
|
<nav class="header-nav">
|
|
@@ -23,25 +77,25 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as c from'fs';import*as d from'path';
|
|
|
23
77
|
<div class="stats-bar">
|
|
24
78
|
<div class="stat active" data-view="routes">
|
|
25
79
|
<div>
|
|
26
|
-
<div class="stat-value">${
|
|
80
|
+
<div class="stat-value">${o.totalRoutes.toLocaleString()}</div>
|
|
27
81
|
<div class="stat-label">Routes</div>
|
|
28
82
|
</div>
|
|
29
83
|
</div>
|
|
30
84
|
<div class="stat" data-view="controllers">
|
|
31
85
|
<div>
|
|
32
|
-
<div class="stat-value">${
|
|
86
|
+
<div class="stat-value">${o.totalControllers}</div>
|
|
33
87
|
<div class="stat-label">Controllers</div>
|
|
34
88
|
</div>
|
|
35
89
|
</div>
|
|
36
90
|
<div class="stat" data-view="models">
|
|
37
91
|
<div>
|
|
38
|
-
<div class="stat-value">${
|
|
92
|
+
<div class="stat-value">${o.totalModels}</div>
|
|
39
93
|
<div class="stat-label">Models</div>
|
|
40
94
|
</div>
|
|
41
95
|
</div>
|
|
42
96
|
<div class="stat" data-view="grpc">
|
|
43
97
|
<div>
|
|
44
|
-
<div class="stat-value">${
|
|
98
|
+
<div class="stat-value">${o.totalGrpcServices}</div>
|
|
45
99
|
<div class="stat-label">gRPC</div>
|
|
46
100
|
</div>
|
|
47
101
|
</div>
|
|
@@ -62,7 +116,7 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as c from'fs';import*as d from'path';
|
|
|
62
116
|
</div>
|
|
63
117
|
|
|
64
118
|
<div class="sidebar-section namespaces" id="namespaceFilter">
|
|
65
|
-
<div class="sidebar-title">Namespaces (${
|
|
119
|
+
<div class="sidebar-title">Namespaces (${o.namespaces.length})</div>
|
|
66
120
|
<div class="namespace-list">
|
|
67
121
|
<div class="namespace-item active" data-namespace="all">
|
|
68
122
|
<span>All</span>
|
|
@@ -1036,6 +1090,9 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as c from'fs';import*as d from'path';
|
|
|
1036
1090
|
updateDiagram();
|
|
1037
1091
|
};
|
|
1038
1092
|
|
|
1093
|
+
// Keep latest mermaid source for copy/export.
|
|
1094
|
+
window.railsMermaidRaw = window.railsMermaidRaw || '';
|
|
1095
|
+
|
|
1039
1096
|
window.updateDiagram = function() {
|
|
1040
1097
|
const countInput = document.getElementById('model-count-input');
|
|
1041
1098
|
const countSelect = document.getElementById('model-count-select');
|
|
@@ -1070,6 +1127,7 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as c from'fs';import*as d from'path';
|
|
|
1070
1127
|
}
|
|
1071
1128
|
|
|
1072
1129
|
const { mermaidCode, modelCount, totalModels } = generateMermaidCode(count, diagramNamespace, diagramFocusModel, diagramDepth);
|
|
1130
|
+
window.railsMermaidRaw = mermaidCode;
|
|
1073
1131
|
|
|
1074
1132
|
// Update diagram - need to recreate SVG
|
|
1075
1133
|
const container = document.getElementById('mermaid-container');
|
|
@@ -1108,6 +1166,7 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as c from'fs';import*as d from'path';
|
|
|
1108
1166
|
const namespaces = getNamespaces();
|
|
1109
1167
|
const modelNames = getModelNames();
|
|
1110
1168
|
const { mermaidCode, modelCount, totalModels } = generateMermaidCode(diagramModelCount, diagramNamespace, diagramFocusModel, diagramDepth);
|
|
1169
|
+
window.railsMermaidRaw = mermaidCode;
|
|
1111
1170
|
|
|
1112
1171
|
let filterText = '';
|
|
1113
1172
|
if (diagramFocusModel) {
|
|
@@ -1122,6 +1181,17 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as c from'fs';import*as d from'path';
|
|
|
1122
1181
|
<div class="diagram-view-wrapper" style="display:flex;flex-direction:column;height:100%;min-height:0;">
|
|
1123
1182
|
<div class="panel-header" style="flex-wrap:wrap;gap:8px;flex-shrink:0;">
|
|
1124
1183
|
<div class="panel-title diagram-title-text">Model Relationships (\${modelCount}/\${totalModels} models\${filterText})</div>
|
|
1184
|
+
<div class="diagram-actions" style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
|
|
1185
|
+
<button class="diagram-action-btn icon" id="rails-copy-btn" onclick="copyRailsMermaid()" title="Copy Mermaid source to clipboard" aria-label="Copy Mermaid source">
|
|
1186
|
+
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" class="icon"><use href="#icon-copy"></use><use xlink:href="#icon-copy"></use></svg>
|
|
1187
|
+
</button>
|
|
1188
|
+
<button class="diagram-action-btn icon" onclick="downloadRailsDiagramSvg()" title="Download diagram as SVG" aria-label="Download SVG">
|
|
1189
|
+
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" class="icon"><use href="#icon-download"></use><use xlink:href="#icon-download"></use></svg>
|
|
1190
|
+
</button>
|
|
1191
|
+
<button class="diagram-action-btn icon" onclick="downloadRailsDiagramPng()" title="Download diagram as PNG" aria-label="Download PNG">
|
|
1192
|
+
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" class="icon"><use href="#icon-image"></use><use xlink:href="#icon-image"></use></svg>
|
|
1193
|
+
</button>
|
|
1194
|
+
</div>
|
|
1125
1195
|
<div class="diagram-filters" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;font-size:12px;">
|
|
1126
1196
|
<label style="display:flex;align-items:center;gap:6px;">
|
|
1127
1197
|
<span>Limit:</span>
|
|
@@ -1138,7 +1208,7 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as c from'fs';import*as d from'path';
|
|
|
1138
1208
|
value="\${isCustom ? diagramModelCount : ''}"
|
|
1139
1209
|
style="width:100px;padding:6px 10px;border-radius:4px;background:#2d2d2d;color:#fff;border:1px solid #444;"
|
|
1140
1210
|
onchange="updateDiagram()" onkeyup="if(event.key==='Enter')updateDiagram()">
|
|
1141
|
-
<button onclick="updateDiagram()"
|
|
1211
|
+
<button class="diagram-action-btn icon primary" onclick="updateDiagram()" title="Apply" aria-label="Apply"><svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" class="icon"><use href="#icon-check"></use><use xlink:href="#icon-check"></use></svg></button>
|
|
1142
1212
|
</div>
|
|
1143
1213
|
</label>
|
|
1144
1214
|
<label style="display:flex;align-items:center;gap:6px;">
|
|
@@ -1154,7 +1224,7 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as c from'fs';import*as d from'path';
|
|
|
1154
1224
|
<option value="">None</option>
|
|
1155
1225
|
\${modelNames.map(name => \`<option value="\${name}" \${diagramFocusModel === name ? 'selected' : ''}>\${name}</option>\`).join('')}
|
|
1156
1226
|
</select>
|
|
1157
|
-
\${diagramFocusModel ? \`<button onclick="clearFocusModel()"
|
|
1227
|
+
\${diagramFocusModel ? \`<button class="diagram-action-btn icon subtle" onclick="clearFocusModel()" title="Clear focus" aria-label="Clear focus"><svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" class="icon"><use href="#icon-x"></use><use xlink:href="#icon-x"></use></svg></button>\` : ''}
|
|
1158
1228
|
</label>
|
|
1159
1229
|
<label style="display:flex;align-items:center;gap:6px;">
|
|
1160
1230
|
<span style="opacity:\${diagramFocusModel ? 1 : 0.5}">Depth:</span>
|
|
@@ -1208,6 +1278,158 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as c from'fs';import*as d from'path';
|
|
|
1208
1278
|
document.head.appendChild(script);
|
|
1209
1279
|
}
|
|
1210
1280
|
|
|
1281
|
+
async function copyTextToClipboard(text) {
|
|
1282
|
+
try {
|
|
1283
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1284
|
+
await navigator.clipboard.writeText(text);
|
|
1285
|
+
return true;
|
|
1286
|
+
}
|
|
1287
|
+
} catch {
|
|
1288
|
+
// fallthrough
|
|
1289
|
+
}
|
|
1290
|
+
try {
|
|
1291
|
+
const ta = document.createElement('textarea');
|
|
1292
|
+
ta.value = text;
|
|
1293
|
+
ta.style.position = 'fixed';
|
|
1294
|
+
ta.style.left = '-9999px';
|
|
1295
|
+
document.body.appendChild(ta);
|
|
1296
|
+
ta.select();
|
|
1297
|
+
const ok = document.execCommand('copy');
|
|
1298
|
+
ta.remove();
|
|
1299
|
+
return ok;
|
|
1300
|
+
} catch {
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function timestamp() {
|
|
1306
|
+
const d = new Date();
|
|
1307
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
1308
|
+
return (
|
|
1309
|
+
String(d.getFullYear()) +
|
|
1310
|
+
pad2(d.getMonth() + 1) +
|
|
1311
|
+
pad2(d.getDate()) +
|
|
1312
|
+
'_' +
|
|
1313
|
+
pad2(d.getHours()) +
|
|
1314
|
+
pad2(d.getMinutes()) +
|
|
1315
|
+
pad2(d.getSeconds())
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function downloadBlob(filename, blob) {
|
|
1320
|
+
const url = URL.createObjectURL(blob);
|
|
1321
|
+
const a = document.createElement('a');
|
|
1322
|
+
a.href = url;
|
|
1323
|
+
a.download = filename;
|
|
1324
|
+
document.body.appendChild(a);
|
|
1325
|
+
a.click();
|
|
1326
|
+
a.remove();
|
|
1327
|
+
setTimeout(() => URL.revokeObjectURL(url), 500);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function getRailsDiagramSvgEl() {
|
|
1331
|
+
const container = document.getElementById('mermaid-container');
|
|
1332
|
+
if (!container) return null;
|
|
1333
|
+
return container.querySelector('svg');
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
window.copyRailsMermaid = async function() {
|
|
1337
|
+
const raw = (window.railsMermaidRaw || '').trim();
|
|
1338
|
+
if (!raw) return;
|
|
1339
|
+
const ok = await copyTextToClipboard(raw);
|
|
1340
|
+
const btn = document.getElementById('rails-copy-btn');
|
|
1341
|
+
if (btn) {
|
|
1342
|
+
btn.classList.toggle('is-ok', ok);
|
|
1343
|
+
btn.classList.toggle('is-fail', !ok);
|
|
1344
|
+
const oldTitle = btn.getAttribute('title') || '';
|
|
1345
|
+
btn.setAttribute('title', ok ? 'Copied' : 'Copy failed');
|
|
1346
|
+
setTimeout(() => {
|
|
1347
|
+
btn.classList.remove('is-ok', 'is-fail');
|
|
1348
|
+
btn.setAttribute('title', oldTitle);
|
|
1349
|
+
}, 900);
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
window.downloadRailsDiagramSvg = function() {
|
|
1354
|
+
const svg = getRailsDiagramSvgEl();
|
|
1355
|
+
if (!svg) return;
|
|
1356
|
+
const ser = new XMLSerializer();
|
|
1357
|
+
const svgText = ser.serializeToString(svg);
|
|
1358
|
+
const blob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' });
|
|
1359
|
+
downloadBlob('repomap-rails-diagram_' + timestamp() + '.svg', blob);
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
window.downloadRailsDiagramPng = function() {
|
|
1363
|
+
const svg = getRailsDiagramSvgEl();
|
|
1364
|
+
if (!svg) return;
|
|
1365
|
+
const ser = new XMLSerializer();
|
|
1366
|
+
const getSvgSize = (svgEl) => {
|
|
1367
|
+
try {
|
|
1368
|
+
const vb = svgEl.viewBox && svgEl.viewBox.baseVal;
|
|
1369
|
+
if (vb && vb.width && vb.height) return { w: vb.width, h: vb.height };
|
|
1370
|
+
} catch {}
|
|
1371
|
+
try {
|
|
1372
|
+
const w = svgEl.width && svgEl.width.baseVal && svgEl.width.baseVal.value;
|
|
1373
|
+
const h = svgEl.height && svgEl.height.baseVal && svgEl.height.baseVal.value;
|
|
1374
|
+
if (w && h) return { w, h };
|
|
1375
|
+
} catch {}
|
|
1376
|
+
try {
|
|
1377
|
+
const bb = svgEl.getBBox();
|
|
1378
|
+
if (bb && bb.width && bb.height) return { w: bb.width, h: bb.height };
|
|
1379
|
+
} catch {}
|
|
1380
|
+
const r = svgEl.getBoundingClientRect();
|
|
1381
|
+
return { w: Math.max(1, r.width), h: Math.max(1, r.height) };
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
const s = getSvgSize(svg);
|
|
1385
|
+
const w = Math.max(1, Math.round(s.w));
|
|
1386
|
+
const h = Math.max(1, Math.round(s.h));
|
|
1387
|
+
|
|
1388
|
+
const svgClone = svg.cloneNode(true);
|
|
1389
|
+
try {
|
|
1390
|
+
svgClone.setAttribute('width', String(w));
|
|
1391
|
+
svgClone.setAttribute('height', String(h));
|
|
1392
|
+
if (!svgClone.getAttribute('viewBox')) {
|
|
1393
|
+
svgClone.setAttribute('viewBox', '0 0 ' + String(w) + ' ' + String(h));
|
|
1394
|
+
}
|
|
1395
|
+
} catch {
|
|
1396
|
+
// ignore
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
let svgText = ser.serializeToString(svgClone);
|
|
1400
|
+
if (!/xmlns=/.test(svgText)) {
|
|
1401
|
+
svgText = svgText.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
1402
|
+
}
|
|
1403
|
+
if (!/xmlns:xlink=/.test(svgText)) {
|
|
1404
|
+
svgText = svgText.replace('<svg', '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
|
|
1405
|
+
}
|
|
1406
|
+
const svgBase64 = btoa(unescape(encodeURIComponent(svgText)));
|
|
1407
|
+
const dataUrl = 'data:image/svg+xml;base64,' + svgBase64;
|
|
1408
|
+
|
|
1409
|
+
const img = new Image();
|
|
1410
|
+
img.decoding = 'async';
|
|
1411
|
+
img.onload = () => {
|
|
1412
|
+
try {
|
|
1413
|
+
const canvas = document.createElement('canvas');
|
|
1414
|
+
canvas.width = w;
|
|
1415
|
+
canvas.height = h;
|
|
1416
|
+
const g = canvas.getContext('2d');
|
|
1417
|
+
if (!g) return;
|
|
1418
|
+
g.fillStyle = '#ffffff';
|
|
1419
|
+
g.fillRect(0, 0, w, h);
|
|
1420
|
+
g.drawImage(img, 0, 0, w, h);
|
|
1421
|
+
canvas.toBlob((pngBlob) => {
|
|
1422
|
+
if (pngBlob) {
|
|
1423
|
+
downloadBlob('repomap-rails-diagram_' + timestamp() + '.png', pngBlob);
|
|
1424
|
+
}
|
|
1425
|
+
}, 'image/png');
|
|
1426
|
+
} catch {
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
img.onerror = () => {};
|
|
1430
|
+
img.src = dataUrl;
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1211
1433
|
// Pan and zoom functionality for mermaid diagram
|
|
1212
1434
|
function initDiagramPanZoom() {
|
|
1213
1435
|
const container = document.getElementById('mermaid-container');
|
|
@@ -1771,5 +1993,5 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as c from'fs';import*as d from'path';
|
|
|
1771
1993
|
`).join("")}
|
|
1772
1994
|
</tbody>
|
|
1773
1995
|
</table>
|
|
1774
|
-
`}highlightParams(t){return t.replace(/:([a-zA-Z_]+)/g,'<span class="param">:$1</span>')}};async function
|
|
1775
|
-
export{i as
|
|
1996
|
+
`}highlightParams(t){return t.replace(/:([a-zA-Z_]+)/g,'<span class="param">:$1</span>')}};async function p(){let n=process.argv[2]||process.cwd(),t=process.argv[3]||m.join(n,"rails-map.html");await new i(n).generate({title:"Rails Application Map",outputPath:t});}var u=import.meta.url===`file://${process.argv[1]}`;u&&p().catch(console.error);
|
|
1997
|
+
export{c as a,i as b};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var E=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(i,n){let r=[],
|
|
1
|
+
var E=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(i,n){let r=[],f=n?.envResult,t=n?.railsAnalysis,o=n?.activeTab||"pages",e=i.repositories[0]?.displayName||i.repositories[0]?.name||"Repository";for(let p of i.repositories){this.graphqlOps.push(...p.analysis?.graphqlOperations||[]),this.apiCalls.push(...p.analysis?.apiCalls||[]);let g=p.analysis?.components||[];for(let a of g)this.components.push({name:a.name,filePath:a.filePath,type:a.type,dependencies:a.dependencies||[],hooks:a.hooks||[]});}for(let p of i.repositories){let g=p.analysis?.pages||[];for(let a of g)r.push({...a,repo:p.name,children:[],parent:null,depth:0});}let{rootPages:l,relations:s}=this.buildHierarchy(r);return this.renderPageMapHtml(r,l,s,e,{envResult:f,railsAnalysis:t,activeTab:o})}buildHierarchy(i){let n=new Map,r=[];for(let t of i)n.set(t.path,t);for(let t of i){let o=t.path.split("/").filter(Boolean);for(let e=o.length-1;e>=1;e--){let l="/"+o.slice(0,e).join("/"),s=n.get(l);if(s){t.parent=l,t.depth=s.depth+1,s.children.includes(t.path)||s.children.push(t.path),r.push({from:l,to:t.path,type:"parent-child",description:`Sub-page of ${l}`});break}}if(t.parent||(t.depth=Math.max(0,o.length-1)),t.layout)for(let e of i)e.path!==t.path&&e.layout===t.layout&&(r.find(s=>s.type==="same-layout"&&(s.from===t.path&&s.to===e.path||s.from===e.path&&s.to===t.path))||r.push({from:t.path,to:e.path,type:"same-layout",description:`Both use ${t.layout}`}));}return {rootPages:i.filter(t=>!t.parent).sort((t,o)=>t.path.localeCompare(o.path)),relations:r}}renderPageMapHtml(i,n,r,f,t){let o=t?.envResult,e=t?.railsAnalysis,l=t?.activeTab||"pages",s=c=>JSON.stringify(c).replace(/</g,"\\u003c"),p=s(this.graphqlOps.map(c=>({name:c.name,type:c.type,variables:c.variables,fields:c.fields,returnType:c.returnType,usedIn:c.usedIn}))),g=s(this.components),a=s(i),h=s(r),v=s(this.apiCalls),y=e?s(e.routes.routes):"[]",d=e?s(e.controllers.controllers):"[]",w=e?s(e.models.models):"[]",C=e?s(e.views):'{ "views": [], "pages": [], "summary": {} }',P=e?s(e.react):'{ "components": [], "entryPoints": [], "summary": {} }',x=e?s(e.grpc):'{ "services": [] }',S=e?s(e.summary):"null",m=o?.hasRails||false,u=o?.hasNextjs||false,I=o?.hasReact||false,b=new Map;for(let c of i){let k=c.path.split("/").filter(Boolean)[0]||"root";b.has(k)||b.set(k,[]),b.get(k)?.push(c);}return `<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
@@ -14,7 +14,7 @@ var E=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(i,n){let
|
|
|
14
14
|
<body>
|
|
15
15
|
<header class="header">
|
|
16
16
|
<div style="display:flex;align-items:center;gap:24px">
|
|
17
|
-
<h1 style="cursor:pointer" onclick="location.href='/'">\u{1F4CA} ${
|
|
17
|
+
<h1 style="cursor:pointer" onclick="location.href='/'">\u{1F4CA} ${f}</h1>
|
|
18
18
|
<nav style="display:flex;gap:4px">
|
|
19
19
|
<a href="/page-map" class="nav-link ${l==="pages"?"active":""}">Page Map</a>
|
|
20
20
|
${m?`<a href="/rails-map" class="nav-link ${l==="rails"?"active":""}">Rails Map</a>`:""}
|
|
@@ -24,7 +24,7 @@ var E=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(i,n){let
|
|
|
24
24
|
</div>
|
|
25
25
|
<div style="display:flex;gap:12px;align-items:center">
|
|
26
26
|
<!-- Environment filter badges -->
|
|
27
|
-
${m&&
|
|
27
|
+
${m&&u?`<div class="env-filters" style="display:flex;gap:4px;margin-right:8px">
|
|
28
28
|
<button class="env-badge env-badge-active" data-env="all" onclick="filterByEnv('all')">All</button>
|
|
29
29
|
<button class="env-badge" data-env="nextjs" onclick="filterByEnv('nextjs')">\u269B\uFE0F Next.js</button>
|
|
30
30
|
<button class="env-badge" data-env="rails" onclick="filterByEnv('rails')">\u{1F6E4}\uFE0F Rails</button>
|
|
@@ -117,7 +117,10 @@ var E=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(i,n){let
|
|
|
117
117
|
<div class="detail" id="detail">
|
|
118
118
|
<div class="detail-header">
|
|
119
119
|
<div class="detail-title" id="detail-title"></div>
|
|
120
|
+
<div class="detail-actions">
|
|
121
|
+
<button class="detail-export" id="detail-export" onclick="exportSelectedPageCsv()" disabled title="Export the selected page as a CSV">Export CSV</button>
|
|
120
122
|
<button class="detail-close" onclick="closeDetail()">\xD7</button>
|
|
123
|
+
</div>
|
|
121
124
|
</div>
|
|
122
125
|
<div class="detail-body" id="detail-body"></div>
|
|
123
126
|
</div>
|
|
@@ -139,8 +142,8 @@ var E=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(i,n){let
|
|
|
139
142
|
// Environment detection results
|
|
140
143
|
const envInfo = {
|
|
141
144
|
hasRails: ${m},
|
|
142
|
-
hasNextjs: ${
|
|
143
|
-
hasReact: ${
|
|
145
|
+
hasNextjs: ${u},
|
|
146
|
+
hasReact: ${I}
|
|
144
147
|
};
|
|
145
148
|
|
|
146
149
|
// Frontend data
|
|
@@ -1880,10 +1883,167 @@ var E=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(i,n){let
|
|
|
1880
1883
|
showDetail(path);
|
|
1881
1884
|
}
|
|
1882
1885
|
|
|
1886
|
+
// Selected page state for export
|
|
1887
|
+
let selectedPagePathForExport = null;
|
|
1888
|
+
|
|
1889
|
+
function csvEscape(v) {
|
|
1890
|
+
const s = v == null ? '' : String(v);
|
|
1891
|
+
if (/[",\\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
|
|
1892
|
+
return s;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
function downloadCsv(filename, csvText) {
|
|
1896
|
+
const blob = new Blob([csvText], { type: 'text/csv;charset=utf-8' });
|
|
1897
|
+
const url = URL.createObjectURL(blob);
|
|
1898
|
+
const a = document.createElement('a');
|
|
1899
|
+
a.href = url;
|
|
1900
|
+
a.download = filename;
|
|
1901
|
+
document.body.appendChild(a);
|
|
1902
|
+
a.click();
|
|
1903
|
+
a.remove();
|
|
1904
|
+
setTimeout(() => URL.revokeObjectURL(url), 250);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
function buildSelectedPageCsv(pagePath) {
|
|
1908
|
+
const page = pageMap.get(pagePath);
|
|
1909
|
+
if (!page) return null;
|
|
1910
|
+
|
|
1911
|
+
const rows = [];
|
|
1912
|
+
// Header
|
|
1913
|
+
rows.push(['section', 'type', 'name', 'origin', 'detail']);
|
|
1914
|
+
|
|
1915
|
+
// Page info
|
|
1916
|
+
rows.push(['page', 'path', page.path || pagePath, '']);
|
|
1917
|
+
rows.push(['page', 'file', page.filePath || '', '']);
|
|
1918
|
+
rows.push(['page', 'layout', page.layout || '', '']);
|
|
1919
|
+
rows.push(['page', 'auth', page.authentication?.required ? 'required' : 'none', '']);
|
|
1920
|
+
if (page.params && page.params.length) {
|
|
1921
|
+
rows.push(['page', 'params', page.params.join(', '), '']);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// Steps
|
|
1925
|
+
(page.steps || []).forEach((st, idx) => {
|
|
1926
|
+
const stepName = st.name || ('Step ' + (st.id || (idx + 1)));
|
|
1927
|
+
const comp = st.component ? ('component: ' + st.component) : '';
|
|
1928
|
+
rows.push(['steps', 'step', stepName, '', comp]);
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
// Related pages (hierarchy + links)
|
|
1932
|
+
if (page.parent) rows.push(['related', 'parent', page.parent, '', '']);
|
|
1933
|
+
(page.children || []).forEach((c) => rows.push(['related', 'child', c, '', '']));
|
|
1934
|
+
(page.linkedPages || []).forEach((lp) => rows.push(['related', 'link', lp, '', '']));
|
|
1935
|
+
|
|
1936
|
+
// GraphQL (from enriched page.dataFetching)
|
|
1937
|
+
const dfs = page.dataFetching || [];
|
|
1938
|
+
dfs
|
|
1939
|
+
.filter((df) => df && df.type !== 'component')
|
|
1940
|
+
.forEach((df) => {
|
|
1941
|
+
const kind = (df.type || '').includes('Mutation') ? 'mutation' : 'query';
|
|
1942
|
+
|
|
1943
|
+
// Human-friendly origin normalization (Direct / Close / Indirect / Common)
|
|
1944
|
+
const rawName = df.operationName || df.queryName || '';
|
|
1945
|
+
const arrowCount = (rawName.match(/\u2192/g) || []).length;
|
|
1946
|
+
let name = String(rawName).replace(/^[\u2192s]+/, '').trim();
|
|
1947
|
+
|
|
1948
|
+
// Extract "(via xxx)" if present
|
|
1949
|
+
let origin = 'Direct';
|
|
1950
|
+
let detail = '';
|
|
1951
|
+
const viaMatch = name.match(/s*(vias+([^)]+))/);
|
|
1952
|
+
if (viaMatch) {
|
|
1953
|
+
detail = viaMatch[1];
|
|
1954
|
+
name = name.replace(viaMatch[0], '').trim();
|
|
1955
|
+
origin = arrowCount ? 'Indirect' : 'Close';
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
const source = String(df.source || '');
|
|
1959
|
+
if (!viaMatch && source) {
|
|
1960
|
+
if (source.startsWith('common:')) {
|
|
1961
|
+
origin = 'Common';
|
|
1962
|
+
detail = source.replace('common:', '');
|
|
1963
|
+
} else if (source.startsWith('close:')) {
|
|
1964
|
+
origin = 'Close';
|
|
1965
|
+
detail = source.replace('close:', '');
|
|
1966
|
+
} else if (source.startsWith('indirect:') || source.startsWith('usedIn:')) {
|
|
1967
|
+
origin = 'Indirect';
|
|
1968
|
+
detail = source.replace(/^indirect:|^usedIn:/, '');
|
|
1969
|
+
} else if (source.startsWith('import:')) {
|
|
1970
|
+
origin = 'Import';
|
|
1971
|
+
detail = source.replace('import:', '');
|
|
1972
|
+
} else if (source.startsWith('hook:')) {
|
|
1973
|
+
origin = 'Close';
|
|
1974
|
+
detail = source.replace('hook:', '');
|
|
1975
|
+
} else if (source.startsWith('component:')) {
|
|
1976
|
+
origin = 'Close';
|
|
1977
|
+
detail = source.replace('component:', '');
|
|
1978
|
+
} else {
|
|
1979
|
+
// Unknown source format; keep in detail
|
|
1980
|
+
detail = source;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
// Map to human labels (English)
|
|
1985
|
+
const originLabelMap = {
|
|
1986
|
+
Direct: 'Direct (this page)',
|
|
1987
|
+
Close: 'Close (nearby)',
|
|
1988
|
+
Indirect: 'Indirect (reference)',
|
|
1989
|
+
Common: 'Common (shared)',
|
|
1990
|
+
Import: 'Import',
|
|
1991
|
+
};
|
|
1992
|
+
const originLabel = originLabelMap[origin] || origin;
|
|
1993
|
+
|
|
1994
|
+
rows.push(['graphql', kind, name, originLabel, detail || '']);
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
// Used components (if present as component refs)
|
|
1998
|
+
dfs
|
|
1999
|
+
.filter((df) => df && df.type === 'component')
|
|
2000
|
+
.forEach((df) => rows.push(['components', 'used', df.operationName || '', '', '']));
|
|
2001
|
+
|
|
2002
|
+
// REST API calls (best-effort match as used in the UI)
|
|
2003
|
+
const pageFileName = page.filePath?.split('/').pop() || '';
|
|
2004
|
+
const pageBaseName = pageFileName.replace(/\\.(tsx?|jsx?)$/, '');
|
|
2005
|
+
const pageApis = apiCallsData.filter(api => {
|
|
2006
|
+
if (!api.filePath || !page.filePath) return false;
|
|
2007
|
+
return api.filePath.includes(page.filePath) ||
|
|
2008
|
+
page.filePath.includes(api.filePath) ||
|
|
2009
|
+
api.filePath.endsWith(pageFileName) ||
|
|
2010
|
+
api.filePath.includes('/' + pageBaseName + '/');
|
|
2011
|
+
});
|
|
2012
|
+
pageApis.forEach((api) => {
|
|
2013
|
+
rows.push(['rest', api.method || '', api.url || '', '', api.filePath || '']);
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
// Serialize
|
|
2017
|
+
return rows.map((r) => r.map(csvEscape).join(',')).join('\\n') + '\\n';
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function exportSelectedPageCsv() {
|
|
2021
|
+
if (!selectedPagePathForExport) return;
|
|
2022
|
+
const csv = buildSelectedPageCsv(selectedPagePathForExport);
|
|
2023
|
+
if (!csv) return;
|
|
2024
|
+
const safe = selectedPagePathForExport.replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
|
|
2025
|
+
const d = new Date();
|
|
2026
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
2027
|
+
const ts =
|
|
2028
|
+
String(d.getFullYear()) +
|
|
2029
|
+
pad2(d.getMonth() + 1) +
|
|
2030
|
+
pad2(d.getDate()) +
|
|
2031
|
+
'_' +
|
|
2032
|
+
pad2(d.getHours()) +
|
|
2033
|
+
pad2(d.getMinutes()) +
|
|
2034
|
+
pad2(d.getSeconds());
|
|
2035
|
+
const fileName = 'repomap-page_' + (safe || 'page') + '_' + ts + '.csv';
|
|
2036
|
+
downloadCsv(fileName, csv);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
1883
2039
|
function showDetail(path) {
|
|
1884
2040
|
const page = pageMap.get(path);
|
|
1885
2041
|
if (!page) return;
|
|
1886
2042
|
|
|
2043
|
+
selectedPagePathForExport = path;
|
|
2044
|
+
const exportBtn = document.getElementById('detail-export');
|
|
2045
|
+
if (exportBtn) exportBtn.disabled = false;
|
|
2046
|
+
|
|
1887
2047
|
const rels = relations.filter(r => r.from === path || r.to === path);
|
|
1888
2048
|
const parent = page.parent ? pageMap.get(page.parent) : null;
|
|
1889
2049
|
const children = (page.children || []).map(c => pageMap.get(c)).filter(Boolean);
|
|
@@ -2230,6 +2390,9 @@ var E=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(i,n){let
|
|
|
2230
2390
|
function closeDetail() {
|
|
2231
2391
|
document.getElementById('detail').classList.remove('open');
|
|
2232
2392
|
document.querySelectorAll('.page-item').forEach(p => p.classList.remove('selected'));
|
|
2393
|
+
selectedPagePathForExport = null;
|
|
2394
|
+
const exportBtn = document.getElementById('detail-export');
|
|
2395
|
+
if (exportBtn) exportBtn.disabled = true;
|
|
2233
2396
|
}
|
|
2234
2397
|
|
|
2235
2398
|
// Filter by stat type
|
|
@@ -3695,7 +3858,7 @@ var E=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(i,n){let
|
|
|
3695
3858
|
setTimeout(updatePageGqlCounts, 100);
|
|
3696
3859
|
</script>
|
|
3697
3860
|
</body>
|
|
3698
|
-
</html>`}buildTreeHtml(i,n){let r=["#ef4444","#f97316","#eab308","#22c55e","#14b8a6","#3b82f6","#8b5cf6","#ec4899"],
|
|
3861
|
+
</html>`}buildTreeHtml(i,n){let r=["#ef4444","#f97316","#eab308","#22c55e","#14b8a6","#3b82f6","#8b5cf6","#ec4899"],f=0;return Array.from(i.entries()).sort((t,o)=>t[0].localeCompare(o[0])).map(([t,o])=>{let e=r[f++%r.length],l=o.sort((a,h)=>a.path.localeCompare(h.path)),s=new Set(l.map(a=>a.path)),p=new Map;for(let a of l){let h=a.path.split("/").filter(Boolean),v=0;for(let y=h.length-1;y>=1;y--){let d="/"+h.slice(0,y).join("/");if(s.has(d)){v=(p.get(d)??0)+1;break}}p.set(a.path,v);}let g=l.map(a=>{let h=this.getPageType(a.path),v=p.get(a.path)??0,d=a.repo||"",w=n.some(u=>u.repo&&u.repo!==d),C=d.split("/").pop()?.split("-").map(u=>u.substring(0,4)).join("-")||d.substring(0,8),P=w&&d?`<span class="tag tag-repo" title="${d}">${C}</span>`:"",x=/^\/[A-Z]/.test(a.path)||a.filePath&&a.filePath.includes("components/pages"),S=x&&a.filePath?a.filePath.replace(/\.tsx?$/,"").replace(/^(frontend\/src\/|src\/)/,""):a.path,m=x?'<span class="tag tag-info" title="SPA Component Page">SPA</span>':"";return `<div class="page-item" data-path="${a.path}" data-repo="${d}" onclick="selectPage('${a.path}')" style="--depth:${v}">
|
|
3699
3862
|
<span class="page-type" style="--type-color:${h.color}">${h.label}</span>
|
|
3700
3863
|
<span class="page-path">${S}</span>
|
|
3701
3864
|
<div class="page-tags">
|