build(web): build frontend UI automatically in build.rs
vnt-web/build.rs now runs the pnpm UI build when static/ is missing or older than ui/src, so the embedded assets no longer need to be committed. Set VNT_WEB_SKIP_UI_BUILD=1 to skip (a placeholder page is written). Remove vnt-web/static from version control.
This commit is contained in:
@@ -11,3 +11,6 @@ vnt-desktop/dist
|
||||
vnt-desktop/src-tauri/gen
|
||||
vnt-desktop/src-tauri/icons/android
|
||||
vnt-desktop/src-tauri/icons/ios
|
||||
|
||||
# 前端构建产物,由 vnt-web/build.rs 自动构建
|
||||
vnt-web/static
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
//! 构建 vnt-web 前自动构建前端 UI(vnt-web/ui -> vnt-web/static)。
|
||||
//!
|
||||
//! - 前端源码(ui/src 等)比 static 产物新、或产物缺失时,调用 pnpm 构建
|
||||
//! - 产物已是最新则跳过,不拖慢增量编译
|
||||
//! - 找不到 pnpm 时:已有产物则告警并沿用;没有产物则报错并给出指引
|
||||
//! - 设置环境变量 VNT_WEB_SKIP_UI_BUILD=1 可完全跳过前端构建
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::time::SystemTime;
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
|
||||
let manifest_dir = Path::new(&manifest_dir);
|
||||
let ui_dir = manifest_dir.join("ui");
|
||||
let static_dir = manifest_dir.join("static");
|
||||
let workspace_root = manifest_dir.parent().expect("workspace root");
|
||||
|
||||
// UI 源码变化时重新运行本脚本
|
||||
println!("cargo:rerun-if-changed={}", ui_dir.join("src").display());
|
||||
println!("cargo:rerun-if-changed={}", ui_dir.join("index.html").display());
|
||||
println!("cargo:rerun-if-changed={}", ui_dir.join("vite.config.js").display());
|
||||
println!("cargo:rerun-if-changed={}", ui_dir.join("package.json").display());
|
||||
// 产物被删除时也要重新运行
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
static_dir.join("index.html").display()
|
||||
);
|
||||
println!("cargo:rerun-if-env-changed=VNT_WEB_SKIP_UI_BUILD");
|
||||
|
||||
if std::env::var("VNT_WEB_SKIP_UI_BUILD").is_ok() {
|
||||
ensure_static_placeholder(&static_dir);
|
||||
return;
|
||||
}
|
||||
|
||||
if static_is_fresh(&ui_dir, &static_dir) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(pnpm) = find_pnpm() else {
|
||||
if static_dir.join("index.html").is_file() {
|
||||
println!(
|
||||
"cargo:warning=未找到 pnpm,沿用 vnt-web/static 中已有的前端产物(可能不是最新)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
panic!(
|
||||
"未找到 pnpm 且 vnt-web/static 没有前端产物。\n\
|
||||
请安装 Node.js 与 pnpm 后重新构建(cargo 会自动完成前端构建),\n\
|
||||
或从发布包中获取 static 目录放入 vnt-web/。"
|
||||
);
|
||||
};
|
||||
|
||||
if !ui_dir.join("node_modules").is_dir() {
|
||||
// ui 依赖使用 workspace catalog,必须在仓库根目录安装
|
||||
run_or_panic(&pnpm, &["install", "--frozen-lockfile"], workspace_root);
|
||||
}
|
||||
run_or_panic(&pnpm, &["--filter", "vnt-web-ui", "build"], workspace_root);
|
||||
}
|
||||
|
||||
/// pnpm 命令名(Windows 上是 pnpm.cmd,由 cmd.exe 执行)
|
||||
fn find_pnpm() -> Option<&'static str> {
|
||||
let candidates: &[&str] = if cfg!(windows) {
|
||||
&["pnpm.cmd", "pnpm"]
|
||||
} else {
|
||||
&["pnpm"]
|
||||
};
|
||||
candidates
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|cmd| Command::new(cmd).arg("--version").output().is_ok())
|
||||
}
|
||||
|
||||
fn run_or_panic(program: &str, args: &[&str], dir: &Path) {
|
||||
println!("cargo:warning=执行前端构建: {} {} ({})", program, args.join(" "), dir.display());
|
||||
let status = Command::new(program)
|
||||
.args(args)
|
||||
.current_dir(dir)
|
||||
.status()
|
||||
.unwrap_or_else(|e| panic!("执行 {} 失败: {}", program, e));
|
||||
if !status.success() {
|
||||
panic!("前端构建失败: {} {} (exit: {:?})", program, args.join(" "), status.code());
|
||||
}
|
||||
}
|
||||
|
||||
/// static 产物是否比 UI 源码新
|
||||
fn static_is_fresh(ui_dir: &Path, static_dir: &Path) -> bool {
|
||||
let Ok(built_at) = std::fs::metadata(static_dir.join("index.html")).and_then(|m| m.modified())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
newest_mtime(&ui_dir.join("src")).is_none_or(|t| t <= built_at)
|
||||
}
|
||||
|
||||
fn newest_mtime(dir: &Path) -> Option<SystemTime> {
|
||||
let mut newest: Option<SystemTime> = None;
|
||||
let mut stack = vec![dir.to_path_buf()];
|
||||
while let Some(d) = stack.pop() {
|
||||
for entry in std::fs::read_dir(&d).ok()?.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
stack.push(path);
|
||||
} else if let Ok(m) = entry.metadata().and_then(|m| m.modified()) {
|
||||
newest = Some(newest.map_or(m, |n| n.max(m)));
|
||||
}
|
||||
}
|
||||
}
|
||||
newest
|
||||
}
|
||||
|
||||
/// 跳过构建时保证 static/ 存在,使 rust_embed 可以编译
|
||||
fn ensure_static_placeholder(static_dir: &Path) {
|
||||
if static_dir.join("index.html").is_file() {
|
||||
return;
|
||||
}
|
||||
println!("cargo:warning=VNT_WEB_SKIP_UI_BUILD 已设置且 static 为空,写入占位页面");
|
||||
std::fs::create_dir_all(static_dir).expect("创建 static 目录失败");
|
||||
std::fs::write(
|
||||
static_dir.join("index.html"),
|
||||
"<!doctype html><html><body><p>VNT Web UI 未构建。请安装 pnpm 后重新执行 cargo build,\
|
||||
或取消 VNT_WEB_SKIP_UI_BUILD。</p></body></html>",
|
||||
)
|
||||
.expect("写入占位页面失败");
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,28 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>VNT Dashboard</title>
|
||||
<link
|
||||
rel="icon"
|
||||
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect x='0' y='0' width='64' height='64' rx='16' fill='%23ffffff' stroke='%23e2e8f0' stroke-width='2'/%3E%3Cpath d='M16 20 L32 48 L48 20' fill='none' stroke='%234f46e5' stroke-width='6' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='16' cy='20' r='6' fill='%236366f1'/%3E%3Ccircle cx='48' cy='20' r='6' fill='%236366f1'/%3E%3Ccircle cx='32' cy='48' r='6' fill='%2322c55e'/%3E%3C/svg%3E"
|
||||
type="image/svg+xml"
|
||||
/>
|
||||
<script>
|
||||
// 首屏前应用主题,避免闪烁
|
||||
(function () {
|
||||
var t = localStorage.getItem("vnt-theme");
|
||||
var dark = t
|
||||
? t === "dark"
|
||||
: window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
if (dark) document.documentElement.classList.add("dark");
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-B15xAuUw.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DI9vlWSX.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user