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