@wtdlee/repomap 0.1.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +527 -0
  3. package/dist/analyzers/base-analyzer.d.ts +46 -0
  4. package/dist/analyzers/base-analyzer.d.ts.map +1 -0
  5. package/dist/analyzers/base-analyzer.js +48 -0
  6. package/dist/analyzers/base-analyzer.js.map +1 -0
  7. package/dist/analyzers/dataflow-analyzer.d.ts +30 -0
  8. package/dist/analyzers/dataflow-analyzer.d.ts.map +1 -0
  9. package/dist/analyzers/dataflow-analyzer.js +426 -0
  10. package/dist/analyzers/dataflow-analyzer.js.map +1 -0
  11. package/dist/analyzers/graphql-analyzer.d.ts +23 -0
  12. package/dist/analyzers/graphql-analyzer.d.ts.map +1 -0
  13. package/dist/analyzers/graphql-analyzer.js +387 -0
  14. package/dist/analyzers/graphql-analyzer.js.map +1 -0
  15. package/dist/analyzers/index.d.ts +6 -0
  16. package/dist/analyzers/index.d.ts.map +1 -0
  17. package/dist/analyzers/index.js +6 -0
  18. package/dist/analyzers/index.js.map +1 -0
  19. package/dist/analyzers/pages-analyzer.d.ts +85 -0
  20. package/dist/analyzers/pages-analyzer.d.ts.map +1 -0
  21. package/dist/analyzers/pages-analyzer.js +1696 -0
  22. package/dist/analyzers/pages-analyzer.js.map +1 -0
  23. package/dist/analyzers/rails/index.d.ts +47 -0
  24. package/dist/analyzers/rails/index.d.ts.map +1 -0
  25. package/dist/analyzers/rails/index.js +146 -0
  26. package/dist/analyzers/rails/index.js.map +1 -0
  27. package/dist/analyzers/rails/rails-controller-analyzer.d.ts +83 -0
  28. package/dist/analyzers/rails/rails-controller-analyzer.d.ts.map +1 -0
  29. package/dist/analyzers/rails/rails-controller-analyzer.js +479 -0
  30. package/dist/analyzers/rails/rails-controller-analyzer.js.map +1 -0
  31. package/dist/analyzers/rails/rails-grpc-analyzer.d.ts +45 -0
  32. package/dist/analyzers/rails/rails-grpc-analyzer.d.ts.map +1 -0
  33. package/dist/analyzers/rails/rails-grpc-analyzer.js +263 -0
  34. package/dist/analyzers/rails/rails-grpc-analyzer.js.map +1 -0
  35. package/dist/analyzers/rails/rails-model-analyzer.d.ts +89 -0
  36. package/dist/analyzers/rails/rails-model-analyzer.d.ts.map +1 -0
  37. package/dist/analyzers/rails/rails-model-analyzer.js +494 -0
  38. package/dist/analyzers/rails/rails-model-analyzer.js.map +1 -0
  39. package/dist/analyzers/rails/rails-react-analyzer.d.ts +42 -0
  40. package/dist/analyzers/rails/rails-react-analyzer.d.ts.map +1 -0
  41. package/dist/analyzers/rails/rails-react-analyzer.js +530 -0
  42. package/dist/analyzers/rails/rails-react-analyzer.js.map +1 -0
  43. package/dist/analyzers/rails/rails-routes-analyzer.d.ts +63 -0
  44. package/dist/analyzers/rails/rails-routes-analyzer.d.ts.map +1 -0
  45. package/dist/analyzers/rails/rails-routes-analyzer.js +541 -0
  46. package/dist/analyzers/rails/rails-routes-analyzer.js.map +1 -0
  47. package/dist/analyzers/rails/rails-view-analyzer.d.ts +50 -0
  48. package/dist/analyzers/rails/rails-view-analyzer.d.ts.map +1 -0
  49. package/dist/analyzers/rails/rails-view-analyzer.js +387 -0
  50. package/dist/analyzers/rails/rails-view-analyzer.js.map +1 -0
  51. package/dist/analyzers/rails/ruby-parser.d.ts +64 -0
  52. package/dist/analyzers/rails/ruby-parser.d.ts.map +1 -0
  53. package/dist/analyzers/rails/ruby-parser.js +213 -0
  54. package/dist/analyzers/rails/ruby-parser.js.map +1 -0
  55. package/dist/analyzers/rest-api-analyzer.d.ts +66 -0
  56. package/dist/analyzers/rest-api-analyzer.d.ts.map +1 -0
  57. package/dist/analyzers/rest-api-analyzer.js +480 -0
  58. package/dist/analyzers/rest-api-analyzer.js.map +1 -0
  59. package/dist/cli.d.ts +3 -0
  60. package/dist/cli.d.ts.map +1 -0
  61. package/dist/cli.js +550 -0
  62. package/dist/cli.js.map +1 -0
  63. package/dist/core/cache.d.ts +48 -0
  64. package/dist/core/cache.d.ts.map +1 -0
  65. package/dist/core/cache.js +152 -0
  66. package/dist/core/cache.js.map +1 -0
  67. package/dist/core/engine.d.ts +47 -0
  68. package/dist/core/engine.d.ts.map +1 -0
  69. package/dist/core/engine.js +320 -0
  70. package/dist/core/engine.js.map +1 -0
  71. package/dist/core/index.d.ts +3 -0
  72. package/dist/core/index.d.ts.map +1 -0
  73. package/dist/core/index.js +3 -0
  74. package/dist/core/index.js.map +1 -0
  75. package/dist/generators/assets/common.css +187 -0
  76. package/dist/generators/assets/docs.css +363 -0
  77. package/dist/generators/assets/page-map.css +305 -0
  78. package/dist/generators/assets/rails-map.css +473 -0
  79. package/dist/generators/index.d.ts +4 -0
  80. package/dist/generators/index.d.ts.map +1 -0
  81. package/dist/generators/index.js +4 -0
  82. package/dist/generators/index.js.map +1 -0
  83. package/dist/generators/markdown-generator.d.ts +26 -0
  84. package/dist/generators/markdown-generator.d.ts.map +1 -0
  85. package/dist/generators/markdown-generator.js +783 -0
  86. package/dist/generators/markdown-generator.js.map +1 -0
  87. package/dist/generators/mermaid-generator.d.ts +36 -0
  88. package/dist/generators/mermaid-generator.d.ts.map +1 -0
  89. package/dist/generators/mermaid-generator.js +365 -0
  90. package/dist/generators/mermaid-generator.js.map +1 -0
  91. package/dist/generators/page-map-generator.d.ts +23 -0
  92. package/dist/generators/page-map-generator.d.ts.map +1 -0
  93. package/dist/generators/page-map-generator.js +3563 -0
  94. package/dist/generators/page-map-generator.js.map +1 -0
  95. package/dist/generators/rails-map-generator.d.ts +22 -0
  96. package/dist/generators/rails-map-generator.d.ts.map +1 -0
  97. package/dist/generators/rails-map-generator.js +909 -0
  98. package/dist/generators/rails-map-generator.js.map +1 -0
  99. package/dist/index.d.ts +11 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +12 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/server/doc-server.d.ts +31 -0
  104. package/dist/server/doc-server.d.ts.map +1 -0
  105. package/dist/server/doc-server.js +1233 -0
  106. package/dist/server/doc-server.js.map +1 -0
  107. package/dist/server/index.d.ts +2 -0
  108. package/dist/server/index.d.ts.map +1 -0
  109. package/dist/server/index.js +2 -0
  110. package/dist/server/index.js.map +1 -0
  111. package/dist/types.d.ts +294 -0
  112. package/dist/types.d.ts.map +1 -0
  113. package/dist/types.js +6 -0
  114. package/dist/types.js.map +1 -0
  115. package/dist/utils/env-detector.d.ts +32 -0
  116. package/dist/utils/env-detector.d.ts.map +1 -0
  117. package/dist/utils/env-detector.js +189 -0
  118. package/dist/utils/env-detector.js.map +1 -0
  119. package/dist/utils/parallel.d.ts +24 -0
  120. package/dist/utils/parallel.d.ts.map +1 -0
  121. package/dist/utils/parallel.js +71 -0
  122. package/dist/utils/parallel.js.map +1 -0
  123. package/package.json +131 -0
