@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,909 @@
1
+ /**
2
+ * Rails Map Generator
3
+ * Rails分析結果をインタラクティブなHTMLページとして生成する
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { analyzeRailsApp, } from '../analyzers/rails/index.js';
8
+ export class RailsMapGenerator {
9
+ rootPath;
10
+ result = null;
11
+ constructor(rootPath) {
12
+ this.rootPath = rootPath;
13
+ }
14
+ async generate(options = {}) {
15
+ if (!this.rootPath)
16
+ throw new Error('Root path required for analysis');
17
+ const { title = 'Rails Application Map' } = options;
18
+ // Run analysis
19
+ this.result = await analyzeRailsApp(this.rootPath);
20
+ // Generate HTML
21
+ const html = this.generateHTML(title);
22
+ // Save if output path specified
23
+ if (options.outputPath) {
24
+ fs.writeFileSync(options.outputPath, html);
25
+ console.log(`\n📄 Generated: ${options.outputPath}`);
26
+ }
27
+ return html;
28
+ }
29
+ // Generate from existing analysis result (for doc-server)
30
+ generateFromResult(analysisResult, title = 'Rails Application Map') {
31
+ this.result = analysisResult;
32
+ return this.generateHTML(title);
33
+ }
34
+ generateHTML(title) {
35
+ if (!this.result)
36
+ throw new Error('Analysis not run');
37
+ const { routes, controllers, models, grpc, summary } = this.result;
38
+ return `<!DOCTYPE html>
39
+ <html lang="en">
40
+ <head>
41
+ <meta charset="UTF-8">
42
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
43
+ <title>${title}</title>
44
+ <link rel="stylesheet" href="/rails-map.css">
45
+ </head>
46
+ <body>
47
+ <header>
48
+ <h1>🛤️ ${title}</h1>
49
+ <nav class="header-nav">
50
+ <a href="/page-map" class="nav-link">Page Map</a>
51
+ <a href="/rails-map" class="nav-link active">Rails Map</a>
52
+ <a href="/docs" class="nav-link">Docs</a>
53
+ </nav>
54
+ <div class="stats-bar">
55
+ <div class="stat active" data-view="routes">
56
+ <div>
57
+ <div class="stat-value">${summary.totalRoutes.toLocaleString()}</div>
58
+ <div class="stat-label">Routes</div>
59
+ </div>
60
+ </div>
61
+ <div class="stat" data-view="controllers">
62
+ <div>
63
+ <div class="stat-value">${summary.totalControllers}</div>
64
+ <div class="stat-label">Controllers</div>
65
+ </div>
66
+ </div>
67
+ <div class="stat" data-view="models">
68
+ <div>
69
+ <div class="stat-value">${summary.totalModels}</div>
70
+ <div class="stat-label">Models</div>
71
+ </div>
72
+ </div>
73
+ <div class="stat" data-view="grpc">
74
+ <div>
75
+ <div class="stat-value">${summary.totalGrpcServices}</div>
76
+ <div class="stat-label">gRPC</div>
77
+ </div>
78
+ </div>
79
+ <div class="stat" data-view="diagram">
80
+ <div>
81
+ <div class="stat-value">📊</div>
82
+ <div class="stat-label">Diagram</div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </header>
87
+
88
+ <div class="container">
89
+ <aside class="sidebar">
90
+ <div class="sidebar-section">
91
+ <div class="sidebar-title">Search</div>
92
+ <input type="text" class="search-box" id="searchBox" placeholder="Search routes, controllers...">
93
+ </div>
94
+
95
+ <div class="sidebar-section namespaces" id="namespaceFilter">
96
+ <div class="sidebar-title">Namespaces (${summary.namespaces.length})</div>
97
+ <div class="namespace-list">
98
+ <div class="namespace-item active" data-namespace="all">
99
+ <span>All</span>
100
+ <span class="namespace-count">${routes.routes.length}</span>
101
+ </div>
102
+ ${this.generateNamespaceList(routes.routes)}
103
+ </div>
104
+ </div>
105
+
106
+ <div class="sidebar-section" id="methodFilter">
107
+ <div class="sidebar-title">HTTP Methods</div>
108
+ <div class="namespace-list methods-list">
109
+ ${this.generateMethodFilters(routes.routes)}
110
+ </div>
111
+ </div>
112
+ </aside>
113
+
114
+ <main class="main-panel" id="mainPanel">
115
+ ${this.generateRoutesView(routes.routes)}
116
+ </main>
117
+
118
+ <aside class="detail-panel" id="detailPanel">
119
+ <div class="empty-state">
120
+ <div class="empty-state-icon">👆</div>
121
+ <div>Select an item to view details</div>
122
+ </div>
123
+ </aside>
124
+ </div>
125
+
126
+ <script>
127
+ // Data
128
+ const routes = ${JSON.stringify(routes.routes)};
129
+ const controllers = ${JSON.stringify(controllers.controllers)};
130
+ const models = ${JSON.stringify(models.models)};
131
+ const grpcServices = ${JSON.stringify(grpc.services)};
132
+
133
+ // State
134
+ let currentView = 'routes';
135
+ let selectedNamespaces = new Set(['all']);
136
+ let selectedMethods = new Set(['all']);
137
+ let searchQuery = '';
138
+ let routesDisplayCount = 200;
139
+ let controllersDisplayCount = 50;
140
+ let modelsDisplayCount = 50;
141
+
142
+ // Filtered data cache for click handlers
143
+ let filteredControllers = [];
144
+ let filteredModels = [];
145
+
146
+ // URL State Management
147
+ function saveStateToUrl() {
148
+ const params = new URLSearchParams();
149
+ params.set('view', currentView);
150
+ if (!selectedNamespaces.has('all')) {
151
+ params.set('ns', [...selectedNamespaces].join(','));
152
+ }
153
+ if (!selectedMethods.has('all')) {
154
+ params.set('method', [...selectedMethods].join(','));
155
+ }
156
+ if (searchQuery) {
157
+ params.set('q', searchQuery);
158
+ }
159
+ const newUrl = window.location.pathname + '?' + params.toString();
160
+ window.history.replaceState({}, '', newUrl);
161
+ }
162
+
163
+ function loadStateFromUrl() {
164
+ const params = new URLSearchParams(window.location.search);
165
+
166
+ if (params.has('view')) {
167
+ currentView = params.get('view');
168
+ document.querySelectorAll('.stat').forEach(s => {
169
+ s.classList.toggle('active', s.dataset.view === currentView);
170
+ });
171
+ }
172
+
173
+ if (params.has('ns')) {
174
+ const ns = params.get('ns').split(',').filter(Boolean);
175
+ if (ns.length > 0) {
176
+ selectedNamespaces = new Set(ns);
177
+ }
178
+ }
179
+
180
+ if (params.has('method')) {
181
+ const methods = params.get('method').split(',').filter(Boolean);
182
+ if (methods.length > 0) {
183
+ selectedMethods = new Set(methods);
184
+ }
185
+ }
186
+
187
+ if (params.has('q')) {
188
+ searchQuery = params.get('q');
189
+ searchBox.value = searchQuery;
190
+ }
191
+
192
+ updateFilterUI();
193
+ }
194
+
195
+ // DOM Elements
196
+ const mainPanel = document.getElementById('mainPanel');
197
+ const detailPanel = document.getElementById('detailPanel');
198
+ const searchBox = document.getElementById('searchBox');
199
+
200
+ // Event Listeners
201
+ document.querySelectorAll('.stat').forEach(stat => {
202
+ stat.addEventListener('click', () => {
203
+ document.querySelectorAll('.stat').forEach(s => s.classList.remove('active'));
204
+ stat.classList.add('active');
205
+ currentView = stat.dataset.view;
206
+ saveStateToUrl();
207
+ renderMainPanel();
208
+ });
209
+ });
210
+
211
+ // Namespace filter (multi-select with Ctrl/Cmd)
212
+ document.querySelectorAll('.namespace-item[data-namespace]').forEach(item => {
213
+ item.addEventListener('click', (e) => {
214
+ const ns = item.dataset.namespace;
215
+ if (e.ctrlKey || e.metaKey) {
216
+ // Multi-select
217
+ if (ns === 'all') {
218
+ selectedNamespaces = new Set(['all']);
219
+ } else {
220
+ selectedNamespaces.delete('all');
221
+ if (selectedNamespaces.has(ns)) {
222
+ selectedNamespaces.delete(ns);
223
+ if (selectedNamespaces.size === 0) selectedNamespaces.add('all');
224
+ } else {
225
+ selectedNamespaces.add(ns);
226
+ }
227
+ }
228
+ } else {
229
+ // Single select
230
+ selectedNamespaces = new Set([ns]);
231
+ }
232
+ updateFilterUI();
233
+ routesDisplayCount = 200;
234
+ saveStateToUrl();
235
+ renderMainPanel();
236
+ });
237
+ });
238
+
239
+ // Method filter (multi-select with Ctrl/Cmd)
240
+ document.querySelectorAll('.namespace-item[data-method]').forEach(item => {
241
+ item.addEventListener('click', (e) => {
242
+ const method = item.dataset.method;
243
+ if (e.ctrlKey || e.metaKey) {
244
+ if (selectedMethods.has('all')) {
245
+ selectedMethods = new Set([method]);
246
+ } else if (selectedMethods.has(method)) {
247
+ selectedMethods.delete(method);
248
+ if (selectedMethods.size === 0) selectedMethods.add('all');
249
+ } else {
250
+ selectedMethods.add(method);
251
+ }
252
+ } else {
253
+ if (selectedMethods.has(method) && selectedMethods.size === 1) {
254
+ selectedMethods = new Set(['all']);
255
+ } else {
256
+ selectedMethods = new Set([method]);
257
+ }
258
+ }
259
+ updateFilterUI();
260
+ routesDisplayCount = 200;
261
+ saveStateToUrl();
262
+ renderMainPanel();
263
+ });
264
+ });
265
+
266
+ function updateFilterUI() {
267
+ document.querySelectorAll('.namespace-item[data-namespace]').forEach(item => {
268
+ item.classList.toggle('active', selectedNamespaces.has(item.dataset.namespace));
269
+ });
270
+ document.querySelectorAll('.namespace-item[data-method]').forEach(item => {
271
+ // Only highlight selected methods, not all when 'all' is selected
272
+ item.classList.toggle('active', selectedMethods.has(item.dataset.method));
273
+ });
274
+ }
275
+
276
+ searchBox.addEventListener('input', (e) => {
277
+ searchQuery = e.target.value.toLowerCase();
278
+ saveStateToUrl();
279
+ renderMainPanel();
280
+ });
281
+
282
+ // Render Functions
283
+ function renderMainPanel() {
284
+ // Disable filters for models/diagram tabs
285
+ const namespaceFilter = document.getElementById('namespaceFilter');
286
+ const methodFilter = document.getElementById('methodFilter');
287
+ const filtersDisabled = currentView === 'models' || currentView === 'diagram';
288
+
289
+ if (namespaceFilter) {
290
+ namespaceFilter.style.opacity = filtersDisabled ? '0.4' : '1';
291
+ namespaceFilter.style.pointerEvents = filtersDisabled ? 'none' : 'auto';
292
+ }
293
+ if (methodFilter) {
294
+ methodFilter.style.opacity = filtersDisabled ? '0.4' : '1';
295
+ methodFilter.style.pointerEvents = filtersDisabled ? 'none' : 'auto';
296
+ }
297
+
298
+ switch (currentView) {
299
+ case 'routes':
300
+ mainPanel.innerHTML = renderRoutesView();
301
+ break;
302
+ case 'controllers':
303
+ mainPanel.innerHTML = renderControllersView();
304
+ break;
305
+ case 'models':
306
+ mainPanel.innerHTML = renderModelsView();
307
+ break;
308
+ case 'grpc':
309
+ mainPanel.innerHTML = renderGrpcView();
310
+ break;
311
+ case 'diagram':
312
+ mainPanel.innerHTML = renderDiagramView();
313
+ setTimeout(loadMermaid, 100);
314
+ break;
315
+ }
316
+ attachEventListeners();
317
+ }
318
+
319
+ function filterRoutes() {
320
+ return routes.filter(route => {
321
+ // Namespace filter (multi-select)
322
+ if (!selectedNamespaces.has('all')) {
323
+ const routeNs = route.namespace || '';
324
+ if (!selectedNamespaces.has(routeNs)) return false;
325
+ }
326
+ // Method filter (multi-select)
327
+ if (!selectedMethods.has('all') && !selectedMethods.has(route.method)) return false;
328
+ // Search filter
329
+ if (searchQuery) {
330
+ const searchStr = (route.path + route.controller + route.action + (route.namespace || '')).toLowerCase();
331
+ if (!searchStr.includes(searchQuery)) return false;
332
+ }
333
+ return true;
334
+ });
335
+ }
336
+
337
+ function renderRoutesView() {
338
+ const filtered = filterRoutes();
339
+ const displayed = filtered.slice(0, routesDisplayCount);
340
+ const hasMore = filtered.length > routesDisplayCount;
341
+
342
+ return \`
343
+ <div class="panel-header">
344
+ <div class="panel-title">Routes <span class="panel-count">(\${Math.min(routesDisplayCount, filtered.length)} / \${filtered.length})</span></div>
345
+ </div>
346
+ <table class="routes-table">
347
+ <thead>
348
+ <tr>
349
+ <th>Method</th>
350
+ <th>Path</th>
351
+ <th>Controller#Action</th>
352
+ </tr>
353
+ </thead>
354
+ <tbody>
355
+ \${displayed.map((route, idx) => \`
356
+ <tr data-type="route" data-index="\${idx}">
357
+ <td><span class="method-badge method-\${route.method}">\${route.method}</span></td>
358
+ <td class="path-text">\${highlightParams(route.path)}</td>
359
+ <td class="controller-text">\${route.controller}#\${route.action}</td>
360
+ </tr>
361
+ \`).join('')}
362
+ </tbody>
363
+ </table>
364
+ \${hasMore ? \`
365
+ <div class="show-more-container">
366
+ <button class="show-more-btn" onclick="loadMoreRoutes()">Show More (+200)</button>
367
+ <span class="show-more-count">\${routesDisplayCount} / \${filtered.length}</span>
368
+ </div>
369
+ \` : ''}
370
+ \`;
371
+ }
372
+
373
+ window.loadMoreRoutes = function() {
374
+ routesDisplayCount += 200;
375
+ renderMainPanel();
376
+ };
377
+
378
+ function renderControllersView() {
379
+ filteredControllers = controllers;
380
+ if (searchQuery) {
381
+ filteredControllers = controllers.filter(c =>
382
+ c.className.toLowerCase().includes(searchQuery) ||
383
+ c.actions.some(a => a.name.toLowerCase().includes(searchQuery))
384
+ );
385
+ }
386
+ if (!selectedNamespaces.has('all')) {
387
+ filteredControllers = filteredControllers.filter(c => selectedNamespaces.has(c.namespace || ''));
388
+ }
389
+ const displayed = filteredControllers.slice(0, controllersDisplayCount);
390
+ const hasMore = filteredControllers.length > controllersDisplayCount;
391
+
392
+ return \`
393
+ <div class="panel-header">
394
+ <div class="panel-title">Controllers <span class="panel-count">(\${Math.min(controllersDisplayCount, filteredControllers.length)} / \${filteredControllers.length})</span></div>
395
+ </div>
396
+ <div>
397
+ \${displayed.map((ctrl, idx) => \`
398
+ <div class="controller-card" data-type="controller" data-index="\${idx}">
399
+ <div class="controller-header">
400
+ <div>
401
+ <div class="controller-name">\${ctrl.className}</div>
402
+ <div class="controller-namespace">\${ctrl.namespace || 'root'} • \${ctrl.actions.length} actions</div>
403
+ </div>
404
+ <span>▶</span>
405
+ </div>
406
+ <div class="controller-actions">
407
+ \${ctrl.actions.map(action => \`
408
+ <div class="action-item">
409
+ <div class="action-visibility visibility-\${action.visibility}"></div>
410
+ <span>\${action.name}</span>
411
+ \${action.rendersJson ? '<span class="tag tag-blue">JSON</span>' : ''}
412
+ \${action.redirectsTo ? '<span class="tag tag-purple">Redirect</span>' : ''}
413
+ </div>
414
+ \`).join('')}
415
+ </div>
416
+ </div>
417
+ \`).join('')}
418
+ </div>
419
+ \${hasMore ? \`
420
+ <div class="show-more-container">
421
+ <button class="show-more-btn" onclick="loadMoreControllers()">Show More (+50)</button>
422
+ <span class="show-more-count">\${controllersDisplayCount} / \${filteredControllers.length}</span>
423
+ </div>
424
+ \` : ''}
425
+ \`;
426
+ }
427
+
428
+ window.loadMoreControllers = function() {
429
+ controllersDisplayCount += 50;
430
+ renderMainPanel();
431
+ };
432
+
433
+ function renderModelsView() {
434
+ filteredModels = models;
435
+ if (searchQuery) {
436
+ filteredModels = models.filter(m =>
437
+ m.className.toLowerCase().includes(searchQuery)
438
+ );
439
+ }
440
+ const displayed = filteredModels.slice(0, modelsDisplayCount);
441
+ const hasMore = filteredModels.length > modelsDisplayCount;
442
+
443
+ return \`
444
+ <div class="panel-header">
445
+ <div class="panel-title">Models <span class="panel-count">(\${Math.min(modelsDisplayCount, filteredModels.length)} / \${filteredModels.length})</span></div>
446
+ </div>
447
+ <div>
448
+ \${displayed.map((model, idx) => \`
449
+ <div class="model-card" data-type="model" data-index="\${idx}">
450
+ <div class="model-name">
451
+ \${model.className}
452
+ \${model.concerns.length > 0 ? \`<span class="tag tag-purple">\${model.concerns.length} concerns</span>\` : ''}
453
+ </div>
454
+ <div class="model-stats">
455
+ <span>\${model.associations.length} assoc</span>
456
+ <span>\${model.validations.length} valid</span>
457
+ <span>\${model.callbacks.length} callbacks</span>
458
+ </div>
459
+ </div>
460
+ \`).join('')}
461
+ </div>
462
+ \${hasMore ? \`
463
+ <div class="show-more-container">
464
+ <button class="show-more-btn" onclick="loadMoreModels()">Show More (+50)</button>
465
+ <span class="show-more-count">\${modelsDisplayCount} / \${filteredModels.length}</span>
466
+ </div>
467
+ \` : ''}
468
+ \`;
469
+ }
470
+
471
+ window.loadMoreModels = function() {
472
+ modelsDisplayCount += 50;
473
+ renderMainPanel();
474
+ };
475
+
476
+ let grpcDisplayCount = 50;
477
+ let filteredGrpc = grpcServices;
478
+
479
+ function renderGrpcView() {
480
+ filteredGrpc = grpcServices;
481
+ if (searchQuery) {
482
+ filteredGrpc = grpcServices.filter(svc =>
483
+ (svc.className && svc.className.toLowerCase().includes(searchQuery)) ||
484
+ (svc.namespace && svc.namespace.toLowerCase().includes(searchQuery)) ||
485
+ (svc.rpcs && svc.rpcs.some(rpc => rpc.name && rpc.name.toLowerCase().includes(searchQuery)))
486
+ );
487
+ }
488
+
489
+ const displayedGrpc = filteredGrpc.slice(0, grpcDisplayCount);
490
+
491
+ return \`
492
+ <div class="panel-header">
493
+ <div class="panel-title">gRPC Services <span class="panel-count">(\${Math.min(grpcDisplayCount, filteredGrpc.length)} / \${filteredGrpc.length})</span></div>
494
+ </div>
495
+ <div style="display:grid;gap:12px">
496
+ \${displayedGrpc.map((svc, idx) => \`
497
+ <div class="model-card" onclick="showGrpcDetail(\${idx})">
498
+ <div class="model-name">
499
+ 🔌 \${svc.className || 'Unknown'}
500
+ </div>
501
+ <div class="model-stats">
502
+ \${svc.namespace ? \`<span>📁 \${svc.namespace}</span>\` : ''}
503
+ <span>⚡ \${svc.rpcs ? svc.rpcs.length : 0} RPCs</span>
504
+ </div>
505
+ </div>
506
+ \`).join('')}
507
+ </div>
508
+ \${filteredGrpc.length > grpcDisplayCount ? \`
509
+ <div class="show-more-container">
510
+ <button class="show-more-btn" onclick="loadMoreGrpc()">Show More (+50)</button>
511
+ <span class="show-more-count">\${grpcDisplayCount} / \${filteredGrpc.length}</span>
512
+ </div>
513
+ \` : ''}
514
+ \`;
515
+ }
516
+
517
+ window.loadMoreGrpc = function() {
518
+ grpcDisplayCount += 50;
519
+ renderMainPanel();
520
+ };
521
+
522
+ window.showGrpcDetail = function(idx) {
523
+ const svc = filteredGrpc[idx];
524
+ if (!svc) return;
525
+
526
+ let detail = \`
527
+ <div class="detail-header">
528
+ <div class="detail-title">🔌 \${svc.className || 'gRPC Service'}</div>
529
+ <button class="close-btn" onclick="closeDetail()">×</button>
530
+ </div>
531
+ <div class="detail-content">
532
+ <div class="detail-section">
533
+ <div class="detail-section-title">Service Info</div>
534
+ <div class="detail-item"><span class="tag tag-purple">class</span>\${svc.className || 'N/A'}</div>
535
+ \${svc.namespace ? \`<div class="detail-item"><span class="tag tag-blue">namespace</span>\${svc.namespace}</div>\` : ''}
536
+ \${svc.filePath ? \`<div class="detail-item"><span class="tag tag-green">file</span><span style="word-break:break-all">\${svc.filePath}</span></div>\` : ''}
537
+ </div>
538
+
539
+ \${svc.rpcs && svc.rpcs.length > 0 ? \`
540
+ <div class="detail-section">
541
+ <div class="detail-section-title">RPCs (\${svc.rpcs.length})</div>
542
+ \${svc.rpcs.map(rpc => \`
543
+ <div class="detail-item">
544
+ <span class="tag tag-orange">rpc</span>
545
+ <span>\${rpc.name || 'unknown'}</span>
546
+ \${rpc.request ? \`<span style="margin-left:auto;font-size:11px;color:var(--text-secondary)">(\${rpc.request})</span>\` : ''}
547
+ </div>
548
+ \`).join('')}
549
+ </div>
550
+ \` : ''}
551
+ </div>
552
+ \`;
553
+
554
+ detailPanel.innerHTML = detail;
555
+ detailPanel.classList.add('open');
556
+ };
557
+
558
+ function renderDiagramView() {
559
+ const topModels = [...models]
560
+ .sort((a, b) => b.associations.length - a.associations.length)
561
+ .slice(0, 15);
562
+
563
+ const modelNames = new Set(topModels.map(m => m.name || m.className));
564
+ let mermaidCode = 'erDiagram\\n';
565
+ const addedRelations = new Set();
566
+
567
+ topModels.forEach(model => {
568
+ const modelName = (model.name || model.className).replace(/[^a-zA-Z0-9]/g, '_');
569
+ model.associations.forEach(assoc => {
570
+ let targetModel = assoc.className || capitalize(singularize(assoc.name));
571
+ targetModel = targetModel.replace(/[^a-zA-Z0-9]/g, '_');
572
+
573
+ if (modelNames.has(assoc.className) || modelNames.has(capitalize(singularize(assoc.name)))) {
574
+ const relKey = [modelName, targetModel].sort().join('-') + assoc.type;
575
+ if (!addedRelations.has(relKey)) {
576
+ addedRelations.add(relKey);
577
+ const rel =
578
+ assoc.type === 'belongs_to' ? '||--o{' :
579
+ assoc.type === 'has_one' ? '||--||' : '||--o{';
580
+ mermaidCode += \` \${modelName} \${rel} \${targetModel} : "\${assoc.type}"\\n\`;
581
+ }
582
+ }
583
+ });
584
+ });
585
+
586
+ // Ensure there's content even if no relations found
587
+ if (mermaidCode === 'erDiagram\\n') {
588
+ topModels.slice(0, 5).forEach(model => {
589
+ const modelName = (model.name || model.className).replace(/[^a-zA-Z0-9]/g, '_');
590
+ mermaidCode += \` \${modelName} {\\n string id\\n }\\n\`;
591
+ });
592
+ }
593
+
594
+ return \`
595
+ <div class="panel-header">
596
+ <div class="panel-title">Model Relationships (Top 15 by associations)</div>
597
+ </div>
598
+ <div class="mermaid-container" id="mermaid-container">
599
+ <pre class="mermaid" id="mermaid-diagram">\${mermaidCode}</pre>
600
+ </div>
601
+ \`;
602
+ }
603
+
604
+ // Load mermaid dynamically
605
+ function loadMermaid() {
606
+ const container = document.getElementById('mermaid-diagram');
607
+ if (!container) return;
608
+
609
+ if (window.mermaid) {
610
+ try {
611
+ // Re-render mermaid diagram
612
+ container.removeAttribute('data-processed');
613
+ window.mermaid.init(undefined, container);
614
+ } catch (e) {
615
+ console.error('Mermaid error:', e);
616
+ }
617
+ return;
618
+ }
619
+ const script = document.createElement('script');
620
+ script.src = 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js';
621
+ script.onload = () => {
622
+ window.mermaid.initialize({
623
+ startOnLoad: false,
624
+ theme: 'dark',
625
+ securityLevel: 'loose'
626
+ });
627
+ const diagram = document.getElementById('mermaid-diagram');
628
+ if (diagram) {
629
+ window.mermaid.init(undefined, diagram);
630
+ }
631
+ };
632
+ document.head.appendChild(script);
633
+ }
634
+
635
+ function highlightParams(path) {
636
+ return path.replace(/:([a-zA-Z_]+)/g, '<span class="param">:$1</span>');
637
+ }
638
+
639
+ function capitalize(str) {
640
+ return str.charAt(0).toUpperCase() + str.slice(1);
641
+ }
642
+
643
+ function singularize(str) {
644
+ if (str.endsWith('ies')) return str.slice(0, -3) + 'y';
645
+ if (str.endsWith('s')) return str.slice(0, -1);
646
+ return str;
647
+ }
648
+
649
+ function attachEventListeners() {
650
+ document.querySelectorAll('[data-type="route"]').forEach(row => {
651
+ row.addEventListener('click', () => {
652
+ const idx = parseInt(row.dataset.index);
653
+ const route = filterRoutes()[idx];
654
+ showRouteDetail(route);
655
+ });
656
+ });
657
+
658
+ document.querySelectorAll('[data-type="controller"]').forEach(card => {
659
+ card.addEventListener('click', (e) => {
660
+ if (e.target.closest('.controller-header')) {
661
+ card.classList.toggle('expanded');
662
+ }
663
+ const idx = parseInt(card.dataset.index);
664
+ // Use filtered array to get correct item
665
+ if (filteredControllers[idx]) {
666
+ showControllerDetail(filteredControllers[idx]);
667
+ }
668
+ });
669
+ });
670
+
671
+ document.querySelectorAll('[data-type="model"]').forEach(card => {
672
+ card.addEventListener('click', () => {
673
+ const idx = parseInt(card.dataset.index);
674
+ // Use filtered array to get correct item
675
+ if (filteredModels[idx]) {
676
+ showModelDetail(filteredModels[idx]);
677
+ }
678
+ });
679
+ });
680
+ }
681
+
682
+ function showRouteDetail(route) {
683
+ detailPanel.innerHTML = \`
684
+ <div class="detail-header">
685
+ <div class="detail-title">Route Details</div>
686
+ <button class="close-btn" onclick="clearDetail()">×</button>
687
+ </div>
688
+ <div class="detail-content">
689
+ <div class="detail-section">
690
+ <div class="detail-section-title">Method & Path</div>
691
+ <div class="detail-item">
692
+ <span class="method-badge method-\${route.method}">\${route.method}</span>
693
+ <code>\${route.path}</code>
694
+ </div>
695
+ </div>
696
+ <div class="detail-section">
697
+ <div class="detail-section-title">Controller</div>
698
+ <div class="detail-item">\${route.controller}#\${route.action}</div>
699
+ </div>
700
+ \${route.namespace ? \`
701
+ <div class="detail-section">
702
+ <div class="detail-section-title">Namespace</div>
703
+ <div class="detail-item">\${route.namespace}</div>
704
+ </div>
705
+ \` : ''}
706
+ <div class="detail-section">
707
+ <div class="detail-section-title">Line</div>
708
+ <div class="detail-item">routes.rb:\${route.line}</div>
709
+ </div>
710
+ </div>
711
+ \`;
712
+ }
713
+
714
+ function showControllerDetail(ctrl) {
715
+ detailPanel.innerHTML = \`
716
+ <div class="detail-header">
717
+ <div class="detail-title">Controller Details</div>
718
+ <button class="close-btn" onclick="clearDetail()">×</button>
719
+ </div>
720
+ <div class="detail-content">
721
+ <div class="detail-section">
722
+ <div class="detail-section-title">Class</div>
723
+ <div class="detail-item">\${ctrl.className}</div>
724
+ <div class="detail-item" style="color: var(--text-secondary)">extends \${ctrl.parentClass}</div>
725
+ </div>
726
+ \${ctrl.beforeActions.length > 0 ? \`
727
+ <div class="detail-section">
728
+ <div class="detail-section-title">Before Actions</div>
729
+ \${ctrl.beforeActions.map(f => \`
730
+ <div class="detail-item">
731
+ <span class="tag tag-orange">before</span>
732
+ \${f.name}
733
+ \${f.only ? \`<br><small style="color: var(--text-secondary)">only: \${f.only.join(', ')}</small>\` : ''}
734
+ </div>
735
+ \`).join('')}
736
+ </div>
737
+ \` : ''}
738
+ \${ctrl.concerns.length > 0 ? \`
739
+ <div class="detail-section">
740
+ <div class="detail-section-title">Concerns</div>
741
+ \${ctrl.concerns.map(c => \`<div class="detail-item"><span class="tag tag-purple">include</span>\${c}</div>\`).join('')}
742
+ </div>
743
+ \` : ''}
744
+ <div class="detail-section">
745
+ <div class="detail-section-title">Actions (\${ctrl.actions.length})</div>
746
+ \${ctrl.actions.slice(0, 20).map(a => \`
747
+ <div class="detail-item">
748
+ <span class="tag tag-\${a.visibility === 'public' ? 'green' : a.visibility === 'private' ? 'pink' : 'orange'}">\${a.visibility}</span>
749
+ \${a.name}
750
+ </div>
751
+ \`).join('')}
752
+ \${ctrl.actions.length > 20 ? '<div class="detail-item" style="color: var(--text-secondary)">...</div>' : ''}
753
+ </div>
754
+ </div>
755
+ \`;
756
+ }
757
+
758
+ function showModelDetail(model) {
759
+ detailPanel.innerHTML = \`
760
+ <div class="detail-header">
761
+ <div class="detail-title">Model Details</div>
762
+ <button class="close-btn" onclick="clearDetail()">×</button>
763
+ </div>
764
+ <div class="detail-content">
765
+ <div class="detail-section">
766
+ <div class="detail-section-title">Class</div>
767
+ <div class="detail-item">\${model.className}</div>
768
+ <div class="detail-item" style="color: var(--text-secondary)">extends \${model.parentClass}</div>
769
+ </div>
770
+ \${model.associations.length > 0 ? \`
771
+ <div class="detail-section">
772
+ <div class="detail-section-title">Associations (\${model.associations.length})</div>
773
+ \${model.associations.slice(0, 15).map(a => \`
774
+ <div class="detail-item">
775
+ <span class="tag tag-blue">\${a.type}</span>
776
+ :\${a.name}
777
+ \${a.through ? \`<small style="color: var(--text-secondary)"> through: \${a.through}</small>\` : ''}
778
+ \${a.polymorphic ? '<span class="tag tag-purple">poly</span>' : ''}
779
+ </div>
780
+ \`).join('')}
781
+ \${model.associations.length > 15 ? '<div class="detail-item" style="color: var(--text-secondary)">...</div>' : ''}
782
+ </div>
783
+ \` : ''}
784
+ \${model.validations.length > 0 ? \`
785
+ <div class="detail-section">
786
+ <div class="detail-section-title">Validations (\${model.validations.length})</div>
787
+ \${model.validations.slice(0, 10).map(v => \`
788
+ <div class="detail-item">
789
+ <span class="tag tag-green">\${v.type}</span>
790
+ \${v.attributes.join(', ')}
791
+ </div>
792
+ \`).join('')}
793
+ </div>
794
+ \` : ''}
795
+ \${model.scopes.length > 0 ? \`
796
+ <div class="detail-section">
797
+ <div class="detail-section-title">Scopes (\${model.scopes.length})</div>
798
+ \${model.scopes.map(s => \`<div class="detail-item"><span class="tag tag-orange">scope</span>\${s.name}</div>\`).join('')}
799
+ </div>
800
+ \` : ''}
801
+ \${model.enums.length > 0 ? \`
802
+ <div class="detail-section">
803
+ <div class="detail-section-title">Enums</div>
804
+ \${model.enums.map(e => \`
805
+ <div class="detail-item">
806
+ <span class="tag tag-pink">enum</span>
807
+ \${e.name}: \${e.values.slice(0, 5).join(', ')}\${e.values.length > 5 ? '...' : ''}
808
+ </div>
809
+ \`).join('')}
810
+ </div>
811
+ \` : ''}
812
+ </div>
813
+ \`;
814
+ }
815
+
816
+ function clearDetail() {
817
+ detailPanel.innerHTML = \`
818
+ <div class="empty-state">
819
+ <div class="empty-state-icon">👆</div>
820
+ <div>Select an item to view details</div>
821
+ </div>
822
+ \`;
823
+ }
824
+
825
+ // Initialize
826
+ loadStateFromUrl();
827
+ renderMainPanel();
828
+ </script>
829
+ </body>
830
+ </html>`;
831
+ }
832
+ generateNamespaceList(routes) {
833
+ const namespaces = new Map();
834
+ for (const route of routes) {
835
+ const ns = route.namespace || 'root';
836
+ namespaces.set(ns, (namespaces.get(ns) || 0) + 1);
837
+ }
838
+ const sorted = [...namespaces.entries()].sort((a, b) => b[1] - a[1]);
839
+ return sorted
840
+ .map(([ns, count]) => `
841
+ <div class="namespace-item" data-namespace="${ns === 'root' ? '' : ns}">
842
+ <span>${ns}</span>
843
+ <span class="namespace-count">${count}</span>
844
+ </div>
845
+ `)
846
+ .join('');
847
+ }
848
+ generateMethodFilters(routes) {
849
+ const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
850
+ const counts = new Map();
851
+ for (const route of routes) {
852
+ counts.set(route.method, (counts.get(route.method) || 0) + 1);
853
+ }
854
+ return methods
855
+ .map((method) => `
856
+ <div class="namespace-item" data-method="${method}">
857
+ <span class="method-badge method-${method}">${method}</span>
858
+ <span class="namespace-count">${counts.get(method) || 0}</span>
859
+ </div>
860
+ `)
861
+ .join('');
862
+ }
863
+ generateRoutesView(routes) {
864
+ return `
865
+ <div class="panel-header">
866
+ <div class="panel-title">Routes (${routes.length})</div>
867
+ </div>
868
+ <table class="routes-table">
869
+ <thead>
870
+ <tr>
871
+ <th>Method</th>
872
+ <th>Path</th>
873
+ <th>Controller#Action</th>
874
+ </tr>
875
+ </thead>
876
+ <tbody>
877
+ ${routes
878
+ .slice(0, 200)
879
+ .map((route, idx) => `
880
+ <tr data-type="route" data-index="${idx}">
881
+ <td><span class="method-badge method-${route.method}">${route.method}</span></td>
882
+ <td class="path-text">${this.highlightParams(route.path)}</td>
883
+ <td class="controller-text">${route.controller}#${route.action}</td>
884
+ </tr>
885
+ `)
886
+ .join('')}
887
+ </tbody>
888
+ </table>
889
+ `;
890
+ }
891
+ highlightParams(path) {
892
+ return path.replace(/:([a-zA-Z_]+)/g, '<span class="param">:$1</span>');
893
+ }
894
+ }
895
+ // Standalone execution
896
+ async function main() {
897
+ const targetPath = process.argv[2] || process.cwd();
898
+ const outputPath = process.argv[3] || path.join(targetPath, 'rails-map.html');
899
+ const generator = new RailsMapGenerator(targetPath);
900
+ await generator.generate({
901
+ title: 'Rails Application Map',
902
+ outputPath,
903
+ });
904
+ }
905
+ const isMainModule = import.meta.url === `file://${process.argv[1]}`;
906
+ if (isMainModule) {
907
+ main().catch(console.error);
908
+ }
909
+ //# sourceMappingURL=rails-map-generator.js.map