refactor: 合并为单 crate,前端改为内嵌 React 控制台

合并 stash-cli / stash-core 为根目录单 crate(lib + bin),新增 React + Vite + TS 控制台替换 Askama 模板。
build.rs 用 walkdir 把 stash-web/dist 同步到 $OUT_DIR/web,由 include_dir! 编译期内嵌,产出单文件二进制。
路由改为 / 与 /assets/** 走内嵌前端,其余交给 ServeDir,缺失路径返回内置 404。同步更新依赖、.gitignore 与 README。
This commit is contained in:
2026-10-21 17:59:08 +08:00
parent 92c510e959
commit e522a98f6f
48 changed files with 5113 additions and 273 deletions
+88
View File
@@ -0,0 +1,88 @@
// build.rs
//
// 把前端产物 stash-web/dist 同步到 $OUT_DIR/web,
// 供 include_dir!("$OUT_DIR/web") 在编译期内嵌进二进制。
//
// 这里只做「校验 + 搬运」,不在这里跑 npm:
// 那样会把 Node 和网络依赖带进 cargo build。
// 编译 Rust 之前请先执行:cd stash-web && npm install && npm run build
use std::{
env, fs,
path::{Path, PathBuf},
};
use walkdir::WalkDir;
/// 前端工程目录,相对 crate 根目录。
const WEB_PROJECT_DIR: &str = "stash-web";
/// 前端构建产物目录,相对前端工程目录。
const WEB_DIST_DIR: &str = "dist";
/// 同步到 $OUT_DIR 下的子目录名,需与 include_dir! 中的路径保持一致。
const EMBED_DIR: &str = "web";
fn main() {
// 前端产物变了要重新嵌入(Cargo 对目录会递归扫描)
println!("cargo:rerun-if-changed={}/{}", WEB_PROJECT_DIR, WEB_DIST_DIR);
println!("cargo:rerun-if-changed={}/package.json", WEB_PROJECT_DIR);
let dist = Path::new(env!("CARGO_MANIFEST_DIR"))
.join(WEB_PROJECT_DIR)
.join(WEB_DIST_DIR);
let out = PathBuf::from(env::var("OUT_DIR").expect("构建环境缺少 OUT_DIR")).join(EMBED_DIR);
// 缺产物就给出人能看懂的报错,而不是 proc-macro 的 "No such file or directory"
assert!(
dist.join("index.html").is_file(),
"未找到前端产物:{}\n请先执行: cd {} && npm install && npm run build",
dist.display(),
WEB_PROJECT_DIR
);
// 必须整体重建,否则 Vite 换 hash 后旧 chunk 会残留在二进制里
let _ = fs::remove_dir_all(&out);
fs::create_dir_all(&out).expect("创建内嵌目录失败");
copy_dir(&dist, &out).expect("复制前端产物失败");
}
/// 用 walkdir 递归复制 `from` 到 `to`,并保留目录结构。
fn copy_dir(from: &Path, to: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut copied = 0usize;
for entry in WalkDir::new(from) {
let entry = entry?;
let relative = entry.path().strip_prefix(from)?;
// 跳过 dist 目录自身
if relative.as_os_str().is_empty() {
continue;
}
let destination = to.join(relative);
if entry.file_type().is_dir() {
fs::create_dir_all(&destination)?;
continue;
}
// 符号链接等非普通文件不参与内嵌
if !entry.file_type().is_file() {
continue;
}
fs::copy(entry.path(), &destination)?;
copied += 1;
}
if copied == 0 {
return Err(std::io::Error::other(format!(
"{} 中没有任何文件可内嵌",
from.display()
))
.into());
}
Ok(())
}