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

659 lines (651 loc) 24.1 kB
import * as fs from "fs"; import * as path from "path"; import { Project, ts, } from "ts-morph"; import { DICompiler } from "./zero-reflection"; export class ASTDependencyCompiler { project; providers = new Map(); modules = new Map(); sourceRoot; constructor(tsConfigPath, sourceRoot = "src") { this.sourceRoot = sourceRoot; this.project = new Project({ ...(tsConfigPath && { tsConfigFilePath: tsConfigPath }), skipAddingFilesFromTsConfig: false, }); } /** * Scan project files for DI metadata */ async scanProject() { const sourceFiles = this.project.getSourceFiles(); for (const sourceFile of sourceFiles) { await this.scanSourceFile(sourceFile); } } /** * Incremental scan of specific files for watch mode */ async incrementalScan(filePaths) { // Clear existing data for affected files for (const filePath of filePaths) { this.clearFileData(filePath); } // Add/update source files in the project for (const filePath of filePaths) { try { const sourceFile = this.project.addSourceFileAtPath(filePath); await this.scanSourceFile(sourceFile); } catch (error) { // File might have been deleted console.debug(`Could not scan file ${filePath}:`, error); } } } /** * Clear cached data for a specific file */ clearFileData(filePath) { const normalizedPath = this.getRelativeImportPath(filePath); // Remove providers from this file for (const [token, provider] of this.providers.entries()) { if (provider.filePath === normalizedPath) { this.providers.delete(token); } } // Remove modules from this file for (const [name, module] of this.modules.entries()) { if (module.filePath === normalizedPath) { this.modules.delete(name); } } } /** * Scan specific source file */ async scanSourceFile(sourceFile) { const classes = sourceFile.getClasses(); for (const classDecl of classes) { const provider = this.extractProviderInfo(classDecl, sourceFile); if (provider) { this.providers.set(provider.token, provider); } } // Look for module declarations const module = this.extractModuleInfo(sourceFile); if (module) { this.modules.set(module.name, module); } } /** * Extract provider information from class declaration */ extractProviderInfo(classDecl, sourceFile) { const injectableDecorator = this.findDecorator(classDecl, "Injectable"); if (!injectableDecorator) return null; const className = classDecl.getName(); if (!className) return null; // Extract decorator arguments safely (no eval!) const decoratorOptions = this.parseDecoratorArguments(injectableDecorator); // Get constructor dependencies const dependencies = this.extractDependencies(classDecl); // Generate relative import path const filePath = this.getRelativeImportPath(sourceFile.getFilePath()); return { token: decoratorOptions.token || className, className, filePath, dependencies, singleton: decoratorOptions.singleton ?? true, scope: decoratorOptions.scope || "singleton", }; } /** * Extract module information from source file */ extractModuleInfo(sourceFile) { // Look for classes with @Module decorator const classes = sourceFile.getClasses(); for (const classDecl of classes) { const moduleDecorator = this.findDecorator(classDecl, "Module"); if (moduleDecorator) { const className = classDecl.getName(); if (!className) continue; const moduleOptions = this.parseDecoratorArguments(moduleDecorator); return { name: className, filePath: this.getRelativeImportPath(sourceFile.getFilePath()), providers: this.resolveProvidersFromOptions(moduleOptions), imports: this.parseModuleImports(moduleOptions.imports || []), exports: this.parseModuleExports(moduleOptions.exports || []), namespace: moduleOptions.namespace, }; } } return null; } /** * Resolve providers from module options */ resolveProvidersFromOptions(moduleOptions) { const providers = []; if (moduleOptions.providers && Array.isArray(moduleOptions.providers)) { for (const providerToken of moduleOptions.providers) { const provider = this.providers.get(providerToken); if (provider) { providers.push(provider); } } } return providers; } /** * Parse module imports */ parseModuleImports(imports) { const moduleImports = []; for (const importItem of imports) { if (typeof importItem === "string") { // Simple string import moduleImports.push({ module: importItem, }); } else if (typeof importItem === "object") { // Detailed import object moduleImports.push({ module: importItem.module || importItem.name, providers: importItem.providers, namespace: importItem.namespace, filePath: importItem.filePath, }); } } return moduleImports; } /** * Parse module exports */ parseModuleExports(exports) { const moduleExports = []; for (const exportItem of exports) { if (typeof exportItem === "string") { // Simple token export moduleExports.push({ token: exportItem, }); } else if (typeof exportItem === "object") { // Detailed export object moduleExports.push({ token: exportItem.token || exportItem.name, name: exportItem.name, reExport: exportItem.reExport || false, fromModule: exportItem.fromModule, }); } } return moduleExports; } /** * Find decorator by name */ 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 ts-morph AST - no eval() or regex */ 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 ts-morph AST (safe, no eval!) */ 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 "deps": result.deps = this.parseArrayValue(initializer); break; case "inject": result.inject = this.parseArrayValue(initializer); break; case "providers": result.providers = this.parseArrayValue(initializer); break; case "imports": result.imports = this.parseArrayValue(initializer); break; case "exports": result.exports = this.parseArrayValue(initializer); break; default: // Try to parse as literal value result[name] = this.parseLiteralValue(initializer); } } } return result; } catch (error) { console.warn("Failed to parse object literal with AST:", error); return {}; } } /** * Parse token value (string, symbol, or identifier) */ 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 */ parseBooleanValue(node) { switch (node.getKind()) { case ts.SyntaxKind.TrueKeyword: return true; case ts.SyntaxKind.FalseKeyword: return false; default: return undefined; } } /** * Parse string value */ parseStringValue(node) { if (node.getKind() === ts.SyntaxKind.StringLiteral) { return node.getLiteralValue(); } return undefined; } /** * Parse array value */ parseArrayValue(node) { if (node.getKind() === ts.SyntaxKind.ArrayLiteralExpression) { const elements = node.getElements(); return elements.map((element) => this.parseLiteralValue(element)); } return []; } /** * Parse literal value (string, number, boolean, identifier) */ 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 */ 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()); } } } 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; return args[0]?.getText().replace(/['"]/g, "") || null; } /** * Compile to DI container with validation */ compile() { // Validate modules and dependencies this.validateModules(); this.validateDependencies(); const compiler = new DICompiler(); // Register all providers for (const provider of this.providers.values()) { compiler.register({ token: provider.token, useClass: provider.className, // Will be resolved at runtime deps: provider.dependencies, scope: provider.scope, singleton: provider.singleton, }); } const container = compiler.compile(); const generatedCode = this.generateRuntimeCode(); return { providers: Array.from(this.providers.values()), modules: Array.from(this.modules.values()), container, generatedCode, }; } /** * Validate module imports and exports */ validateModules() { const errors = []; for (const module of this.modules.values()) { // Validate imports for (const importItem of module.imports) { const importedModule = this.modules.get(importItem.module); if (!importedModule) { errors.push(`Module ${module.name}: imported module '${importItem.module}' not found`); continue; } // Check if requested providers are actually exported if (importItem.providers) { for (const providerToken of importItem.providers) { const isExported = importedModule.exports.some((exp) => exp.token === providerToken); if (!isExported) { errors.push(`Module ${module.name}: imported provider '${String(providerToken)}' is not exported by '${importItem.module}'`); } } } } // Validate exports for (const exportItem of module.exports) { // Check if exported provider exists const provider = this.providers.get(exportItem.token); if (!provider && !exportItem.reExport) { errors.push(`Module ${module.name}: exported provider '${String(exportItem.token)}' not found`); } // Validate re-exports if (exportItem.reExport && exportItem.fromModule) { const sourceModule = this.modules.get(exportItem.fromModule); if (!sourceModule) { errors.push(`Module ${module.name}: re-export from module '${exportItem.fromModule}' not found`); } } } // Check for duplicate exports const exportTokens = module.exports.map((exp) => exp.token); const duplicates = exportTokens.filter((token, index) => exportTokens.indexOf(token) !== index); if (duplicates.length > 0) { errors.push(`Module ${module.name}: duplicate exports: ${duplicates.join(", ")}`); } } if (errors.length > 0) { throw new Error(`Module validation failed:\n${errors.join("\n")}`); } } /** * Validate dependencies and scope rules */ validateDependencies() { const errors = []; for (const provider of this.providers.values()) { // Check for missing dependencies for (const dep of provider.dependencies) { if (!this.providers.has(dep) && !this.isBuiltInToken(dep)) { errors.push(`Provider '${String(provider.token)}': dependency '${String(dep)}' not found`); } } // Validate scope rules if (provider.scope === "request") { for (const dep of provider.dependencies) { const depProvider = this.providers.get(dep); if (depProvider && depProvider.scope === "singleton") { console.warn(`Warning: Request-scoped provider '${String(provider.token)}' depends on singleton '${String(dep)}'. ` + "This may cause memory leaks."); } } } // Check for circular dependencies const visited = new Set(); const visiting = new Set(); if (this.hasCircularDependency(provider.token, visited, visiting)) { errors.push(`Circular dependency detected involving '${String(provider.token)}'`); } } // Check for duplicate tokens const tokens = Array.from(this.providers.keys()); const duplicates = tokens.filter((token, index) => tokens.indexOf(token) !== index); if (duplicates.length > 0) { errors.push(`Duplicate provider tokens: ${duplicates.join(", ")}`); } if (errors.length > 0) { throw new Error(`Dependency validation failed:\n${errors.join("\n")}`); } } /** * Check for circular dependencies */ hasCircularDependency(token, visited, visiting) { if (visiting.has(token)) { return true; } if (visited.has(token)) { return false; } visiting.add(token); const provider = this.providers.get(token); if (provider) { for (const dep of provider.dependencies) { if (this.hasCircularDependency(dep, visited, visiting)) { return true; } } } visiting.delete(token); visited.add(token); return false; } /** * Check if token is built-in (framework provided) */ isBuiltInToken(token) { const builtInTokens = [ "FastifyRequest", "FastifyReply", "FastifyInstance", "Logger", "Config", ]; return builtInTokens.includes(String(token)); } /** * Generate ESM-safe runtime code for the compiled container */ generateRuntimeCode() { const imports = new Set(); const providers = []; const modules = []; // Generate imports and provider registrations for (const provider of this.providers.values()) { const esmPath = this.convertToESMPath(provider.filePath); imports.add(`import { ${provider.className} } from '${esmPath}';`); providers.push(` compiler.register({ token: '${String(provider.token)}', useClass: ${provider.className}, deps: [${provider.dependencies.map((dep) => `'${String(dep)}'`).join(", ")}], scope: '${provider.scope}', singleton: ${provider.singleton}, });`); } // Generate module registrations for (const module of this.modules.values()) { const esmPath = this.convertToESMPath(module.filePath); imports.add(`import { ${module.name} } from '${esmPath}';`); const moduleImports = module.imports .map((imp) => `'${imp.module}'`) .join(", "); const moduleExports = module.exports .map((exp) => `'${String(exp.token)}'`) .join(", "); modules.push(` // Module: ${module.name} moduleRegistry.register({ name: '${module.name}', imports: [${moduleImports}], exports: [${moduleExports}], ${module.namespace ? `namespace: '${module.namespace}',` : ""} });`); } return ` // Generated DI container - DO NOT EDIT // This file is auto-generated. Do not modify manually. import { DICompiler, FastContainer } from './di/zero-reflection.js'; import { ModuleRegistry } from './di/module-registry.js'; ${Array.from(imports).join("\n")} export function createCompiledContainer(): FastContainer { const compiler = new DICompiler(); const moduleRegistry = new ModuleRegistry(); // Register providers ${providers.join("\n")} // Register modules ${modules.join("\n")} return new FastContainer(compiler.compile()); } export const container = createCompiledContainer(); // Export for HMR and testing export { DICompiler, FastContainer }; // Module metadata for debugging export const moduleMetadata = { generated: '${new Date().toISOString()}', providers: ${this.providers.size}, modules: ${this.modules.size}, framework: 'Pulzar DI v1.0.0' }; `; } /** * Convert file path to ESM-compatible path with .js extension */ convertToESMPath(filePath) { let esmPath = filePath; // Ensure relative path if (!esmPath.startsWith("./") && !esmPath.startsWith("../")) { esmPath = "./" + esmPath; } // Add .js extension for ESM compatibility if (!esmPath.endsWith(".js") && !esmPath.endsWith(".mjs")) { esmPath = esmPath.replace(/\.ts$/, "") + ".js"; } return esmPath; } /** * Get relative import path with proper ESM handling */ getRelativeImportPath(absolutePath) { const relativePath = path.relative(process.cwd(), absolutePath); let withoutExtension = relativePath.replace(/\.ts$/, ""); // 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; } /** * Save generated code to file */ async saveGeneratedCode(outputPath, result) { await fs.promises.writeFile(outputPath, result.generatedCode, "utf8"); // Also save metadata as JSON const metadataPath = outputPath.replace(/\.ts$/, ".metadata.json"); const metadata = { providers: result.providers, modules: result.modules, timestamp: new Date().toISOString(), }; await fs.promises.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf8"); } } export function createASTCompiler(tsConfigPath, sourceRoot) { return new ASTDependencyCompiler(tsConfigPath, sourceRoot); } //# sourceMappingURL=ast-compiler.js.map