template-engine-copier skill: initial import

Copier + Jinja template-repository design skill: reference architecture,
implementation workflow, validation checklist, and a full worked example
template (questionnaire, extensions, resolvers, wrapper-first .scripts).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 22:30:29 +03:00
co-authored by Claude Opus 5
commit 97991021ff
29 changed files with 932 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
---
_template_suffix: ".jinja"
_exclude:
- ".git"
- ".copier"
- ".scripts"
- ".venv"
- "Taskfile.yml"
- "pyproject.toml"
- "copier-answers-new.yml"
_jinja_extensions:
- copier_templates_extensions.TemplateExtensionLoader
- .copier/extensions/context_hook_extension/hook.py:ContextHookExtension
- .copier/extensions/resolvers/resolve_safe_identifier.py:SafeIdentifierExtension
_answers_file: copier-answers.yml
!include .copier/questionnaire/*.yml
@@ -0,0 +1,72 @@
import os
import shutil
from copier_templates_extensions import ContextHook
class ContextHookExtension(ContextHook):
update = False
def hook(self, context):
_ensure_flat_variables(context)
_ensure_resources_instances(context)
return context
def _ensure_flat_variables(context):
if 'flat' not in context or not isinstance(context.get('flat'), dict):
context['flat'] = {}
metadata = context.get('template_metadata', {}) if isinstance(context, dict) else {}
features = context.get('features', {}) if isinstance(context, dict) else {}
modules = features.get('modules', {}) if isinstance(features, dict) else {}
domain_name = metadata.get('domain', '') if isinstance(metadata, dict) else ''
project_name = metadata.get('project', '') if isinstance(metadata, dict) else ''
context['flat']['domain_name'] = domain_name
context['flat']['project_name'] = project_name
instances = modules.get('instances', []) if isinstance(modules, dict) else []
is_enabled = bool(modules.get('is_enabled', False)) if isinstance(modules, dict) else False
names = []
for instance in instances if isinstance(instances, list) else []:
if isinstance(instance, dict) and instance.get('name'):
names.append(f"{domain_name}-{project_name}-{instance['name']}")
context['flat']['modules_is_enabled'] = is_enabled
context['flat']['module_instance_names'] = names
def _ensure_resources_instances(context):
"""Expand folders containing [INDEX] into concrete per-instance directories."""
base_path = './src'
if not os.path.exists(base_path):
return
targets = []
for root, dirs, _ in os.walk(base_path):
for d in dirs:
if '[INDEX]' in d:
targets.append(os.path.join(root, d))
for path in targets:
parent = os.path.dirname(path)
instances = context.get('flat', {}).get('module_instance_names', [])
for idx, name in enumerate(instances):
dst = os.path.join(parent, name)
if os.path.exists(dst):
continue
os.makedirs(dst, exist_ok=True)
for fn in os.listdir(path):
src_file = os.path.join(path, fn)
if not os.path.isfile(src_file):
continue
with open(src_file, 'r', encoding='utf-8') as f:
content = f.read().replace('INDEX', str(idx))
with open(os.path.join(dst, fn), 'w', encoding='utf-8') as f:
f.write(content)
if os.path.exists(path):
shutil.rmtree(path)
@@ -0,0 +1,17 @@
from jinja2.ext import Extension
def resolve_safe_identifier(name: str) -> str:
return (
name
.replace("-", "_")
.replace("{", "")
.replace("}", "")
.replace(".", "_")
)
class SafeIdentifierExtension(Extension):
def __init__(self, environment):
super().__init__(environment)
environment.globals["resolve_safe_identifier"] = resolve_safe_identifier
@@ -0,0 +1 @@
from .loggers import info, warning, debug
@@ -0,0 +1,16 @@
import logging
logging.basicConfig(level=logging.INFO)
def info(name, message, extra=None):
logging.getLogger(name).info(message)
def warning(name, message, extra=None):
logging.getLogger(name).warning(message)
def debug(name, message, extra=None):
logging.getLogger(name).debug(message)
@@ -0,0 +1,18 @@
template_metadata:
type: yaml
help: "Generic template metadata used by naming and rendering hooks"
default: |
domain: demo
project: sample-product
features:
type: yaml
help: "Feature/module matrix for template rendering"
default: |
modules:
is_enabled: true
instances:
- name: users
version: 1.0.0
- name: billing
version: 1.0.0