ng-di-graph 0.5.0 โ†’ 0.7.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.
package/README.md CHANGED
@@ -19,8 +19,8 @@ npm install -g ng-di-graph
19
19
 
20
20
  ## Quick start
21
21
  ```bash
22
- # Analyze the whole project and print JSON (auto-discover tsconfig)
23
- ng-di-graph --format json
22
+ # Analyze the whole project and print text output (default)
23
+ ng-di-graph
24
24
 
25
25
  # Generate a Mermaid diagram and save it
26
26
  ng-di-graph --format mermaid --out docs/di-graph.mmd
@@ -45,13 +45,16 @@ ng-di-graph --project ./tsconfig.json --format json
45
45
  - ๐Ÿ” **Dependency Analysis** - Extract DI relationships from `@Injectable`, `@Component`, and `@Directive` classes
46
46
  - ๐ŸŽฏ **Constructor Injection** - Analyze constructor parameters with type annotations and `@Inject()` tokens
47
47
  - ๐Ÿท๏ธ **Decorator Flags** - Capture `@Optional`, `@Self`, `@SkipSelf`, and `@Host` parameter decorators
48
- - ๐Ÿ“Š **Multiple Output Formats** - JSON (machine-readable) and Mermaid (visual flowcharts)
48
+ - ๐Ÿ“Š **Multiple Output Formats** - Text (default), JSON (machine-readable), Mermaid (visual flowcharts)
49
49
  - ๐ŸŽจ **Entry Point Filtering** - Generate sub-graphs from specific starting nodes
50
50
  - ๐Ÿ”„ **Bidirectional Analysis** - Explore upstream dependencies, downstream consumers, or both
51
51
  - ๐Ÿ” **Circular Detection** - Automatically detect and report circular dependencies
52
+ - ๐Ÿงน **Angular Core Filtering** - Hide `@angular/core` nodes by default (`--include-angular-core` to show)
52
53
 
53
54
  ## Common commands
54
- - Full project, JSON output:
55
+ - Full project, text output (default):
56
+ `ng-di-graph`
57
+ - JSON output to stdout:
55
58
  `ng-di-graph --format json`
56
59
  - Mermaid diagram to file:
57
60
  `ng-di-graph --format mermaid --out docs/di-graph.mmd`
@@ -73,25 +76,58 @@ Option | Default | Description
73
76
  `filePaths` | none | Positional alias for `--files`; combines with `--files` when both are set.
74
77
  `-p, --project <path>` | auto-discovered | Path to the TypeScript config file to analyze (omit to auto-discover).
75
78
  `--files <paths...>` | none | Limit analysis to specific files or directories.
76
- `-f, --format <format>` | `json` | Output as `json` or `mermaid`.
79
+ `-f, --format <format>` | `text` | Output as `text`, `json`, or `mermaid`.
77
80
  `-e, --entry <symbol...>` | none | Start the graph from one or more symbols.
78
81
  `-d, --direction <dir>` | `downstream` | `downstream`, `upstream`, or `both` relative to entries.
79
82
  `--include-decorators` | `false` | Add `@Optional`, `@Self`, `@SkipSelf`, `@Host` flags to edges.
83
+ `--include-angular-core` | `false` | Include nodes that originate from `@angular/core` imports.
80
84
  `--out <file>` | stdout | Write output to a file.
81
85
  `-v, --verbose` | `false` | Show detailed parsing and resolution logs.
82
86
  `-h, --help` | `false` | Display CLI help.
83
87
 
84
88
  ## Output Formats
85
89
 
90
+ ### Text Format (default)
91
+
92
+ ```text
93
+ Project: ./tsconfig.json
94
+ Scope: direction=downstream, entry=all
95
+ Files: 12 (skipped: 0)
96
+
97
+ Dependencies (A depends on B):
98
+ AppComponent
99
+ โ””โ”€ UserService
100
+
101
+ UserService
102
+ โ”œโ”€ AuthService
103
+ โ””โ”€ Logger
104
+ ```
105
+
86
106
  ### JSON Format
87
107
 
88
108
  ```json
