///|
let new_project_name_placeholder = "__WARREN_PROJECT_NAME__"
///|
let rabbita_version_placeholder = "__RABBITA_VERSION__"
///|
let rabbita_version = "0.15.2"
///|
let default_new_project_template = "default"
///|
enum TemplateEntry {
Dir(String, Array[TemplateEntry])
File(String, String)
}
///|
let default_template_dir : TemplateEntry = Dir("", [
File("moon.mod", templates.default.moon_mod),
File("README.md", templates.default.readme_md),
Dir("main", [
File("moon.pkg", templates.default.main.moon_pkg),
File("main.mbt", templates.default.main.main_mbt),
]),
Dir("public", [
File("styles.css", templates.default.public.styles_css),
File("index.html", templates.default.public.index_html),
]),
])
///|
let minimized_template_dir : TemplateEntry = Dir("", [
File("moon.mod", templates.minimized.moon_mod),
File("moon.pkg", templates.minimized.moon_pkg),
File("main.mbt", templates.minimized.main_mbt),
])
///|
fn render_new_project_file(content : String, project_name : String) -> String {
let content = content.split(new_project_name_placeholder).join(project_name)
let content = content.split(rabbita_version_placeholder).join(rabbita_version)
content
}
///|
fn is_valid_project_name(name : String) -> Bool {
let mut valid = name != "" && name != "." && name != ".."
let invalid_parts = [" ", "\t", "\n", "\r", "/", "\\", ":", "\"", "'"]
for part in invalid_parts {
if name.contains(part) {
valid = false
}
}
valid
}
///|
async fn write_new_project_file(
root : String,
relative_path : String,
content : String,
) -> Unit {
let path = Path::join(root, relative_path).to_string()
let parent = Path::dirname(path).to_string()
if parent != "" && parent != "." && !@fs.exists(parent) {
@fs.mkdir(parent, permission=0o755, recursive=true)
}
@fs.write_file(path, content, create_mode=CreateOrTruncate, permission=0o644)
}
///|
async fn write_template_entry(
root : String,
parent : String,
entry : TemplateEntry,
project_name : String,
) -> Unit {
match entry {
File(name, content) => {
let path = if parent == "" {
name
} else {
Path::join(parent, name).to_string()
}
write_new_project_file(
root,
path,
render_new_project_file(content, project_name),
)
}
Dir(name, children) => {
let dir = if name == "" {
parent
} else if parent == "" {
name
} else {
Path::join(parent, name).to_string()
}
for child in children {
write_template_entry(root, dir, child, project_name)
}
}
}
}
///|
async fn new_project(
project_root : SourcePath,
dir : String,
template_name : String,
) -> Unit {
let project_root_string = source_path_to_string(project_root)
let requested_target = resolve_from(project_root_string, dir)
let requested_relative = Path::relative(
requested_target,
base=project_root_string,
).to_string()
let project_name = Path::basename(requested_target).to_owned()
let template_dir = match template_name {
"default" => default_template_dir
"minimized" => minimized_template_dir
_ =>
fail(
"Unknown template `\{template_name}`. Available templates: default, minimized.",
)
}
guard is_valid_project_name(project_name) else {
fail(
"Invalid project directory name `\{project_name}`. Avoid spaces, path separators, and quotes.",
)
}
guard path_is_inside(requested_target, project_root_string, allow_equal=false) else {
fail("New project directory must stay inside -C `\{project_root_string}`.")
}
guard !protected_output_path(requested_relative) else {
fail("Refusing dangerous new project path: \{requested_target}")
}
guard !@fs.exists(requested_target) else {
fail("Target path already exists: \{requested_target}")
}
let (lexical_parent, canonical_parent) = nearest_existing_parent(
requested_target,
)
guard @fs.kind(canonical_parent) is Directory else {
fail("New project parent is not a directory: \{lexical_parent}")
}
let target_dir = rebuild_from_canonical_parent(
requested_target, lexical_parent, canonical_parent,
)
guard path_is_inside(target_dir, project_root_string, allow_equal=false) else {
fail("New project directory resolves outside -C: \{requested_target}")
}
let target_relative = Path::relative(target_dir, base=project_root_string).to_string()
guard !protected_output_path(target_relative) else {
fail("Refusing dangerous new project path: \{requested_target}")
}
@fs.mkdir(target_dir, permission=0o755, recursive=true)
write_template_entry(target_dir, "", template_dir, project_name)
println("Created Warren project in \{target_dir}")
println("")
println("Next steps:")
println(" cd \{target_dir}")
let browser_entry = if template_name == "default" { "main" } else { "." }
println(" warren dev --browser-entry \{browser_entry}")
}
///|
test "render_new_project_file replaces placeholders" {
inspect(
render_new_project_file("local/__WARREN_PROJECT_NAME__", "demo"),
content="local/demo",
)
}
///|
test "is_valid_project_name rejects empty and path-like names" {
inspect(is_valid_project_name("demo-app"), content="true")
inspect(is_valid_project_name(""), content="false")
inspect(is_valid_project_name("two words"), content="false")
inspect(is_valid_project_name("nested/path"), content="false")
}