ng-di-graph 0.5.0 → 0.6.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,15 @@ 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
52
 
53
53
  ## Common commands
54
- - Full project, JSON output:
54
+ - Full project, text output (default):
55
+ `ng-di-graph`
56
+ - JSON output to stdout:
55
57
  `ng-di-graph --format json`
56
58
  - Mermaid diagram to file:
57
59
  `ng-di-graph --format mermaid --out docs/di-graph.mmd`
@@ -73,7 +75,7 @@ Option | Default | Description
73
75
  `filePaths` | none | Positional alias for `--files`; combines with `--files` when both are set.
74
76
  `-p, --project <path>` | auto-discovered | Path to the TypeScript config file to analyze (omit to auto-discover).
75
77
  `--files <paths...>` | none | Limit analysis to specific files or directories.
76
- `-f, --format <format>` | `json` | Output as `json` or `mermaid`.
78
+ `-f, --format <format>` | `text` | Output as `text`, `json`, or `mermaid`.
77
79
  `-e, --entry <symbol...>` | none | Start the graph from one or more symbols.
78
80
  `-d, --direction <dir>` | `downstream` | `downstream`, `upstream`, or `both` relative to entries.
79
81
  `--include-decorators` | `false` | Add `@Optional`, `@Self`, `@SkipSelf`, `@Host` flags to edges.
@@ -83,15 +85,47 @@ Option | Default | Description
83
85
 
84
86
  ## Output Formats
85
87
 
88
+ ### Text Format (default)
89
+
90
+ ```text
91
+ Project: ./tsconfig.json
92
+ Scope: direction=downstream, entry=all
93
+ Files: 12 (skipped: 0)
94
+
95
+ Dependencies (A depends on B):
96
+ AppComponent
97
+ └─ UserService
98
+
99
+ UserService
100
+ ├─ AuthService
101
+ └─ Logger
102
+ ```
103
+
86
104
  ### JSON Format
87
105
 
88
106
  ```json
89
107
  {
90
108
  "nodes": [
91
- { "id": "AppComponent", "kind": "component" },
92
- { "id": "UserService", "kind": "service" },
93
- { "id": "AuthService", "kind": "service" },
94
- { "id": "Logger", "kind": "service" }
109
+ {
110
+ "id": "AppComponent",
111
+ "kind": "component",
112
+ "source": { "filePath": "/path/to/src/app.component.ts", "line": 12, "column": 14 }
113
+ },
114
+ {
115
+ "id": "UserService",
116
+ "kind": "service",
117
+ "source": { "filePath": "/path/to/src/user.service.ts", "line": 8, "column": 14 }
118
+ },
119
+ {
120
+ "id": "AuthService",
121
+ "kind": "service",
122
+ "source": { "filePath": "/path/to/src/auth.service.ts", "line": 20, "column": 14 }
123
+ },
124
+ {
125
+ "id": "Logger",
126
+ "kind": "service",
127
+ "source": { "filePath": "/path/to/src/logger.service.ts", "line": 5, "column": 14 }
128
+ }
95
129
  ],
96
130
  "edges": [
97
131
  {
@@ -119,6 +153,11 @@ Option | Default | Description
119
153
  - `directive` - Classes decorated with `@Directive()`
120
154
  - `unknown` - Could not determine decorator type
121
155
 
156
+ **Node Source** (when available):
157
+ - `source.filePath` - Absolute file path reported by the TypeScript project
158
+ - `source.line` / `source.column` - 1-based position of the class name token
159
+ - `source` is omitted for `unknown` nodes created from unresolved tokens
160
+
122
161
  **Edge Flags** (when `--include-decorators` is used):
123
162
  - `optional` - Parameter has `@Optional()` decorator
124
163
  - `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,7 +444,8 @@ 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;
