@notjustcoders/ioc-arise
Version:
Arise type-safe IoC containers from your code. Zero overhead, zero coupling.
330 lines (327 loc) • 16.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TypeDeclarationGenerator = void 0;
const path_1 = require("path");
const fs_1 = require("fs");
const errorFactory_1 = require("../errors/errorFactory");
class TypeDeclarationGenerator {
static generate(classes, outputPath, factories, values, pathsResolver) {
// Check for name collisions before generating
this.checkForNameCollisions(classes);
// outputPath already has .d.ts extension from the generator
const typeDeclarations = this.generateTypeDeclarations(classes, outputPath, factories, values, pathsResolver);
(0, fs_1.mkdirSync)((0, path_1.dirname)(outputPath), { recursive: true });
(0, fs_1.writeFileSync)(outputPath, typeDeclarations);
}
static generateTypeDeclarations(classes, outputPath, factories, values, pathsResolver) {
// Filter classes that have interface implementations or abstract class implementations
const interfaceClasses = classes.filter(cls => cls.interfaceName);
const abstractClasses = classes.filter(cls => cls.abstractClassName);
// Classes without interfaces or abstract classes (direct class registrations)
const directClasses = classes.filter(cls => !cls.interfaceName && !cls.abstractClassName);
// Check if we have anything to generate (classes, factories, or values)
const hasFactories = factories && factories.length > 0;
const hasValues = values && values.length > 0;
const hasClasses = interfaceClasses.length > 0 || abstractClasses.length > 0 || directClasses.length > 0;
if (!hasClasses && !hasFactories && !hasValues) {
// Nothing to generate, return empty interface
return `/**
* This file is auto-generated by ioc-arise.
* Do not modify this file manually.
*/
export interface ContainerRegistry {}
`;
}
const typeImports = this.generateTypeImports(interfaceClasses, abstractClasses, directClasses, outputPath, factories, values, pathsResolver);
// Build registry entries for interfaces, abstract classes, direct classes, and factories
const registryEntries = [];
interfaceClasses.forEach(cls => {
registryEntries.push(` '${cls.interfaceName}': ${cls.interfaceName};`);
});
abstractClasses.forEach(cls => {
// For abstract classes, we use the concrete implementation type
registryEntries.push(` '${cls.abstractClassName}': ${cls.name};`);
});
// For direct classes, use the class name as the token and the class type
directClasses.forEach(cls => {
registryEntries.push(` '${cls.name}': ${cls.name};`);
});
// For factories, use the function name as the token
if (factories) {
factories.forEach(factory => {
if (factory.instanceFactoryFor) {
// Instance factory: registered under the interface name with the interface type
registryEntries.push(` '${factory.instanceFactoryFor}': ${factory.instanceFactoryFor};`);
}
else {
const token = factory.token || factory.name;
// Use ReturnType<typeof factoryName> as the type
registryEntries.push(` '${token}': ReturnType<typeof ${factory.name}>;`);
}
});
}
// For values, use the interface name or value name as the token
if (values) {
values.forEach(value => {
const token = value.token || value.interfaceName || value.name;
// Use the interface type if available, otherwise use typeof valueName
const type = value.interfaceName || `typeof ${value.name}`;
registryEntries.push(` '${token}': ${type};`);
});
}
return `/**
* This file is auto-generated by ioc-arise.
* Do not modify this file manually.
*
* This file provides type-safe resolution for string-based tokens.
*/
${typeImports}
export interface ContainerRegistry {
${registryEntries.join('\n')}
}
`;
}
static generateTypeImports(interfaceClasses, abstractClasses, directClasses, outputPath, factories, values, pathsResolver) {
const outputDir = (0, path_1.dirname)(outputPath);
const imports = [];
// Build a map of interface names to their actual file paths by searching the source
const interfaceLocations = this.findInterfaceLocations(interfaceClasses, outputDir, pathsResolver);
interfaceClasses.forEach(cls => {
if (!cls.interfaceName)
return;
const relativePath = interfaceLocations.get(cls.interfaceName);
if (relativePath) {
imports.push(`import type { ${cls.interfaceName} } from '${relativePath}';`);
}
});
// For abstract classes, import the concrete implementation class
abstractClasses.forEach(cls => {
let relativePath = (0, path_1.relative)(outputDir, cls.filePath);
relativePath = relativePath.replace(/\.ts$/, '');
if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`;
}
relativePath = relativePath.replace(/\\/g, '/');
imports.push(`import type { ${cls.name} } from '${relativePath}';`);
});
// For direct classes, import without aliases
directClasses.forEach(cls => {
let relativePath = (0, path_1.relative)(outputDir, cls.filePath);
relativePath = relativePath.replace(/\.ts$/, '');
if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`;
}
relativePath = relativePath.replace(/\\/g, '/');
imports.push(`import type { ${cls.name} } from '${relativePath}';`);
});
// Import factory functions for ReturnType (or interface types for instance factories)
if (factories) {
factories.forEach(factory => {
if (factory.instanceFactoryFor) {
// For instance factories: import the interface type, not the factory function
const interfacePath = this.findInterfaceLocationInFile(factory.filePath, factory.instanceFactoryFor, outputDir, pathsResolver);
if (interfacePath) {
imports.push(`import type { ${factory.instanceFactoryFor} } from '${interfacePath}';`);
}
}
else {
let relativePath = (0, path_1.relative)(outputDir, factory.filePath);
relativePath = relativePath.replace(/\.ts$/, '');
if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`;
}
relativePath = relativePath.replace(/\\/g, '/');
imports.push(`import { ${factory.name} } from '${relativePath}';`);
}
});
}
// Import interface types for values (we don't need to import the values themselves in .d.ts)
if (values) {
values.forEach(value => {
// Only import the interface type, not the value itself
if (value.interfaceName) {
// Find interface location by checking imports in the value file
const interfacePath = this.findInterfaceLocationForValue(value, outputDir, pathsResolver);
if (interfacePath) {
imports.push(`import type { ${value.interfaceName} } from '${interfacePath}';`);
}
}
});
}
return imports.join('\n');
}
/**
* Finds the relative import path for an interface name by scanning a given source file's imports.
* Used by instance factories to locate the interface type they implement.
*/
static findInterfaceLocationInFile(sourceFilePath, interfaceName, outputDir, pathsResolver) {
try {
const fileContent = (0, fs_1.readFileSync)(sourceFilePath, 'utf-8');
const importRegex = new RegExp(`import\\s+(?:type\\s+)?\\{[^}]*\\b${interfaceName}\\b[^}]*\\}\\s+from\\s+['"]([^'"]+)['"]`, 'g');
const match = importRegex.exec(fileContent);
if (match && match[1]) {
const importedFrom = match[1];
const absoluteFromAlias = pathsResolver?.resolveImportToAbsolute(importedFrom) ?? null;
const absolutePath = absoluteFromAlias
?? (0, path_1.join)((0, path_1.dirname)(sourceFilePath), importedFrom);
const normalizedPath = absolutePath.endsWith('.ts')
? absolutePath
: absolutePath + '.ts';
if ((0, fs_1.existsSync)(normalizedPath)) {
let relativePath = (0, path_1.relative)(outputDir, normalizedPath);
relativePath = relativePath.replace(/\.ts$/, '');
if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`;
}
return relativePath.replace(/\\/g, '/');
}
}
// Fallback: look for a file named {InterfaceName}.ts in the same directory
const sameDir = (0, path_1.dirname)(sourceFilePath);
const candidatePath = (0, path_1.join)(sameDir, interfaceName + '.ts');
if ((0, fs_1.existsSync)(candidatePath)) {
let relativePath = (0, path_1.relative)(outputDir, candidatePath);
relativePath = relativePath.replace(/\.ts$/, '');
if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`;
}
return relativePath.replace(/\\/g, '/');
}
return null;
}
catch {
return null;
}
}
static findInterfaceLocationForValue(value, outputDir, pathsResolver) {
try {
// Read the value file to find where the interface is imported from
const fileContent = (0, fs_1.readFileSync)(value.filePath, 'utf-8');
// Match: import { InterfaceName } from './path';
// or: import type { InterfaceName } from './path';
const importRegex = new RegExp(`import\\s+(?:type\\s+)?\\{[^}]*\\b${value.interfaceName}\\b[^}]*\\}\\s+from\\s+['"]([^'"]+)['"]`, 'g');
const match = importRegex.exec(fileContent);
if (match && match[1]) {
const interfacePathFromValue = match[1];
// Try to resolve as a path alias first
const absoluteFromAlias = pathsResolver?.resolveImportToAbsolute(interfacePathFromValue) ?? null;
const absoluteInterfacePath = absoluteFromAlias
?? (0, path_1.join)((0, path_1.dirname)(value.filePath), interfacePathFromValue);
const normalizedInterfacePath = absoluteInterfacePath.endsWith('.ts')
? absoluteInterfacePath
: absoluteInterfacePath + '.ts';
// Check if the file exists
if ((0, fs_1.existsSync)(normalizedInterfacePath)) {
// Convert to relative path from output directory
let relativePath = (0, path_1.relative)(outputDir, normalizedInterfacePath);
relativePath = relativePath.replace(/\.ts$/, '');
if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`;
}
return relativePath.replace(/\\/g, '/');
}
}
// Fallback: assume interface is in a file named I{InterfaceName}.ts in the same directory
const valueDir = (0, path_1.dirname)(value.filePath);
const interfaceFileName = value.interfaceName + '.ts';
const interfacePath = (0, path_1.join)(valueDir, interfaceFileName);
if ((0, fs_1.existsSync)(interfacePath)) {
let relativePath = (0, path_1.relative)(outputDir, interfacePath);
relativePath = relativePath.replace(/\.ts$/, '');
if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`;
}
return relativePath.replace(/\\/g, '/');
}
return null;
}
catch (error) {
return null;
}
}
static findInterfaceLocations(classes, outputDir, pathsResolver) {
const interfaceLocations = new Map();
const interfaceNames = new Set(classes.filter(c => c.interfaceName).map(c => c.interfaceName));
if (interfaceNames.size === 0)
return interfaceLocations;
// For each class with an interface, parse its file to find the actual interface import
classes.forEach(cls => {
if (!cls.interfaceName || interfaceLocations.has(cls.interfaceName))
return;
try {
const fileContent = (0, fs_1.readFileSync)(cls.filePath, 'utf-8');
// Match: import { InterfaceName } from './path';
// or: import type { InterfaceName } from './path';
const importRegex = new RegExp(`import\\s+(?:type\\s+)?\\{[^}]*\\b${cls.interfaceName}\\b[^}]*\\}\\s+from\\s+['"]([^'"]+)['"]`, 'g');
const match = importRegex.exec(fileContent);
if (match && match[1]) {
const interfacePathFromClass = match[1];
// Try path alias resolution first, fall back to relative resolution
const resolvedFromAlias = pathsResolver?.resolveImportToAbsolute(interfacePathFromClass) ?? null;
const interfaceAbsPath = resolvedFromAlias
?? require('path').resolve((0, path_1.dirname)(cls.filePath), interfacePathFromClass);
// Calculate relative path from output directory
let relativePath = (0, path_1.relative)(outputDir, interfaceAbsPath);
relativePath = relativePath.replace(/\.ts$/, '');
// Normalize
if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`;
}
relativePath = relativePath.replace(/\\/g, '/');
interfaceLocations.set(cls.interfaceName, relativePath);
}
}
catch (error) {
// If we can't read the file or parse it, skip
const { Logger } = require('../utils/logger');
Logger.debug(`Could not parse interface location for ${cls.interfaceName} in ${cls.filePath}`);
}
});
return interfaceLocations;
}
static generateRegistryInterface(classes) {
const registrations = classes
.filter(cls => cls.interfaceName)
.map(cls => ` '${cls.interfaceName}': ${cls.interfaceName};`)
.join('\n');
return `export interface ContainerRegistry {
${registrations}
}`;
}
/**
* Checks for class name collisions and throws an error if any are found
*/
static checkForNameCollisions(classes) {
const nameToClasses = new Map();
classes.forEach(cls => {
if (!nameToClasses.has(cls.name)) {
nameToClasses.set(cls.name, []);
}
nameToClasses.get(cls.name).push(cls);
});
// Check for collisions
for (const [className, classesWithSameName] of nameToClasses.entries()) {
if (classesWithSameName.length > 1) {
const filePaths = classesWithSameName.map(cls => cls.filePath);
throw errorFactory_1.ErrorFactory.classNameCollision(className, filePaths);
}
}
}
static generateModuleAugmentation() {
return `declare module '@notjustcoders/di-container' {
interface Container {
/**
* Type-safe resolve for registered string tokens.
* Returns the correctly typed instance based on the token.
*/
resolve<K extends keyof ContainerRegistry>(token: K): ContainerRegistry[K];
/**
* Generic resolve for class constructors or other token types.
*/
resolve<T>(token: import('@notjustcoders/di-container').Token<T>): T;
}
}`;
}
}
exports.TypeDeclarationGenerator = TypeDeclarationGenerator;
//# sourceMappingURL=type-declaration-generator.js.map