v0.9.9
This commit is contained in:
@@ -11,6 +11,16 @@ fn required_string<'a>(meta: &'a Value, key: &str, file: &str) -> &'a str {
|
||||
.unwrap_or_else(|| panic!("{file}: meta.{key} must be a non-empty string"))
|
||||
}
|
||||
|
||||
fn has_text(value: Option<&Value>) -> bool {
|
||||
match value {
|
||||
Some(Value::String(text)) => !text.trim().is_empty(),
|
||||
Some(Value::Object(items)) => items
|
||||
.values()
|
||||
.any(|item| item.as_str().is_some_and(|text| !text.trim().is_empty())),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn fnv1a_update(mut hash: u64, bytes: &[u8]) -> u64 {
|
||||
for byte in bytes {
|
||||
hash ^= u64::from(*byte);
|
||||
@@ -62,8 +72,10 @@ fn bundle_app_js(web_dir: &Path) -> String {
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
|
||||
let lang_dir = manifest_dir.join("lang");
|
||||
let preset_dir = manifest_dir.join("presets");
|
||||
let web_dir = manifest_dir.join("web");
|
||||
println!("cargo:rerun-if-changed={}", lang_dir.display());
|
||||
println!("cargo:rerun-if-changed={}", preset_dir.display());
|
||||
|
||||
let theme_init_path = web_dir.join("theme-init.js");
|
||||
let styles_path = web_dir.join("styles.css");
|
||||
@@ -191,12 +203,86 @@ fn main() {
|
||||
panic!("lang/en.json is required as the default fallback language");
|
||||
}
|
||||
|
||||
let mut preset_files: Vec<PathBuf> = fs::read_dir(&preset_dir)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", preset_dir.display()))
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("json"))
|
||||
.collect();
|
||||
preset_files.sort();
|
||||
if preset_files.is_empty() {
|
||||
panic!("no flow preset files found in {}", preset_dir.display());
|
||||
}
|
||||
|
||||
let mut preset_assets = Vec::new();
|
||||
let mut preset_manifest = Vec::new();
|
||||
for path in preset_files {
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.expect("UTF-8 preset filename");
|
||||
let stem = path
|
||||
.file_stem()
|
||||
.and_then(|value| value.to_str())
|
||||
.expect("UTF-8 preset id");
|
||||
if !stem.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_') {
|
||||
panic!("{filename}: filename may only contain ASCII letters, digits, '-' and '_'");
|
||||
}
|
||||
let source = fs::read_to_string(&path)
|
||||
.unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
|
||||
build_hash = fnv1a_update(build_hash, source.as_bytes());
|
||||
let document: Value = serde_json::from_str(&source)
|
||||
.unwrap_or_else(|error| panic!("{filename}: invalid JSON: {error}"));
|
||||
let object = document
|
||||
.as_object()
|
||||
.unwrap_or_else(|| panic!("{filename}: preset root must be an object"));
|
||||
let id = object
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| panic!("{filename}: id must be a non-empty string"));
|
||||
if id != stem {
|
||||
panic!("{filename}: id '{id}' must match filename '{stem}.json'");
|
||||
}
|
||||
let category = object
|
||||
.get("category")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| panic!("{filename}: category must be a non-empty string"));
|
||||
if !has_text(object.get("name")) {
|
||||
panic!("{filename}: name must be a string or localized object with text");
|
||||
}
|
||||
if !has_text(object.get("description")) {
|
||||
panic!("{filename}: description must be a string or localized object with text");
|
||||
}
|
||||
let flow = object
|
||||
.get("flow")
|
||||
.and_then(Value::as_object)
|
||||
.unwrap_or_else(|| panic!("{filename}: flow must be an object"));
|
||||
if !flow.get("nodes").is_some_and(Value::is_array) || !flow.get("edges").is_some_and(Value::is_array) {
|
||||
panic!("{filename}: flow.nodes and flow.edges must be arrays");
|
||||
}
|
||||
preset_manifest.push(json!({
|
||||
"id": id,
|
||||
"category": category,
|
||||
"file": filename,
|
||||
}));
|
||||
preset_assets.push((filename.to_owned(), path));
|
||||
}
|
||||
|
||||
let manifest_json = serde_json::to_string(&json!({
|
||||
"default": "en",
|
||||
"languages": manifest_languages
|
||||
}))
|
||||
.expect("serialize language manifest");
|
||||
|
||||
let preset_manifest_json = serde_json::to_string(&json!({
|
||||
"version": 1,
|
||||
"presets": preset_manifest,
|
||||
}))
|
||||
.expect("serialize preset manifest");
|
||||
|
||||
let mut generated = String::new();
|
||||
generated.push_str("// @generated by build.rs - do not edit.\n");
|
||||
generated.push_str(&format!(
|
||||
@@ -228,6 +314,19 @@ fn main() {
|
||||
));
|
||||
}
|
||||
generated.push_str("];\n");
|
||||
generated.push_str(&format!(
|
||||
"pub const PRESET_MANIFEST_JSON: &str = {:?};\n",
|
||||
preset_manifest_json
|
||||
));
|
||||
generated.push_str("pub const PRESET_ASSETS: &[(&str, &str)] = &[\n");
|
||||
for (filename, path) in preset_assets {
|
||||
generated.push_str(&format!(
|
||||
" ({:?}, include_str!({:?})),\n",
|
||||
filename,
|
||||
path.to_string_lossy()
|
||||
));
|
||||
}
|
||||
generated.push_str("];\n");
|
||||
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
|
||||
fs::write(out_dir.join("app.bundle.js"), app_js).expect("write generated app.bundle.js");
|
||||
|
||||
Reference in New Issue
Block a user