201 lines
6.9 KiB
Python
201 lines
6.9 KiB
Python
"""Template engine using Jinja2 for project scaffolding."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Optional
|
|
|
|
from jinja2 import (
|
|
BaseLoader,
|
|
Environment,
|
|
FileSystemLoader,
|
|
TemplateSyntaxError,
|
|
)
|
|
|
|
from .config import Config
|
|
|
|
|
|
class TemplateEngine:
|
|
"""Jinja2 template rendering engine for project scaffolding."""
|
|
|
|
SUPPORTED_LANGUAGES = ["python", "nodejs", "go", "rust"]
|
|
|
|
def __init__(self, template_path: Optional[str] = None, config: Optional[Config] = None):
|
|
self.template_path = template_path
|
|
self.config = config or Config()
|
|
self._env: Optional[Environment] = None
|
|
|
|
def _get_environment(self) -> Environment:
|
|
"""Get or create the Jinja2 environment."""
|
|
if self._env is not None:
|
|
return self._env
|
|
|
|
if self.template_path and not os.path.isabs(self.template_path):
|
|
search_dirs = self.config.get_template_dirs()
|
|
search_dirs.insert(0, str(Path.cwd()))
|
|
self._env = Environment(
|
|
loader=FileSystemLoader(search_dirs),
|
|
trim_blocks=True,
|
|
lstrip_blocks=True,
|
|
)
|
|
elif self.template_path:
|
|
template_dir = Path(self.template_path).parent
|
|
self._env = Environment(
|
|
loader=FileSystemLoader([str(template_dir)]),
|
|
trim_blocks=True,
|
|
lstrip_blocks=True,
|
|
)
|
|
else:
|
|
project_root = self._get_project_root()
|
|
self._env = Environment(
|
|
loader=FileSystemLoader(str(project_root)),
|
|
trim_blocks=True,
|
|
lstrip_blocks=True,
|
|
)
|
|
|
|
return self._env
|
|
|
|
def _get_project_root(self) -> Path:
|
|
"""Get the project root directory for built-in templates."""
|
|
return Path(__file__).parent / "templates"
|
|
|
|
def _validate_template(self, template_name: str) -> bool:
|
|
"""Validate that a template exists."""
|
|
env = self._get_environment()
|
|
try:
|
|
env.get_template(template_name)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
def _render_template(
|
|
self, template_name: str, context: Dict[str, Any]
|
|
) -> str:
|
|
"""Render a single template with context."""
|
|
env = self._get_environment()
|
|
template = env.get_template(template_name)
|
|
return template.render(**context)
|
|
|
|
def render_language_template(
|
|
self, language: str, context: Dict[str, Any], output_dir: Path
|
|
) -> None:
|
|
"""Render all templates for a language."""
|
|
if language not in self.SUPPORTED_LANGUAGES:
|
|
raise ValueError(
|
|
f"Unsupported language: {language}. "
|
|
f"Supported: {', '.join(self.SUPPORTED_LANGUAGES)}"
|
|
)
|
|
|
|
env = self._get_environment()
|
|
language_dir = language
|
|
|
|
try:
|
|
project_root = self._get_project_root() / language_dir
|
|
for root, dirs, files in os.walk(project_root):
|
|
rel_path = Path(root).relative_to(project_root)
|
|
|
|
rel_path_str = rel_path.as_posix()
|
|
rel_path_str = rel_path_str.replace("{{project_slug}}", context.get("project_slug", ""))
|
|
rel_path_str = rel_path_str.replace("{{ project_name }}", context.get("project_name", ""))
|
|
|
|
dest_dir = output_dir / rel_path_str
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
for file in files:
|
|
if file.endswith((".j2", ".jinja2")):
|
|
template_path = (
|
|
Path(root) / file
|
|
).relative_to(project_root).as_posix()
|
|
dest_file = Path(dest_dir) / file[:-3]
|
|
|
|
try:
|
|
content = self._render_template(
|
|
language_dir + "/" + template_path, context
|
|
)
|
|
dest_file.write_text(content)
|
|
except TemplateSyntaxError as e:
|
|
raise ValueError(
|
|
f"Template syntax error in {template_path}: {e}"
|
|
)
|
|
else:
|
|
src_file = Path(root) / file
|
|
dest_file = Path(dest_dir) / file
|
|
dest_file.write_bytes(src_file.read_bytes())
|
|
|
|
except Exception as e:
|
|
raise RuntimeError(f"Error rendering language template: {e}")
|
|
|
|
def render_project(
|
|
self,
|
|
context: Dict[str, Any],
|
|
output_dir: Path,
|
|
template: str,
|
|
) -> None:
|
|
"""Render a custom project template."""
|
|
if not self._validate_template(template):
|
|
raise ValueError(f"Template not found: {template}")
|
|
|
|
try:
|
|
for root, dirs, files in os.walk(
|
|
self._get_environment().loader.searchpath[0]
|
|
):
|
|
for pattern in self._get_environment().loader.get_files(
|
|
template
|
|
):
|
|
pass
|
|
|
|
env = self._get_environment()
|
|
base_template = env.get_template(template)
|
|
|
|
for item in base_template.list_templates():
|
|
pass
|
|
|
|
except Exception as e:
|
|
raise ValueError(f"Error rendering project template: {e}")
|
|
|
|
def render_ci_template(
|
|
self,
|
|
ci_provider: str,
|
|
context: Dict[str, Any],
|
|
output_dir: Path,
|
|
) -> None:
|
|
"""Render CI/CD template for a provider."""
|
|
ci_templates = {
|
|
"github": "ci/.github/workflows/ci.yml.j2",
|
|
"gitlab": "ci/.gitlab-ci.yml.j2",
|
|
}
|
|
|
|
if ci_provider not in ci_templates:
|
|
raise ValueError(
|
|
f"Unsupported CI provider: {ci_provider}. "
|
|
f"Supported: {', '.join(ci_templates.keys())}"
|
|
)
|
|
|
|
env = self._get_environment()
|
|
template_name = ci_templates[ci_provider]
|
|
|
|
try:
|
|
content = self._render_template(template_name, context)
|
|
|
|
if ci_provider == "github":
|
|
workflow_dir = output_dir / ".github" / "workflows"
|
|
workflow_dir.mkdir(parents=True, exist_ok=True)
|
|
output_file = workflow_dir / "ci.yml"
|
|
else:
|
|
output_file = output_dir / ".gitlab-ci.yml"
|
|
|
|
output_file.write_text(content)
|
|
|
|
except Exception as e:
|
|
raise RuntimeError(f"Error rendering CI template: {e}")
|
|
|
|
def get_template_list(self) -> list[str]:
|
|
"""Get list of available templates."""
|
|
env = self._get_environment()
|
|
return env.list_templates()
|
|
|
|
def validate_context(self, context: Dict[str, Any]) -> list[str]:
|
|
"""Validate that all required context variables are provided."""
|
|
required = ["project_name", "author"]
|
|
missing = [var for var in required if var not in context or not context[var]]
|
|
return missing
|