@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.
- package/LICENSE +21 -0
- package/README.md +527 -0
- package/dist/analyzers/base-analyzer.d.ts +46 -0
- package/dist/analyzers/base-analyzer.d.ts.map +1 -0
- package/dist/analyzers/base-analyzer.js +48 -0
- package/dist/analyzers/base-analyzer.js.map +1 -0
- package/dist/analyzers/dataflow-analyzer.d.ts +30 -0
- package/dist/analyzers/dataflow-analyzer.d.ts.map +1 -0
- package/dist/analyzers/dataflow-analyzer.js +426 -0
- package/dist/analyzers/dataflow-analyzer.js.map +1 -0
- package/dist/analyzers/graphql-analyzer.d.ts +23 -0
- package/dist/analyzers/graphql-analyzer.d.ts.map +1 -0
- package/dist/analyzers/graphql-analyzer.js +387 -0
- package/dist/analyzers/graphql-analyzer.js.map +1 -0
- package/dist/analyzers/index.d.ts +6 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +6 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/analyzers/pages-analyzer.d.ts +85 -0
- package/dist/analyzers/pages-analyzer.d.ts.map +1 -0
- package/dist/analyzers/pages-analyzer.js +1696 -0
- package/dist/analyzers/pages-analyzer.js.map +1 -0
- package/dist/analyzers/rails/index.d.ts +47 -0
- package/dist/analyzers/rails/index.d.ts.map +1 -0
- package/dist/analyzers/rails/index.js +146 -0
- package/dist/analyzers/rails/index.js.map +1 -0
- package/dist/analyzers/rails/rails-controller-analyzer.d.ts +83 -0
- package/dist/analyzers/rails/rails-controller-analyzer.d.ts.map +1 -0
- package/dist/analyzers/rails/rails-controller-analyzer.js +479 -0
- package/dist/analyzers/rails/rails-controller-analyzer.js.map +1 -0
- package/dist/analyzers/rails/rails-grpc-analyzer.d.ts +45 -0
- package/dist/analyzers/rails/rails-grpc-analyzer.d.ts.map +1 -0
- package/dist/analyzers/rails/rails-grpc-analyzer.js +263 -0
- package/dist/analyzers/rails/rails-grpc-analyzer.js.map +1 -0
- package/dist/analyzers/rails/rails-model-analyzer.d.ts +89 -0
- package/dist/analyzers/rails/rails-model-analyzer.d.ts.map +1 -0
- package/dist/analyzers/rails/rails-model-analyzer.js +494 -0
- package/dist/analyzers/rails/rails-model-analyzer.js.map +1 -0
- package/dist/analyzers/rails/rails-react-analyzer.d.ts +42 -0
- package/dist/analyzers/rails/rails-react-analyzer.d.ts.map +1 -0
- package/dist/analyzers/rails/rails-react-analyzer.js +530 -0
- package/dist/analyzers/rails/rails-react-analyzer.js.map +1 -0
- package/dist/analyzers/rails/rails-routes-analyzer.d.ts +63 -0
- package/dist/analyzers/rails/rails-routes-analyzer.d.ts.map +1 -0
- package/dist/analyzers/rails/rails-routes-analyzer.js +541 -0
- package/dist/analyzers/rails/rails-routes-analyzer.js.map +1 -0
- package/dist/analyzers/rails/rails-view-analyzer.d.ts +50 -0
- package/dist/analyzers/rails/rails-view-analyzer.d.ts.map +1 -0
- package/dist/analyzers/rails/rails-view-analyzer.js +387 -0
- package/dist/analyzers/rails/rails-view-analyzer.js.map +1 -0
- package/dist/analyzers/rails/ruby-parser.d.ts +64 -0
- package/dist/analyzers/rails/ruby-parser.d.ts.map +1 -0
- package/dist/analyzers/rails/ruby-parser.js +213 -0
- package/dist/analyzers/rails/ruby-parser.js.map +1 -0
- package/dist/analyzers/rest-api-analyzer.d.ts +66 -0
- package/dist/analyzers/rest-api-analyzer.d.ts.map +1 -0
- package/dist/analyzers/rest-api-analyzer.js +480 -0
- package/dist/analyzers/rest-api-analyzer.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +550 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/cache.d.ts +48 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +152 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/engine.d.ts +47 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +320 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -0
- package/dist/generators/assets/common.css +187 -0
- package/dist/generators/assets/docs.css +363 -0
- package/dist/generators/assets/page-map.css +305 -0
- package/dist/generators/assets/rails-map.css +473 -0
- package/dist/generators/index.d.ts +4 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +4 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/markdown-generator.d.ts +26 -0
- package/dist/generators/markdown-generator.d.ts.map +1 -0
- package/dist/generators/markdown-generator.js +783 -0
- package/dist/generators/markdown-generator.js.map +1 -0
- package/dist/generators/mermaid-generator.d.ts +36 -0
- package/dist/generators/mermaid-generator.d.ts.map +1 -0
- package/dist/generators/mermaid-generator.js +365 -0
- package/dist/generators/mermaid-generator.js.map +1 -0
- package/dist/generators/page-map-generator.d.ts +23 -0
- package/dist/generators/page-map-generator.d.ts.map +1 -0
- package/dist/generators/page-map-generator.js +3563 -0
- package/dist/generators/page-map-generator.js.map +1 -0
- package/dist/generators/rails-map-generator.d.ts +22 -0
- package/dist/generators/rails-map-generator.d.ts.map +1 -0
- package/dist/generators/rails-map-generator.js +909 -0
- package/dist/generators/rails-map-generator.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/server/doc-server.d.ts +31 -0
- package/dist/server/doc-server.d.ts.map +1 -0
- package/dist/server/doc-server.js +1233 -0
- package/dist/server/doc-server.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types.d.ts +294 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/env-detector.d.ts +32 -0
- package/dist/utils/env-detector.d.ts.map +1 -0
- package/dist/utils/env-detector.js +189 -0
- package/dist/utils/env-detector.js.map +1 -0
- package/dist/utils/parallel.d.ts +24 -0
- package/dist/utils/parallel.d.ts.map +1 -0
- package/dist/utils/parallel.js +71 -0
- package/dist/utils/parallel.js.map +1 -0
- 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
|