89
109
  {
90
110
  "nodes": [
91
- { "id": "AppComponent", "kind": "component" },
92
- { "id": "UserService", "kind": "service" },
93
- { "id": "AuthService", "kind": "service" },
94
- { "id": "Logger", "kind": "service" }
111
+ {
112
+ "id": "AppComponent",
113
+ "kind": "component",
114
+ "source": { "filePath": "/path/to/src/app.component.ts", "line": 12, "column": 14 }
115
+ },
116
+ {
117
+ "id": "UserService",
118
+ "kind": "service",
119
+ "source": { "filePath": "/path/to/src/user.service.ts", "line": 8, "column": 14 }
120
+ },
121
+ {
122
+ "id": "AuthService",
123
+ "kind": "service",
124
+ "source": { "filePath": "/path/to/src/auth.service.ts", "line": 20, "column": 14 }
125
+ },
126
+ {
127
+ "id": "Logger",
128
+ "kind": "service",
129
+ "source": { "filePath": "/path/to/src/logger.service.ts", "line": 5, "column": 14 }
130
+ }
95
131
  ],
96
132
  "edges": [
97
133
  {
@@ -119,6 +155,11 @@ Option | Default | Description
119
155
  - `directive` - Classes decorated with `@Directive()`
120
156
  - `unknown` - Could not determine decorator type
121
157
 
158
+ **Node Source** (when available):
159
+ - `source.filePath` - Absolute file path reported by the TypeScript project
160
+ - `source.line` / `source.column` - 1-based position of the class name token
161
+ - `source` is omitted for `unknown` nodes created from unresolved tokens
162
+
122
163
  **Edge Flags** (when `--include-decorators` is used):
123
164
  - `optional` - Parameter has `@Optional()` decorator
124
165
  - `self` - Parameter has `@Self()` decorator
@@ -378,6 +378,9 @@ function validateInput(parsedClasses) {
378
378
  if (typeof parsedClass.kind !== "string") throw new Error("ParsedClass must have a valid kind property");
379
379
  if (parsedClass.dependencies == null) throw new Error("ParsedClass must have a dependencies array");
380
380
  if (!Array.isArray(parsedClass.dependencies)) throw new Error("ParsedClass dependencies must be an array");
381
+ if (!parsedClass.source) throw new Error("ParsedClass must have a valid source property");
382
+ if (typeof parsedClass.source.filePath !== "string" || parsedClass.source.filePath === "") throw new Error("ParsedClass source must have a valid filePath");
383
+ if (!Number.isFinite(parsedClass.source.line) || !Number.isFinite(parsedClass.source.column) || parsedClass.source.line < 1 || parsedClass.source.column < 1) throw new Error("ParsedClass source must include valid line and column values");
381
384
  for (const dependency of parsedClass.dependencies) if (typeof dependency.token !== "string") throw new Error("ParsedDependency must have a valid token property");
382
385
  }
383
386
  }
@@ -441,21 +444,27 @@ function buildGraph(parsedClasses, logger) {
441
444
  const edges = [];
442
445
  for (const parsedClass of parsedClasses) if (!nodeMap.has(parsedClass.name)) nodeMap.set(parsedClass.name, {
443
446
  id: parsedClass.name,
444
- kind: parsedClass.kind
447
+ kind: parsedClass.kind,
448
+ source: parsedClass.source
445
449
  });
446
450
  logger?.info(LogCategory.GRAPH_CONSTRUCTION, `Created ${nodeMap.size} nodes`, { nodeCount: nodeMap.size });
447
451
  let unknownNodeCount = 0;
448
452
  for (const parsedClass of parsedClasses) for (const dependency of parsedClass.dependencies) {
449
453
  if (!nodeMap.has(dependency.token)) {
450
- nodeMap.set(dependency.token, {
454
+ const unknownNode = {
451
455
  id: dependency.token,
452
456
  kind: "unknown"
453
- });
457
+ };
458
+ if (dependency.origin) unknownNode.origin = dependency.origin;
459
+ nodeMap.set(dependency.token, unknownNode);
454
460
  unknownNodeCount++;
455
461
  logger?.warn(LogCategory.GRAPH_CONSTRUCTION, `Created unknown node: ${dependency.token}`, {
456
462
  nodeId: dependency.token,
457
463
  referencedBy: parsedClass.name
458
464
  });
465
+ } else {
466
+ const existingNode = nodeMap.get(dependency.token);
467
+ if (existingNode && existingNode.kind === "unknown" && !existingNode.origin && dependency.origin) existingNode.origin = dependency.origin;
459
468
  }
460
469
  const edge = {
461
470
  from: parsedClass.name,
@@ -527,24 +536,57 @@ function validateAndTraverseEntryPoints(entryPoints, graph, adjacencyList, resul
527
536
  * @returns Filtered graph containing only nodes reachable from entry points
528
537
  */
529
538
  function filterGraph(graph, options, logger) {
530
- if (!options.entry || options.entry.length === 0) return graph;
539
+ const baseGraph = filterGraphByOrigin(graph, options.includeAngularCore ?? false);
540
+ if (!options.entry || options.entry.length === 0) {
541
+ const includedNodeIds$1 = new Set(baseGraph.nodes.map((node) => node.id));
542
+ return {
543
+ nodes: baseGraph.nodes,
544
+ edges: baseGraph.edges,
545
+ circularDependencies: filterCircularDependencies(baseGraph, includedNodeIds$1)
546
+ };
547
+ }
531
548
  const includedNodeIds = /* @__PURE__ */ new Set();
532
549
  if (options.direction === "both") {
533
- const upstreamAdjacencyList = buildAdjacencyList(graph, "upstream");
550
+ const upstreamAdjacencyList = buildAdjacencyList(baseGraph, "upstream");
534
551
  const upstreamNodes = /* @__PURE__ */ new Set();
535
- validateAndTraverseEntryPoints(options.entry, graph, upstreamAdjacencyList, upstreamNodes, options, logger);
536
- const downstreamAdjacencyList = buildAdjacencyList(graph, "downstream");
552
+ validateAndTraverseEntryPoints(options.entry, baseGraph, upstreamAdjacencyList, upstreamNodes, options, logger);
553
+ const downstreamAdjacencyList = buildAdjacencyList(baseGraph, "downstream");
537
554
  const downstreamNodes = /* @__PURE__ */ new Set();
538
- validateAndTraverseEntryPoints(options.entry, graph, downstreamAdjacencyList, downstreamNodes, options, logger);
555
+ validateAndTraverseEntryPoints(options.entry, baseGraph, downstreamAdjacencyList, downstreamNodes, options, logger);
539
556
  const combinedNodes = new Set([...upstreamNodes, ...downstreamNodes]);
540
557
  for (const nodeId of combinedNodes) includedNodeIds.add(nodeId);
541
558
  } else {
542
- const adjacencyList = buildAdjacencyList(graph, options.direction);
543
- validateAndTraverseEntryPoints(options.entry, graph, adjacencyList, includedNodeIds, options, logger);
559
+ const adjacencyList = buildAdjacencyList(baseGraph, options.direction);
560
+ validateAndTraverseEntryPoints(options.entry, baseGraph, adjacencyList, includedNodeIds, options, logger);
561
+ }
562
+ const filteredNodes = baseGraph.nodes.filter((node) => includedNodeIds.has(node.id));
563
+ const filteredEdges = baseGraph.edges.filter((edge) => includedNodeIds.has(edge.from) && includedNodeIds.has(edge.to));
564
+ const filteredCircularDeps = filterCircularDependencies(baseGraph, includedNodeIds);
565
+ if (options.verbose && logger) {
566
+ logger.info(LogCategory.FILTERING, "Filtered graph summary", {
567
+ nodeCount: filteredNodes.length,
568
+ edgeCount: filteredEdges.length
569
+ });
570
+ logger.debug(LogCategory.FILTERING, "Entry points applied", { entryPoints: options.entry });
544
571
  }
545
- const filteredNodes = graph.nodes.filter((node) => includedNodeIds.has(node.id));
546
- const filteredEdges = graph.edges.filter((edge) => includedNodeIds.has(edge.from) && includedNodeIds.has(edge.to));
547
- const filteredCircularDeps = graph.circularDependencies.filter((cycle) => {
572
+ return {
573
+ nodes: filteredNodes,
574
+ edges: filteredEdges,
575
+ circularDependencies: filteredCircularDeps
576
+ };
577
+ }
578
+ function filterGraphByOrigin(graph, includeAngularCore) {
579
+ if (includeAngularCore) return graph;
580
+ const filteredNodes = graph.nodes.filter((node) => node.origin !== "angular-core");
581
+ const includedNodeIds = new Set(filteredNodes.map((node) => node.id));
582
+ return {
583
+ nodes: filteredNodes,
584
+ edges: graph.edges.filter((edge) => includedNodeIds.has(edge.from) && includedNodeIds.has(edge.to)),
585
+ circularDependencies: graph.circularDependencies
586
+ };
587
+ }
588
+ function filterCircularDependencies(graph, includedNodeIds) {
589
+ return graph.circularDependencies.filter((cycle) => {
548
590
  if (cycle.length < 2) return false;
549
591
  const isSelfLoop = cycle.length === 2 && cycle[0] === cycle[1];
550
592
  const isProperCycleWithClosing = cycle.length >= 3 && cycle[0] === cycle[cycle.length - 1];
@@ -559,18 +601,6 @@ function filterGraph(graph, options, logger) {
559
601
  }
560
602
  return true;
561
603
  });
562
- if (options.verbose && logger) {
563
- logger.info(LogCategory.FILTERING, "Filtered graph summary", {
564
- nodeCount: filteredNodes.length,
565
- edgeCount: filteredEdges.length
566
- });
567
- logger.debug(LogCategory.FILTERING, "Entry points applied", { entryPoints: options.entry });
568
- }
569
- return {
570
- nodes: filteredNodes,
571
- edges: filteredEdges,
572
- circularDependencies: filteredCircularDeps
573
- };
574
604
  }
575
605
  /**
576
606
  * Traverses the graph from a starting node using DFS
@@ -646,6 +676,7 @@ var OutputHandler = class {
646
676
  * Implements FR-03: Constructor token resolution
647
677
  */
648
678
  const GLOBAL_WARNING_KEYS = /* @__PURE__ */ new Set();
679
+ const ANGULAR_CORE_MODULE = "@angular/core";
649
680
  const formatTsDiagnostics = (diagnostics) => diagnostics.map((diagnostic) => {
650
681
  const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
651
682
  if (diagnostic.file && diagnostic.start !== void 0) {
@@ -660,6 +691,12 @@ var AngularParser = class {
660
691
  this._logger = _logger;
661
692
  this._typeResolutionCache = /* @__PURE__ */ new Map();
662
693
  this._circularTypeRefs = /* @__PURE__ */ new Set();
694
+ this._angularCoreImportCache = /* @__PURE__ */ new Map();
695
+ this._angularCoreAliasMatchers = [];
696
+ this._processingStats = {
697
+ processedFileCount: 0,
698
+ skippedFileCount: 0
699
+ };
663
700
  this._structuredWarnings = {
664
701
  categories: {
665
702
  typeResolution: [],
@@ -710,6 +747,12 @@ var AngularParser = class {
710
747
  };
711
748
  }
712
749
  /**
750
+ * Get file processing stats from the most recent parse run.
751
+ */
752
+ getProcessingStats() {
753
+ return { ...this._processingStats };
754
+ }
755
+ /**
713
756
  * Add structured warning to collection (Task 3.3)
714
757
  * Includes global deduplication for both structured warnings and console output
715
758
  * @param category Warning category
@@ -759,6 +802,8 @@ var AngularParser = class {
759
802
  const message = formatTsDiagnostics(parsedConfig.errors);
760
803
  throw ErrorHandler.createError(`TypeScript configuration error: ${message}`, "PROJECT_LOAD_FAILED", this._options.project, { diagnosticCount: parsedConfig.errors.length });
761
804
  }
805
+ this.cacheAngularCoreAliases(configFile.config);
806
+ this._angularCoreImportCache.clear();
762
807
  this._project = new ts_morph.Project({ tsConfigFilePath: tsConfigPath });
763
808
  if (!this._project) throw ErrorHandler.createError("Failed to load TypeScript project", "PROJECT_LOAD_FAILED", this._options.project);
764
809
  } catch (error) {
@@ -898,6 +943,10 @@ var AngularParser = class {
898
943
  const sourceFiles = this.getTargetSourceFiles();
899
944
  let processedFiles = 0;
900
945
  let skippedFiles = 0;
946
+ this._processingStats = {
947
+ processedFileCount: 0,
948
+ skippedFileCount: 0
949
+ };
901
950
  this._circularTypeRefs.clear();
902
951
  this._logger?.time("findDecoratedClasses");
903
952
  this._logger?.info(LogCategory.FILE_PROCESSING, "Starting file processing", { fileCount: sourceFiles.length });
@@ -948,6 +997,10 @@ var AngularParser = class {
948
997
  timing: elapsed
949
998
  });
950
999
  if (decoratedClasses.length === 0) ErrorHandler.warn("No decorated classes found in the project");
1000
+ this._processingStats = {
1001
+ processedFileCount: processedFiles,
1002
+ skippedFileCount: skippedFiles
1003
+ };
951
1004
  return decoratedClasses;
952
1005
  }
953
1006
  /**
@@ -963,6 +1016,13 @@ var AngularParser = class {
963
1016
  else ErrorHandler.warn(message);
964
1017
  return null;
965
1018
  }
1019
+ const nameNode = classDeclaration.getNameNode();
1020
+ if (!nameNode) {
1021
+ const message = "Skipping class without name node - classes must be named for dependency injection analysis";
1022
+ if (this._logger) this._logger.warn(LogCategory.AST_ANALYSIS, message, { className });
1023
+ else ErrorHandler.warn(message);
1024
+ return null;
1025
+ }
966
1026
  const decorators = classDeclaration.getDecorators();
967
1027
  if (this._options.verbose) {
968
1028
  const decoratorNames = decorators.map((d) => this.getDecoratorName(d)).join(", ");
@@ -980,10 +1040,19 @@ var AngularParser = class {
980
1040
  });
981
1041
  return null;
982
1042
  }
1043
+ const nodeKind = this.determineNodeKind(angularDecorator);
1044
+ const sourceFile = classDeclaration.getSourceFile();
1045
+ const filePath = sourceFile.getFilePath();
1046
+ const { line, column } = sourceFile.getLineAndColumnAtPos(nameNode.getStart());
983
1047
  return {
984
1048
  name: className,
985
- kind: this.determineNodeKind(angularDecorator),
986
- filePath: classDeclaration.getSourceFile().getFilePath(),
1049
+ kind: nodeKind,
1050
+ filePath,
1051
+ source: {
1052
+ filePath,
1053
+ line,
1054
+ column
1055
+ },
987
1056
  dependencies: this.extractConstructorDependencies(classDeclaration)
988
1057
  };
989
1058
  }
@@ -1023,15 +1092,79 @@ var AngularParser = class {
1023
1092
  */
1024
1093
  resolveDecoratorAlias(sourceFile, decoratorName) {
1025
1094
  const importDeclarations = sourceFile.getImportDeclarations();
1026
- for (const importDecl of importDeclarations) if (importDecl.getModuleSpecifierValue() === "@angular/core") {
1095
+ for (const importDecl of importDeclarations) {
1096
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
1097
+ if (this.isAngularCoreModuleSpecifier(moduleSpecifier)) {
1098
+ const namedImports = importDecl.getNamedImports();
1099
+ for (const namedImport of namedImports) {
1100
+ const alias = namedImport.getAliasNode();
1101
+ if (alias && alias.getText() === decoratorName) return namedImport.getName();
1102
+ if (!alias && namedImport.getName() === decoratorName) return decoratorName;
1103
+ }
1104
+ }
1105
+ }
1106
+ return null;
1107
+ }
1108
+ cacheAngularCoreAliases(tsConfig) {
1109
+ this._angularCoreAliasMatchers = [];
1110
+ if (!tsConfig || typeof tsConfig !== "object") return;
1111
+ const configRecord = tsConfig;
1112
+ const paths = (typeof configRecord.compilerOptions === "object" && configRecord.compilerOptions ? configRecord.compilerOptions : void 0)?.paths;
1113
+ if (!paths) return;
1114
+ for (const [alias, targets] of Object.entries(paths)) {
1115
+ if (!Array.isArray(targets)) continue;
1116
+ if (!targets.some((target) => this.isAngularCorePathTarget(target))) continue;
1117
+ this._angularCoreAliasMatchers.push(this.buildAliasMatcher(alias));
1118
+ }
1119
+ }
1120
+ buildAliasMatcher(alias) {
1121
+ const pattern = alias.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
1122
+ return /* @__PURE__ */ new RegExp(`^${pattern}$`);
1123
+ }
1124
+ isAngularCorePathTarget(target) {
1125
+ const normalized = target.replace(/\\/g, "/");
1126
+ return normalized.includes(`${ANGULAR_CORE_MODULE}/`) || normalized.includes(ANGULAR_CORE_MODULE);
1127
+ }
1128
+ isAngularCoreModuleSpecifier(moduleSpecifier) {
1129
+ if (moduleSpecifier === ANGULAR_CORE_MODULE || moduleSpecifier.startsWith(`${ANGULAR_CORE_MODULE}/`)) return true;
1130
+ return this._angularCoreAliasMatchers.some((matcher) => matcher.test(moduleSpecifier));
1131
+ }
1132
+ getAngularCoreImportMap(sourceFile) {
1133
+ const cacheKey = sourceFile.getFilePath();
1134
+ const cached = this._angularCoreImportCache.get(cacheKey);
1135
+ if (cached) return cached;
1136
+ const named = /* @__PURE__ */ new Set();
1137
+ const namespaces = /* @__PURE__ */ new Set();
1138
+ const importDeclarations = sourceFile.getImportDeclarations();
1139
+ for (const importDecl of importDeclarations) {
1140
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
1141
+ if (!this.isAngularCoreModuleSpecifier(moduleSpecifier)) continue;
1027
1142
  const namedImports = importDecl.getNamedImports();
1028
1143
  for (const namedImport of namedImports) {
1029
1144
  const alias = namedImport.getAliasNode();
1030
- if (alias && alias.getText() === decoratorName) return namedImport.getName();
1031
- if (!alias && namedImport.getName() === decoratorName) return decoratorName;
1145
+ named.add(alias ? alias.getText() : namedImport.getName());
1032
1146
  }
1147
+ const namespaceImport = importDecl.getNamespaceImport();
1148
+ if (namespaceImport) namespaces.add(namespaceImport.getText());
1033
1149
  }
1034
- return null;
1150
+ const map = {
1151
+ named,
1152
+ namespaces
1153
+ };
1154
+ this._angularCoreImportCache.set(cacheKey, map);
1155
+ return map;
1156
+ }
1157
+ resolveDependencyOrigin(token, sourceFile) {
1158
+ const importMap = this.getAngularCoreImportMap(sourceFile);
1159
+ const normalizedToken = this.normalizeTokenForOrigin(token);
1160
+ if (normalizedToken.includes(".")) {
1161
+ const namespace = normalizedToken.split(".")[0];
1162
+ if (importMap.namespaces.has(namespace)) return "angular-core";
1163
+ }
1164
+ if (importMap.named.has(normalizedToken)) return "angular-core";
1165
+ }
1166
+ normalizeTokenForOrigin(token) {
1167
+ return token.split("<")[0].replace(/\[\]$/, "");
1035
1168
  }
1036
1169
  /**
1037
1170
  * Determine NodeKind from Angular decorator
@@ -1370,7 +1503,8 @@ var AngularParser = class {
1370
1503
  */
1371
1504
  parseConstructorParameter(param) {
1372
1505
  const parameterName = param.getName();
1373
- const filePath = param.getSourceFile().getFilePath();
1506
+ const sourceFile = param.getSourceFile();
1507
+ const filePath = sourceFile.getFilePath();
1374
1508
  const lineNumber = param.getStartLineNumber();
1375
1509
  const columnNumber = param.getStart() - param.getStartLinePos();
1376
1510
  const startTime = performance.now();
@@ -1383,7 +1517,8 @@ var AngularParser = class {
1383
1517
  if (token) return {
1384
1518
  token,
1385
1519
  flags,
1386
- parameterName
1520
+ parameterName,
1521
+ origin: this.resolveDependencyOrigin(token, sourceFile)
1387
1522
  };
1388
1523
  }
1389
1524
  const initializer = param.getInitializer();
@@ -1394,7 +1529,8 @@ var AngularParser = class {
1394
1529
  return {
1395
1530
  token: injectResult.token,
1396
1531
  flags: finalFlags,
1397
- parameterName
1532
+ parameterName,
1533
+ origin: this.resolveDependencyOrigin(injectResult.token, sourceFile)
1398
1534
  };
1399
1535
  }
1400
1536
  }
@@ -1404,7 +1540,8 @@ var AngularParser = class {
1404
1540
  if (token) return {
1405
1541
  token,
1406
1542
  flags,
1407
- parameterName
1543
+ parameterName,
1544
+ origin: this.resolveDependencyOrigin(token, sourceFile)
1408
1545
  };
1409
1546
  }
1410
1547
  const type = param.getType();
@@ -1431,7 +1568,8 @@ var AngularParser = class {
1431
1568
  if (resolvedToken) return {
1432
1569
  token: resolvedToken,
1433
1570
  flags,
1434
- parameterName
1571
+ parameterName,
1572
+ origin: this.resolveDependencyOrigin(resolvedToken, sourceFile)
1435
1573
  };
1436
1574
  return null;
1437
1575
  } finally {
@@ -1552,7 +1690,8 @@ var AngularParser = class {
1552
1690
  return {
1553
1691
  token,
1554
1692
  flags,
1555
- parameterName: propertyName
1693
+ parameterName: propertyName,
1694
+ origin: this.resolveDependencyOrigin(token, property.getSourceFile())
1556
1695
  };
1557
1696
  } catch (error) {
1558
1697
  if (this._options.verbose) {
@@ -1681,12 +1820,15 @@ var AngularParser = class {
1681
1820
  isAngularInjectImported(sourceFile) {
1682
1821
  try {
1683
1822
  const importDeclarations = sourceFile.getImportDeclarations();
1684
- for (const importDecl of importDeclarations) if (importDecl.getModuleSpecifierValue() === "@angular/core") {
1685
- const namedImports = importDecl.getNamedImports();
1686
- for (const namedImport of namedImports) {
1687
- const importName = namedImport.getName();
1688
- const alias = namedImport.getAliasNode();
1689
- if (importName === "inject" || alias && alias.getText() === "inject") return true;
1823
+ for (const importDecl of importDeclarations) {
1824
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
1825
+ if (this.isAngularCoreModuleSpecifier(moduleSpecifier)) {
1826
+ const namedImports = importDecl.getNamedImports();
1827
+ for (const namedImport of namedImports) {
1828
+ const importName = namedImport.getName();
1829
+ const alias = namedImport.getAliasNode();
1830
+ if (importName === "inject" || alias && alias.getText() === "inject") return true;
1831
+ }
1690
1832
  }
1691
1833
  }
1692
1834
  return false;
@@ -1899,7 +2041,11 @@ var JsonFormatter = class {
1899
2041
  nodeCount: graph.nodes.length,
1900
2042
  edgeCount: graph.edges.length
1901
2043
  });
1902
- const result = JSON.stringify(graph, null, 2);
2044
+ const sanitizedGraph = {
2045
+ ...graph,
2046
+ nodes: graph.nodes.map(({ origin, ...node }) => node)
2047
+ };
2048
+ const result = JSON.stringify(sanitizedGraph, null, 2);
1903
2049
  const elapsed = this._logger?.timeEnd("json-format") ?? 0;
1904
2050
  this._logger?.info(LogCategory.PERFORMANCE, "JSON output complete", {
1905
2051
  outputSize: result.length,
@@ -1974,6 +2120,79 @@ var MermaidFormatter = class {
1974
2120
  }
1975
2121
  };
1976
2122
 
2123
+ //#endregion
2124
+ //#region src/formatters/text-formatter.ts
2125
+ var TextFormatter = class {
2126
+ constructor(context = {}, logger) {
2127
+ this._context = context;
2128
+ this._logger = logger;
2129
+ }
2130
+ format(graph) {
2131
+ this._logger?.time("text-format");
2132
+ this._logger?.info(LogCategory.PERFORMANCE, "Generating text output", {
2133
+ nodeCount: graph.nodes.length,
2134
+ edgeCount: graph.edges.length
2135
+ });
2136
+ const warningCount = this._context.warningCount ?? this._context.warnings?.length ?? 0;
2137
+ const circularDependencyCount = graph.circularDependencies.length;
2138
+ const lines = [];
2139
+ if (this._context.projectPath) lines.push(`Project: ${this._context.projectPath}`);
2140
+ if (this._context.direction) {
2141
+ const entry = this._context.entry && this._context.entry.length > 0 ? this._context.entry.join(", ") : "all";
2142
+ lines.push(`Scope: direction=${this._context.direction}, entry=${entry}`);
2143
+ }
2144
+ lines.push(`Files: ${this._context.processedFileCount ?? 0} (skipped: ${this._context.skippedFileCount ?? 0})`);
2145
+ if (warningCount > 0) lines.push(`Warnings: ${warningCount}`);
2146
+ if (circularDependencyCount > 0) lines.push(`Circular dependencies: ${circularDependencyCount}`);
2147
+ const dependencyGroups = this.getDependencyGroups(graph, 10);
2148
+ if (dependencyGroups.length > 0) {
2149
+ lines.push("");
2150
+ lines.push("Dependencies (A depends on B):");
2151
+ lines.push(...dependencyGroups);
2152
+ }
2153
+ const result = `${lines.join("\n")}\n`;
2154
+ const elapsed = this._logger?.timeEnd("text-format") ?? 0;
2155
+ this._logger?.info(LogCategory.PERFORMANCE, "Text output complete", {
2156
+ outputSize: result.length,
2157
+ elapsed
2158
+ });
2159
+ return result;
2160
+ }
2161
+ getDependencyGroups(graph, limit) {
2162
+ const uniqueEdges = /* @__PURE__ */ new Map();
2163
+ for (const edge of graph.edges) {
2164
+ const key = `${edge.from}->${edge.to}`;
2165
+ if (!uniqueEdges.has(key)) uniqueEdges.set(key, {
2166
+ from: edge.from,
2167
+ to: edge.to
2168
+ });
2169
+ }
2170
+ const sortedEdges = [...uniqueEdges.values()].sort((a, b) => {
2171
+ const fromOrder = a.from.localeCompare(b.from);
2172
+ return fromOrder !== 0 ? fromOrder : a.to.localeCompare(b.to);
2173
+ }).slice(0, limit);
2174
+ const grouped = /* @__PURE__ */ new Map();
2175
+ for (const edge of sortedEdges) {
2176
+ const deps = grouped.get(edge.from);
2177
+ if (deps) deps.push(edge.to);
2178
+ else grouped.set(edge.from, [edge.to]);
2179
+ }
2180
+ const lines = [];
2181
+ const groups = [...grouped.entries()];
2182
+ for (let i = 0; i < groups.length; i++) {
2183
+ const [from, deps] = groups[i];
2184
+ lines.push(from);
2185
+ for (let depIndex = 0; depIndex < deps.length; depIndex++) {
2186
+ const dep = deps[depIndex];
2187
+ const prefix = depIndex === deps.length - 1 ? "โ””โ”€" : "โ”œโ”€";
2188
+ lines.push(`${prefix} ${dep}`);
2189
+ }
2190
+ if (i < groups.length - 1) lines.push("");
2191
+ }
2192
+ return lines;
2193
+ }
2194
+ };
2195
+
1977
2196
  //#endregion
1978
2197
  //#region src/cli/project-resolver.ts
1979
2198
  const TSCONFIG_NAME = "tsconfig.json";
@@ -2168,7 +2387,7 @@ function enforceMinimumNodeVersion() {
2168
2387
  enforceMinimumNodeVersion();
2169
2388
  const program = new commander.Command();
2170
2389
  program.name("ng-di-graph").description("Angular DI dependency graph CLI tool").version("0.1.0");
2171
- program.argument("[filePaths...]", "TypeScript files to analyze (alias for --files)").option("-p, --project <path>", "tsconfig.json path (auto-discovered if omitted)").option("--files <paths...>", "specific file paths to analyze (similar to eslint targets)").option("-f, --format <format>", "output format: json | mermaid", "json").option("-e, --entry <symbol...>", "starting nodes for sub-graph").option("-d, --direction <dir>", "filtering direction: upstream|downstream|both", "downstream").option("--include-decorators", "include Optional/Self/SkipSelf/Host flags", false).option("--out <file>", "output file (stdout if omitted)").option("-v, --verbose", "show detailed parsing information", false);
2390
+ program.argument("[filePaths...]", "TypeScript files to analyze (alias for --files)").option("-p, --project <path>", "tsconfig.json path (auto-discovered if omitted)").option("--files <paths...>", "specific file paths to analyze (similar to eslint targets)").option("-f, --format <format>", "output format: text | json | mermaid", "text").option("-e, --entry <symbol...>", "starting nodes for sub-graph").option("-d, --direction <dir>", "filtering direction: upstream|downstream|both", "downstream").option("--include-decorators", "include Optional/Self/SkipSelf/Host flags", false).option("--include-angular-core", "include @angular/core nodes", false).option("--out <file>", "output file (stdout if omitted)").option("-v, --verbose", "show detailed parsing information", false);
2172
2391
  program.action(async (filePaths = [], options) => {
2173
2392
  try {
2174
2393
  const mergedFiles = mergeFileTargets(filePaths, options.files);
@@ -2184,7 +2403,11 @@ program.action(async (filePaths = [], options) => {
2184
2403
  "downstream",
2185
2404
  "both"
2186
2405
  ].includes(options.direction)) throw ErrorHandler.createError(`Invalid direction: ${options.direction}. Must be 'upstream', 'downstream', or 'both'`, "INVALID_ARGUMENTS");
2187
- if (options.format && !["json", "mermaid"].includes(options.format)) throw ErrorHandler.createError(`Invalid format: ${options.format}. Must be 'json' or 'mermaid'`, "INVALID_ARGUMENTS");
2406
+ if (options.format && ![
2407
+ "json",
2408
+ "mermaid",
2409
+ "text"
2410
+ ].includes(options.format)) throw ErrorHandler.createError(`Invalid format: ${options.format}. Must be 'text', 'json', or 'mermaid'`, "INVALID_ARGUMENTS");
2188
2411
  const tsconfigLikePositional = filePaths.find((filePath) => /(?:^|[\\/])tsconfig(?:\.[^/\\]+)?\.json$/.test(filePath));
2189
2412
  if (tsconfigLikePositional) throw ErrorHandler.createError(`Positional argument "${tsconfigLikePositional}" looks like a tsconfig. Use --project "${tsconfigLikePositional}" instead.`, "INVALID_ARGUMENTS");
2190
2413
  const cliOptions = {
@@ -2194,6 +2417,7 @@ program.action(async (filePaths = [], options) => {
2194
2417
  entry: options.entry,
2195
2418
  direction: options.direction,
2196
2419
  includeDecorators: options.includeDecorators,
2420
+ includeAngularCore: options.includeAngularCore,
2197
2421
  out: options.out,
2198
2422
  verbose: options.verbose
2199
2423
  };
@@ -2222,14 +2446,25 @@ program.action(async (filePaths = [], options) => {
2222
2446
  console.log(`โœ… Graph built: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
2223
2447
  if (graph.circularDependencies.length > 0) console.log(`โš ๏ธ Detected ${graph.circularDependencies.length} circular dependencies`);
2224
2448
  }
2225
- if (cliOptions.entry && cliOptions.entry.length > 0) {
2226
- if (cliOptions.verbose) console.log(`๐Ÿ” Filtering graph by entry points: ${cliOptions.entry.join(", ")}`);
2449
+ if (cliOptions.entry && cliOptions.entry.length > 0 || !cliOptions.includeAngularCore) {
2450
+ if (cliOptions.verbose && cliOptions.entry && cliOptions.entry.length > 0) console.log(`๐Ÿ” Filtering graph by entry points: ${cliOptions.entry.join(", ")}`);
2227
2451
  graph = filterGraph(graph, cliOptions, logger);
2228
- if (cliOptions.verbose) console.log(`โœ… Filtered graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
2452
+ if (cliOptions.verbose && cliOptions.entry && cliOptions.entry.length > 0) console.log(`โœ… Filtered graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
2229
2453
  }
2230
2454
  let formatter;
2231
2455
  if (cliOptions.format === "mermaid") formatter = new MermaidFormatter(logger);
2232
- else formatter = new JsonFormatter(logger);
2456
+ else if (cliOptions.format === "json") formatter = new JsonFormatter(logger);
2457
+ else {
2458
+ const processingStats = parser.getProcessingStats();
2459
+ formatter = new TextFormatter({
2460
+ projectPath: cliOptions.project,
2461
+ direction: cliOptions.direction,
2462
+ entry: cliOptions.entry,
2463
+ processedFileCount: processingStats.processedFileCount,
2464
+ skippedFileCount: processingStats.skippedFileCount,
2465
+ warningCount: parser.getStructuredWarnings().totalCount
2466
+ }, logger);
2467
+ }
2233
2468
  const formattedOutput = formatter.format(graph);
2234
2469
  await new OutputHandler().writeOutput(formattedOutput, cliOptions.out);
2235
2470
  if (cliOptions.verbose && cliOptions.out) console.log(`โœ… Output written to: ${cliOptions.out}`);