@@ -660,6 +664,10 @@ var AngularParser = class {
660
664
  this._logger = _logger;
661
665
  this._typeResolutionCache = /* @__PURE__ */ new Map();
662
666
  this._circularTypeRefs = /* @__PURE__ */ new Set();
667
+ this._processingStats = {
668
+ processedFileCount: 0,
669
+ skippedFileCount: 0
670
+ };
663
671
  this._structuredWarnings = {
664
672
  categories: {
665
673
  typeResolution: [],
@@ -710,6 +718,12 @@ var AngularParser = class {
710
718
  };
711
719
  }
712
720
  /**
721
+ * Get file processing stats from the most recent parse run.
722
+ */
723
+ getProcessingStats() {
724
+ return { ...this._processingStats };
725
+ }
726
+ /**
713
727
  * Add structured warning to collection (Task 3.3)
714
728
  * Includes global deduplication for both structured warnings and console output
715
729
  * @param category Warning category
@@ -898,6 +912,10 @@ var AngularParser = class {
898
912
  const sourceFiles = this.getTargetSourceFiles();
899
913
  let processedFiles = 0;
900
914
  let skippedFiles = 0;
915
+ this._processingStats = {
916
+ processedFileCount: 0,
917
+ skippedFileCount: 0
918
+ };
901
919
  this._circularTypeRefs.clear();
902
920
  this._logger?.time("findDecoratedClasses");
903
921
  this._logger?.info(LogCategory.FILE_PROCESSING, "Starting file processing", { fileCount: sourceFiles.length });
@@ -948,6 +966,10 @@ var AngularParser = class {
948
966
  timing: elapsed
949
967
  });
950
968
  if (decoratedClasses.length === 0) ErrorHandler.warn("No decorated classes found in the project");
969
+ this._processingStats = {
970
+ processedFileCount: processedFiles,
971
+ skippedFileCount: skippedFiles
972
+ };
951
973
  return decoratedClasses;
952
974
  }
953
975
  /**
@@ -963,6 +985,13 @@ var AngularParser = class {
963
985
  else ErrorHandler.warn(message);
964
986
  return null;
965
987
  }
988
+ const nameNode = classDeclaration.getNameNode();
989
+ if (!nameNode) {
990
+ const message = "Skipping class without name node - classes must be named for dependency injection analysis";
991
+ if (this._logger) this._logger.warn(LogCategory.AST_ANALYSIS, message, { className });
992
+ else ErrorHandler.warn(message);
993
+ return null;
994
+ }
966
995
  const decorators = classDeclaration.getDecorators();
967
996
  if (this._options.verbose) {
968
997
  const decoratorNames = decorators.map((d) => this.getDecoratorName(d)).join(", ");
@@ -980,10 +1009,19 @@ var AngularParser = class {
980
1009
  });
981
1010
  return null;
982
1011
  }
1012
+ const nodeKind = this.determineNodeKind(angularDecorator);
1013
+ const sourceFile = classDeclaration.getSourceFile();
1014
+ const filePath = sourceFile.getFilePath();
1015
+ const { line, column } = sourceFile.getLineAndColumnAtPos(nameNode.getStart());
983
1016
  return {
984
1017
  name: className,
985
- kind: this.determineNodeKind(angularDecorator),
986
- filePath: classDeclaration.getSourceFile().getFilePath(),
1018
+ kind: nodeKind,
1019
+ filePath,
1020
+ source: {
1021
+ filePath,
1022
+ line,
1023
+ column
1024
+ },
987
1025
  dependencies: this.extractConstructorDependencies(classDeclaration)
988
1026
  };
989
1027
  }
@@ -1974,6 +2012,79 @@ var MermaidFormatter = class {
1974
2012
  }
1975
2013
  };
1976
2014
 
2015
+ //#endregion
2016
+ //#region src/formatters/text-formatter.ts
2017
+ var TextFormatter = class {
2018
+ constructor(context = {}, logger) {
2019
+ this._context = context;
2020
+ this._logger = logger;
2021
+ }
2022
+ format(graph) {
2023
+ this._logger?.time("text-format");
2024
+ this._logger?.info(LogCategory.PERFORMANCE, "Generating text output", {
2025
+ nodeCount: graph.nodes.length,
2026
+ edgeCount: graph.edges.length
2027
+ });
2028
+ const warningCount = this._context.warningCount ?? this._context.warnings?.length ?? 0;
2029
+ const circularDependencyCount = graph.circularDependencies.length;
2030
+ const lines = [];
2031
+ if (this._context.projectPath) lines.push(`Project: ${this._context.projectPath}`);
2032
+ if (this._context.direction) {
2033
+ const entry = this._context.entry && this._context.entry.length > 0 ? this._context.entry.join(", ") : "all";
2034
+ lines.push(`Scope: direction=${this._context.direction}, entry=${entry}`);
2035
+ }
2036
+ lines.push(`Files: ${this._context.processedFileCount ?? 0} (skipped: ${this._context.skippedFileCount ?? 0})`);
2037
+ if (warningCount > 0) lines.push(`Warnings: ${warningCount}`);
2038
+ if (circularDependencyCount > 0) lines.push(`Circular dependencies: ${circularDependencyCount}`);
2039
+ const dependencyGroups = this.getDependencyGroups(graph, 10);
2040
+ if (dependencyGroups.length > 0) {
2041
+ lines.push("");
2042
+ lines.push("Dependencies (A depends on B):");
2043
+ lines.push(...dependencyGroups);
2044
+ }
2045
+ const result = `${lines.join("\n")}\n`;
2046
+ const elapsed = this._logger?.timeEnd("text-format") ?? 0;
2047
+ this._logger?.info(LogCategory.PERFORMANCE, "Text output complete", {
2048
+ outputSize: result.length,
2049
+ elapsed
2050
+ });
2051
+ return result;
2052
+ }
2053
+ getDependencyGroups(graph, limit) {
2054
+ const uniqueEdges = /* @__PURE__ */ new Map();
2055
+ for (const edge of graph.edges) {
2056
+ const key = `${edge.from}->${edge.to}`;
2057
+ if (!uniqueEdges.has(key)) uniqueEdges.set(key, {
2058
+ from: edge.from,
2059
+ to: edge.to
2060
+ });
2061
+ }
2062
+ const sortedEdges = [...uniqueEdges.values()].sort((a, b) => {
2063
+ const fromOrder = a.from.localeCompare(b.from);
2064
+ return fromOrder !== 0 ? fromOrder : a.to.localeCompare(b.to);
2065
+ }).slice(0, limit);
2066
+ const grouped = /* @__PURE__ */ new Map();
2067
+ for (const edge of sortedEdges) {
2068
+ const deps = grouped.get(edge.from);
2069
+ if (deps) deps.push(edge.to);
2070
+ else grouped.set(edge.from, [edge.to]);
2071
+ }
2072
+ const lines = [];
2073
+ const groups = [...grouped.entries()];
2074
+ for (let i = 0; i < groups.length; i++) {
2075
+ const [from, deps] = groups[i];
2076
+ lines.push(from);
2077
+ for (let depIndex = 0; depIndex < deps.length; depIndex++) {
2078
+ const dep = deps[depIndex];
2079
+ const prefix = depIndex === deps.length - 1 ? "└─" : "├─";
2080
+ lines.push(`${prefix} ${dep}`);
2081
+ }
2082
+ if (i < groups.length - 1) lines.push("");
2083
+ }
2084
+ return lines;
2085
+ }
2086
+ };
2087
+
1977
2088
  //#endregion
