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 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-20
9
+ **Target Angular Versions:** 17-21
10
10
 
11
11
  ## Prerequisites
12
12
  - Node.js 20.x (npm 10+)
13
- - Angular project targeting v17-20 with a `tsconfig.json` to discover or reference
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.
@@ -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
- nodeMap.set(dependency.token, {
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
- if (!options.entry || options.entry.length === 0) return graph;
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(graph, "upstream");
550
+ const upstreamAdjacencyList = buildAdjacencyList(baseGraph, "upstream");
538
551
  const upstreamNodes = /* @__PURE__ */ new Set();
539
- validateAndTraverseEntryPoints(options.entry, graph, upstreamAdjacencyList, upstreamNodes, options, logger);
540
- const downstreamAdjacencyList = buildAdjacencyList(graph, "downstream");
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, graph, downstreamAdjacencyList, downstreamNodes, options, logger);
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(graph, options.direction);
547
- validateAndTraverseEntryPoints(options.entry, graph, adjacencyList, includedNodeIds, options, logger);
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
- const filteredNodes = graph.nodes.filter((node) => includedNodeIds.has(node.id));
550
- const filteredEdges = graph.edges.filter((edge) => includedNodeIds.has(edge.from) && includedNodeIds.has(edge.to));
551
- const filteredCircularDeps = graph.circularDependencies.filter((cycle) => {
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) if (importDecl.getModuleSpecifierValue() === "@angular/core") {
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
- if (alias && alias.getText() === decoratorName) return namedImport.getName();
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
- return null;
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 filePath = param.getSourceFile().getFilePath();
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
- if (expression.getText() !== "inject") return null;
1582
- if (!this.isAngularInjectImported(property.getSourceFile())) return null;
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
- * Check if inject() function is imported from @angular/core
1715
- * Prevents false positives from custom inject() functions
1716
- * @param sourceFile Source file to check imports
1717
- * @returns True if Angular inject is imported
1718
- */
1719
- isAngularInjectImported(sourceFile) {
1720
- try {
1721
- const importDeclarations = sourceFile.getImportDeclarations();
1722
- for (const importDecl of importDeclarations) if (importDecl.getModuleSpecifierValue() === "@angular/core") {
1723
- const namedImports = importDecl.getNamedImports();
1724
- for (const namedImport of namedImports) {
1725
- const importName = namedImport.getName();
1726
- const alias = namedImport.getAliasNode();
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
- if (callIdentifier.getText() !== "inject") return null;
1852
+ const functionName = callIdentifier.getText();
1754
1853
  const sourceFile$1 = expression.getSourceFile();
1755
- if (!this.isAngularInjectImported(sourceFile$1)) return null;
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 result = JSON.stringify(graph, null, 2);
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);