@nexus-ioc/graph-analyzer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/LICENSE +21 -0
  3. package/README.md +742 -0
  4. package/dist/analyzer/circular-dependency-detector.d.ts +94 -0
  5. package/dist/analyzer/circular-dependency-detector.js +203 -0
  6. package/dist/analyzer/circular-dependency-detector.js.map +1 -0
  7. package/dist/analyzer/module-depth-analyzer.d.ts +117 -0
  8. package/dist/analyzer/module-depth-analyzer.js +221 -0
  9. package/dist/analyzer/module-depth-analyzer.js.map +1 -0
  10. package/dist/analyzer/provider-scope-analyzer.d.ts +91 -0
  11. package/dist/analyzer/provider-scope-analyzer.js +137 -0
  12. package/dist/analyzer/provider-scope-analyzer.js.map +1 -0
  13. package/dist/analyzer/unused-provider-detector.d.ts +73 -0
  14. package/dist/analyzer/unused-provider-detector.js +147 -0
  15. package/dist/analyzer/unused-provider-detector.js.map +1 -0
  16. package/dist/cli.d.ts +2 -0
  17. package/dist/cli.js +537 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/config/config-loader.d.ts +27 -0
  20. package/dist/config/config-loader.js +205 -0
  21. package/dist/config/config-loader.js.map +1 -0
  22. package/dist/config/config-schema.d.ts +83 -0
  23. package/dist/config/config-schema.js +97 -0
  24. package/dist/config/config-schema.js.map +1 -0
  25. package/dist/index.d.ts +41 -0
  26. package/dist/index.js +50 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/interfaces/graph-output.interface.d.ts +126 -0
  29. package/dist/interfaces/graph-output.interface.js +3 -0
  30. package/dist/interfaces/graph-output.interface.js.map +1 -0
  31. package/dist/interfaces/providers.interface.d.ts +10 -0
  32. package/dist/interfaces/providers.interface.js +3 -0
  33. package/dist/interfaces/providers.interface.js.map +1 -0
  34. package/dist/parser/dependency-extractor.d.ts +88 -0
  35. package/dist/parser/dependency-extractor.js +334 -0
  36. package/dist/parser/dependency-extractor.js.map +1 -0
  37. package/dist/parser/parse-entry-file.d.ts +15 -0
  38. package/dist/parser/parse-entry-file.js +46 -0
  39. package/dist/parser/parse-entry-file.js.map +1 -0
  40. package/dist/parser/parse-imports.d.ts +15 -0
  41. package/dist/parser/parse-imports.js +65 -0
  42. package/dist/parser/parse-imports.js.map +1 -0
  43. package/dist/parser/parse-ns-module.d.ts +39 -0
  44. package/dist/parser/parse-ns-module.js +122 -0
  45. package/dist/parser/parse-ns-module.js.map +1 -0
  46. package/dist/parser/parse-provider.d.ts +2 -0
  47. package/dist/parser/parse-provider.js +7 -0
  48. package/dist/parser/parse-provider.js.map +1 -0
  49. package/dist/parser/parse-ts-config.d.ts +10 -0
  50. package/dist/parser/parse-ts-config.js +40 -0
  51. package/dist/parser/parse-ts-config.js.map +1 -0
  52. package/dist/parser/parser-rec.d.ts +1 -0
  53. package/dist/parser/parser-rec.js +52 -0
  54. package/dist/parser/parser-rec.js.map +1 -0
  55. package/dist/parser/parser.d.ts +1 -0
  56. package/dist/parser/parser.js +61 -0
  57. package/dist/parser/parser.js.map +1 -0
  58. package/dist/parser/provider/class-parser.d.ts +20 -0
  59. package/dist/parser/provider/class-parser.js +137 -0
  60. package/dist/parser/provider/class-parser.js.map +1 -0
  61. package/dist/parser/provider/providers-parser.d.ts +9 -0
  62. package/dist/parser/provider/providers-parser.js +32 -0
  63. package/dist/parser/provider/providers-parser.js.map +1 -0
  64. package/dist/parser/provider/use-class-parser.d.ts +25 -0
  65. package/dist/parser/provider/use-class-parser.js +161 -0
  66. package/dist/parser/provider/use-class-parser.js.map +1 -0
  67. package/dist/parser/provider/use-factory-parser.d.ts +15 -0
  68. package/dist/parser/provider/use-factory-parser.js +53 -0
  69. package/dist/parser/provider/use-factory-parser.js.map +1 -0
  70. package/dist/parser/provider/use-value-parser.d.ts +13 -0
  71. package/dist/parser/provider/use-value-parser.js +41 -0
  72. package/dist/parser/provider/use-value-parser.js.map +1 -0
  73. package/dist/parser/rules.d.ts +10 -0
  74. package/dist/parser/rules.js +37 -0
  75. package/dist/parser/rules.js.map +1 -0
  76. package/dist/utils/error-formatter.d.ts +27 -0
  77. package/dist/utils/error-formatter.js +177 -0
  78. package/dist/utils/error-formatter.js.map +1 -0
  79. package/dist/utils/validator.d.ts +45 -0
  80. package/dist/utils/validator.js +252 -0
  81. package/dist/utils/validator.js.map +1 -0
  82. package/dist/visualize/generator.d.ts +26 -0
  83. package/dist/visualize/generator.js +75 -0
  84. package/dist/visualize/generator.js.map +1 -0
  85. package/dist/visualize/graph-analyzer.d.ts +126 -0
  86. package/dist/visualize/graph-analyzer.js +199 -0
  87. package/dist/visualize/graph-analyzer.js.map +1 -0
  88. package/dist/visualize/html-generator.d.ts +71 -0
  89. package/dist/visualize/html-generator.js +1312 -0
  90. package/dist/visualize/html-generator.js.map +1 -0
  91. package/dist/visualize/json-formatter.d.ts +106 -0
  92. package/dist/visualize/json-formatter.js +252 -0
  93. package/dist/visualize/json-formatter.js.map +1 -0
  94. package/docs/API_REFERENCE.md +747 -0
  95. package/docs/ARCHITECTURE.md +486 -0
  96. package/docs/CLI_GUIDE.md +576 -0
  97. package/docs/HTML_VISUALIZATION.md +365 -0
  98. package/package.json +85 -0
