ng-di-graph 0.4.7 → 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
@@ -10,7 +10,7 @@ A command-line tool that analyzes Angular TypeScript codebases to extract depend
10
10
 
11
11
  ## Prerequisites
12
12
  - Node.js 20.x (npm 10+)
13
- - Angular project targeting v17-20 with a `tsconfig.json` you can point to
13
+ - Angular project targeting v17-20 with a `tsconfig.json` to discover or reference
14
14
 
15
15
  ## Installation
16
16
  ```bash
@@ -19,20 +19,23 @@ npm install -g ng-di-graph
19
19
 
20
20
  ## Quick start
21
21
  ```bash
22
- # Analyze the whole project and print JSON
23
- ng-di-graph --project ./tsconfig.json --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
- ng-di-graph --project ./tsconfig.json --format mermaid --out docs/di-graph.mmd
26
+ ng-di-graph --format mermaid --out docs/di-graph.mmd
27
27
 
28
28
  # Target specific files via positional filePaths
29
- ng-di-graph src/app/app.component.ts src/app/auth.service.ts --project ./tsconfig.json --format json
29
+ ng-di-graph src/app/app.component.ts src/app/auth.service.ts --format json
30
30
 
31
- # Use the default tsconfig at ./tsconfig.json (no --project needed)
31
+ # Auto-discover the nearest tsconfig.json (no --project needed)
32
32
  ng-di-graph src/app --format mermaid --out docs/di-graph.mmd
33
33
 
34
34
  # Focus on a symbol and see who depends on it
35
- ng-di-graph --project ./tsconfig.json --entry UserService --direction upstream
35
+ ng-di-graph --entry UserService --direction upstream
36
+
37
+ # Override auto-discovery with an explicit tsconfig
38
+ ng-di-graph --project ./tsconfig.json --format json
36
39
  ```
37
40
 
38
41
  ## Features
@@ -42,22 +45,24 @@ ng-di-graph --project ./tsconfig.json --entry UserService --direction upstream
42
45
  - 🔍 **Dependency Analysis** - Extract DI relationships from `@Injectable`, `@Component`, and `@Directive` classes
43
46
  - 🎯 **Constructor Injection** - Analyze constructor parameters with type annotations and `@Inject()` tokens
44
47
  - 🏷️ **Decorator Flags** - Capture `@Optional`, `@Self`, `@SkipSelf`, and `@Host` parameter decorators
45
- - 📊 **Multiple Output Formats** - JSON (machine-readable) and Mermaid (visual flowcharts)
48
+ - 📊 **Multiple Output Formats** - Text (default), JSON (machine-readable), Mermaid (visual flowcharts)
46
49
  - 🎨 **Entry Point Filtering** - Generate sub-graphs from specific starting nodes
47
50
  - 🔄 **Bidirectional Analysis** - Explore upstream dependencies, downstream consumers, or both
48
51
  - 🔁 **Circular Detection** - Automatically detect and report circular dependencies
49
52
 
50
53
  ## Common commands
51
- - Full project, JSON output:
52
- `ng-di-graph --project ./tsconfig.json --format json`
54
+ - Full project, text output (default):
55
+ `ng-di-graph`
56
+ - JSON output to stdout:
57
+ `ng-di-graph --format json`
53
58
  - Mermaid diagram to file:
54
- `ng-di-graph --project ./tsconfig.json --format mermaid --out docs/di-graph.mmd`
59
+ `ng-di-graph --format mermaid --out docs/di-graph.mmd`
55
60
  - Filter to specific symbols:
56
- `ng-di-graph --project ./tsconfig.json --entry AppComponent`
61
+ `ng-di-graph --entry AppComponent`
57
62
  - Upstream consumers of a service:
58
- `ng-di-graph --project ./tsconfig.json --entry UserService --direction upstream`
63
+ `ng-di-graph --entry UserService --direction upstream`
59
64
  - Include decorator flags with verbose logging:
60
- `ng-di-graph --project ./tsconfig.json --include-decorators --verbose`
65
+ `ng-di-graph --include-decorators --verbose`
61
66
 
62
67
  ## CLI options at a glance
63
68
 
@@ -68,9 +73,9 @@ ng-di-graph [filePaths...] [options]
68
73
  Option | Default | Description
69
74
  -- | -- | --
70
75
  `filePaths` | none | Positional alias for `--files`; combines with `--files` when both are set.
71
- `-p, --project <path>` | `./tsconfig.json` | Path to the TypeScript config file to analyze.
76
+ `-p, --project <path>` | auto-discovered | Path to the TypeScript config file to analyze (omit to auto-discover).
72
77
  `--files <paths...>` | none | Limit analysis to specific files or directories.
73
- `-f, --format <format>` | `json` | Output as `json` or `mermaid`.
78
+ `-f, --format <format>` | `text` | Output as `text`, `json`, or `mermaid`.
74
79
  `-e, --entry <symbol...>` | none | Start the graph from one or more symbols.
75
80
  `-d, --direction <dir>` | `downstream` | `downstream`, `upstream`, or `both` relative to entries.
76
81
  `--include-decorators` | `false` | Add `@Optional`, `@Self`, `@SkipSelf`, `@Host` flags to edges.
@@ -80,15 +85,47 @@ Option | Default | Description
80
85
 
81
86
  ## Output Formats
82
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
+
83
104
  ### JSON Format
84
105
 
85
106
  ```json
