ng-di-graph 0.3.0 → 0.4.1
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 +15 -7
- package/dist/cli/index.cjs +285 -138
- package/dist/cli/index.cjs.map +1 -1
- package/dist/core/graph-filter.d.ts +3 -1
- package/dist/core/parser.d.ts +7 -0
- package/dist/types/index.d.ts +5 -0
- package/package.json +1 -1
package/dist/cli/index.cjs
CHANGED
|
@@ -510,32 +510,37 @@ function buildGraph(parsedClasses, logger) {
|
|
|
510
510
|
* @param adjacencyList The adjacency list for traversal
|
|
511
511
|
* @param resultSet The set to collect traversal results
|
|
512
512
|
* @param options CLI options for verbose output
|
|
513
|
+
* @param logger Optional logger for structured output
|
|
513
514
|
*/
|
|
514
|
-
function validateAndTraverseEntryPoints(entryPoints, graph, adjacencyList, resultSet, options) {
|
|
515
|
+
function validateAndTraverseEntryPoints(entryPoints, graph, adjacencyList, resultSet, options, logger) {
|
|
515
516
|
for (const entryPoint of entryPoints) if (graph.nodes.some((n) => n.id === entryPoint)) traverseFromEntry(entryPoint, adjacencyList, resultSet);
|
|
516
|
-
else if (options.verbose
|
|
517
|
+
else if (options.verbose && logger) {
|
|
518
|
+
const message = `Entry point '${entryPoint}' not found in graph`;
|
|
519
|
+
logger.warn(LogCategory.FILTERING, message, { entryPoint });
|
|
520
|
+
}
|
|
517
521
|
}
|
|
518
522
|
/**
|
|
519
523
|
* Filters a graph based on entry points and traversal direction
|
|
520
524
|
* @param graph The graph to filter
|
|
521
525
|
* @param options CLI options containing entry points and direction
|
|
526
|
+
* @param logger Optional logger for structured output
|
|
522
527
|
* @returns Filtered graph containing only nodes reachable from entry points
|
|
523
528
|
*/
|
|
524
|
-
function filterGraph(graph, options) {
|
|
529
|
+
function filterGraph(graph, options, logger) {
|
|
525
530
|
if (!options.entry || options.entry.length === 0) return graph;
|
|
526
531
|
const includedNodeIds = /* @__PURE__ */ new Set();
|
|
527
532
|
if (options.direction === "both") {
|
|
528
533
|
const upstreamAdjacencyList = buildAdjacencyList(graph, "upstream");
|
|
529
534
|
const upstreamNodes = /* @__PURE__ */ new Set();
|
|
530
|
-
validateAndTraverseEntryPoints(options.entry, graph, upstreamAdjacencyList, upstreamNodes, options);
|
|
535
|
+
validateAndTraverseEntryPoints(options.entry, graph, upstreamAdjacencyList, upstreamNodes, options, logger);
|
|
531
536
|
const downstreamAdjacencyList = buildAdjacencyList(graph, "downstream");
|
|
532
537
|
const downstreamNodes = /* @__PURE__ */ new Set();
|
|
533
|
-
validateAndTraverseEntryPoints(options.entry, graph, downstreamAdjacencyList, downstreamNodes, options);
|
|
538
|
+
validateAndTraverseEntryPoints(options.entry, graph, downstreamAdjacencyList, downstreamNodes, options, logger);
|
|
534
539
|
const combinedNodes = new Set([...upstreamNodes, ...downstreamNodes]);
|
|
535
540
|
for (const nodeId of combinedNodes) includedNodeIds.add(nodeId);
|
|
536
541
|
} else {
|
|
537
542
|
const adjacencyList = buildAdjacencyList(graph, options.direction);
|
|
538
|
-
validateAndTraverseEntryPoints(options.entry, graph, adjacencyList, includedNodeIds, options);
|
|
543
|
+
validateAndTraverseEntryPoints(options.entry, graph, adjacencyList, includedNodeIds, options, logger);
|
|
539
544
|
}
|
|
540
545
|
const filteredNodes = graph.nodes.filter((node) => includedNodeIds.has(node.id));
|
|
541
546
|
const filteredEdges = graph.edges.filter((edge) => includedNodeIds.has(edge.from) && includedNodeIds.has(edge.to));
|
|
@@ -554,9 +559,12 @@ function filterGraph(graph, options) {
|
|
|
554
559
|
}
|
|
555
560
|
return true;
|
|
556
561
|
});
|
|
557
|
-
if (options.verbose) {
|
|
558
|
-
|
|
559
|
-
|
|
562
|
+
if (options.verbose && logger) {
|
|
563
|
+
logger.info(LogCategory.FILTERING, "Filtered graph summary", {
|
|
564
|
+
nodeCount: filteredNodes.length,
|
|
565
|
+
edgeCount: filteredEdges.length
|
|
566
|
+
});
|
|
567
|
+
logger.debug(LogCategory.FILTERING, "Entry points applied", { entryPoints: options.entry });
|
|
560
568
|
}
|
|
561
569
|
return {
|
|
562
570
|
nodes: filteredNodes,
|
|
@@ -663,6 +671,21 @@ var AngularParser = class {
|
|
|
663
671
|
totalCount: 0
|
|
664
672
|
};
|
|
665
673
|
}
|
|
674
|
+
verboseInfo(category, message, context) {
|
|
675
|
+
if (!this._options.verbose || !this._logger) return;
|
|
676
|
+
this._logger.info(category, message, context);
|
|
677
|
+
}
|
|
678
|
+
verboseDebug(category, message, context) {
|
|
679
|
+
if (!this._options.verbose || !this._logger) return;
|
|
680
|
+
this._logger.debug(category, message, context);
|
|
681
|
+
}
|
|
682
|
+
warn(category, message, context) {
|
|
683
|
+
if (this._logger) {
|
|
684
|
+
this._logger.warn(category, message, context);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
ErrorHandler.warn(message, context?.filePath);
|
|
688
|
+
}
|
|
666
689
|
/**
|
|
667
690
|
* Reset global warning deduplication state (useful for testing)
|
|
668
691
|
*/
|
|
@@ -698,8 +721,23 @@ var AngularParser = class {
|
|
|
698
721
|
this._structuredWarnings.categories[category].push(warning);
|
|
699
722
|
this._structuredWarnings.totalCount++;
|
|
700
723
|
const location = warning.line ? `${warning.file}:${warning.line}:${warning.column}` : warning.file;
|
|
701
|
-
|
|
702
|
-
if (
|
|
724
|
+
const formattedWarning = `[${warning.severity.toUpperCase()}] ${warning.message} (${location})`;
|
|
725
|
+
if (this._logger) {
|
|
726
|
+
this._logger.warn(LogCategory.ERROR_RECOVERY, formattedWarning, {
|
|
727
|
+
category,
|
|
728
|
+
filePath: warning.file,
|
|
729
|
+
lineNumber: warning.line,
|
|
730
|
+
suggestion: warning.suggestion
|
|
731
|
+
});
|
|
732
|
+
if (warning.suggestion && this._options.verbose) this._logger.info(LogCategory.ERROR_RECOVERY, warning.suggestion, {
|
|
733
|
+
category,
|
|
734
|
+
filePath: warning.file,
|
|
735
|
+
lineNumber: warning.line
|
|
736
|
+
});
|
|
737
|
+
} else {
|
|
738
|
+
ErrorHandler.warn(formattedWarning, warning.file);
|
|
739
|
+
if (warning.suggestion && this._options.verbose) ErrorHandler.warn(warning.suggestion, warning.file);
|
|
740
|
+
}
|
|
703
741
|
GLOBAL_WARNING_KEYS.add(warnKey);
|
|
704
742
|
}
|
|
705
743
|
}
|
|
@@ -723,12 +761,6 @@ var AngularParser = class {
|
|
|
723
761
|
}
|
|
724
762
|
this._project = new ts_morph.Project({ tsConfigFilePath: tsConfigPath });
|
|
725
763
|
if (!this._project) throw ErrorHandler.createError("Failed to load TypeScript project", "PROJECT_LOAD_FAILED", this._options.project);
|
|
726
|
-
this._project.getSourceFiles();
|
|
727
|
-
const diagnostics = this._project.getProgram().getConfigFileParsingDiagnostics();
|
|
728
|
-
if (diagnostics.length > 0) {
|
|
729
|
-
const message = diagnostics[0].getMessageText();
|
|
730
|
-
throw ErrorHandler.createError(`TypeScript configuration error: ${message}`, "PROJECT_LOAD_FAILED", this._options.project, { diagnosticCount: diagnostics.length });
|
|
731
|
-
}
|
|
732
764
|
} catch (error) {
|
|
733
765
|
if (error instanceof CliError) throw error;
|
|
734
766
|
if (error instanceof Error) {
|
|
@@ -758,6 +790,103 @@ var AngularParser = class {
|
|
|
758
790
|
return this.findDecoratedClasses();
|
|
759
791
|
}
|
|
760
792
|
/**
|
|
793
|
+
* Retrieve source files, optionally filtering by user-provided file paths
|
|
794
|
+
*/
|
|
795
|
+
getTargetSourceFiles() {
|
|
796
|
+
if (!this._project) throw ErrorHandler.createError("Failed to load TypeScript project", "PROJECT_LOAD_FAILED", this._options.project);
|
|
797
|
+
if (!this._options.files || this._options.files.length === 0) return this._project.getSourceFiles();
|
|
798
|
+
const projectDir = node_path.default.dirname(node_path.default.resolve(this._options.project));
|
|
799
|
+
const targetPaths = this._options.files.map((filePath) => {
|
|
800
|
+
if (node_path.default.isAbsolute(filePath)) {
|
|
801
|
+
const normalized = node_path.default.normalize(filePath);
|
|
802
|
+
return {
|
|
803
|
+
raw: filePath,
|
|
804
|
+
normalized,
|
|
805
|
+
isDirectory: (0, node_fs.existsSync)(normalized) && (0, node_fs.statSync)(normalized).isDirectory()
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
const projectResolved = node_path.default.normalize(node_path.default.resolve(projectDir, filePath));
|
|
809
|
+
const cwdResolved = node_path.default.normalize(node_path.default.resolve(filePath));
|
|
810
|
+
const normalizedPath = (0, node_fs.existsSync)(projectResolved) ? projectResolved : cwdResolved;
|
|
811
|
+
return {
|
|
812
|
+
raw: filePath,
|
|
813
|
+
normalized: normalizedPath,
|
|
814
|
+
isDirectory: (0, node_fs.existsSync)(normalizedPath) && (0, node_fs.statSync)(normalizedPath).isDirectory()
|
|
815
|
+
};
|
|
816
|
+
});
|
|
817
|
+
const isFileMatch = (filePath, target) => {
|
|
818
|
+
if (target.isDirectory) {
|
|
819
|
+
const relative = node_path.default.relative(target.normalized, filePath);
|
|
820
|
+
return relative !== "" && !relative.startsWith("..") && !node_path.default.isAbsolute(relative);
|
|
821
|
+
}
|
|
822
|
+
return filePath === target.normalized;
|
|
823
|
+
};
|
|
824
|
+
const directoryTargets = targetPaths.filter((target) => target.isDirectory);
|
|
825
|
+
const fileTargets = targetPaths.filter((target) => !target.isDirectory);
|
|
826
|
+
const directoryAdds = [];
|
|
827
|
+
for (const target of directoryTargets) {
|
|
828
|
+
if (!(0, node_fs.existsSync)(target.normalized) || !(0, node_fs.statSync)(target.normalized).isDirectory()) continue;
|
|
829
|
+
const directoryGlob = `${target.normalized.replace(/\\/g, "/")}/**/*.{ts,tsx}`;
|
|
830
|
+
const added = this._project.addSourceFilesAtPaths(directoryGlob);
|
|
831
|
+
if (added.length > 0) {
|
|
832
|
+
directoryAdds.push({
|
|
833
|
+
target,
|
|
834
|
+
addedCount: added.length
|
|
835
|
+
});
|
|
836
|
+
this._logger?.warn(LogCategory.FILE_PROCESSING, "Added directory outside tsconfig scope", {
|
|
837
|
+
directory: target.raw,
|
|
838
|
+
addedCount: added.length
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
let sourceFiles = this._project.getSourceFiles();
|
|
843
|
+
let matchedFiles = sourceFiles.filter((sourceFile) => {
|
|
844
|
+
const filePath = node_path.default.normalize(sourceFile.getFilePath());
|
|
845
|
+
return targetPaths.some((target) => isFileMatch(filePath, target));
|
|
846
|
+
});
|
|
847
|
+
const missingFileTargets = fileTargets.filter((target) => matchedFiles.every((file) => !isFileMatch(node_path.default.normalize(file.getFilePath()), target)));
|
|
848
|
+
if (missingFileTargets.length > 0) {
|
|
849
|
+
const addedFiles = [];
|
|
850
|
+
for (const target of missingFileTargets) {
|
|
851
|
+
if (!(0, node_fs.existsSync)(target.normalized)) continue;
|
|
852
|
+
const added = this._project.addSourceFileAtPathIfExists(target.normalized);
|
|
853
|
+
if (added) {
|
|
854
|
+
addedFiles.push(added);
|
|
855
|
+
this._logger?.warn(LogCategory.FILE_PROCESSING, "Added file outside tsconfig scope", { filePath: target.raw });
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (addedFiles.length > 0 || directoryAdds.length > 0) {
|
|
859
|
+
sourceFiles = this._project.getSourceFiles();
|
|
860
|
+
matchedFiles = sourceFiles.filter((sourceFile) => {
|
|
861
|
+
const filePath = node_path.default.normalize(sourceFile.getFilePath());
|
|
862
|
+
return targetPaths.some((target) => isFileMatch(filePath, target));
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
const missingTargets = targetPaths.filter((target) => matchedFiles.every((file) => !isFileMatch(node_path.default.normalize(file.getFilePath()), target)));
|
|
867
|
+
if (missingTargets.length > 0) {
|
|
868
|
+
const emptyDirectories = missingTargets.filter((target) => target.isDirectory && (0, node_fs.existsSync)(target.normalized) && (0, node_fs.statSync)(target.normalized).isDirectory());
|
|
869
|
+
if (emptyDirectories.length > 0) {
|
|
870
|
+
const firstEmpty = emptyDirectories[0];
|
|
871
|
+
throw ErrorHandler.createError(`Directory "${firstEmpty.raw}" contained no TypeScript files`, "FILE_NOT_FOUND", firstEmpty.raw);
|
|
872
|
+
}
|
|
873
|
+
const missingList = missingTargets.map((target) => target.raw).join(", ");
|
|
874
|
+
throw ErrorHandler.createError(`Target file(s) not found in project: ${missingList}`, "FILE_NOT_FOUND", missingTargets[0]?.raw);
|
|
875
|
+
}
|
|
876
|
+
this._logger?.info(LogCategory.FILE_PROCESSING, "Applied file filter", {
|
|
877
|
+
targetCount: targetPaths.length,
|
|
878
|
+
matchedCount: matchedFiles.length
|
|
879
|
+
});
|
|
880
|
+
if (this._options.verbose) {
|
|
881
|
+
this._logger?.info(LogCategory.FILE_PROCESSING, `Filtering to specific file(s): ${matchedFiles.length}`, {
|
|
882
|
+
matchedCount: matchedFiles.length,
|
|
883
|
+
targetCount: targetPaths.length
|
|
884
|
+
});
|
|
885
|
+
for (const file of matchedFiles) this._logger?.debug(LogCategory.FILE_PROCESSING, "Including file", { filePath: file.getFilePath() });
|
|
886
|
+
}
|
|
887
|
+
return matchedFiles;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
761
890
|
* Find all classes decorated with @Injectable, @Component, or @Directive
|
|
762
891
|
* Implements FR-02: Decorated Class Collection
|
|
763
892
|
* @returns Promise<ParsedClass[]> List of decorated classes
|
|
@@ -766,20 +895,18 @@ var AngularParser = class {
|
|
|
766
895
|
if (!this._project) this.loadProject();
|
|
767
896
|
if (!this._project) throw ErrorHandler.createError("Failed to load TypeScript project", "PROJECT_LOAD_FAILED", this._options.project);
|
|
768
897
|
const decoratedClasses = [];
|
|
769
|
-
const sourceFiles = this.
|
|
898
|
+
const sourceFiles = this.getTargetSourceFiles();
|
|
770
899
|
let processedFiles = 0;
|
|
771
900
|
let skippedFiles = 0;
|
|
772
901
|
this._circularTypeRefs.clear();
|
|
773
902
|
this._logger?.time("findDecoratedClasses");
|
|
774
903
|
this._logger?.info(LogCategory.FILE_PROCESSING, "Starting file processing", { fileCount: sourceFiles.length });
|
|
775
|
-
|
|
904
|
+
this._logger?.info(LogCategory.FILE_PROCESSING, "Processing source files", { fileCount: sourceFiles.length });
|
|
776
905
|
for (const sourceFile of sourceFiles) {
|
|
777
906
|
const filePath = sourceFile.getFilePath();
|
|
778
907
|
try {
|
|
779
|
-
|
|
780
|
-
this._logger?.debug(LogCategory.FILE_PROCESSING, "Processing file", { filePath });
|
|
908
|
+
this._logger?.debug(LogCategory.FILE_PROCESSING, "Parsing file", { filePath });
|
|
781
909
|
const classes = sourceFile.getClasses();
|
|
782
|
-
if (this._options.verbose) console.log(`File: ${filePath}, Classes: ${classes.length}`);
|
|
783
910
|
this._logger?.debug(LogCategory.AST_ANALYSIS, "Analyzing classes in file", {
|
|
784
911
|
filePath,
|
|
785
912
|
classCount: classes.length
|
|
@@ -788,7 +915,6 @@ var AngularParser = class {
|
|
|
788
915
|
const parsedClass = this.parseClassDeclaration(classDeclaration);
|
|
789
916
|
if (parsedClass) {
|
|
790
917
|
decoratedClasses.push(parsedClass);
|
|
791
|
-
if (this._options.verbose) console.log(`Found decorated class: ${parsedClass.name} (${parsedClass.kind})`);
|
|
792
918
|
this._logger?.info(LogCategory.AST_ANALYSIS, "Found decorated class", {
|
|
793
919
|
className: parsedClass.name,
|
|
794
920
|
kind: parsedClass.kind,
|
|
@@ -810,7 +936,10 @@ var AngularParser = class {
|
|
|
810
936
|
ErrorHandler.warn(`Failed to parse file (skipping): ${error instanceof Error ? error.message : "Unknown error"}`, filePath);
|
|
811
937
|
}
|
|
812
938
|
}
|
|
813
|
-
|
|
939
|
+
this._logger?.info(LogCategory.FILE_PROCESSING, "File processing summary", {
|
|
940
|
+
processedFiles,
|
|
941
|
+
skippedFiles
|
|
942
|
+
});
|
|
814
943
|
const elapsed = this._logger?.timeEnd("findDecoratedClasses") || 0;
|
|
815
944
|
this._logger?.info(LogCategory.PERFORMANCE, "File processing complete", {
|
|
816
945
|
totalClasses: decoratedClasses.length,
|
|
@@ -829,17 +958,26 @@ var AngularParser = class {
|
|
|
829
958
|
parseClassDeclaration(classDeclaration) {
|
|
830
959
|
const className = classDeclaration.getName();
|
|
831
960
|
if (!className) {
|
|
832
|
-
|
|
961
|
+
const message = "Skipping anonymous class - classes must be named for dependency injection analysis";
|
|
962
|
+
if (this._logger) this._logger.warn(LogCategory.AST_ANALYSIS, message);
|
|
963
|
+
else ErrorHandler.warn(message);
|
|
833
964
|
return null;
|
|
834
965
|
}
|
|
835
966
|
const decorators = classDeclaration.getDecorators();
|
|
836
967
|
if (this._options.verbose) {
|
|
837
968
|
const decoratorNames = decorators.map((d) => this.getDecoratorName(d)).join(", ");
|
|
838
|
-
|
|
969
|
+
this._logger?.debug(LogCategory.AST_ANALYSIS, "Decorator metadata", {
|
|
970
|
+
className,
|
|
971
|
+
decoratorCount: decorators.length,
|
|
972
|
+
decoratorNames
|
|
973
|
+
});
|
|
839
974
|
}
|
|
840
975
|
const angularDecorator = this.findAngularDecorator(decorators);
|
|
841
976
|
if (!angularDecorator) {
|
|
842
|
-
if (this._options.verbose && decorators.length > 0)
|
|
977
|
+
if (this._options.verbose && decorators.length > 0) this._logger?.debug(LogCategory.AST_ANALYSIS, "No Angular decorator found", {
|
|
978
|
+
className,
|
|
979
|
+
decoratorCount: decorators.length
|
|
980
|
+
});
|
|
843
981
|
return null;
|
|
844
982
|
}
|
|
845
983
|
return {
|
|
@@ -921,14 +1059,14 @@ var AngularParser = class {
|
|
|
921
1059
|
if (parent && parent.getKind() === ts_morph.SyntaxKind.CallExpression) {
|
|
922
1060
|
const grandParent = parent.getParent();
|
|
923
1061
|
if (grandParent && grandParent.getKind() === ts_morph.SyntaxKind.CallExpression) {
|
|
924
|
-
|
|
925
|
-
|
|
1062
|
+
this.warn(LogCategory.AST_ANALYSIS, "Skipping anonymous class - classes must be named for dependency injection analysis", { filePath: sourceFile.getFilePath() });
|
|
1063
|
+
this.verboseDebug(LogCategory.AST_ANALYSIS, `Anonymous class found in ${sourceFile.getFilePath()}`);
|
|
926
1064
|
}
|
|
927
1065
|
}
|
|
928
1066
|
}
|
|
929
1067
|
});
|
|
930
1068
|
} catch (error) {
|
|
931
|
-
|
|
1069
|
+
this.verboseDebug(LogCategory.AST_ANALYSIS, `Could not detect anonymous classes in ${sourceFile.getFilePath()}: ${error}`);
|
|
932
1070
|
}
|
|
933
1071
|
}
|
|
934
1072
|
/**
|
|
@@ -957,11 +1095,11 @@ var AngularParser = class {
|
|
|
957
1095
|
};
|
|
958
1096
|
const startTime = performance.now();
|
|
959
1097
|
if (this._options.verbose && this._options.includeDecorators) {
|
|
960
|
-
|
|
1098
|
+
this.verboseInfo(LogCategory.AST_ANALYSIS, "=== Decorator Analysis ===");
|
|
961
1099
|
const className = classDeclaration.getName() || "unknown";
|
|
962
|
-
|
|
1100
|
+
this.verboseInfo(LogCategory.AST_ANALYSIS, `Analyzing decorators for class: ${className}`, { className });
|
|
963
1101
|
}
|
|
964
|
-
if (this._options.verbose && !this._options.includeDecorators)
|
|
1102
|
+
if (this._options.verbose && !this._options.includeDecorators) this.verboseInfo(LogCategory.AST_ANALYSIS, "Decorator analysis disabled - --include-decorators flag not set");
|
|
965
1103
|
const constructors = classDeclaration.getConstructors();
|
|
966
1104
|
if (constructors.length > 0) {
|
|
967
1105
|
const parameters = constructors[0].getParameters();
|
|
@@ -1015,7 +1153,7 @@ var AngularParser = class {
|
|
|
1015
1153
|
* @returns Token string or null
|
|
1016
1154
|
*/
|
|
1017
1155
|
handleGenericType(typeText, _filePath, _lineNumber, _columnNumber) {
|
|
1018
|
-
|
|
1156
|
+
this.verboseDebug(LogCategory.TYPE_RESOLUTION, `Processing generic type: ${typeText}`);
|
|
1019
1157
|
return typeText;
|
|
1020
1158
|
}
|
|
1021
1159
|
/**
|
|
@@ -1102,7 +1240,10 @@ var AngularParser = class {
|
|
|
1102
1240
|
*/
|
|
1103
1241
|
extractTypeTokenEnhanced(typeNode, filePath, lineNumber, columnNumber) {
|
|
1104
1242
|
const typeText = typeNode.getText();
|
|
1105
|
-
|
|
1243
|
+
this.verboseInfo(LogCategory.TYPE_RESOLUTION, `Type resolution steps: Processing '${typeText}' at ${filePath}:${lineNumber}:${columnNumber}`, {
|
|
1244
|
+
filePath,
|
|
1245
|
+
lineNumber
|
|
1246
|
+
});
|
|
1106
1247
|
if (this.isCircularTypeReference(typeText, typeNode)) {
|
|
1107
1248
|
this.addStructuredWarning("circularReferences", {
|
|
1108
1249
|
type: "circular_type_reference",
|
|
@@ -1166,7 +1307,10 @@ var AngularParser = class {
|
|
|
1166
1307
|
* @returns Resolved token or null
|
|
1167
1308
|
*/
|
|
1168
1309
|
resolveInferredTypeEnhanced(type, typeText, param, filePath, lineNumber, columnNumber) {
|
|
1169
|
-
|
|
1310
|
+
this.verboseInfo(LogCategory.TYPE_RESOLUTION, `Attempting to resolve inferred type: ${typeText}`, {
|
|
1311
|
+
filePath,
|
|
1312
|
+
lineNumber
|
|
1313
|
+
});
|
|
1170
1314
|
const symbol = type.getSymbol?.();
|
|
1171
1315
|
if (symbol) {
|
|
1172
1316
|
const symbolName = symbol.getName();
|
|
@@ -1268,14 +1412,20 @@ var AngularParser = class {
|
|
|
1268
1412
|
cacheKey = `${filePath}:${parameterName}:${typeText}`;
|
|
1269
1413
|
if (this._typeResolutionCache.has(cacheKey)) {
|
|
1270
1414
|
const cachedResult = this._typeResolutionCache.get(cacheKey);
|
|
1271
|
-
|
|
1415
|
+
this.verboseDebug(LogCategory.TYPE_RESOLUTION, `Cache hit for parameter '${parameterName}': ${typeText}`, {
|
|
1416
|
+
parameterName,
|
|
1417
|
+
cacheKey
|
|
1418
|
+
});
|
|
1272
1419
|
return cachedResult ? {
|
|
1273
1420
|
token: cachedResult,
|
|
1274
1421
|
flags,
|
|
1275
1422
|
parameterName
|
|
1276
1423
|
} : null;
|
|
1277
1424
|
}
|
|
1278
|
-
|
|
1425
|
+
this.verboseDebug(LogCategory.TYPE_RESOLUTION, `Cache miss for parameter '${parameterName}': ${typeText}`, {
|
|
1426
|
+
parameterName,
|
|
1427
|
+
cacheKey
|
|
1428
|
+
});
|
|
1279
1429
|
const resolvedToken = this.resolveInferredTypeEnhanced(type, typeText, param, filePath, lineNumber, columnNumber);
|
|
1280
1430
|
this._typeResolutionCache.set(cacheKey, resolvedToken);
|
|
1281
1431
|
if (resolvedToken) return {
|
|
@@ -1369,7 +1519,10 @@ var AngularParser = class {
|
|
|
1369
1519
|
if (this._options.verbose) {
|
|
1370
1520
|
const className = classDeclaration.getName() || "unknown";
|
|
1371
1521
|
const filePath = classDeclaration.getSourceFile().getFilePath();
|
|
1372
|
-
|
|
1522
|
+
this.warn(LogCategory.ERROR_RECOVERY, `Failed to extract inject() dependencies for class '${className}' in ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
1523
|
+
className,
|
|
1524
|
+
filePath
|
|
1525
|
+
});
|
|
1373
1526
|
}
|
|
1374
1527
|
}
|
|
1375
1528
|
return dependencies;
|
|
@@ -1405,7 +1558,10 @@ var AngularParser = class {
|
|
|
1405
1558
|
if (this._options.verbose) {
|
|
1406
1559
|
const propertyName = property.getName() || "unknown";
|
|
1407
1560
|
const filePath = property.getSourceFile().getFilePath();
|
|
1408
|
-
|
|
1561
|
+
this.warn(LogCategory.ERROR_RECOVERY, `Failed to parse inject() property '${propertyName}' in ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
1562
|
+
propertyName,
|
|
1563
|
+
filePath
|
|
1564
|
+
});
|
|
1409
1565
|
}
|
|
1410
1566
|
return null;
|
|
1411
1567
|
}
|
|
@@ -1431,7 +1587,7 @@ var AngularParser = class {
|
|
|
1431
1587
|
const propertyAssignment = prop;
|
|
1432
1588
|
const name = propertyAssignment.getName();
|
|
1433
1589
|
if (!supportedOptions.has(name)) {
|
|
1434
|
-
if (this._options.verbose)
|
|
1590
|
+
if (this._options.verbose) this.warn(LogCategory.AST_ANALYSIS, `Unknown inject() option: '${name}' - ignoring`);
|
|
1435
1591
|
continue;
|
|
1436
1592
|
}
|
|
1437
1593
|
const initializer = propertyAssignment.getInitializer();
|
|
@@ -1451,7 +1607,7 @@ var AngularParser = class {
|
|
|
1451
1607
|
}
|
|
1452
1608
|
}
|
|
1453
1609
|
} catch (error) {
|
|
1454
|
-
if (this._options.verbose)
|
|
1610
|
+
if (this._options.verbose) this.warn(LogCategory.ERROR_RECOVERY, `Failed to parse inject() options: ${error instanceof Error ? error.message : String(error)}`);
|
|
1455
1611
|
}
|
|
1456
1612
|
return flags;
|
|
1457
1613
|
}
|
|
@@ -1496,14 +1652,21 @@ var AngularParser = class {
|
|
|
1496
1652
|
name: decoratorName,
|
|
1497
1653
|
reason: "Unknown or unsupported decorator"
|
|
1498
1654
|
});
|
|
1499
|
-
|
|
1655
|
+
const className = parameter.getFirstAncestorByKind(ts_morph.SyntaxKind.ClassDeclaration)?.getName() || void 0;
|
|
1656
|
+
this.warn(LogCategory.AST_ANALYSIS, `Unknown or unsupported decorator: @${decoratorName}() - This decorator is not recognized as an Angular DI decorator and will be ignored.`, {
|
|
1657
|
+
filePath: parameter.getSourceFile().getFilePath(),
|
|
1658
|
+
className
|
|
1659
|
+
});
|
|
1500
1660
|
}
|
|
1501
1661
|
}
|
|
1502
1662
|
} catch (error) {
|
|
1503
1663
|
if (this._options.verbose) {
|
|
1504
1664
|
const paramName = parameter.getName();
|
|
1505
1665
|
const filePath = parameter.getSourceFile().getFilePath();
|
|
1506
|
-
|
|
1666
|
+
this.warn(LogCategory.ERROR_RECOVERY, `Failed to extract decorators for parameter '${paramName}' in ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
1667
|
+
paramName,
|
|
1668
|
+
filePath
|
|
1669
|
+
});
|
|
1507
1670
|
}
|
|
1508
1671
|
return {};
|
|
1509
1672
|
}
|
|
@@ -1528,7 +1691,7 @@ var AngularParser = class {
|
|
|
1528
1691
|
}
|
|
1529
1692
|
return false;
|
|
1530
1693
|
} catch (error) {
|
|
1531
|
-
if (this._options.verbose)
|
|
1694
|
+
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() });
|
|
1532
1695
|
return true;
|
|
1533
1696
|
}
|
|
1534
1697
|
}
|
|
@@ -1542,15 +1705,19 @@ var AngularParser = class {
|
|
|
1542
1705
|
if (!expression) return null;
|
|
1543
1706
|
if (expression.getKind() !== ts_morph.SyntaxKind.CallExpression) return null;
|
|
1544
1707
|
const callExpression = expression;
|
|
1708
|
+
const sourceFile = expression.getSourceFile();
|
|
1709
|
+
const warnVerbose = (message) => {
|
|
1710
|
+
if (this._options.verbose) this.warn(LogCategory.AST_ANALYSIS, message, { filePath: sourceFile.getFilePath() });
|
|
1711
|
+
};
|
|
1545
1712
|
try {
|
|
1546
1713
|
const callIdentifier = callExpression.getExpression();
|
|
1547
1714
|
if (callIdentifier.getKind() !== ts_morph.SyntaxKind.Identifier) return null;
|
|
1548
1715
|
if (callIdentifier.getText() !== "inject") return null;
|
|
1549
|
-
const sourceFile = expression.getSourceFile();
|
|
1550
|
-
if (!this.isAngularInjectImported(sourceFile)) return null;
|
|
1716
|
+
const sourceFile$1 = expression.getSourceFile();
|
|
1717
|
+
if (!this.isAngularInjectImported(sourceFile$1)) return null;
|
|
1551
1718
|
const args = callExpression.getArguments();
|
|
1552
1719
|
if (args.length === 0) {
|
|
1553
|
-
|
|
1720
|
+
warnVerbose("inject() called without token parameter - skipping");
|
|
1554
1721
|
return null;
|
|
1555
1722
|
}
|
|
1556
1723
|
const tokenArg = args[0];
|
|
@@ -1558,22 +1725,22 @@ var AngularParser = class {
|
|
|
1558
1725
|
if (tokenArg.getKind() === ts_morph.SyntaxKind.StringLiteral) {
|
|
1559
1726
|
token = tokenArg.getText().slice(1, -1);
|
|
1560
1727
|
if (!token) {
|
|
1561
|
-
|
|
1728
|
+
warnVerbose("inject() called with empty string token - skipping");
|
|
1562
1729
|
return null;
|
|
1563
1730
|
}
|
|
1564
1731
|
} else if (tokenArg.getKind() === ts_morph.SyntaxKind.Identifier) {
|
|
1565
1732
|
token = tokenArg.getText();
|
|
1566
1733
|
if (token === "undefined" || token === "null") {
|
|
1567
|
-
|
|
1734
|
+
warnVerbose(`inject() called with ${token} token - skipping`);
|
|
1568
1735
|
return null;
|
|
1569
1736
|
}
|
|
1570
1737
|
} else if (tokenArg.getKind() === ts_morph.SyntaxKind.NullKeyword) {
|
|
1571
|
-
|
|
1738
|
+
warnVerbose("inject() called with null token - skipping");
|
|
1572
1739
|
return null;
|
|
1573
1740
|
} else {
|
|
1574
1741
|
token = tokenArg.getText();
|
|
1575
1742
|
if (!token) {
|
|
1576
|
-
|
|
1743
|
+
warnVerbose("inject() called with invalid token expression - skipping");
|
|
1577
1744
|
return null;
|
|
1578
1745
|
}
|
|
1579
1746
|
}
|
|
@@ -1581,9 +1748,7 @@ var AngularParser = class {
|
|
|
1581
1748
|
if (args.length > 1 && this._options.includeDecorators) {
|
|
1582
1749
|
const optionsArg = args[1];
|
|
1583
1750
|
if (optionsArg.getKind() === ts_morph.SyntaxKind.ObjectLiteralExpression) flags = this.parseInjectOptions(optionsArg);
|
|
1584
|
-
else if (optionsArg.getKind() !== ts_morph.SyntaxKind.NullKeyword && optionsArg.getKind() !== ts_morph.SyntaxKind.UndefinedKeyword) {
|
|
1585
|
-
if (this._options.verbose) console.warn(`inject() called with invalid options type: ${optionsArg.getKindName()} - expected object literal`);
|
|
1586
|
-
}
|
|
1751
|
+
else if (optionsArg.getKind() !== ts_morph.SyntaxKind.NullKeyword && optionsArg.getKind() !== ts_morph.SyntaxKind.UndefinedKeyword) warnVerbose(`inject() called with invalid options type: ${optionsArg.getKindName()} - expected object literal`);
|
|
1587
1752
|
}
|
|
1588
1753
|
return {
|
|
1589
1754
|
token,
|
|
@@ -1591,10 +1756,7 @@ var AngularParser = class {
|
|
|
1591
1756
|
source: "inject"
|
|
1592
1757
|
};
|
|
1593
1758
|
} catch (error) {
|
|
1594
|
-
if (this._options.verbose) {
|
|
1595
|
-
const filePath = expression.getSourceFile().getFilePath();
|
|
1596
|
-
console.warn(`Warning: Failed to analyze inject() call in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1597
|
-
}
|
|
1759
|
+
if (this._options.verbose) warnVerbose(`Failed to analyze inject() call in ${sourceFile.getFilePath()}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1598
1760
|
return null;
|
|
1599
1761
|
}
|
|
1600
1762
|
}
|
|
@@ -1606,8 +1768,11 @@ var AngularParser = class {
|
|
|
1606
1768
|
*/
|
|
1607
1769
|
collectVerboseStats(param, dependency, verboseStats) {
|
|
1608
1770
|
const paramName = param.getName();
|
|
1609
|
-
|
|
1610
|
-
|
|
1771
|
+
this.verboseDebug(LogCategory.AST_ANALYSIS, "Parameter analysis", {
|
|
1772
|
+
paramName,
|
|
1773
|
+
token: dependency.token,
|
|
1774
|
+
flags: dependency.flags
|
|
1775
|
+
});
|
|
1611
1776
|
if (Object.keys(dependency.flags || {}).length > 0) {
|
|
1612
1777
|
verboseStats.parametersWithDecorators++;
|
|
1613
1778
|
if (dependency.flags?.optional) verboseStats.decoratorCounts.optional++;
|
|
@@ -1627,7 +1792,10 @@ var AngularParser = class {
|
|
|
1627
1792
|
"Host"
|
|
1628
1793
|
].includes(decoratorName)) {
|
|
1629
1794
|
hasLegacyDecorators = true;
|
|
1630
|
-
|
|
1795
|
+
this.verboseDebug(LogCategory.AST_ANALYSIS, `Legacy decorator detected`, {
|
|
1796
|
+
decoratorName,
|
|
1797
|
+
paramName
|
|
1798
|
+
});
|
|
1631
1799
|
}
|
|
1632
1800
|
}
|
|
1633
1801
|
const initializer = param.getInitializer();
|
|
@@ -1638,29 +1806,44 @@ var AngularParser = class {
|
|
|
1638
1806
|
hasInjectPattern = true;
|
|
1639
1807
|
verboseStats.injectPatternsUsed++;
|
|
1640
1808
|
const flagsStr = JSON.stringify(dependency.flags);
|
|
1641
|
-
|
|
1809
|
+
this.verboseDebug(LogCategory.AST_ANALYSIS, "inject() options detected", {
|
|
1810
|
+
paramName,
|
|
1811
|
+
flags: flagsStr
|
|
1812
|
+
});
|
|
1642
1813
|
}
|
|
1643
1814
|
}
|
|
1644
1815
|
}
|
|
1645
1816
|
if (hasLegacyDecorators) {
|
|
1646
1817
|
verboseStats.legacyDecoratorsUsed++;
|
|
1647
1818
|
if (hasInjectPattern) {
|
|
1648
|
-
|
|
1649
|
-
|
|
1819
|
+
this.verboseInfo(LogCategory.AST_ANALYSIS, "Decorator precedence analysis", {
|
|
1820
|
+
paramName,
|
|
1821
|
+
legacyDecorators: true,
|
|
1822
|
+
injectPattern: true
|
|
1823
|
+
});
|
|
1650
1824
|
const appliedFlags = Object.keys(dependency.flags || {}).filter((key) => dependency.flags?.[key] === true).map((key) => `@${key.charAt(0).toUpperCase() + key.slice(1)}`).join(", ");
|
|
1651
|
-
|
|
1825
|
+
this.verboseDebug(LogCategory.AST_ANALYSIS, "Applied decorator flags", {
|
|
1826
|
+
paramName,
|
|
1827
|
+
appliedFlags
|
|
1828
|
+
});
|
|
1652
1829
|
const injectResult = this.analyzeInjectCall(initializer);
|
|
1653
1830
|
if (injectResult && Object.keys(injectResult.flags).length > 0) {
|
|
1654
1831
|
const overriddenFlags = JSON.stringify(injectResult.flags);
|
|
1655
|
-
|
|
1832
|
+
this.verboseDebug(LogCategory.AST_ANALYSIS, "Overridden inject() options", {
|
|
1833
|
+
paramName,
|
|
1834
|
+
overriddenFlags
|
|
1835
|
+
});
|
|
1656
1836
|
}
|
|
1657
1837
|
const finalFlags = JSON.stringify(dependency.flags);
|
|
1658
|
-
|
|
1838
|
+
this.verboseDebug(LogCategory.AST_ANALYSIS, "Final decorator flags", {
|
|
1839
|
+
paramName,
|
|
1840
|
+
finalFlags
|
|
1841
|
+
});
|
|
1659
1842
|
}
|
|
1660
1843
|
}
|
|
1661
1844
|
} else {
|
|
1662
1845
|
verboseStats.parametersWithoutDecorators++;
|
|
1663
|
-
|
|
1846
|
+
this.verboseDebug(LogCategory.AST_ANALYSIS, "No decorators detected", { paramName });
|
|
1664
1847
|
}
|
|
1665
1848
|
}
|
|
1666
1849
|
/**
|
|
@@ -1670,69 +1853,24 @@ var AngularParser = class {
|
|
|
1670
1853
|
* @param classDeclaration Class being analyzed
|
|
1671
1854
|
*/
|
|
1672
1855
|
outputVerboseAnalysis(dependencies, verboseStats, classDeclaration) {
|
|
1673
|
-
if (!this._options.verbose) return;
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
if (initializer) {
|
|
1692
|
-
const injectResult = this.analyzeInjectCall(initializer);
|
|
1693
|
-
if (injectResult) {
|
|
1694
|
-
if (injectResult.token.startsWith("\"") && injectResult.token.endsWith("\"")) console.log(`String token: ${injectResult.token}`);
|
|
1695
|
-
else console.log(`Service token: ${injectResult.token}`);
|
|
1696
|
-
if (Object.keys(injectResult.flags).length > 0) {
|
|
1697
|
-
const flagsStr = JSON.stringify(injectResult.flags);
|
|
1698
|
-
console.log(`inject() options detected: ${flagsStr}`);
|
|
1699
|
-
} else console.log("inject() with no options");
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
if (verboseStats.skippedDecorators.length > 0) {
|
|
1707
|
-
console.log("Skipped Decorators");
|
|
1708
|
-
for (const skipped of verboseStats.skippedDecorators) {
|
|
1709
|
-
console.log(`${skipped.name}`);
|
|
1710
|
-
console.log(`Reason: ${skipped.reason}`);
|
|
1711
|
-
}
|
|
1712
|
-
console.log(`Total skipped: ${verboseStats.skippedDecorators.length}`);
|
|
1713
|
-
}
|
|
1714
|
-
console.log("Performance Metrics");
|
|
1715
|
-
console.log(`Decorator processing time: ${verboseStats.totalProcessingTime.toFixed(2)}ms`);
|
|
1716
|
-
console.log(`Total parameters analyzed: ${verboseStats.totalParameters}`);
|
|
1717
|
-
if (verboseStats.totalParameters > 0) {
|
|
1718
|
-
const avgTime = verboseStats.totalProcessingTime / verboseStats.totalParameters;
|
|
1719
|
-
console.log(`Average time per parameter: ${avgTime.toFixed(3)}ms`);
|
|
1720
|
-
}
|
|
1721
|
-
console.log("=== Analysis Summary ===");
|
|
1722
|
-
console.log(`Total dependencies: ${dependencies.length}`);
|
|
1723
|
-
console.log(`With decorator flags: ${verboseStats.parametersWithDecorators}`);
|
|
1724
|
-
console.log(`Without decorator flags: ${verboseStats.parametersWithoutDecorators}`);
|
|
1725
|
-
console.log(`Legacy decorators used: ${verboseStats.legacyDecoratorsUsed}`);
|
|
1726
|
-
console.log(`inject() patterns used: ${verboseStats.injectPatternsUsed}`);
|
|
1727
|
-
if (verboseStats.skippedDecorators.length > 0) console.log(`Unknown decorators skipped: ${verboseStats.skippedDecorators.length}`);
|
|
1728
|
-
if (verboseStats.parametersWithDecorators > 0) {
|
|
1729
|
-
console.log("Flags distribution:");
|
|
1730
|
-
if (verboseStats.decoratorCounts.optional > 0) console.log(`optional: ${verboseStats.decoratorCounts.optional}`);
|
|
1731
|
-
if (verboseStats.decoratorCounts.self > 0) console.log(`self: ${verboseStats.decoratorCounts.self}`);
|
|
1732
|
-
if (verboseStats.decoratorCounts.skipSelf > 0) console.log(`skipSelf: ${verboseStats.decoratorCounts.skipSelf}`);
|
|
1733
|
-
if (verboseStats.decoratorCounts.host > 0) console.log(`host: ${verboseStats.decoratorCounts.host}`);
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1856
|
+
if (!this._options.verbose || !this._logger) return;
|
|
1857
|
+
const className = classDeclaration.getName() || "unknown";
|
|
1858
|
+
this._logger.info(LogCategory.AST_ANALYSIS, "Decorator analysis summary", {
|
|
1859
|
+
className,
|
|
1860
|
+
decoratorCounts: verboseStats.decoratorCounts,
|
|
1861
|
+
parametersWithDecorators: verboseStats.parametersWithDecorators,
|
|
1862
|
+
parametersWithoutDecorators: verboseStats.parametersWithoutDecorators,
|
|
1863
|
+
injectPatternsUsed: verboseStats.injectPatternsUsed,
|
|
1864
|
+
skippedDecorators: verboseStats.skippedDecorators.length,
|
|
1865
|
+
legacyDecoratorsUsed: verboseStats.legacyDecoratorsUsed,
|
|
1866
|
+
totalProcessingTime: verboseStats.totalProcessingTime,
|
|
1867
|
+
totalParameters: verboseStats.totalParameters,
|
|
1868
|
+
dependencyCount: dependencies.length
|
|
1869
|
+
});
|
|
1870
|
+
if (verboseStats.skippedDecorators.length > 0) this._logger.debug(LogCategory.AST_ANALYSIS, "Skipped decorators", {
|
|
1871
|
+
skippedDecorators: verboseStats.skippedDecorators,
|
|
1872
|
+
className
|
|
1873
|
+
});
|
|
1736
1874
|
}
|
|
1737
1875
|
};
|
|
1738
1876
|
|
|
@@ -1858,8 +1996,8 @@ function enforceMinimumNodeVersion() {
|
|
|
1858
1996
|
process.exit(1);
|
|
1859
1997
|
}
|
|
1860
1998
|
enforceMinimumNodeVersion();
|
|
1861
|
-
function resolveProjectPath(
|
|
1862
|
-
const candidatePath =
|
|
1999
|
+
function resolveProjectPath(projectOption) {
|
|
2000
|
+
const candidatePath = projectOption;
|
|
1863
2001
|
try {
|
|
1864
2002
|
if ((0, node_fs.statSync)(candidatePath).isDirectory()) {
|
|
1865
2003
|
const tsconfigPath = (0, node_path.join)(candidatePath, "tsconfig.json");
|
|
@@ -1870,18 +2008,22 @@ function resolveProjectPath(projectPath, projectOption) {
|
|
|
1870
2008
|
}
|
|
1871
2009
|
const program = new commander.Command();
|
|
1872
2010
|
program.name("ng-di-graph").description("Angular DI dependency graph CLI tool").version("0.1.0");
|
|
1873
|
-
program.argument("[
|
|
1874
|
-
program.action(async (
|
|
2011
|
+
program.argument("[filePaths...]", "TypeScript files to analyze (alias for --files)").option("-p, --project <path>", "tsconfig.json path", "./tsconfig.json").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
|
+
program.action(async (filePaths = [], options) => {
|
|
1875
2013
|
try {
|
|
1876
|
-
const project = resolveProjectPath(
|
|
2014
|
+
const project = resolveProjectPath(options.project);
|
|
1877
2015
|
if (options.direction && ![
|
|
1878
2016
|
"upstream",
|
|
1879
2017
|
"downstream",
|
|
1880
2018
|
"both"
|
|
1881
2019
|
].includes(options.direction)) throw ErrorHandler.createError(`Invalid direction: ${options.direction}. Must be 'upstream', 'downstream', or 'both'`, "INVALID_ARGUMENTS");
|
|
1882
2020
|
if (options.format && !["json", "mermaid"].includes(options.format)) throw ErrorHandler.createError(`Invalid format: ${options.format}. Must be 'json' or 'mermaid'`, "INVALID_ARGUMENTS");
|
|
2021
|
+
const tsconfigLikePositional = filePaths.find((filePath) => /(?:^|[\\/])tsconfig(?:\.[^/\\]+)?\.json$/.test(filePath));
|
|
2022
|
+
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);
|
|
1883
2024
|
const cliOptions = {
|
|
1884
2025
|
project,
|
|
2026
|
+
files: mergedFiles.length > 0 ? mergedFiles : void 0,
|
|
1885
2027
|
format: options.format,
|
|
1886
2028
|
entry: options.entry,
|
|
1887
2029
|
direction: options.direction,
|
|
@@ -1917,7 +2059,7 @@ program.action(async (projectPath, options) => {
|
|
|
1917
2059
|
}
|
|
1918
2060
|
if (cliOptions.entry && cliOptions.entry.length > 0) {
|
|
1919
2061
|
if (cliOptions.verbose) console.log(`🔍 Filtering graph by entry points: ${cliOptions.entry.join(", ")}`);
|
|
1920
|
-
graph = filterGraph(graph, cliOptions);
|
|
2062
|
+
graph = filterGraph(graph, cliOptions, logger);
|
|
1921
2063
|
if (cliOptions.verbose) console.log(`✅ Filtered graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
|
|
1922
2064
|
}
|
|
1923
2065
|
let formatter;
|
|
@@ -1945,6 +2087,11 @@ program.action(async (projectPath, options) => {
|
|
|
1945
2087
|
}
|
|
1946
2088
|
}
|
|
1947
2089
|
});
|
|
2090
|
+
function mergeFileTargets(positionalFiles, flagFiles) {
|
|
2091
|
+
const merged = [];
|
|
2092
|
+
for (const filePath of [...positionalFiles, ...flagFiles ?? []]) if (!merged.includes(filePath)) merged.push(filePath);
|
|
2093
|
+
return merged;
|
|
2094
|
+
}
|
|
1948
2095
|
process.on("unhandledRejection", (reason, promise) => {
|
|
1949
2096
|
const error = ErrorHandler.createError(`Unhandled promise rejection: ${reason}`, "INTERNAL_ERROR", void 0, { promise: String(promise) });
|
|
1950
2097
|
ErrorHandler.handleError(error, false);
|