1978
2089
  //#region src/cli/project-resolver.ts
1979
2090
  const TSCONFIG_NAME = "tsconfig.json";
@@ -2168,7 +2279,7 @@ function enforceMinimumNodeVersion() {
2168
2279
  enforceMinimumNodeVersion();
2169
2280
  const program = new commander.Command();
2170
2281
  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);
2282
+ 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("--out <file>", "output file (stdout if omitted)").option("-v, --verbose", "show detailed parsing information", false);
2172
2283
  program.action(async (filePaths = [], options) => {
2173
2284
  try {
2174
2285
  const mergedFiles = mergeFileTargets(filePaths, options.files);
@@ -2184,7 +2295,11 @@ program.action(async (filePaths = [], options) => {
2184
2295
  "downstream",
2185
2296
  "both"
2186
2297
  ].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");
2298
+ if (options.format && ![
2299
+ "json",
2300
+ "mermaid",
2301
+ "text"
2302
+ ].includes(options.format)) throw ErrorHandler.createError(`Invalid format: ${options.format}. Must be 'text', 'json', or 'mermaid'`, "INVALID_ARGUMENTS");
2188
2303
  const tsconfigLikePositional = filePaths.find((filePath) => /(?:^|[\\/])tsconfig(?:\.[^/\\]+)?\.json$/.test(filePath));
2189
2304
  if (tsconfigLikePositional) throw ErrorHandler.createError(`Positional argument "${tsconfigLikePositional}" looks like a tsconfig. Use --project "${tsconfigLikePositional}" instead.`, "INVALID_ARGUMENTS");
2190
2305
  const cliOptions = {
@@ -2229,7 +2344,18 @@ program.action(async (filePaths = [], options) => {
2229
2344
  }
2230
2345
  let formatter;
2231
2346
  if (cliOptions.format === "mermaid") formatter = new MermaidFormatter(logger);
2232
- else formatter = new JsonFormatter(logger);
2347
+ else if (cliOptions.format === "json") formatter = new JsonFormatter(logger);
2348
+ else {
2349
+ const processingStats = parser.getProcessingStats();
2350
+ formatter = new TextFormatter({
2351
+ projectPath: cliOptions.project,
2352
+ direction: cliOptions.direction,
2353
+ entry: cliOptions.entry,
2354
+ processedFileCount: processingStats.processedFileCount,
2355
+ skippedFileCount: processingStats.skippedFileCount,
2356
+ warningCount: parser.getStructuredWarnings().totalCount
2357
+ }, logger);
2358
+ }
2233
2359
  const formattedOutput = formatter.format(graph);
2234
2360
  await new OutputHandler().writeOutput(formattedOutput, cliOptions.out);
2235
2361
  if (cliOptions.verbose && cliOptions.out) console.log(`✅ Output written to: ${cliOptions.out}`);