ng-di-graph 0.4.6 → 0.5.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 +70 -136
- package/dist/cli/index.cjs +184 -19
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/project-resolver.d.ts +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,95 +8,78 @@ A command-line tool that analyzes Angular TypeScript codebases to extract depend
|
|
|
8
8
|
|
|
9
9
|
**Target Angular Versions:** 17-20
|
|
10
10
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
- npm 10+ (ships with Node 20). Use your preferred version manager (mise recommended) to match the pinned runtime before installing dependencies.
|
|
15
|
-
|
|
16
|
-
## Features
|
|
17
|
-
|
|
18
|
-
✨ **Complete Feature Set** - Production-ready dependency graph analysis for Angular applications
|
|
19
|
-
|
|
20
|
-
- 🔍 **Dependency Analysis** - Extract DI relationships from `@Injectable`, `@Component`, and `@Directive` classes
|
|
21
|
-
- 🎯 **Constructor Injection** - Analyze constructor parameters with type annotations and `@Inject()` tokens
|
|
22
|
-
- 🏷️ **Decorator Flags** - Capture `@Optional`, `@Self`, `@SkipSelf`, and `@Host` parameter decorators
|
|
23
|
-
- 📊 **Multiple Output Formats** - JSON (machine-readable) and Mermaid (visual flowcharts)
|
|
24
|
-
- 🎨 **Entry Point Filtering** - Generate sub-graphs from specific starting nodes
|
|
25
|
-
- 🔄 **Bidirectional Analysis** - Explore upstream dependencies, downstream consumers, or both
|
|
26
|
-
- 🔁 **Circular Detection** - Automatically detect and report circular dependencies
|
|
11
|
+
## Prerequisites
|
|
12
|
+
- Node.js 20.x (npm 10+)
|
|
13
|
+
- Angular project targeting v17-20 with a `tsconfig.json` to discover or reference
|
|
27
14
|
|
|
28
15
|
## Installation
|
|
29
|
-
|
|
30
16
|
```bash
|
|
31
17
|
npm install -g ng-di-graph
|
|
32
18
|
```
|
|
33
19
|
|
|
34
|
-
##
|
|
35
|
-
|
|
20
|
+
## Quick start
|
|
36
21
|
```bash
|
|
37
|
-
#
|
|
38
|
-
ng-di-graph
|
|
22
|
+
# Analyze the whole project and print JSON (auto-discover tsconfig)
|
|
23
|
+
ng-di-graph --format json
|
|
39
24
|
|
|
40
|
-
#
|
|
41
|
-
ng-di-graph --
|
|
25
|
+
# Generate a Mermaid diagram and save it
|
|
26
|
+
ng-di-graph --format mermaid --out docs/di-graph.mmd
|
|
42
27
|
|
|
43
|
-
#
|
|
44
|
-
ng-di-graph
|
|
28
|
+
# Target specific files via positional filePaths
|
|
29
|
+
ng-di-graph src/app/app.component.ts src/app/auth.service.ts --format json
|
|
45
30
|
|
|
46
|
-
#
|
|
47
|
-
ng-di-graph
|
|
31
|
+
# Auto-discover the nearest tsconfig.json (no --project needed)
|
|
32
|
+
ng-di-graph src/app --format mermaid --out docs/di-graph.mmd
|
|
48
33
|
|
|
49
|
-
# Focus on a
|
|
50
|
-
ng-di-graph --
|
|
34
|
+
# Focus on a symbol and see who depends on it
|
|
35
|
+
ng-di-graph --entry UserService --direction upstream
|
|
51
36
|
|
|
52
|
-
#
|
|
53
|
-
ng-di-graph --project ./tsconfig.json --
|
|
37
|
+
# Override auto-discovery with an explicit tsconfig
|
|
38
|
+
ng-di-graph --project ./tsconfig.json --format json
|
|
39
|
+
```
|
|
54
40
|
|
|
55
|
-
|
|
56
|
-
ng-di-graph --project ./tsconfig.json --include-decorators
|
|
41
|
+
## Features
|
|
57
42
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
43
|
+
✨ **Complete Feature Set** - Production-ready dependency graph analysis for Angular applications
|
|
44
|
+
|
|
45
|
+
- 🔍 **Dependency Analysis** - Extract DI relationships from `@Injectable`, `@Component`, and `@Directive` classes
|
|
46
|
+
- 🎯 **Constructor Injection** - Analyze constructor parameters with type annotations and `@Inject()` tokens
|
|
47
|
+
- 🏷️ **Decorator Flags** - Capture `@Optional`, `@Self`, `@SkipSelf`, and `@Host` parameter decorators
|
|
48
|
+
- 📊 **Multiple Output Formats** - JSON (machine-readable) and Mermaid (visual flowcharts)
|
|
49
|
+
- 🎨 **Entry Point Filtering** - Generate sub-graphs from specific starting nodes
|
|
50
|
+
- 🔄 **Bidirectional Analysis** - Explore upstream dependencies, downstream consumers, or both
|
|
51
|
+
- 🔁 **Circular Detection** - Automatically detect and report circular dependencies
|
|
61
52
|
|
|
62
|
-
##
|
|
53
|
+
## Common commands
|
|
54
|
+
- Full project, JSON output:
|
|
55
|
+
`ng-di-graph --format json`
|
|
56
|
+
- Mermaid diagram to file:
|
|
57
|
+
`ng-di-graph --format mermaid --out docs/di-graph.mmd`
|
|
58
|
+
- Filter to specific symbols:
|
|
59
|
+
`ng-di-graph --entry AppComponent`
|
|
60
|
+
- Upstream consumers of a service:
|
|
61
|
+
`ng-di-graph --entry UserService --direction upstream`
|
|
62
|
+
- Include decorator flags with verbose logging:
|
|
63
|
+
`ng-di-graph --include-decorators --verbose`
|
|
64
|
+
|
|
65
|
+
## CLI options at a glance
|
|
63
66
|
|
|
64
67
|
```
|
|
65
68
|
ng-di-graph [filePaths...] [options]
|
|
66
|
-
|
|
67
|
-
Arguments:
|
|
68
|
-
filePaths Optional list of files/directories to analyze (alias for --files)
|
|
69
|
-
|
|
70
|
-
Options:
|
|
71
|
-
-p, --project <path> Path to tsconfig.json (default: ./tsconfig.json)
|
|
72
|
-
--files <paths...> Specific file paths to analyze (similar to eslint targets)
|
|
73
|
-
-f, --format <format> Output format: json | mermaid (default: json)
|
|
74
|
-
-e, --entry <symbol...> Starting nodes for sub-graph filtering
|
|
75
|
-
-d, --direction <dir> Filter direction: upstream | downstream | both (default: downstream)
|
|
76
|
-
--include-decorators Include @Optional, @Self, @SkipSelf, @Host flags in output
|
|
77
|
-
--out <file> Output file path (prints to stdout if omitted)
|
|
78
|
-
-v, --verbose Show detailed parsing and resolution information
|
|
79
|
-
-h, --help Display help information
|
|
80
69
|
```
|
|
81
70
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
- `downstream` (default): Show what the entry depends on
|
|
95
|
-
- `upstream`: Show what depends on the entry
|
|
96
|
-
- `both`: Show both upstream and downstream dependencies
|
|
97
|
-
- **`--include-decorators`**: Add parameter decorator information to edge flags
|
|
98
|
-
- **`--out`**: Save output to a file instead of stdout
|
|
99
|
-
- **`--verbose`**: Enable detailed logging including timing metrics, memory usage, and type resolution details
|
|
71
|
+
Option | Default | Description
|
|
72
|
+
-- | -- | --
|
|
73
|
+
`filePaths` | none | Positional alias for `--files`; combines with `--files` when both are set.
|
|
74
|
+
`-p, --project <path>` | auto-discovered | Path to the TypeScript config file to analyze (omit to auto-discover).
|
|
75
|
+
`--files <paths...>` | none | Limit analysis to specific files or directories.
|
|
76
|
+
`-f, --format <format>` | `json` | Output as `json` or `mermaid`.
|
|
77
|
+
`-e, --entry <symbol...>` | none | Start the graph from one or more symbols.
|
|
78
|
+
`-d, --direction <dir>` | `downstream` | `downstream`, `upstream`, or `both` relative to entries.
|
|
79
|
+
`--include-decorators` | `false` | Add `@Optional`, `@Self`, `@SkipSelf`, `@Host` flags to edges.
|
|
80
|
+
`--out <file>` | stdout | Write output to a file.
|
|
81
|
+
`-v, --verbose` | `false` | Show detailed parsing and resolution logs.
|
|
82
|
+
`-h, --help` | `false` | Display CLI help.
|
|
100
83
|
|
|
101
84
|
## Output Formats
|
|
102
85
|
|
|
@@ -107,21 +90,26 @@ Options:
|
|
|
107
90
|
"nodes": [
|
|
108
91
|
{ "id": "AppComponent", "kind": "component" },
|
|
109
92
|
{ "id": "UserService", "kind": "service" },
|
|
110
|
-
{ "id": "AuthService", "kind": "service" }
|
|
93
|
+
{ "id": "AuthService", "kind": "service" },
|
|
94
|
+
{ "id": "Logger", "kind": "service" }
|
|
111
95
|
],
|
|
112
96
|
"edges": [
|
|
113
97
|
{
|
|
114
98
|
"from": "AppComponent",
|
|
115
|
-
"to": "UserService"
|
|
116
|
-
"flags": { "optional": false }
|
|
99
|
+
"to": "UserService"
|
|
117
100
|
},
|
|
118
101
|
{
|
|
119
102
|
"from": "UserService",
|
|
120
103
|
"to": "AuthService",
|
|
121
|
-
"flags": { "optional": true, "self": false }
|
|
104
|
+
"flags": { "optional": true, "self": false, "skipSelf": false, "host": false }
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"from": "AuthService",
|
|
108
|
+
"to": "Logger",
|
|
109
|
+
"flags": { "optional": false, "self": false, "skipSelf": false, "host": false }
|
|
122
110
|
}
|
|
123
111
|
],
|
|
124
|
-
"circularDependencies": []
|
|
112
|
+
"circularDependencies": [["UserService", "AuthService", "UserService"]]
|
|
125
113
|
}
|
|
126
114
|
```
|
|
127
115
|
|
|
@@ -145,66 +133,18 @@ flowchart LR
|
|
|
145
133
|
UserService --> AuthService
|
|
146
134
|
```
|
|
147
135
|
|
|
148
|
-
Mermaid diagrams can be
|
|
149
|
-
- Rendered in GitHub/GitLab markdown
|
|
150
|
-
- Viewed in the [Mermaid Live Editor](https://mermaid.live/)
|
|
151
|
-
- Embedded in documentation sites
|
|
152
|
-
- Converted to images using CLI tools
|
|
153
|
-
|
|
154
|
-
## Use Cases
|
|
155
|
-
|
|
156
|
-
### 1. Test Planning
|
|
157
|
-
Quickly identify all dependencies of a component to plan test mocks:
|
|
158
|
-
```bash
|
|
159
|
-
ng-di-graph --project ./tsconfig.json --entry MyComponent --format json
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### 2. Impact Analysis
|
|
163
|
-
Find all consumers of a service before making breaking changes:
|
|
164
|
-
```bash
|
|
165
|
-
ng-di-graph --project ./tsconfig.json --entry UserService --direction upstream
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### 3. Documentation
|
|
169
|
-
Generate visual dependency diagrams for README or design docs:
|
|
170
|
-
```bash
|
|
171
|
-
ng-di-graph --project ./tsconfig.json --format mermaid --out docs/architecture.mmd
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### 4. Circular Dependency Detection
|
|
175
|
-
Identify circular dependencies in your DI graph:
|
|
176
|
-
```bash
|
|
177
|
-
ng-di-graph --project ./tsconfig.json --verbose
|
|
178
|
-
# Check the "circularDependencies" array in output
|
|
179
|
-
```
|
|
136
|
+
Mermaid diagrams can be rendered in GitHub/GitLab markdown, viewed in the [Mermaid Live Editor](https://mermaid.live/), or converted to images with CLI tools.
|
|
180
137
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
# Shows detailed type resolution and warnings
|
|
186
|
-
```
|
|
138
|
+
## Use cases
|
|
139
|
+
- Test planning: surface dependencies to decide what to mock.
|
|
140
|
+
- Impact analysis: list consumers before changing a service.
|
|
141
|
+
- Documentation: add Mermaid diagrams to READMEs or architecture docs.
|
|
187
142
|
|
|
188
143
|
## Release workflow
|
|
144
|
+
Releases are automated via Release Please; tag pushes run lint → typecheck → test → build → pack → publish. Maintainer runbook: see `docs/release/ci-publish.md`.
|
|
189
145
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
- Maintainer runbook: see docs/release/ci-publish.md.
|
|
193
|
-
|
|
194
|
-
## Error Handling
|
|
195
|
-
|
|
196
|
-
The tool provides graceful error handling:
|
|
197
|
-
|
|
198
|
-
- **File Parsing Failures** - Skips unparseable files and continues processing
|
|
199
|
-
- **Type Resolution Issues** - Logs warnings for `any`/`unknown` types
|
|
200
|
-
- **Missing tsconfig.json** - Clear error message with suggestion
|
|
201
|
-
- **Invalid CLI Arguments** - Help message with usage examples
|
|
202
|
-
- **Circular Dependencies** - Detected and reported without blocking analysis
|
|
203
|
-
- **Memory Constraints** - Suggestions for chunking large codebases
|
|
204
|
-
|
|
205
|
-
Exit codes:
|
|
206
|
-
- `0` - Success
|
|
207
|
-
- `1` - Fatal error (invalid config, parsing failure, etc.)
|
|
146
|
+
## Error handling
|
|
147
|
+
The CLI reports clear failures and continues past unparseable files; rerun with `--verbose` for detailed logs.
|
|
208
148
|
|
|
209
149
|
## Contributing
|
|
210
150
|
|
|
@@ -213,9 +153,3 @@ Contributions are welcome! Please see our [Contributing Guide](CONTRIBUTING.md)
|
|
|
213
153
|
## License
|
|
214
154
|
|
|
215
155
|
MIT License - See [LICENSE](LICENSE) file for details
|
|
216
|
-
|
|
217
|
-
## Version
|
|
218
|
-
|
|
219
|
-
Current version: **0.1.0**
|
|
220
|
-
|
|
221
|
-
All core features are complete and production-ready.
|
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);
|
|
@@ -402,11 +402,11 @@ function detectCircularDependencies(edges, nodes) {
|
|
|
402
402
|
/**
|
|
403
403
|
* DFS helper function to detect cycles
|
|
404
404
|
*/
|
|
405
|
-
function dfs(node, path$
|
|
405
|
+
function dfs(node, path$2) {
|
|
406
406
|
if (processedNodes.has(node)) return;
|
|
407
407
|
if (recursionStack.has(node)) {
|
|
408
|
-
const cycleStartIndex = path$
|
|
409
|
-
const cyclePath = [...path$
|
|
408
|
+
const cycleStartIndex = path$2.indexOf(node);
|
|
409
|
+
const cyclePath = [...path$2.slice(cycleStartIndex), node];
|
|
410
410
|
circularDependencies.push(cyclePath);
|
|
411
411
|
for (let i = 0; i < cyclePath.length - 1; i++) {
|
|
412
412
|
const edgeKey = `${cyclePath[i]}->${cyclePath[i + 1]}`;
|
|
@@ -415,7 +415,7 @@ function detectCircularDependencies(edges, nodes) {
|
|
|
415
415
|
return;
|
|
416
416
|
}
|
|
417
417
|
recursionStack.add(node);
|
|
418
|
-
const newPath = [...path$
|
|
418
|
+
const newPath = [...path$2, node];
|
|
419
419
|
const neighbors = adjacencyList.get(node) || [];
|
|
420
420
|
for (const neighbor of neighbors) dfs(neighbor, newPath);
|
|
421
421
|
recursionStack.delete(node);
|
|
@@ -1974,6 +1974,176 @@ var MermaidFormatter = class {
|
|
|
1974
1974
|
}
|
|
1975
1975
|
};
|
|
1976
1976
|
|
|
1977
|
+
//#endregion
|
|
1978
|
+
//#region src/cli/project-resolver.ts
|
|
1979
|
+
const TSCONFIG_NAME = "tsconfig.json";
|
|
1980
|
+
const WORKSPACE_FILES = ["angular.json", "workspace.json"];
|
|
1981
|
+
const WORKSPACE_TARGET_ORDER = [
|
|
1982
|
+
"build",
|
|
1983
|
+
"test",
|
|
1984
|
+
"serve",
|
|
1985
|
+
"lint",
|
|
1986
|
+
"e2e"
|
|
1987
|
+
];
|
|
1988
|
+
function resolveProjectPath(options) {
|
|
1989
|
+
const { projectOption, fileTargets, cwd, logger } = options;
|
|
1990
|
+
if (projectOption) {
|
|
1991
|
+
const resolved = normalizeExplicitProject(projectOption);
|
|
1992
|
+
logDiscovery(logger, "Using explicit --project path", { project: resolved });
|
|
1993
|
+
return resolved;
|
|
1994
|
+
}
|
|
1995
|
+
const resolvedFromTargets = resolveFromFileTargets(fileTargets, cwd, logger);
|
|
1996
|
+
if (resolvedFromTargets) {
|
|
1997
|
+
logDiscovery(logger, "Auto-discovered tsconfig.json from file targets", { project: resolvedFromTargets });
|
|
1998
|
+
return resolvedFromTargets;
|
|
1999
|
+
}
|
|
2000
|
+
const startDir = fileTargets.length > 0 ? getCommonAncestor(fileTargets, cwd) : cwd;
|
|
2001
|
+
const tsconfigPath = findNearestTsconfig(startDir);
|
|
2002
|
+
if (tsconfigPath) {
|
|
2003
|
+
logDiscovery(logger, "Auto-discovered tsconfig.json", {
|
|
2004
|
+
startDir,
|
|
2005
|
+
project: tsconfigPath
|
|
2006
|
+
});
|
|
2007
|
+
return tsconfigPath;
|
|
2008
|
+
}
|
|
2009
|
+
const workspaceResolution = resolveWorkspaceTsconfig(startDir);
|
|
2010
|
+
if (workspaceResolution) {
|
|
2011
|
+
logDiscovery(logger, "Resolved tsconfig via Angular workspace", {
|
|
2012
|
+
workspace: workspaceResolution.workspacePath,
|
|
2013
|
+
project: workspaceResolution.tsconfigPath
|
|
2014
|
+
});
|
|
2015
|
+
return workspaceResolution.tsconfigPath;
|
|
2016
|
+
}
|
|
2017
|
+
throw ErrorHandler.createError(`tsconfig.json not found starting from: ${startDir}`, "TSCONFIG_NOT_FOUND", startDir, { startDir });
|
|
2018
|
+
}
|
|
2019
|
+
function normalizeExplicitProject(projectOption) {
|
|
2020
|
+
try {
|
|
2021
|
+
if ((0, node_fs.statSync)(projectOption).isDirectory()) {
|
|
2022
|
+
const tsconfigPath = node_path.default.join(projectOption, TSCONFIG_NAME);
|
|
2023
|
+
if ((0, node_fs.existsSync)(tsconfigPath)) return tsconfigPath;
|
|
2024
|
+
}
|
|
2025
|
+
} catch {}
|
|
2026
|
+
return projectOption;
|
|
2027
|
+
}
|
|
2028
|
+
function resolveFromFileTargets(fileTargets, cwd, logger) {
|
|
2029
|
+
if (fileTargets.length === 0) return;
|
|
2030
|
+
const targetDirectories = getTargetDirectories(fileTargets, cwd);
|
|
2031
|
+
const resolvedConfigs = /* @__PURE__ */ new Map();
|
|
2032
|
+
const missingTargets = [];
|
|
2033
|
+
for (const directory of targetDirectories) {
|
|
2034
|
+
const configPath = findNearestTsconfig(directory);
|
|
2035
|
+
if (configPath) {
|
|
2036
|
+
const targets = resolvedConfigs.get(configPath) ?? [];
|
|
2037
|
+
targets.push(directory);
|
|
2038
|
+
resolvedConfigs.set(configPath, targets);
|
|
2039
|
+
} else missingTargets.push(directory);
|
|
2040
|
+
}
|
|
2041
|
+
if (resolvedConfigs.size > 1) {
|
|
2042
|
+
const configPaths = Array.from(resolvedConfigs.keys());
|
|
2043
|
+
throw ErrorHandler.createError("File targets resolve to multiple tsconfig.json files. Use --project to select one.", "INVALID_ARGUMENTS", void 0, { configs: configPaths });
|
|
2044
|
+
}
|
|
2045
|
+
if (resolvedConfigs.size === 1 && missingTargets.length > 0) {
|
|
2046
|
+
const [configPath] = resolvedConfigs.keys();
|
|
2047
|
+
throw ErrorHandler.createError("Some file targets do not resolve to a tsconfig.json. Use --project to select one.", "INVALID_ARGUMENTS", void 0, {
|
|
2048
|
+
configPath,
|
|
2049
|
+
missingTargets
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
const [singleConfig] = resolvedConfigs.keys();
|
|
2053
|
+
if (singleConfig) {
|
|
2054
|
+
logDiscovery(logger, "Resolved tsconfig.json for file targets", { project: singleConfig });
|
|
2055
|
+
return singleConfig;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
function getTargetDirectories(fileTargets, cwd) {
|
|
2059
|
+
const directories = /* @__PURE__ */ new Set();
|
|
2060
|
+
for (const target of fileTargets) {
|
|
2061
|
+
const directory = resolveTargetDirectory(resolvePath(target, cwd));
|
|
2062
|
+
directories.add(directory);
|
|
2063
|
+
}
|
|
2064
|
+
return [...directories];
|
|
2065
|
+
}
|
|
2066
|
+
function resolveTargetDirectory(targetPath) {
|
|
2067
|
+
try {
|
|
2068
|
+
if ((0, node_fs.statSync)(targetPath).isDirectory()) return targetPath;
|
|
2069
|
+
} catch {}
|
|
2070
|
+
return node_path.default.dirname(targetPath);
|
|
2071
|
+
}
|
|
2072
|
+
function findNearestTsconfig(startDir) {
|
|
2073
|
+
return typescript.findConfigFile(startDir, typescript.sys.fileExists, TSCONFIG_NAME);
|
|
2074
|
+
}
|
|
2075
|
+
function resolveWorkspaceTsconfig(startDir) {
|
|
2076
|
+
for (const workspaceFile of WORKSPACE_FILES) {
|
|
2077
|
+
const workspacePath = typescript.findConfigFile(startDir, typescript.sys.fileExists, workspaceFile);
|
|
2078
|
+
if (!workspacePath) continue;
|
|
2079
|
+
const workspaceConfig = readJsonConfig(workspacePath);
|
|
2080
|
+
if (!workspaceConfig) continue;
|
|
2081
|
+
const tsconfigPath = selectWorkspaceTsconfig(workspaceConfig, workspacePath);
|
|
2082
|
+
if (tsconfigPath) return {
|
|
2083
|
+
workspacePath,
|
|
2084
|
+
tsconfigPath
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
function readJsonConfig(configPath) {
|
|
2089
|
+
const configFile = typescript.readConfigFile(configPath, typescript.sys.readFile);
|
|
2090
|
+
if (configFile.error) return;
|
|
2091
|
+
return configFile.config;
|
|
2092
|
+
}
|
|
2093
|
+
function selectWorkspaceTsconfig(workspaceConfig, workspacePath) {
|
|
2094
|
+
if (!workspaceConfig || typeof workspaceConfig !== "object") return;
|
|
2095
|
+
const config = workspaceConfig;
|
|
2096
|
+
const projects = config.projects ?? {};
|
|
2097
|
+
const projectNames = Object.keys(projects);
|
|
2098
|
+
if (projectNames.length === 0) return;
|
|
2099
|
+
const projectConfig = projects[(config.defaultProject && projects[config.defaultProject] ? config.defaultProject : projectNames.length === 1 ? projectNames[0] : void 0) ?? projectNames[0]];
|
|
2100
|
+
const tsconfig = findWorkspaceTargetTsconfig(projectConfig?.architect ?? projectConfig?.targets ?? {});
|
|
2101
|
+
if (!tsconfig) return;
|
|
2102
|
+
const resolvedPath = node_path.default.resolve(node_path.default.dirname(workspacePath), tsconfig);
|
|
2103
|
+
return (0, node_fs.existsSync)(resolvedPath) ? resolvedPath : void 0;
|
|
2104
|
+
}
|
|
2105
|
+
function findWorkspaceTargetTsconfig(targets) {
|
|
2106
|
+
for (const targetName of WORKSPACE_TARGET_ORDER) {
|
|
2107
|
+
const tsconfig = targets[targetName]?.options?.tsConfig;
|
|
2108
|
+
if (tsconfig) return tsconfig;
|
|
2109
|
+
}
|
|
2110
|
+
for (const targetName of Object.keys(targets).sort()) {
|
|
2111
|
+
const tsconfig = targets[targetName]?.options?.tsConfig;
|
|
2112
|
+
if (tsconfig) return tsconfig;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
function getCommonAncestor(fileTargets, cwd) {
|
|
2116
|
+
const targetDirs = getTargetDirectories(fileTargets, cwd);
|
|
2117
|
+
if (targetDirs.length === 0) return cwd;
|
|
2118
|
+
const [first, ...rest] = targetDirs.map((dir) => node_path.default.resolve(dir));
|
|
2119
|
+
const baseParts = splitPath(first);
|
|
2120
|
+
let commonLength = baseParts.length;
|
|
2121
|
+
for (const dir of rest) {
|
|
2122
|
+
const parts = splitPath(dir);
|
|
2123
|
+
commonLength = Math.min(commonLength, parts.length);
|
|
2124
|
+
for (let i = 0; i < commonLength; i += 1) if (parts[i] !== baseParts[i]) {
|
|
2125
|
+
commonLength = i;
|
|
2126
|
+
break;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
const commonParts = baseParts.slice(0, commonLength);
|
|
2130
|
+
if (commonParts.length === 0) return node_path.default.parse(first).root;
|
|
2131
|
+
const [root, ...segments] = commonParts;
|
|
2132
|
+
return root ? node_path.default.join(root, ...segments) : node_path.default.join(...segments);
|
|
2133
|
+
}
|
|
2134
|
+
function splitPath(value) {
|
|
2135
|
+
const resolved = node_path.default.resolve(value);
|
|
2136
|
+
const { root } = node_path.default.parse(resolved);
|
|
2137
|
+
const segments = resolved.slice(root.length).split(node_path.default.sep).filter(Boolean);
|
|
2138
|
+
return root ? [root, ...segments] : segments;
|
|
2139
|
+
}
|
|
2140
|
+
function resolvePath(value, cwd) {
|
|
2141
|
+
return node_path.default.isAbsolute(value) ? value : node_path.default.resolve(cwd, value);
|
|
2142
|
+
}
|
|
2143
|
+
function logDiscovery(logger, message, context) {
|
|
2144
|
+
logger?.info(LogCategory.FILE_PROCESSING, message, context);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
1977
2147
|
//#endregion
|
|
1978
2148
|
//#region src/cli/index.ts
|
|
1979
2149
|
/**
|
|
@@ -1996,22 +2166,19 @@ function enforceMinimumNodeVersion() {
|
|
|
1996
2166
|
process.exit(1);
|
|
1997
2167
|
}
|
|
1998
2168
|
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
2169
|
const program = new commander.Command();
|
|
2010
2170
|
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
|
|
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);
|
|
2012
2172
|
program.action(async (filePaths = [], options) => {
|
|
2013
2173
|
try {
|
|
2014
|
-
const
|
|
2174
|
+
const mergedFiles = mergeFileTargets(filePaths, options.files);
|
|
2175
|
+
const logger = createLogger(options.verbose);
|
|
2176
|
+
const project = resolveProjectPath({
|
|
2177
|
+
projectOption: options.project,
|
|
2178
|
+
fileTargets: mergedFiles,
|
|
2179
|
+
cwd: process.cwd(),
|
|
2180
|
+
logger
|
|
2181
|
+
});
|
|
2015
2182
|
if (options.direction && ![
|
|
2016
2183
|
"upstream",
|
|
2017
2184
|
"downstream",
|
|
@@ -2020,7 +2187,6 @@ program.action(async (filePaths = [], options) => {
|
|
|
2020
2187
|
if (options.format && !["json", "mermaid"].includes(options.format)) throw ErrorHandler.createError(`Invalid format: ${options.format}. Must be 'json' or 'mermaid'`, "INVALID_ARGUMENTS");
|
|
2021
2188
|
const tsconfigLikePositional = filePaths.find((filePath) => /(?:^|[\\/])tsconfig(?:\.[^/\\]+)?\.json$/.test(filePath));
|
|
2022
2189
|
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
2190
|
const cliOptions = {
|
|
2025
2191
|
project,
|
|
2026
2192
|
files: mergedFiles.length > 0 ? mergedFiles : void 0,
|
|
@@ -2031,7 +2197,6 @@ program.action(async (filePaths = [], options) => {
|
|
|
2031
2197
|
out: options.out,
|
|
2032
2198
|
verbose: options.verbose
|
|
2033
2199
|
};
|
|
2034
|
-
const logger = createLogger(cliOptions.verbose);
|
|
2035
2200
|
if (logger) {
|
|
2036
2201
|
logger.time("total-execution");
|
|
2037
2202
|
logger.info(LogCategory.FILE_PROCESSING, "CLI execution started", {
|