use farmfe_core::{
cache_item,
module::{ModuleId, ModuleType},
plugin::{PluginAnalyzeDepsHookResultEntry, ResolveKind},
swc_html_ast::{Child, Document, Element},
HashMap,
};
use farmfe_toolkit::swc_html_visit::{Visit, VisitWith};
pub const HTML_INLINE_ID_PREFIX: &str = "virtual:html-inline:";
pub struct DepsAnalyzer {
deps: Option<Vec<PluginAnalyzeDepsHookResultEntry>>,
html_id: ModuleId,
pub inline_deps_map: HashMap<String, HtmlInlineModule>,
}
impl DepsAnalyzer {
pub fn new(html_id: ModuleId) -> Self {
Self {
deps: None,
html_id,
inline_deps_map: HashMap::default(),
}
}
pub fn analyze_deps(&mut self, document: &Document) -> Vec<PluginAnalyzeDepsHookResultEntry> {
document.visit_with(self);
self.deps.take().unwrap_or(vec![])
}
fn insert_dep(&mut self, dep: PluginAnalyzeDepsHookResultEntry) {
if let Some(deps) = &mut self.deps {
deps.push(dep);
} else {
self.deps.replace(vec![dep]);
}
}
fn generate_virtual_id(&mut self, code: String, module_type: ModuleType) -> String {
let id = self.html_id.to_string();
let size = self.inline_deps_map.len();
let virtual_id = format!("{HTML_INLINE_ID_PREFIX}{id}_{size}");
self.inline_deps_map.insert(
virtual_id.clone(),
HtmlInlineModule {
html_id: self.html_id.clone(),
id: virtual_id.clone(),
code,
module_type,
},
);
virtual_id
}
}
impl Visit for DepsAnalyzer {
fn visit_element(&mut self, element: &Element) {
if let Some(value) = get_script_src_or_code(Some(self), element) {
self.insert_dep(PluginAnalyzeDepsHookResultEntry {
kind: ResolveKind::ScriptSrc,
source: value,
})
} else if let Some(value) = get_href_link_or_code(Some(self), element) {
self.insert_dep(PluginAnalyzeDepsHookResultEntry {
kind: ResolveKind::LinkHref,
source: value,
})
}
element.visit_children_with(self);
}
}
pub fn get_script_type_module_code(element: &Element) -> Option<String> {
if element.tag_name.to_string() == "script" {
let src_attr = element
.attributes
.iter()
.find(|&attr| attr.name.to_string() == "type");
let mut is_module = false;
if let Some(src_attr) = src_attr {
if let Some(value) = &src_attr.value {
let value = value.to_string();
is_module = value == "module";
}
}
if !is_module {
return None;
}
for child in &element.children {
if let Child::Text(text) = child {
let code = text.data.to_string();
return Some(code);
}
}
}
None
}
fn get_script_src_or_code(
analyzer: Option<&mut DepsAnalyzer>,
element: &Element,
) -> Option<String> {
if element.tag_name.to_string() == "script" {
let src_attr = element
.attributes
.iter()
.find(|&attr| attr.name.to_string() == "src");
if let Some(src_attr) = src_attr {
if let Some(value) = &src_attr.value {
let value = value.trim().to_string();
if should_ignore_source(&value) {
return None;
}
Some(value)
} else {
None
}
} else {
if let Some(analyzer) = analyzer {
if let Some(code) = get_script_type_module_code(element) {
return Some(analyzer.generate_virtual_id(code, ModuleType::Js));
}
}
None
}
} else {
None
}
}
pub fn get_script_src_value(element: &Element) -> Option<String> {
get_script_src_or_code(None, element)
}
pub fn is_link_css(element: &Element) -> bool {
element.tag_name.to_string() == "link"
&& element
.attributes
.iter()
.any(|attr| attr.name.to_string() == "rel" && attr.value.as_deref() == Some("stylesheet"))
}
pub fn get_link_css_code(element: &Element) -> Option<String> {
if is_link_css(element) {
for child in &element.children {
if let Child::Text(text) = child {
let code = text.data.to_string();
return Some(code);
}
}
}
None
}
fn get_href_link_or_code(analyzer: Option<&mut DepsAnalyzer>, element: &Element) -> Option<String> {
if is_link_css(element) {
let src_attr = element
.attributes
.iter()
.find(|&attr| attr.name.to_string() == "href");
if let Some(src_attr) = src_attr {
if let Some(value) = &src_attr.value {
let value = value.trim().to_string();
if should_ignore_source(&value) {
return None;
}
Some(value)
} else {
None
}
} else {
if let Some(analyzer) = analyzer {
if let Some(code) = get_link_css_code(element) {
return Some(analyzer.generate_virtual_id(code, ModuleType::Css));
}
}
None
}
} else {
None
}
}
pub fn get_href_link_value(element: &Element) -> Option<String> {
get_href_link_or_code(None, element)
}
pub fn should_ignore_source(source: &str) -> bool {
source.starts_with("http")
|| source.starts_with('#')
|| source.starts_with('?')
|| source.starts_with("data:")
}
#[cache_item(farmfe_core)]
#[derive(Debug, Clone)]
pub struct HtmlInlineModule {
pub html_id: ModuleId,
pub id: String,
pub code: String,
pub module_type: ModuleType,
}