1use std::fs;
7use std::path::{Path, PathBuf};
8
9use color_eyre::eyre::{Result, WrapErr, bail, eyre};
10use fse_schema::{Schema, snapshot};
11
12use crate::config::OrmConfig;
13
14pub struct ModuleInfo {
15 pub name: String,
16 pub dir: PathBuf,
19}
20
21pub fn discover(root: &Path, cfg: &OrmConfig) -> Result<Vec<ModuleInfo>> {
24 if cfg.modules.is_empty() {
25 return Ok(Vec::new());
26 }
27 let output = std::process::Command::new("cargo")
28 .args(["metadata", "--format-version", "1"])
29 .current_dir(root)
30 .output()
31 .wrap_err("cannot run cargo metadata")?;
32 if !output.status.success() {
33 bail!(
34 "cargo metadata failed: {}",
35 String::from_utf8_lossy(&output.stderr)
36 );
37 }
38 let meta: serde_json::Value =
39 serde_json::from_slice(&output.stdout).wrap_err("cargo metadata output")?;
40 let packages = meta["packages"]
41 .as_array()
42 .ok_or_else(|| eyre!("cargo metadata output has no packages"))?;
43
44 let mut modules = Vec::new();
45 for name in &cfg.modules {
46 let package = packages
47 .iter()
48 .find(|p| p["name"].as_str() == Some(name))
49 .ok_or_else(|| {
50 eyre!("module crate `{name}` not found — is it a dependency in Cargo.toml?")
51 })?;
52 let manifest = package["manifest_path"]
53 .as_str()
54 .ok_or_else(|| eyre!("module `{name}`: no manifest_path"))?;
55 let dir = PathBuf::from(manifest)
56 .parent()
57 .ok_or_else(|| eyre!("module `{name}`: bad manifest_path"))?
58 .to_path_buf();
59 modules.push(ModuleInfo {
60 name: name.clone(),
61 dir,
62 });
63 }
64 Ok(modules)
65}
66
67pub fn load_schema(module: &ModuleInfo) -> Result<Schema> {
69 let path = module.dir.join(".fse/schema.json");
70 let raw = fs::read_to_string(&path).map_err(|_| {
71 eyre!(
72 "module `{}` ships no schema snapshot ({}) — the module author must run \
73 `fse migrate` and include .fse/schema.json in the published crate",
74 module.name,
75 path.display()
76 )
77 })?;
78 Ok(snapshot::schema_from_json(&raw)?)
79}
80
81pub fn sync(root: &Path, cfg: &OrmConfig) -> Result<()> {
85 let modules = discover(root, cfg)?;
86 let base = root.join(".fse/modules");
87 if base.exists() {
88 fs::remove_dir_all(&base).wrap_err_with(|| format!("cannot clear {}", base.display()))?;
89 }
90 if modules.is_empty() {
91 println!("no modules configured (fse.toml [orm] modules).");
92 return Ok(());
93 }
94 for module in &modules {
95 let src = module.dir.join("frontend");
96 if !src.exists() {
97 println!("{}: no frontend/ sources.", module.name);
98 continue;
99 }
100 let dest = base.join(&module.name).join("frontend");
101 copy_dir(&src, &dest)?;
102 println!("{}: frontend synced to {}", module.name, dest.display());
103 }
104 Ok(())
105}
106
107fn copy_dir(src: &Path, dest: &Path) -> Result<()> {
108 fs::create_dir_all(dest).wrap_err_with(|| dest.display().to_string())?;
109 for entry in fs::read_dir(src).wrap_err_with(|| src.display().to_string())? {
110 let entry = entry.wrap_err_with(|| src.display().to_string())?;
111 let from = entry.path();
112 let to = dest.join(entry.file_name());
113 if from.is_dir() {
114 copy_dir(&from, &to)?;
115 } else {
116 fs::copy(&from, &to).wrap_err_with(|| from.display().to_string())?;
117 }
118 }
119 Ok(())
120}