@@ -0,0 +1,1312 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HtmlGenerator = void 0;
4
+ const json_formatter_1 = require("./json-formatter");
5
+ /**
6
+ * Generates interactive HTML visualizations of dependency graphs
7
+ *
8
+ * Creates a self-contained HTML file with an interactive graph using Cytoscape.js.
9
+ * The graph includes modules and providers with clickable nodes, zoom/pan capabilities,
10
+ * and detailed information panels.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const generator = new HtmlGenerator(modulesGraph, 'src/main.ts', {
15
+ * ideProtocol: 'vscode',
16
+ * title: 'My App Dependencies',
17
+ * darkTheme: true
18
+ * });
19
+ *
20
+ * const html = generator.generate();
21
+ * fs.writeFileSync('graph.html', html);
22
+ * ```
23
+ */
24
+ class HtmlGenerator {
25
+ constructor(graph, entryPoint, options = {}) {
26
+ this.graph = graph;
27
+ this.entryPoint = entryPoint;
28
+ this.options = options;
29
+ this.formatter = new json_formatter_1.JsonFormatter(graph, entryPoint);
30
+ }
31
+ /**
32
+ * Generate the complete HTML document
33
+ *
34
+ * @returns Complete HTML string ready to be written to a file
35
+ */
36
+ generate() {
37
+ const graphData = this.formatter.format();
38
+ const cytoscapeData = this.convertToCytoscapeFormat(graphData);
39
+ return this.generateHtmlTemplate(graphData, cytoscapeData);
40
+ }
41
+ /**
42
+ * Convert GraphOutput to Cytoscape.js format with compound nodes
43
+ */
44
+ convertToCytoscapeFormat(graphData) {
45
+ const nodes = [];
46
+ const edges = [];
47
+ // Create a set of existing module names for validation
48
+ const existingModules = new Set(graphData.modules.map((module) => module.name));
49
+ // Add module nodes (these will be parent/compound nodes)
50
+ for (const module of graphData.modules) {
51
+ nodes.push({
52
+ data: {
53
+ id: `module-${module.name}`,
54
+ label: module.name,
55
+ type: "module",
56
+ path: module.path,
57
+ isGlobal: module.isGlobal,
58
+ imports: module.imports,
59
+ exports: module.exports,
60
+ providers: module.providers,
61
+ },
62
+ classes: ["module", module.isGlobal ? "global" : ""],
63
+ });
64
+ // Add edges for module imports (only for modules that exist in the graph)
65
+ for (const importedModule of module.imports) {
66
+ // Only create edge if the target module exists in the graph
67
+ if (existingModules.has(importedModule)) {
68
+ edges.push({
69
+ data: {
70
+ id: `edge-${module.name}-${importedModule}`,
71
+ source: `module-${module.name}`,
72
+ target: `module-${importedModule}`,
73
+ type: "import",
74
+ },
75
+ classes: ["import"],
76
+ });
77
+ }
78
+ }
79
+ }
80
+ // Create a map of provider tokens to their full node IDs for dependency resolution
81
+ const providerMap = new Map();
82
+ for (const provider of graphData.providers) {
83
+ const nodeId = `provider-${provider.module}-${provider.token}`;
84
+ providerMap.set(provider.token, nodeId);
85
+ }
86
+ // Create a map of module names to their node IDs for export resolution
87
+ const moduleMap = new Map();
88
+ for (const module of graphData.modules) {
89
+ const nodeId = `module-${module.name}`;
90
+ moduleMap.set(module.name, nodeId);
91
+ }
92
+ // Add provider nodes as children of their parent modules
93
+ for (const provider of graphData.providers) {
94
+ const nodeId = `provider-${provider.module}-${provider.token}`;
95
+ // Check if provider has dependencies without explicit @Inject decorators
96
+ const hasMissingDecorators = provider.dependencies?.some((dep) => dep.hasExplicitDecorator === false) ?? false;
97
+ const classes = ["provider", provider.type.toLowerCase()];
98
+ if (hasMissingDecorators) {
99
+ classes.push("missing-decorator");
100
+ }
101
+ nodes.push({
102
+ data: {
103
+ id: nodeId,
104
+ label: provider.token,
105
+ type: "provider",
106
+ providerType: provider.type,
107
+ module: provider.module,
108
+ scope: provider.scope,
109
+ dependencies: provider.dependencies,
110
+ value: provider.value,
111
+ factory: provider.factory,
112
+ useClass: provider.useClass,
113
+ hasMissingDecorators,
114
+ // Set parent to create compound node structure
115
+ parent: `module-${provider.module}`,
116
+ },
117
+ classes,
118
+ });
119
+ // Note: We no longer need "provides" edges since providers are children of modules
120
+ // Add edges for provider dependencies (only for providers that exist in the graph)
121
+ if (provider.dependencies && provider.dependencies.length > 0) {
122
+ for (const dep of provider.dependencies) {
123
+ if (dep.token) {
124
+ // Find the provider node for this dependency
125
+ const depNodeId = providerMap.get(dep.token);
126
+ // Only create edge if the target provider exists in the graph
127
+ if (depNodeId) {
128
+ edges.push({
129
+ data: {
130
+ id: `edge-${nodeId}-${depNodeId}`,
131
+ source: nodeId,
132
+ target: depNodeId,
133
+ type: "dependency",
134
+ optional: dep.optional,
135
+ },
136
+ classes: ["dependency", dep.optional ? "optional" : ""],
137
+ });
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ // Convert Maps to plain objects for JSON serialization
144
+ const providerMapObject = {};
145
+ providerMap.forEach((nodeId, token) => {
146
+ providerMapObject[token] = nodeId;
147
+ });
148
+ const moduleMapObject = {};
149
+ moduleMap.forEach((nodeId, name) => {
150
+ moduleMapObject[name] = nodeId;
151
+ });
152
+ return {
153
+ nodes,
154
+ edges,
155
+ providerMap: providerMapObject,
156
+ moduleMap: moduleMapObject,
157
+ };
158
+ }
159
+ /**
160
+ * Get IDE URL for a file path
161
+ */
162
+ getIdeUrl(filePath, line = 1, column = 1) {
163
+ const protocol = this.options.ideProtocol || "vscode";
164
+ if (this.options.customIdeUrl) {
165
+ return this.options.customIdeUrl
166
+ .replace("{path}", filePath)
167
+ .replace("{line}", String(line))
168
+ .replace("{column}", String(column));
169
+ }
170
+ switch (protocol) {
171
+ case "vscode":
172
+ return `vscode://file/${filePath}:${line}:${column}`;
173
+ case "webstorm":
174
+ case "idea":
175
+ return `idea://open?file=${filePath}&line=${line}&column=${column}`;
176
+ default:
177
+ return `vscode://file/${filePath}:${line}:${column}`;
178
+ }
179
+ }
180
+ /**
181
+ * Generate warning section for providers with missing @Inject decorators
182
+ */
183
+ generateMissingDecoratorsWarning(graphData) {
184
+ const providersWithMissingDecorators = graphData.providers.filter((provider) => provider.dependencies?.some((dep) => dep.hasExplicitDecorator === false));
185
+ if (providersWithMissingDecorators.length === 0) {
186
+ return "";
187
+ }
188
+ const providersList = providersWithMissingDecorators
189
+ .map((provider) => {
190
+ const missingDeps = provider.dependencies?.filter((dep) => dep.hasExplicitDecorator === false) || [];
191
+ return `<li><strong>${provider.token}</strong> (${provider.module}): ${missingDeps.map((d) => d.token).join(", ")}</li>`;
192
+ })
193
+ .join("");
194
+ return `
195
+ <div style="background: #fef2f2; border-left: 4px solid #ef4444; border-radius: 8px; padding: 1.25rem 1.5rem; margin-top: 1.25rem; box-shadow: 0 2px 8px rgba(239, 68, 68, 0.15);">
196
+ <div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem;">
197
+ <span style="font-size: 1.75rem;">⚠️</span>
198
+ <strong style="color: #dc2626; font-size: 1.15rem; font-weight: 700;">Missing @Inject Decorators Detected</strong>
199
+ </div>
200
+ <p style="color: #991b1b; margin-bottom: 1rem; font-size: 0.95rem; line-height: 1.6;">
201
+ <strong>${providersWithMissingDecorators.length}</strong> provider(s) have dependencies without explicit @Inject decorators.
202
+ This may cause runtime errors in production. Affected providers:
203
+ </p>
204
+ <ul style="color: #991b1b; font-size: 0.9rem; margin-left: 1.5rem; line-height: 1.8;">
205
+ ${providersList}
206
+ </ul>
207
+ </div>
208
+ `;
209
+ }
210
+ /**
211
+ * Generate the complete HTML template
212
+ */
213
+ generateHtmlTemplate(graphData, cytoscapeData) {
214
+ const title = this.options.title || "Dependency Graph";
215
+ const isDark = this.options.darkTheme || false;
216
+ return `<!DOCTYPE html>
217
+ <html lang="en">
218
+ <head>
219
+ <meta charset="UTF-8">
220
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
221
+ <title>${title}</title>
222
+ <script src="https://unpkg.com/cytoscape@3.28.1/dist/cytoscape.min.js"></script>
223
+ <style>
224
+ ${this.generateStyles(isDark)}
225
+ </style>
226
+ </head>
227
+ <body>
228
+ <div class="container">
229
+ <header>
230
+ <h1>${title}</h1>
231
+ <div class="metadata">
232
+ <span>Entry Point: <code>${graphData.metadata.entryPoint}</code></span>
233
+ <span>Modules: <strong>${graphData.metadata.totalModules}</strong></span>
234
+ <span>Providers: <strong>${graphData.metadata.totalProviders}</strong></span>
235
+ <span>Analyzed: ${new Date(graphData.metadata.analyzedAt).toLocaleString()}</span>
236
+ </div>
237
+ ${this.generateMissingDecoratorsWarning(graphData)}
238
+ </header>
239
+
240
+ <div class="controls">
241
+ <div class="control-group">
242
+ <label>View Mode:</label>
243
+ <button id="view-all" class="active">All</button>
244
+ <button id="view-modules">Modules Only</button>
245
+ <button id="view-providers">Providers Only</button>
246
+ </div>
247
+ <div class="control-group">
248
+ <label>Search:</label>
249
+ <input type="text" id="search" placeholder="Search modules or providers..." />
250
+ </div>
251
+ <div class="control-group">
252
+ <button id="reset-zoom">Reset View</button>
253
+ <button id="fit-graph">Fit to Screen</button>
254
+ </div>
255
+ </div>
256
+
257
+ <div class="main-content">
258
+ <div id="cy"></div>
259
+ <div id="info-panel" class="info-panel">
260
+ <h3>Node Information</h3>
261
+ <p class="placeholder">Click on a node to see details</p>
262
+ <div id="node-details" style="display: none;"></div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+
267
+ <script>
268
+ ${this.generateScript(graphData, cytoscapeData)}
269
+ </script>
270
+ </body>
271
+ </html>`;
272
+ }
273
+ /**
274
+ * Generate CSS styles
275
+ */
276
+ generateStyles(isDark) {
277
+ // Enhanced color palette for better accessibility and visual appeal
278
+ const bgColor = isDark ? "#1a1a1a" : "#f8f9fa";
279
+ const textColor = isDark ? "#e0e0e0" : "#2c3e50";
280
+ const borderColor = isDark ? "#404040" : "#dee2e6";
281
+ const panelBg = isDark ? "#242424" : "#ffffff";
282
+ const accentColor = isDark ? "#3b82f6" : "#2563eb";
283
+ const accentHover = isDark ? "#60a5fa" : "#1d4ed8";
284
+ const mutedText = isDark ? "#9ca3af" : "#6b7280";
285
+ const cardBg = isDark ? "#2d2d2d" : "#ffffff";
286
+ const shadowColor = isDark ? "rgba(0, 0, 0, 0.5)" : "rgba(0, 0, 0, 0.1)";
287
+ return `
288
+ * {
289
+ margin: 0;
290
+ padding: 0;
291
+ box-sizing: border-box;
292
+ }
293
+
294
+ body {
295
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
296
+ background: ${bgColor};
297
+ color: ${textColor};
298
+ overflow: hidden;
299
+ line-height: 1.6;
300
+ }
301
+
302
+ .container {
303
+ display: flex;
304
+ flex-direction: column;
305
+ height: 100vh;
306
+ }
307
+
308
+ header {
309
+ padding: 1.5rem 2rem;
310
+ border-bottom: 2px solid ${borderColor};
311
+ background: ${panelBg};
312
+ box-shadow: 0 2px 8px ${shadowColor};
313
+ }
314
+
315
+ header h1 {
316
+ font-size: 1.75rem;
317
+ font-weight: 700;
318
+ margin-bottom: 0.75rem;
319
+ color: ${textColor};
320
+ letter-spacing: -0.025em;
321
+ }
322
+
323
+ .metadata {
324
+ display: flex;
325
+ gap: 2rem;
326
+ font-size: 0.9rem;
327
+ color: ${mutedText};
328
+ flex-wrap: wrap;
329
+ }
330
+
331
+ .metadata span {
332
+ display: flex;
333
+ align-items: center;
334
+ gap: 0.5rem;
335
+ }
336
+
337
+ .metadata strong {
338
+ color: ${accentColor};
339
+ font-weight: 600;
340
+ }
341
+
342
+ .metadata code {
343
+ background: ${isDark ? "#1a1a1a" : "#f1f3f5"};
344
+ padding: 0.25rem 0.5rem;
345
+ border-radius: 4px;
346
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
347
+ font-size: 0.85rem;
348
+ border: 1px solid ${borderColor};
349
+ }
350
+
351
+ .controls {
352
+ padding: 1.25rem 2rem;
353
+ border-bottom: 1px solid ${borderColor};
354
+ display: flex;
355
+ gap: 2.5rem;
356
+ align-items: center;
357
+ background: ${panelBg};
358
+ flex-wrap: wrap;
359
+ }
360
+
361
+ .control-group {
362
+ display: flex;
363
+ gap: 0.75rem;
364
+ align-items: center;
365
+ }
366
+
367
+ .control-group label {
368
+ font-weight: 600;
369
+ font-size: 0.9rem;
370
+ color: ${mutedText};
371
+ text-transform: uppercase;
372
+ letter-spacing: 0.05em;
373
+ font-size: 0.75rem;
374
+ }
375
+
376
+ button {
377
+ padding: 0.625rem 1.25rem;
378
+ border: 1px solid ${borderColor};
379
+ background: ${cardBg};
380
+ color: ${textColor};
381
+ border-radius: 6px;
382
+ cursor: pointer;
383
+ font-size: 0.9rem;
384
+ font-weight: 500;
385
+ transition: all 0.2s ease;
386
+ box-shadow: 0 1px 3px ${shadowColor};
387
+ }
388
+
389
+ button:hover {
390
+ background: ${isDark ? "#3a3a3a" : "#f1f3f5"};
391
+ transform: translateY(-1px);
392
+ box-shadow: 0 2px 6px ${shadowColor};
393
+ }
394
+
395
+ button:active {
396
+ transform: translateY(0);
397
+ box-shadow: 0 1px 2px ${shadowColor};
398
+ }
399
+
400
+ button.active {
401
+ background: ${accentColor};
402
+ color: white;
403
+ border-color: ${accentColor};
404
+ box-shadow: 0 2px 6px ${isDark ? "rgba(59, 130, 246, 0.4)" : "rgba(37, 99, 235, 0.3)"};
405
+ }
406
+
407
+ button.active:hover {
408
+ background: ${accentHover};
409
+ border-color: ${accentHover};
410
+ }
411
+
412
+ input[type="text"] {
413
+ padding: 0.625rem 1rem;
414
+ border: 1px solid ${borderColor};
415
+ background: ${cardBg};
416
+ color: ${textColor};
417
+ border-radius: 6px;
418
+ font-size: 0.9rem;
419
+ min-width: 300px;
420
+ transition: all 0.2s ease;
421
+ box-shadow: 0 1px 3px ${shadowColor};
422
+ }
423
+
424
+ input[type="text"]:focus {
425
+ outline: none;
426
+ border-color: ${accentColor};
427
+ box-shadow: 0 0 0 3px ${isDark ? "rgba(59, 130, 246, 0.2)" : "rgba(37, 99, 235, 0.1)"};
428
+ }
429
+
430
+ input[type="text"]::placeholder {
431
+ color: ${mutedText};
432
+ }
433
+
434
+ .main-content {
435
+ display: flex;
436
+ flex: 1;
437
+ overflow: hidden;
438
+ }
439
+
440
+ #cy {
441
+ flex: 1;
442
+ background: ${bgColor};
443
+ }
444
+
445
+ .info-panel {
446
+ width: 380px;
447
+ border-left: 2px solid ${borderColor};
448
+ background: ${panelBg};
449
+ padding: 2rem;
450
+ overflow-y: auto;
451
+ box-shadow: -2px 0 8px ${shadowColor};
452
+ }
453
+
454
+ .info-panel h3 {
455
+ margin-bottom: 1.5rem;
456
+ font-size: 1.25rem;
457
+ font-weight: 700;
458
+ color: ${textColor};
459
+ letter-spacing: -0.025em;
460
+ }
461
+
462
+ .info-panel .placeholder {
463
+ color: ${mutedText};
464
+ font-style: italic;
465
+ text-align: center;
466
+ padding: 2rem 0;
467
+ }
468
+
469
+ .info-item {
470
+ margin-bottom: 1.5rem;
471
+ padding-bottom: 1.5rem;
472
+ border-bottom: 1px solid ${borderColor};
473
+ }
474
+
475
+ .info-item:last-child {
476
+ border-bottom: none;
477
+ }
478
+
479
+ .info-item label {
480
+ display: block;
481
+ font-weight: 700;
482
+ margin-bottom: 0.5rem;
483
+ font-size: 0.85rem;
484
+ color: ${mutedText};
485
+ text-transform: uppercase;
486
+ letter-spacing: 0.05em;
487
+ }
488
+
489
+ .info-item .value {
490
+ font-size: 1rem;
491
+ line-height: 1.6;
492
+ }
493
+
494
+ .info-item code {
495
+ background: ${isDark ? "#1a1a1a" : "#f1f3f5"};
496
+ padding: 0.25rem 0.5rem;
497
+ border-radius: 4px;
498
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
499
+ font-size: 0.875rem;
500
+ border: 1px solid ${borderColor};
501
+ word-break: break-all;
502
+ }
503
+
504
+ .info-item a {
505
+ color: ${accentColor};
506
+ text-decoration: none;
507
+ font-weight: 500;
508
+ transition: color 0.2s ease;
509
+ }
510
+
511
+ .info-item a:hover {
512
+ color: ${accentHover};
513
+ text-decoration: underline;
514
+ }
515
+
516
+ .badge {
517
+ display: inline-block;
518
+ padding: 0.375rem 0.75rem;
519
+ border-radius: 6px;
520
+ font-size: 0.75rem;
521
+ font-weight: 700;
522
+ margin-right: 0.5rem;
523
+ text-transform: uppercase;
524
+ letter-spacing: 0.05em;
525
+ box-shadow: 0 1px 3px ${shadowColor};
526
+ }
527
+
528
+ .badge.module { background: #3b82f6; color: white; }
529
+ .badge.provider { background: #10b981; color: white; }
530
+ .badge.class { background: #3b82f6; color: white; }
531
+ .badge.usevalue { background: #10b981; color: white; }
532
+ .badge.usefactory { background: #f59e0b; color: white; }
533
+ .badge.useclass { background: #eab308; color: #1a1a1a; }
534
+ .badge.global { background: #a855f7; color: white; }
535
+
536
+ .dependency-list {
537
+ list-style: none;
538
+ padding-left: 0;
539
+ }
540
+
541
+ .dependency-list li {
542
+ padding: 0.5rem 0;
543
+ font-size: 0.9rem;
544
+ line-height: 1.6;
545
+ }
546
+
547
+ .dependency-list li::before {
548
+ content: "→ ";
549
+ color: ${accentColor};
550
+ font-weight: bold;
551
+ margin-right: 0.25rem;
552
+ }
553
+
554
+ .node-link {
555
+ color: ${accentColor};
556
+ text-decoration: none;
557
+ cursor: pointer;
558
+ transition: all 0.2s ease;
559
+ font-weight: 500;
560
+ }
561
+
562
+ .node-link:hover {
563
+ color: ${accentHover};
564
+ text-decoration: underline;
565
+ }
566
+
567
+ .node-link code {
568
+ color: inherit;
569
+ }
570
+
571
+ /* Scrollbar styling */
572
+ .info-panel::-webkit-scrollbar {
573
+ width: 8px;
574
+ }
575
+
576
+ .info-panel::-webkit-scrollbar-track {
577
+ background: ${isDark ? "#1a1a1a" : "#f1f3f5"};
578
+ }
579
+
580
+ .info-panel::-webkit-scrollbar-thumb {
581
+ background: ${isDark ? "#404040" : "#cbd5e0"};
582
+ border-radius: 4px;
583
+ }
584
+
585
+ .info-panel::-webkit-scrollbar-thumb:hover {
586
+ background: ${isDark ? "#505050" : "#a0aec0"};
587
+ }
588
+ `;
589
+ }
590
+ /**
591
+ * Generate JavaScript code
592
+ */
593
+ generateScript(_graphData, cytoscapeData) {
594
+ const ideUrlTemplate = this.getIdeUrl("{path}", 1, 1);
595
+ return `
596
+ const graphData = ${JSON.stringify({ nodes: cytoscapeData.nodes, edges: cytoscapeData.edges }, null, 2)};
597
+ const providerMap = ${JSON.stringify(cytoscapeData.providerMap, null, 2)};
598
+ const moduleMap = ${JSON.stringify(cytoscapeData.moduleMap, null, 2)};
599
+
600
+ // Initialize Cytoscape
601
+ const cy = cytoscape({
602
+ container: document.getElementById('cy'),
603
+ elements: graphData,
604
+
605
+ // User interaction options
606
+ userPanningEnabled: true,
607
+ userZoomingEnabled: true,
608
+ boxSelectionEnabled: false,
609
+
610
+ // Panning configuration - require actual drag movement
611
+ panningEnabled: true,
612
+
613
+ style: [
614
+ {
615
+ selector: 'node',
616
+ style: {
617
+ 'label': 'data(label)',
618
+ 'text-valign': 'center',
619
+ 'text-halign': 'center',
620
+ 'font-size': '13px',
621
+ 'font-weight': '700',
622
+ 'color': '#1a1a1a',
623
+ 'text-outline-width': 3,
624
+ 'text-outline-color': '#ffffff',
625
+ 'width': '90px',
626
+ 'height': '90px',
627
+ 'border-width': 3,
628
+ 'border-color': '#64748b',
629
+ 'text-wrap': 'wrap',
630
+ 'text-max-width': '80px'
631
+ }
632
+ },
633
+ {
634
+ // Parent/compound nodes (modules containing providers)
635
+ selector: ':parent',
636
+ style: {
637
+ 'text-valign': 'top',
638
+ 'text-halign': 'center',
639
+ 'text-margin-y': -12,
640
+ 'background-opacity': 0.08,
641
+ 'background-color': '#3b82f6',
642
+ 'border-width': 3,
643
+ 'border-color': '#2563eb',
644
+ 'border-opacity': 0.6,
645
+ 'padding': '15px',
646
+ 'shape': 'roundrectangle',
647
+ 'font-size': '15px',
648
+ 'font-weight': 'bold',
649
+ 'compound-sizing-wrt-labels': 'include',
650
+ 'min-width': '100px',
651
+ 'min-height': '80px',
652
+ 'color': '#1e40af',
653
+ 'text-outline-width': 2,
654
+ 'text-outline-color': '#ffffff'
655
+ }
656
+ },
657
+ {
658
+ selector: ':parent.global',
659
+ style: {
660
+ 'background-color': '#a855f7',
661
+ 'border-color': '#9333ea',
662
+ 'border-width': 4,
663
+ 'color': '#7e22ce'
664
+ }
665
+ },
666
+ {
667
+ // Module nodes without children (standalone modules)
668
+ selector: 'node.module',
669
+ style: {
670
+ 'background-color': '#3b82f6',
671
+ 'border-color': '#2563eb',
672
+ 'shape': 'roundrectangle',
673
+ 'width': '140px',
674
+ 'height': '70px',
675
+ 'color': '#ffffff',
676
+ 'text-outline-width': 0,
677
+ 'font-size': '14px'
678
+ }
679
+ },
680
+ {
681
+ selector: 'node.module.global',
682
+ style: {
683
+ 'background-color': '#a855f7',
684
+ 'border-width': 4,
685
+ 'border-color': '#9333ea'
686
+ }
687
+ },
688
+ {
689
+ selector: 'node.provider',
690
+ style: {
691
+ 'shape': 'ellipse',
692
+ 'width': '95px',
693
+ 'height': '95px',
694
+ 'font-size': '12px',
695
+ 'text-wrap': 'wrap',
696
+ 'text-max-width': '85px'
697
+ }
698
+ },
699
+ {
700
+ selector: 'node.provider.class',
701
+ style: {
702
+ 'background-color': '#10b981',
703
+ 'border-color': '#059669',
704
+ 'border-width': 3,
705
+ 'color': '#ffffff',
706
+ 'text-outline-width': 0
707
+ }
708
+ },
709
+ {
710
+ selector: 'node.provider.usevalue',
711
+ style: {
712
+ 'background-color': '#10b981',
713
+ 'border-color': '#059669',
714
+ 'border-width': 3,
715
+ 'color': '#ffffff',
716
+ 'text-outline-width': 0
717
+ }
718
+ },
719
+ {
720
+ selector: 'node.provider.usefactory',
721
+ style: {
722
+ 'background-color': '#f59e0b',
723
+ 'border-color': '#d97706',
724
+ 'border-width': 3,
725
+ 'color': '#ffffff',
726
+ 'text-outline-width': 0
727
+ }
728
+ },
729
+ {
730
+ selector: 'node.provider.useclass',
731
+ style: {
732
+ 'background-color': '#eab308',
733
+ 'border-color': '#ca8a04',
734
+ 'border-width': 3,
735
+ 'color': '#1a1a1a',
736
+ 'text-outline-width': 2,
737
+ 'text-outline-color': '#ffffff'
738
+ }
739
+ },
740
+ {
741
+ // Providers with missing @Inject decorators (error state)
742
+ selector: 'node.provider.missing-decorator',
743
+ style: {
744
+ 'border-width': 4,
745
+ 'border-color': '#ef4444',
746
+ 'border-style': 'double'
747
+ }
748
+ },
749
+ {
750
+ selector: 'edge',
751
+ style: {
752
+ 'width': 2.5,
753
+ 'curve-style': 'bezier',
754
+ 'target-arrow-shape': 'triangle',
755
+ 'arrow-scale': 1.8,
756
+ 'opacity': 0.85
757
+ }
758
+ },
759
+ {
760
+ selector: 'edge.import',
761
+ style: {
762
+ 'line-color': '#64748b',
763
+ 'target-arrow-color': '#64748b',
764
+ 'width': 2.5
765
+ }
766
+ },
767
+ {
768
+ selector: 'edge.provides',
769
+ style: {
770
+ 'line-color': '#10b981',
771
+ 'target-arrow-color': '#10b981',
772
+ 'line-style': 'dashed',
773
+ 'width': 2.5
774
+ }
775
+ },
776
+ {
777
+ selector: 'edge.dependency',
778
+ style: {
779
+ 'line-color': '#f59e0b',
780
+ 'target-arrow-color': '#f59e0b',
781
+ 'width': 3
782
+ }
783
+ },
784
+ {
785
+ selector: 'edge.dependency.optional',
786
+ style: {
787
+ 'line-style': 'dotted',
788
+ 'opacity': 0.6
789
+ }
790
+ },
791
+ {
792
+ selector: 'edge.dimmed',
793
+ style: {
794
+ 'opacity': 0.1,
795
+ 'z-index': 0
796
+ }
797
+ },
798
+ {
799
+ selector: 'node.dimmed',
800
+ style: {
801
+ 'opacity': 0.3
802
+ }
803
+ },
804
+ {
805
+ selector: 'node.highlighted',
806
+ style: {
807
+ 'border-width': 6,
808
+ 'border-color': '#FF8C00', // Bright orange for maximum visibility
809
+ 'background-color': '#FFD700', // Bright gold/yellow
810
+ 'opacity': 1,
811
+ 'z-index': 999, // Bring to front
812
+ 'text-outline-color': '#000',
813
+ 'text-outline-width': 2,
814
+ 'font-weight': 'bold',
815
+ 'font-size': '14px'
816
+ }
817
+ },
818
+ {
819
+ selector: ':parent.highlighted',
820
+ style: {
821
+ 'border-width': 8,
822
+ 'border-color': '#FF8C00', // Bright orange
823
+ 'background-opacity': 0.2,
824
+ 'background-color': '#FFD700',
825
+ 'opacity': 1,
826
+ 'z-index': 998
827
+ }
828
+ },
829
+ {
830
+ selector: 'edge.highlighted',
831
+ style: {
832
+ 'width': 6, // Thicker edges for better visibility
833
+ 'line-color': '#FF8C00', // Bright orange
834
+ 'target-arrow-color': '#FF8C00',
835
+ 'opacity': 1,
836
+ 'z-index': 999,
837
+ 'arrow-scale': 2 // Larger arrows
838
+ }
839
+ }
840
+ ],
841
+ layout: {
842
+ name: 'cose',
843
+ animate: true,
844
+ animationDuration: 600,
845
+ animationEasing: 'ease-out',
846
+ nodeRepulsion: 10000,
847
+ idealEdgeLength: 120,
848
+ edgeElasticity: 120,
849
+ nestingFactor: 1.8,
850
+ gravity: 0.8,
851
+ numIter: 1200,
852
+ initialTemp: 250,
853
+ coolingFactor: 0.95,
854
+ minTemp: 1.0,
855
+ // Compound node specific options
856
+ componentSpacing: 120,
857
+ nodeOverlap: 15,
858
+ padding: 25
859
+ }
860
+ });
861
+
862
+ // Hide dependency edges by default (show only module structure)
863
+ cy.edges('.dependency').hide();
864
+
865
+ // Fix for unwanted panning on click: Track mouse movement to distinguish click from drag
866
+ let mouseDownPos = null;
867
+ let isDragging = false;
868
+ const DRAG_THRESHOLD = 5; // pixels - minimum movement to be considered a drag
869
+
870
+ cy.on('mousedown', function(evt) {
871
+ if (evt.target === cy) {
872
+ // Store initial mouse position when clicking on background
873
+ mouseDownPos = { x: evt.position.x, y: evt.position.y };
874
+ isDragging = false;
875
+ }
876
+ });
877
+
878
+ cy.on('mousemove', function(evt) {
879
+ if (mouseDownPos) {
880
+ // Calculate distance moved
881
+ const dx = evt.position.x - mouseDownPos.x;
882
+ const dy = evt.position.y - mouseDownPos.y;
883
+ const distance = Math.sqrt(dx * dx + dy * dy);
884
+
885
+ // If moved more than threshold, it's a drag
886
+ if (distance > DRAG_THRESHOLD) {
887
+ isDragging = true;
888
+ }
889
+ }
890
+ });
891
+
892
+ cy.on('mouseup', function(evt) {
893
+ mouseDownPos = null;
894
+ isDragging = false;
895
+ });
896
+
897
+ // Track currently selected node for edge filtering
898
+ let selectedNode = null;
899
+
900
+ // Node click handler
901
+ cy.on('tap', 'node', function(evt) {
902
+ const node = evt.target;
903
+ const data = node.data();
904
+
905
+ // Store the selected node
906
+ selectedNode = node;
907
+
908
+ // Clear previous highlights and dimming
909
+ cy.elements().removeClass('highlighted dimmed');
910
+
911
+ if (data.type === 'module') {
912
+ // MODULE SELECTION: Show only edges related to this module
913
+
914
+ // Get all edges connected to this module
915
+ const moduleEdges = node.connectedEdges('.import');
916
+
917
+ // Hide all edges first, then show only the module's edges
918
+ cy.edges().hide();
919
+
920
+ // Show and highlight only the module's import/export edges
921
+ moduleEdges.show().addClass('highlighted');
922
+
923
+ // Highlight the selected module
924
+ node.addClass('highlighted');
925
+
926
+ // Highlight connected modules
927
+ moduleEdges.connectedNodes('[type="module"]').addClass('highlighted');
928
+
929
+ // Dim all other nodes except highlighted ones
930
+ cy.nodes().not('.highlighted').addClass('dimmed');
931
+
932
+ } else if (data.type === 'provider') {
933
+ // PROVIDER SELECTION: Show only edges related to this provider
934
+
935
+ // Get all dependency edges connected to this provider
936
+ const providerEdges = node.connectedEdges('.dependency');
937
+
938
+ // Hide all edges first, then show only the provider's edges
939
+ cy.edges().hide();
940
+
941
+ // Show and highlight only the provider's dependency edges
942
+ providerEdges.show().addClass('highlighted');
943
+
944
+ // Highlight the selected provider
945
+ node.addClass('highlighted');
946
+
947
+ // Highlight connected providers
948
+ providerEdges.connectedNodes('[type="provider"]').addClass('highlighted');
949
+
950
+ // Dim all other nodes except highlighted ones
951
+ cy.nodes().not('.highlighted').addClass('dimmed');
952
+ }
953
+
954
+ // Show node details
955
+ showNodeDetails(data);
956
+ });
957
+
958
+ // Canvas click handler for deselection
959
+ cy.on('tap', function(evt) {
960
+ // Check if the tap was on the background (not on a node or edge)
961
+ // AND it wasn't a drag operation
962
+ if (evt.target === cy && !isDragging) {
963
+ // Reset selection
964
+ selectedNode = null;
965
+
966
+ // Clear all highlights and dimming
967
+ cy.elements().removeClass('highlighted dimmed');
968
+
969
+ // Restore default view: show all module edges, hide dependency edges
970
+ cy.edges('.import').show();
971
+ cy.edges('.dependency').hide();
972
+
973
+ // Ensure all nodes are visible
974
+ cy.nodes().show();
975
+
976
+ // Hide details panel or show default message
977
+ document.getElementById('details').innerHTML = '<p style="color: #999; text-align: center; padding: 20px;">Click on a node to see details</p>';
978
+ }
979
+ });
980
+
981
+ // Node hover handler
982
+ cy.on('mouseover', 'node', function(evt) {
983
+ const node = evt.target;
984
+ node.style('cursor', 'pointer');
985
+ });
986
+
987
+ // View mode controls
988
+ document.getElementById('view-all').addEventListener('click', function() {
989
+ setActiveButton(this);
990
+ // Reset selection
991
+ selectedNode = null;
992
+ // Clear highlights and dimming
993
+ cy.elements().removeClass('highlighted dimmed');
994
+ // Show all nodes
995
+ cy.nodes().show();
996
+ // Show import edges, hide dependency edges (default view)
997
+ cy.edges('.import').show();
998
+ cy.edges('.dependency').hide();
999
+ cy.fit();
1000
+ });
1001
+
1002
+ document.getElementById('view-modules').addEventListener('click', function() {
1003
+ setActiveButton(this);
1004
+ // Reset selection
1005
+ selectedNode = null;
1006
+ // Clear highlights and dimming
1007
+ cy.elements().removeClass('highlighted dimmed');
1008
+ // Hide provider nodes (children) but keep parent modules visible
1009
+ cy.nodes('[type="provider"]').hide();
1010
+ cy.edges('.dependency').hide();
1011
+ // Show parent modules and import edges
1012
+ cy.nodes(':parent').show();
1013
+ cy.nodes('[type="module"]').show();
1014
+ cy.edges('.import').show();
1015
+ cy.fit();
1016
+ });
1017
+
1018
+ document.getElementById('view-providers').addEventListener('click', function() {
1019
+ setActiveButton(this);
1020
+ // Reset selection
1021
+ selectedNode = null;
1022
+ // Clear highlights and dimming
1023
+ cy.elements().removeClass('highlighted dimmed');
1024
+ // Show parent modules (to see grouping) and provider nodes
1025
+ cy.nodes(':parent').show();
1026
+ cy.nodes('[type="provider"]').show();
1027
+ // Show all dependency edges in this view
1028
+ cy.edges('.dependency').show();
1029
+ // Hide import edges but keep module containers visible for context
1030
+ cy.edges('.import').hide();
1031
+ cy.fit();
1032
+ });
1033
+
1034
+ // Reset zoom
1035
+ document.getElementById('reset-zoom').addEventListener('click', function() {
1036
+ cy.zoom(1);
1037
+ cy.center();
1038
+ });
1039
+
1040
+ // Fit graph
1041
+ document.getElementById('fit-graph').addEventListener('click', function() {
1042
+ cy.fit();
1043
+ });
1044
+
1045
+ // Search functionality
1046
+ document.getElementById('search').addEventListener('input', function(e) {
1047
+ const searchTerm = e.target.value.toLowerCase();
1048
+
1049
+ if (!searchTerm) {
1050
+ // Reset to default view
1051
+ selectedNode = null;
1052
+ cy.nodes().show();
1053
+ cy.edges('.import').show();
1054
+ cy.edges('.dependency').hide();
1055
+ cy.elements().removeClass('highlighted dimmed');
1056
+ return;
1057
+ }
1058
+
1059
+ cy.elements().removeClass('highlighted dimmed');
1060
+
1061
+ const matchingNodes = cy.nodes().filter(function(node) {
1062
+ const label = node.data('label').toLowerCase();
1063
+ return label.includes(searchTerm);
1064
+ });
1065
+
1066
+ if (matchingNodes.length > 0) {
1067
+ matchingNodes.addClass('highlighted');
1068
+ cy.fit(matchingNodes, 50);
1069
+ }
1070
+ });
1071
+
1072
+ function setActiveButton(button) {
1073
+ document.querySelectorAll('.control-group button').forEach(btn => {
1074
+ btn.classList.remove('active');
1075
+ });
1076
+ button.classList.add('active');
1077
+ }
1078
+
1079
+ function navigateToNode(nodeId) {
1080
+ const node = cy.getElementById(nodeId);
1081
+ if (node.length > 0) {
1082
+ const data = node.data();
1083
+
1084
+ // Store the selected node
1085
+ selectedNode = node;
1086
+
1087
+ // Clear previous highlights and dimming
1088
+ cy.elements().removeClass('highlighted dimmed');
1089
+
1090
+ if (data.type === 'module') {
1091
+ // MODULE SELECTION: Show only edges related to this module
1092
+
1093
+ // Get all edges connected to this module
1094
+ const moduleEdges = node.connectedEdges('.import');
1095
+
1096
+ // Hide all edges first, then show only the module's edges
1097
+ cy.edges().hide();
1098
+
1099
+ // Show and highlight only the module's import/export edges
1100
+ moduleEdges.show().addClass('highlighted');
1101
+
1102
+ // Highlight the selected module
1103
+ node.addClass('highlighted');
1104
+
1105
+ // Highlight connected modules
1106
+ moduleEdges.connectedNodes('[type="module"]').addClass('highlighted');
1107
+
1108
+ // Dim all other nodes except highlighted ones
1109
+ cy.nodes().not('.highlighted').addClass('dimmed');
1110
+
1111
+ } else if (data.type === 'provider') {
1112
+ // PROVIDER SELECTION: Show only edges related to this provider
1113
+
1114
+ // Get all dependency edges connected to this provider
1115
+ const providerEdges = node.connectedEdges('.dependency');
1116
+
1117
+ // Hide all edges first, then show only the provider's edges
1118
+ cy.edges().hide();
1119
+
1120
+ // Show and highlight only the provider's dependency edges
1121
+ providerEdges.show().addClass('highlighted');
1122
+
1123
+ // Highlight the selected provider
1124
+ node.addClass('highlighted');
1125
+
1126
+ // Highlight connected providers
1127
+ providerEdges.connectedNodes('[type="provider"]').addClass('highlighted');
1128
+
1129
+ // Dim all other nodes except highlighted ones
1130
+ cy.nodes().not('.highlighted').addClass('dimmed');
1131
+ }
1132
+
1133
+ // Animate pan and zoom to the node
1134
+ cy.animate({
1135
+ center: { eles: node },
1136
+ zoom: 1.5,
1137
+ duration: 500,
1138
+ easing: 'ease-in-out-cubic'
1139
+ });
1140
+
1141
+ // Show node details
1142
+ showNodeDetails(data);
1143
+ }
1144
+ }
1145
+
1146
+ function showNodeDetails(data) {
1147
+ const placeholder = document.querySelector('.placeholder');
1148
+ const detailsDiv = document.getElementById('node-details');
1149
+
1150
+ placeholder.style.display = 'none';
1151
+ detailsDiv.style.display = 'block';
1152
+
1153
+ let html = '';
1154
+
1155
+ if (data.type === 'module') {
1156
+ html = \`
1157
+ <div class="info-item">
1158
+ <label>Type</label>
1159
+ <div class="value">
1160
+ <span class="badge module">Module</span>
1161
+ \${data.isGlobal ? '<span class="badge global">Global</span>' : ''}
1162
+ </div>
1163
+ </div>
1164
+ <div class="info-item">
1165
+ <label>Name</label>
1166
+ <div class="value"><code>\${data.label}</code></div>
1167
+ </div>
1168
+ <div class="info-item">
1169
+ <label>File Path</label>
1170
+ <div class="value">
1171
+ <a href="${ideUrlTemplate.replace("{path}", "' + data.path + '")}" target="_blank">
1172
+ \${data.path}
1173
+ </a>
1174
+ </div>
1175
+ </div>
1176
+ <div class="info-item">
1177
+ <label>Imports (\${data.imports.length})</label>
1178
+ <ul class="dependency-list">
1179
+ \${data.imports.map(imp => {
1180
+ const nodeId = 'module-' + imp;
1181
+ const exists = cy.getElementById(nodeId).length > 0;
1182
+ return exists
1183
+ ? '<li><a href="#" class="node-link" data-node-id="' + nodeId + '">' + imp + '</a></li>'
1184
+ : '<li>' + imp + '</li>';
1185
+ }).join('')}
1186
+ </ul>
1187
+ </div>
1188
+ <div class="info-item">
1189
+ <label>Providers (\${data.providers.length})</label>
1190
+ <ul class="dependency-list">
1191
+ \${data.providers.map(prov => {
1192
+ // Use providerMap to get the correct node ID
1193
+ const nodeId = providerMap[prov];
1194
+ const exists = nodeId && cy.getElementById(nodeId).length > 0;
1195
+ return exists
1196
+ ? '<li><a href="#" class="node-link" data-node-id="' + nodeId + '">' + prov + '</a></li>'
1197
+ : '<li>' + prov + '</li>';
1198
+ }).join('')}
1199
+ </ul>
1200
+ </div>
1201
+ \${data.exports.length > 0 ? \`
1202
+ <div class="info-item">
1203
+ <label>Exports (\${data.exports.length})</label>
1204
+ <ul class="dependency-list">
1205
+ \${data.exports.map(exp => {
1206
+ // Check if export is a provider or a module
1207
+ let nodeId = providerMap[exp] || moduleMap[exp];
1208
+ const exists = nodeId && cy.getElementById(nodeId).length > 0;
1209
+ return exists
1210
+ ? '<li><a href="#" class="node-link" data-node-id="' + nodeId + '">' + exp + '</a></li>'
1211
+ : '<li>' + exp + '</li>';
1212
+ }).join('')}
1213
+ </ul>
1214
+ </div>
1215
+ \` : ''}
1216
+ \`;
1217
+ } else if (data.type === 'provider') {
1218
+ html = \`
1219
+ <div class="info-item">
1220
+ <label>Type</label>
1221
+ <div class="value">
1222
+ <span class="badge provider">Provider</span>
1223
+ <span class="badge \${data.providerType.toLowerCase()}">\${data.providerType}</span>
1224
+ </div>
1225
+ </div>
1226
+ <div class="info-item">
1227
+ <label>Token</label>
1228
+ <div class="value"><code>\${data.label}</code></div>
1229
+ </div>
1230
+ <div class="info-item">
1231
+ <label>Module</label>
1232
+ <div class="value">
1233
+ \${(() => {
1234
+ const nodeId = 'module-' + data.module;
1235
+ const exists = cy.getElementById(nodeId).length > 0;
1236
+ return exists
1237
+ ? '<a href="#" class="node-link" data-node-id="' + nodeId + '"><code>' + data.module + '</code></a>'
1238
+ : '<code>' + data.module + '</code>';
1239
+ })()}
1240
+ </div>
1241
+ </div>
1242
+ \${data.scope ? \`
1243
+ <div class="info-item">
1244
+ <label>Scope</label>
1245
+ <div class="value">\${data.scope}</div>
1246
+ </div>
1247
+ \` : ''}
1248
+ \${data.value !== undefined ? \`
1249
+ <div class="info-item">
1250
+ <label>Value</label>
1251
+ <div class="value"><code>\${data.value}</code></div>
1252
+ </div>
1253
+ \` : ''}
1254
+ \${data.factory ? \`
1255
+ <div class="info-item">
1256
+ <label>Factory</label>
1257
+ <div class="value"><code>\${data.factory}</code></div>
1258
+ </div>
1259
+ \` : ''}
1260
+ \${data.useClass ? \`
1261
+ <div class="info-item">
1262
+ <label>Use Class</label>
1263
+ <div class="value"><code>\${data.useClass}</code></div>
1264
+ </div>
1265
+ \` : ''}
1266
+ \${data.hasMissingDecorators ? \`
1267
+ <div class="info-item" style="background: #ffebee; padding: 0.75rem; border-left: 3px solid #f44336; margin-bottom: 1rem;">
1268
+ <label style="color: #c62828; font-weight: bold;">⚠️ Missing @Inject Decorators</label>
1269
+ <div class="value" style="color: #c62828; font-size: 0.875rem;">
1270
+ Some dependencies are missing explicit @Inject decorators. This may cause runtime errors.
1271
+ </div>
1272
+ </div>
1273
+ \` : ''}
1274
+ \${data.dependencies && data.dependencies.length > 0 ? \`
1275
+ <div class="info-item">
1276
+ <label>Dependencies (\${data.dependencies.length})</label>
1277
+ <ul class="dependency-list">
1278
+ \${data.dependencies.map(dep => {
1279
+ const token = dep.token || 'Unknown';
1280
+ // Use providerMap to get the correct node ID
1281
+ const nodeId = providerMap[token];
1282
+ const exists = nodeId && cy.getElementById(nodeId).length > 0;
1283
+ const link = exists
1284
+ ? '<a href="#" class="node-link" data-node-id="' + nodeId + '">' + token + '</a>'
1285
+ : token;
1286
+ const warning = dep.hasExplicitDecorator === false
1287
+ ? ' <span style="color: #f44336; font-weight: bold;">⚠️ Missing @Inject</span>'
1288
+ : '';
1289
+ return '<li>' + link + (dep.optional ? ' <em>(optional)</em>' : '') + warning + '</li>';
1290
+ }).join('')}
1291
+ </ul>
1292
+ </div>
1293
+ \` : ''}
1294
+ \`;
1295
+ }
1296
+
1297
+ detailsDiv.innerHTML = html;
1298
+
1299
+ // Attach click handlers to node links
1300
+ detailsDiv.querySelectorAll('.node-link').forEach(link => {
1301
+ link.addEventListener('click', function(e) {
1302
+ e.preventDefault();
1303
+ const nodeId = this.getAttribute('data-node-id');
1304
+ navigateToNode(nodeId);
1305
+ });
1306
+ });
1307
+ }
1308
+ `;
1309
+ }
1310
+ }
1311
+ exports.HtmlGenerator = HtmlGenerator;
1312
+ //# sourceMappingURL=html-generator.js.map