nest-graph-inspector 0.3.0 → 0.5.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.
@@ -14,6 +14,8 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
14
14
  var NestGraphInspectorSetup_1;
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.NestGraphInspectorSetup = void 0;
17
+ const node_fs_1 = require("node:fs");
18
+ const node_path_1 = require("node:path");
17
19
  const common_1 = require("@nestjs/common");
18
20
  const core_1 = require("@nestjs/core");
19
21
  const nest_graph_inspector_config_1 = require("./nest-graph-inspector.config");
@@ -22,6 +24,7 @@ const http_output_adapter_1 = require("./adapters/http-output.adapter");
22
24
  const file_output_adapter_1 = require("./adapters/file-output.adapter");
23
25
  const json_output_adapter_1 = require("./adapters/json-output.adapter");
24
26
  const viewer_output_adapter_1 = require("./adapters/viewer-output.adapter");
27
+ const ts_morph_1 = require("ts-morph");
25
28
  let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspectorSetup {
26
29
  options;
27
30
  modulesContainer;
@@ -35,6 +38,7 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
35
38
  ignoreImport;
36
39
  nestCoreModuleName;
37
40
  nestCoreProviders;
41
+ tsMorphProject = this.createTsMorphProject();
38
42
  constructor(options, modulesContainer, httpOutputAdapter, fileOutputAdapter, jsonOutputAdapter, viewerOutputAdapter) {
39
43
  this.options = options;
40
44
  this.modulesContainer = modulesContainer;
@@ -65,30 +69,70 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
65
69
  ];
66
70
  }
