UNPKG

@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
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