use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use velox_sfc::{
ComponentResolver, RenderMode, compile_template_to_rs_full_with_mode, parse_sfc,
to_stub_rs_unwrapped,
};
const ROW_CHILD: &str = r#"
<script setup>
pub struct Props {
pub label: String,
}
pub struct State {
pub props: Props,
}
impl State {
pub fn new() -> Self {
State { props: Props { label: String::new() } }
}
}
</script>
<template>
<li class="row">{{ label }}</li>
</template>
"#;
const FN_ROW_CHILD: &str = r#"
<script setup>
pub struct Props {
pub label: String,
pub on_confirm: Option<Box<dyn Fn(&str)>>,
}
pub struct State {
pub props: Props,
}
impl State {
pub fn new() -> Self {
State { props: Props { label: String::new(), on_confirm: None } }
}
}
</script>
<template>
<li class="row">{{ label }}</li>
</template>
"#;
const LOOPED_PARENT: &str = r#"
<script setup>
import RowChild from './row_child.vx'
use std::rc::Rc;
use velox_core::signal::Signal;
pub struct Props {
pub items: Vec<String>,
}
pub struct State {
pub props: Props,
pub items: Rc<Signal<Vec<String>>>,
pub log: Signal<Vec<String>>,
}
impl State {
pub fn new() -> Self {
State {
props: Props { items: Vec::new() },
items: Rc::new(Signal::new(Vec::new())),
log: Signal::new(Vec::new()),
}
}
pub fn on_confirm(&self, payload: &str) {
self.log.update(|mut seen| {
seen.push(String::from(payload));
seen
});
}
}
</script>
<template>
<ul class="rows">
<li v-for="item in items">
<RowChild :label="item" @confirm="on_confirm" />
</li>
</ul>
</template>
"#;
const LOOPED_FN_PROP_PARENT: &str = r#"
<script setup>
import FnRowChild from './fn_row_child.vx'
use std::rc::Rc;
use velox_core::signal::Signal;
pub struct Props {
pub items: Vec<String>,
}
pub struct State {
pub props: Props,
pub items: Rc<Signal<Vec<String>>>,
}
impl State {
pub fn new() -> Self {
State {
props: Props { items: Vec::new() },
items: Rc::new(Signal::new(Vec::new())),
}
}
pub fn on_confirm(&self, _payload: &str) {}
}
</script>
<template>
<ul class="rows">
<li v-for="item in items">
<FnRowChild :label="item" :on_confirm="on_confirm" />
</li>
</ul>
</template>
"#;
const CHILD_FILES: [(&str, &str); 2] = [("row_child", ROW_CHILD), ("fn_row_child", FN_ROW_CHILD)];
const ALIASES: &str = "use super::fn_row_child as FnRowChild;\nuse super::row_child as RowChild;\n";
fn generate(source: &str, name: &str, base: &Path) -> String {
let sfc = parse_sfc(source).unwrap_or_else(|e| panic!("{name}: parse failed: {e}"));
let script_setup = sfc.script_setup.as_ref().map(|s| s.content.as_str());
let tpl = sfc
.template
.as_ref()
.map(|t| t.content.as_str())
.unwrap_or("");
let mut resolver = ComponentResolver::new(base.to_path_buf());
if let Some(setup) = script_setup {
resolver.parse_imports(setup);
}
let render = compile_template_to_rs_full_with_mode(
tpl,
name,
Some(&mut resolver),
script_setup,
None,
RenderMode::State,
)
.unwrap_or_else(|e| panic!("{name}: template compilation failed: {e}"));
let mut module = to_stub_rs_unwrapped(&sfc, name, Some(base));
module.push('\n');
module.push_str(ALIASES);
module.push_str("\n\n");
module.push_str(&render);
module
}
fn scaffold_and_build(parent_name: &str, parent_body: &str) -> (ScratchCrate, Output) {
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let scratch = Path::new(env!("CARGO_MANIFEST_DIR")).join("target/props-loop-dispatch");
std::fs::create_dir_all(&scratch).expect("create scratch dir");
let root = scratch.join(format!("{parent_name}-{unique}"));
let src = root.join("src");
std::fs::create_dir_all(&src).expect("create temp crate src");
let workspace = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("velox-sfc has a parent")
.to_path_buf();
let manifest = format!(
r#"[package]
name = "props_loop_dispatch"
version = "0.0.0"
edition = "2024"
publish = false
[workspace]
[[bin]]
name = "props_loop_dispatch"
path = "src/main.rs"
[dependencies]
velox-core = {{ path = "{}" }}
velox-dom = {{ path = "{}" }}
"#,
workspace.join("velox-core").display(),
workspace.join("velox-dom").display(),
);
std::fs::write(root.join("Cargo.toml"), manifest).expect("write manifest");
for (file, body) in CHILD_FILES {
std::fs::write(root.join(format!("{file}.vx")), body).expect("write vx file");
}
for (name, body) in CHILD_FILES {
std::fs::write(src.join(format!("{name}.rs")), generate(body, name, &root))
.expect("write generated child module");
}
std::fs::write(
src.join(format!("{parent_name}.rs")),
generate(parent_body, parent_name, &root),
)
.expect("write generated parent module");
std::fs::write(
src.join("main.rs"),
format!("mod fn_row_child;\nmod row_child;\nmod {parent_name};\n\nfn main() {{}}\n"),
)
.expect("write main");
let out = cargo_in(&root, "build");
(ScratchCrate { root }, out)
}
struct ScratchCrate {
root: PathBuf,
}
impl Drop for ScratchCrate {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
fn cargo_in(root: &Path, subcommand: &str) -> Output {
Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()))
.arg(subcommand)
.current_dir(root)
.env("CARGO_TARGET_DIR", root.join("target"))
.output()
.expect("cargo failed to spawn")
}
fn report(out: &Output) -> String {
format!(
"--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
)
}
#[test]
fn a_bound_child_tag_in_a_props_loop_generates_code_that_compiles() {
let (scratch, out) = scaffold_and_build("looped_parent", LOOPED_PARENT);
assert!(
out.status.success(),
"the generated module for a `@event` bound child inside a `v-for` over a \
declared `Vec` prop did not build\n{}\n(scratch: {})",
report(&out),
scratch.root.display()
);
}
#[test]
fn a_fn_prop_child_tag_in_a_props_loop_generates_code_that_compiles() {
let (scratch, out) = scaffold_and_build("looped_fn_prop_parent", LOOPED_FN_PROP_PARENT);
assert!(
out.status.success(),
"the generated module for a function-prop bound child inside a `v-for` over \
a declared `Vec` prop did not build\n{}\n(scratch: {})",
report(&out),
scratch.root.display()
);
}