67
71
  async onModuleInit() {
68
- const rootModuleClass = this.options.rootModule;
69
- const moduleMap = rootModuleClass
70
- ? this.buildModuleMap(rootModuleClass)
71
- : this.buildModuleMapFromAutoDetect();
72
- if (!this.options.outputs?.length) {
72
+ await this.inspectAndPublishGraph();
73
+ }
74
+ async inspectAndPublishGraph() {
75
+ if (!this.hasOutput) {
73
76
  return;
74
77
  }
75
- const graphOutput = this.enrichModuleMap(moduleMap);
76
- await Promise.all(this.options.outputs.map(async (output) => {
77
- output = this.withDefaultOutputOptions(output);
78
- const adapter = this.outputAdapters[output.type];
79
- if (!adapter) {
80
- this.logger.warn(`Unsupported output type: ${output.type}`);
81
- return;
82
- }
83
- try {
84
- const { message } = await adapter.execute(graphOutput, output);
85
- this.logger.debug(message);
86
- }
87
- catch (err) {
88
- this.logger.error(`Failed to execute output adapter for type ${output.type}`, err);
89
- }
78
+ const rootModule = this.getRootModule();
79
+ const moduleTree = this.getModuleTree(rootModule);
80
+ await this.publishModuleTree(moduleTree);
81
+ }
82
+ get hasOutput() {
83
+ return !!this.options.outputs?.length;
84
+ }
85
+ getRootModule() {
86
+ const rootModuleClass = this.options.rootModule;
87
+ return rootModuleClass
88
+ ? this.getRootModuleFromClass(rootModuleClass)
89
+ : this.findRootModule();
90
+ }
91
+ getRootModuleFromClass(rootModuleClass) {
92
+ const root = [...this.modulesContainer.values()].find((m) => m.metatype === rootModuleClass);
93
+ if (!root) {
94
+ throw new Error(`Root module not found: ${rootModuleClass.name}`);
95
+ }
96
+ return root;
97
+ }
98
+ getModuleTree(rootModule) {
99
+ const moduleTree = this.resolveModuleTree(rootModule);
100
+ this.resolveModuleMembers(moduleTree);
101
+ this.appendNestCoreModule(moduleTree);
102
+ return moduleTree;
103
+ }
104
+ async publishModuleTree(moduleTree) {
105
+ const moduleMap = this.createModuleMapFromModuleTree(moduleTree);
106
+ const graphOutput = this.createGraphOutput(moduleMap);
107
+ const outputs = this.prepareOutputs();
108
+ await this.publishOutputs({ graphOutput, outputs });
109
+ }
110
+ createGraphOutput(moduleMap) {
111
+ return this.enrichModuleMap(moduleMap);
112
+ }
113
+ prepareOutputs() {
114
+ return this.options.outputs ?? [];
115
+ }
116
+ async publishOutputs(param) {
117
+ await Promise.all(param.outputs.map(async (output) => {
118
+ await this.publishSingleOutput(param.graphOutput, output);
90
119
  }));
91
120
  }
121
+ async publishSingleOutput(graphOutput, output) {
122
+ output = this.withDefaultOutputOptions(output);
123
+ const adapter = this.outputAdapters[output.type];
124
+ if (!adapter) {
125
+ this.logger.warn(`Unsupported output type: ${output.type}`);
126
+ return;
127
+ }
128
+ try {
129
+ const { message } = await adapter.execute(graphOutput, output);
130
+ this.logger.debug(message);
131
+ }
132
+ catch (err) {
133
+ this.logger.error(`Failed to execute output adapter for type ${output.type}`, err);
134
+ }
135
+ }
92
136
  withDefaultOutputOptions(output) {
93
137
  if (output.type !== 'viewer') {
94
138
  return output;
@@ -107,37 +151,19 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
107
151
  }
108
152
  buildModuleMapFromAutoDetect() {
109
153
  const root = this.findRootModule();
110
- return this.buildModuleMapFromRef(root);
154
+ const moduleTree = this.getModuleTree(root);
155
+ return this.createModuleMapFromModuleTree(moduleTree);
111
156
  }
112
157
  buildModuleMap(rootModuleClass) {
113
- const root = [...this.modulesContainer.values()].find((m) => m.metatype === rootModuleClass);
114
- if (!root) {
115
- throw new Error(`Root module not found: ${rootModuleClass.name}`);
116
- }
117
- return this.buildModuleMapFromRef(root);
158
+ const root = this.getRootModuleFromClass(rootModuleClass);
159
+ const moduleTree = this.getModuleTree(root);
160
+ return this.createModuleMapFromModuleTree(moduleTree);
118
161
  }
119
- buildModuleMapFromRef(root) {
120
- const reachable = this.collectReachableModules(root, new Set());
121
- const modules = {};
122
- for (const moduleRef of reachable) {
123
- const moduleName = this.moduleName(moduleRef);
124
- if (this.ignoreImport.includes(moduleName)) {
125
- continue;
126
- }
127
- modules[moduleName] = {
128
- imports: this.extractImports(moduleRef),
129
- exports: this.extractExports(moduleRef),
130
- providers: this.extractProviders(moduleRef, moduleName),
131
- controllers: this.extractControllers(moduleRef, moduleName),
132
- };
133
- }
134
- const usedNestCoreProviders = this.findUsedNestCoreProviders(modules);
135
- if (usedNestCoreProviders.length > 0) {
136
- modules[this.nestCoreModuleName] = this.buildNestCoreModule(usedNestCoreProviders);
137
- }
162
+ createModuleMapFromModuleTree(moduleTree) {
163
+ const modules = this.flattenModuleTree(moduleTree);
138
164
  return {
139
- version: '2',
140
- root: this.moduleName(root),
165
+ version: '3',
166
+ root: moduleTree.name,
141
167
  modules,
142
168
  };
143
169
  }
@@ -156,21 +182,69 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
156
182
  }
157
183
  throw new Error('Could not auto-detect root module. No module imports NestGraphInspectorModule.');
158
184
  }
159
- buildNestCoreModule(usedProviders) {
185
+ resolveModuleTree(moduleRef, visited = new Set()) {
186
+ if (visited.has(moduleRef)) {
187
+ return this.createModuleTreeReference(moduleRef);
188
+ }
189
+ visited.add(moduleRef);
190
+ return {
191
+ name: this.moduleName(moduleRef),
192
+ jsdoc: this.extractModuleJsDoc(moduleRef),
193
+ moduleRef,
194
+ imports: [],
195
+ exports: [],
196
+ providers: [],
197
+ controllers: [],
198
+ children: [...moduleRef.imports.values()]
199
+ .filter((childModule) => !this.shouldIgnoreModule(childModule))
200
+ .map((childModule) => this.resolveModuleTree(childModule, visited)),
201
+ };
202
+ }
203
+ createModuleTreeReference(moduleRef) {
160
204
  return {
205
+ name: this.moduleName(moduleRef),
206
+ jsdoc: this.extractModuleJsDoc(moduleRef),
207
+ moduleRef,
208
+ imports: [],
209
+ exports: [],
210
+ providers: [],
211
+ controllers: [],
212
+ children: [],
213
+ };
214
+ }
215
+ resolveModuleMembers(moduleTree) {
216
+ this.walkModuleTree(moduleTree, (node) => {
217
+ if (!node.moduleRef) {
218
+ return;
219
+ }
220
+ node.imports = node.children.map((child) => child.name);
221
+ node.exports = this.extractExports(node.moduleRef);
222
+ node.providers = this.extractProviders(node.moduleRef, node.name);
223
+ node.controllers = this.extractControllers(node.moduleRef, node.name);
224
+ });
225
+ }
226
+ appendNestCoreModule(moduleTree) {
227
+ const usedProviders = this.findUsedNestCoreProvidersFromTree(moduleTree);
228
+ if (!usedProviders.length) {
229
+ return;
230
+ }
231
+ moduleTree.children.push({
232
+ name: this.nestCoreModuleName,
233
+ moduleRef: null,
161
234
  imports: [],
162
235
  exports: [...usedProviders],
163
- providers: usedProviders.map((providerName) => ({
164
- name: providerName,
236
+ providers: usedProviders.map((name) => ({
237
+ name,
165
238
  dependencies: [],
166
239
  })),
167
240
  controllers: [],
168
- };
241
+ children: [],
242
+ });
169
243
  }
170
- findUsedNestCoreProviders(modules) {
244
+ findUsedNestCoreProvidersFromTree(moduleTree) {
171
245
  const usedProviders = new Set();
172
- for (const moduleData of Object.values(modules)) {
173
- for (const provider of moduleData.providers) {
246
+ this.walkModuleTree(moduleTree, (node) => {
247
+ for (const provider of node.providers) {
174
248
  for (const dependencyName of provider.dependencies) {
175
249
  const nestCoreProviderName = this.extractNestCoreProviderName(dependencyName);
176
250
  if (nestCoreProviderName) {
@@ -178,7 +252,7 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
178
252
  }
179
253
  }
180
254
  }
181
- for (const controller of moduleData.controllers) {
255
+ for (const controller of node.controllers) {
182
256
  for (const dependencyName of controller.dependencies) {
183
257
  const nestCoreProviderName = this.extractNestCoreProviderName(dependencyName);
184
258
  if (nestCoreProviderName) {
@@ -186,9 +260,34 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
186
260
  }
187
261
  }
188
262
  }
189
- }
263
+ });
190
264
  return this.nestCoreProviders.filter((providerName) => usedProviders.has(providerName));
191
265
  }
266
+ flattenModuleTree(moduleTree) {
267
+ const modules = {};
268
+ this.walkModuleTree(moduleTree, (node) => {
269
+ if (modules[node.name] || this.ignoreImport.includes(node.name)) {
270
+ return;
271
+ }
272
+ modules[node.name] = {
273
+ ...(node.jsdoc ? { jsdoc: node.jsdoc } : {}),
274
+ imports: node.imports,
275
+ exports: node.exports,
276
+ providers: node.providers,
277
+ controllers: node.controllers,
278
+ };
279
+ });
280
+ return modules;
281
+ }
282
+ walkModuleTree(moduleTree, visit) {
283
+ visit(moduleTree);
284
+ for (const child of moduleTree.children) {
285
+ this.walkModuleTree(child, visit);
286
+ }
287
+ }
288
+ shouldIgnoreModule(moduleRef) {
289
+ return this.ignoreImport.includes(this.moduleName(moduleRef));
290
+ }
192
291
  extractNestCoreProviderName(dependencyName) {
193
292
  const prefix = `${this.nestCoreModuleName}:`;
194
293
  if (!dependencyName.startsWith(prefix)) {
@@ -203,16 +302,6 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
203
302
  }
204
303
  return providerName;
205
304
  }
206
- collectReachableModules(root, visited) {
207
- if (visited.has(root))
208
- return [];
209
- visited.add(root);
210
- const result = [root];
211
- for (const imported of root.imports.values()) {
212
- result.push(...this.collectReachableModules(imported, visited));
213
- }
214
- return result;
215
- }
216
305
  extractImports(moduleRef) {
217
306
  return [...moduleRef.imports.values()]
218
307
  .map((importedModuleRef) => this.moduleName(importedModuleRef))
@@ -224,59 +313,116 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
224
313
  .filter((exportName) => !!exportName && !this.ignoreProvider.includes(exportName));
225
314
  }
226
315
  extractProviders(moduleRef, moduleName) {
227
- return [...moduleRef.providers.values()]
228
- .map((wrapper) => this.extractProvider(wrapper, moduleName, moduleRef))
229
- .filter((provider) => !!provider);
316
+ return this.extractModuleMembers({
317
+ wrappers: moduleRef.providers.values(),
318
+ moduleRef,
319
+ extract: (wrapper) => this.extractModuleMember({
320
+ wrapper,
321
+ moduleRef,
322
+ shouldIgnore: (name) => this.ignoreProvider.includes(name),
323
+ }),
324
+ shouldSkip: (provider) => provider.name === moduleName,
325
+ });
230
326
  }
231
327
  extractControllers(moduleRef, moduleName) {
232
- return [...moduleRef.controllers.values()]
233
- .map((wrapper) => this.extractController(wrapper, moduleName, moduleRef))
234
- .filter((controller) => !!controller);
328
+ return this.extractModuleMembers({
329
+ wrappers: moduleRef.controllers.values(),
330
+ moduleRef,
331
+ extract: (wrapper) => this.extractModuleMember({ wrapper, moduleRef }),
332
+ });
235
333
  }
236
- extractProvider(wrapper, moduleName, moduleRef) {
237
- const name = this.wrapperName(wrapper);
238
- if (!name || this.ignoreProvider.includes(name) || name === moduleName) {
334
+ extractModuleMembers(param) {
335
+ return [...param.wrappers].reduce((items, wrapper) => {
336
+ const executableWrapper = wrapper;
337
+ const item = param.extract(executableWrapper, param.moduleRef);
338
+ if (item) {
339
+ if (param.shouldSkip?.(item)) {
340
+ return items;
341
+ }
342
+ items.push(item);
343
+ }
344
+ return items;
345
+ }, []);
346
+ }
347
+ extractModuleMember(param) {
348
+ const { wrapper, moduleRef, shouldIgnore = () => false } = param;
349
+ const name = this.wrapperClassName(wrapper) || this.tokenName(wrapper.token);
350
+ if (!name || shouldIgnore(name)) {
239
351
  return null;
240
352
  }
353
+ const jsdoc = this.extractClassJsDoc(wrapper);
241
354
  return {
242
355
  name,
356
+ ...(jsdoc ? { jsdoc } : {}),
243
357
  dependencies: this.extractDependencies(wrapper, moduleRef),
244
358
  };
245
359
  }
246
- extractController(wrapper, moduleName, moduleRef) {
247
- const name = this.wrapperName(wrapper);
248
- if (!name) {
249
- return null;
360
+ createTsMorphProject() {
361
+ const tsConfigFilePath = (0, node_path_1.join)(process.cwd(), 'tsconfig.json');
362
+ if (!(0, node_fs_1.existsSync)(tsConfigFilePath)) {
363
+ this.logger.warn(`Could not find tsconfig.json at ${tsConfigFilePath}; JSDoc metadata will be skipped.`);
364
+ return new ts_morph_1.Project();
250
365
  }
251
- return {
252
- name,
253
- dependencies: this.extractDependencies(wrapper, moduleRef),
254
- };
366
+ return new ts_morph_1.Project({
367
+ tsConfigFilePath,
368
+ skipAddingFilesFromTsConfig: false,
369
+ });
370
+ }
371
+ extractModuleJsDoc(moduleRef) {
372
+ return moduleRef.metatype
373
+ ? this.extractClassJsDocByName(moduleRef.metatype.name)
374
+ : undefined;
375
+ }
376
+ extractClassJsDoc(wrapper) {
377
+ const className = this.wrapperClassName(wrapper);
378
+ return className ? this.extractClassJsDocByName(className) : undefined;
379
+ }
380
+ extractClassJsDocByName(className) {
381
+ const targetClass = this.tsMorphProject
382
+ .getSourceFiles()
383
+ .flatMap((sourceFile) => sourceFile.getClasses())
384
+ .find((classDeclaration) => classDeclaration.getName() === className);
385
+ return targetClass
386
+ ?.getJsDocs()
387
+ .map((doc) => doc.getCommentText())
388
+ .filter((comment) => !!comment)
389
+ .join('\n');
390
+ }
391
+ wrapperClassName(wrapper) {
392
+ if (wrapper.metatype?.name) {
393
+ return wrapper.metatype.name;
394
+ }
395
+ const instance = wrapper.instance;
396
+ if (instance &&
397
+ (typeof instance === 'object' || typeof instance === 'function')) {
398
+ return instance.constructor?.name ?? null;
399
+ }
400
+ return null;
255
401
  }
256
402
  extractDependencies(wrapper, moduleRef) {
257
403
  const dependencies = new Set();
258
404
  if (Array.isArray(wrapper?.inject)) {
259
405
  for (const token of wrapper.inject) {
260
- const dependencyName = this.resolveDependencyName(token, moduleRef);
406
+ const dependencyName = this.resolveDependencyName(this.resolveInjectionToken(token), moduleRef);
261
407
  if (dependencyName) {
262
408
  dependencies.add(dependencyName);
263
409
  }
264
410
  }
265
411
  }
266
- const ctorDeps = wrapper?.getCtorMetadata?.() ?? [];
412
+ const ctorDeps = wrapper.getCtorMetadata?.() ?? [];
267
413
  for (const depWrapper of ctorDeps) {
268
414
  if (depWrapper) {
269
- const dependencyName = this.resolveDependencyName(depWrapper.token ?? depWrapper.name, moduleRef);
415
+ const dependencyName = this.resolveDependencyName(depWrapper.token, moduleRef);
270
416
  if (dependencyName && dependencyName !== 'Object') {
271
417
  dependencies.add(dependencyName);
272
418
  }
273
419
  }
274
420
  }
275
- const propertyDeps = wrapper?.getPropertiesMetadata?.() ?? [];
421
+ const propertyDeps = wrapper.getPropertiesMetadata?.() ?? [];
276
422
  for (const propertyDep of propertyDeps) {
277
423
  const depWrapper = propertyDep?.wrapper;
278
424
  if (depWrapper) {
279
- const dependencyName = this.resolveDependencyName(depWrapper.token ?? depWrapper.name, moduleRef);
425
+ const dependencyName = this.resolveDependencyName(depWrapper.token, moduleRef);
280
426
  if (dependencyName && dependencyName !== 'Object') {
281
427
  dependencies.add(dependencyName);
282
428
  }
@@ -284,6 +430,12 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
284
430
  }
285
431
  return [...dependencies];
286
432
  }
433
+ resolveInjectionToken(token) {
434
+ if (typeof token === 'object' && token !== null && 'token' in token) {
435
+ return token.token;
436
+ }
437
+ return token;
438
+ }
287
439
  resolveDependencyName(token, moduleRef) {
288
440
  const tokenName = this.tokenName(token);
289
441
  if (!tokenName) {
@@ -313,7 +465,14 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
313
465
  if (!this.isSameProviderToken(wrapper, token)) {
314
466
  continue;
315
467
  }
316
- const providerName = this.wrapperName(wrapper);
468
+ const providerInstance = wrapper.instance &&
469
+ (typeof wrapper.instance === 'object' ||
470
+ typeof wrapper.instance === 'function')
471
+ ? wrapper.instance
472
+ : null;
473
+ const providerName = wrapper.metatype?.name ||
474
+ providerInstance?.constructor?.name ||
475
+ this.tokenName(wrapper.token);
317
476
  if (!providerName) {
318
477
  return null;
319
478
  }
@@ -331,7 +490,9 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
331
490
  if (wrapper?.metatype === token) {
332
491
  return true;
333
492
  }
334
- if (wrapper?.instance?.constructor === token) {
493
+ if (wrapper.instance &&
494
+ typeof wrapper.instance === 'object' &&
495
+ wrapper.instance.constructor === token) {
335
496
  return true;
336
497
  }
337
498
  return false;
@@ -342,15 +503,12 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
342
503
  .filter((exportName) => !!exportName));
343
504
  }
344
505
  exportName(exportedItem) {
345
- return (this.wrapperName(exportedItem) ||
346
- this.tokenName(exportedItem?.token || exportedItem));
506
+ return this.tokenName(exportedItem);
347
507
  }
348
508
  isExportedProviderToken(moduleRef, token) {
349
509
  const tokenName = this.tokenName(token);
350
510
  for (const exportedItem of moduleRef.exports.values()) {
351
- if (exportedItem === token ||
352
- exportedItem?.token === token ||
353
- exportedItem?.metatype === token) {
511
+ if (exportedItem === token) {
354
512
  return true;
355
513
  }
356
514
  if (tokenName && this.exportName(exportedItem) === tokenName) {
@@ -377,12 +535,6 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
377
535
  this.tokenName(moduleRef.token) ||
378
536
  'AnonymousModule');
379
537
  }
380
- wrapperName(wrapper) {
381
- return (wrapper?.metatype?.name ||
382
- wrapper?.instance?.constructor?.name ||
383
- this.tokenName(wrapper?.token) ||
384
- null);
385
- }
386
538
  tokenName(token) {
387
539
  if (!token)
388
540
  return null;
@@ -392,7 +544,7 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
392
544
  return token.toString();
393
545
  if (typeof token === 'function')
394
546
  return token.name;
395
- return String(token);
547
+ return null;
396
548
  }
397
549
  enrichModuleMap(moduleMap) {
398
550
  if (!moduleMap.modules) {
@@ -415,8 +567,199 @@ let NestGraphInspectorSetup = NestGraphInspectorSetup_1 = class NestGraphInspect
415
567
  return {
416
568
  ...moduleMap,
417
569
  modules: enrichedModules,
570
+ cycles: this.findGraphCycles(enrichedModules),
571
+ };
572
+ }
573
+ findGraphCycles(modules) {
574
+ let nextId = 1;
575
+ const nextCycleId = () => nextId++;
576
+ return {
577
+ modules: this.findModuleCycles(modules, nextCycleId),
578
+ ...this.findDependencyCycles(modules, nextCycleId),
579
+ };
580
+ }
581
+ findModuleCycles(modules, nextCycleId) {
582
+ const graph = this.createGraph(Object.keys(modules));
583
+ for (const [moduleName, moduleData] of Object.entries(modules)) {
584
+ for (const importedModuleName of moduleData.imports) {
585
+ if (modules[importedModuleName]) {
586
+ graph.get(moduleName)?.add(importedModuleName);
587
+ }
588
+ }
589
+ }
590
+ return this.findCycles(graph, nextCycleId);
591
+ }
592
+ findDependencyCycles(modules, nextCycleId) {
593
+ const nodes = new Map();
594
+ for (const [moduleName, moduleData] of Object.entries(modules)) {
595
+ for (const provider of moduleData.providers) {
596
+ const key = this.dependencyNodeKey(moduleName, provider.name);
597
+ nodes.set(key, {
598
+ key,
599
+ kind: 'provider',
600
+ moduleName,
601
+ name: provider.name,
602
+ });
603
+ }
604
+ for (const controller of moduleData.controllers) {
605
+ const key = this.dependencyNodeKey(moduleName, controller.name);
606
+ nodes.set(key, {
607
+ key,
608
+ kind: 'controller',
609
+ moduleName,
610
+ name: controller.name,
611
+ });
612
+ }
613
+ }
614
+ const graph = this.createGraph([...nodes.keys()]);
615
+ for (const [moduleName, moduleData] of Object.entries(modules)) {
616
+ for (const provider of moduleData.providers) {
617
+ this.addDependencyEdges(graph, this.dependencyNodeKey(moduleName, provider.name), provider.dependencies, nodes);
618
+ }
619
+ for (const controller of moduleData.controllers) {
620
+ this.addDependencyEdges(graph, this.dependencyNodeKey(moduleName, controller.name), controller.dependencies, nodes);
621
+ }
622
+ }
623
+ const cycles = this.findCycles(graph, nextCycleId);
624
+ return {
625
+ providers: cycles
626
+ .filter((cycle) => nodes.get(cycle.from)?.kind === 'provider')
627
+ .map((cycle) => this.toProviderCycle(cycle, nodes)),
628
+ controllers: cycles.filter((cycle) => nodes.get(cycle.from)?.kind === 'controller'),
629
+ };
630
+ }
631
+ addDependencyEdges(graph, sourceKey, dependencies, nodes) {
632
+ const sourceEdges = graph.get(sourceKey);
633
+ if (!sourceEdges) {
634
+ return;
635
+ }
636
+ for (const dependency of dependencies) {
637
+ const targetKey = this.dependencyNodeKey(dependency.providedBy.name, dependency.token);
638
+ if (nodes.has(targetKey)) {
639
+ sourceEdges.add(targetKey);
640
+ }
641
+ }
642
+ }
643
+ createGraph(keys) {
644
+ const graph = new Map();
645
+ for (const key of keys) {
646
+ graph.set(key, new Set());
647
+ }
648
+ return graph;
649
+ }
650
+ findCycles(graph, nextCycleId) {
651
+ const cycles = [];
652
+ const seenCycleKeys = new Set();
653
+ const reachableKeys = new Map();
654
+ for (const source of graph.keys()) {
655
+ reachableKeys.set(source, this.findReachableKeys(source, graph));
656
+ }
657
+ for (const [source, targets] of graph) {
658
+ for (const target of targets) {
659
+ if (source !== target && !reachableKeys.get(target)?.has(source)) {
660
+ continue;
661
+ }
662
+ const path = source === target
663
+ ? [source, source]
664
+ : [source, ...this.findPath(target, source, graph)];
665
+ const cycleKey = this.getCanonicalCycleKey(path);
666
+ if (seenCycleKeys.has(cycleKey)) {
667
+ continue;
668
+ }
669
+ seenCycleKeys.add(cycleKey);
670
+ cycles.push({
671
+ id: nextCycleId(),
672
+ from: source,
673
+ to: target,
674
+ type: this.getCycleType(source, target, graph),
675
+ path,
676
+ });
677
+ }
678
+ }
679
+ return cycles;
680
+ }
681
+ getCanonicalCycleKey(path) {
682
+ const cyclePath = path.slice(0, -1);
683
+ if (cyclePath.length <= 1) {
684
+ return cyclePath.join('->');
685
+ }
686
+ const rotations = cyclePath.map((_, index) => [
687
+ ...cyclePath.slice(index),
688
+ ...cyclePath.slice(0, index),
689
+ ]);
690
+ return rotations
691
+ .map((rotation) => rotation.join('->'))
692
+ .sort((a, b) => a.localeCompare(b))[0];
693
+ }
694
+ toProviderCycle(cycle, nodes) {
695
+ return {
696
+ ...cycle,
697
+ path: cycle.path.map((key) => this.toProviderCyclePathItem(key, nodes)),
698
+ };
699
+ }
700
+ toProviderCyclePathItem(key, nodes) {
701
+ const node = nodes.get(key);
702
+ if (node) {
703
+ return {
704
+ module: { name: node.moduleName },
705
+ provider: { name: node.name },
706
+ };
707
+ }
708
+ const separatorIndex = key.indexOf(':');
709
+ if (separatorIndex === -1) {
710
+ return {
711
+ module: { name: '' },
712
+ provider: { name: key },
713
+ };
714
+ }
715
+ return {
716
+ module: { name: key.slice(0, separatorIndex) },
717
+ provider: { name: key.slice(separatorIndex + 1) },
418
718
  };
419
719
  }
720
+ getCycleType(source, target, graph) {
721
+ if (source === target || graph.get(target)?.has(source)) {
722
+ return 'direct';
723
+ }
724
+ return 'indirect';
725
+ }
726
+ findPath(source, target, graph) {
727
+ const visited = new Set();
728
+ const pendingPaths = [[source]];
729
+ while (pendingPaths.length > 0) {
730
+ const currentPath = pendingPaths.shift();
731
+ const current = currentPath?.[currentPath.length - 1];
732
+ if (!currentPath || !current || visited.has(current)) {
733
+ continue;
734
+ }
735
+ if (current === target) {
736
+ return currentPath;
737
+ }
738
+ visited.add(current);
739
+ for (const next of graph.get(current) ?? []) {
740
+ pendingPaths.push([...currentPath, next]);
741
+ }
742
+ }
743
+ return [source, target];
744
+ }
745
+ findReachableKeys(source, graph) {
746
+ const reachableKeys = new Set();
747
+ const pendingKeys = [...(graph.get(source) ?? [])];
748
+ while (pendingKeys.length > 0) {
749
+ const currentKey = pendingKeys.pop();
750
+ if (!currentKey || reachableKeys.has(currentKey)) {
751
+ continue;
752
+ }
753
+ reachableKeys.add(currentKey);
754
+ for (const nextKey of graph.get(currentKey) ?? []) {
755
+ pendingKeys.push(nextKey);
756
+ }
757
+ }
758
+ return reachableKeys;
759
+ }
760
+ dependencyNodeKey(moduleName, dependencyName) {
761
+ return `${moduleName}:${dependencyName}`;
762
+ }
420
763
  enrichDependency(dependency, currentModule) {
421
764
  const colonIndex = dependency.indexOf(':');
422
765
  if (colonIndex !== -1) {