use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use base64::Engine as _;
use rand::RngCore;
pub fn run(target: &str, db_spec: Option<&str>) -> Result<()> {
let root = PathBuf::from(target);
if root.exists() {
anyhow::bail!("path already exists: {}", root.display());
}
let pkg_name = root
.file_name()
.and_then(|n| n.to_str())
.map(sanitize_pkg_name)
.unwrap_or_else(|| "app".to_string());
if pkg_name.is_empty() {
anyhow::bail!(
"could not derive a package name from path: {}",
root.display()
);
}
let db_plan = DbPlan::resolve(db_spec, &pkg_name)?;
fs::create_dir_all(&root)?;
create_directories(&root)?;
write_root_files(&root, &pkg_name)?;
write_vendor_shim(&root, &pkg_name)?;
write_app(&root)?;
write_bootstrap(&root)?;
write_config(&root)?;
write_database(&root)?;
write_routes(&root)?;
write_lang(&root)?;
write_resources(&root, &pkg_name)?;
write_frontend(&root)?;
write_public(&root, &pkg_name)?;
write_storage(&root)?;
write_tests(&root)?;
finalize_env(&root, &db_plan)?;
let db_status = db_plan.provision(&root);
println!();
println!(" ✓ scaffolded {} ({})", root.display(), pkg_name);
println!(" ✓ wrote .env with a freshly generated APP_KEY");
println!(" {} {}", db_status.icon(), db_status.message());
println!();
println!(" next:");
println!(" cd {} && anvil serve", root.display());
println!();
println!(" to scaffold features:");
println!(" anvil make:model Post --with-migration");
println!(" anvil make:controller PostController --resource");
println!(" anvil migrate # apply your migrations");
println!();
Ok(())
}
pub fn run_tiny(target: &str) -> Result<()> {
let root = PathBuf::from(target);
if root.exists() {
anyhow::bail!("path already exists: {}", root.display());
}
let pkg_name = root
.file_name()
.and_then(|n| n.to_str())
.map(sanitize_pkg_name)
.unwrap_or_else(|| "app".to_string());
if pkg_name.is_empty() {
anyhow::bail!(
"could not derive a package name from path: {}",
root.display()
);
}
fs::create_dir_all(&root)?;
let anvilforge_dep = internal_dep_spec("anvil")?;
write(
&root,
"Cargo.toml",
&format!(
r#"[package]
name = "{pkg_name}"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "{pkg_name}"
path = "main.rs"
[dependencies]
anvilforge = {anvilforge_dep}
tokio = {{ version = "1", features = ["full"] }}
sqlx = {{ version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] }}
anyhow = "1"
"#,
),
)?;
write(
&root,
"main.rs",
r#"//! The smallest possible Anvilforge app — one file.
//!
//! Run: `cargo run` → http://127.0.0.1:8080
//!
//! `anvil new --tiny` is the minimal opt-out from the full Laravel-style
//! scaffold. Useful for demos and benchmarks. Run `anvil new <name>` (no
//! flag) for the production-shaped layout with models, migrations,
//! controllers, the container, etc.
use anvilforge::prelude::*;
use anvilforge::container::ContainerBuilder;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Anvilforge's container needs *some* DB pool to build. For a
// tiny single-file demo, an in-memory SQLite is the path of least
// resistance — zero filesystem state, no env vars, no migrations.
// Swap for Postgres/MySQL by changing the URL.
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await?;
let container = ContainerBuilder::from_env()
.driver_pool(anvilforge::cast::Pool::Sqlite(pool))
.build();
let pool_for_app = container.driver_pool();
let app = Application::builder()
.container(move |_b| {
ContainerBuilder::from_env().driver_pool(pool_for_app.clone())
})
.web(|r: Router| {
r.get("/", |_: State<Container>| async {
"Hello from Anvilforge (tiny)\n"
})
})
.build();
let addr = "127.0.0.1:8080".parse()?;
println!("listening on http://{addr}");
app.serve(addr).await?;
Ok(())
}
"#,
)?;
println!();
println!(
" ✓ scaffolded {} ({}, tiny mode)",
root.display(),
pkg_name
);
println!();
println!(" next:");
println!(" cd {} && cargo run", root.display());
println!();
println!(" scaffolded files:");
println!(" Cargo.toml");
println!(" main.rs");
println!();
println!(
" for the full Laravel-style scaffold (models, migrations, etc.):\n anvil new {} --no-tiny (or just `anvil new <other-name>` without --tiny)",
root.display()
);
println!();
Ok(())
}
fn finalize_env(root: &Path, db_plan: &DbPlan) -> Result<()> {
let example_path = root.join(".env.example");
let target_path = root.join(".env");
if !example_path.exists() {
return Ok(());
}
let example = fs::read_to_string(&example_path).context("read .env.example")?;
let key = generate_app_key();
let mut env_contents = example.replace("APP_KEY=", &format!("APP_KEY=\"{key}\""));
if !db_plan.matches_default_sqlite() {
env_contents = replace_env_key(&env_contents, "DATABASE_URL", db_plan.url());
}
fs::write(&target_path, env_contents).context("write .env")?;
Ok(())
}
fn replace_env_key(contents: &str, key: &str, value: &str) -> String {
let mut found = false;
let lines: Vec<String> = contents
.lines()
.map(|line| {
let trimmed = line.trim_start();
if !found && trimmed.starts_with(key) && trimmed[key.len()..].starts_with('=') {
found = true;
format!("{key}={value}")
} else {
line.to_string()
}
})
.collect();
let mut s = lines.join("\n");
if contents.ends_with('\n') && !s.ends_with('\n') {
s.push('\n');
}
if !found {
if !s.ends_with('\n') {
s.push('\n');
}
s.push_str(&format!("{key}={value}\n"));
}
s
}
enum DbPlan {
Sqlite { file: PathBuf, url: String },
Postgres {
url: String,
db_name: String,
host: String,
port: u16,
user: String,
},
Mysql {
url: String,
db_name: String,
host: String,
port: u16,
user: String,
},
Custom { url: String },
}
impl DbPlan {
fn resolve(spec: Option<&str>, pkg_name: &str) -> Result<Self> {
let default_sqlite = || DbPlan::Sqlite {
file: PathBuf::from("database/anvil.db"),
url: "sqlite://database/anvil.db?mode=rwc".to_string(),
};
let Some(spec) = spec else {
return Ok(default_sqlite());
};
if spec.contains("://") {
return Ok(Self::from_url(spec, pkg_name));
}
match spec.to_ascii_lowercase().as_str() {
"sqlite" => Ok(default_sqlite()),
"postgres" | "pg" | "postgresql" => Ok(DbPlan::Postgres {
url: format!("postgres://postgres@127.0.0.1:5432/{pkg_name}"),
db_name: pkg_name.to_string(),
host: "127.0.0.1".to_string(),
port: 5432,
user: "postgres".to_string(),
}),
"mysql" | "mariadb" => Ok(DbPlan::Mysql {
url: format!("mysql://root@127.0.0.1:3306/{pkg_name}"),
db_name: pkg_name.to_string(),
host: "127.0.0.1".to_string(),
port: 3306,
user: "root".to_string(),
}),
other => anyhow::bail!(
"unknown --db value: {other}. Use `sqlite`, `postgres`, `mysql`, or a full URL"
),
}
}
fn from_url(url: &str, pkg_name: &str) -> Self {
let lower = url.to_ascii_lowercase();
if lower.starts_with("sqlite://") {
let after = &url["sqlite://".len()..];
let path_part = after.split('?').next().unwrap_or(after);
return DbPlan::Sqlite {
file: PathBuf::from(path_part),
url: url.to_string(),
};
}
if lower.starts_with("postgres://") || lower.starts_with("postgresql://") {
if let Some(parsed) = parse_simple_db_url(url, 5432, "postgres") {
if parsed.db_name == pkg_name || !parsed.db_name.is_empty() {
return DbPlan::Postgres {
url: url.to_string(),
db_name: parsed.db_name,
host: parsed.host,
port: parsed.port,
user: parsed.user,
};
}
}
}
if lower.starts_with("mysql://") || lower.starts_with("mariadb://") {
if let Some(parsed) = parse_simple_db_url(url, 3306, "root") {
if !parsed.db_name.is_empty() {
return DbPlan::Mysql {
url: url.to_string(),
db_name: parsed.db_name,
host: parsed.host,
port: parsed.port,
user: parsed.user,
};
}
}
}
DbPlan::Custom {
url: url.to_string(),
}
}
fn url(&self) -> &str {
match self {
DbPlan::Sqlite { url, .. }
| DbPlan::Postgres { url, .. }
| DbPlan::Mysql { url, .. }
| DbPlan::Custom { url } => url,
}
}
fn matches_default_sqlite(&self) -> bool {
matches!(self, DbPlan::Sqlite { url, .. } if url == "sqlite://database/anvil.db?mode=rwc")
}
fn provision(&self, project_root: &Path) -> ProvisionStatus {
match self {
DbPlan::Sqlite { file, .. } => {
let abs = project_root.join(file);
if let Some(parent) = abs.parent() {
let _ = fs::create_dir_all(parent);
}
match fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&abs)
{
Ok(_) => {
ProvisionStatus::ok(format!("created SQLite DB at {}", file.display()))
}
Err(e) => ProvisionStatus::warn(format!(
"could not touch SQLite file {}: {e}. It'll be created on first connect.",
file.display()
)),
}
}
DbPlan::Postgres {
db_name,
host,
port,
user,
..
} => {
let bin = find_client_bin("psql");
let mut cmd = std::process::Command::new(&bin);
cmd.args([
"-h",
host,
"-p",
&port.to_string(),
"-U",
user,
"-d",
"postgres",
"-v",
"ON_ERROR_STOP=1",
"-c",
&format!("CREATE DATABASE \"{db_name}\""),
]);
run_create_db("PostgreSQL", db_name, &bin, cmd)
}
DbPlan::Mysql {
db_name,
host,
port,
user,
..
} => {
let bin = find_client_bin("mysql");
let mut cmd = std::process::Command::new(&bin);
cmd.args([
"-h",
host,
"-P",
&port.to_string(),
"-u",
user,
"-e",
&format!("CREATE DATABASE `{db_name}`"),
]);
run_create_db("MySQL", db_name, &bin, cmd)
}
DbPlan::Custom { url } => ProvisionStatus::info(format!(
"DATABASE_URL set to {url} (provisioning skipped — unrecognized scheme)"
)),
}
}
}
struct ProvisionStatus {
level: StatusLevel,
message: String,
}
enum StatusLevel {
Ok,
Info,
Warn,
}
impl ProvisionStatus {
fn ok(message: String) -> Self {
Self {
level: StatusLevel::Ok,
message,
}
}
fn info(message: String) -> Self {
Self {
level: StatusLevel::Info,
message,
}
}
fn warn(message: String) -> Self {
Self {
level: StatusLevel::Warn,
message,
}
}
fn icon(&self) -> &'static str {
match self.level {
StatusLevel::Ok => "✓",
StatusLevel::Info => "•",
StatusLevel::Warn => "!",
}
}
fn message(&self) -> &str {
&self.message
}
}
struct ParsedDbUrl {
user: String,
host: String,
port: u16,
db_name: String,
}
fn parse_simple_db_url(url: &str, default_port: u16, default_user: &str) -> Option<ParsedDbUrl> {
let after_scheme = url.split_once("://").map(|(_, rest)| rest)?;
let (authority_and_path, _query) = match after_scheme.split_once('?') {
Some((a, b)) => (a, Some(b)),
None => (after_scheme, None),
};
let (authority, path) = authority_and_path
.split_once('/')
.unwrap_or((authority_and_path, ""));
let (userinfo, host_port) = match authority.rsplit_once('@') {
Some((u, h)) => (Some(u), h),
None => (None, authority),
};
let user = userinfo
.map(|u| u.split(':').next().unwrap_or("").to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| default_user.to_string());
let (host, port) = match host_port.rsplit_once(':') {
Some((h, p)) => (h.to_string(), p.parse().unwrap_or(default_port)),
None => (host_port.to_string(), default_port),
};
Some(ParsedDbUrl {
user,
host,
port,
db_name: path.to_string(),
})
}
fn find_client_bin(name: &str) -> PathBuf {
if cfg!(target_os = "macos") {
if let Some(home) = std::env::var_os("HOME") {
let p = PathBuf::from(home)
.join("Library/Application Support/Herd/bin")
.join(name);
if p.exists() {
return p;
}
}
}
PathBuf::from(name)
}
fn run_create_db(
kind: &str,
db_name: &str,
bin: &Path,
mut cmd: std::process::Command,
) -> ProvisionStatus {
let output = match cmd.output() {
Ok(o) => o,
Err(e) => {
return ProvisionStatus::warn(format!(
"couldn't run `{}` ({e}). Create the {kind} database `{db_name}` manually.",
bin.display()
));
}
};
if output.status.success() {
return ProvisionStatus::ok(format!("created {kind} database `{db_name}`"));
}
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("already exists") || stderr.contains("database exists") {
return ProvisionStatus::info(format!(
"{kind} database `{db_name}` already exists — reusing it"
));
}
ProvisionStatus::warn(format!(
"{kind} `CREATE DATABASE {db_name}` failed: {}",
stderr.trim()
))
}
fn generate_app_key() -> String {
let mut bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut bytes);
base64::engine::general_purpose::STANDARD.encode(bytes)
}
fn create_directories(root: &Path) -> Result<()> {
let dirs = [
".anvil",
"app/Console",
"app/Events",
"app/Exceptions",
"app/Http/Controllers",
"app/Http/Middleware",
"app/Http/Requests",
"app/Jobs",
"app/Listeners",
"app/Mail",
"app/Models",
"app/Notifications",
"app/Policies",
"app/Providers",
"app/Rules",
"bootstrap",
"config",
"database/factories",
"database/migrations",
"database/seeders",
"lang/en",
"public/build",
"resources/css",
"resources/js",
"resources/views/components",
"resources/views/layouts",
"resources/views/pages",
"routes",
"storage/app",
"storage/framework/cache",
"storage/framework/sessions",
"storage/framework/views",
"storage/logs",
"tests/Feature",
"tests/Unit",
];
for d in dirs {
fs::create_dir_all(root.join(d)).context("create dir")?;
}
Ok(())
}
fn write_root_files(root: &Path, name: &str) -> Result<()> {
let anvilforge_dep = internal_dep_spec("anvil")?;
let anvilforge_test_dep = internal_dep_spec("anvil-test")?;
let forge_codegen_dep = internal_dep_spec("forge-codegen")?;
write(
root,
"Cargo.toml",
&format!(
r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2021"
build = ".anvil/build.rs"
[[bin]]
name = "{name}"
path = ".anvil/main.rs"
[lib]
path = ".anvil/lib.rs"
[dependencies]
anvilforge = {anvilforge_dep}
tokio = {{ version = "1", features = ["full"] }}
serde = {{ version = "1", features = ["derive"] }}
serde_json = "1"
sqlx = {{ version = "0.8", features = ["runtime-tokio-rustls", "postgres", "sqlite", "mysql"] }}
async-trait = "0.1"
thiserror = "1"
anyhow = "1"
chrono = {{ version = "0.4", features = ["serde"] }}
uuid = {{ version = "1", features = ["v4", "serde"] }}
tracing = "0.1"
garde = {{ version = "0.20", features = ["full"] }}
rust-embed = {{ version = "8.5", optional = true }}
[features]
default = []
# Bake `public/` into the binary at compile time so the customer can ship a
# single executable + .env. Without this, the app reads from `public/` on disk.
# Compile-time embedded templates are always on (free — controlled by build.rs).
embed-assets = ["anvilforge/embed-assets", "dep:rust-embed"]
[build-dependencies]
anvilforge-templates-codegen = {forge_codegen_dep}
[dev-dependencies]
anvilforge-test = {anvilforge_test_dep}
"#,
),
)?;
write(
root,
".env.example",
r#"APP_NAME="My App"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8080
APP_ADDR=127.0.0.1:8080
# Default filter: debug for your app, warn for noisy crates. Override with
# RUST_LOG-style directives, e.g. `info,sqlx=debug` to debug DB queries.
LOG_LEVEL=debug,sqlx=warn,hyper=warn,tower_http=info
LOG_FORMAT=pretty
# SQLite by default — zero-config for development, matches `laravel new`'s UX.
# Switch to Postgres or MySQL by replacing the URL with one of:
# postgres://user:pass@localhost:5432/app
# mysql://user:pass@localhost:3306/app
DATABASE_URL=sqlite://database/anvil.db?mode=rwc
DB_POOL=10
# For multiple connections (Laravel's `config/database.php` map):
# DB_CONNECTIONS=default,replica,analytics
# DB_DEFAULT=default
# DB_REPLICA_URL=postgres://replica.local:5432/app
# DB_REPLICA_POOL=5
# DB_REPLICA_READ_URLS=postgres://r1/app,postgres://r2/app
SESSION_DRIVER=file
SESSION_LIFETIME=120
CACHE_DRIVER=moka
QUEUE_DRIVER=database
FILESYSTEM_DISK=local
MAIL_MAILER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_FROM_ADDRESS=hello@example.com
MAIL_FROM_NAME="${APP_NAME}"
REDIS_URL=redis://127.0.0.1:6379
"#,
)?;
write(
root,
".gitignore",
r#"/target
**/*.rs.bk
.env
.env.*
!.env.example
node_modules/
public/build/
!public/build/.gitkeep
storage/app/*
!storage/app/.gitkeep
storage/logs/*
!storage/logs/.gitkeep
storage/framework/cache/*
!storage/framework/cache/.gitkeep
storage/framework/sessions/*
!storage/framework/sessions/.gitkeep
storage/framework/views/*
!storage/framework/views/.gitkeep
# Local SQLite databases — don't commit dev data. Schema lives in
# database/migrations/; recreate with `anvil migrate:fresh`.
database/*.db
database/*.db-journal
database/*.db-wal
database/*.db-shm
.DS_Store
.idea/
.vscode/
"#,
)?;
write(
root,
"rust-toolchain.toml",
r#"[toolchain]
channel = "stable"
components = ["rustfmt", "clippy"]
"#,
)?;
write(
root,
".cargo/config.toml",
r#"# Cargo configuration for this Anvilforge app.
#
# The default `[profile.dev]` in Cargo.toml is already tuned (line-tables-only
# debug info, split-debuginfo, codegen-units = 256). The sections below are
# off by default — uncomment them once you've installed the matching tools.
# Run `anvil doctor` to see what's installed locally.
[alias]
# Short aliases. `cargo a serve` works from anywhere in this project.
a = "run --quiet -- "
# ────────────────────────────────────────────────────────────────────────────
# Faster linker — biggest single dev-loop win on a large project.
# ────────────────────────────────────────────────────────────────────────────
# Linux: `sudo apt install mold` then uncomment:
# [target.x86_64-unknown-linux-gnu]
# linker = "clang"
# rustflags = ["-C", "link-arg=-fuse-ld=mold"]
#
# macOS: `brew install llvm` then uncomment:
# [target.x86_64-apple-darwin]
# rustflags = ["-C", "link-arg=-fuse-ld=lld"]
# [target.aarch64-apple-darwin]
# rustflags = ["-C", "link-arg=-fuse-ld=lld"]
# ────────────────────────────────────────────────────────────────────────────
# sccache — cross-project compile cache. Installs once, applies everywhere.
# ────────────────────────────────────────────────────────────────────────────
# `cargo install sccache --locked` then:
# [build]
# rustc-wrapper = "sccache"
# ────────────────────────────────────────────────────────────────────────────
# Cranelift — 2-3× faster `rustc` for debug builds. Nightly only.
# ────────────────────────────────────────────────────────────────────────────
# `rustup component add rustc-codegen-cranelift-preview --toolchain nightly`
# then run `anvil dev --fast` (or uncomment to opt every build in):
# [profile.dev]
# codegen-backend = "cranelift"
"#,
)?;
write(
root,
"vite.config.js",
r#"import { defineConfig } from 'vite';
export default defineConfig({
build: {
manifest: true,
outDir: 'public/build',
rollupOptions: {
input: ['resources/css/app.css', 'resources/js/app.js'],
},
},
server: {
host: 'localhost',
port: 5173,
strictPort: true,
},
});
"#,
)?;
write(
root,
"package.json",
&format!(
r#"{{
"name": "{name}",
"private": true,
"type": "module",
"scripts": {{
"dev": "vite",
"build": "vite build"
}},
"devDependencies": {{
"vite": "^5.0.0"
}}
}}
"#,
),
)?;
write(
root,
"README.md",
&format!(
r#"# {name}
A web app built with [Anvilforge](https://github.com/anvilforge/anvilforge) — Laravel's developer experience, Rust's runtime.
## Quickstart
```bash
anvil serve
```
That's it — the scaffolder already wrote `.env` with a fresh `APP_KEY` and SQLite is the default DB, so the welcome page loads at <http://localhost:8080> with zero configuration.
Switch to Postgres or MySQL whenever you're ready — edit `DATABASE_URL` in `.env`.
To apply migrations (you'll need this once you start adding models):
```bash
anvil migrate
```
## Directory layout (Laravel-style)
```
app/ models, controllers, jobs, etc.
bootstrap/ application builder + service provider registration
config/ typed config modules
database/ migrations, factories, seeders
lang/ translation files
public/ public assets + Vite build output
resources/ Forge templates + frontend source
routes/ web, api, channels, console route definitions
storage/ local files, logs, framework cache
tests/ Feature/ and Unit/ test suites
.anvil/ framework shims (main.rs/lib.rs/build.rs) — never edit; hidden by dotfile convention
```
## Useful commands
```bash
anvil serve --watch # dev server with auto-reload
anvil migrate # apply pending migrations
anvil migrate:rollback # undo the last migration batch
anvil migrate:fresh --seed # drop + remigrate + seed
anvil db:seed # run database seeders
anvil make:model Post --with-migration
anvil make:controller PostController --resource
anvil make:auth # scaffold login/register/logout
anvil queue:work # start the queue worker
anvil schedule:run # run scheduled tasks (call from cron)
anvil test # run tests
```
## Shipping a single binary
To bake `public/build/` into the executable for a one-file deploy:
```bash
cargo build --release --features embed-assets
```
See the [embedded-assets deploy guide](https://github.com/anvilforge/anvilforge/blob/main/docs/src/production/deploy.md#single-binary-deploy-with-embedded-static-assets)
for the `embed_static!` macro and the bootstrap registration. The
default disk-served path keeps working without the feature; the
embedded set is consulted first when the feature is on and the prefix
matches.
"#,
),
)?;
Ok(())
}
fn write_vendor_shim(root: &Path, pkg_name: &str) -> Result<()> {
let crate_name = pkg_name.replace('-', "_");
write(
root,
".anvil/lib.rs",
r#"//! Library shim — glues Laravel-style top-level directories into the Rust
//! module tree via `#[path]` attributes. Framework-owned, don't edit.
#![allow(non_snake_case)]
#[path = "../app/mod.rs"]
pub mod app;
#[path = "../bootstrap/mod.rs"]
pub mod bootstrap;
#[path = "../config/mod.rs"]
pub mod config;
#[path = "../database/mod.rs"]
pub mod database;
#[path = "../lang/mod.rs"]
pub mod lang;
#[path = "../routes/mod.rs"]
pub mod routes;
// Pull in `inventory::submit!` blocks generated by `forge_codegen::emit_embedded_registry`
// from build.rs. Templates registered this way win over disk reads at runtime —
// the single-binary distribution path. If the file is empty (no
// `resources/views/`), this expands to nothing.
include!(concat!(env!("OUT_DIR"), "/spark_embedded_templates.rs"));
"#,
)?;
write(
root,
".anvil/build.rs",
r#"//! Build script — compiles Forge templates to Askama and emits an inventory
//! of embedded template sources for single-binary distribution.
//! Framework-owned, don't edit.
fn main() {
println!("cargo:rerun-if-changed=resources/views");
if let Err(e) = forge_codegen::compile_dir(
std::path::Path::new("resources/views"),
std::path::Path::new("target/forge"),
) {
eprintln!("cargo:warning=forge codegen: {e}");
}
// Bake template sources into the binary via `inventory::submit!`. Included
// from `.anvil/lib.rs`. The disk path stays active for dev (hot reload);
// the embedded registry wins when both are present.
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR set by cargo");
let target = std::path::PathBuf::from(out_dir).join("spark_embedded_templates.rs");
if let Err(e) = forge_codegen::emit_embedded_registry(
std::path::Path::new("resources/views"),
&target,
) {
eprintln!("cargo:warning=forge codegen (embed): {e}");
}
}
"#,
)?;
write(
root,
".anvil/main.rs",
&format!(
r#"//! Entry point — dispatches subcommands and calls `bootstrap::app::build`.
#![allow(non_snake_case)]
use std::net::SocketAddr;
use anvilforge::prelude::*;
use anvilforge::cache::CacheStore;
use anvilforge::container::ContainerBuilder;
use anyhow::Result;
use {crate_name}::{{bootstrap, routes}};
use {crate_name}::database::seeders::DatabaseSeeder;
#[tokio::main]
async fn main() -> Result<()> {{
let env_path = anvilforge::config::load_dotenv();
anvilforge::tracing_init::init();
if let Some(path) = env_path {{
tracing::info!(path = %path.display(), "loaded .env");
}}
let args: Vec<String> = std::env::args().collect();
let subcommand = args.get(1).map(String::as_str).unwrap_or("serve");
match subcommand {{
"serve" => serve().await,
"migrate" => run_migrate(&args[2..]).await,
"migrate:rollback" => run_migrate_rollback(&args[2..]).await,
"migrate:reset" => run_migrate_reset().await,
"migrate:refresh" => run_migrate_refresh(&args[2..]).await,
"migrate:fresh" => run_migrate_fresh(&args[2..]).await,
"migrate:install" => run_migrate_install().await,
"migrate:status" => run_migrate_status().await,
"db:seed" => run_seed(&args[2..]).await,
"db:wipe" => run_db_wipe().await,
"queue:work" => run_queue_worker().await,
"schedule:run" => run_schedule().await,
other => {{
eprintln!("unknown subcommand: {{other}}");
std::process::exit(2);
}}
}}
}}
async fn build_pool() -> Result<anvilforge::cast::Pool> {{
let cfg = anvilforge::config::DatabaseConfig::from_env();
let pool = anvilforge::cast::connect(cfg.default_url(), cfg.default_pool_size()).await?;
Ok(pool)
}}
async fn build_connections() -> Result<anvilforge::cast::ConnectionManager> {{
let cfg = anvilforge::config::DatabaseConfig::from_env();
use std::collections::HashMap;
let mut conns: HashMap<String, anvilforge::cast::Connection> = HashMap::new();
for (name, conn_cfg) in &cfg.connections {{
if conn_cfg.url.is_empty() {{ continue; }}
let write = anvilforge::cast::connect(&conn_cfg.url, conn_cfg.pool_size).await?;
let mut reads = Vec::new();
for ru in &conn_cfg.read_urls {{
reads.push(anvilforge::cast::connect(ru, conn_cfg.pool_size).await?);
}}
conns.insert(name.clone(), anvilforge::cast::Connection {{
name: name.clone(), write, reads,
}});
}}
Ok(anvilforge::cast::ConnectionManager::from_connections(cfg.default.clone(), conns))
}}
async fn build_container() -> Result<Container> {{
let connections = build_connections().await?;
let container = ContainerBuilder::from_env()
.connections(connections)
.cache(CacheStore::moka(1024))
.build();
Ok(container)
}}
async fn serve() -> Result<()> {{
let container = build_container().await?;
let app = bootstrap::app::build(container).await?;
let addr: SocketAddr = std::env::var("APP_ADDR")
.unwrap_or_else(|_| "127.0.0.1:8080".to_string())
.parse()?;
tracing::info!(%addr, "serving");
app.serve(addr).await?;
Ok(())
}}
fn has_flag(args: &[String], name: &str) -> bool {{
args.iter().any(|a| a == name)
}}
async fn run_migrate(args: &[String]) -> Result<()> {{
let pool = build_pool().await?;
let runner = anvilforge::cast::MigrationRunner::new(pool);
if has_flag(args, "--pretend") {{
for line in runner.pretend().await? {{ println!("{{line}}"); }}
return Ok(());
}}
let applied = if has_flag(args, "--step") {{
runner.run_up_step().await?
}} else {{
runner.run_up().await?
}};
if applied.is_empty() {{
println!("nothing to migrate");
}} else {{
for name in applied {{ println!("migrated: {{name}}"); }}
}}
if has_flag(args, "--seed") {{ run_seed(&[]).await?; }}
Ok(())
}}
async fn run_migrate_rollback(args: &[String]) -> Result<()> {{
let pool = build_pool().await?;
let runner = anvilforge::cast::MigrationRunner::new(pool);
let steps: u32 = args.iter().position(|a| a == "--steps")
.and_then(|i| args.get(i + 1))
.and_then(|s| s.parse().ok())
.unwrap_or(1);
let mut all_rolled: Vec<String> = Vec::new();
for _ in 0..steps {{
let rolled = runner.rollback().await?;
if rolled.is_empty() {{ break; }}
all_rolled.extend(rolled);
}}
if all_rolled.is_empty() {{
println!("nothing to roll back");
}} else {{
for name in all_rolled {{ println!("rolled back: {{name}}"); }}
}}
Ok(())
}}
async fn run_migrate_reset() -> Result<()> {{
let pool = build_pool().await?;
let runner = anvilforge::cast::MigrationRunner::new(pool);
let rolled = runner.reset().await?;
for name in rolled {{ println!("rolled back: {{name}}"); }}
Ok(())
}}
async fn run_migrate_refresh(args: &[String]) -> Result<()> {{
let pool = build_pool().await?;
let runner = anvilforge::cast::MigrationRunner::new(pool);
let applied = runner.refresh().await?;
for name in applied {{ println!("migrated: {{name}}"); }}
if has_flag(args, "--seed") {{ run_seed(&[]).await?; }}
Ok(())
}}
async fn run_migrate_fresh(args: &[String]) -> Result<()> {{
let pool = build_pool().await?;
let runner = anvilforge::cast::MigrationRunner::new(pool);
runner.fresh().await?;
println!("fresh migrations complete");
if has_flag(args, "--seed") {{ run_seed(&[]).await?; }}
Ok(())
}}
async fn run_migrate_install() -> Result<()> {{
let pool = build_pool().await?;
let runner = anvilforge::cast::MigrationRunner::new(pool);
runner.install().await?;
println!("migrations table ready");
Ok(())
}}
async fn run_migrate_status() -> Result<()> {{
let pool = build_pool().await?;
let runner = anvilforge::cast::MigrationRunner::new(pool);
let status = runner.status().await?;
println!("{{:<60}} {{:<8}} {{}}", "Migration", "Status", "Batch");
println!("{{:-<60}} {{:-<8}} {{:-<5}}", "", "", "");
for s in &status {{
let state = if s.applied {{ "applied" }} else {{ "pending" }};
let batch = s.batch.map(|b| b.to_string()).unwrap_or_else(|| "-".into());
println!("{{:<60}} {{:<8}} {{}}", s.name, state, batch);
}}
if status.is_empty() {{ println!("(no migrations registered)"); }}
Ok(())
}}
async fn run_db_wipe() -> Result<()> {{
let pool = build_pool().await?;
let runner = anvilforge::cast::MigrationRunner::new(pool);
runner.wipe().await?;
println!("database wiped");
Ok(())
}}
async fn run_seed(args: &[String]) -> Result<()> {{
let container = build_container().await?;
let class = args.iter().position(|a| a == "--class")
.and_then(|i| args.get(i + 1).cloned());
if let Some(class) = class {{
DatabaseSeeder::run_named(&container, &class).await?;
println!("seeded: {{class}}");
}} else {{
DatabaseSeeder::run_root(&container).await?;
println!("seeders complete");
}}
Ok(())
}}
async fn run_queue_worker() -> Result<()> {{
let container = build_container().await?;
let shutdown = anvilforge::shutdown::ShutdownHandle::new().install();
anvilforge::queue::run_worker(container, "default".into(), shutdown).await?;
Ok(())
}}
async fn run_schedule() -> Result<()> {{
let container = build_container().await?;
let schedule = routes::console::schedule();
schedule.run_due(&container).await?;
Ok(())
}}
"#,
),
)?;
Ok(())
}
fn write_app(root: &Path) -> Result<()> {
write(
root,
"app/mod.rs",
r#"//! Application code. Mirrors Laravel's `app/` directory.
pub mod Console;
pub mod Events;
pub mod Exceptions;
pub mod Http;
pub mod Jobs;
pub mod Listeners;
pub mod Mail;
pub mod Models;
pub mod Notifications;
pub mod Policies;
pub mod Providers;
pub mod Rules;
"#,
)?;
write(
root,
"app/Console/mod.rs",
r#"#[path = "Kernel.rs"]
mod kernel;
pub use kernel::Kernel;
"#,
)?;
write(
root,
"app/Console/Kernel.rs",
r#"//! App-level CLI commands. `.anvil/main.rs` handles framework subcommand
//! dispatch; extend here to register custom commands.
pub struct Kernel;
impl Kernel {
pub fn commands() -> Vec<&'static str> { Vec::new() }
}
"#,
)?;
write(
root,
"app/Events/mod.rs",
r#"//! Events your app dispatches. Plain structs that `Serialize + Deserialize`.
"#,
)?;
write(
root,
"app/Exceptions/mod.rs",
r#"#[path = "Handler.rs"]
mod handler;
pub use handler::Handler;
"#,
)?;
write(
root,
"app/Exceptions/Handler.rs",
r#"//! Custom exception handling — override how errors render per status code.
use anvilforge::prelude::*;
pub struct Handler;
impl Handler {
/// Hook called by the framework on each error. Return `Some(response)` to
/// override the default rendering, or `None` to use Anvilforge's built-in
/// `IntoResponse` impl on `Error`.
pub fn render(_error: &Error) -> Option<anvilforge::axum::response::Response> {
None
}
}
"#,
)?;
write(
root,
"app/Http/mod.rs",
r#"pub mod Controllers;
pub mod Middleware;
pub mod Requests;
"#,
)?;
write(
root,
"app/Http/Controllers/mod.rs",
r#"#[path = "HomeController.rs"]
mod home_controller;
pub use home_controller::HomeController;
"#,
)?;
write(
root,
"app/Http/Controllers/HomeController.rs",
r##"//! Home controller — example of a basic controller.
use anvilforge::prelude::*;
pub struct HomeController;
impl HomeController {
pub async fn index() -> Result<ViewResponse> {
Ok(ViewResponse::new(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Anvilforge</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 4rem auto; padding: 0 1rem; line-height: 1.6; color: #333; }
h1 { color: #c2410c; }
code { background: #f5f5f4; padding: 2px 6px; border-radius: 4px; font-size: 0.95em; }
</style>
</head>
<body>
<h1>Forged in Rust</h1>
<p>Your Anvilforge app is up. Edit <code>app/Http/Controllers/HomeController.rs</code> or <code>routes/web.rs</code> to customize.</p>
</body>
</html>"#.to_string(),
))
}
pub async fn health() -> &'static str {
"ok"
}
}
"##,
)?;
write(
root,
"app/Http/Middleware/mod.rs",
r#"//! Custom HTTP middleware. Register names in `bootstrap/app.rs` and reference
//! by name from route declarations: `.middleware(["my_mw"])`.
"#,
)?;
write(
root,
"app/Http/Requests/mod.rs",
r#"//! Form request structs — `#[derive(FormRequest)]` makes them Axum extractors
//! that parse + validate the request body and return a typed struct.
"#,
)?;
for (path, body) in [
(
"app/Jobs/mod.rs",
"//! Background jobs — `#[derive(Job)]` makes them dispatchable.\n",
),
(
"app/Listeners/mod.rs",
"//! Event listeners — register in `app/Providers/EventServiceProvider.rs`.\n",
),
(
"app/Mail/mod.rs",
"//! Mailables — types that implement `anvilforge::mail::Mailable`.\n",
),
(
"app/Notifications/mod.rs",
"//! Notifications — types that implement `anvilforge::notification::Notification`.\n",
),
(
"app/Policies/mod.rs",
"//! Authorization policies — implement `Policy<User, Subject>` per model.\n",
),
(
"app/Rules/mod.rs",
"//! Custom validation rules — composable garde validators.\n",
),
] {
write(root, path, body)?;
}
write(
root,
"app/Models/mod.rs",
r#"#[path = "User.rs"]
mod user;
pub use user::User;
"#,
)?;
write(
root,
"app/Models/User.rs",
r#"//! The default User model.
use anvilforge::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize, Model)]
#[table("users")]
pub struct User {
pub id: i64,
pub name: String,
pub email: String,
/// Argon2id PHC string — the hashed password. Named `password` for Laravel
/// parity (`$user->password` in PHP is also the hash). Form input arrives
/// as `payload.password` (plaintext) and is hashed via `auth::hash_password`
/// before insert.
#[serde(skip_serializing)]
pub password: String,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
}
"#,
)?;
write(
root,
"app/Providers/mod.rs",
r#"#[path = "AppServiceProvider.rs"]
mod app_service_provider;
#[path = "AuthServiceProvider.rs"]
mod auth_service_provider;
#[path = "RouteServiceProvider.rs"]
mod route_service_provider;
pub use app_service_provider::AppServiceProvider;
pub use auth_service_provider::AuthServiceProvider;
pub use route_service_provider::RouteServiceProvider;
"#,
)?;
write(
root,
"app/Providers/AppServiceProvider.rs",
r#"//! Application-level service provider. Register bindings in `register`,
//! perform side effects (event listeners, etc.) in `boot`.
use anvilforge::prelude::*;
pub struct AppServiceProvider;
impl AppServiceProvider {
pub fn register(_container: &Container) {
// Bind custom services here.
// e.g., container.bind(MyService::new());
}
pub fn boot(_container: &Container) {
// Side effects at app boot.
}
}
"#,
)?;
write(
root,
"app/Providers/AuthServiceProvider.rs",
r#"//! Auth-related provider. Register policies here.
use anvilforge::prelude::*;
pub struct AuthServiceProvider;
impl AuthServiceProvider {
pub fn boot(_container: &Container) {
// Policies are type-based in Anvilforge — just `use` your policy
// structs where they're invoked via `authorize::<Policy, _, _>(...)`.
}
}
"#,
)?;
write(
root,
"app/Providers/RouteServiceProvider.rs",
r#"//! Route provider — bind route-related concerns here.
pub struct RouteServiceProvider;
impl RouteServiceProvider {
pub fn boot() {
// URL generators, route model bindings, etc.
}
}
"#,
)?;
Ok(())
}
fn write_bootstrap(root: &Path) -> Result<()> {
write(
root,
"bootstrap/mod.rs",
r#"pub mod app;
pub mod embedded_assets;
pub mod providers;
"#,
)?;
write(
root,
"bootstrap/embedded_assets.rs",
r#"//! Compile-time-embedded `public/` assets for single-binary distribution.
//!
//! With `--features embed-assets`: `public/` is baked into the binary at
//! compile time and served from memory. The customer ships just the binary
//! and a `.env` — no `public/` folder on disk required.
//!
//! Without the feature: this module is empty and the framework's default
//! disk-served `ServeDir` handles `/assets/*` from `public/` on disk.
#[cfg(feature = "embed-assets")]
anvilforge::embed_static!(PublicAssets, "/assets", "public");
#[cfg(not(feature = "embed-assets"))]
pub fn register() {}
"#,
)?;
write(
root,
"bootstrap/app.rs",
r#"//! The single entry point that wires container, middleware, routes, and
//! service providers — Laravel 11's `bootstrap/app.php` equivalent.
use anvilforge::prelude::*;
use anvilforge::Application;
use crate::app::Providers::{AppServiceProvider, AuthServiceProvider, RouteServiceProvider};
use crate::routes;
pub async fn build(container: Container) -> anyhow::Result<Application> {
// When built `--features embed-assets`, point `/assets/*` at the binary's
// baked-in `public/` instead of the on-disk folder. No-op otherwise.
crate::bootstrap::embedded_assets::register();
// Register phase.
AppServiceProvider::register(&container);
// Build the application: middleware registry + routes. We use
// `driver_pool()` rather than `pool()` so the same scaffold runs against
// SQLite (the default), Postgres, or MySQL — whatever DATABASE_URL points
// at — without code changes.
let driver_pool = container.driver_pool();
let app = Application::builder()
.container(move |_b| {
anvilforge::container::ContainerBuilder::from_env()
.driver_pool(driver_pool.clone())
})
// Picks up `[static_files]`, `[tls]`, body limits, rate limits, etc.
// from `config/anvil.toml`. Missing file = framework defaults + env.
.server_config_file("config/anvil.toml")
.web(routes::web::register)
.api(routes::api::register)
.build();
// Boot phase.
AppServiceProvider::boot(&container);
AuthServiceProvider::boot(&container);
RouteServiceProvider::boot();
Ok(app)
}
"#,
)?;
write(
root,
"bootstrap/providers.rs",
r#"//! Service provider list. Add additional providers here as you create them.
"#,
)?;
Ok(())
}
fn write_config(root: &Path) -> Result<()> {
write(
root,
"config/mod.rs",
r#"//! Typed config modules — each returns a struct loaded from `.env`.
//!
//! Anvilforge's framework defaults (in `anvilforge::config`) cover the common
//! cases. Use these per-app modules to override or add custom config.
pub mod app;
pub mod auth;
pub mod cache;
pub mod database;
pub mod filesystems;
pub mod mail;
pub mod queue;
pub mod session;
"#,
)?;
write(root, "config/app.rs", "pub use anvilforge::config::AppConfig as Config;\npub fn config() -> Config { Config::from_env() }\n")?;
write(
root,
"config/auth.rs",
"//! Auth config — provider mapping, password reset table, etc.\n",
)?;
write(root, "config/cache.rs", "pub use anvilforge::config::CacheConfig as Config;\npub fn config() -> Config { Config::from_env() }\n")?;
write(root, "config/filesystems.rs", "pub use anvilforge::config::FilesystemConfig as Config;\npub fn config() -> Config { Config::from_env() }\n")?;
write(root, "config/mail.rs", "pub use anvilforge::config::MailConfig as Config;\npub fn config() -> Config { Config::from_env() }\n")?;
write(root, "config/queue.rs", "pub use anvilforge::config::QueueConfig as Config;\npub fn config() -> Config { Config::from_env() }\n")?;
write(root, "config/session.rs", "pub use anvilforge::config::SessionConfig as Config;\npub fn config() -> Config { Config::from_env() }\n")?;
write(root, "config/database.rs", DATABASE_CONFIG)?;
write(root, "config/anvil.toml", DEFAULT_ANVIL_TOML)?;
Ok(())
}
const DEFAULT_ANVIL_TOML: &str = r#"# Anvilforge server config — Laravel's nginx.conf equivalent. Loaded by
# `Application::run()` at boot; env vars (APP_ADDR, TLS_CERT, TLS_KEY) override
# values here.
bind = "127.0.0.1:8080"
server_name = []
[limits]
body_max = "10MB"
request_timeout = "30s"
[compression]
enabled = true
algorithms = ["gzip", "br"]
min_size = "1KB"
# /assets/* served from the on-disk `public/` folder, or — when built with
# `--features embed-assets` — from the binary's embedded copy. Same URL surface
# either way, so production swaps to the single-binary mode invisibly.
[static_files."/assets"]
dir = "public"
cache = "1y"
[trailing_slash]
mode = "ignore"
action = "redirect"
[cors]
enabled = false
allow_origins = ["*"]
allow_methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
allow_headers = ["Content-Type", "Authorization", "X-CSRF-TOKEN"]
"#;
const DATABASE_CONFIG: &str = r##"//! Database configuration. Mirrors Laravel's `config/database.php`.
//!
//! ## Multiple connections
//!
//! Set `DB_CONNECTIONS=default,replica,analytics` in `.env`. Each connection
//! pulls its URL/pool size from prefixed env vars:
//!
//! ```text
//! DB_CONNECTIONS=default,replica,analytics
//! DB_DEFAULT=default
//!
//! DATABASE_URL=postgres://... # the "default" connection
//! DB_POOL=10
//!
//! DB_REPLICA_URL=postgres://replica/...
//! DB_REPLICA_POOL=5
//! DB_REPLICA_READ_URLS=postgres://r1/...,postgres://r2/... # comma-separated
//!
//! DB_ANALYTICS_URL=postgres://analytics/...
//! ```
//!
//! ## Switching connections per query
//!
//! ```ignore
//! // Use the default connection (the common case):
//! let users = User::query().get(c.pool()).await?;
//!
//! // Run a query against a specific connection:
//! let replica = c.connection("replica").expect("replica connection");
//! let users: Vec<User> = sqlx::query_as("SELECT * FROM users")
//! .fetch_all(replica.reader())
//! .await?;
//! ```
pub use anvilforge::config::{ConnectionConfig, ConnectionDriver, DatabaseConfig as Config};
pub fn config() -> Config {
Config::from_env()
}
"##;
fn write_database(root: &Path) -> Result<()> {
write(
root,
"database/mod.rs",
r#"pub mod factories;
pub mod migrations;
pub mod seeders;
"#,
)?;
write(
root,
"database/factories/mod.rs",
r#"//! Model factories — define `Factory` impls per model for tests.
"#,
)?;
write(
root,
"database/migrations/mod.rs",
r#"//! Database migrations.
//!
//! Each `*.rs` file is `mod`-included here. `smith make:migration` appends the
//! line for you. Each migration file uses `#[derive(Migration)]`, which
//! registers it with `inventory` — `MigrationRunner::new(pool)` auto-discovers
//! every registered migration. No manual `all()` Vec.
#[path = "2026_01_01_000001_create_users_table.rs"]
pub mod create_users_table;
#[path = "2026_01_01_000002_create_jobs_table.rs"]
pub mod create_jobs_table;
"#,
)?;
write(
root,
"database/migrations/2026_01_01_000001_create_users_table.rs",
r#"use anvilforge::prelude::*;
use anvilforge::cast::Schema;
#[derive(Migration)]
pub struct CreateUsersTable;
impl CastMigration for CreateUsersTable {
fn name(&self) -> &'static str {
"2026_01_01_000001_create_users_table"
}
fn up(&self, s: &mut Schema) {
s.create("users", |t| {
t.id();
t.string("name").not_null();
t.string("email").not_null().unique();
t.string("password").not_null();
t.timestamps();
});
}
fn down(&self, s: &mut Schema) {
s.drop_if_exists("users");
}
}
"#,
)?;
write(
root,
"database/migrations/2026_01_01_000002_create_jobs_table.rs",
r#"use anvilforge::prelude::*;
use anvilforge::cast::Schema;
#[derive(Migration)]
pub struct CreateJobsTable;
impl CastMigration for CreateJobsTable {
fn name(&self) -> &'static str {
"2026_01_01_000002_create_jobs_table"
}
// Built with the portable schema builder so the same migration runs on
// SQLite (the default), Postgres, and MySQL without changes.
fn up(&self, s: &mut Schema) {
s.create("jobs", |t| {
t.uuid_id();
t.string("job_type").not_null();
t.json("payload").not_null();
t.integer("attempts").not_null().default("0");
t.integer("max_attempts").not_null().default("3");
t.string("queue").not_null();
t.timestamp("available_at").not_null().use_current();
});
s.create("failed_jobs", |t| {
t.uuid_id();
t.string("job_type").not_null();
t.json("payload").not_null();
t.text("error").not_null();
t.timestamp("failed_at").not_null().use_current();
});
}
fn down(&self, s: &mut Schema) {
s.drop_if_exists("failed_jobs");
s.drop_if_exists("jobs");
}
}
"#,
)?;
write(
root,
"database/seeders/mod.rs",
r#"//! Database seeders. Register each one in `DatabaseSeeder::registry()`.
#[path = "DatabaseSeeder.rs"]
mod database_seeder;
pub use database_seeder::DatabaseSeeder;
"#,
)?;
write(
root,
"database/seeders/DatabaseSeeder.rs",
r#"//! Root seeder. `smith db:seed` calls `DatabaseSeeder::run(&c)`.
//!
//! Every seeder with `#[derive(Seeder)]` is auto-registered via inventory.
//! No manual registry maintenance needed — `smith make:seeder MySeeder` is
//! enough to make it discoverable by name.
//!
//! Inside `run()`, dispatch to sub-seeders via `registry.run(c, "Name")`
//! — the Rust analog of Laravel's `$this->call([UserSeeder::class, ...])`.
use anvilforge::prelude::*;
use anvilforge::seeder::{Seeder, SeederRegistry};
use anvilforge::async_trait::async_trait;
#[derive(Seeder)]
pub struct DatabaseSeeder;
impl DatabaseSeeder {
/// Auto-discovered registry of every `#[derive(Seeder)]` struct in the workspace.
pub fn registry() -> SeederRegistry {
SeederRegistry::from_inventory()
}
pub async fn run_root(c: &Container) -> Result<()> {
let seeder = DatabaseSeeder;
seeder.run(c).await
}
pub async fn run_named(c: &Container, class: &str) -> Result<()> {
Self::registry().run(c, class).await
}
}
#[async_trait]
impl Seeder for DatabaseSeeder {
fn name(&self) -> &'static str { "DatabaseSeeder" }
async fn run(&self, c: &Container) -> Result<()> {
// A starter row so a freshly-scaffolded app has something live to
// show on `anvil migrate --seed`. Delete this once you have your
// own seeders. Guarded on SQLite (the scaffold default) so it
// no-ops gracefully if you've switched to Postgres/MySQL but
// haven't yet adapted the seeder.
//
// Laravel parity: `User::factory()->create(['name' => …])`.
if let Some(pool) = c.driver_pool().as_sqlite() {
let count: i64 = anvilforge::cast::sqlx::query_scalar(
"SELECT COUNT(*) FROM users",
)
.fetch_one(pool)
.await
.unwrap_or(0);
if count == 0 {
anvilforge::cast::sqlx::query(
"INSERT INTO users (name, email, password) VALUES (?, ?, ?)",
)
.bind("Demo User")
.bind("demo@example.com")
.bind("$argon2id$placeholder")
.execute(pool)
.await
.ok();
tracing::info!("seeded one demo user (demo@example.com)");
}
}
// Add `$this->call([...])`-style sub-seeder calls here:
// let registry = Self::registry();
// registry.run(c, "UserSeeder").await?;
// registry.run(c, "PostSeeder").await?;
Ok(())
}
}
"#,
)?;
Ok(())
}
fn write_routes(root: &Path) -> Result<()> {
write(
root,
"routes/mod.rs",
r#"pub mod api;
pub mod channels;
pub mod console;
pub mod web;
"#,
)?;
write(
root,
"routes/web.rs",
r#"//! Web routes (HTML responses, session + CSRF stack).
use anvilforge::prelude::*;
use crate::app::Http::Controllers::HomeController;
pub fn register(r: Router) -> Router {
r.get("/", HomeController::index)
.get("/health", HomeController::health)
}
"#,
)?;
write(
root,
"routes/api.rs",
r#"//! API routes (JSON responses, bearer-token auth, no CSRF).
use anvilforge::prelude::*;
pub fn register(r: Router) -> Router {
r.get("/ping", ping)
}
async fn ping() -> Json<serde_json::Value> {
Json(serde_json::json!({ "ok": true }))
}
"#,
)?;
write(
root,
"routes/channels.rs",
r#"//! Broadcasting channel definitions (WebSocket auth, presence membership).
//! Add channel authorizers here as your app grows.
"#,
)?;
write(
root,
"routes/console.rs",
r#"//! Scheduled tasks. Called via `smith schedule:run` (typically from system cron).
use anvilforge::schedule::Schedule;
pub fn schedule() -> Schedule {
Schedule::new()
// Examples:
// schedule.daily_at("02:00", Arc::new(GenerateReports));
// schedule.hourly(Arc::new(PruneOldLogs));
}
"#,
)?;
Ok(())
}
fn write_lang(root: &Path) -> Result<()> {
write(
root,
"lang/mod.rs",
r#"//! Translation strings. v0.1 ships a placeholder — real i18n in v0.2.
pub mod en;
"#,
)?;
write(
root,
"lang/en/mod.rs",
r#"//! English translations.
pub fn message(key: &str) -> &'static str {
match key {
_ => "",
}
}
"#,
)?;
Ok(())
}
fn write_resources(root: &Path, name: &str) -> Result<()> {
write(
root,
"resources/views/layouts/app.forge.html",
&format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>@yield("title", "{name}")</title>
@vite(["resources/css/app.css", "resources/js/app.js"])
@stack("head")
</head>
<body>
<header><h1>{name}</h1></header>
<main>@yield("content")</main>
@stack("scripts")
</body>
</html>
"#,
),
)?;
write(
root,
"resources/views/pages/welcome.forge.html",
r#"@extends("layouts.app")
@section("title", "Welcome")
@section("content")
<h2>Welcome</h2>
<p>This is a Forge template at <code>resources/views/pages/welcome.forge.html</code>.</p>
<x-alert type="info">Components compile down to Askama macros.</x-alert>
@endsection
"#,
)?;
write(
root,
"resources/views/components/alert.forge.html",
r#"<div class="alert alert-{{ type }}">{{ slot }}</div>
"#,
)?;
Ok(())
}
fn write_frontend(root: &Path) -> Result<()> {
write(
root,
"resources/css/app.css",
r#":root {
--color-primary: #c2410c;
--color-text: #333;
}
body {
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
color: var(--color-text);
padding: 2rem;
max-width: 64rem;
margin: 0 auto;
}
.alert {
padding: 0.75rem 1rem;
border-radius: 0.375rem;
border: 1px solid;
}
.alert-info { background: #eff6ff; border-color: #93c5fd; color: #1e40af; }
.alert-error { background: #fef2f2; border-color: #fca5a5; color: #991b1b; }
"#,
)?;
write(
root,
"resources/js/app.js",
r#"// Vite bundles this and `app.css` into `public/build/`.
console.log("anvilforge app loaded");
"#,
)?;
Ok(())
}
fn write_public(root: &Path, name: &str) -> Result<()> {
write(
root,
"public/index.html",
&format!(
r#"<!DOCTYPE html>
<html>
<head><title>{name}</title></head>
<body>Served by Anvilforge.</body>
</html>
"#,
),
)?;
write(root, "public/build/.gitkeep", "")?;
Ok(())
}
fn write_storage(root: &Path) -> Result<()> {
for k in [
"storage/app/.gitkeep",
"storage/logs/.gitkeep",
"storage/framework/cache/.gitkeep",
"storage/framework/sessions/.gitkeep",
"storage/framework/views/.gitkeep",
] {
write(root, k, "")?;
}
Ok(())
}
fn write_tests(root: &Path) -> Result<()> {
write(
root,
"tests/Feature.rs",
r#"//! Feature test binary. Each `#[test]` here runs through the full app stack.
#[path = "Feature/mod.rs"]
mod features;
"#,
)?;
write(
root,
"tests/Feature/mod.rs",
r#"//! Feature tests. Add new test files here and `pub mod`-include them.
#[test]
fn placeholder() {
assert!(true);
}
"#,
)?;
write(
root,
"tests/Unit.rs",
r#"//! Unit test binary.
#[path = "Unit/mod.rs"]
mod units;
"#,
)?;
write(
root,
"tests/Unit/mod.rs",
r#"//! Unit tests.
#[test]
fn placeholder() {
assert!(true);
}
"#,
)?;
Ok(())
}
fn sanitize_pkg_name(raw: &str) -> String {
let lower = raw.to_ascii_lowercase();
let mut out = String::new();
for c in lower.chars() {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
out.push(c);
} else {
out.push('-');
}
}
out.trim_matches('-').to_string()
}
fn write(root: &Path, rel: &str, content: &str) -> Result<()> {
let path = root.join(rel);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).ok();
}
fs::write(&path, content).with_context(|| format!("writing {}", path.display()))?;
Ok(())
}
fn internal_dep_spec(crate_dir_name: &str) -> Result<String> {
let crate_path = format!("crates/{crate_dir_name}");
if let Ok(path) = std::env::var("ANVILFORGE_PATH").or_else(|_| std::env::var("ANVIL_PATH")) {
return Ok(format!(r#"{{ path = "{path}/{crate_path}" }}"#));
}
if let Some(workspace_root) = find_anvilforge_workspace() {
return Ok(format!(
r#"{{ path = "{}" }}"#,
workspace_root.join(&crate_path).display()
));
}
let embedded = embedded_workspace_root();
if embedded.join(&crate_path).join("Cargo.toml").exists() {
return Ok(format!(
r#"{{ path = "{}" }}"#,
embedded.join(&crate_path).display()
));
}
Ok(r#"{ version = "0.1" }"#.to_string())
}
fn find_anvilforge_workspace() -> Option<PathBuf> {
let mut dir = std::env::current_dir().ok()?;
loop {
let cargo = dir.join("Cargo.toml");
if cargo.exists() {
if let Ok(content) = std::fs::read_to_string(&cargo) {
if content.contains("[workspace]") && content.contains("anvilforge") {
return Some(dir);
}
}
}
if !dir.pop() {
return None;
}
}
}
fn embedded_workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."))
}
#[cfg(test)]
mod db_plan_tests {
use super::*;
#[test]
fn default_is_sqlite_pointed_at_database_dir() {
let plan = DbPlan::resolve(None, "my_app").unwrap();
assert_eq!(plan.url(), "sqlite://database/anvil.db?mode=rwc");
assert!(plan.matches_default_sqlite());
assert!(matches!(plan, DbPlan::Sqlite { .. }));
}
#[test]
fn postgres_shorthand_resolves_to_herd_defaults() {
let plan = DbPlan::resolve(Some("postgres"), "blog").unwrap();
match plan {
DbPlan::Postgres {
url,
db_name,
host,
port,
user,
} => {
assert_eq!(url, "postgres://postgres@127.0.0.1:5432/blog");
assert_eq!(db_name, "blog");
assert_eq!(host, "127.0.0.1");
assert_eq!(port, 5432);
assert_eq!(user, "postgres");
}
_ => panic!("expected Postgres"),
}
}
#[test]
fn mysql_shorthand_resolves_to_herd_defaults() {
let plan = DbPlan::resolve(Some("mysql"), "blog").unwrap();
match plan {
DbPlan::Mysql {
url,
db_name,
host,
port,
user,
} => {
assert_eq!(url, "mysql://root@127.0.0.1:3306/blog");
assert_eq!(db_name, "blog");
assert_eq!(host, "127.0.0.1");
assert_eq!(port, 3306);
assert_eq!(user, "root");
}
_ => panic!("expected MySQL"),
}
}
#[test]
fn full_postgres_url_is_parsed_for_provisioning() {
let plan = DbPlan::resolve(
Some("postgres://alice:secret@db.local:6543/shop"),
"ignored",
)
.unwrap();
match plan {
DbPlan::Postgres {
url,
db_name,
host,
port,
user,
} => {
assert_eq!(url, "postgres://alice:secret@db.local:6543/shop");
assert_eq!(db_name, "shop");
assert_eq!(host, "db.local");
assert_eq!(port, 6543);
assert_eq!(user, "alice");
}
_ => panic!("expected Postgres"),
}
}
#[test]
fn sqlite_full_url_extracts_file_path() {
let plan = DbPlan::resolve(Some("sqlite://var/db/app.db?mode=rwc"), "app").unwrap();
match plan {
DbPlan::Sqlite { file, url } => {
assert_eq!(file, PathBuf::from("var/db/app.db"));
assert_eq!(url, "sqlite://var/db/app.db?mode=rwc");
}
_ => panic!("expected Sqlite"),
}
let plan = DbPlan::resolve(Some("sqlite://var/db/app.db?mode=rwc"), "app").unwrap();
assert!(!plan.matches_default_sqlite());
}
#[test]
fn unknown_shorthand_errors() {
assert!(DbPlan::resolve(Some("oracle"), "app").is_err());
}
#[test]
fn replace_env_key_swaps_only_first_match() {
let env = "APP_KEY=\"x\"\nDATABASE_URL=sqlite://database/anvil.db?mode=rwc\nLOG=info\n";
let out = replace_env_key(env, "DATABASE_URL", "postgres://127.0.0.1/blog");
assert_eq!(
out,
"APP_KEY=\"x\"\nDATABASE_URL=postgres://127.0.0.1/blog\nLOG=info\n"
);
}
#[test]
fn replace_env_key_appends_when_missing() {
let env = "APP_KEY=\"x\"\n";
let out = replace_env_key(env, "DATABASE_URL", "postgres://127.0.0.1/blog");
assert_eq!(
out,
"APP_KEY=\"x\"\nDATABASE_URL=postgres://127.0.0.1/blog\n"
);
}
}