use serde::{Deserialize, Serialize};
use crate::Result;
use fob_graph::{ExportKind, ModuleGraph};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BundleMetadata {
exports: Vec<ExportInfo>,
imports: Vec<ImportInfo>,
total_size: usize,
asset_count: usize,
module_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExportInfo {
pub name: String,
pub is_default: bool,
pub source_module: String,
pub is_used: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ImportInfo {
pub specifier: String,
pub imported_by: Vec<String>,
pub is_type_only: bool,
}
impl BundleMetadata {
pub async fn from_graph(
graph: &ModuleGraph,
total_size: usize,
asset_count: usize,
) -> Result<Self> {
let exports = extract_exports(graph).await?;
let imports = extract_imports(graph).await?;
let module_count = graph.len()?;
Ok(Self {
exports,
imports,
total_size,
asset_count,
module_count,
})
}
pub fn exports(&self) -> &[ExportInfo] {
&self.exports
}
pub fn imports(&self) -> &[ImportInfo] {
&self.imports
}
pub fn total_size(&self) -> usize {
self.total_size
}
pub fn asset_count(&self) -> usize {
self.asset_count
}
pub fn module_count(&self) -> usize {
self.module_count
}
pub fn has_export(&self, name: &str) -> bool {
self.exports.iter().any(|e| e.name == name)
}
pub fn has_default_export(&self) -> bool {
self.exports.iter().any(|e| e.is_default)
}
pub fn default_exports(&self) -> Vec<&ExportInfo> {
self.exports.iter().filter(|e| e.is_default).collect()
}
pub fn named_exports(&self) -> Vec<&ExportInfo> {
self.exports.iter().filter(|e| !e.is_default).collect()
}
pub fn unused_exports(&self) -> Vec<&ExportInfo> {
self.exports.iter().filter(|e| !e.is_used).collect()
}
}
async fn extract_exports(graph: &ModuleGraph) -> Result<Vec<ExportInfo>> {
let mut exports = Vec::new();
let modules = graph.modules()?;
for module in modules {
for export in module.exports.iter() {
exports.push(ExportInfo {
name: if matches!(export.kind, ExportKind::Default) {
"default".to_string()
} else {
export.name.clone()
},
is_default: matches!(export.kind, ExportKind::Default),
source_module: module.path.display().to_string(),
is_used: export.is_used,
});
}
}
Ok(exports)
}
async fn extract_imports(graph: &ModuleGraph) -> Result<Vec<ImportInfo>> {
use rustc_hash::FxHashMap;
let mut import_map: FxHashMap<String, ImportInfo> = FxHashMap::default();
let modules = graph.modules()?;
for module in modules {
for import in module.imports.iter() {
if import.is_external() {
let info = import_map
.entry(import.source.clone())
.or_insert_with(|| ImportInfo {
specifier: import.source.clone(),
imported_by: Vec::new(),
is_type_only: import.is_type_only(),
});
let module_path = module.path.display().to_string();
if !info.imported_by.contains(&module_path) {
info.imported_by.push(module_path);
}
if !import.is_type_only() {
info.is_type_only = false;
}
}
}
}
Ok(import_map.into_values().collect())
}
#[cfg(test)]
mod tests {
use super::*;
use fob_graph::{
Export, ExportKind, Import, ImportKind, ImportSpecifier, Module, ModuleGraph, ModuleId,
SourceSpan, SourceType,
};
async fn create_test_graph() -> ModuleGraph {
let graph = ModuleGraph::new().unwrap();
let entry_id = ModuleId::new_virtual("entry.js");
let entry = Module::builder(entry_id.clone(), "entry.js".into(), SourceType::JavaScript)
.exports(vec![
Export::new(
"Component",
ExportKind::Named,
true,
false,
None,
false,
false,
SourceSpan::new("entry.js", 0, 0),
),
Export::new(
"default",
ExportKind::Default,
true,
false,
None,
false,
false,
SourceSpan::new("entry.js", 0, 0),
),
])
.imports(vec![Import::new(
"react",
vec![ImportSpecifier::Named("useState".into())],
ImportKind::Static,
None,
SourceSpan::new("entry.js", 0, 0),
)])
.entry(true)
.build();
graph.add_module(entry).unwrap();
graph
}
#[tokio::test]
async fn test_extract_exports() {
let graph = create_test_graph().await;
let exports = extract_exports(&graph).await.unwrap();
assert_eq!(exports.len(), 2);
assert!(exports.iter().any(|e| e.name == "Component"));
assert!(exports.iter().any(|e| e.is_default));
}
#[tokio::test]
async fn test_extract_imports() {
let graph = create_test_graph().await;
let imports = extract_imports(&graph).await.unwrap();
assert_eq!(imports.len(), 1);
assert_eq!(imports[0].specifier, "react");
assert!(!imports[0].is_type_only);
}
#[tokio::test]
async fn test_has_default_export() {
let graph = create_test_graph().await;
let metadata = BundleMetadata::from_graph(&graph, 1024, 1).await.unwrap();
assert!(metadata.has_default_export());
}
#[tokio::test]
async fn test_has_export() {
let graph = create_test_graph().await;
let metadata = BundleMetadata::from_graph(&graph, 1024, 1).await.unwrap();
assert!(metadata.has_export("Component"));
assert!(!metadata.has_export("NonExistent"));
}
#[tokio::test]
async fn test_named_exports() {
let graph = create_test_graph().await;
let metadata = BundleMetadata::from_graph(&graph, 1024, 1).await.unwrap();
let named = metadata.named_exports();
assert_eq!(named.len(), 1);
assert_eq!(named[0].name, "Component");
}
}