Rustelo/scripts/generator/new-site.nu
Jesús Pérez c0281e759c
chore: update
2026-07-18 20:14:32 +01:00

190 lines
9 KiB
Text

#!/usr/bin/env nu
# new-site.nu — scaffold a new Rustelo website from a self-contained template.
#
# Interactive: nu new-site.nu
# Non-interactive (InitConfig-style, like `cargo rustelo init --config`):
# nu new-site.nu --project acme --render-mode htmx-ssr --domain acme.dev \
# --site-title "Acme" --languages "en,es" --default-lang en --ontoref minimal --yes
#
# The site is created as a sibling of the rustelo repo (constellation or flat).
# All {{RUSTELO_ROOT}} placeholders in template files are replaced with the
# correct relative path from each file's execution context to the framework root.
def fw-root [] { $env.FILE_PWD | path join ".." ".." | path expand }
def ask [prompt: string, default: string] {
let ans = (input $"($prompt) [($default)]: ")
if ($ans | str trim | is-empty) { $default } else { ($ans | str trim) }
}
def snake [s: string] { $s | str replace --all "-" "_" }
# Compute the relative path from directory `from` to directory `to`.
# Both must be absolute expanded paths.
def rel-path [from: string, to: string] {
let from_parts = ($from | path split | where { |p| ($p | str length) > 0 })
let to_parts = ($to | path split | where { |p| ($p | str length) > 0 })
let min_len = ([$from_parts $to_parts] | each { length } | math min)
mut common = 0
for i in 0..<$min_len {
if ($from_parts | get $i) == ($to_parts | get $i) {
$common = ($common + 1)
} else {
break
}
}
let ups = ($from_parts | length) - $common
let down = ($to_parts | skip $common)
let parts = ((0..<$ups | each { ".." }) ++ $down)
if ($parts | is-empty) { "." } else { $parts | str join "/" }
}
# Replace {{RUSTELO_ROOT}} in every text file under `root` with the correct
# relative path from that file's execution-context directory to `fw`.
#
# Files under `workspace_root_dirs` (like justfiles/) run in the workspace
# root cwd context even though they sit in a subdirectory, so they receive the
# workspace-root-relative path rather than the file-directory-relative path.
def apply-rustelo-root [root: string, fw: string] {
let bin = [png jpg jpeg gif ico svg webp avif woff woff2 ttf otf eot wasm
db "db-shm" "db-wal" pdf zip tgz gz xz lock map]
let workspace_root_dirs = ["justfiles"]
let files = (glob ($root | path join "**" "*") --no-dir
| where { |f| ($f | path parse | get extension) not-in $bin }
| where { |f| not ($f | str contains "node_modules") })
for f in $files {
try {
let c = (open --raw $f | decode utf-8)
if not ($c | str contains "{{RUSTELO_ROOT}}") { continue }
let file_dir = ($f | path dirname)
let rel_parts = ($file_dir | path relative-to $root | path split
| where { |p| ($p | str length) > 0 })
let effective_dir = if (($rel_parts | length) > 0
and ($rel_parts | first) in $workspace_root_dirs) {
$root
} else {
$file_dir
}
let rustelo_rel = (rel-path $effective_dir $fw)
($c | str replace --all "{{RUSTELO_ROOT}}" $rustelo_rel) | save -f $f
} catch { }
}
}
def main [
--project: string = ""
--render-mode: string = ""
--site-title: string = ""
--domain: string = ""
--languages: string = ""
--default-lang: string = ""
--ontoref: string = ""
--out: string = "" # parent dir for the new site (default: sibling of fw repo)
--yes # non-interactive: take provided flags / defaults
] {
let fw = (fw-root)
let opts = (^nickel export ($fw | path join "templates" "options.ncl") | from json)
let defs = ($opts.questions | reduce --fold {} { |q, acc| $acc | insert $q.id $q.default })
# resolve each answer: flag > prompt (interactive) > default
let interactive = (not $yes)
let project = (if ($project | is-not-empty) { $project } else if $interactive { ask "Project name (kebab-case)" $defs.project } else { $defs.project })
let render_mode = (if ($render_mode | is-not-empty) { $render_mode } else if $interactive { ask "Render mode (htmx-ssr|leptos-hydration)" $defs.render_mode } else { $defs.render_mode })
let site_title = (if ($site_title | is-not-empty) { $site_title } else if $interactive { ask "Site title" $defs.site_title } else { $defs.site_title })
let domain = (if ($domain | is-not-empty) { $domain } else if $interactive { ask "Domain (no scheme)" $defs.domain } else { $defs.domain })
let languages = (if ($languages | is-not-empty) { $languages } else if $interactive { ask "Languages (csv)" $defs.languages } else { $defs.languages })
let default_lang = (if ($default_lang | is-not-empty) { $default_lang } else if $interactive { ask "Default language" $defs.default_lang } else { $defs.default_lang })
let ontoref = (if ($ontoref | is-not-empty) { $ontoref } else if $interactive { ask "ontoref onboarding (full|minimal|none)" $defs.ontoref } else { $defs.ontoref })
if ($render_mode not-in ($opts.template_for | columns)) {
error make { msg: $"unknown render_mode ($render_mode); expected one of ($opts.template_for | columns)" }
}
let tmpl_name = ($opts.template_for | get $render_mode)
let tmpl = ($fw | path join "templates" $tmpl_name)
if not ($tmpl | path exists) { error make { msg: $"template not found: ($tmpl)" } }
# Determine parent directory for the new site.
# In constellation layout (fw basename == "code"), the fw repo root is one
# level up, and the site should be a sibling of that repo root.
let parent = if ($out | is-not-empty) {
($out | path expand)
} else if (($fw | path basename) == "code") {
$fw | path dirname | path dirname
} else {
$fw | path dirname
}
let dest = ($parent | path join $project)
if ($dest | path exists) { error make { msg: $"destination exists: ($dest)" } }
print $"[new-site] ($project) mode=($render_mode) → ($dest)"
cp -r $tmpl $dest
rm -f ($dest | path join "TEMPLATE-SETUP.md") # re-added at end with status
# Substitute {{RUSTELO_ROOT}} with the correct per-file relative path to fw
apply-rustelo-root $dest $fw
# --- rename crate identifiers website-* → <project>-* (skip if project == website)
if $project != "website" {
let p = $project
let ps = (snake $project)
let renames = [
["website-pages-htmx" $"($p)-pages-htmx"] ["website_pages_htmx" $"($ps)_pages_htmx"]
["website-pages" $"($p)-pages"] ["website_pages" $"($ps)_pages"]
["website-client" $"($p)-client"] ["website_client" $"($ps)_client"]
["rustelo-htmx-server" $"($p)-htmx-server"] ["rustelo_htmx_server" $"($ps)_htmx_server"]
["rustelo-leptos-server" $"($p)-leptos-server"] ["rustelo_leptos_server" $"($ps)_leptos_server"]
["website-shared" $"($p)-shared"] ["website_shared" $"($ps)_shared"]
["name = \"website\"" $"name = \"($p)\""] # leptos metadata output name
]
sanitize-tree $dest $renames
}
# --- set deploy/identity values
let domain_subs = [
["example.com" $domain]
]
sanitize-tree $dest $domain_subs
# --- ontoref onboarding
let onto = ($env.FILE_PWD | path join "onto-onboard.nu")
nu $onto --level $ontoref --root $dest --project $project --languages $languages --profile $render_mode
# --- write a fresh setup checklist with the resolved values
[
$"# ($site_title) — generated site"
""
$"- project: ($project) render mode: ($render_mode) languages: ($languages) (default ($default_lang))"
$"- domain: ($domain) ontoref: ($ontoref)"
""
"## Remaining COMPLETE items"
"- site/config/site.ncl — set site name (title) and languages/default_language."
"- site/config/routes.ncl — your routes; content under site/content/."
"- .env (copy from .env.example) — session secret, DB, SMTP/OAuth."
"- provisioning/project.ncl + lian-build/build_directives.ncl — registry, namespace, secrets (search: example.com, registry.example.com, secrets-base)."
"- Logos under site/public/images/logos/ + site/config/site.ncl logo.*"
"- Replace the example post in site/content/blog/{en,es}/getting-started/."
""
"Build: just build-auto (auto-detects profile)"
"Pack: just build::distro"
"Verify: cargo check"
] | str join "\n" | save -f ($dest | path join "SETUP.md")
print ""
print $"[new-site] done → ($dest)"
print $" next: cd ($dest); cat SETUP.md; cargo check"
}
# replace a list of [from to] pairs across all text files in a tree (binary-safe).
def sanitize-tree [root: string, pairs: list] {
let bin = [png jpg jpeg gif ico svg webp avif woff woff2 ttf otf eot wasm db "db-shm" "db-wal" pdf zip tgz gz xz lock map]
let files = (glob ($root | path join "**" "*") --no-dir
| where { |f| (($f | path parse | get extension) not-in $bin) and (not ($f | str contains "node_modules")) })
for f in $files {
try {
mut c = (open --raw $f | decode utf-8)
for pair in $pairs { $c = ($c | str replace --all ($pair | get 0) ($pair | get 1)) }
$c | save -f $f
} catch { }
}
}