ng-di-graph 0.6.0 โ†’ 0.7.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
@@ -49,6 +49,7 @@ ng-di-graph --project ./tsconfig.json --format json
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
+ - ๐Ÿงน **Angular Core Filtering** - Hide `@angular/core` nodes by default (`--include-angular-core` to show)
52
53
 
53
54
  ## Common commands
54
55
  - Full project, text output (default):
@@ -79,6 +80,7 @@ Option | Default | Description
79
80
  `-e, --entry <symbol...>` | none | Start the graph from one or more symbols.
80
81
  `-d, --direction <dir>` | `downstream` | `downstream`, `upstream`, or `both` relative to entries.
81
82
  `--include-decorators` | `false` | Add `@Optional`, `@Self`, `@SkipSelf`, `@Host` flags to edges.
83
+ `--include-angular-core` | `false` | Include nodes that originate from `@angular/core` imports.
82
84
  `--out <file>` | stdout | Write output to a file.
83
85
  `-v, --verbose` | `false` | Show detailed parsing and resolution logs.
84
86
  `-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);
548
561
  }
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) => {
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 });
571
+ }
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,8 @@ 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._angularCoreAliasMatchers = [];
667
696
  this._processingStats = {
668
697
  processedFileCount: 0,
669
698
  skippedFileCount: 0
@@ -773,6 +802,8 @@ var AngularParser = class {
773
802
  const message = formatTsDiagnostics(parsedConfig.errors);
774
803
  throw ErrorHandler.createError(`TypeScript configuration error: ${message}`, "PROJECT_LOAD_FAILED", this._options.project, { diagnosticCount: parsedConfig.errors.length });
775
804
  }
805
+ this.cacheAngularCoreAliases(configFile.config);
806
+ this._angularCoreImportCache.clear();
776
807
  this._project = new ts_morph.Project({ tsConfigFilePath: tsConfigPath });
777
808
  if (!this._project) throw ErrorHandler.createError("Failed to load TypeScript project", "PROJECT_LOAD_FAILED", this._options.project);
778
809
  } catch (error) {
@@ -1061,15 +1092,79 @@ var AngularParser = class {
1061
1092
  */
1062
1093
  resolveDecoratorAlias(sourceFile, decoratorName) {
1063
1094
  const importDeclarations = sourceFile.getImportDeclarations();
1064
- for (const importDecl of importDeclarations) if (importDecl.getModuleSpecifierValue() === "@angular/core") {
1095
+ for (const importDecl of importDeclarations) {
1096
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
1097
+ if (this.isAngularCoreModuleSpecifier(moduleSpecifier)) {
1098
+ const namedImports = importDecl.getNamedImports();
1099
+ for (const namedImport of namedImports) {
1100
+ const alias = namedImport.getAliasNode();
1101
+ if (alias && alias.getText() === decoratorName) return namedImport.getName();
1102
+ if (!alias && namedImport.getName() === decoratorName) return decoratorName;
1103
+ }
1104
+ }
1105
+ }
1106
+ return null;
1107
+ }
1108
+ cacheAngularCoreAliases(tsConfig) {
1109
+ this._angularCoreAliasMatchers = [];
1110
+ if (!tsConfig || typeof tsConfig !== "object") return;
1111
+ const configRecord = tsConfig;
1112
+ const paths = (typeof configRecord.compilerOptions === "object" && configRecord.compilerOptions ? configRecord.compilerOptions : void 0)?.paths;
1113
+ if (!paths) return;
1114
+ for (const [alias, targets] of Object.entries(paths)) {
1115
+ if (!Array.isArray(targets)) continue;
1116
+ if (!targets.some((target) => this.isAngularCorePathTarget(target))) continue;
1117
+ this._angularCoreAliasMatchers.push(this.buildAliasMatcher(alias));
1118
+ }
1119
+ }
1120
+ buildAliasMatcher(alias) {
1121
+ const pattern = alias.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
1122
+ return /* @__PURE__ */ new RegExp(`^${pattern}$`);
1123
+ }
1124
+ isAngularCorePathTarget(target) {
1125
+ const normalized = target.replace(/\\/g, "/");
1126
+ return normalized.includes(`${ANGULAR_CORE_MODULE}/`) || normalized.includes(ANGULAR_CORE_MODULE);
1127
+ }
1128
+ isAngularCoreModuleSpecifier(moduleSpecifier) {
1129
+ if (moduleSpecifier === ANGULAR_CORE_MODULE || moduleSpecifier.startsWith(`${ANGULAR_CORE_MODULE}/`)) return true;
1130
+ return this._angularCoreAliasMatchers.some((matcher) => matcher.test(moduleSpecifier));
1131
+ }
1132
+ getAngularCoreImportMap(sourceFile) {
1133
+ const cacheKey = sourceFile.getFilePath();
1134
+ const cached = this._angularCoreImportCache.get(cacheKey);
1135
+ if (cached) return cached;
1136
+ const named = /* @__PURE__ */ new Set();
1137
+ const namespaces = /* @__PURE__ */ new Set();
1138
+ const importDeclarations = sourceFile.getImportDeclarations();
1139
+ for (const importDecl of importDeclarations) {
1140
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
1141
+ if (!this.isAngularCoreModuleSpecifier(moduleSpecifier)) continue;
1065
1142
  const namedImports = importDecl.getNamedImports();
1066
1143
  for (const namedImport of namedImports) {
1067
1144
  const alias = namedImport.getAliasNode();
1068
- if (alias && alias.getText() === decoratorName) return namedImport.getName();
1069
- if (!alias && namedImport.getName() === decoratorName) return decoratorName;
1145
+ named.add(alias ? alias.getText() : namedImport.getName());
1070
1146
  }
1147
+ const namespaceImport = importDecl.getNamespaceImport();
1148
+ if (namespaceImport) namespaces.add(namespaceImport.getText());
1071
1149
  }
1072
- return null;
1150
+ const map = {
1151
+ named,
1152
+ namespaces
1153
+ };
1154
+ this._angularCoreImportCache.set(cacheKey, map);
1155
+ return map;
1156
+ }
1157
+ resolveDependencyOrigin(token, sourceFile) {
1158
+ const importMap = this.getAngularCoreImportMap(sourceFile);
1159
+ const normalizedToken = this.normalizeTokenForOrigin(token);
1160
+ if (normalizedToken.includes(".")) {
1161
+ const namespace = normalizedToken.split(".")[0];
1162
+ if (importMap.namespaces.has(namespace)) return "angular-core";
1163
+ }
1164
+ if (importMap.named.has(normalizedToken)) return "angular-core";
1165
+ }
1166
+ normalizeTokenForOrigin(token) {
1167
+ return token.split("<")[0].replace(/\[\]$/, "");
1073
1168
  }
1074
1169
  /**
1075
1170
  * Determine NodeKind from Angular decorator
@@ -1408,7 +1503,8 @@ var AngularParser = class {
1408
1503
  */
1409
1504
  parseConstructorParameter(param) {
1410
1505
  const parameterName = param.getName();
1411
- const filePath = param.getSourceFile().getFilePath();
1506
+ const sourceFile = param.getSourceFile();
1507
+ const filePath = sourceFile.getFilePath();
1412
1508
  const lineNumber = param.getStartLineNumber();
1413
1509
  const columnNumber = param.getStart() - param.getStartLinePos();
1414
1510
  const startTime = performance.now();
@@ -1421,7 +1517,8 @@ var AngularParser = class {
1421
1517
  if (token) return {
1422
1518
  token,
1423
1519
  flags,
1424
- parameterName
1520
+ parameterName,
1521
+ origin: this.resolveDependencyOrigin(token, sourceFile)
1425
1522
  };
1426
1523
  }
1427
1524
  const initializer = param.getInitializer();
@@ -1432,7 +1529,8 @@ var AngularParser = class {
1432
1529
  return {
1433
1530
  token: injectResult.token,
1434
1531
  flags: finalFlags,
1435
- parameterName
1532
+ parameterName,
1533
+ origin: this.resolveDependencyOrigin(injectResult.token, sourceFile)
1436
1534
  };
1437
1535
  }
1438
1536
  }
@@ -1442,7 +1540,8 @@ var AngularParser = class {
1442
1540
  if (token) return {
1443
1541
  token,
1444
1542
  flags,
1445
- parameterName
1543
+ parameterName,
1544
+ origin: this.resolveDependencyOrigin(token, sourceFile)
1446
1545
  };
1447
1546
  }
1448
1547
  const type = param.getType();
@@ -1469,7 +1568,8 @@ var AngularParser = class {
1469
1568
  if (resolvedToken) return {
1470
1569
  token: resolvedToken,
1471
1570
  flags,
1472
- parameterName
1571
+ parameterName,
1572
+ origin: this.resolveDependencyOrigin(resolvedToken, sourceFile)
1473
1573
  };
1474
1574
  return null;
1475
1575
  } finally {
@@ -1590,7 +1690,8 @@ var AngularParser = class {
1590
1690
  return {
1591
1691
  token,
1592
1692
  flags,
1593
- parameterName: propertyName
1693
+ parameterName: propertyName,
1694
+ origin: this.resolveDependencyOrigin(token, property.getSourceFile())
1594
1695
  };
1595
1696
  } catch (error) {
1596
1697
  if (this._options.verbose) {
@@ -1719,12 +1820,15 @@ var AngularParser = class {
1719
1820
  isAngularInjectImported(sourceFile) {
1720
1821
  try {
1721
1822
  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;
1823
+ for (const importDecl of importDeclarations) {
1824
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
1825
+ if (this.isAngularCoreModuleSpecifier(moduleSpecifier)) {
1826
+ const namedImports = importDecl.getNamedImports();
1827
+ for (const namedImport of namedImports) {
1828
+ const importName = namedImport.getName();
1829
+ const alias = namedImport.getAliasNode();
1830
+ if (importName === "inject" || alias && alias.getText() === "inject") return true;
1831
+ }
1728
1832
  }
1729
1833
  }
1730
1834
  return false;
@@ -1937,7 +2041,11 @@ var JsonFormatter = class {
1937
2041
  nodeCount: graph.nodes.length,
1938
2042
  edgeCount: graph.edges.length
1939
2043
  });
1940
- const result = JSON.stringify(graph, null, 2);
2044
+ const sanitizedGraph = {
2045
+ ...graph,
2046
+ nodes: graph.nodes.map(({ origin, ...node }) => node)
2047
+ };
2048
+ const result = JSON.stringify(sanitizedGraph, null, 2);
1941
2049
  const elapsed = this._logger?.timeEnd("json-format") ?? 0;
1942
2050
  this._logger?.info(LogCategory.PERFORMANCE, "JSON output complete", {
1943
2051
  outputSize: result.length,
@@ -2279,7 +2387,7 @@ function enforceMinimumNodeVersion() {
2279
2387
  enforceMinimumNodeVersion();
2280
2388
  const program = new commander.Command();
2281
2389
  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);
2390
+ 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
2391
  program.action(async (filePaths = [], options) => {
2284
2392
  try {
2285
2393
  const mergedFiles = mergeFileTargets(filePaths, options.files);
@@ -2309,6 +2417,7 @@ program.action(async (filePaths = [], options) => {
2309
2417
  entry: options.entry,
2310
2418
  direction: options.direction,
2311
2419
  includeDecorators: options.includeDecorators,
2420
+ includeAngularCore: options.includeAngularCore,
2312
2421
  out: options.out,
2313
2422
  verbose: options.verbose
2314
2423
  };
@@ -2337,10 +2446,10 @@ program.action(async (filePaths = [], options) => {
2337
2446
  console.log(`โœ… Graph built: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
2338
2447
  if (graph.circularDependencies.length > 0) console.log(`โš ๏ธ Detected ${graph.circularDependencies.length} circular dependencies`);
2339
2448
  }
2340
- if (cliOptions.entry && cliOptions.entry.length > 0) {
2341
- if (cliOptions.verbose) console.log(`๐Ÿ” Filtering graph by entry points: ${cliOptions.entry.join(", ")}`);
2449
+ if (cliOptions.entry && cliOptions.entry.length > 0 || !cliOptions.includeAngularCore) {
2450
+ if (cliOptions.verbose && cliOptions.entry && cliOptions.entry.length > 0) console.log(`๐Ÿ” Filtering graph by entry points: ${cliOptions.entry.join(", ")}`);
2342
2451
  graph = filterGraph(graph, cliOptions, logger);
2343
- if (cliOptions.verbose) console.log(`โœ… Filtered graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
2452
+ if (cliOptions.verbose && cliOptions.entry && cliOptions.entry.length > 0) console.log(`โœ… Filtered graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
2344
2453
  }
2345
2454
  let formatter;
2346
2455
  if (cliOptions.format === "mermaid") formatter = new MermaidFormatter(logger);