use boa_engine::ast::declaration::ImportName;
use concepts::{FunctionRegistry, IfcFqnName, PackageIfcFns};
use std::collections::HashMap;
use std::str::FromStr;
fn camel_to_kebab(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 4);
for (i, ch) in s.char_indices() {
if ch.is_uppercase() {
if i > 0 {
result.push('-');
}
for lower in ch.to_lowercase() {
result.push(lower);
}
} else {
result.push(ch);
}
}
result
}
#[derive(Debug)]
pub(crate) struct NamedFnImport {
pub js_name: String,
pub wit_name: String,
}
fn kebab_to_camel(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut capitalize_next = false;
for ch in s.chars() {
if ch == '-' {
capitalize_next = true;
} else if capitalize_next {
for upper in ch.to_uppercase() {
result.push(upper);
}
capitalize_next = false;
} else {
result.push(ch);
}
}
result
}
fn extract_and_verify<'a>(
js_code: &str,
all_exports: &'a [PackageIfcFns],
) -> Result<HashMap<IfcFqnName, &'a PackageIfcFns>, String> {
let mut interner = boa_engine::interner::Interner::new();
let mut parser = boa_engine::parser::Parser::new(boa_engine::Source::from_bytes(js_code));
let scope = boa_engine::ast::scope::Scope::new_global();
let module = parser
.parse_module(&scope, &mut interner)
.map_err(|e| format!("import extraction parse error: {e}"))?;
let mut referenced: HashMap<IfcFqnName, &PackageIfcFns> = HashMap::new();
for entry in module.items().import_entries() {
let specifier = interner
.resolve_expect(entry.module_request())
.utf8()
.ok_or_else(|| "import specifier is not valid UTF-8".to_string())?;
if specifier.starts_with("./") || specifier.starts_with("../") {
continue;
}
let ifc_fqn = IfcFqnName::from_str(specifier).map_err(|e| {
format!(
"import specifier `{specifier}` is not a WIT interface FQN \
(`ns:pkg/ifc` or `ns:pkg/ifc@ver`): {e}"
)
})?;
if ifc_fqn.is_namespace_obelisk() {
continue;
}
let ifc = all_exports
.iter()
.find(|pkg| pkg.ifc_fqn == ifc_fqn)
.ok_or_else(|| format!("interface `{ifc_fqn}` not found for import"))?;
if let ImportName::Name(sym) = entry.import_name() {
let js_name = interner
.resolve_expect(sym)
.utf8()
.ok_or_else(|| format!("imported name from `{specifier}` is not valid UTF-8"))?;
verify_named_import(js_name, ifc)?;
}
referenced.entry(ifc_fqn).or_insert(ifc);
}
Ok(referenced)
}
fn verify_named_import(js_name: &str, ifc: &PackageIfcFns) -> Result<(), String> {
let wit_name = camel_to_kebab(js_name);
if !ifc.fns.contains_key(wit_name.as_str()) {
return Err(format!(
"function `{ifc_fqn}.{wit_name}` (imported as `{js_name}`) not found",
ifc_fqn = ifc.ifc_fqn,
));
}
Ok(())
}
pub(crate) fn resolve_js_imports(
js_code: &str,
fn_registry: &dyn FunctionRegistry,
) -> Result<HashMap<IfcFqnName, Vec<NamedFnImport>>, String> {
let all_exports = fn_registry.all_exports();
let referenced = extract_and_verify(js_code, all_exports)?;
Ok(referenced
.into_iter()
.map(|(ifc_fqn, ifc)| (ifc_fqn, expand_interface(ifc)))
.collect())
}
fn expand_interface(ifc: &PackageIfcFns) -> Vec<NamedFnImport> {
ifc.fns
.keys()
.map(|fn_name| {
let wit_name = fn_name.to_string();
let js_name = kebab_to_camel(&wit_name);
NamedFnImport { js_name, wit_name }
})
.collect()
}