@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,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, '"');
|
|
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
|