@pulzar/core
Version:
Next-generation Node.js framework for ultra-fast web applications with zero-reflection DI, GraphQL, WebSockets, events, and edge runtime support
498 lines • 17.5 kB
JavaScript
import * as fs from "fs";
import * as path from "path";
import { Project, ts, } from "ts-morph";
export class DependencyScanner {
providers = new Map();
modules = new Map();
project;
scannedFiles = new Set();
constructor(tsConfigPath) {
this.project = new Project({
...(tsConfigPath && { tsConfigFilePath: tsConfigPath }),
skipAddingFilesFromTsConfig: false,
});
}
/**
* Scan a single TypeScript file for DI metadata
*/
scanFile(filePath) {
try {
// Normalize and resolve file path
const resolvedPath = path.resolve(filePath);
if (this.scannedFiles.has(resolvedPath)) {
// Return cached results for already scanned files
return Array.from(this.providers.values()).filter((provider) => provider.filePath === this.getRelativeImportPath(resolvedPath));
}
// Check if file exists
if (!fs.existsSync(resolvedPath)) {
console.warn(`File not found: ${resolvedPath}`);
return [];
}
// Check if it's a TypeScript file
if (!resolvedPath.endsWith(".ts") && !resolvedPath.endsWith(".tsx")) {
console.warn(`Not a TypeScript file: ${resolvedPath}`);
return [];
}
// Add source file to project
const sourceFile = this.project.addSourceFileAtPath(resolvedPath);
const providers = this.scanSourceFile(sourceFile);
this.scannedFiles.add(resolvedPath);
return providers;
}
catch (error) {
console.error(`Error scanning file ${filePath}:`, error);
return [];
}
}
/**
* Scan a directory recursively for TypeScript files
*/
scanDirectory(dirPath) {
try {
const resolvedPath = path.resolve(dirPath);
if (!fs.existsSync(resolvedPath)) {
console.warn(`Directory not found: ${resolvedPath}`);
return [];
}
const stats = fs.statSync(resolvedPath);
if (!stats.isDirectory()) {
console.warn(`Path is not a directory: ${resolvedPath}`);
return [];
}
const allProviders = [];
const filesToScan = this.getTypeScriptFiles(resolvedPath);
for (const filePath of filesToScan) {
const providers = this.scanFile(filePath);
allProviders.push(...providers);
}
return allProviders;
}
catch (error) {
console.error(`Error scanning directory ${dirPath}:`, error);
return [];
}
}
/**
* Get all TypeScript files in a directory recursively
*/
getTypeScriptFiles(dirPath) {
const files = [];
const scanDir = (currentDir) => {
try {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
// Skip node_modules, .git, and other hidden directories
if (!entry.name.startsWith(".") &&
entry.name !== "node_modules" &&
entry.name !== "dist" &&
entry.name !== "build") {
scanDir(fullPath);
}
}
else if (entry.isFile()) {
// Include .ts and .tsx files, exclude .d.ts files
if ((entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) &&
!entry.name.endsWith(".d.ts") &&
!entry.name.endsWith(".test.ts") &&
!entry.name.endsWith(".spec.ts")) {
files.push(fullPath);
}
}
}
}
catch (error) {
console.warn(`Could not read directory ${currentDir}:`, error);
}
};
scanDir(dirPath);
return files;
}
/**
* Scan a source file for providers and modules
*/
scanSourceFile(sourceFile) {
const providers = [];
const classes = sourceFile.getClasses();
for (const classDecl of classes) {
// Scan for providers
const provider = this.extractProviderFromClass(classDecl, sourceFile);
if (provider) {
this.providers.set(provider.token, provider);
providers.push(provider);
}
// Scan for modules
const module = this.extractModuleFromClass(classDecl, sourceFile);
if (module) {
this.modules.set(module.name, module);
}
}
return providers;
}
/**
* Extract provider information from a class declaration
*/
extractProviderFromClass(classDecl, sourceFile) {
const injectableDecorator = this.findDecorator(classDecl, "Injectable");
if (!injectableDecorator) {
return null;
}
const className = classDecl.getName();
if (!className) {
return null;
}
// Parse decorator options
const decoratorOptions = this.parseDecoratorArguments(injectableDecorator);
// Extract constructor dependencies
const dependencies = this.extractDependencies(classDecl);
// Create a mock class constructor for the token
const mockClass = class {
};
Object.defineProperty(mockClass, "name", { value: className });
return {
token: decoratorOptions.token || className,
class: mockClass,
dependencies,
singleton: decoratorOptions.singleton ?? true,
scope: decoratorOptions.scope ||
(decoratorOptions.singleton !== false ? "singleton" : "transient"),
filePath: this.getRelativeImportPath(sourceFile.getFilePath()),
};
}
/**
* Extract module information from a class declaration
*/
extractModuleFromClass(classDecl, sourceFile) {
const moduleDecorator = this.findDecorator(classDecl, "Module");
if (!moduleDecorator) {
return null;
}
const className = classDecl.getName();
if (!className) {
return null;
}
const decoratorOptions = this.parseDecoratorArguments(moduleDecorator);
return {
name: className,
providers: decoratorOptions.providers || [],
imports: decoratorOptions.imports || [],
exports: decoratorOptions.exports || [],
filePath: this.getRelativeImportPath(sourceFile.getFilePath()),
namespace: decoratorOptions.namespace,
};
}
/**
* Find a decorator by name on a class declaration
*/
findDecorator(classDecl, decoratorName) {
return classDecl.getDecorators().find((decorator) => {
const expression = decorator.getCallExpression();
if (expression) {
const identifier = expression.getExpression();
return identifier.getText() === decoratorName;
}
return decorator.getName() === decoratorName;
});
}
/**
* Parse decorator arguments using AST
*/
parseDecoratorArguments(decorator) {
const callExpression = decorator.getCallExpression();
if (!callExpression)
return {};
const args = callExpression.getArguments();
if (args.length === 0)
return {};
const firstArg = args[0];
if (firstArg &&
firstArg.getKind() === ts.SyntaxKind.ObjectLiteralExpression) {
return this.parseObjectLiteralAST(firstArg);
}
return {};
}
/**
* Parse object literal using AST
*/
parseObjectLiteralAST(objectLiteral) {
const result = {};
try {
const properties = objectLiteral.getProperties();
for (const prop of properties) {
if (prop.getKind() === ts.SyntaxKind.PropertyAssignment) {
const name = prop.getName();
const initializer = prop.getInitializer();
if (!initializer)
continue;
switch (name) {
case "token":
result.token = this.parseTokenValue(initializer);
break;
case "singleton":
result.singleton = this.parseBooleanValue(initializer);
break;
case "scope":
result.scope = this.parseStringValue(initializer);
break;
case "providers":
case "imports":
case "exports":
result[name] = this.parseArrayValue(initializer);
break;
case "namespace":
result.namespace = this.parseStringValue(initializer);
break;
default:
result[name] = this.parseLiteralValue(initializer);
}
}
}
return result;
}
catch (error) {
console.warn("Failed to parse object literal with AST:", error);
return {};
}
}
/**
* Parse token value from AST node
*/
parseTokenValue(node) {
switch (node.getKind()) {
case ts.SyntaxKind.StringLiteral:
return node.getLiteralValue();
case ts.SyntaxKind.Identifier:
return node.getText();
case ts.SyntaxKind.PropertyAccessExpression:
return node.getText();
default:
return node.getText();
}
}
/**
* Parse boolean value from AST node
*/
parseBooleanValue(node) {
switch (node.getKind()) {
case ts.SyntaxKind.TrueKeyword:
return true;
case ts.SyntaxKind.FalseKeyword:
return false;
default:
return undefined;
}
}
/**
* Parse string value from AST node
*/
parseStringValue(node) {
if (node.getKind() === ts.SyntaxKind.StringLiteral) {
return node.getLiteralValue();
}
return undefined;
}
/**
* Parse array value from AST node
*/
parseArrayValue(node) {
if (node.getKind() === ts.SyntaxKind.ArrayLiteralExpression) {
const elements = node.getElements();
return elements.map((element) => this.parseLiteralValue(element));
}
return [];
}
/**
* Parse literal value from AST node
*/
parseLiteralValue(node) {
switch (node.getKind()) {
case ts.SyntaxKind.StringLiteral:
return node.getLiteralValue();
case ts.SyntaxKind.NumericLiteral:
return Number(node.getLiteralValue());
case ts.SyntaxKind.TrueKeyword:
return true;
case ts.SyntaxKind.FalseKeyword:
return false;
case ts.SyntaxKind.Identifier:
return node.getText();
case ts.SyntaxKind.PropertyAccessExpression:
return node.getText();
default:
return node.getText();
}
}
/**
* Extract constructor dependencies from a class
*/
extractDependencies(classDecl) {
const constructor = classDecl.getConstructors()[0];
if (!constructor)
return [];
const parameters = constructor.getParameters();
const dependencies = [];
for (const param of parameters) {
// Look for @Inject decorator
const injectDecorator = param
.getDecorators()
.find((d) => d.getName() === "Inject" ||
d.getCallExpression()?.getExpression().getText() === "Inject");
if (injectDecorator) {
const token = this.extractInjectToken(injectDecorator);
if (token) {
dependencies.push(token);
}
}
else {
// Use parameter type as token
const typeNode = param.getTypeNode();
if (typeNode) {
dependencies.push(typeNode.getText());
}
else {
// Fallback to parameter name
const paramName = param.getName();
if (paramName) {
dependencies.push(paramName);
}
}
}
}
return dependencies;
}
/**
* Extract token from @Inject decorator
*/
extractInjectToken(decorator) {
const callExpression = decorator.getCallExpression();
if (!callExpression)
return null;
const args = callExpression.getArguments();
if (args.length === 0)
return null;
const firstArg = args[0];
return firstArg ? this.parseLiteralValue(firstArg) : null;
}
/**
* Get relative import path for a file
*/
getRelativeImportPath(absolutePath) {
const relativePath = path.relative(process.cwd(), absolutePath);
let withoutExtension = relativePath.replace(/\.tsx?$/, "");
// Handle different OS path separators
withoutExtension = withoutExtension.replace(/\\/g, "/");
// Ensure it starts with ./ for relative imports
if (!withoutExtension.startsWith("./") &&
!withoutExtension.startsWith("../")) {
withoutExtension = "./" + withoutExtension;
}
return withoutExtension;
}
addProvider(provider) {
this.providers.set(provider.token, provider);
}
addModule(module) {
this.modules.set(module.name, module);
}
getProviders() {
return Array.from(this.providers.values());
}
getModules() {
return Array.from(this.modules.values());
}
getProvider(token) {
return this.providers.get(token);
}
getModule(name) {
return this.modules.get(name);
}
generateGraph() {
// Generate dependency graph from scanned providers
const graph = {
providers: this.getProviders(),
modules: this.getModules(),
dependencies: this.buildDependencyMap(),
};
return graph;
}
buildDependencyMap() {
const dependencyMap = new Map();
for (const provider of this.providers.values()) {
dependencyMap.set(provider.token, provider.dependencies);
}
return dependencyMap;
}
validate() {
const errors = [];
// Check for circular dependencies
for (const provider of this.providers.values()) {
const visited = new Set();
if (this.hasCircularDependency(provider.token, visited)) {
errors.push(`Circular dependency detected for provider: ${String(provider.token)}`);
}
}
// Check for missing dependencies
for (const provider of this.providers.values()) {
for (const dep of provider.dependencies) {
if (!this.providers.has(dep)) {
errors.push(`Missing dependency: ${String(dep)} for provider: ${String(provider.token)}`);
}
}
}
return {
valid: errors.length === 0,
errors,
};
}
hasCircularDependency(token, visited) {
if (visited.has(token)) {
return true;
}
visited.add(token);
const provider = this.providers.get(token);
if (provider) {
for (const dep of provider.dependencies) {
if (this.hasCircularDependency(dep, visited)) {
return true;
}
}
}
visited.delete(token);
return false;
}
/**
* Clear all scanned data
*/
clear() {
this.providers.clear();
this.modules.clear();
this.scannedFiles.clear();
}
/**
* Get scanning statistics
*/
getStats() {
return {
totalProviders: this.providers.size,
totalModules: this.modules.size,
scannedFiles: this.scannedFiles.size,
providersByScope: this.getProvidersByScope(),
};
}
/**
* Get providers grouped by scope
*/
getProvidersByScope() {
const scopes = { singleton: 0, transient: 0, request: 0 };
for (const provider of this.providers.values()) {
const scope = provider.scope || "singleton";
scopes[scope]++;
}
return scopes;
}
}
export function createDependencyScanner(tsConfigPath) {
return new DependencyScanner(tsConfigPath);
}
//# sourceMappingURL=scanner.js.map