0.0.1-alpha.5
This commit is contained in:
195
crates/knowledge-web-lib/src/assets.rs
Normal file
195
crates/knowledge-web-lib/src/assets.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
// file: crates/knowledge-web-lib/src/assets.rs
|
||||
// version: 1
|
||||
|
||||
//! 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('/')));
|
||||
}
|
||||
Reference in New Issue
Block a user