191 lines
9 KiB
Text
191 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 { }
|
||
|
|
}
|
||
|
|
}
|