#![allow(clippy::panic, clippy::print_stdout, clippy::print_stderr)] use std::{
env, fs, io,
path::{Path, PathBuf},
process::Command,
};
fn main() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
let studio_src = Path::new(&manifest_dir).join("studio");
let build_dir = Path::new(&out_dir).join("studio");
let dist_dir = build_dir.join("dist");
println!("cargo:rerun-if-changed=studio/src/app.ts");
println!("cargo:rerun-if-changed=studio/package.json");
println!("cargo:rerun-if-changed=studio/package-lock.json");
println!("cargo:rerun-if-changed=studio/tsconfig.json");
if let Err(e) = stage_studio(&studio_src, &build_dir) {
eprintln!("cargo:warning=failed to stage studio sources: {e}");
emit_fallback_dist(&dist_dir);
println!("cargo:rustc-env=FRAISEQL_STUDIO_DIST={}", dist_dir.display());
return;
}
let node_modules = build_dir.join("node_modules");
if !node_modules.exists() {
let status = Command::new("npm")
.args([
"ci",
"--ignore-scripts",
"--prefer-offline",
"--no-audit",
"--no-fund",
])
.current_dir(&build_dir)
.status();
match status {
Ok(s) if s.success() => {},
Ok(s) => panic!(
"npm ci failed with status {s} — the Studio SPA build requires a \
package-lock.json in sync with package.json (crates/fraiseql-server/studio). \
Run `npm install` there to refresh the lockfile."
),
Err(e) => {
eprintln!("cargo:warning=npm not found ({e}), skipping Studio SPA build");
emit_fallback_dist(&dist_dir);
println!("cargo:rustc-env=FRAISEQL_STUDIO_DIST={}", dist_dir.display());
return;
},
}
}
let status = Command::new("npm").args(["run", "build"]).current_dir(&build_dir).status();
match status {
Ok(s) if s.success() => {},
Ok(s) => panic!("npm run build failed with status {s}"),
Err(e) => {
eprintln!("cargo:warning=npm not found ({e}), skipping Studio SPA build");
emit_fallback_dist(&dist_dir);
},
}
println!("cargo:rustc-env=FRAISEQL_STUDIO_DIST={}", dist_dir.display());
}
fn stage_studio(src: &Path, dst: &Path) -> io::Result<()> {
fs::create_dir_all(dst)?;
for file in ["package.json", "package-lock.json", "tsconfig.json"] {
let from = src.join(file);
if from.exists() {
fs::copy(&from, dst.join(file))?;
}
}
let src_dir = src.join("src");
if src_dir.exists() {
copy_dir_all(&src_dir, &dst.join("src"))?;
}
Ok(())
}
fn copy_dir_all(src: &Path, dst: &Path) -> io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let dst_path = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&entry.path(), &dst_path)?;
} else {
fs::copy(entry.path(), &dst_path)?;
}
}
Ok(())
}
fn emit_fallback_dist(dist_dir: &PathBuf) {
fs::create_dir_all(dist_dir).expect("failed to create studio/dist/");
let js_path = dist_dir.join("app.js");
if !js_path.exists() {
fs::write(&js_path, b"// FraiseQL Studio placeholder\n")
.expect("failed to write fallback app.js");
}
let css_path = dist_dir.join("app.css");
if !css_path.exists() {
fs::write(&css_path, b"/* FraiseQL Studio placeholder */\n")
.expect("failed to write fallback app.css");
}
}