نظام القوالب
يستخدم FORGE محرك القوالب Tera لتوليد الأكواد المصدرية. كل ملف في المشروع المُولّد يأتي من قالب .tera يُعالج بمتغيرات مُشتقة من إعدادات forge.yaml للمشروع. هذه البنية تجعل مخرجات FORGE حتمية بالكامل: نفس الإعدادات تُنتج دائماً نفس الكود.
كيف تعمل القوالب
عند تشغيل forge new أو أي أمر forge make:*، يتبع FORGE هذا الخط:
- قراءة الإعدادات من
forge.yaml(اسم التطبيق، الميزات، اللغات، المزودين) - بناء كائن السياق الذي يحتوي جميع متغيرات القالب
- تحديد قوالب
.teraالمناسبة من مجلد القوالب - عرض كل قالب عبر محرك Tera، باستبدال المتغيرات وتقييم الشروط
- كتابة المخرجات إلى مجلد المشروع مع إزالة امتداد
.tera
forge.yaml ──→ متغيرات السياق ──→ محرك Tera ──→ الكود المُولّد
↑
قوالب .teraبنية مجلد القوالب
تُنظّم القوالب حسب إطار الخلفية والواجهة الأمامية، مع قوالب مشتركة للأمور العابرة:
templates/
├── backends/
│ ├── rust/ ← Rust + Axum (default backend)
│ │ ├── manifest.yaml
│ │ ├── api/
│ │ │ ├── Cargo.toml.tera
│ │ │ └── src/
│ │ │ ├── main.rs.tera
│ │ │ ├── config/
│ │ │ ├── models/
│ │ │ ├── handlers/
│ │ │ │ ├── auth.rs.tera
│ │ │ │ ├── profile.rs.tera
│ │ │ │ └── admin/
│ │ │ │ ├── users.rs.tera
│ │ │ │ ├── contents.rs.tera
│ │ │ │ └── menus.rs.tera
│ │ │ ├── middleware/
│ │ │ ├── routes/
│ │ │ ├── services/
│ │ │ ├── dto/
│ │ │ ├── providers/
│ │ │ │ ├── sms/
│ │ │ │ ├── email/
│ │ │ │ ├── storage/
│ │ │ │ └── payments/
│ │ │ ├── error/
│ │ │ └── utils/
│ │ └── migrations/
│ │ ├── 00001_create_languages_table.sql.tera
│ │ ├── 00002_create_users_table.sql.tera
│ │ └── ... (17 migration templates)
│ ├── laravel/
│ ├── fastapi/
│ └── node/
│
├── frontends/
│ ├── nextjs/ ← Next.js (default frontend)
│ │ ├── web/ ← Public web app
│ │ │ ├── app/
│ │ │ │ ├── layout.tsx.tera
│ │ │ │ ├── page.tsx.tera
│ │ │ │ ├── (auth)/
│ │ │ │ ├── (protected)/
│ │ │ │ └── [slug]/
│ │ │ ├── components/
│ │ │ │ ├── site-header.tsx.tera
│ │ │ │ ├── site-footer.tsx.tera
│ │ │ │ ├── dynamic-nav.tsx.tera
│ │ │ │ ├── language-switcher.tsx.tera
│ │ │ │ └── providers/
│ │ │ └── lib/
│ │ │ ├── api.ts.tera
│ │ │ └── types.ts.tera
│ │ └── admin/ ← Admin dashboard
│ │ ├── app/
│ │ │ ├── (dashboard)/
│ │ │ │ ├── users/
│ │ │ │ ├── contents/
│ │ │ │ ├── menus/
│ │ │ │ └── settings/
│ │ └── components/
│ ├── nuxtjs/
│ ├── angular/
│ └── vanilla/
│
├── modules/ ← Optional feature modules
│ ├── crm/
│ └── helpdesk/
│
└── shared/ ← Shared templates
├── config/
│ ├── forge.yaml.tera
│ └── env.example.tera
├── infra/
│ ├── docker/
│ └── caddy/
└── database/
└── seeders/اتفاقية تسمية الملفات
امتداد .tera يُشير إلى أن الملف قالب:
| نمط الملف | المعنى |
|---|---|
main.rs.tera | قالب مصدر Rust، يُخرج main.rs |
page.tsx.tera | قالب TypeScript React، يُخرج page.tsx |
00002_create_users_table.sql.tera | قالب ترحيل SQL |
Cargo.toml.tera | قالب إعدادات TOML |
docker-compose.yml.tera | قالب إعدادات YAML |
README.md.tera | قالب Markdown |
الملفات بدون امتداد .tera تُنسخ كما هي بدون معالجة.
متغيرات القالب
كل قالب يستقبل سياقاً قياسياً مبنياً من forge.yaml. هذه المجموعة الكاملة من المتغيرات المتاحة:
متغيرات التطبيق
app:
name: "myapp" # {{ app.name }}
display_name: "My Application" # {{ app.display_name }}
description: "A FORGE app" # {{ app.description }}
version: "1.0.0" # {{ app.version }}الخلفية والواجهة الأمامية
backend:
framework: "rust" # {{ backend.framework }}
port: 8080 # {{ backend.port }}
frontend:
framework: "nextjs" # {{ frontend.framework }}
web_port: 3000 # {{ frontend.web_port }}
admin_port: 3001 # {{ frontend.admin_port }}قاعدة البيانات
database:
driver: "postgresql" # {{ database.driver }}
host: "localhost" # {{ database.host }}
port: 5432 # {{ database.port }}
name: "myapp" # {{ database.name }}النطاقات
domains:
web: "myapp.test" # {{ domains.web }}
admin: "admin.myapp.test" # {{ domains.admin }}
api: "api.myapp.test" # {{ domains.api }}المصادقة
auth:
method: "email_password" # {{ auth.method }}
jwt_expiry: "60m" # {{ auth.jwt_expiry }}اللغات
languages:
default: "en" # {{ languages.default }}
list: # {% for lang in languages.list %}
- code: "en"
name: "English"
direction: "ltr"
- code: "ar"
name: "Arabic"
direction: "rtl"المزودون
providers:
sms: "twilio" # {{ providers.sms }}
email: "smtp" # {{ providers.email }}
storage: "local" # {{ providers.storage }}بيانات FORGE الوصفية
forge:
version: "2.0.0" # {{ forge.version }}
generated_at: "2026-01-21" # {{ forge.generated_at }}المرشحات المخصصة
يُسجّل FORGE مرشحات Tera مخصصة لتحويلات السلاسل الشائعة:
{{ app.name | snake_case }} → my_app
{{ app.name | pascal_case }} → MyApp
{{ app.name | kebab_case }} → my-app
{{ app.name | camel_case }} → myApp
{{ app.name | upper }} → MYAPPالكتل الشرطية
تستخدم القوالب تدفق التحكم في Tera لتضمين أو استبعاد الكود بناءً على الإعدادات. هكذا تُنتج مجموعة قوالب واحدة مخرجات مختلفة لمشاريع مختلفة.
شروط الميزات
// main.rs.tera
use {{ app.name | snake_case }}::config::AppConfig;
use {{ app.name | snake_case }}::routes;
{% if feature_content %}
use {{ app.name | snake_case }}::handlers::contents;
{% endif %}
{% if feature_menus %}
use {{ app.name | snake_case }}::handlers::menus;
{% endif %}طريقة المصادقة
// handlers/auth.rs.tera
{% if auth.method == "email_password" %}
pub async fn login(
Json(payload): Json<LoginRequest>,
) -> Result<Json<AuthResponse>, ApiError> {
let user = find_by_email(&payload.email).await?;
verify_password(&payload.password, &user.password)?;
let token = generate_jwt(&user)?;
Ok(Json(AuthResponse { user, token }))
}
{% elif auth.method == "mobile_otp" %}
pub async fn request_otp(
Json(payload): Json<OtpRequest>,
) -> Result<Json<MessageResponse>, ApiError> {
let code = generate_otp();
store_otp(&payload.phone, &code).await?;
sms_provider.send_otp(&payload.phone, &code).await?;
Ok(Json(MessageResponse { message: "OTP sent" }))
}
{% endif %}قوالب واعية باللغة
// layout.tsx.tera
{% if languages.list | length > 1 %}
import { LanguageSwitcher } from '@/components/language-switcher'
{% endif %}
export default function RootLayout({ children }) {
return (
<html lang="{{ languages.default }}"
{%- if languages.list | filter(attribute="direction", value="rtl") | length > 0 %}
dir={direction}
{%- endif %}>
<body>
{% if languages.list | length > 1 %}
<LanguageSwitcher />
{% endif %}
{children}
</body>
</html>
)
}اختيار المزود
// providers/sms/mod.rs.tera
{% if providers.sms == "twilio" %}
mod twilio;
pub use twilio::TwilioProvider as SmsDriver;
{% elif providers.sms == "unifonic" %}
mod unifonic;
pub use unifonic::UnifonicProvider as SmsDriver;
{% else %}
mod log_provider;
pub use log_provider::LogProvider as SmsDriver;
{% endif %}مثال قالب: شرح كامل
هذا مثال كامل يُظهر قالب ترحيل وما يُنتجه.
قالب الإدخال
-- 00002_create_users_table.sql.tera
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
{% if auth.method == "mobile_otp" %}
phone VARCHAR(50) UNIQUE NOT NULL,
{% else %}
phone VARCHAR(50) UNIQUE,
{% endif %}
password VARCHAR(255) NOT NULL,
avatar VARCHAR(500),
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
{% if auth.method == "mobile_otp" %}
CREATE INDEX idx_users_phone ON users(phone);
{% endif %}الإعدادات
# forge.yaml
auth:
method: "email_password"المخرجات المُولّدة
-- 00002_create_users_table.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(50) UNIQUE,
password VARCHAR(255) NOT NULL,
avatar VARCHAR(500),
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);لاحظ أن عمود phone اختياري (بدون NOT NULL) وفهرس الهاتف محذوف لأن طريقة المصادقة email_password، وليس mobile_otp.
التكرار في القوالب
تتكرر القوالب كثيراً على المجموعات، مثل توليد كود لكل لغة مُعدّة:
-- seeders/01_languages.sql.tera
INSERT INTO languages (code, name, direction, is_default, is_active) VALUES
{% for lang in languages.list %}
('{{ lang.code }}', '{{ lang.name }}', '{{ lang.direction }}',
{{ lang.code == languages.default }}, true)
{%- if not loop.last %},{% endif %}
{% endfor %};مع لغتين مُعدّتين، يُنتج هذا:
INSERT INTO languages (code, name, direction, is_default, is_active) VALUES
('en', 'English', 'ltr', true, true),
('ar', 'Arabic', 'rtl', false, true);تحميل القوالب
يدعم FORGE وضعين لتحميل القوالب:
مبني على الملفات (التطوير)
أثناء التطوير، تُقرأ القوالب من نظام الملفات:
let engine = TemplateEngine::new(Path::new("templates/backends/rust"))?;هذا يسمح بالتكرار السريع على القوالب بدون إعادة ترجمة الـ CLI.
مُضمّن (الإنتاج)
في بناءات الإنتاج، جميع القوالب تُترجم داخل الملف التنفيذي باستخدام rust-embed:
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "templates/"]
struct Templates;هذا يجعل forge CLI ملفاً تنفيذياً واحداً مكتفياً ذاتياً بدون تبعيات خارجية. يمكن للمستخدمين تجاوز القوالب المُضمّنة بوضع إصدارات مخصصة في ~/.forge/templates/.
نصيحة
إذا كنت تحتاج لتخصيص قالب معين لمؤسستك، انسخه إلى ~/.forge/templates/ بنفس المسار النسبي. يتحقق FORGE من هذا المجلد أولاً قبل الرجوع إلى القوالب المُضمّنة.
بيان القالب
كل خلفية وواجهة أمامية لها manifest.yaml يُخبر FORGE أي قوالب يُعالج، وأي مجلدات يُنشئ، وأي خطاطيف ما بعد التوليد يُشغّل:
# templates/backends/rust/manifest.yaml
name: rust
display_name: Rust (Axum)
version: 2.0.0
forge_compatibility:
minimum: "2.0.0"
maximum: "3.x.x"
files:
- source: "api/Cargo.toml.tera"
destination: "apps/api/Cargo.toml"
template: true
- source: "api/src/main.rs.tera"
destination: "apps/api/src/main.rs"
template: true
- source: "api/rust-toolchain.toml"
destination: "apps/api/rust-toolchain.toml"
template: false # Copied as-is
directories:
- "apps/api/src/config"
- "apps/api/src/handlers"
- "apps/api/src/models"
- "apps/api/src/middleware"
- "apps/api/src/routes"
- "apps/api/src/services"
- "apps/api/src/dto"
- "apps/api/src/error"
- "apps/api/src/utils"
hooks:
post_generate:
- "cd apps/api && cargo check"تحذير
لا تُعدّل يدوياً الملفات المُعلّمة كـ managed في البيان. قد يُستبدلها FORGE أثناء الترقيات. ضع المنطق المخصص في ملفات core (مثل الخدمات) التي لن يُعدّلها FORGE أبداً.