Files
project-scaffold-cli/project_scaffold_cli/template_engine.py

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