@wtdlee/repomap 0.3.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 -303
- 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 +4 -2
- 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
- package/dist/utils/port.d.ts +0 -15
- package/dist/utils/port.js +0 -41
|
@@ -1,201 +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.port = port;
|
|
30
|
-
this.app = express();
|
|
31
|
-
this.server = http.createServer(this.app);
|
|
32
|
-
this.io = new Server(this.server);
|
|
33
|
-
this.engine = new DocGeneratorEngine(config, { noCache: options?.noCache });
|
|
34
|
-
this.setupRoutes();
|
|
35
|
-
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;
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
res.type('text/css').send(css);
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
res.status(404).send('CSS not found');
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
// Main page - redirect to page-map
|
|
55
|
-
this.app.get('/', (req, res) => {
|
|
56
|
-
res.redirect('/page-map');
|
|
57
|
-
});
|
|
58
|
-
// Interactive page map (main view) - now with environment awareness
|
|
59
|
-
this.app.get('/page-map', (req, res) => {
|
|
60
|
-
if (!this.currentReport) {
|
|
61
|
-
res.status(503).send('Documentation not ready yet');
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const generator = new PageMapGenerator();
|
|
65
|
-
res.send(generator.generatePageMapHtml(this.currentReport, {
|
|
66
|
-
envResult: this.envResult,
|
|
67
|
-
railsAnalysis: this.railsAnalysis,
|
|
68
|
-
}));
|
|
69
|
-
});
|
|
70
|
-
// Rails map (standalone view)
|
|
71
|
-
this.app.get('/rails-map', (req, res) => {
|
|
72
|
-
if (!this.railsAnalysis) {
|
|
73
|
-
res.status(404).send('No Rails environment detected');
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
const generator = new RailsMapGenerator();
|
|
77
|
-
res.send(generator.generateFromResult(this.railsAnalysis));
|
|
78
|
-
});
|
|
79
|
-
// Markdown pages - index
|
|
80
|
-
this.app.get('/docs', async (req, res) => {
|
|
81
|
-
res.send(await this.renderPage('index'));
|
|
82
|
-
});
|
|
83
|
-
// Markdown pages - specific path
|
|
84
|
-
this.app.get('/docs/*', async (req, res) => {
|
|
85
|
-
const pagePath = req.params[0] || 'index';
|
|
86
|
-
res.send(await this.renderPage(pagePath));
|
|
87
|
-
});
|
|
88
|
-
// API endpoints
|
|
89
|
-
this.app.get('/api/report', (req, res) => {
|
|
90
|
-
res.json(this.currentReport);
|
|
91
|
-
});
|
|
92
|
-
// Environment detection result
|
|
93
|
-
this.app.get('/api/env', (req, res) => {
|
|
94
|
-
res.json(this.envResult);
|
|
95
|
-
});
|
|
96
|
-
// Rails analysis result
|
|
97
|
-
this.app.get('/api/rails', (req, res) => {
|
|
98
|
-
if (this.railsAnalysis) {
|
|
99
|
-
res.json(this.railsAnalysis);
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
res.status(404).json({ error: 'No Rails analysis available' });
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
this.app.get('/api/diagram/:name', (req, res) => {
|
|
106
|
-
const diagram = this.currentReport?.diagrams.find((d) => d.title.toLowerCase().replace(/\s+/g, '-') === req.params.name);
|
|
107
|
-
if (diagram) {
|
|
108
|
-
res.json(diagram);
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
res.status(404).json({ error: 'Diagram not found' });
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
// Regenerate endpoint
|
|
115
|
-
this.app.post('/api/regenerate', async (req, res) => {
|
|
116
|
-
try {
|
|
117
|
-
await this.regenerate();
|
|
118
|
-
res.json({ success: true });
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
res.status(500).json({ error: error.message });
|
|
122
|
-
}
|
|
123
|
-
});
|
|
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 {
|
|
124
49
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 "";
|
|
132
60
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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);
|
|
154
281
|
}
|
|
155
|
-
|
|
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);
|
|
156
291
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
});
|
|
172
356
|
}
|
|
173
|
-
|
|
357
|
+
}
|
|
174
358
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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");
|
|
192
473
|
}
|
|
193
|
-
|
|
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>`;
|
|
194
564
|
}
|
|
195
|
-
getHtmlTemplate(content)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
}
|
|
581
|
+
}
|
|
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>
|
|
199
607
|
<html lang="ja">
|
|
200
608
|
<head>
|
|
201
609
|
<meta charset="UTF-8">
|
|
@@ -273,10 +681,10 @@ export class DocServer {
|
|
|
273
681
|
<body>
|
|
274
682
|
<header class="header">
|
|
275
683
|
<div style="display:flex;align-items:center;gap:24px">
|
|
276
|
-
<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>
|
|
277
685
|
<nav style="display:flex;gap:4px">
|
|
278
686
|
<a href="/page-map" class="nav-link">Page Map</a>
|
|
279
|
-
${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>' : ""}
|
|
280
688
|
<a href="/docs" class="nav-link active">Docs</a>
|
|
281
689
|
<a href="/api/report" class="nav-link" target="_blank">API</a>
|
|
282
690
|
</nav>
|
|
@@ -288,14 +696,14 @@ export class DocServer {
|
|
|
288
696
|
<div class="nav-group">
|
|
289
697
|
<span class="nav-group-title">Documentation</span>
|
|
290
698
|
<div class="nav-subitems">
|
|
291
|
-
${this.config.repositories
|
|
292
|
-
|
|
699
|
+
${this.config.repositories.map(
|
|
700
|
+
(repo) => `
|
|
293
701
|
<a href="/docs/repos/${repo.name}/pages">Pages</a>
|
|
294
702
|
<a href="/docs/repos/${repo.name}/components">Components</a>
|
|
295
703
|
<a href="/docs/repos/${repo.name}/graphql">GraphQL</a>
|
|
296
704
|
<a href="/docs/repos/${repo.name}/dataflow">Data Flow</a>
|
|
297
|
-
`
|
|
298
|
-
|
|
705
|
+
`
|
|
706
|
+
).join("")}
|
|
299
707
|
</div>
|
|
300
708
|
</div>
|
|
301
709
|
<div class="nav-group">
|
|
@@ -321,10 +729,10 @@ export class DocServer {
|
|
|
321
729
|
<div class="detail-modal-content">
|
|
322
730
|
<div class="detail-modal-header">
|
|
323
731
|
<div style="display:flex;align-items:center;gap:8px">
|
|
324
|
-
<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>
|
|
325
733
|
<h3 id="modalTitle">Details</h3>
|
|
326
734
|
</div>
|
|
327
|
-
<button class="detail-modal-close" onclick="closeModal()"
|
|
735
|
+
<button class="detail-modal-close" onclick="closeModal()">\xD7</button>
|
|
328
736
|
</div>
|
|
329
737
|
<div id="modalBody"></div>
|
|
330
738
|
</div>
|
|
@@ -350,10 +758,10 @@ export class DocServer {
|
|
|
350
758
|
container.className = 'mermaid-container';
|
|
351
759
|
container.innerHTML = \`
|
|
352
760
|
<div class="mermaid-controls">
|
|
353
|
-
<button onclick="zoomDiagram(\${idx}, 0.8)" title="
|
|
354
|
-
<button onclick="zoomDiagram(\${idx}, 1.25)" title="
|
|
355
|
-
<button onclick="zoomDiagram(\${idx}, 'reset')" title="
|
|
356
|
-
<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>
|
|
357
765
|
</div>
|
|
358
766
|
<div class="mermaid-wrapper" id="wrapper-\${idx}">
|
|
359
767
|
<div class="mermaid-inner" id="inner-\${idx}"></div>
|
|
@@ -481,17 +889,17 @@ export class DocServer {
|
|
|
481
889
|
modalHistory = [];
|
|
482
890
|
|
|
483
891
|
// Clean name: remove icons and extract operation name from patterns like "GraphQL: OPERATION_NAME"
|
|
484
|
-
let cleanName = text.replace(/[\u{1F512}\u{1F4E1}\
|
|
892
|
+
let cleanName = text.replace(/[\u{1F512}\u{1F4E1}\u270F\uFE0F\u{1F504}]/gu, '').trim();
|
|
485
893
|
// Handle "GraphQL: OPERATION_NAME" pattern
|
|
486
894
|
if (cleanName.includes('GraphQL:')) {
|
|
487
|
-
cleanName = cleanName.replace(/^.*GraphQL
|
|
895
|
+
cleanName = cleanName.replace(/^.*GraphQL:s*/, '').trim();
|
|
488
896
|
}
|
|
489
897
|
// Handle "API: OPERATION_NAME" pattern
|
|
490
898
|
if (cleanName.includes('API:')) {
|
|
491
|
-
cleanName = cleanName.replace(/^.*API
|
|
899
|
+
cleanName = cleanName.replace(/^.*API:s*/, '').trim();
|
|
492
900
|
}
|
|
493
901
|
// Remove any remaining prefixes like "Query:", "Mutation:"
|
|
494
|
-
cleanName = cleanName.replace(/^(Query|Mutation|Fragment)
|
|
902
|
+
cleanName = cleanName.replace(/^(Query|Mutation|Fragment):s*/i, '').trim();
|
|
495
903
|
|
|
496
904
|
const op = window.findGraphQLOp?.(cleanName);
|
|
497
905
|
|
|
@@ -1118,7 +1526,7 @@ export class DocServer {
|
|
|
1118
1526
|
async function regenerate() {
|
|
1119
1527
|
try {
|
|
1120
1528
|
const btn = document.querySelector('.regenerate-btn');
|
|
1121
|
-
btn.textContent = '
|
|
1529
|
+
btn.textContent = '\u23F3 \u751F\u6210\u4E2D...';
|
|
1122
1530
|
btn.disabled = true;
|
|
1123
1531
|
|
|
1124
1532
|
const res = await fetch('/api/regenerate', { method: 'POST' });
|
|
@@ -1127,119 +1535,110 @@ export class DocServer {
|
|
|
1127
1535
|
if (data.success) {
|
|
1128
1536
|
window.location.reload();
|
|
1129
1537
|
} else {
|
|
1130
|
-
alert('
|
|
1538
|
+
alert('\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ' + data.error);
|
|
1131
1539
|
}
|
|
1132
1540
|
} catch (e) {
|
|
1133
|
-
alert('
|
|
1541
|
+
alert('\u30A8\u30E9\u30FC: ' + e.message);
|
|
1134
1542
|
} finally {
|
|
1135
1543
|
const btn = document.querySelector('.regenerate-btn');
|
|
1136
|
-
btn.textContent = '
|
|
1544
|
+
btn.textContent = '\u{1F504} \u518D\u751F\u6210';
|
|
1137
1545
|
btn.disabled = false;
|
|
1138
1546
|
}
|
|
1139
1547
|
}
|
|
1140
1548
|
</script>
|
|
1141
1549
|
</body>
|
|
1142
1550
|
</html>`;
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
console.log(` ${env.type} features: ${env.features.join(', ')}`);
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
// Generate initial documentation for frontend
|
|
1158
|
-
console.log('\n📚 Generating documentation...');
|
|
1159
|
-
this.currentReport = await this.engine.generate();
|
|
1160
|
-
// If Rails is detected, also analyze Rails
|
|
1161
|
-
if (this.envResult.hasRails) {
|
|
1162
|
-
console.log('\n🛤️ Analyzing Rails application...');
|
|
1163
|
-
try {
|
|
1164
|
-
this.railsAnalysis = await analyzeRailsApp(rootPath);
|
|
1165
|
-
console.log(` ✅ Rails analysis complete`);
|
|
1166
|
-
}
|
|
1167
|
-
catch (error) {
|
|
1168
|
-
console.error(` ⚠️ Rails analysis failed:`, error.message);
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
// Find available port (auto-detect if requested port is in use)
|
|
1172
|
-
try {
|
|
1173
|
-
const availablePort = await findAvailablePort(this.port);
|
|
1174
|
-
if (availablePort !== this.port) {
|
|
1175
|
-
console.log(`\n⚠️ Port ${this.port} is in use, using port ${availablePort} instead`);
|
|
1176
|
-
}
|
|
1177
|
-
this.port = availablePort;
|
|
1178
|
-
}
|
|
1179
|
-
catch (error) {
|
|
1180
|
-
console.error(`\n❌ Failed to find available port: ${error.message}`);
|
|
1181
|
-
process.exit(1);
|
|
1182
|
-
}
|
|
1183
|
-
// Start server
|
|
1184
|
-
this.server.listen(this.port, () => {
|
|
1185
|
-
console.log(`\n🌐 Documentation server running at http://localhost:${this.port}`);
|
|
1186
|
-
if (this.envResult?.hasRails && this.envResult?.hasNextjs) {
|
|
1187
|
-
console.log(' 📊 Multiple environments detected - use tabs to switch views');
|
|
1188
|
-
}
|
|
1189
|
-
console.log(' Press Ctrl+C to stop\n');
|
|
1190
|
-
});
|
|
1191
|
-
// Open browser
|
|
1192
|
-
if (openBrowser) {
|
|
1193
|
-
const open = (await import('open')).default;
|
|
1194
|
-
await open(`http://localhost:${this.port}`);
|
|
1195
|
-
}
|
|
1196
|
-
// Watch for changes
|
|
1197
|
-
if (this.config.watch.enabled) {
|
|
1198
|
-
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(", ")}`);
|
|
1199
1561
|
}
|
|
1562
|
+
}
|
|
1200
1563
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
console.error(`⚠️ Rails re-analysis failed:`, error.message);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
this.io.emit('reload');
|
|
1215
|
-
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
|
+
}
|
|
1216
1574
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
clearTimeout(timeout);
|
|
1229
|
-
timeout = setTimeout(async () => {
|
|
1230
|
-
await this.regenerate();
|
|
1231
|
-
}, this.config.watch.debounce);
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
})();
|
|
1235
|
-
}
|
|
1236
|
-
catch (error) {
|
|
1237
|
-
console.warn(`Warning: Could not watch directory ${dir}:`, error.message);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
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);
|
|
1240
1586
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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}`);
|
|
1244
1598
|
}
|
|
1245
|
-
|
|
1599
|
+
if (this.config.watch.enabled) {
|
|
1600
|
+
this.watchForChanges();
|
|
1601
|
+
}
|
|
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 };
|