86
107
  {
87
108
  "nodes": [
88
- { "id": "AppComponent", "kind": "component" },
89
- { "id": "UserService", "kind": "service" },
90
- { "id": "AuthService", "kind": "service" },
91
- { "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
+ }
92
129
  ],
93
130
  "edges": [
94
131
  {
@@ -116,6 +153,11 @@ Option | Default | Description
116
153
  - `directive` - Classes decorated with `@Directive()`
117
154
  - `unknown` - Could not determine decorator type
118
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
+
119
161
  **Edge Flags** (when `--include-decorators` is used):
120
162
  - `optional` - Parameter has `@Optional()` decorator
121
163
  - `self` - Parameter has `@Self()` decorator
@@ -26,10 +26,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  }) : target, mod));
27
27
 
28
28
  //#endregion
29
+ let commander = require("commander");
29
30
  let node_fs = require("node:fs");
30
31
  let node_path = require("node:path");
31
32
  node_path = __toESM(node_path);
32
- let commander = require("commander");
33
33
  let ts_morph = require("ts-morph");
34
34
  let typescript = require("typescript");
35
35
  typescript = __toESM(typescript);
@@ -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
  }
@@ -402,11 +405,11 @@ function detectCircularDependencies(edges, nodes) {
402
405
  /**
403
406
  * DFS helper function to detect cycles
404
407
  */
405
- function dfs(node, path$1) {
408
+ function dfs(node, path$2) {
406
409
  if (processedNodes.has(node)) return;
407
410
  if (recursionStack.has(node)) {
408
- const cycleStartIndex = path$1.indexOf(node);
409
- const cyclePath = [...path$1.slice(cycleStartIndex), node];
411
+ const cycleStartIndex = path$2.indexOf(node);
412
+ const cyclePath = [...path$2.slice(cycleStartIndex), node];
410
413
  circularDependencies.push(cyclePath);
411
414
  for (let i = 0; i < cyclePath.length - 1; i++) {
412
415
  const edgeKey = `${cyclePath[i]}->${cyclePath[i + 1]}`;
@@ -415,7 +418,7 @@ function detectCircularDependencies(edges, nodes) {
415
418
  return;
416
419
  }
417
420
  recursionStack.add(node);
418
- const newPath = [...path$1, node];
421
+ const newPath = [...path$2, node];
419
422
  const neighbors = adjacencyList.get(node) || [];
420
423
  for (const neighbor of neighbors) dfs(neighbor, newPath);
421
424
  recursionStack.delete(node);
@@ -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,249 @@ 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
+
2088
+ //#endregion
2089
+ //#region src/cli/project-resolver.ts
2090
+ const TSCONFIG_NAME = "tsconfig.json";
2091
+ const WORKSPACE_FILES = ["angular.json", "workspace.json"];
2092
+ const WORKSPACE_TARGET_ORDER = [
2093
+ "build",
2094
+ "test",
2095
+ "serve",
2096
+ "lint",
2097
+ "e2e"
2098
+ ];
2099
+ function resolveProjectPath(options) {
2100
+ const { projectOption, fileTargets, cwd, logger } = options;
2101
+ if (projectOption) {
2102
+ const resolved = normalizeExplicitProject(projectOption);
2103
+ logDiscovery(logger, "Using explicit --project path", { project: resolved });
2104
+ return resolved;
2105
+ }
2106
+ const resolvedFromTargets = resolveFromFileTargets(fileTargets, cwd, logger);
2107
+ if (resolvedFromTargets) {
2108
+ logDiscovery(logger, "Auto-discovered tsconfig.json from file targets", { project: resolvedFromTargets });
2109
+ return resolvedFromTargets;
2110
+ }
2111
+ const startDir = fileTargets.length > 0 ? getCommonAncestor(fileTargets, cwd) : cwd;
2112
+ const tsconfigPath = findNearestTsconfig(startDir);
2113
+ if (tsconfigPath) {
2114
+ logDiscovery(logger, "Auto-discovered tsconfig.json", {
2115
+ startDir,
2116
+ project: tsconfigPath
2117
+ });
2118
+ return tsconfigPath;
2119
+ }
2120
+ const workspaceResolution = resolveWorkspaceTsconfig(startDir);
2121
+ if (workspaceResolution) {
2122
+ logDiscovery(logger, "Resolved tsconfig via Angular workspace", {
2123
+ workspace: workspaceResolution.workspacePath,
2124
+ project: workspaceResolution.tsconfigPath
2125
+ });
2126
+ return workspaceResolution.tsconfigPath;
2127
+ }
2128
+ throw ErrorHandler.createError(`tsconfig.json not found starting from: ${startDir}`, "TSCONFIG_NOT_FOUND", startDir, { startDir });
2129
+ }
2130
+ function normalizeExplicitProject(projectOption) {
2131
+ try {
2132
+ if ((0, node_fs.statSync)(projectOption).isDirectory()) {
2133
+ const tsconfigPath = node_path.default.join(projectOption, TSCONFIG_NAME);
2134
+ if ((0, node_fs.existsSync)(tsconfigPath)) return tsconfigPath;
2135
+ }
2136
+ } catch {}
2137
+ return projectOption;
2138
+ }
2139
+ function resolveFromFileTargets(fileTargets, cwd, logger) {
2140
+ if (fileTargets.length === 0) return;
2141
+ const targetDirectories = getTargetDirectories(fileTargets, cwd);
2142
+ const resolvedConfigs = /* @__PURE__ */ new Map();
2143
+ const missingTargets = [];
2144
+ for (const directory of targetDirectories) {
2145
+ const configPath = findNearestTsconfig(directory);
2146
+ if (configPath) {
2147
+ const targets = resolvedConfigs.get(configPath) ?? [];
2148
+ targets.push(directory);
2149
+ resolvedConfigs.set(configPath, targets);
2150
+ } else missingTargets.push(directory);
2151
+ }
2152
+ if (resolvedConfigs.size > 1) {
2153
+ const configPaths = Array.from(resolvedConfigs.keys());
2154
+ throw ErrorHandler.createError("File targets resolve to multiple tsconfig.json files. Use --project to select one.", "INVALID_ARGUMENTS", void 0, { configs: configPaths });
2155
+ }
2156
+ if (resolvedConfigs.size === 1 && missingTargets.length > 0) {
2157
+ const [configPath] = resolvedConfigs.keys();
2158
+ throw ErrorHandler.createError("Some file targets do not resolve to a tsconfig.json. Use --project to select one.", "INVALID_ARGUMENTS", void 0, {
2159
+ configPath,
2160
+ missingTargets
2161
+ });
2162
+ }
2163
+ const [singleConfig] = resolvedConfigs.keys();
2164
+ if (singleConfig) {
2165
+ logDiscovery(logger, "Resolved tsconfig.json for file targets", { project: singleConfig });
2166
+ return singleConfig;
2167
+ }
2168
+ }
2169
+ function getTargetDirectories(fileTargets, cwd) {
2170
+ const directories = /* @__PURE__ */ new Set();
2171
+ for (const target of fileTargets) {
2172
+ const directory = resolveTargetDirectory(resolvePath(target, cwd));
2173
+ directories.add(directory);
2174
+ }
2175
+ return [...directories];
2176
+ }
2177
+ function resolveTargetDirectory(targetPath) {
2178
+ try {
2179
+ if ((0, node_fs.statSync)(targetPath).isDirectory()) return targetPath;
2180
+ } catch {}
2181
+ return node_path.default.dirname(targetPath);
2182
+ }
2183
+ function findNearestTsconfig(startDir) {
2184
+ return typescript.findConfigFile(startDir, typescript.sys.fileExists, TSCONFIG_NAME);
2185
+ }
2186
+ function resolveWorkspaceTsconfig(startDir) {
2187
+ for (const workspaceFile of WORKSPACE_FILES) {
2188
+ const workspacePath = typescript.findConfigFile(startDir, typescript.sys.fileExists, workspaceFile);
2189
+ if (!workspacePath) continue;
2190
+ const workspaceConfig = readJsonConfig(workspacePath);
2191
+ if (!workspaceConfig) continue;
2192
+ const tsconfigPath = selectWorkspaceTsconfig(workspaceConfig, workspacePath);
2193
+ if (tsconfigPath) return {
2194
+ workspacePath,
2195
+ tsconfigPath
2196
+ };
2197
+ }
2198
+ }
2199
+ function readJsonConfig(configPath) {
2200
+ const configFile = typescript.readConfigFile(configPath, typescript.sys.readFile);
2201
+ if (configFile.error) return;
2202
+ return configFile.config;
2203
+ }
2204
+ function selectWorkspaceTsconfig(workspaceConfig, workspacePath) {
2205
+ if (!workspaceConfig || typeof workspaceConfig !== "object") return;
2206
+ const config = workspaceConfig;
2207
+ const projects = config.projects ?? {};
2208
+ const projectNames = Object.keys(projects);
2209
+ if (projectNames.length === 0) return;
2210
+ const projectConfig = projects[(config.defaultProject && projects[config.defaultProject] ? config.defaultProject : projectNames.length === 1 ? projectNames[0] : void 0) ?? projectNames[0]];
2211
+ const tsconfig = findWorkspaceTargetTsconfig(projectConfig?.architect ?? projectConfig?.targets ?? {});
2212
+ if (!tsconfig) return;
2213
+ const resolvedPath = node_path.default.resolve(node_path.default.dirname(workspacePath), tsconfig);
2214
+ return (0, node_fs.existsSync)(resolvedPath) ? resolvedPath : void 0;
2215
+ }
2216
+ function findWorkspaceTargetTsconfig(targets) {
2217
+ for (const targetName of WORKSPACE_TARGET_ORDER) {
2218
+ const tsconfig = targets[targetName]?.options?.tsConfig;
2219
+ if (tsconfig) return tsconfig;
2220
+ }
2221
+ for (const targetName of Object.keys(targets).sort()) {
2222
+ const tsconfig = targets[targetName]?.options?.tsConfig;
2223
+ if (tsconfig) return tsconfig;
2224
+ }
2225
+ }
2226
+ function getCommonAncestor(fileTargets, cwd) {
2227
+ const targetDirs = getTargetDirectories(fileTargets, cwd);
2228
+ if (targetDirs.length === 0) return cwd;
2229
+ const [first, ...rest] = targetDirs.map((dir) => node_path.default.resolve(dir));
2230
+ const baseParts = splitPath(first);
2231
+ let commonLength = baseParts.length;
2232
+ for (const dir of rest) {
2233
+ const parts = splitPath(dir);
2234
+ commonLength = Math.min(commonLength, parts.length);
2235
+ for (let i = 0; i < commonLength; i += 1) if (parts[i] !== baseParts[i]) {
2236
+ commonLength = i;
2237
+ break;
2238
+ }
2239
+ }
2240
+ const commonParts = baseParts.slice(0, commonLength);
2241
+ if (commonParts.length === 0) return node_path.default.parse(first).root;
2242
+ const [root, ...segments] = commonParts;
2243
+ return root ? node_path.default.join(root, ...segments) : node_path.default.join(...segments);
2244
+ }
2245
+ function splitPath(value) {
2246
+ const resolved = node_path.default.resolve(value);
2247
+ const { root } = node_path.default.parse(resolved);
2248
+ const segments = resolved.slice(root.length).split(node_path.default.sep).filter(Boolean);
2249
+ return root ? [root, ...segments] : segments;
2250
+ }
2251
+ function resolvePath(value, cwd) {
2252
+ return node_path.default.isAbsolute(value) ? value : node_path.default.resolve(cwd, value);
2253
+ }
2254
+ function logDiscovery(logger, message, context) {
2255
+ logger?.info(LogCategory.FILE_PROCESSING, message, context);
2256
+ }
2257
+
1977
2258
  //#endregion
1978
2259
  //#region src/cli/index.ts
1979
2260
  /**
@@ -1996,31 +2277,31 @@ function enforceMinimumNodeVersion() {
1996
2277
  process.exit(1);
1997
2278
  }
1998
2279
  enforceMinimumNodeVersion();
1999
- function resolveProjectPath(projectOption) {
2000
- const candidatePath = projectOption;
2001
- try {
2002
- if ((0, node_fs.statSync)(candidatePath).isDirectory()) {
2003
- const tsconfigPath = (0, node_path.join)(candidatePath, "tsconfig.json");
2004
- if ((0, node_fs.existsSync)(tsconfigPath)) return tsconfigPath;
2005
- }
2006
- } catch {}
2007
- return candidatePath;
2008
- }
2009
2280
  const program = new commander.Command();
2010
2281
  program.name("ng-di-graph").description("Angular DI dependency graph CLI tool").version("0.1.0");
2011
- program.argument("[filePaths...]", "TypeScript files to analyze (alias for --files)").option("-p, --project <path>", "tsconfig.json path", "./tsconfig.json").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);
2012
2283
  program.action(async (filePaths = [], options) => {
2013
2284
  try {
2014
- const project = resolveProjectPath(options.project);
2285
+ const mergedFiles = mergeFileTargets(filePaths, options.files);
2286
+ const logger = createLogger(options.verbose);
2287
+ const project = resolveProjectPath({
2288
+ projectOption: options.project,
2289
+ fileTargets: mergedFiles,
2290
+ cwd: process.cwd(),
2291
+ logger
2292
+ });
2015
2293
  if (options.direction && ![
2016
2294
  "upstream",
2017
2295
  "downstream",
2018
2296
  "both"
2019
2297
  ].includes(options.direction)) throw ErrorHandler.createError(`Invalid direction: ${options.direction}. Must be 'upstream', 'downstream', or 'both'`, "INVALID_ARGUMENTS");
2020
- 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");
2021
2303
  const tsconfigLikePositional = filePaths.find((filePath) => /(?:^|[\\/])tsconfig(?:\.[^/\\]+)?\.json$/.test(filePath));
2022
2304
  if (tsconfigLikePositional) throw ErrorHandler.createError(`Positional argument "${tsconfigLikePositional}" looks like a tsconfig. Use --project "${tsconfigLikePositional}" instead.`, "INVALID_ARGUMENTS");
2023
- const mergedFiles = mergeFileTargets(filePaths, options.files);
2024
2305
  const cliOptions = {
2025
2306
  project,
2026
2307
  files: mergedFiles.length > 0 ? mergedFiles : void 0,
@@ -2031,7 +2312,6 @@ program.action(async (filePaths = [], options) => {
2031
2312
  out: options.out,
2032
2313
  verbose: options.verbose
2033
2314
  };
2034
- const logger = createLogger(cliOptions.verbose);
2035
2315
  if (logger) {
2036
2316
  logger.time("total-execution");
2037
2317
  logger.info(LogCategory.FILE_PROCESSING, "CLI execution started", {
@@ -2064,7 +2344,18 @@ program.action(async (filePaths = [], options) => {
2064
2344
  }
2065
2345
  let formatter;
2066
2346
  if (cliOptions.format === "mermaid") formatter = new MermaidFormatter(logger);
2067
- 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
+ }
2068
2359
  const formattedOutput = formatter.format(graph);
2069
2360
  await new OutputHandler().writeOutput(formattedOutput, cliOptions.out);
2070
2361
  if (cliOptions.verbose && cliOptions.out) console.log(`✅ Output written to: ${cliOptions.out}`);