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 +48 -9
- package/dist/cli/index.cjs +132 -6
- package/dist/cli/index.cjs.map +1 -1
- 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
|
@@ -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
|
|
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
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)
|
|
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,
|
|
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>` | `
|
|
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
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
package/dist/cli/index.cjs
CHANGED
|
@@ -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:
|
|
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,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", "
|
|
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 && ![
|
|
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}`);
|