UNPKG

@notjustcoders/ioc-arise

Version:

Arise type-safe IoC containers from your code. Zero overhead, zero coupling.

330 lines (327 loc) 16.8 kB
"use strict"; 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