200 lines
7.4 KiB
Rust
200 lines
7.4 KiB
Rust
// file: crates/knowledge-web-lib/src/assets.rs
|
|
// version: 2
|
|
|
|
//! Vite development and production asset resolution.
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
struct ManifestEntry {
|
|
file: String,
|
|
#[serde(default)]
|
|
css: Vec<String>,
|
|
#[serde(default, rename = "isEntry")]
|
|
is_entry: bool,
|
|
}
|
|
|
|
/// Internal resolved asset set consumed by the Maud renderer and Actix runtime.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub(crate) struct ViteAssets {
|
|
scripts: Vec<String>,
|
|
stylesheets: Vec<String>,
|
|
static_assets_directory: Option<std::path::PathBuf>,
|
|
static_mount_path: Option<String>,
|
|
}
|
|
|
|
impl crate::ViteAssets {
|
|
/// Build development asset references from a configured Vite origin.
|
|
pub(crate) fn development(origin: &str) -> Result<crate::ViteAssets, String> {
|
|
let normalized = origin.trim().trim_end_matches('/');
|
|
if normalized.is_empty() {
|
|
return Err("KNOWLEDGE_VITE_DEV_ORIGIN must not be empty".to_owned());
|
|
}
|
|
return Ok(crate::ViteAssets {
|
|
scripts: vec![format!("{normalized}/@vite/client"), format!("{normalized}/src/main.ts")],
|
|
stylesheets: Vec::new(),
|
|
static_assets_directory: None,
|
|
static_mount_path: None,
|
|
});
|
|
}
|
|
|
|
/// Resolve production asset references from Vite manifest JSON text.
|
|
pub(crate) fn from_manifest_text(manifest: &str, asset_base: &str, static_assets_directory: std::path::PathBuf) -> Result<crate::ViteAssets, String> {
|
|
let entries: std::collections::BTreeMap<String, ManifestEntry> = match serde_json::from_str(manifest) {
|
|
Ok(value) => value,
|
|
Err(error) => return Err(format!("invalid Vite manifest JSON: {error}")),
|
|
};
|
|
let entry = match entries.get("src/main.ts") {
|
|
Some(value) => value,
|
|
None => return Err("Vite manifest is missing src/main.ts".to_owned()),
|
|
};
|
|
if !entry.is_entry {
|
|
return Err("Vite manifest src/main.ts is not marked as an entry".to_owned());
|
|
}
|
|
if entry.file.trim().is_empty() {
|
|
return Err("Vite manifest src/main.ts has an empty JavaScript file".to_owned());
|
|
}
|
|
let javascript = match asset_url(asset_base, entry.file.as_str()) {
|
|
Ok(value) => value,
|
|
Err(error) => return Err(error),
|
|
};
|
|
let mut stylesheets = Vec::new();
|
|
for stylesheet in &entry.css {
|
|
let resolved = match asset_url(asset_base, stylesheet.as_str()) {
|
|
Ok(value) => value,
|
|
Err(error) => return Err(error),
|
|
};
|
|
stylesheets.push(resolved);
|
|
}
|
|
let static_mount_path = local_static_mount_path(asset_base);
|
|
return Ok(crate::ViteAssets {
|
|
scripts: vec![javascript],
|
|
stylesheets,
|
|
static_assets_directory: Some(static_assets_directory),
|
|
static_mount_path,
|
|
});
|
|
}
|
|
|
|
/// Return module script URLs in load order.
|
|
pub(crate) fn scripts(&self) -> &[String] {
|
|
return self.scripts.as_slice();
|
|
}
|
|
|
|
/// Return production stylesheet URLs. Development CSS is injected by Vite.
|
|
pub(crate) fn stylesheets(&self) -> &[String] {
|
|
return self.stylesheets.as_slice();
|
|
}
|
|
|
|
/// Return the local production assets directory when static serving is available.
|
|
pub(crate) fn static_assets_directory(&self) -> Option<&std::path::Path> {
|
|
match self.static_assets_directory.as_deref() {
|
|
Some(value) => return Some(value),
|
|
None => return None,
|
|
}
|
|
}
|
|
|
|
/// Return the local HTTP mount used for built assets, or `None` for an external asset base.
|
|
pub(crate) fn static_mount_path(&self) -> Option<&str> {
|
|
match self.static_mount_path.as_deref() {
|
|
Some(value) => return Some(value),
|
|
None => return None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resolve the active Vite asset mode from process environment variables.
|
|
pub(crate) fn load_assets_from_environment() -> Result<crate::ViteAssets, std::io::Error> {
|
|
let development_origin = environment_value("KNOWLEDGE_VITE_DEV_ORIGIN");
|
|
let manifest_path = environment_value("KNOWLEDGE_VITE_MANIFEST_PATH");
|
|
match (development_origin, manifest_path) {
|
|
(Some(_), Some(_)) => {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
"configure either KNOWLEDGE_VITE_DEV_ORIGIN or KNOWLEDGE_VITE_MANIFEST_PATH, not both",
|
|
));
|
|
},
|
|
(Some(origin), None) => {
|
|
let assets = match crate::ViteAssets::development(origin.as_str()) {
|
|
Ok(value) => value,
|
|
Err(error) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, error)),
|
|
};
|
|
return Ok(assets);
|
|
},
|
|
(None, Some(path)) => {
|
|
return load_manifest_assets(path.as_str());
|
|
},
|
|
(None, None) => {
|
|
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "configure KNOWLEDGE_VITE_DEV_ORIGIN or KNOWLEDGE_VITE_MANIFEST_PATH"));
|
|
},
|
|
}
|
|
}
|
|
|
|
fn environment_value(name: &str) -> Option<String> {
|
|
match std::env::var(name) {
|
|
Ok(value) => {
|
|
if value.trim().is_empty() {
|
|
return None;
|
|
}
|
|
return Some(value);
|
|
},
|
|
Err(_) => return None,
|
|
}
|
|
}
|
|
|
|
fn load_manifest_assets(path: &str) -> Result<crate::ViteAssets, std::io::Error> {
|
|
let manifest_path = std::path::PathBuf::from(path);
|
|
let text = match std::fs::read_to_string(manifest_path.as_path()) {
|
|
Ok(value) => value,
|
|
Err(error) => return Err(error),
|
|
};
|
|
let manifest_directory = match manifest_path.parent() {
|
|
Some(value) => value,
|
|
None => {
|
|
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Vite manifest path has no parent directory"));
|
|
},
|
|
};
|
|
let output_root = match manifest_directory.parent() {
|
|
Some(value) => value,
|
|
None => {
|
|
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Vite manifest path must be located below an output root"));
|
|
},
|
|
};
|
|
let asset_base = match environment_value("KNOWLEDGE_ASSET_BASE_PATH") {
|
|
Some(value) => value,
|
|
None => "/".to_owned(),
|
|
};
|
|
let assets = match crate::ViteAssets::from_manifest_text(text.as_str(), asset_base.as_str(), output_root.join("assets")) {
|
|
Ok(value) => value,
|
|
Err(error) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, error)),
|
|
};
|
|
return Ok(assets);
|
|
}
|
|
|
|
fn asset_url(base: &str, relative: &str) -> Result<String, String> {
|
|
let normalized_base = base.trim();
|
|
let normalized_relative = relative.trim().trim_start_matches('/');
|
|
if normalized_base.is_empty() {
|
|
return Err("KNOWLEDGE_ASSET_BASE_PATH must not be empty".to_owned());
|
|
}
|
|
if normalized_relative.is_empty() {
|
|
return Err("Vite manifest contains an empty asset path".to_owned());
|
|
}
|
|
if normalized_base == "/" {
|
|
return Ok(format!("/{normalized_relative}"));
|
|
}
|
|
return Ok(format!("{}/{normalized_relative}", normalized_base.trim_end_matches('/')));
|
|
}
|
|
|
|
fn local_static_mount_path(base: &str) -> Option<String> {
|
|
let normalized = base.trim();
|
|
if !normalized.starts_with('/') {
|
|
return None;
|
|
}
|
|
if normalized == "/" {
|
|
return Some("/assets".to_owned());
|
|
}
|
|
return Some(format!("{}/assets", normalized.trim_end_matches('/')));
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/assets.rs"]
|
|
mod tests;
|