@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.
Files changed (69) hide show
  1. package/dist/analyzers/index.d.ts +69 -5
  2. package/dist/analyzers/index.js +1 -5
  3. package/dist/chunk-3PWXDB7B.js +153 -0
  4. package/dist/{generators/page-map-generator.js → chunk-3YFXZAP7.js} +322 -358
  5. package/dist/chunk-6F4PWJZI.js +1 -0
  6. package/dist/{generators/rails-map-generator.js → chunk-E4WRODSI.js} +86 -94
  7. package/dist/chunk-GNBMJMET.js +2519 -0
  8. package/dist/{server/doc-server.js → chunk-M6YNU536.js} +702 -290
  9. package/dist/chunk-OWM6WNLE.js +2610 -0
  10. package/dist/chunk-SSU6QFTX.js +1058 -0
  11. package/dist/cli.d.ts +0 -1
  12. package/dist/cli.js +348 -452
  13. package/dist/dataflow-analyzer-BfAiqVUp.d.ts +180 -0
  14. package/dist/env-detector-EEMVUEIA.js +1 -0
  15. package/dist/generators/index.d.ts +431 -3
  16. package/dist/generators/index.js +2 -3
  17. package/dist/index.d.ts +53 -10
  18. package/dist/index.js +8 -11
  19. package/dist/page-map-generator-6MJGPBVA.js +1 -0
  20. package/dist/rails-UWSDRS33.js +1 -0
  21. package/dist/rails-map-generator-D2URLMVJ.js +2 -0
  22. package/dist/server/index.d.ts +33 -1
  23. package/dist/server/index.js +7 -1
  24. package/dist/types.d.ts +39 -37
  25. package/dist/types.js +1 -5
  26. package/package.json +5 -3
  27. package/dist/analyzers/base-analyzer.d.ts +0 -45
  28. package/dist/analyzers/base-analyzer.js +0 -47
  29. package/dist/analyzers/dataflow-analyzer.d.ts +0 -29
  30. package/dist/analyzers/dataflow-analyzer.js +0 -425
  31. package/dist/analyzers/graphql-analyzer.d.ts +0 -22
  32. package/dist/analyzers/graphql-analyzer.js +0 -386
  33. package/dist/analyzers/pages-analyzer.d.ts +0 -84
  34. package/dist/analyzers/pages-analyzer.js +0 -1695
  35. package/dist/analyzers/rails/index.d.ts +0 -46
  36. package/dist/analyzers/rails/index.js +0 -145
  37. package/dist/analyzers/rails/rails-controller-analyzer.d.ts +0 -82
  38. package/dist/analyzers/rails/rails-controller-analyzer.js +0 -478
  39. package/dist/analyzers/rails/rails-grpc-analyzer.d.ts +0 -44
  40. package/dist/analyzers/rails/rails-grpc-analyzer.js +0 -262
  41. package/dist/analyzers/rails/rails-model-analyzer.d.ts +0 -88
  42. package/dist/analyzers/rails/rails-model-analyzer.js +0 -493
  43. package/dist/analyzers/rails/rails-react-analyzer.d.ts +0 -41
  44. package/dist/analyzers/rails/rails-react-analyzer.js +0 -529
  45. package/dist/analyzers/rails/rails-routes-analyzer.d.ts +0 -62
  46. package/dist/analyzers/rails/rails-routes-analyzer.js +0 -540
  47. package/dist/analyzers/rails/rails-view-analyzer.d.ts +0 -49
  48. package/dist/analyzers/rails/rails-view-analyzer.js +0 -386
  49. package/dist/analyzers/rails/ruby-parser.d.ts +0 -63
  50. package/dist/analyzers/rails/ruby-parser.js +0 -212
  51. package/dist/analyzers/rest-api-analyzer.d.ts +0 -65
  52. package/dist/analyzers/rest-api-analyzer.js +0 -479
  53. package/dist/core/cache.d.ts +0 -47
  54. package/dist/core/cache.js +0 -151
  55. package/dist/core/engine.d.ts +0 -46
  56. package/dist/core/engine.js +0 -319
  57. package/dist/core/index.d.ts +0 -2
  58. package/dist/core/index.js +0 -2
  59. package/dist/generators/markdown-generator.d.ts +0 -25
  60. package/dist/generators/markdown-generator.js +0 -782
  61. package/dist/generators/mermaid-generator.d.ts +0 -35
  62. package/dist/generators/mermaid-generator.js +0 -364
  63. package/dist/generators/page-map-generator.d.ts +0 -22
  64. package/dist/generators/rails-map-generator.d.ts +0 -21
  65. package/dist/server/doc-server.d.ts +0 -30
  66. package/dist/utils/env-detector.d.ts +0 -31
  67. package/dist/utils/env-detector.js +0 -188
  68. package/dist/utils/parallel.d.ts +0 -23
  69. 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 { 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();
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
- 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
- });
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
- setupSocketIO() {
125
- this.io.on('connection', (socket) => {
126
- console.log('Client connected');
127
- socket.on('disconnect', () => {
128
- console.log('Client disconnected');
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
- 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>`;
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
- return this.getHtmlTemplate(content);
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
- 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
- }
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
- return JSON.stringify(ops);
357
+ }
173
358
  }
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
- }
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
- return JSON.stringify(calls);
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
- getHtmlTemplate(content) {
195
- const graphqlData = this.getGraphQLData();
196
- const apiCallsData = this.getApiCallsData();
197
- return `<!DOCTYPE html>
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='/'">📊 ${this.config.repositories[0]?.displayName || this.config.repositories[0]?.name || 'Repository'}</h1>
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
- .map((repo) => `
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
- .join('')}
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">← Back</button>
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()">×</button>
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="縮小">➖</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>
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}\u{270F}\u{FE0F}\u{1F504}]/gu, '').trim();
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:\s*/, '').trim();
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:\s*/, '').trim();
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):\s*/i, '').trim();
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('生成に失敗しました: ' + data.error);
1538
+ alert('\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ' + data.error);
1130
1539
  }
1131
1540
  } catch (e) {
1132
- alert('エラー: ' + e.message);
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
- 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();
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
- 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');
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
- 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
- }
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
- stop() {
1229
- this.server.close();
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 };