@@ -0,0 +1,1233 @@
1
+ import express from 'express';
2
+ import { Server } from 'socket.io';
3
+ import * as http from 'http';
4
+ import * as fs from 'fs/promises';
5
+ import * as path from 'path';
6
+ import { marked } from 'marked';
7
+ import { DocGeneratorEngine } from '../core/engine.js';
8
+ import { PageMapGenerator } from '../generators/page-map-generator.js';
9
+ import { RailsMapGenerator } from '../generators/rails-map-generator.js';
10
+ import { detectEnvironments } from '../utils/env-detector.js';
11
+ import { analyzeRailsApp } from '../analyzers/rails/index.js';
12
+ /**
13
+ * Documentation server with live reload
14
+ * ライブリロード機能付きドキュメントサーバー
15
+ */
16
+ export class DocServer {
17
+ config;
18
+ port;
19
+ app;
20
+ server;
21
+ io;
22
+ engine;
23
+ currentReport = null;
24
+ envResult = null;
25
+ railsAnalysis = null;
26
+ constructor(config, port = 3030, options) {
27
+ this.config = config;
28
+ this.port = port;
29
+ this.app = express();
30
+ this.server = http.createServer(this.app);
31
+ this.io = new Server(this.server);
32
+ this.engine = new DocGeneratorEngine(config, { noCache: options?.noCache });
33
+ this.setupRoutes();
34
+ this.setupSocketIO();
35
+ }
36
+ setupRoutes() {
37
+ // Serve static assets
38
+ this.app.use('/assets', express.static(path.join(this.config.outputDir, 'assets')));
39
+ // Serve CSS files from generators/assets
40
+ const cssFiles = ['common.css', 'page-map.css', 'docs.css', 'rails-map.css'];
41
+ cssFiles.forEach((file) => {
42
+ this.app.get(`/${file}`, async (req, res) => {
43
+ try {
44
+ const cssPath = new URL(`../generators/assets/${file}`, import.meta.url);
45
+ const css = await fs.readFile(cssPath, 'utf-8');
46
+ res.type('text/css').send(css);
47
+ }
48
+ catch {
49
+ res.status(404).send('CSS not found');
50
+ }
51
+ });
52
+ });
53
+ // Main page - redirect to page-map
54
+ this.app.get('/', (req, res) => {
55
+ res.redirect('/page-map');
56
+ });
57
+ // Interactive page map (main view) - now with environment awareness
58
+ this.app.get('/page-map', (req, res) => {
59
+ if (!this.currentReport) {
60
+ res.status(503).send('Documentation not ready yet');
61
+ return;
62
+ }
63
+ const generator = new PageMapGenerator();
64
+ res.send(generator.generatePageMapHtml(this.currentReport, {
65
+ envResult: this.envResult,
66
+ railsAnalysis: this.railsAnalysis,
67
+ }));
68
+ });
69
+ // Rails map (standalone view)
70
+ this.app.get('/rails-map', (req, res) => {
71
+ if (!this.railsAnalysis) {
72
+ res.status(404).send('No Rails environment detected');
73
+ return;
74
+ }
75
+ const generator = new RailsMapGenerator();
76
+ res.send(generator.generateFromResult(this.railsAnalysis));
77
+ });
78
+ // Markdown pages - index
79
+ this.app.get('/docs', async (req, res) => {
80
+ res.send(await this.renderPage('index'));
81
+ });
82
+ // Markdown pages - specific path
83
+ this.app.get('/docs/*', async (req, res) => {
84
+ const pagePath = req.params[0] || 'index';
85
+ res.send(await this.renderPage(pagePath));
86
+ });
87
+ // API endpoints
88
+ this.app.get('/api/report', (req, res) => {
89
+ res.json(this.currentReport);
90
+ });
91
+ // Environment detection result
92
+ this.app.get('/api/env', (req, res) => {
93
+ res.json(this.envResult);
94
+ });
95
+ // Rails analysis result
96
+ this.app.get('/api/rails', (req, res) => {
97
+ if (this.railsAnalysis) {
98
+ res.json(this.railsAnalysis);
99
+ }
100
+ else {
101
+ res.status(404).json({ error: 'No Rails analysis available' });
102
+ }
103
+ });
104
+ this.app.get('/api/diagram/:name', (req, res) => {
105
+ const diagram = this.currentReport?.diagrams.find((d) => d.title.toLowerCase().replace(/\s+/g, '-') === req.params.name);
106
+ if (diagram) {
107
+ res.json(diagram);
108
+ }
109
+ else {
110
+ res.status(404).json({ error: 'Diagram not found' });
111
+ }
112
+ });
113
+ // Regenerate endpoint
114
+ this.app.post('/api/regenerate', async (req, res) => {
115
+ try {
116
+ await this.regenerate();
117
+ res.json({ success: true });
118
+ }
119
+ catch (error) {
120
+ res.status(500).json({ error: error.message });
121
+ }
122
+ });
123
+ }
124
+ setupSocketIO() {
125
+ this.io.on('connection', (socket) => {
126
+ console.log('Client connected');
127
+ socket.on('disconnect', () => {
128
+ console.log('Client disconnected');
129
+ });
130
+ });
131
+ }
132
+ async renderPage(pagePath) {
133
+ // Remove .md extension if present
134
+ const cleanPath = pagePath.replace(/\.md$/, '');
135
+ const mdPath = path.join(this.config.outputDir, `${cleanPath}.md`);
136
+ let content = '';
137
+ try {
138
+ const markdown = await fs.readFile(mdPath, 'utf-8');
139
+ // Parse markdown to HTML
140
+ let html = await marked.parse(markdown);
141
+ // Convert mermaid code blocks to mermaid divs
142
+ // marked renders: <pre><code class="language-mermaid">...</code></pre>
143
+ // mermaid expects: <div class="mermaid">...</div>
144
+ html = html.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, '<div class="mermaid">$1</div>');
145
+ // Wrap tables for horizontal scroll
146
+ html = html.replace(/<table>/g, '<div class="table-wrapper"><table>');
147
+ html = html.replace(/<\/table>/g, '</table></div>');
148
+ content = html;
149
+ }
150
+ catch (e) {
151
+ console.error(`Failed to render page: ${mdPath}`, e);
152
+ content = `<h1>Page not found</h1><p>Path: ${cleanPath}</p>`;
153
+ }
154
+ return this.getHtmlTemplate(content);
155
+ }
156
+ getGraphQLData() {
157
+ if (!this.currentReport)
158
+ return '[]';
159
+ const ops = [];
160
+ for (const repo of this.currentReport.repositories) {
161
+ for (const op of repo.analysis?.graphqlOperations || []) {
162
+ ops.push({
163
+ name: op.name,
164
+ type: op.type,
165
+ returnType: op.returnType,
166
+ variables: op.variables,
167
+ fields: op.fields,
168
+ usedIn: op.usedIn,
169
+ });
170
+ }
171
+ }
172
+ return JSON.stringify(ops);
173
+ }
174
+ getApiCallsData() {
175
+ if (!this.currentReport)
176
+ return '[]';
177
+ const calls = [];
178
+ for (const repo of this.currentReport.repositories) {
179
+ for (const call of repo.analysis?.apiCalls || []) {
180
+ calls.push({
181
+ id: call.id,
182
+ method: call.method,
183
+ url: call.url,
184
+ callType: call.callType,
185
+ filePath: call.filePath,
186
+ line: call.line,
187
+ containingFunction: call.containingFunction,
188
+ requiresAuth: call.requiresAuth,
189
+ });
190
+ }
191
+ }
192
+ return JSON.stringify(calls);
193
+ }
194
+ getHtmlTemplate(content) {
195
+ const graphqlData = this.getGraphQLData();
196
+ const apiCallsData = this.getApiCallsData();
197
+ return `<!DOCTYPE html>
198
+ <html lang="ja">
199
+ <head>
200
+ <meta charset="UTF-8">
201
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
202
+ <title>${this.config.site.title}</title>
203
+ <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
204
+ <script src="/socket.io/socket.io.js"></script>
205
+ <script>
206
+ window.graphqlOps = ${graphqlData};
207
+ window.apiCalls = ${apiCallsData};
208
+ // Create multiple lookup maps for different naming conventions
209
+ window.gqlMap = new Map();
210
+ window.gqlMapNormalized = new Map();
211
+
212
+ // Normalize name: remove Query/Mutation suffix, convert to lowercase
213
+ function normalizeName(name) {
214
+ return name
215
+ .replace(/Query$|Mutation$|Fragment$/i, '')
216
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
217
+ .toLowerCase();
218
+ }
219
+
220
+ // Convert to UPPER_SNAKE_CASE
221
+ function toUpperSnake(name) {
222
+ return name
223
+ .replace(/Query$|Mutation$|Fragment$/i, '')
224
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
225
+ .toUpperCase();
226
+ }
227
+
228
+ window.graphqlOps.forEach(op => {
229
+ // Store by exact name
230
+ window.gqlMap.set(op.name, op);
231
+ // Store by name without suffix
232
+ window.gqlMap.set(op.name.replace(/Query$|Mutation$|Fragment$/i, ''), op);
233
+ // Store by normalized name
234
+ window.gqlMapNormalized.set(normalizeName(op.name), op);
235
+ // Store by UPPER_SNAKE_CASE
236
+ window.gqlMap.set(toUpperSnake(op.name), op);
237
+ // Store by return type if available
238
+ if (op.returnType) {
239
+ window.gqlMap.set(op.returnType, op);
240
+ window.gqlMapNormalized.set(op.returnType.toLowerCase(), op);
241
+ }
242
+ });
243
+
244
+ // Enhanced lookup function
245
+ window.findGraphQLOp = function(name) {
246
+ if (!name) return null;
247
+ // Try exact match first
248
+ let op = window.gqlMap.get(name);
249
+ if (op) return op;
250
+
251
+ // Try normalized match
252
+ op = window.gqlMapNormalized.get(normalizeName(name));
253
+ if (op) return op;
254
+
255
+ // Try UPPER_SNAKE_CASE
256
+ op = window.gqlMap.get(name.toUpperCase().replace(/-/g, '_'));
257
+ if (op) return op;
258
+
259
+ // Try partial match
260
+ for (const [key, val] of window.gqlMap.entries()) {
261
+ if (key.toLowerCase().includes(name.toLowerCase()) ||
262
+ name.toLowerCase().includes(key.toLowerCase())) {
263
+ return val;
264
+ }
265
+ }
266
+
267
+ return null;
268
+ };
269
+ </script>
270
+ <link rel="stylesheet" href="/docs.css">
271
+ </head>
272
+ <body>
273
+ <header class="header">
274
+ <div style="display:flex;align-items:center;gap:24px">
275
+ <h1 style="cursor:pointer" onclick="location.href='/'">📊 ${this.config.repositories[0]?.displayName || this.config.repositories[0]?.name || 'Repository'}</h1>
276
+ <nav style="display:flex;gap:4px">
277
+ <a href="/page-map" class="nav-link">Page Map</a>
278
+ ${this.railsAnalysis ? '<a href="/rails-map" class="nav-link">Rails Map</a>' : ''}
279
+ <a href="/docs" class="nav-link active">Docs</a>
280
+ <a href="/api/report" class="nav-link" target="_blank">API</a>
281
+ </nav>
282
+ </div>
283
+ </header>
284
+ <div class="main">
285
+ <aside class="sidebar">
286
+ <nav>
287
+ <div class="nav-group">
288
+ <span class="nav-group-title">Documentation</span>
289
+ <div class="nav-subitems">
290
+ ${this.config.repositories
291
+ .map((repo) => `
292
+ <a href="/docs/repos/${repo.name}/pages">Pages</a>
293
+ <a href="/docs/repos/${repo.name}/components">Components</a>
294
+ <a href="/docs/repos/${repo.name}/graphql">GraphQL</a>
295
+ <a href="/docs/repos/${repo.name}/dataflow">Data Flow</a>
296
+ `)
297
+ .join('')}
298
+ </div>
299
+ </div>
300
+ <div class="nav-group">
301
+ <span class="nav-group-title">Analysis</span>
302
+ <div class="nav-subitems">
303
+ <a href="/docs/cross-repo">Cross Repository</a>
304
+ <a href="/docs/diagrams">Diagrams</a>
305
+ </div>
306
+ </div>
307
+ </nav>
308
+ <button class="regenerate-btn" onclick="regenerate()">Regenerate</button>
309
+ </aside>
310
+ <div class="content-area">
311
+ <div class="content">
312
+ ${content}
313
+ </div>
314
+ </div>
315
+ </div>
316
+ <div class="live-indicator">Live</div>
317
+
318
+ <!-- Detail Modal -->
319
+ <div class="detail-modal" id="detailModal">
320
+ <div class="detail-modal-content">
321
+ <div class="detail-modal-header">
322
+ <div style="display:flex;align-items:center;gap:8px">
323
+ <button id="modalBackBtn" onclick="modalBack()" style="display:none;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:4px 8px;cursor:pointer;font-size:14px">← Back</button>
324
+ <h3 id="modalTitle">Details</h3>
325
+ </div>
326
+ <button class="detail-modal-close" onclick="closeModal()">×</button>
327
+ </div>
328
+ <div id="modalBody"></div>
329
+ </div>
330
+ </div>
331
+
332
+ <script>
333
+ // Initialize Mermaid
334
+ mermaid.initialize({
335
+ startOnLoad: false,
336
+ theme: 'neutral',
337
+ securityLevel: 'loose',
338
+ flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'basis' }
339
+ });
340
+
341
+ // Diagram state per diagram
342
+ const diagramStates = new Map();
343
+
344
+ // Render all mermaid diagrams on page load
345
+ document.addEventListener('DOMContentLoaded', async () => {
346
+ // Wrap mermaid divs with container and controls
347
+ document.querySelectorAll('.mermaid').forEach((el, idx) => {
348
+ const container = document.createElement('div');
349
+ container.className = 'mermaid-container';
350
+ container.innerHTML = \`
351
+ <div class="mermaid-controls">
352
+ <button onclick="zoomDiagram(\${idx}, 0.8)" title="縮小">➖</button>
353
+ <button onclick="zoomDiagram(\${idx}, 1.25)" title="拡大">➕</button>
354
+ <button onclick="zoomDiagram(\${idx}, 'reset')" title="リセット">🔄</button>
355
+ <button onclick="toggleFullscreen(\${idx})" title="全画面">⛶</button>
356
+ </div>
357
+ <div class="mermaid-wrapper" id="wrapper-\${idx}">
358
+ <div class="mermaid-inner" id="inner-\${idx}"></div>
359
+ </div>
360
+ \`;
361
+ el.parentNode.insertBefore(container, el);
362
+ container.querySelector('.mermaid-inner').appendChild(el);
363
+ el.dataset.idx = idx;
364
+ diagramStates.set(idx, { zoom: 1, panX: 0, panY: 0 });
365
+
366
+ // Setup drag handlers
367
+ setupDragHandlers(idx);
368
+ });
369
+
370
+ try {
371
+ await mermaid.run({ querySelector: '.mermaid' });
372
+
373
+ // Add click handlers to nodes
374
+ document.querySelectorAll('.mermaid .node').forEach(node => {
375
+ node.addEventListener('click', (e) => {
376
+ e.stopPropagation();
377
+ const text = node.querySelector('span, text, .nodeLabel')?.textContent || '';
378
+ showNodeDetail(text, node);
379
+ });
380
+ });
381
+ } catch (e) {
382
+ console.error('Mermaid rendering error:', e);
383
+ }
384
+ });
385
+
386
+ function setupDragHandlers(idx) {
387
+ const wrapper = document.getElementById(\`wrapper-\${idx}\`);
388
+ const inner = document.getElementById(\`inner-\${idx}\`);
389
+ if (!wrapper || !inner) return;
390
+
391
+ let isDragging = false;
392
+ let startX, startY, startPanX, startPanY;
393
+
394
+ wrapper.addEventListener('mousedown', (e) => {
395
+ if (e.target.closest('.node')) return; // Don't drag when clicking nodes
396
+ isDragging = true;
397
+ wrapper.classList.add('dragging');
398
+ startX = e.clientX;
399
+ startY = e.clientY;
400
+ const state = diagramStates.get(idx);
401
+ startPanX = state.panX;
402
+ startPanY = state.panY;
403
+ e.preventDefault();
404
+ });
405
+
406
+ document.addEventListener('mousemove', (e) => {
407
+ if (!isDragging) return;
408
+ const state = diagramStates.get(idx);
409
+ state.panX = startPanX + (e.clientX - startX);
410
+ state.panY = startPanY + (e.clientY - startY);
411
+ updateTransform(idx);
412
+ });
413
+
414
+ document.addEventListener('mouseup', () => {
415
+ isDragging = false;
416
+ wrapper.classList.remove('dragging');
417
+ });
418
+
419
+ // Mouse wheel zoom - increased max zoom to 20 for detailed viewing
420
+ wrapper.addEventListener('wheel', (e) => {
421
+ e.preventDefault();
422
+ const state = diagramStates.get(idx);
423
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
424
+ state.zoom = Math.max(0.05, Math.min(20, state.zoom * delta));
425
+ updateTransform(idx);
426
+ });
427
+ }
428
+
429
+ function updateTransform(idx) {
430
+ const inner = document.getElementById(\`inner-\${idx}\`);
431
+ const state = diagramStates.get(idx);
432
+ if (inner && state) {
433
+ inner.style.transform = \`translate(\${state.panX}px, \${state.panY}px) scale(\${state.zoom})\`;
434
+ }
435
+ }
436
+
437
+ function zoomDiagram(idx, factor) {
438
+ const state = diagramStates.get(idx);
439
+ if (!state) return;
440
+
441
+ if (factor === 'reset') {
442
+ state.zoom = 1;
443
+ state.panX = 0;
444
+ state.panY = 0;
445
+ } else {
446
+ // Increased max zoom to 20 for detailed viewing of large diagrams
447
+ state.zoom = Math.max(0.05, Math.min(20, state.zoom * factor));
448
+ }
449
+ updateTransform(idx);
450
+ }
451
+
452
+ function toggleFullscreen(idx) {
453
+ const container = document.getElementById(\`wrapper-\${idx}\`)?.closest('.mermaid-container');
454
+ if (!container) return;
455
+ const wrapper = container.querySelector('.mermaid-wrapper');
456
+
457
+ if (container.classList.contains('fullscreen-mode')) {
458
+ container.classList.remove('fullscreen-mode');
459
+ container.style.cssText = '';
460
+ if (wrapper) {
461
+ wrapper.style.height = '';
462
+ wrapper.style.maxHeight = '';
463
+ }
464
+ } else {
465
+ container.classList.add('fullscreen-mode');
466
+ container.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:999;background:white;border-radius:0;margin:0;padding:10px;box-sizing:border-box;';
467
+ if (wrapper) {
468
+ wrapper.style.height = 'calc(100vh - 60px)';
469
+ wrapper.style.maxHeight = 'calc(100vh - 60px)';
470
+ }
471
+ }
472
+ }
473
+
474
+ function showNodeDetail(text, node) {
475
+ const modal = document.getElementById('detailModal');
476
+ const title = document.getElementById('modalTitle');
477
+ const body = document.getElementById('modalBody');
478
+
479
+ // Reset history for new modal opening
480
+ modalHistory = [];
481
+
482
+ // Clean name: remove icons and extract operation name from patterns like "GraphQL: OPERATION_NAME"
483
+ let cleanName = text.replace(/[\u{1F512}\u{1F4E1}\u{270F}\u{FE0F}\u{1F504}]/gu, '').trim();
484
+ // Handle "GraphQL: OPERATION_NAME" pattern
485
+ if (cleanName.includes('GraphQL:')) {
486
+ cleanName = cleanName.replace(/^.*GraphQL:\s*/, '').trim();
487
+ }
488
+ // Handle "API: OPERATION_NAME" pattern
489
+ if (cleanName.includes('API:')) {
490
+ cleanName = cleanName.replace(/^.*API:\s*/, '').trim();
491
+ }
492
+ // Remove any remaining prefixes like "Query:", "Mutation:"
493
+ cleanName = cleanName.replace(/^(Query|Mutation|Fragment):\s*/i, '').trim();
494
+
495
+ const op = window.findGraphQLOp?.(cleanName);
496
+
497
+ let titleText, html;
498
+
499
+ if (op) {
500
+ titleText = op.name;
501
+ html = \`<div class="detail-section">
502
+ <h4>Type</h4>
503
+ <p><span class="detail-badge \${op.type}">\${op.type.toUpperCase()}</span></p>
504
+ </div>\`;
505
+
506
+ if (op.returnType) {
507
+ html += \`<div class="detail-section"><h4>Return</h4><p><code>\${op.returnType}</code></p></div>\`;
508
+ }
509
+
510
+ if (op.fields?.length) {
511
+ // Show full GraphQL operation structure
512
+ const opKeyword = op.type === 'mutation' ? 'mutation' : (op.type === 'fragment' ? 'fragment' : 'query');
513
+ const varStr = op.variables?.length ? '(' + op.variables.map(v => '$' + v.name + ': ' + v.type).join(', ') + ')' : '';
514
+ const fragmentOn = op.type === 'fragment' && op.returnType ? ' on ' + op.returnType : '';
515
+
516
+ let gqlCode = opKeyword + ' ' + op.name + varStr + fragmentOn + ' {\\n';
517
+ gqlCode += formatGqlFields(op.fields, 1);
518
+ gqlCode += '\\n}';
519
+
520
+ html += '<div class="detail-section"><h4>GraphQL</h4><pre style="background:#0f172a;color:#e2e8f0;padding:12px;border-radius:6px;font-size:12px;overflow-x:auto;white-space:pre;">' + gqlCode + '</pre></div>';
521
+ }
522
+
523
+ if (op.usedIn?.length) {
524
+ html += '<div class="detail-section"><h4>Used In</h4><div style="max-height:100px;overflow-y:auto">';
525
+ op.usedIn.forEach(f => { html += \`<p style="font-size:12px;color:#666;margin:2px 0">\${f}</p>\`; });
526
+ html += '</div></div>';
527
+ }
528
+ } else {
529
+ // Try partial matching for operations
530
+ let partialMatch = null;
531
+ if (window.graphqlOps && cleanName) {
532
+ const searchTerm = cleanName.toLowerCase().replace(/_/g, '');
533
+ partialMatch = window.graphqlOps.find(o => {
534
+ const opName = o.name.toLowerCase().replace(/_/g, '');
535
+ return opName.includes(searchTerm) || searchTerm.includes(opName);
536
+ });
537
+ }
538
+
539
+ if (partialMatch) {
540
+ titleText = partialMatch.name;
541
+ html = \`<div class="detail-section">
542
+ <h4>Type</h4>
543
+ <p><span class="detail-badge \${partialMatch.type}">\${partialMatch.type.toUpperCase()}</span></p>
544
+ </div>\`;
545
+
546
+ if (partialMatch.returnType) {
547
+ html += \`<div class="detail-section"><h4>Return</h4><p><code>\${partialMatch.returnType}</code></p></div>\`;
548
+ }
549
+
550
+ if (partialMatch.fields?.length) {
551
+ const opKeyword = partialMatch.type === 'mutation' ? 'mutation' : (partialMatch.type === 'fragment' ? 'fragment' : 'query');
552
+ const varStr = partialMatch.variables?.length ? '(' + partialMatch.variables.map(v => '$' + v.name + ': ' + v.type).join(', ') + ')' : '';
553
+ const fragmentOn = partialMatch.type === 'fragment' && partialMatch.returnType ? ' on ' + partialMatch.returnType : '';
554
+
555
+ let gqlCode = opKeyword + ' ' + partialMatch.name + varStr + fragmentOn + ' {\\n';
556
+ gqlCode += formatGqlFields(partialMatch.fields, 1);
557
+ gqlCode += '\\n}';
558
+
559
+ html += '<div class="detail-section"><h4>GraphQL</h4><pre style="background:#0f172a;color:#e2e8f0;padding:12px;border-radius:6px;font-size:12px;overflow-x:auto;white-space:pre;">' + gqlCode + '</pre></div>';
560
+ }
561
+
562
+ if (partialMatch.usedIn?.length) {
563
+ html += '<div class="detail-section"><h4>Used In</h4><div style="max-height:100px;overflow-y:auto">';
564
+ partialMatch.usedIn.forEach(f => { html += \`<p style="font-size:12px;color:#666;margin:2px 0">\${f}</p>\`; });
565
+ html += '</div></div>';
566
+ }
567
+ } else {
568
+ const info = parseNodeInfo(text);
569
+ titleText = cleanName || info.name || text;
570
+ html = \`
571
+ <div class="detail-section">
572
+ <h4>Type</h4>
573
+ <p><span class="detail-badge \${info.type}">\${getTypeBadge(info.type)}</span></p>
574
+ </div>
575
+ <div class="detail-section">
576
+ <h4>Operation Name</h4>
577
+ <p><code>\${cleanName}</code></p>
578
+ </div>
579
+ <div class="detail-section" style="color:#666;font-size:12px">
580
+ <p>This operation is referenced in the diagram but detailed information is not available in the parsed data.</p>
581
+ </div>
582
+ \`;
583
+ }
584
+ }
585
+
586
+ pushModalHistory(titleText, html);
587
+ title.textContent = titleText;
588
+ body.innerHTML = html;
589
+ updateBackButton();
590
+ modal.classList.add('open');
591
+ }
592
+
593
+ function formatGqlFields(fields, indent) {
594
+ if (!fields?.length) return '';
595
+ const lines = [];
596
+ for (const f of fields) {
597
+ const prefix = ' '.repeat(indent);
598
+ if (f.fields?.length) {
599
+ lines.push(prefix + f.name + ' {');
600
+ lines.push(formatGqlFields(f.fields, indent + 1));
601
+ lines.push(prefix + '}');
602
+ } else {
603
+ lines.push(prefix + f.name);
604
+ }
605
+ }
606
+ return lines.join('\\n');
607
+ }
608
+
609
+ function parseNodeInfo(text) {
610
+ const info = { type: 'unknown', name: text };
611
+
612
+ // Detect type from text patterns
613
+ if (text.includes('Query') || text.includes('QUERY') || text.toLowerCase().includes('usequery')) {
614
+ info.type = 'query';
615
+ info.operation = text.replace(/^use/, '').replace(/Query$/, '');
616
+ } else if (text.includes('Mutation') || text.includes('MUTATION') || text.toLowerCase().includes('usemutation')) {
617
+ info.type = 'mutation';
618
+ info.operation = text.replace(/^use/, '').replace(/Mutation$/, '');
619
+ } else if (text.includes('Context') || text.includes('Provider')) {
620
+ info.type = 'context';
621
+ info.context = text;
622
+ } else if (text.includes('Fragment') || text.includes('FRAGMENT')) {
623
+ info.type = 'fragment';
624
+ }
625
+
626
+ // Extract name from common patterns
627
+ const nameMatch = text.match(/^([A-Z][a-zA-Z]+)/);
628
+ if (nameMatch) {
629
+ info.name = nameMatch[1];
630
+ }
631
+
632
+ return info;
633
+ }
634
+
635
+ function getTypeBadge(type) {
636
+ const badges = {
637
+ query: '[QUERY]',
638
+ mutation: '[MUTATION]',
639
+ context: '[CONTEXT]',
640
+ fragment: '[FRAGMENT]',
641
+ unknown: '[COMPONENT]'
642
+ };
643
+ return badges[type] || badges.unknown;
644
+ }
645
+
646
+ function closeModal() {
647
+ document.getElementById('detailModal').classList.remove('open');
648
+ modalHistory = [];
649
+ updateBackButton();
650
+ }
651
+
652
+ // Close modal on backdrop click - go back if history exists
653
+ document.getElementById('detailModal')?.addEventListener('click', (e) => {
654
+ if (e.target.id === 'detailModal') {
655
+ if (modalHistory.length > 1) {
656
+ modalBack();
657
+ } else {
658
+ closeModal();
659
+ }
660
+ }
661
+ });
662
+
663
+ // Make GraphQL operations clickable
664
+ document.addEventListener('DOMContentLoaded', () => {
665
+ // Find h4 elements with Query/Mutation names (code blocks following h4)
666
+ document.querySelectorAll('h4 code, h3 + p + h4 code').forEach(el => {
667
+ const text = el.textContent || '';
668
+ if (text && !text.includes(' ')) {
669
+ el.style.cursor = 'pointer';
670
+ el.style.textDecoration = 'underline';
671
+ el.style.textDecorationStyle = 'dotted';
672
+ el.addEventListener('click', () => showGraphQLDetail(text, el));
673
+ }
674
+ });
675
+
676
+ // Also make inline code in tables clickable if it looks like an operation name
677
+ document.querySelectorAll('td code').forEach(el => {
678
+ const text = el.textContent || '';
679
+ if (text && /^[A-Z][a-zA-Z]+$/.test(text)) {
680
+ el.style.cursor = 'pointer';
681
+ el.addEventListener('click', () => showGraphQLDetail(text, el));
682
+ }
683
+ });
684
+
685
+ // Make gql-op spans clickable
686
+ document.querySelectorAll('.gql-op').forEach(el => {
687
+ el.addEventListener('click', (e) => {
688
+ e.stopPropagation();
689
+ const opName = el.dataset.op || el.textContent?.replace(/^[QM]:\\s*/, '') || '';
690
+ if (opName) showGraphQLDetail(opName, el);
691
+ });
692
+ });
693
+
694
+ // Make gql-ref (component references) clickable
695
+ document.querySelectorAll('.gql-ref').forEach(el => {
696
+ el.addEventListener('click', (e) => {
697
+ e.stopPropagation();
698
+ const refName = el.dataset.ref || '';
699
+ const queriesData = el.dataset.queries;
700
+ const mutationsData = el.dataset.mutations;
701
+
702
+ if (queriesData || mutationsData) {
703
+ // Use stored data for accurate display
704
+ const queries = queriesData ? JSON.parse(queriesData) : [];
705
+ const mutations = mutationsData ? JSON.parse(mutationsData) : [];
706
+ showComponentOps(refName, queries, mutations);
707
+ } else if (refName) {
708
+ showComponentDetail(refName);
709
+ }
710
+ });
711
+ });
712
+
713
+ // Make gql-more (show more) clickable
714
+ document.querySelectorAll('.gql-more').forEach(el => {
715
+ el.addEventListener('click', (e) => {
716
+ e.stopPropagation();
717
+ const pagePath = el.dataset.page;
718
+ const type = el.dataset.type;
719
+ if (pagePath) showAllOperations(pagePath, type);
720
+ });
721
+ });
722
+ });
723
+
724
+ function showGraphQLDetail(name, el) {
725
+ const modal = document.getElementById('detailModal');
726
+ const title = document.getElementById('modalTitle');
727
+ const body = document.getElementById('modalBody');
728
+
729
+ // Reset history if modal is not open (first level)
730
+ if (!modal.classList.contains('open')) {
731
+ modalHistory = [];
732
+ }
733
+
734
+ // Use enhanced lookup function
735
+ const op = window.findGraphQLOp?.(name);
736
+
737
+ let titleText, html;
738
+
739
+ if (op) {
740
+ titleText = op.name;
741
+ html = \`<div class="detail-section">
742
+ <h4>Type</h4>
743
+ <p><span class="detail-badge \${op.type}">\${op.type.toUpperCase()}</span></p>
744
+ </div>\`;
745
+
746
+ if (op.returnType) {
747
+ html += \`<div class="detail-section"><h4>Return Type</h4><p><code>\${op.returnType}</code></p></div>\`;
748
+ }
749
+
750
+ if (op.variables?.length) {
751
+ html += '<div class="detail-section"><h4>Variables</h4><div style="background:#f1f5f9;padding:10px;border-radius:6px">';
752
+ op.variables.forEach(v => {
753
+ html += \`<div style="margin:4px 0"><code style="color:#0369a1">\${v.name}</code>: <code>\${v.type}</code></div>\`;
754
+ });
755
+ html += '</div></div>';
756
+ }
757
+
758
+ if (op.fields?.length) {
759
+ // Show full GraphQL operation structure
760
+ const opKeyword = op.type === 'mutation' ? 'mutation' : (op.type === 'fragment' ? 'fragment' : 'query');
761
+ const varStr = op.variables?.length ? '(' + op.variables.map(v => '$' + v.name + ': ' + v.type).join(', ') + ')' : '';
762
+ const fragmentOn = op.type === 'fragment' && op.returnType ? ' on ' + op.returnType : '';
763
+
764
+ let gqlCode = opKeyword + ' ' + op.name + varStr + fragmentOn + ' {\\n';
765
+ gqlCode += formatGqlFieldsStatic(op.fields, 1);
766
+ gqlCode += '\\n}';
767
+
768
+ html += '<div class="detail-section"><h4>GraphQL</h4><pre style="background:#0f172a;color:#e2e8f0;padding:12px;border-radius:6px;font-size:12px;overflow-x:auto;white-space:pre;">' + gqlCode + '</pre></div>';
769
+ }
770
+
771
+ if (op.usedIn?.length) {
772
+ html += '<div class="detail-section"><h4>Used In</h4><div style="font-size:12px;color:#666;max-height:100px;overflow-y:auto">';
773
+ op.usedIn.forEach(f => { html += \`<div style="margin:2px 0">\${f}</div>\`; });
774
+ html += '</div></div>';
775
+ }
776
+ } else {
777
+ // Fallback for unknown operations
778
+ let type = 'operation';
779
+ if (name.toLowerCase().includes('query') || name.endsWith('Query')) type = 'query';
780
+ else if (name.toLowerCase().includes('mutation') || name.endsWith('Mutation')) type = 'mutation';
781
+
782
+ titleText = name;
783
+ html = \`
784
+ <div class="detail-section">
785
+ <h4>Type</h4>
786
+ <p><span class="detail-badge \${type}">\${type.toUpperCase()}</span></p>
787
+ </div>
788
+ <div class="detail-section">
789
+ <h4>Operation Name</h4>
790
+ <p><code>\${name}</code></p>
791
+ </div>
792
+ <div class="detail-section" style="color:#666;font-size:13px">
793
+ <p>Detailed field information not available for this operation.</p>
794
+ </div>
795
+ \`;
796
+ }
797
+
798
+ pushModalHistory(titleText, html);
799
+ title.textContent = titleText;
800
+ body.innerHTML = html;
801
+ updateBackButton();
802
+ modal.classList.add('open');
803
+ }
804
+
805
+ // Modal history for back navigation (moved earlier in the code)
806
+ // let modalHistory = [];
807
+
808
+ function pushModalHistory(title, html) {
809
+ modalHistory.push({ title, html });
810
+ }
811
+
812
+ function modalBack() {
813
+ if (modalHistory.length > 1) {
814
+ modalHistory.pop(); // Remove current
815
+ const prev = modalHistory[modalHistory.length - 1];
816
+ document.getElementById('modalTitle').textContent = prev.title;
817
+ document.getElementById('modalBody').innerHTML = prev.html;
818
+ updateBackButton();
819
+ }
820
+ }
821
+
822
+ function updateBackButton() {
823
+ const backBtn = document.getElementById('modalBackBtn');
824
+ if (backBtn) {
825
+ backBtn.style.display = modalHistory.length > 1 ? 'inline-block' : 'none';
826
+ }
827
+ }
828
+
829
+ function renderOpsSection(type, ops, initialCount = 8) {
830
+ if (ops.length === 0) return '';
831
+
832
+ const typeClass = type === 'Mutations' ? 'mutation' : (type === 'Fragments' ? 'fragment' : '');
833
+ const badgeStyle = type === 'Fragments' ? 'background:#e0e7ff;border-color:#a5b4fc;color:#4338ca;' : '';
834
+ const visibleOps = ops.slice(0, initialCount);
835
+ const hiddenOps = ops.slice(initialCount);
836
+ const sectionId = 'ops-' + type.toLowerCase() + '-' + Date.now();
837
+
838
+ let html = '<div class="detail-section"><h4>' + type + ' (' + ops.length + ')</h4>';
839
+ html += '<div id="' + sectionId + '" style="display:flex;flex-wrap:wrap;gap:6px">';
840
+
841
+ for (const op of visibleOps) {
842
+ html += \`<span class="gql-op \${typeClass}" style="\${badgeStyle}cursor:pointer" onclick="showGraphQLDetailWithHistory('\${op.name}')">\${op.name}</span>\`;
843
+ }
844
+
845
+ if (hiddenOps.length > 0) {
846
+ const hiddenData = JSON.stringify(hiddenOps.map(o => o.name)).replace(/"/g, '&quot;');
847
+ html += \`<span class="gql-more" onclick="expandOpsSection('\${sectionId}', \${hiddenData}, '\${typeClass}', '\${badgeStyle.replace(/'/g, "\\\\'")}')">+\${hiddenOps.length} more</span>\`;
848
+ }
849
+
850
+ html += '</div></div>';
851
+ return html;
852
+ }
853
+
854
+ window.expandOpsSection = function(sectionId, names, typeClass, badgeStyle) {
855
+ const section = document.getElementById(sectionId);
856
+ if (!section) return;
857
+
858
+ // Remove the "more" button
859
+ const moreBtn = section.querySelector('.gql-more');
860
+ if (moreBtn) moreBtn.remove();
861
+
862
+ // Add hidden items
863
+ for (const name of names) {
864
+ const span = document.createElement('span');
865
+ span.className = 'gql-op ' + typeClass;
866
+ span.style.cssText = badgeStyle + 'cursor:pointer';
867
+ span.textContent = name;
868
+ span.onclick = () => showGraphQLDetailWithHistory(name);
869
+ section.appendChild(span);
870
+ }
871
+ };
872
+
873
+ function showGraphQLDetailWithHistory(name) {
874
+ const op = window.findGraphQLOp?.(name);
875
+ if (!op) {
876
+ showGraphQLDetail(name);
877
+ return;
878
+ }
879
+
880
+ const title = op.name;
881
+ let html = \`<div class="detail-section">
882
+ <h4>Type</h4>
883
+ <p><span class="detail-badge \${op.type}">\${op.type.toUpperCase()}</span></p>
884
+ </div>\`;
885
+
886
+ if (op.returnType) {
887
+ html += \`<div class="detail-section"><h4>Return Type</h4><p><code>\${op.returnType}</code></p></div>\`;
888
+ }
889
+
890
+ if (op.variables?.length) {
891
+ html += '<div class="detail-section"><h4>Variables</h4><div style="background:#f1f5f9;padding:10px;border-radius:6px">';
892
+ op.variables.forEach(v => {
893
+ html += \`<div style="margin:4px 0"><code style="color:#0369a1">\${v.name}</code>: <code>\${v.type}</code></div>\`;
894
+ });
895
+ html += '</div></div>';
896
+ }
897
+
898
+ if (op.fields?.length) {
899
+ // Show full GraphQL operation structure
900
+ const opKeyword = op.type === 'mutation' ? 'mutation' : (op.type === 'fragment' ? 'fragment' : 'query');
901
+ const varStr = op.variables?.length ? '(' + op.variables.map(v => '$' + v.name + ': ' + v.type).join(', ') + ')' : '';
902
+ const fragmentOn = op.type === 'fragment' && op.returnType ? ' on ' + op.returnType : '';
903
+
904
+ let gqlCode = opKeyword + ' ' + op.name + varStr + fragmentOn + ' {\\n';
905
+ gqlCode += formatGqlFieldsStatic(op.fields, 1);
906
+ gqlCode += '\\n}';
907
+
908
+ html += '<div class="detail-section"><h4>GraphQL</h4><pre style="background:#0f172a;color:#e2e8f0;padding:12px;border-radius:6px;font-size:12px;overflow-x:auto;white-space:pre;">' + gqlCode + '</pre></div>';
909
+ }
910
+
911
+ if (op.usedIn?.length) {
912
+ html += '<div class="detail-section"><h4>Used In</h4><div style="font-size:12px;color:#666;max-height:100px;overflow-y:auto">';
913
+ op.usedIn.forEach(f => { html += \`<div style="margin:2px 0">\${f}</div>\`; });
914
+ html += '</div></div>';
915
+ }
916
+
917
+ pushModalHistory(title, html);
918
+ document.getElementById('modalTitle').textContent = title;
919
+ document.getElementById('modalBody').innerHTML = html;
920
+ updateBackButton();
921
+ }
922
+
923
+ function showComponentOps(componentName, queryNames, mutationNames) {
924
+ const modal = document.getElementById('detailModal');
925
+ const title = document.getElementById('modalTitle');
926
+ const body = document.getElementById('modalBody');
927
+
928
+ // Reset history
929
+ modalHistory = [];
930
+
931
+ // Find operations by exact names
932
+ const queries = [];
933
+ const mutations = [];
934
+
935
+ if (window.graphqlOps) {
936
+ for (const name of queryNames) {
937
+ const op = window.findGraphQLOp?.(name);
938
+ if (op) queries.push(op);
939
+ }
940
+ for (const name of mutationNames) {
941
+ const op = window.findGraphQLOp?.(name);
942
+ if (op) mutations.push(op);
943
+ }
944
+ }
945
+
946
+ let html = \`<div class="detail-section">
947
+ <h4>Component</h4>
948
+ <p><span class="detail-badge component">\${componentName}</span></p>
949
+ </div>\`;
950
+
951
+ html += \`<div class="detail-section">
952
+ <h4>Operations</h4>
953
+ <p style="color:#666;font-size:13px">\${queryNames.length} queries, \${mutationNames.length} mutations</p>
954
+ </div>\`;
955
+
956
+ if (queries.length > 0) {
957
+ html += renderOpsSection('Queries', queries, 5);
958
+ }
959
+
960
+ if (mutations.length > 0) {
961
+ html += renderOpsSection('Mutations', mutations, 5);
962
+ }
963
+
964
+ pushModalHistory(componentName, html);
965
+ title.textContent = componentName;
966
+ body.innerHTML = html;
967
+ updateBackButton();
968
+ modal.classList.add('open');
969
+ }
970
+
971
+ function showComponentDetail(componentName) {
972
+ const modal = document.getElementById('detailModal');
973
+ const title = document.getElementById('modalTitle');
974
+ const body = document.getElementById('modalBody');
975
+
976
+ // Reset history
977
+ modalHistory = [];
978
+
979
+ // Find related GraphQL operations
980
+ const queries = [];
981
+ const mutations = [];
982
+ const fragments = [];
983
+
984
+ if (window.graphqlOps) {
985
+ const keywords = componentName
986
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
987
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
988
+ .split(/\\s+/)
989
+ .filter(k => k.length > 3 && !['Page', 'Container', 'Wrapper', 'Form', 'Component', 'Provider'].includes(k));
990
+
991
+ const added = new Set();
992
+
993
+ for (const op of window.graphqlOps) {
994
+ if (added.has(op.name)) continue;
995
+
996
+ const matchesUsedIn = op.usedIn?.some(path => {
997
+ const pathLower = path.toLowerCase();
998
+ const compLower = componentName.toLowerCase();
999
+ return pathLower.includes('/' + compLower) ||
1000
+ pathLower.includes(compLower + '.') ||
1001
+ keywords.some(kw => pathLower.includes(kw.toLowerCase()));
1002
+ });
1003
+
1004
+ if (matchesUsedIn) {
1005
+ added.add(op.name);
1006
+ if (op.type === 'query') queries.push(op);
1007
+ else if (op.type === 'mutation') mutations.push(op);
1008
+ else if (op.type === 'fragment') fragments.push(op);
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ let html = \`<div class="detail-section">
1014
+ <h4>Type</h4>
1015
+ <p><span class="detail-badge component">COMPONENT</span></p>
1016
+ </div>\`;
1017
+
1018
+ const hasOps = queries.length > 0 || mutations.length > 0 || fragments.length > 0;
1019
+
1020
+ if (hasOps) {
1021
+ html += renderOpsSection('Queries', queries);
1022
+ html += renderOpsSection('Mutations', mutations);
1023
+ html += renderOpsSection('Fragments', fragments, 5);
1024
+ } else {
1025
+ html += \`<div class="detail-section" style="color:#666;font-size:13px">
1026
+ <p>No directly related GraphQL operations found for this component.</p>
1027
+ </div>\`;
1028
+ }
1029
+
1030
+ pushModalHistory(componentName, html);
1031
+ title.textContent = componentName;
1032
+ body.innerHTML = html;
1033
+ updateBackButton();
1034
+ modal.classList.add('open');
1035
+ }
1036
+
1037
+ function showAllOperations(pagePath, filterType) {
1038
+ const modal = document.getElementById('detailModal');
1039
+ const title = document.getElementById('modalTitle');
1040
+ const body = document.getElementById('modalBody');
1041
+
1042
+ // Reset history
1043
+ modalHistory = [];
1044
+
1045
+ // Find all operations for this page
1046
+ const queries = [];
1047
+ const mutations = [];
1048
+ const added = new Set();
1049
+
1050
+ if (window.graphqlOps) {
1051
+ const pathKeywords = pagePath.split('/').filter(s => s && s.length > 2 && !s.startsWith(':') && !s.startsWith('['));
1052
+
1053
+ for (const op of window.graphqlOps) {
1054
+ if (added.has(op.name)) continue;
1055
+
1056
+ const matchesType = !filterType ||
1057
+ (filterType === 'query' ? op.type === 'query' : op.type === 'mutation');
1058
+
1059
+ const matchesPath = op.usedIn?.some(path =>
1060
+ pathKeywords.some(kw => path.toLowerCase().includes(kw.toLowerCase()))
1061
+ );
1062
+
1063
+ if (matchesType && matchesPath) {
1064
+ added.add(op.name);
1065
+ if (op.type === 'query') queries.push(op);
1066
+ else if (op.type === 'mutation') mutations.push(op);
1067
+ }
1068
+ }
1069
+ }
1070
+
1071
+ const titleText = \`\${pagePath} - \${filterType ? (filterType === 'query' ? 'Queries' : 'Mutations') : 'All Operations'}\`;
1072
+
1073
+ let html = '';
1074
+
1075
+ if (queries.length > 0 && (!filterType || filterType === 'query')) {
1076
+ html += renderOpsSection('Queries', queries);
1077
+ }
1078
+
1079
+ if (mutations.length > 0 && (!filterType || filterType === 'mutation')) {
1080
+ html += renderOpsSection('Mutations', mutations);
1081
+ }
1082
+
1083
+ if (queries.length === 0 && mutations.length === 0) {
1084
+ html = '<div class="detail-section"><p style="color:#666">No operations found for this page.</p></div>';
1085
+ }
1086
+
1087
+ pushModalHistory(titleText, html);
1088
+ title.textContent = titleText;
1089
+ body.innerHTML = html;
1090
+ updateBackButton();
1091
+ modal.classList.add('open');
1092
+ }
1093
+
1094
+ function formatGqlFieldsStatic(fields, indent) {
1095
+ if (!fields?.length) return '';
1096
+ const lines = [];
1097
+ for (const f of fields) {
1098
+ const prefix = ' '.repeat(indent);
1099
+ if (f.fields?.length) {
1100
+ lines.push(prefix + f.name + ' {');
1101
+ lines.push(formatGqlFieldsStatic(f.fields, indent + 1));
1102
+ lines.push(prefix + '}');
1103
+ } else {
1104
+ lines.push(prefix + f.name);
1105
+ }
1106
+ }
1107
+ return lines.join('\\n');
1108
+ }
1109
+
1110
+ // Socket.IO for live reload
1111
+ const socket = io();
1112
+ socket.on('reload', () => {
1113
+ window.location.reload();
1114
+ });
1115
+
1116
+ // Regenerate function
1117
+ async function regenerate() {
1118
+ try {
1119
+ const btn = document.querySelector('.regenerate-btn');
1120
+ btn.textContent = '⏳ 生成中...';
1121
+ btn.disabled = true;
1122
+
1123
+ const res = await fetch('/api/regenerate', { method: 'POST' });
1124
+ const data = await res.json();
1125
+
1126
+ if (data.success) {
1127
+ window.location.reload();
1128
+ } else {
1129
+ alert('生成に失敗しました: ' + data.error);
1130
+ }
1131
+ } catch (e) {
1132
+ alert('エラー: ' + e.message);
1133
+ } finally {
1134
+ const btn = document.querySelector('.regenerate-btn');
1135
+ btn.textContent = '🔄 再生成';
1136
+ btn.disabled = false;
1137
+ }
1138
+ }
1139
+ </script>
1140
+ </body>
1141
+ </html>`;
1142
+ }
1143
+ async start(openBrowser = true) {
1144
+ // Detect environments first
1145
+ const rootPath = this.config.repositories[0]?.path || process.cwd();
1146
+ console.log('🔍 Detecting project environments...');
1147
+ this.envResult = await detectEnvironments(rootPath);
1148
+ if (this.envResult.environments.length > 0) {
1149
+ console.log(` Found: ${this.envResult.environments.map((e) => e.type).join(', ')}`);
1150
+ for (const env of this.envResult.environments) {
1151
+ if (env.features.length > 0) {
1152
+ console.log(` ${env.type} features: ${env.features.join(', ')}`);
1153
+ }
1154
+ }
1155
+ }
1156
+ // Generate initial documentation for frontend
1157
+ console.log('\n📚 Generating documentation...');
1158
+ this.currentReport = await this.engine.generate();
1159
+ // If Rails is detected, also analyze Rails
1160
+ if (this.envResult.hasRails) {
1161
+ console.log('\n🛤️ Analyzing Rails application...');
1162
+ try {
1163
+ this.railsAnalysis = await analyzeRailsApp(rootPath);
1164
+ console.log(` ✅ Rails analysis complete`);
1165
+ }
1166
+ catch (error) {
1167
+ console.error(` ⚠️ Rails analysis failed:`, error.message);
1168
+ }
1169
+ }
1170
+ // Start server
1171
+ this.server.listen(this.port, () => {
1172
+ console.log(`\n🌐 Documentation server running at http://localhost:${this.port}`);
1173
+ if (this.envResult?.hasRails && this.envResult?.hasNextjs) {
1174
+ console.log(' 📊 Multiple environments detected - use tabs to switch views');
1175
+ }
1176
+ console.log(' Press Ctrl+C to stop\n');
1177
+ });
1178
+ // Open browser
1179
+ if (openBrowser) {
1180
+ const open = (await import('open')).default;
1181
+ await open(`http://localhost:${this.port}`);
1182
+ }
1183
+ // Watch for changes
1184
+ if (this.config.watch.enabled) {
1185
+ this.watchForChanges();
1186
+ }
1187
+ }
1188
+ async regenerate() {
1189
+ console.log('\n🔄 Regenerating documentation...');
1190
+ this.currentReport = await this.engine.generate();
1191
+ // Re-analyze Rails if detected
1192
+ if (this.envResult?.hasRails) {
1193
+ const rootPath = this.config.repositories[0]?.path || process.cwd();
1194
+ try {
1195
+ this.railsAnalysis = await analyzeRailsApp(rootPath);
1196
+ }
1197
+ catch (error) {
1198
+ console.error(`⚠️ Rails re-analysis failed:`, error.message);
1199
+ }
1200
+ }
1201
+ this.io.emit('reload');
1202
+ console.log('✅ Documentation regenerated');
1203
+ }
1204
+ async watchForChanges() {
1205
+ const watchDirs = this.config.repositories.map((r) => r.path);
1206
+ let timeout = null;
1207
+ for (const dir of watchDirs) {
1208
+ try {
1209
+ const watcher = fs.watch(dir, { recursive: true });
1210
+ (async () => {
1211
+ for await (const event of watcher) {
1212
+ if (event.filename &&
1213
+ (event.filename.endsWith('.ts') || event.filename.endsWith('.tsx'))) {
1214
+ if (timeout)
1215
+ clearTimeout(timeout);
1216
+ timeout = setTimeout(async () => {
1217
+ await this.regenerate();
1218
+ }, this.config.watch.debounce);
1219
+ }
1220
+ }
1221
+ })();
1222
+ }
1223
+ catch (error) {
1224
+ console.warn(`Warning: Could not watch directory ${dir}:`, error.message);
1225
+ }
1226
+ }
1227
+ }
1228
+ stop() {
1229
+ this.server.close();
1230
+ console.log('\n👋 Server stopped');
1231
+ }
1232
+ }
1233
+ //# sourceMappingURL=doc-server.js.map