use super::*;
pub(crate) fn set_global_state(state: Arc<AppState>) -> Result<(), EuvError> {
APP_STATE.set(state).map_err(|_: Arc<AppState>| {
EuvError::Message(ERROR_GLOBAL_STATE_ALREADY_INITIALIZED.to_string())
})
}
pub(crate) fn get_global_state() -> Option<Arc<AppState>> {
APP_STATE.get().cloned()
}
async fn resolve_inline_js(config: &HtmlConfig) -> Result<String, EuvError> {
let (pkg_dir, wasm_url): (PathBuf, String) = resolve_bridge_location(config)?;
let js_name: String = config
.get_import_path()
.rsplit('/')
.next()
.unwrap_or_default()
.to_string();
let js_path: PathBuf = pkg_dir.join(&js_name);
if !js_path.is_file() {
return Err(EuvError::IoPath {
message: format!(
"wasm-pack wrote no JS bridge to inline; {} is missing, so this build \
cannot produce a JavaScript-free page",
js_path.display()
),
path: js_path,
error: io::Error::new(io::ErrorKind::NotFound, "no wasm-bindgen JS bridge"),
});
}
build_inline_bridge(&pkg_dir, &js_name, &wasm_url).await
}
fn resolve_bridge_location(config: &HtmlConfig) -> Result<(PathBuf, String), EuvError> {
let import_path: &str = config.get_import_path();
let Some((relative_dir, js_name)) = import_path.rsplit_once('/') else {
return Err(EuvError::Message(format!(
"import path '{import_path}' does not name a generated bridge, so this build \
cannot produce a JavaScript-free page"
)));
};
let js_name: &str = js_name.split(['?', '#']).next().unwrap_or(js_name);
let Some(stem) = js_name.strip_suffix(JS_EXTENSION) else {
return Err(EuvError::Message(format!(
"import path '{import_path}' does not name a JavaScript bridge, so this build \
cannot produce a JavaScript-free page"
)));
};
let pkg_dir: PathBuf = config.get_serving_root().join(relative_dir);
let wasm_url: String = format!("{relative_dir}/{stem}{WASM_FILE_SUFFIX}");
Ok((pkg_dir, wasm_url))
}
pub(crate) fn normalize_bootstrap(template: &str) -> Result<String, EuvError> {
let script_tag: String = format!("<script>{INLINE_JS_PLACEHOLDER}</script>");
let template: String = replace_module_bootstrap(template, &script_tag);
if template.contains(IMPORT_PATH_PLACEHOLDER) {
return Err(EuvError::Message(String::from(
"the index.html template still carries __IMPORT_PATH__ outside a bootstrap \
script; replace it with __EUV_INLINE_JS__",
)));
}
match template.matches(INLINE_JS_PLACEHOLDER).count() {
1 => return Ok(template),
0 => {}
_ => {
return Err(EuvError::Message(String::from(
"the index.html template must contain exactly one WASM bootstrap",
)));
}
}
let body_close: usize = template.rfind("</body>").ok_or_else(|| {
EuvError::Message(String::from(
"the index.html template has no __EUV_INLINE_JS__ placeholder and no </body> to \
inject one before; add the placeholder so the page boots its WASM module",
))
})?;
let mut out: String = String::with_capacity(template.len() + script_tag.len());
out.push_str(&template[..body_close]);
out.push_str(&script_tag);
out.push('\n');
out.push_str(&template[body_close..]);
Ok(out)
}
fn replace_module_bootstrap(template: &str, replacement: &str) -> String {
let lowered: String = template.to_ascii_lowercase();
let mut out: String = String::with_capacity(template.len());
let mut cursor: usize = 0;
while let Some(open_offset) = lowered[cursor..].find("<script") {
let open: usize = cursor + open_offset;
let Some(open_end) = lowered[open..].find('>').map(|end: usize| open + end + 1) else {
break;
};
let tag: &str = &template[open..open_end];
let tag_end: Option<usize> = lowered[open_end..]
.find("</script")
.map(|end: usize| open_end + end);
let Some(close_end) = tag_end
.and_then(|close: usize| lowered[close..].find('>').map(|end: usize| close + end + 1))
else {
break;
};
let body: &str = &template[open_end..tag_end.unwrap_or(close_end)];
out.push_str(&template[cursor..open]);
if body.contains(IMPORT_PATH_PLACEHOLDER) && !tag.contains("src=") && !tag.contains("src =")
{
out.push_str(replacement);
} else {
out.push_str(&template[open..close_end]);
}
cursor = close_end;
}
out.push_str(&template[cursor..]);
out
}
pub(crate) async fn generate_html(config: &HtmlConfig) -> Result<String, EuvError> {
let template_content: String = if let Some(custom_path) = config.try_get_custom_index_html() {
let bytes: Vec<u8> =
read(custom_path)
.await
.map_err(|error: io::Error| EuvError::IoPath {
message: ERROR_READ_CUSTOM_INDEX_HTML.to_string(),
path: custom_path.to_path_buf(),
error,
})?;
String::from_utf8(bytes).map_err(|error: FromUtf8Error| EuvError::Utf8 {
message: ERROR_CUSTOM_INDEX_HTML_NOT_UTF8.to_string(),
error,
})?
} else if config.get_is_release() {
INDEX_HTML_RELEASE.to_string()
} else {
INDEX_HTML_DEV.to_string()
};
let inline_js: String = resolve_inline_js(config).await?;
let template_content: String = normalize_bootstrap(&template_content)?;
let base_href_tag: String = if let Some(prefix) = config.try_get_serving_route_prefix() {
let path: String = format!("/{prefix}");
format!("<base href=\"{}/\" />", path.trim_end_matches('/'))
} else {
String::new()
};
let html: String = template_content
.replace(BASE_HREF_PLACEHOLDER, &base_href_tag)
.replace(RELOAD_ROUTE_PLACEHOLDER, RELOAD_ROUTE)
.replace(INLINE_JS_PLACEHOLDER, &inline_js)
.replace(LOADING_STYLE_PLACEHOLDER, LOADING_STYLE)
.replace(LOADING_ELEMENT_PLACEHOLDER, LOADING_ELEMENT)
.replace(LOADING_REMOVAL_PLACEHOLDER, LOADING_REMOVAL_SCRIPT);
let index_path: PathBuf = config.get_serving_root().join(INDEX_HTML_FILE_NAME);
create_dir_all(config.get_serving_root())
.await
.map_err(|error: io::Error| EuvError::Io {
message: ERROR_CREATE_STATIC_DIRECTORY.to_string(),
error,
})?;
write(&index_path, &html)
.await
.map_err(|error: io::Error| EuvError::Io {
message: ERROR_WRITE_INDEX_HTML.to_string(),
error,
})?;
Ok(html)
}
pub async fn resolve_www_dir(www_dir: &Path) -> PathBuf {
if metadata(www_dir.join(INDEX_HTML_FILE_NAME)).await.is_ok() {
return www_dir.to_path_buf();
}
let parent_name: Option<&str> = www_dir
.file_name()
.and_then(|file_name_os_str: &ffi::OsStr| file_name_os_str.to_str());
if let Some(name) = parent_name {
let nested: PathBuf = www_dir.join(name);
if metadata(nested.join(INDEX_HTML_FILE_NAME)).await.is_ok() {
return nested;
}
}
www_dir.to_path_buf()
}
pub fn resolve_pkg_dir(args: &ModeArgs) -> PathBuf {
resolve_out_dir(args)
}
pub async fn resolve_file_in_base(base: &Path, path: &str) -> Option<PathBuf> {
let file_path: PathBuf = base.join(path);
let canonical_path: PathBuf = canonicalize(&file_path).await.ok()?;
let base_canonical: PathBuf = canonicalize(base).await.ok()?;
canonical_path
.starts_with(&base_canonical)
.then_some(file_path)
}