ruch
Version:
Revolutionary React TypeScript CLI with hexagonal architecture & AI-powered development assistance. Create maintainable, scalable applications with domain-driven design and integrated AI tooling.
170 lines (143 loc) • 5.06 kB
text/typescript
import path from 'path';
import type { FileSystem } from './file-operations';
import { validateFileExists, readFile } from './file-operations';
import type { DomainInfo } from '../templates/context';
/**
* Pure function to capitalize first letter of a string
*/
export const capitalizeFirstLetter = (str: string): string =>
str.charAt(0).toUpperCase() + str.slice(1);
/**
* Pure function to create domain info from domain name
*/
export const createDomainInfo = (domainName: string): DomainInfo => ({
name: domainName,
capitalizedName: capitalizeFirstLetter(domainName)
});
/**
* Pure function to build domains directory path
*/
export const buildDomainsPath = (projectRoot: string = process.cwd()): string =>
path.join(projectRoot, 'src', 'domains');
/**
* Pure function to build context directory path
*/
export const buildContextPath = (projectRoot: string = process.cwd()): string =>
path.join(projectRoot, 'src', 'context');
/**
* Pure function to build service context file path
*/
export const buildServiceContextFilePath = (projectRoot: string = process.cwd()): string =>
path.join(buildContextPath(projectRoot), 'ServiceContext.tsx');
/**
* Pure function to build service context test file path
*/
export const buildServiceContextTestFilePath = (projectRoot: string = process.cwd()): string =>
path.join(buildContextPath(projectRoot), 'ServiceContext.test.tsx');
/**
* Pure function to validate if a domain has required structure
*/
export const validateDomainStructure = async (
fileSystem: FileSystem,
domainPath: string,
domainName: string
): Promise<boolean> => {
const capitalizedName = capitalizeFirstLetter(domainName);
const requiredFiles = [
path.join(domainPath, 'services', `${capitalizedName}Service.ts`),
path.join(domainPath, 'adapters', `${capitalizedName}Adapter.ts`)
];
const validationResults = await Promise.all(
requiredFiles.map(filePath => validateFileExists(fileSystem, filePath))
);
return validationResults.every(exists => exists);
};
/**
* Pure function to filter valid domains from directory entries
*/
export const filterValidDomains = async (
fileSystem: FileSystem,
domainsPath: string,
directoryEntries: string[]
): Promise<string[]> => {
const validationPromises = directoryEntries.map(async (entry) => {
const domainPath = path.join(domainsPath, entry);
const isDirectory = await isDirectoryEntry(fileSystem, domainPath);
if (!isDirectory) return null;
const hasValidStructure = await validateDomainStructure(fileSystem, domainPath, entry);
return hasValidStructure ? entry : null;
});
const validationResults = await Promise.all(validationPromises);
return validationResults.filter((entry): entry is string => entry !== null);
};
/**
* Pure function to check if path is a directory
*/
export const isDirectoryEntry = async (
fileSystem: FileSystem,
entryPath: string
): Promise<boolean> => {
try {
const stats = await fileSystem.stat(entryPath);
return stats.isDirectory();
} catch {
return false;
}
};
/**
* Get all valid domains from the domains directory
*/
export const getExistingDomains = async (
fileSystem: FileSystem,
projectRoot: string = process.cwd()
): Promise<DomainInfo[]> => {
const domainsPath = buildDomainsPath(projectRoot);
const domainsExist = await validateFileExists(fileSystem, domainsPath);
if (!domainsExist) {
return [];
}
try {
const entries = await fileSystem.readdir(domainsPath);
const validDomainNames = await filterValidDomains(fileSystem, domainsPath, entries);
return validDomainNames.map(createDomainInfo);
} catch {
return [];
}
};
/**
* Parse existing service context to extract current domains
*/
export const parseExistingServiceContext = async (
fileSystem: FileSystem,
projectRoot: string = process.cwd()
): Promise<DomainInfo[]> => {
const contextFilePath = buildServiceContextFilePath(projectRoot);
const contextExists = await validateFileExists(fileSystem, contextFilePath);
if (!contextExists) {
return [];
}
try {
const content = await readFile(fileSystem, contextFilePath, 'utf8');
const serviceMatches = content.match(/(\w+)Service:\s*\w+Service/g) || [];
const domainNames = serviceMatches
.map((match: string) => match.replace(/Service:\s*\w+Service/, ''))
.filter((name: string) => name.length > 0);
return domainNames.map(createDomainInfo);
} catch {
return [];
}
};
/**
* Find domains that are not yet in the service context
*/
export const findMissingDomains = async (
fileSystem: FileSystem,
projectRoot: string = process.cwd()
): Promise<DomainInfo[]> => {
const [existingDomains, contextDomains] = await Promise.all([
getExistingDomains(fileSystem, projectRoot),
parseExistingServiceContext(fileSystem, projectRoot)
]);
const contextDomainNames = new Set(contextDomains.map(d => d.name));
return existingDomains.filter(domain => !contextDomainNames.has(domain.name));
};