@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.
@@ -1,5 +1,58 @@
1
- import {k}from'./chunk-H7VVRHQZ.js';import*as c from'fs';import*as d from'path';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&&(c.writeFileSync(t.outputPath,s),console.log(`
2
- \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:n}=this.result;return `<!DOCTYPE html>
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">${n.totalRoutes.toLocaleString()}</div>
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">${n.totalControllers}</div>
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">${n.totalModels}</div>
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">${n.totalGrpcServices}</div>
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 (${n.namespaces.length})</div>
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()" style="padding:6px 12px;border-radius:4px;background:#3b82f6;color:#fff;border:none;cursor:pointer;">Apply</button>
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()" style="padding:4px 8px;border-radius:4px;background:#666;color:#fff;border:none;cursor:pointer;" title="Clear focus">\u2715</button>\` : ''}
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 m(){let o=process.argv[2]||process.cwd(),t=process.argv[3]||d.join(o,"rails-map.html");await new i(o).generate({title:"Rails Application Map",outputPath:t});}var p=import.meta.url===`file://${process.argv[1]}`;p&&m().catch(console.error);
1775
- export{i as a};
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=[],u=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:u,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,u,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,f=o?.hasNextjs||false,R=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>
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} ${u}</h1>
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&&f?`<div class="env-filters" style="display:flex;gap:4px;margin-right:8px">
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: ${f},
143
- hasReact: ${R}
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"],u=0;return Array.from(i.entries()).sort((t,o)=>t[0].localeCompare(o[0])).map(([t,o])=>{let e=r[u++%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(f=>f.repo&&f.repo!==d),C=d.split("/").pop()?.split("-").map(f=>f.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}">
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">