@wtdlee/repomap 0.2.0 → 0.3.1
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/dist/analyzers/index.d.ts +69 -5
- package/dist/analyzers/index.js +1 -5
- package/dist/chunk-3PWXDB7B.js +153 -0
- package/dist/{generators/page-map-generator.js → chunk-3YFXZAP7.js} +322 -358
- package/dist/chunk-6F4PWJZI.js +1 -0
- package/dist/{generators/rails-map-generator.js → chunk-E4WRODSI.js} +86 -94
- package/dist/chunk-GNBMJMET.js +2519 -0
- package/dist/{server/doc-server.js → chunk-M6YNU536.js} +702 -290
- package/dist/chunk-OWM6WNLE.js +2610 -0
- package/dist/chunk-SSU6QFTX.js +1058 -0
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +348 -452
- package/dist/dataflow-analyzer-BfAiqVUp.d.ts +180 -0
- package/dist/env-detector-EEMVUEIA.js +1 -0
- package/dist/generators/index.d.ts +431 -3
- package/dist/generators/index.js +2 -3
- package/dist/index.d.ts +53 -10
- package/dist/index.js +8 -11
- package/dist/page-map-generator-6MJGPBVA.js +1 -0
- package/dist/rails-UWSDRS33.js +1 -0
- package/dist/rails-map-generator-D2URLMVJ.js +2 -0
- package/dist/server/index.d.ts +33 -1
- package/dist/server/index.js +7 -1
- package/dist/types.d.ts +39 -37
- package/dist/types.js +1 -5
- package/package.json +5 -3
- package/dist/analyzers/base-analyzer.d.ts +0 -45
- package/dist/analyzers/base-analyzer.js +0 -47
- package/dist/analyzers/dataflow-analyzer.d.ts +0 -29
- package/dist/analyzers/dataflow-analyzer.js +0 -425
- package/dist/analyzers/graphql-analyzer.d.ts +0 -22
- package/dist/analyzers/graphql-analyzer.js +0 -386
- package/dist/analyzers/pages-analyzer.d.ts +0 -84
- package/dist/analyzers/pages-analyzer.js +0 -1695
- package/dist/analyzers/rails/index.d.ts +0 -46
- package/dist/analyzers/rails/index.js +0 -145
- package/dist/analyzers/rails/rails-controller-analyzer.d.ts +0 -82
- package/dist/analyzers/rails/rails-controller-analyzer.js +0 -478
- package/dist/analyzers/rails/rails-grpc-analyzer.d.ts +0 -44
- package/dist/analyzers/rails/rails-grpc-analyzer.js +0 -262
- package/dist/analyzers/rails/rails-model-analyzer.d.ts +0 -88
- package/dist/analyzers/rails/rails-model-analyzer.js +0 -493
- package/dist/analyzers/rails/rails-react-analyzer.d.ts +0 -41
- package/dist/analyzers/rails/rails-react-analyzer.js +0 -529
- package/dist/analyzers/rails/rails-routes-analyzer.d.ts +0 -62
- package/dist/analyzers/rails/rails-routes-analyzer.js +0 -540
- package/dist/analyzers/rails/rails-view-analyzer.d.ts +0 -49
- package/dist/analyzers/rails/rails-view-analyzer.js +0 -386
- package/dist/analyzers/rails/ruby-parser.d.ts +0 -63
- package/dist/analyzers/rails/ruby-parser.js +0 -212
- package/dist/analyzers/rest-api-analyzer.d.ts +0 -65
- package/dist/analyzers/rest-api-analyzer.js +0 -479
- package/dist/core/cache.d.ts +0 -47
- package/dist/core/cache.js +0 -151
- package/dist/core/engine.d.ts +0 -46
- package/dist/core/engine.js +0 -319
- package/dist/core/index.d.ts +0 -2
- package/dist/core/index.js +0 -2
- package/dist/generators/markdown-generator.d.ts +0 -25
- package/dist/generators/markdown-generator.js +0 -782
- package/dist/generators/mermaid-generator.d.ts +0 -35
- package/dist/generators/mermaid-generator.js +0 -364
- package/dist/generators/page-map-generator.d.ts +0 -22
- package/dist/generators/rails-map-generator.d.ts +0 -21
- package/dist/server/doc-server.d.ts +0 -30
- package/dist/utils/env-detector.d.ts +0 -31
- package/dist/utils/env-detector.js +0 -188
- package/dist/utils/parallel.d.ts +0 -23
- package/dist/utils/parallel.js +0 -70
|
@@ -1,200 +1,609 @@
|
|
|
1
|
+
import { RailsMapGenerator } from './chunk-E4WRODSI.js';
|
|
2
|
+
import { detectEnvironments } from './chunk-3PWXDB7B.js';
|
|
3
|
+
import { RestApiAnalyzer, DataFlowAnalyzer, GraphQLAnalyzer, PagesAnalyzer } from './chunk-GNBMJMET.js';
|
|
4
|
+
import { MermaidGenerator, MarkdownGenerator } from './chunk-SSU6QFTX.js';
|
|
5
|
+
import { PageMapGenerator } from './chunk-3YFXZAP7.js';
|
|
6
|
+
import { analyzeRailsApp } from './chunk-OWM6WNLE.js';
|
|
7
|
+
import { simpleGit } from 'simple-git';
|
|
8
|
+
import * as fs from 'fs/promises';
|
|
9
|
+
import * as path2 from 'path';
|
|
10
|
+
import fg from 'fast-glob';
|
|
11
|
+
import * as crypto from 'crypto';
|
|
1
12
|
import express from 'express';
|
|
2
13
|
import { Server } from 'socket.io';
|
|
3
14
|
import * as http from 'http';
|
|
4
|
-
import * as fs from 'fs/promises';
|
|
5
|
-
import * as path from 'path';
|
|
6
15
|
import { marked } from 'marked';
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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();
|
|
16
|
+
import * as net from 'net';
|
|
17
|
+
|
|
18
|
+
var CACHE_VERSION = "1.1";
|
|
19
|
+
var AnalysisCache = class {
|
|
20
|
+
cacheDir;
|
|
21
|
+
manifest;
|
|
22
|
+
manifestPath;
|
|
23
|
+
dirty = false;
|
|
24
|
+
constructor(repoPath) {
|
|
25
|
+
this.cacheDir = path2.join(repoPath, ".repomap-cache");
|
|
26
|
+
this.manifestPath = path2.join(this.cacheDir, "manifest.json");
|
|
27
|
+
this.manifest = { version: CACHE_VERSION, entries: {} };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Initialize cache directory and load manifest
|
|
31
|
+
*/
|
|
32
|
+
async init() {
|
|
33
|
+
try {
|
|
34
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.warn(` Warning: Could not create cache directory: ${err.message}`);
|
|
37
|
+
return;
|
|
35
38
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
});
|
|
39
|
+
try {
|
|
40
|
+
const data = await fs.readFile(this.manifestPath, "utf-8");
|
|
41
|
+
const loaded = JSON.parse(data);
|
|
42
|
+
if (loaded.version === CACHE_VERSION) {
|
|
43
|
+
this.manifest = loaded;
|
|
44
|
+
} else {
|
|
45
|
+
console.log(" Cache version mismatch, clearing cache...");
|
|
46
|
+
await this.clear();
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
123
49
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Compute hash for a file
|
|
53
|
+
*/
|
|
54
|
+
async computeFileHash(filePath) {
|
|
55
|
+
try {
|
|
56
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
57
|
+
return crypto.createHash("md5").update(content).digest("hex");
|
|
58
|
+
} catch {
|
|
59
|
+
return "";
|
|
131
60
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Compute hash for multiple files
|
|
64
|
+
* Sort files first for consistent ordering, then use a sample of files
|
|
65
|
+
*/
|
|
66
|
+
async computeFilesHash(filePaths) {
|
|
67
|
+
const sortedFiles = [...filePaths].sort();
|
|
68
|
+
const sampleSize = 50;
|
|
69
|
+
let sampleFiles;
|
|
70
|
+
if (sortedFiles.length <= sampleSize * 2) {
|
|
71
|
+
sampleFiles = sortedFiles;
|
|
72
|
+
} else {
|
|
73
|
+
sampleFiles = [...sortedFiles.slice(0, sampleSize), ...sortedFiles.slice(-sampleSize)];
|
|
74
|
+
}
|
|
75
|
+
const hashes = await Promise.all(sampleFiles.map((f) => this.computeFileHash(f)));
|
|
76
|
+
const countHash = crypto.createHash("md5").update(String(sortedFiles.length)).digest("hex");
|
|
77
|
+
return crypto.createHash("md5").update(hashes.join("") + countHash).digest("hex");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get cached data if valid
|
|
81
|
+
*/
|
|
82
|
+
get(key, currentHash) {
|
|
83
|
+
const entry = this.manifest.entries[key];
|
|
84
|
+
if (entry && entry.hash === currentHash) {
|
|
85
|
+
return entry.data;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Store data in cache
|
|
91
|
+
*/
|
|
92
|
+
set(key, hash, data) {
|
|
93
|
+
this.manifest.entries[key] = {
|
|
94
|
+
hash,
|
|
95
|
+
timestamp: Date.now(),
|
|
96
|
+
data
|
|
97
|
+
};
|
|
98
|
+
this.dirty = true;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Save manifest to disk
|
|
102
|
+
*/
|
|
103
|
+
async save() {
|
|
104
|
+
if (!this.dirty) return;
|
|
105
|
+
try {
|
|
106
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
107
|
+
await fs.writeFile(this.manifestPath, JSON.stringify(this.manifest, null, 2));
|
|
108
|
+
this.dirty = false;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.warn(" Warning: Failed to save cache:", error.message);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Clear all cache
|
|
115
|
+
*/
|
|
116
|
+
async clear() {
|
|
117
|
+
this.manifest = { version: CACHE_VERSION, entries: {} };
|
|
118
|
+
this.dirty = true;
|
|
119
|
+
try {
|
|
120
|
+
await fs.rm(this.cacheDir, { recursive: true, force: true });
|
|
121
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get cache statistics
|
|
127
|
+
*/
|
|
128
|
+
getStats() {
|
|
129
|
+
const entries = Object.keys(this.manifest.entries).length;
|
|
130
|
+
const size = JSON.stringify(this.manifest).length;
|
|
131
|
+
return {
|
|
132
|
+
entries,
|
|
133
|
+
size: size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(1)}MB` : `${(size / 1024).toFixed(1)}KB`
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// src/core/engine.ts
|
|
139
|
+
var DocGeneratorEngine = class {
|
|
140
|
+
config;
|
|
141
|
+
mermaidGenerator;
|
|
142
|
+
markdownGenerator;
|
|
143
|
+
noCache;
|
|
144
|
+
constructor(config, options) {
|
|
145
|
+
this.config = config;
|
|
146
|
+
this.mermaidGenerator = new MermaidGenerator();
|
|
147
|
+
this.markdownGenerator = new MarkdownGenerator();
|
|
148
|
+
this.noCache = options?.noCache ?? false;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Run documentation generation for all configured repositories
|
|
152
|
+
*/
|
|
153
|
+
async generate() {
|
|
154
|
+
console.log("\u{1F680} Starting documentation generation...\n");
|
|
155
|
+
const repositoryReports = [];
|
|
156
|
+
for (const repoConfig of this.config.repositories) {
|
|
157
|
+
try {
|
|
158
|
+
console.log(`
|
|
159
|
+
\u{1F4E6} Analyzing ${repoConfig.displayName}...`);
|
|
160
|
+
const report2 = await this.analyzeRepository(repoConfig);
|
|
161
|
+
repositoryReports.push(report2);
|
|
162
|
+
console.log(`\u2705 Completed ${repoConfig.displayName}`);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(`\u274C Failed to analyze ${repoConfig.name}:`, error.message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
console.log("\n\u{1F517} Running cross-repository analysis...");
|
|
168
|
+
const crossRepoAnalysis = this.analyzeCrossRepo(repositoryReports);
|
|
169
|
+
console.log("\n\u{1F4CA} Generating diagrams...");
|
|
170
|
+
const results = repositoryReports.map((r) => r.analysis);
|
|
171
|
+
const crossRepoLinks = this.extractCrossRepoLinks(results);
|
|
172
|
+
const diagrams = this.mermaidGenerator.generateAll(results, crossRepoLinks);
|
|
173
|
+
const report = {
|
|
174
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
175
|
+
repositories: repositoryReports,
|
|
176
|
+
crossRepoAnalysis,
|
|
177
|
+
diagrams
|
|
178
|
+
};
|
|
179
|
+
console.log("\n\u{1F4DD} Writing documentation...");
|
|
180
|
+
await this.writeDocumentation(report);
|
|
181
|
+
console.log("\n\u2728 Documentation generation complete!");
|
|
182
|
+
console.log(`\u{1F4C1} Output: ${this.config.outputDir}`);
|
|
183
|
+
return report;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Analyze a single repository (with caching)
|
|
187
|
+
*/
|
|
188
|
+
async analyzeRepository(repoConfig) {
|
|
189
|
+
const cache = new AnalysisCache(repoConfig.path);
|
|
190
|
+
await cache.init();
|
|
191
|
+
const { version, commitHash } = await this.getRepoInfo(repoConfig);
|
|
192
|
+
const sourceFiles = await fg(["**/*.{ts,tsx,graphql}"], {
|
|
193
|
+
cwd: repoConfig.path,
|
|
194
|
+
ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**"],
|
|
195
|
+
absolute: true
|
|
196
|
+
});
|
|
197
|
+
const contentHash = await cache.computeFilesHash(sourceFiles);
|
|
198
|
+
const cacheKey = `analysis_v${version}_${repoConfig.name}_${commitHash}`;
|
|
199
|
+
if (this.noCache) {
|
|
200
|
+
console.log(` \u{1F504} Cache disabled, analyzing from scratch...`);
|
|
201
|
+
}
|
|
202
|
+
const cachedResult = this.noCache ? null : cache.get(cacheKey, contentHash);
|
|
203
|
+
if (cachedResult) {
|
|
204
|
+
console.log(` \u26A1 Using cached analysis (hash: ${contentHash.slice(0, 8)}...)`);
|
|
205
|
+
const summary2 = {
|
|
206
|
+
totalPages: cachedResult.pages.length,
|
|
207
|
+
totalComponents: cachedResult.components.length,
|
|
208
|
+
totalGraphQLOperations: cachedResult.graphqlOperations.length,
|
|
209
|
+
totalDataFlows: cachedResult.dataFlows.length,
|
|
210
|
+
authRequiredPages: cachedResult.pages.filter((p) => p.authentication.required).length,
|
|
211
|
+
publicPages: cachedResult.pages.filter((p) => !p.authentication.required).length
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
name: repoConfig.name,
|
|
215
|
+
displayName: repoConfig.displayName,
|
|
216
|
+
version,
|
|
217
|
+
commitHash,
|
|
218
|
+
analysis: cachedResult,
|
|
219
|
+
summary: summary2
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const analyzers = repoConfig.analyzers.map((analyzerType) => this.createAnalyzer(analyzerType, repoConfig)).filter((a) => a !== null);
|
|
223
|
+
console.log(` Running ${analyzers.length} analyzers in parallel...`);
|
|
224
|
+
const startTime = Date.now();
|
|
225
|
+
const analysisResults = await Promise.all(analyzers.map((analyzer) => analyzer.analyze()));
|
|
226
|
+
console.log(` Analysis completed in ${((Date.now() - startTime) / 1e3).toFixed(1)}s`);
|
|
227
|
+
const analysis = this.mergeAnalysisResults(
|
|
228
|
+
analysisResults,
|
|
229
|
+
repoConfig.name,
|
|
230
|
+
version,
|
|
231
|
+
commitHash
|
|
232
|
+
);
|
|
233
|
+
cache.set(cacheKey, contentHash, analysis);
|
|
234
|
+
await cache.save();
|
|
235
|
+
console.log(` \u{1F4BE} Analysis cached (hash: ${contentHash.slice(0, 8)}...)`);
|
|
236
|
+
const summary = {
|
|
237
|
+
totalPages: analysis.pages.length,
|
|
238
|
+
totalComponents: analysis.components.length,
|
|
239
|
+
totalGraphQLOperations: analysis.graphqlOperations.length,
|
|
240
|
+
totalDataFlows: analysis.dataFlows.length,
|
|
241
|
+
authRequiredPages: analysis.pages.filter((p) => p.authentication.required).length,
|
|
242
|
+
publicPages: analysis.pages.filter((p) => !p.authentication.required).length
|
|
243
|
+
};
|
|
244
|
+
return {
|
|
245
|
+
name: repoConfig.name,
|
|
246
|
+
displayName: repoConfig.displayName,
|
|
247
|
+
version,
|
|
248
|
+
commitHash,
|
|
249
|
+
analysis,
|
|
250
|
+
summary
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Get repository version and commit info
|
|
255
|
+
*/
|
|
256
|
+
async getRepoInfo(repoConfig) {
|
|
257
|
+
try {
|
|
258
|
+
const git = simpleGit(repoConfig.path);
|
|
259
|
+
const log = await git.log({ n: 1 });
|
|
260
|
+
const commitHash = log.latest?.hash || "unknown";
|
|
261
|
+
let version = "unknown";
|
|
262
|
+
try {
|
|
263
|
+
const packageJsonPath = path2.join(repoConfig.path, "package.json");
|
|
264
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
|
|
265
|
+
version = packageJson.version || "unknown";
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
return { version, commitHash };
|
|
269
|
+
} catch {
|
|
270
|
+
return { version: "unknown", commitHash: "unknown" };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Create analyzer instance based on type
|
|
275
|
+
*/
|
|
276
|
+
createAnalyzer(type, config) {
|
|
277
|
+
switch (type) {
|
|
278
|
+
case "pages":
|
|
279
|
+
if (config.type === "nextjs" || config.type === "rails" || config.type === "generic") {
|
|
280
|
+
return new PagesAnalyzer(config);
|
|
153
281
|
}
|
|
154
|
-
|
|
282
|
+
break;
|
|
283
|
+
case "graphql":
|
|
284
|
+
return new GraphQLAnalyzer(config);
|
|
285
|
+
case "dataflow":
|
|
286
|
+
case "components":
|
|
287
|
+
return new DataFlowAnalyzer(config);
|
|
288
|
+
case "rest-api":
|
|
289
|
+
case "api":
|
|
290
|
+
return new RestApiAnalyzer(config);
|
|
155
291
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Merge multiple analysis results
|
|
296
|
+
*/
|
|
297
|
+
mergeAnalysisResults(results, repository, version, commitHash) {
|
|
298
|
+
const merged = {
|
|
299
|
+
repository,
|
|
300
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
301
|
+
version,
|
|
302
|
+
commitHash,
|
|
303
|
+
pages: [],
|
|
304
|
+
graphqlOperations: [],
|
|
305
|
+
apiCalls: [],
|
|
306
|
+
components: [],
|
|
307
|
+
dataFlows: [],
|
|
308
|
+
apiEndpoints: [],
|
|
309
|
+
models: [],
|
|
310
|
+
crossRepoLinks: []
|
|
311
|
+
};
|
|
312
|
+
for (const result of results) {
|
|
313
|
+
if (result.pages) merged.pages.push(...result.pages);
|
|
314
|
+
if (result.graphqlOperations) merged.graphqlOperations.push(...result.graphqlOperations);
|
|
315
|
+
if (result.apiCalls) merged.apiCalls.push(...result.apiCalls);
|
|
316
|
+
if (result.components) merged.components.push(...result.components);
|
|
317
|
+
if (result.dataFlows) merged.dataFlows.push(...result.dataFlows);
|
|
318
|
+
if (result.apiEndpoints) merged.apiEndpoints.push(...result.apiEndpoints);
|
|
319
|
+
if (result.models) merged.models.push(...result.models);
|
|
320
|
+
if (result.crossRepoLinks) merged.crossRepoLinks.push(...result.crossRepoLinks);
|
|
321
|
+
}
|
|
322
|
+
return merged;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Analyze cross-repository relationships
|
|
326
|
+
*/
|
|
327
|
+
analyzeCrossRepo(reports) {
|
|
328
|
+
const sharedTypes = [];
|
|
329
|
+
const apiConnections = [];
|
|
330
|
+
const navigationFlows = [];
|
|
331
|
+
const dataFlowAcrossRepos = [];
|
|
332
|
+
const operationsByName = /* @__PURE__ */ new Map();
|
|
333
|
+
for (const report of reports) {
|
|
334
|
+
for (const op of report.analysis.graphqlOperations) {
|
|
335
|
+
const repos = operationsByName.get(op.name) || [];
|
|
336
|
+
repos.push(report.name);
|
|
337
|
+
operationsByName.set(op.name, repos);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (const [name, repos] of operationsByName) {
|
|
341
|
+
if (repos.length > 1) {
|
|
342
|
+
sharedTypes.push(name);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const frontendRepos = reports.filter((r) => r.analysis.pages.length > 0);
|
|
346
|
+
const backendRepos = reports.filter((r) => r.analysis.apiEndpoints.length > 0);
|
|
347
|
+
for (const frontend of frontendRepos) {
|
|
348
|
+
for (const backend of backendRepos) {
|
|
349
|
+
for (const endpoint of backend.analysis.apiEndpoints) {
|
|
350
|
+
apiConnections.push({
|
|
351
|
+
frontend: frontend.name,
|
|
352
|
+
backend: backend.name,
|
|
353
|
+
endpoint: endpoint.path,
|
|
354
|
+
operations: frontend.analysis.graphqlOperations.filter((op) => op.usedIn.length > 0).map((op) => op.name)
|
|
355
|
+
});
|
|
171
356
|
}
|
|
172
|
-
|
|
357
|
+
}
|
|
173
358
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
359
|
+
return {
|
|
360
|
+
sharedTypes,
|
|
361
|
+
apiConnections,
|
|
362
|
+
navigationFlows,
|
|
363
|
+
dataFlowAcrossRepos
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Extract cross-repository links
|
|
368
|
+
*/
|
|
369
|
+
extractCrossRepoLinks(results) {
|
|
370
|
+
const links = [];
|
|
371
|
+
const operationsByName = /* @__PURE__ */ new Map();
|
|
372
|
+
for (const result of results) {
|
|
373
|
+
for (const op of result.graphqlOperations) {
|
|
374
|
+
const existing = operationsByName.get(op.name) || [];
|
|
375
|
+
existing.push(result);
|
|
376
|
+
operationsByName.set(op.name, existing);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
for (const [name, repos] of operationsByName) {
|
|
380
|
+
if (repos.length > 1) {
|
|
381
|
+
links.push({
|
|
382
|
+
sourceRepo: repos[0].repository,
|
|
383
|
+
sourcePath: `graphql/${name}`,
|
|
384
|
+
targetRepo: repos[1].repository,
|
|
385
|
+
targetPath: `graphql/${name}`,
|
|
386
|
+
linkType: "graphql-operation",
|
|
387
|
+
description: `Shared GraphQL operation: ${name}`
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return links;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Write documentation to output directory
|
|
395
|
+
*/
|
|
396
|
+
async writeDocumentation(report) {
|
|
397
|
+
const outputDir = this.config.outputDir;
|
|
398
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
399
|
+
const docs = this.markdownGenerator.generateDocumentation(report);
|
|
400
|
+
for (const [filePath, content] of docs) {
|
|
401
|
+
const fullPath = path2.join(outputDir, filePath);
|
|
402
|
+
const dir = path2.dirname(fullPath);
|
|
403
|
+
await fs.mkdir(dir, { recursive: true });
|
|
404
|
+
await fs.writeFile(fullPath, content, "utf-8");
|
|
405
|
+
console.log(` \u{1F4C4} ${filePath}`);
|
|
406
|
+
}
|
|
407
|
+
const jsonPath = path2.join(outputDir, "report.json");
|
|
408
|
+
await fs.writeFile(jsonPath, JSON.stringify(report, null, 2), "utf-8");
|
|
409
|
+
console.log(` \u{1F4CB} report.json`);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
function isPortAvailable(port) {
|
|
413
|
+
return new Promise((resolve) => {
|
|
414
|
+
const server = net.createServer();
|
|
415
|
+
server.once("error", (err) => {
|
|
416
|
+
if (err.code === "EADDRINUSE") {
|
|
417
|
+
resolve(false);
|
|
418
|
+
} else {
|
|
419
|
+
resolve(false);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
server.once("listening", () => {
|
|
423
|
+
server.close();
|
|
424
|
+
resolve(true);
|
|
425
|
+
});
|
|
426
|
+
server.listen(port);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
430
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
431
|
+
const port = startPort + i;
|
|
432
|
+
if (await isPortAvailable(port)) {
|
|
433
|
+
return port;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
throw new Error(
|
|
437
|
+
`No available port found between ${startPort} and ${startPort + maxAttempts - 1}`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/server/doc-server.ts
|
|
442
|
+
var DocServer = class {
|
|
443
|
+
config;
|
|
444
|
+
port;
|
|
445
|
+
app;
|
|
446
|
+
server;
|
|
447
|
+
io;
|
|
448
|
+
engine;
|
|
449
|
+
currentReport = null;
|
|
450
|
+
envResult = null;
|
|
451
|
+
railsAnalysis = null;
|
|
452
|
+
constructor(config, port = 3030, options) {
|
|
453
|
+
this.config = config;
|
|
454
|
+
this.port = port;
|
|
455
|
+
this.app = express();
|
|
456
|
+
this.server = http.createServer(this.app);
|
|
457
|
+
this.io = new Server(this.server);
|
|
458
|
+
this.engine = new DocGeneratorEngine(config, { noCache: options?.noCache });
|
|
459
|
+
this.setupRoutes();
|
|
460
|
+
this.setupSocketIO();
|
|
461
|
+
}
|
|
462
|
+
setupRoutes() {
|
|
463
|
+
this.app.use("/assets", express.static(path2.join(this.config.outputDir, "assets")));
|
|
464
|
+
const cssFiles = ["common.css", "page-map.css", "docs.css", "rails-map.css"];
|
|
465
|
+
cssFiles.forEach((file) => {
|
|
466
|
+
this.app.get(`/${file}`, async (req, res) => {
|
|
467
|
+
try {
|
|
468
|
+
const cssPath = new URL(`../generators/assets/${file}`, import.meta.url);
|
|
469
|
+
const css = await fs.readFile(cssPath, "utf-8");
|
|
470
|
+
res.type("text/css").send(css);
|
|
471
|
+
} catch {
|
|
472
|
+
res.status(404).send("CSS not found");
|
|
191
473
|
}
|
|
192
|
-
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
this.app.get("/", (req, res) => {
|
|
477
|
+
res.redirect("/page-map");
|
|
478
|
+
});
|
|
479
|
+
this.app.get("/page-map", (req, res) => {
|
|
480
|
+
if (!this.currentReport) {
|
|
481
|
+
res.status(503).send("Documentation not ready yet");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const generator = new PageMapGenerator();
|
|
485
|
+
res.send(
|
|
486
|
+
generator.generatePageMapHtml(this.currentReport, {
|
|
487
|
+
envResult: this.envResult,
|
|
488
|
+
railsAnalysis: this.railsAnalysis
|
|
489
|
+
})
|
|
490
|
+
);
|
|
491
|
+
});
|
|
492
|
+
this.app.get("/rails-map", (req, res) => {
|
|
493
|
+
if (!this.railsAnalysis) {
|
|
494
|
+
res.status(404).send("No Rails environment detected");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const generator = new RailsMapGenerator();
|
|
498
|
+
res.send(generator.generateFromResult(this.railsAnalysis));
|
|
499
|
+
});
|
|
500
|
+
this.app.get("/docs", async (req, res) => {
|
|
501
|
+
res.send(await this.renderPage("index"));
|
|
502
|
+
});
|
|
503
|
+
this.app.get("/docs/*", async (req, res) => {
|
|
504
|
+
const pagePath = req.params[0] || "index";
|
|
505
|
+
res.send(await this.renderPage(pagePath));
|
|
506
|
+
});
|
|
507
|
+
this.app.get("/api/report", (req, res) => {
|
|
508
|
+
res.json(this.currentReport);
|
|
509
|
+
});
|
|
510
|
+
this.app.get("/api/env", (req, res) => {
|
|
511
|
+
res.json(this.envResult);
|
|
512
|
+
});
|
|
513
|
+
this.app.get("/api/rails", (req, res) => {
|
|
514
|
+
if (this.railsAnalysis) {
|
|
515
|
+
res.json(this.railsAnalysis);
|
|
516
|
+
} else {
|
|
517
|
+
res.status(404).json({ error: "No Rails analysis available" });
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
this.app.get("/api/diagram/:name", (req, res) => {
|
|
521
|
+
const diagram = this.currentReport?.diagrams.find(
|
|
522
|
+
(d) => d.title.toLowerCase().replace(/\s+/g, "-") === req.params.name
|
|
523
|
+
);
|
|
524
|
+
if (diagram) {
|
|
525
|
+
res.json(diagram);
|
|
526
|
+
} else {
|
|
527
|
+
res.status(404).json({ error: "Diagram not found" });
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
this.app.post("/api/regenerate", async (req, res) => {
|
|
531
|
+
try {
|
|
532
|
+
await this.regenerate();
|
|
533
|
+
res.json({ success: true });
|
|
534
|
+
} catch (error) {
|
|
535
|
+
res.status(500).json({ error: error.message });
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
setupSocketIO() {
|
|
540
|
+
this.io.on("connection", (socket) => {
|
|
541
|
+
console.log("Client connected");
|
|
542
|
+
socket.on("disconnect", () => {
|
|
543
|
+
console.log("Client disconnected");
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
async renderPage(pagePath) {
|
|
548
|
+
const cleanPath = pagePath.replace(/\.md$/, "");
|
|
549
|
+
const mdPath = path2.join(this.config.outputDir, `${cleanPath}.md`);
|
|
550
|
+
let content = "";
|
|
551
|
+
try {
|
|
552
|
+
const markdown = await fs.readFile(mdPath, "utf-8");
|
|
553
|
+
let html = await marked.parse(markdown);
|
|
554
|
+
html = html.replace(
|
|
555
|
+
/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,
|
|
556
|
+
'<div class="mermaid">$1</div>'
|
|
557
|
+
);
|
|
558
|
+
html = html.replace(/<table>/g, '<div class="table-wrapper"><table>');
|
|
559
|
+
html = html.replace(/<\/table>/g, "</table></div>");
|
|
560
|
+
content = html;
|
|
561
|
+
} catch (e) {
|
|
562
|
+
console.error(`Failed to render page: ${mdPath}`, e);
|
|
563
|
+
content = `<h1>Page not found</h1><p>Path: ${cleanPath}</p>`;
|
|
564
|
+
}
|
|
565
|
+
return this.getHtmlTemplate(content);
|
|
566
|
+
}
|
|
567
|
+
getGraphQLData() {
|
|
568
|
+
if (!this.currentReport) return "[]";
|
|
569
|
+
const ops = [];
|
|
570
|
+
for (const repo of this.currentReport.repositories) {
|
|
571
|
+
for (const op of repo.analysis?.graphqlOperations || []) {
|
|
572
|
+
ops.push({
|
|
573
|
+
name: op.name,
|
|
574
|
+
type: op.type,
|
|
575
|
+
returnType: op.returnType,
|
|
576
|
+
variables: op.variables,
|
|
577
|
+
fields: op.fields,
|
|
578
|
+
usedIn: op.usedIn
|
|
579
|
+
});
|
|
580
|
+
}
|
|
193
581
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
582
|
+
return JSON.stringify(ops);
|
|
583
|
+
}
|
|
584
|
+
getApiCallsData() {
|
|
585
|
+
if (!this.currentReport) return "[]";
|
|
586
|
+
const calls = [];
|
|
587
|
+
for (const repo of this.currentReport.repositories) {
|
|
588
|
+
for (const call of repo.analysis?.apiCalls || []) {
|
|
589
|
+
calls.push({
|
|
590
|
+
id: call.id,
|
|
591
|
+
method: call.method,
|
|
592
|
+
url: call.url,
|
|
593
|
+
callType: call.callType,
|
|
594
|
+
filePath: call.filePath,
|
|
595
|
+
line: call.line,
|
|
596
|
+
containingFunction: call.containingFunction,
|
|
597
|
+
requiresAuth: call.requiresAuth
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return JSON.stringify(calls);
|
|
602
|
+
}
|
|
603
|
+
getHtmlTemplate(content) {
|
|
604
|
+
const graphqlData = this.getGraphQLData();
|
|
605
|
+
const apiCallsData = this.getApiCallsData();
|
|
606
|
+
return `<!DOCTYPE html>
|
|
198
607
|
<html lang="ja">
|
|
199
608
|
<head>
|
|
200
609
|
<meta charset="UTF-8">
|
|
@@ -272,10 +681,10 @@ export class DocServer {
|
|
|
272
681
|
<body>
|
|
273
682
|
<header class="header">
|
|
274
683
|
<div style="display:flex;align-items:center;gap:24px">
|
|
275
|
-
<h1 style="cursor:pointer" onclick="location.href='/'"
|
|
684
|
+
<h1 style="cursor:pointer" onclick="location.href='/'">\u{1F4CA} ${this.config.repositories[0]?.displayName || this.config.repositories[0]?.name || "Repository"}</h1>
|
|
276
685
|
<nav style="display:flex;gap:4px">
|
|
277
686
|
<a href="/page-map" class="nav-link">Page Map</a>
|
|
278
|
-
${this.railsAnalysis ? '<a href="/rails-map" class="nav-link">Rails Map</a>' :
|
|
687
|
+
${this.railsAnalysis ? '<a href="/rails-map" class="nav-link">Rails Map</a>' : ""}
|
|
279
688
|
<a href="/docs" class="nav-link active">Docs</a>
|
|
280
689
|
<a href="/api/report" class="nav-link" target="_blank">API</a>
|
|
281
690
|
</nav>
|
|
@@ -287,14 +696,14 @@ export class DocServer {
|
|
|
287
696
|
<div class="nav-group">
|
|
288
697
|
<span class="nav-group-title">Documentation</span>
|
|
289
698
|
<div class="nav-subitems">
|
|
290
|
-
${this.config.repositories
|
|
291
|
-
|
|
699
|
+
${this.config.repositories.map(
|
|
700
|
+
(repo) => `
|
|
292
701
|
<a href="/docs/repos/${repo.name}/pages">Pages</a>
|
|
293
702
|
<a href="/docs/repos/${repo.name}/components">Components</a>
|
|
294
703
|
<a href="/docs/repos/${repo.name}/graphql">GraphQL</a>
|
|
295
704
|
<a href="/docs/repos/${repo.name}/dataflow">Data Flow</a>
|
|
296
|
-
`
|
|
297
|
-
|
|
705
|
+
`
|
|
706
|
+
).join("")}
|
|
298
707
|
</div>
|
|
299
708
|
</div>
|
|
300
709
|
<div class="nav-group">
|
|
@@ -320,10 +729,10 @@ export class DocServer {
|
|
|
320
729
|
<div class="detail-modal-content">
|
|
321
730
|
<div class="detail-modal-header">
|
|
322
731
|
<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"
|
|
732
|
+
<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">\u2190 Back</button>
|
|
324
733
|
<h3 id="modalTitle">Details</h3>
|
|
325
734
|
</div>
|
|
326
|
-
<button class="detail-modal-close" onclick="closeModal()"
|
|
735
|
+
<button class="detail-modal-close" onclick="closeModal()">\xD7</button>
|
|
327
736
|
</div>
|
|
328
737
|
<div id="modalBody"></div>
|
|
329
738
|
</div>
|
|
@@ -349,10 +758,10 @@ export class DocServer {
|
|
|
349
758
|
container.className = 'mermaid-container';
|
|
350
759
|
container.innerHTML = \`
|
|
351
760
|
<div class="mermaid-controls">
|
|
352
|
-
<button onclick="zoomDiagram(\${idx}, 0.8)" title="
|
|
353
|
-
<button onclick="zoomDiagram(\${idx}, 1.25)" title="
|
|
354
|
-
<button onclick="zoomDiagram(\${idx}, 'reset')" title="
|
|
355
|
-
<button onclick="toggleFullscreen(\${idx})" title="
|
|
761
|
+
<button onclick="zoomDiagram(\${idx}, 0.8)" title="\u7E2E\u5C0F">\u2796</button>
|
|
762
|
+
<button onclick="zoomDiagram(\${idx}, 1.25)" title="\u62E1\u5927">\u2795</button>
|
|
763
|
+
<button onclick="zoomDiagram(\${idx}, 'reset')" title="\u30EA\u30BB\u30C3\u30C8">\u{1F504}</button>
|
|
764
|
+
<button onclick="toggleFullscreen(\${idx})" title="\u5168\u753B\u9762">\u26F6</button>
|
|
356
765
|
</div>
|
|
357
766
|
<div class="mermaid-wrapper" id="wrapper-\${idx}">
|
|
358
767
|
<div class="mermaid-inner" id="inner-\${idx}"></div>
|
|
@@ -480,17 +889,17 @@ export class DocServer {
|
|
|
480
889
|
modalHistory = [];
|
|
481
890
|
|
|
482
891
|
// Clean name: remove icons and extract operation name from patterns like "GraphQL: OPERATION_NAME"
|
|
483
|
-
let cleanName = text.replace(/[\u{1F512}\u{1F4E1}\
|
|
892
|
+
let cleanName = text.replace(/[\u{1F512}\u{1F4E1}\u270F\uFE0F\u{1F504}]/gu, '').trim();
|
|
484
893
|
// Handle "GraphQL: OPERATION_NAME" pattern
|
|
485
894
|
if (cleanName.includes('GraphQL:')) {
|
|
486
|
-
cleanName = cleanName.replace(/^.*GraphQL
|
|
895
|
+
cleanName = cleanName.replace(/^.*GraphQL:s*/, '').trim();
|
|
487
896
|
}
|
|
488
897
|
// Handle "API: OPERATION_NAME" pattern
|
|
489
898
|
if (cleanName.includes('API:')) {
|
|
490
|
-
cleanName = cleanName.replace(/^.*API
|
|
899
|
+
cleanName = cleanName.replace(/^.*API:s*/, '').trim();
|
|
491
900
|
}
|
|
492
901
|
// Remove any remaining prefixes like "Query:", "Mutation:"
|
|
493
|
-
cleanName = cleanName.replace(/^(Query|Mutation|Fragment)
|
|
902
|
+
cleanName = cleanName.replace(/^(Query|Mutation|Fragment):s*/i, '').trim();
|
|
494
903
|
|
|
495
904
|
const op = window.findGraphQLOp?.(cleanName);
|
|
496
905
|
|
|
@@ -1117,7 +1526,7 @@ export class DocServer {
|
|
|
1117
1526
|
async function regenerate() {
|
|
1118
1527
|
try {
|
|
1119
1528
|
const btn = document.querySelector('.regenerate-btn');
|
|
1120
|
-
btn.textContent = '
|
|
1529
|
+
btn.textContent = '\u23F3 \u751F\u6210\u4E2D...';
|
|
1121
1530
|
btn.disabled = true;
|
|
1122
1531
|
|
|
1123
1532
|
const res = await fetch('/api/regenerate', { method: 'POST' });
|
|
@@ -1126,107 +1535,110 @@ export class DocServer {
|
|
|
1126
1535
|
if (data.success) {
|
|
1127
1536
|
window.location.reload();
|
|
1128
1537
|
} else {
|
|
1129
|
-
alert('
|
|
1538
|
+
alert('\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ' + data.error);
|
|
1130
1539
|
}
|
|
1131
1540
|
} catch (e) {
|
|
1132
|
-
alert('
|
|
1541
|
+
alert('\u30A8\u30E9\u30FC: ' + e.message);
|
|
1133
1542
|
} finally {
|
|
1134
1543
|
const btn = document.querySelector('.regenerate-btn');
|
|
1135
|
-
btn.textContent = '
|
|
1544
|
+
btn.textContent = '\u{1F504} \u518D\u751F\u6210';
|
|
1136
1545
|
btn.disabled = false;
|
|
1137
1546
|
}
|
|
1138
1547
|
}
|
|
1139
1548
|
</script>
|
|
1140
1549
|
</body>
|
|
1141
1550
|
</html>`;
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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();
|
|
1551
|
+
}
|
|
1552
|
+
async start(openBrowser = true) {
|
|
1553
|
+
const rootPath = this.config.repositories[0]?.path || process.cwd();
|
|
1554
|
+
console.log("\u{1F50D} Detecting project environments...");
|
|
1555
|
+
this.envResult = await detectEnvironments(rootPath);
|
|
1556
|
+
if (this.envResult.environments.length > 0) {
|
|
1557
|
+
console.log(` Found: ${this.envResult.environments.map((e) => e.type).join(", ")}`);
|
|
1558
|
+
for (const env of this.envResult.environments) {
|
|
1559
|
+
if (env.features.length > 0) {
|
|
1560
|
+
console.log(` ${env.type} features: ${env.features.join(", ")}`);
|
|
1186
1561
|
}
|
|
1562
|
+
}
|
|
1187
1563
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
console.error(`⚠️ Rails re-analysis failed:`, error.message);
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
this.io.emit('reload');
|
|
1202
|
-
console.log('✅ Documentation regenerated');
|
|
1564
|
+
console.log("\n\u{1F4DA} Generating documentation...");
|
|
1565
|
+
this.currentReport = await this.engine.generate();
|
|
1566
|
+
if (this.envResult.hasRails) {
|
|
1567
|
+
console.log("\n\u{1F6E4}\uFE0F Analyzing Rails application...");
|
|
1568
|
+
try {
|
|
1569
|
+
this.railsAnalysis = await analyzeRailsApp(rootPath);
|
|
1570
|
+
console.log(` \u2705 Rails analysis complete`);
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
console.error(` \u26A0\uFE0F Rails analysis failed:`, error.message);
|
|
1573
|
+
}
|
|
1203
1574
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1575
|
+
try {
|
|
1576
|
+
const availablePort = await findAvailablePort(this.port);
|
|
1577
|
+
if (availablePort !== this.port) {
|
|
1578
|
+
console.log(`
|
|
1579
|
+
\u26A0\uFE0F Port ${this.port} is in use, using port ${availablePort} instead`);
|
|
1580
|
+
}
|
|
1581
|
+
this.port = availablePort;
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
console.error(`
|
|
1584
|
+
\u274C Failed to find available port: ${error.message}`);
|
|
1585
|
+
process.exit(1);
|
|
1586
|
+
}
|
|
1587
|
+
this.server.listen(this.port, () => {
|
|
1588
|
+
console.log(`
|
|
1589
|
+
\u{1F310} Documentation server running at http://localhost:${this.port}`);
|
|
1590
|
+
if (this.envResult?.hasRails && this.envResult?.hasNextjs) {
|
|
1591
|
+
console.log(" \u{1F4CA} Multiple environments detected - use tabs to switch views");
|
|
1592
|
+
}
|
|
1593
|
+
console.log(" Press Ctrl+C to stop\n");
|
|
1594
|
+
});
|
|
1595
|
+
if (openBrowser) {
|
|
1596
|
+
const open = (await import('open')).default;
|
|
1597
|
+
await open(`http://localhost:${this.port}`);
|
|
1227
1598
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
console.log('\n👋 Server stopped');
|
|
1599
|
+
if (this.config.watch.enabled) {
|
|
1600
|
+
this.watchForChanges();
|
|
1231
1601
|
}
|
|
1232
|
-
}
|
|
1602
|
+
}
|
|
1603
|
+
async regenerate() {
|
|
1604
|
+
console.log("\n\u{1F504} Regenerating documentation...");
|
|
1605
|
+
this.currentReport = await this.engine.generate();
|
|
1606
|
+
if (this.envResult?.hasRails) {
|
|
1607
|
+
const rootPath = this.config.repositories[0]?.path || process.cwd();
|
|
1608
|
+
try {
|
|
1609
|
+
this.railsAnalysis = await analyzeRailsApp(rootPath);
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
console.error(`\u26A0\uFE0F Rails re-analysis failed:`, error.message);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
this.io.emit("reload");
|
|
1615
|
+
console.log("\u2705 Documentation regenerated");
|
|
1616
|
+
}
|
|
1617
|
+
async watchForChanges() {
|
|
1618
|
+
const watchDirs = this.config.repositories.map((r) => r.path);
|
|
1619
|
+
let timeout = null;
|
|
1620
|
+
for (const dir of watchDirs) {
|
|
1621
|
+
try {
|
|
1622
|
+
const watcher = fs.watch(dir, { recursive: true });
|
|
1623
|
+
(async () => {
|
|
1624
|
+
for await (const event of watcher) {
|
|
1625
|
+
if (event.filename && (event.filename.endsWith(".ts") || event.filename.endsWith(".tsx"))) {
|
|
1626
|
+
if (timeout) clearTimeout(timeout);
|
|
1627
|
+
timeout = setTimeout(async () => {
|
|
1628
|
+
await this.regenerate();
|
|
1629
|
+
}, this.config.watch.debounce);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
})();
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
console.warn(`Warning: Could not watch directory ${dir}:`, error.message);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
stop() {
|
|
1639
|
+
this.server.close();
|
|
1640
|
+
console.log("\n\u{1F44B} Server stopped");
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1644
|
+
export { DocGeneratorEngine, DocServer };
|