ng-di-graph 0.6.0 โ 0.8.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 +5 -2
- package/dist/cli/index.cjs +167 -63
- package/dist/cli/index.cjs.map +1 -1
- package/dist/core/parser.d.ts +11 -7
- package/dist/types/index.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
A command-line tool that analyzes Angular TypeScript codebases to extract dependency injection relationships.
|
|
8
8
|
|
|
9
|
-
**Target Angular Versions:** 17-
|
|
9
|
+
**Target Angular Versions:** 17-21
|
|
10
10
|
|
|
11
11
|
## Prerequisites
|
|
12
12
|
- Node.js 20.x (npm 10+)
|
|
13
|
-
- Angular project targeting v17-
|
|
13
|
+
- Angular project targeting v17-21 with a `tsconfig.json` to discover or reference
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
```bash
|
|
@@ -44,11 +44,13 @@ ng-di-graph --project ./tsconfig.json --format json
|
|
|
44
44
|
|
|
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
|
+
- โจ **Modern DI Support** - Analyze dependencies from constructor injection and the `inject()` function
|
|
47
48
|
- ๐ท๏ธ **Decorator Flags** - Capture `@Optional`, `@Self`, `@SkipSelf`, and `@Host` parameter decorators
|
|
48
49
|
- ๐ **Multiple Output Formats** - Text (default), JSON (machine-readable), Mermaid (visual flowcharts)
|
|
49
50
|
- ๐จ **Entry Point Filtering** - Generate sub-graphs from specific starting nodes
|
|
50
51
|
- ๐ **Bidirectional Analysis** - Explore upstream dependencies, downstream consumers, or both
|
|
51
52
|
- ๐ **Circular Detection** - Automatically detect and report circular dependencies
|
|
53
|
+
- ๐งน **Angular Core Filtering** - Hide `@angular/core` nodes by default (`--include-angular-core` to show)
|
|
52
54
|
|
|
53
55
|
## Common commands
|
|
54
56
|
- Full project, text output (default):
|
|
@@ -79,6 +81,7 @@ Option | Default | Description
|
|
|
79
81
|
`-e, --entry <symbol...>` | none | Start the graph from one or more symbols.
|
|
80
82
|
`-d, --direction <dir>` | `downstream` | `downstream`, `upstream`, or `both` relative to entries.
|
|
81
83
|
`--include-decorators` | `false` | Add `@Optional`, `@Self`, `@SkipSelf`, `@Host` flags to edges.
|
|
84
|
+
`--include-angular-core` | `false` | Include nodes that originate from `@angular/core` imports.
|
|
82
85
|
`--out <file>` | stdout | Write output to a file.
|
|
83
86
|
`-v, --verbose` | `false` | Show detailed parsing and resolution logs.
|
|
84
87
|
`-h, --help` | `false` | Display CLI help.
|
package/dist/cli/index.cjs
CHANGED
|
@@ -451,15 +451,20 @@ function buildGraph(parsedClasses, logger) {
|
|
|
451
451
|
let unknownNodeCount = 0;
|
|
452
452
|
for (const parsedClass of parsedClasses) for (const dependency of parsedClass.dependencies) {
|
|
453
453
|
if (!nodeMap.has(dependency.token)) {
|
|
454
|
-
|
|
454
|
+
const unknownNode = {
|
|
455
455
|
id: dependency.token,
|
|
456
456
|
kind: "unknown"
|
|
457
|
-
}
|
|
457
|
+
};
|
|
458
|
+
if (dependency.origin) unknownNode.origin = dependency.origin;
|
|
459
|
+
nodeMap.set(dependency.token, unknownNode);
|
|
458
460
|
unknownNodeCount++;
|
|
459
461
|
logger?.warn(LogCategory.GRAPH_CONSTRUCTION, `Created unknown node: ${dependency.token}`, {
|
|
460
462
|
nodeId: dependency.token,
|
|
461
463
|
referencedBy: parsedClass.name
|
|
462
464
|
});
|
|
465
|
+
} else {
|
|
466
|
+
const existingNode = nodeMap.get(dependency.token);
|
|
467
|
+
if (existingNode && existingNode.kind === "unknown" && !existingNode.origin && dependency.origin) existingNode.origin = dependency.origin;
|
|
463
468
|
}
|
|
464
469
|
const edge = {
|
|
465
470
|
from: parsedClass.name,
|
|
@@ -531,24 +536,57 @@ function validateAndTraverseEntryPoints(entryPoints, graph, adjacencyList, resul
|
|
|
531
536
|
* @returns Filtered graph containing only nodes reachable from entry points
|
|
532
537
|
*/
|
|
533
538
|
function filterGraph(graph, options, logger) {
|
|
534
|
-
|
|
539
|
+
const baseGraph = filterGraphByOrigin(graph, options.includeAngularCore ?? false);
|
|
540
|
+
if (!options.entry || options.entry.length === 0) {
|
|
541
|
+
const includedNodeIds$1 = new Set(baseGraph.nodes.map((node) => node.id));
|
|
542
|
+
return {
|
|
543
|
+
nodes: baseGraph.nodes,
|
|
544
|
+
edges: baseGraph.edges,
|
|
545
|
+
circularDependencies: filterCircularDependencies(baseGraph, includedNodeIds$1)
|
|
546
|
+
};
|
|
547
|
+
}
|
|
535
548
|
const includedNodeIds = /* @__PURE__ */ new Set();
|
|
536
549
|
if (options.direction === "both") {
|
|
537
|
-
const upstreamAdjacencyList = buildAdjacencyList(
|
|
550
|
+
const upstreamAdjacencyList = buildAdjacencyList(baseGraph, "upstream");
|
|
538
551
|
const upstreamNodes = /* @__PURE__ */ new Set();
|
|
539
|
-
validateAndTraverseEntryPoints(options.entry,
|
|
540
|
-
const downstreamAdjacencyList = buildAdjacencyList(
|
|
552
|
+
validateAndTraverseEntryPoints(options.entry, baseGraph, upstreamAdjacencyList, upstreamNodes, options, logger);
|
|
553
|
+
const downstreamAdjacencyList = buildAdjacencyList(baseGraph, "downstream");
|
|
541
554
|
const downstreamNodes = /* @__PURE__ */ new Set();
|
|
542
|
-
validateAndTraverseEntryPoints(options.entry,
|
|
555
|
+
validateAndTraverseEntryPoints(options.entry, baseGraph, downstreamAdjacencyList, downstreamNodes, options, logger);
|
|
543
556
|
const combinedNodes = new Set([...upstreamNodes, ...downstreamNodes]);
|
|
544
557
|
for (const nodeId of combinedNodes) includedNodeIds.add(nodeId);
|
|
545
558
|
} else {
|
|
546
|
-
const adjacencyList = buildAdjacencyList(
|
|
547
|
-
validateAndTraverseEntryPoints(options.entry,
|
|
559
|
+
const adjacencyList = buildAdjacencyList(baseGraph, options.direction);
|
|
560
|
+
validateAndTraverseEntryPoints(options.entry, baseGraph, adjacencyList, includedNodeIds, options, logger);
|
|
561
|
+
}
|
|
562
|
+
const filteredNodes = baseGraph.nodes.filter((node) => includedNodeIds.has(node.id));
|
|
563
|
+
const filteredEdges = baseGraph.edges.filter((edge) => includedNodeIds.has(edge.from) && includedNodeIds.has(edge.to));
|
|
564
|
+
const filteredCircularDeps = filterCircularDependencies(baseGraph, includedNodeIds);
|
|
565
|
+
if (options.verbose && logger) {
|
|
566
|
+
logger.info(LogCategory.FILTERING, "Filtered graph summary", {
|
|
567
|
+
nodeCount: filteredNodes.length,
|
|
568
|
+
edgeCount: filteredEdges.length
|
|
569
|
+
});
|
|
570
|
+
logger.debug(LogCategory.FILTERING, "Entry points applied", { entryPoints: options.entry });
|
|
548
571
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
572
|
+
return {
|
|
573
|
+
nodes: filteredNodes,
|
|
574
|
+
edges: filteredEdges,
|
|
575
|
+
circularDependencies: filteredCircularDeps
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function filterGraphByOrigin(graph, includeAngularCore) {
|
|
579
|
+
if (includeAngularCore) return graph;
|
|
580
|
+
const filteredNodes = graph.nodes.filter((node) => node.origin !== "angular-core");
|
|
581
|
+
const includedNodeIds = new Set(filteredNodes.map((node) => node.id));
|
|
582
|
+
return {
|
|
583
|
+
nodes: filteredNodes,
|
|
584
|
+
edges: graph.edges.filter((edge) => includedNodeIds.has(edge.from) && includedNodeIds.has(edge.to)),
|
|
585
|
+
circularDependencies: graph.circularDependencies
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function filterCircularDependencies(graph, includedNodeIds) {
|
|
589
|
+
return graph.circularDependencies.filter((cycle) => {
|
|
552
590
|
if (cycle.length < 2) return false;
|
|
553
591
|
const isSelfLoop = cycle.length === 2 && cycle[0] === cycle[1];
|
|
554
592
|
const isProperCycleWithClosing = cycle.length >= 3 && cycle[0] === cycle[cycle.length - 1];
|
|
@@ -563,18 +601,6 @@ function filterGraph(graph, options, logger) {
|
|
|
563
601
|
}
|
|
564
602
|
return true;
|
|
565
603
|
});
|
|
566
|
-
if (options.verbose && logger) {
|
|
567
|
-
logger.info(LogCategory.FILTERING, "Filtered graph summary", {
|
|
568
|
-
nodeCount: filteredNodes.length,
|
|
569
|
-
edgeCount: filteredEdges.length
|
|
570
|
-
});
|
|
571
|
-
logger.debug(LogCategory.FILTERING, "Entry points applied", { entryPoints: options.entry });
|
|
572
|
-
}
|
|
573
|
-
return {
|
|
574
|
-
nodes: filteredNodes,
|
|
575
|
-
edges: filteredEdges,
|
|
576
|
-
circularDependencies: filteredCircularDeps
|
|
577
|
-
};
|
|
578
604
|
}
|
|
579
605
|
/**
|
|
580
606
|
* Traverses the graph from a starting node using DFS
|
|
@@ -650,6 +676,7 @@ var OutputHandler = class {
|
|
|
650
676
|
* Implements FR-03: Constructor token resolution
|
|
651
677
|
*/
|
|
652
678
|
const GLOBAL_WARNING_KEYS = /* @__PURE__ */ new Set();
|
|
679
|
+
const ANGULAR_CORE_MODULE = "@angular/core";
|
|
653
680
|
const formatTsDiagnostics = (diagnostics) => diagnostics.map((diagnostic) => {
|
|
654
681
|
const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
|
655
682
|
if (diagnostic.file && diagnostic.start !== void 0) {
|
|
@@ -664,6 +691,9 @@ var AngularParser = class {
|
|
|
664
691
|
this._logger = _logger;
|
|
665
692
|
this._typeResolutionCache = /* @__PURE__ */ new Map();
|
|
666
693
|
this._circularTypeRefs = /* @__PURE__ */ new Set();
|
|
694
|
+
this._angularCoreImportCache = /* @__PURE__ */ new Map();
|
|
695
|
+
this._angularInjectIdentifiersCache = /* @__PURE__ */ new Map();
|
|
696
|
+
this._angularCoreAliasMatchers = [];
|
|
667
697
|
this._processingStats = {
|
|
668
698
|
processedFileCount: 0,
|
|
669
699
|
skippedFileCount: 0
|
|
@@ -773,6 +803,9 @@ var AngularParser = class {
|
|
|
773
803
|
const message = formatTsDiagnostics(parsedConfig.errors);
|
|
774
804
|
throw ErrorHandler.createError(`TypeScript configuration error: ${message}`, "PROJECT_LOAD_FAILED", this._options.project, { diagnosticCount: parsedConfig.errors.length });
|
|
775
805
|
}
|
|
806
|
+
this.cacheAngularCoreAliases(configFile.config);
|
|
807
|
+
this._angularCoreImportCache.clear();
|
|
808
|
+
this._angularInjectIdentifiersCache.clear();
|
|
776
809
|
this._project = new ts_morph.Project({ tsConfigFilePath: tsConfigPath });
|
|
777
810
|
if (!this._project) throw ErrorHandler.createError("Failed to load TypeScript project", "PROJECT_LOAD_FAILED", this._options.project);
|
|
778
811
|
} catch (error) {
|
|
@@ -1061,15 +1094,79 @@ var AngularParser = class {
|
|
|
1061
1094
|
*/
|
|
1062
1095
|
resolveDecoratorAlias(sourceFile, decoratorName) {
|
|
1063
1096
|
const importDeclarations = sourceFile.getImportDeclarations();
|
|
1064
|
-
for (const importDecl of importDeclarations)
|
|
1097
|
+
for (const importDecl of importDeclarations) {
|
|
1098
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
1099
|
+
if (this.isAngularCoreModuleSpecifier(moduleSpecifier)) {
|
|
1100
|
+
const namedImports = importDecl.getNamedImports();
|
|
1101
|
+
for (const namedImport of namedImports) {
|
|
1102
|
+
const alias = namedImport.getAliasNode();
|
|
1103
|
+
if (alias && alias.getText() === decoratorName) return namedImport.getName();
|
|
1104
|
+
if (!alias && namedImport.getName() === decoratorName) return decoratorName;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
cacheAngularCoreAliases(tsConfig) {
|
|
1111
|
+
this._angularCoreAliasMatchers = [];
|
|
1112
|
+
if (!tsConfig || typeof tsConfig !== "object") return;
|
|
1113
|
+
const configRecord = tsConfig;
|
|
1114
|
+
const paths = (typeof configRecord.compilerOptions === "object" && configRecord.compilerOptions ? configRecord.compilerOptions : void 0)?.paths;
|
|
1115
|
+
if (!paths) return;
|
|
1116
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
1117
|
+
if (!Array.isArray(targets)) continue;
|
|
1118
|
+
if (!targets.some((target) => this.isAngularCorePathTarget(target))) continue;
|
|
1119
|
+
this._angularCoreAliasMatchers.push(this.buildAliasMatcher(alias));
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
buildAliasMatcher(alias) {
|
|
1123
|
+
const pattern = alias.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
|
|
1124
|
+
return /* @__PURE__ */ new RegExp(`^${pattern}$`);
|
|
1125
|
+
}
|
|
1126
|
+
isAngularCorePathTarget(target) {
|
|
1127
|
+
const normalized = target.replace(/\\/g, "/");
|
|
1128
|
+
return normalized.includes(`${ANGULAR_CORE_MODULE}/`) || normalized.includes(ANGULAR_CORE_MODULE);
|
|
1129
|
+
}
|
|
1130
|
+
isAngularCoreModuleSpecifier(moduleSpecifier) {
|
|
1131
|
+
if (moduleSpecifier === ANGULAR_CORE_MODULE || moduleSpecifier.startsWith(`${ANGULAR_CORE_MODULE}/`)) return true;
|
|
1132
|
+
return this._angularCoreAliasMatchers.some((matcher) => matcher.test(moduleSpecifier));
|
|
1133
|
+
}
|
|
1134
|
+
getAngularCoreImportMap(sourceFile) {
|
|
1135
|
+
const cacheKey = sourceFile.getFilePath();
|
|
1136
|
+
const cached = this._angularCoreImportCache.get(cacheKey);
|
|
1137
|
+
if (cached) return cached;
|
|
1138
|
+
const named = /* @__PURE__ */ new Set();
|
|
1139
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
1140
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
1141
|
+
for (const importDecl of importDeclarations) {
|
|
1142
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
1143
|
+
if (!this.isAngularCoreModuleSpecifier(moduleSpecifier)) continue;
|
|
1065
1144
|
const namedImports = importDecl.getNamedImports();
|
|
1066
1145
|
for (const namedImport of namedImports) {
|
|
1067
1146
|
const alias = namedImport.getAliasNode();
|
|
1068
|
-
|
|
1069
|
-
if (!alias && namedImport.getName() === decoratorName) return decoratorName;
|
|
1147
|
+
named.add(alias ? alias.getText() : namedImport.getName());
|
|
1070
1148
|
}
|
|
1149
|
+
const namespaceImport = importDecl.getNamespaceImport();
|
|
1150
|
+
if (namespaceImport) namespaces.add(namespaceImport.getText());
|
|
1071
1151
|
}
|
|
1072
|
-
|
|
1152
|
+
const map = {
|
|
1153
|
+
named,
|
|
1154
|
+
namespaces
|
|
1155
|
+
};
|
|
1156
|
+
this._angularCoreImportCache.set(cacheKey, map);
|
|
1157
|
+
return map;
|
|
1158
|
+
}
|
|
1159
|
+
resolveDependencyOrigin(token, sourceFile) {
|
|
1160
|
+
const importMap = this.getAngularCoreImportMap(sourceFile);
|
|
1161
|
+
const normalizedToken = this.normalizeTokenForOrigin(token);
|
|
1162
|
+
if (normalizedToken.includes(".")) {
|
|
1163
|
+
const namespace = normalizedToken.split(".")[0];
|
|
1164
|
+
if (importMap.namespaces.has(namespace)) return "angular-core";
|
|
1165
|
+
}
|
|
1166
|
+
if (importMap.named.has(normalizedToken)) return "angular-core";
|
|
1167
|
+
}
|
|
1168
|
+
normalizeTokenForOrigin(token) {
|
|
1169
|
+
return token.split("<")[0].replace(/\[\]$/, "");
|
|
1073
1170
|
}
|
|
1074
1171
|
/**
|
|
1075
1172
|
* Determine NodeKind from Angular decorator
|
|
@@ -1408,7 +1505,8 @@ var AngularParser = class {
|
|
|
1408
1505
|
*/
|
|
1409
1506
|
parseConstructorParameter(param) {
|
|
1410
1507
|
const parameterName = param.getName();
|
|
1411
|
-
const
|
|
1508
|
+
const sourceFile = param.getSourceFile();
|
|
1509
|
+
const filePath = sourceFile.getFilePath();
|
|
1412
1510
|
const lineNumber = param.getStartLineNumber();
|
|
1413
1511
|
const columnNumber = param.getStart() - param.getStartLinePos();
|
|
1414
1512
|
const startTime = performance.now();
|
|
@@ -1421,7 +1519,8 @@ var AngularParser = class {
|
|
|
1421
1519
|
if (token) return {
|
|
1422
1520
|
token,
|
|
1423
1521
|
flags,
|
|
1424
|
-
parameterName
|
|
1522
|
+
parameterName,
|
|
1523
|
+
origin: this.resolveDependencyOrigin(token, sourceFile)
|
|
1425
1524
|
};
|
|
1426
1525
|
}
|
|
1427
1526
|
const initializer = param.getInitializer();
|
|
@@ -1432,7 +1531,8 @@ var AngularParser = class {
|
|
|
1432
1531
|
return {
|
|
1433
1532
|
token: injectResult.token,
|
|
1434
1533
|
flags: finalFlags,
|
|
1435
|
-
parameterName
|
|
1534
|
+
parameterName,
|
|
1535
|
+
origin: this.resolveDependencyOrigin(injectResult.token, sourceFile)
|
|
1436
1536
|
};
|
|
1437
1537
|
}
|
|
1438
1538
|
}
|
|
@@ -1442,7 +1542,8 @@ var AngularParser = class {
|
|
|
1442
1542
|
if (token) return {
|
|
1443
1543
|
token,
|
|
1444
1544
|
flags,
|
|
1445
|
-
parameterName
|
|
1545
|
+
parameterName,
|
|
1546
|
+
origin: this.resolveDependencyOrigin(token, sourceFile)
|
|
1446
1547
|
};
|
|
1447
1548
|
}
|
|
1448
1549
|
const type = param.getType();
|
|
@@ -1469,7 +1570,8 @@ var AngularParser = class {
|
|
|
1469
1570
|
if (resolvedToken) return {
|
|
1470
1571
|
token: resolvedToken,
|
|
1471
1572
|
flags,
|
|
1472
|
-
parameterName
|
|
1573
|
+
parameterName,
|
|
1574
|
+
origin: this.resolveDependencyOrigin(resolvedToken, sourceFile)
|
|
1473
1575
|
};
|
|
1474
1576
|
return null;
|
|
1475
1577
|
} finally {
|
|
@@ -1578,8 +1680,8 @@ var AngularParser = class {
|
|
|
1578
1680
|
const callExpression = initializer;
|
|
1579
1681
|
const expression = callExpression.getExpression();
|
|
1580
1682
|
if (expression.getKind() !== ts_morph.SyntaxKind.Identifier) return null;
|
|
1581
|
-
|
|
1582
|
-
if (!this.
|
|
1683
|
+
const identifier = expression.getText();
|
|
1684
|
+
if (!this.getAngularInjectIdentifiers(property.getSourceFile()).has(identifier)) return null;
|
|
1583
1685
|
const args = callExpression.getArguments();
|
|
1584
1686
|
if (args.length === 0) return null;
|
|
1585
1687
|
const token = args[0].getText().replace(/['"]/g, "");
|
|
@@ -1590,7 +1692,8 @@ var AngularParser = class {
|
|
|
1590
1692
|
return {
|
|
1591
1693
|
token,
|
|
1592
1694
|
flags,
|
|
1593
|
-
parameterName: propertyName
|
|
1695
|
+
parameterName: propertyName,
|
|
1696
|
+
origin: this.resolveDependencyOrigin(token, property.getSourceFile())
|
|
1594
1697
|
};
|
|
1595
1698
|
} catch (error) {
|
|
1596
1699
|
if (this._options.verbose) {
|
|
@@ -1710,28 +1813,24 @@ var AngularParser = class {
|
|
|
1710
1813
|
}
|
|
1711
1814
|
return flags;
|
|
1712
1815
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
if (importName === "inject" || alias && alias.getText() === "inject") return true;
|
|
1728
|
-
}
|
|
1816
|
+
getAngularInjectIdentifiers(sourceFile) {
|
|
1817
|
+
const cacheKey = sourceFile.getFilePath();
|
|
1818
|
+
const cached = this._angularInjectIdentifiersCache.get(cacheKey);
|
|
1819
|
+
if (cached) return cached;
|
|
1820
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
1821
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
1822
|
+
for (const importDecl of importDeclarations) {
|
|
1823
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
1824
|
+
if (!this.isAngularCoreModuleSpecifier(moduleSpecifier)) continue;
|
|
1825
|
+
const namedImports = importDecl.getNamedImports();
|
|
1826
|
+
for (const namedImport of namedImports) {
|
|
1827
|
+
if (namedImport.getName() !== "inject") continue;
|
|
1828
|
+
const alias = namedImport.getAliasNode();
|
|
1829
|
+
identifiers.add(alias ? alias.getText() : "inject");
|
|
1729
1830
|
}
|
|
1730
|
-
return false;
|
|
1731
|
-
} catch (error) {
|
|
1732
|
-
if (this._options.verbose) this.warn(LogCategory.ERROR_RECOVERY, `Could not verify inject() import in ${sourceFile.getFilePath()}: ${error instanceof Error ? error.message : String(error)}`, { filePath: sourceFile.getFilePath() });
|
|
1733
|
-
return true;
|
|
1734
1831
|
}
|
|
1832
|
+
this._angularInjectIdentifiersCache.set(cacheKey, identifiers);
|
|
1833
|
+
return identifiers;
|
|
1735
1834
|
}
|
|
1736
1835
|
/**
|
|
1737
1836
|
* Analyze inject() function call expression to extract token and options
|
|
@@ -1750,9 +1849,9 @@ var AngularParser = class {
|
|
|
1750
1849
|
try {
|
|
1751
1850
|
const callIdentifier = callExpression.getExpression();
|
|
1752
1851
|
if (callIdentifier.getKind() !== ts_morph.SyntaxKind.Identifier) return null;
|
|
1753
|
-
|
|
1852
|
+
const functionName = callIdentifier.getText();
|
|
1754
1853
|
const sourceFile$1 = expression.getSourceFile();
|
|
1755
|
-
if (!this.
|
|
1854
|
+
if (!this.getAngularInjectIdentifiers(sourceFile$1).has(functionName)) return null;
|
|
1756
1855
|
const args = callExpression.getArguments();
|
|
1757
1856
|
if (args.length === 0) {
|
|
1758
1857
|
warnVerbose("inject() called without token parameter - skipping");
|
|
@@ -1937,7 +2036,11 @@ var JsonFormatter = class {
|
|
|
1937
2036
|
nodeCount: graph.nodes.length,
|
|
1938
2037
|
edgeCount: graph.edges.length
|
|
1939
2038
|
});
|
|
1940
|
-
const
|
|
2039
|
+
const sanitizedGraph = {
|
|
2040
|
+
...graph,
|
|
2041
|
+
nodes: graph.nodes.map(({ origin, ...node }) => node)
|
|
2042
|
+
};
|
|
2043
|
+
const result = JSON.stringify(sanitizedGraph, null, 2);
|
|
1941
2044
|
const elapsed = this._logger?.timeEnd("json-format") ?? 0;
|
|
1942
2045
|
this._logger?.info(LogCategory.PERFORMANCE, "JSON output complete", {
|
|
1943
2046
|
outputSize: result.length,
|
|
@@ -2279,7 +2382,7 @@ function enforceMinimumNodeVersion() {
|
|
|
2279
2382
|
enforceMinimumNodeVersion();
|
|
2280
2383
|
const program = new commander.Command();
|
|
2281
2384
|
program.name("ng-di-graph").description("Angular DI dependency graph CLI tool").version("0.1.0");
|
|
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);
|
|
2385
|
+
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("--include-angular-core", "include @angular/core nodes", false).option("--out <file>", "output file (stdout if omitted)").option("-v, --verbose", "show detailed parsing information", false);
|
|
2283
2386
|
program.action(async (filePaths = [], options) => {
|
|
2284
2387
|
try {
|
|
2285
2388
|
const mergedFiles = mergeFileTargets(filePaths, options.files);
|
|
@@ -2309,6 +2412,7 @@ program.action(async (filePaths = [], options) => {
|
|
|
2309
2412
|
entry: options.entry,
|
|
2310
2413
|
direction: options.direction,
|
|
2311
2414
|
includeDecorators: options.includeDecorators,
|
|
2415
|
+
includeAngularCore: options.includeAngularCore,
|
|
2312
2416
|
out: options.out,
|
|
2313
2417
|
verbose: options.verbose
|
|
2314
2418
|
};
|
|
@@ -2337,10 +2441,10 @@ program.action(async (filePaths = [], options) => {
|
|
|
2337
2441
|
console.log(`โ
Graph built: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
|
|
2338
2442
|
if (graph.circularDependencies.length > 0) console.log(`โ ๏ธ Detected ${graph.circularDependencies.length} circular dependencies`);
|
|
2339
2443
|
}
|
|
2340
|
-
if (cliOptions.entry && cliOptions.entry.length > 0) {
|
|
2341
|
-
if (cliOptions.verbose) console.log(`๐ Filtering graph by entry points: ${cliOptions.entry.join(", ")}`);
|
|
2444
|
+
if (cliOptions.entry && cliOptions.entry.length > 0 || !cliOptions.includeAngularCore) {
|
|
2445
|
+
if (cliOptions.verbose && cliOptions.entry && cliOptions.entry.length > 0) console.log(`๐ Filtering graph by entry points: ${cliOptions.entry.join(", ")}`);
|
|
2342
2446
|
graph = filterGraph(graph, cliOptions, logger);
|
|
2343
|
-
if (cliOptions.verbose) console.log(`โ
Filtered graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
|
|
2447
|
+
if (cliOptions.verbose && cliOptions.entry && cliOptions.entry.length > 0) console.log(`โ
Filtered graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
|
|
2344
2448
|
}
|
|
2345
2449
|
let formatter;
|
|
2346
2450
|
if (cliOptions.format === "mermaid") formatter = new MermaidFormatter(logger);
|