woaru
Version:
Universal Project Setup Autopilot - Analyze and automatically configure development tools for ANY programming language
404 lines • 15.1 kB
JavaScript
/**
* Template Registry - Manages available project templates
*/
import fs from 'fs-extra';
import * as path from 'path';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
// ES module compatibility
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export class TemplateRegistry {
templatesPath;
templates = new Map();
categories = new Map();
constructor(templatesPath = path.join(__dirname, '../templates')) {
this.templatesPath = templatesPath;
// Always load builtin templates first
this.loadBuiltinTemplates();
// Then try to load additional templates from filesystem
this.loadTemplates();
}
/**
* Register a new template
*/
register(template) {
const validation = this.validate(template);
if (!validation.valid) {
throw new Error(`Invalid template "${template.id}": ${validation.errors.map(e => e.message).join(', ')}`);
}
this.templates.set(template.id, template);
// Update category index
const category = template.category;
if (!this.categories.has(category)) {
this.categories.set(category, []);
}
const categoryTemplates = this.categories.get(category);
if (!categoryTemplates) {
throw new Error(`Category ${category} not found in registry`);
}
const existingIndex = categoryTemplates.findIndex(t => t.id === template.id);
if (existingIndex >= 0) {
categoryTemplates[existingIndex] = template;
}
else {
categoryTemplates.push(template);
}
}
/**
* Get template by ID
*/
get(id) {
return this.templates.get(id);
}
/**
* Get templates by category
*/
getByCategory(category) {
return this.categories.get(category) || [];
}
/**
* List all templates
*/
list() {
return Array.from(this.templates.values());
}
/**
* Validate template structure
*/
validate(template) {
const errors = [];
const warnings = [];
// Required fields
if (!template.id) {
errors.push({ code: 'MISSING_ID', message: 'Template ID is required' });
}
if (!template.name) {
errors.push({
code: 'MISSING_NAME',
message: 'Template name is required',
});
}
if (!template.description) {
errors.push({
code: 'MISSING_DESCRIPTION',
message: 'Template description is required',
});
}
if (!template.language) {
errors.push({
code: 'MISSING_LANGUAGE',
message: 'Template language is required',
});
}
// Validate category
const validCategories = [
'frontend',
'backend',
'fullstack',
'mobile',
'desktop',
];
if (!validCategories.includes(template.category)) {
errors.push({
code: 'INVALID_CATEGORY',
message: `Invalid category "${template.category}". Must be one of: ${validCategories.join(', ')}`,
});
}
// Validate package manager
const validPackageManagers = [
'npm',
'yarn',
'pnpm',
'pip',
'poetry',
'pipenv',
];
if (!validPackageManagers.includes(template.packageManager)) {
errors.push({
code: 'INVALID_PACKAGE_MANAGER',
message: `Invalid package manager "${template.packageManager}". Must be one of: ${validPackageManagers.join(', ')}`,
});
}
// Validate structure
if (!template.structure ||
!template.structure.directories ||
!template.structure.files) {
errors.push({
code: 'INVALID_STRUCTURE',
message: 'Template must have structure with directories and files',
});
}
// Validate features
if (template.features) {
template.features.forEach((feature, index) => {
if (!feature.id || !feature.name || !feature.description) {
errors.push({
code: 'INVALID_FEATURE',
message: `Feature at index ${index} is missing required fields (id, name, description)`,
});
}
// Check for circular dependencies
if (feature.dependencies && feature.dependencies.includes(feature.id)) {
errors.push({
code: 'CIRCULAR_DEPENDENCY',
message: `Feature "${feature.id}" cannot depend on itself`,
});
}
// Check for conflicts with dependencies
if (feature.dependencies && feature.conflicts) {
const conflictingDeps = feature.dependencies.filter(dep => feature.conflicts?.includes(dep));
if (conflictingDeps.length > 0) {
errors.push({
code: 'CONFLICTING_DEPENDENCIES',
message: `Feature "${feature.id}" has conflicting dependencies: ${conflictingDeps.join(', ')}`,
});
}
}
});
}
// Warnings for best practices
if (!template.dependencies.runtime ||
template.dependencies.runtime.length === 0) {
warnings.push({
code: 'NO_RUNTIME_DEPS',
message: 'Template has no runtime dependencies - this might be intentional',
});
}
if (!template.features || template.features.length === 0) {
warnings.push({
code: 'NO_FEATURES',
message: 'Template has no optional features - consider adding some for flexibility',
});
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Load templates from filesystem
*/
async loadTemplates() {
try {
if (!(await fs.pathExists(this.templatesPath))) {
console.warn(chalk.yellow(`⚠️ Templates directory not found: ${this.templatesPath}`));
this.loadBuiltinTemplates();
return;
}
const templateDirs = await fs.readdir(this.templatesPath);
for (const templateDir of templateDirs) {
const templatePath = path.join(this.templatesPath, templateDir);
const templateFilePath = path.join(templatePath, 'template.json');
if (await fs.pathExists(templateFilePath)) {
try {
const templateData = await fs.readJson(templateFilePath);
this.register(templateData);
}
catch (error) {
console.warn(chalk.yellow(`⚠️ Failed to load template from ${templatePath}:`, error));
}
}
}
// Additional templates loaded successfully
}
catch {
// Filesystem loading failed, but builtin templates are already loaded
console.warn(chalk.yellow('⚠️ Failed to load additional templates from filesystem, using builtin templates only'));
}
}
/**
* Load builtin templates as fallback
*/
loadBuiltinTemplates() {
// React/Next.js Template
const nextjsTemplate = {
id: 'nextjs',
name: 'Next.js Application',
description: 'Modern React application with Next.js, TypeScript, and Tailwind CSS',
category: 'frontend',
language: 'TypeScript',
frameworks: ['React', 'Next.js'],
packageManager: 'npm',
structure: {
directories: [
{ path: 'src' },
{ path: 'src/pages' },
{ path: 'src/components' },
{ path: 'src/styles' },
{ path: 'src/utils' },
{ path: 'public' },
{ path: 'tests', conditional: { feature: 'testing' } },
],
files: [
{ source: 'base/.gitignore', destination: '.gitignore' },
{
source: 'base/README.md.hbs',
destination: 'README.md',
template: true,
},
],
templates: [
{
source: 'nextjs/package.json.hbs',
destination: 'package.json',
variables: {},
},
{
source: 'nextjs/tsconfig.json',
destination: 'tsconfig.json',
variables: {},
},
{
source: 'nextjs/next.config.js.hbs',
destination: 'next.config.js',
variables: {},
},
],
},
dependencies: {
runtime: ['next', 'react', 'react-dom'],
development: [
'typescript',
'@types/react',
'@types/node',
'eslint',
'eslint-config-next',
],
},
configuration: {
'package.json': { template: 'nextjs/package.json.hbs' },
'tsconfig.json': { template: 'nextjs/tsconfig.json' },
'.eslintrc.json': { template: 'nextjs/.eslintrc.json' },
},
features: [
{
id: 'tailwind',
name: 'Tailwind CSS',
description: 'Utility-first CSS framework',
category: 'styling',
default: true,
additionalDeps: {
runtime: [],
development: ['tailwindcss', 'postcss', 'autoprefixer'],
},
configurations: [
{
file: 'tailwind.config.js',
operation: 'replace',
content: { template: 'nextjs/tailwind.config.js.hbs' },
},
],
},
{
id: 'testing',
name: 'Testing Setup',
description: 'Jest and React Testing Library configuration',
category: 'testing',
default: false,
additionalDeps: {
runtime: [],
development: [
'jest',
'@testing-library/react',
'@testing-library/jest-dom',
],
},
},
],
};
// Python FastAPI Template
const fastapiTemplate = {
id: 'python-fastapi',
name: 'Python FastAPI',
description: 'Modern Python API with FastAPI, SQLAlchemy, and async support',
category: 'backend',
language: 'Python',
frameworks: ['FastAPI'],
packageManager: 'pip',
structure: {
directories: [
{ path: 'src' },
{ path: 'src/api' },
{ path: 'src/models' },
{ path: 'src/services' },
{ path: 'src/utils' },
{ path: 'tests' },
{ path: 'docs' },
{ path: 'scripts' },
],
files: [
{ source: 'base/.gitignore', destination: '.gitignore' },
{ source: 'python/.gitignore', destination: '.gitignore' },
{
source: 'base/README.md.hbs',
destination: 'README.md',
template: true,
},
],
templates: [
{
source: 'python-fastapi/main.py.hbs',
destination: 'src/main.py',
variables: {},
},
{
source: 'python-fastapi/requirements.txt.hbs',
destination: 'requirements.txt',
variables: {},
},
{
source: 'python-fastapi/pyproject.toml.hbs',
destination: 'pyproject.toml',
variables: {},
},
],
},
dependencies: {
runtime: ['fastapi', 'uvicorn[standard]', 'pydantic', 'sqlalchemy'],
development: ['pytest', 'black', 'ruff', 'mypy'],
},
configuration: {
'pyproject.toml': { template: 'python-fastapi/pyproject.toml.hbs' },
'requirements.txt': { template: 'python-fastapi/requirements.txt.hbs' },
'.pre-commit-config.yaml': {
template: 'python-fastapi/.pre-commit-config.yaml',
},
},
features: [
{
id: 'database',
name: 'Database Support',
description: 'SQLAlchemy ORM with async support',
category: 'database',
default: true,
additionalDeps: {
runtime: ['asyncpg', 'alembic'],
development: [],
},
},
{
id: 'auth',
name: 'Authentication',
description: 'JWT-based authentication system',
category: 'security',
default: false,
dependencies: ['database'],
additionalDeps: {
runtime: [
'python-jose[cryptography]',
'passlib[bcrypt]',
'python-multipart',
],
development: [],
},
},
],
};
this.register(nextjsTemplate);
this.register(fastapiTemplate);
}
}
//# sourceMappingURL=TemplateRegistry.js.map