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 +62 -20
- package/dist/cli/index.cjs +315 -24
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/project-resolver.d.ts +9 -0
- package/dist/core/parser.d.ts +8 -0
- package/dist/formatters/text-formatter.d.ts +9 -0
- package/dist/types/index.d.ts +18 -1
- package/package.json +1 -1
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`
|
|
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
|
|
23
|
-
ng-di-graph
|
|
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 --
|
|
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 --
|
|
29
|
+
ng-di-graph src/app/app.component.ts src/app/auth.service.ts --format json
|
|
30
30
|
|
|
31
|
-
#
|
|
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 --
|
|
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)
|
|
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,
|
|
52
|
-
`ng-di-graph
|
|
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 --
|
|
59
|
+
`ng-di-graph --format mermaid --out docs/di-graph.mmd`
|
|
55
60
|
- Filter to specific symbols:
|
|
56
|
-
`ng-di-graph --
|
|
61
|
+
`ng-di-graph --entry AppComponent`
|
|
57
62
|
- Upstream consumers of a service:
|
|
58
|
-
`ng-di-graph --
|
|
63
|
+
`ng-di-graph --entry UserService --direction upstream`
|
|
59
64
|
- Include decorator flags with verbose logging:
|
|
60
|
-
`ng-di-graph --
|
|
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>` |
|
|
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>` | `
|
|
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
|
-
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
package/dist/cli/index.cjs
CHANGED
|
@@ -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$
|
|
408
|
+
function dfs(node, path$2) {
|
|
406
409
|
if (processedNodes.has(node)) return;
|
|
407
410
|
if (recursionStack.has(node)) {
|
|
408
|
-
const cycleStartIndex = path$
|
|
409
|
-
const cyclePath = [...path$
|
|
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$
|
|
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:
|
|
986
|
-
filePath
|
|
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
|
|
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
|
|
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 && ![
|
|
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}`);
|