use std::{
path::{Path, PathBuf},
sync::Arc,
};
use path_clean::PathClean;
use rolldown::{BundlerBuilder as RolldownBundlerBuilder, BundlerOptions, InputItem};
use rolldown_plugin::{__inner::SharedPluginable, Plugin};
use rustc_hash::FxHashMap;
use crate::analysis::AnalyzedBundle;
use crate::builders::{asset_plugin::AssetDetectionPlugin, asset_registry::AssetRegistry};
use crate::diagnostics;
use crate::module_collection_plugin::ModuleCollectionPlugin;
use crate::{Error, Result};
use fob_graph::analysis::stats::compute_stats;
use fob_graph::{AnalysisResult, CacheAnalysis, TransformationTrace};
pub(crate) fn normalize_entry_path(entry: impl AsRef<Path>) -> String {
let cleaned: PathBuf = entry.as_ref().to_path_buf().clean();
cleaned.to_string_lossy().into_owned()
}
pub trait IntoPlugin {
fn into_plugin(self) -> SharedPluginable;
}
pub(crate) struct PluginHandle<P>(P);
pub fn plugin<P>(plugin: P) -> impl IntoPlugin
where
P: Plugin + 'static,
{
PluginHandle(plugin)
}
impl IntoPlugin for SharedPluginable {
fn into_plugin(self) -> SharedPluginable {
self
}
}
impl<T> IntoPlugin for Arc<T>
where
T: Plugin + 'static,
{
fn into_plugin(self) -> SharedPluginable {
self
}
}
impl<P> IntoPlugin for PluginHandle<P>
where
P: Plugin + 'static,
{
fn into_plugin(self) -> SharedPluginable {
Arc::new(self.0)
}
}
#[derive(Clone, Debug)]
pub(crate) struct EntrySpec {
pub name: Option<String>,
pub import: String,
}
pub(crate) struct BundlePlan {
pub entries: Vec<EntrySpec>,
pub options: BundlerOptions,
pub plugins: Vec<SharedPluginable>,
pub cwd: Option<PathBuf>,
pub virtual_files: FxHashMap<String, String>,
pub runtime: Option<Arc<dyn crate::Runtime>>,
}
pub(crate) async fn execute_bundle(plan: BundlePlan) -> Result<AnalyzedBundle> {
let BundlePlan {
entries,
mut options,
mut plugins,
cwd,
virtual_files,
runtime,
} = plan;
let scan_cwd = match cwd.clone() {
Some(path) => {
eprintln!("[FOB_BUILD] Using explicit cwd: {}", path.display());
path
}
None => {
if let Some(ref rt) = runtime {
match rt.get_cwd() {
Ok(path) => {
eprintln!("[FOB_BUILD] Using runtime cwd: {}", path.display());
path
}
Err(e) => {
#[cfg(target_family = "wasm")]
{
return Err(Error::InvalidConfig(format!(
"Failed to get working directory from runtime: {}. \
On WASM, you must provide either an explicit cwd or a runtime with get_cwd() support.",
e
)));
}
#[cfg(not(target_family = "wasm"))]
{
eprintln!(
"[FOB_BUILD] Runtime get_cwd() failed: {}, falling back to std::env::current_dir()",
e
);
std::env::current_dir()?
}
}
}
} else {
#[cfg(target_family = "wasm")]
{
return Err(Error::InvalidConfig(
"No working directory available on WASM. \
You must provide either an explicit cwd via BuildOptions or a Runtime implementation.".to_string()
));
}
#[cfg(not(target_family = "wasm"))]
{
eprintln!(
"[FOB_BUILD] No runtime or explicit cwd, using std::env::current_dir()"
);
std::env::current_dir()?
}
}
}
};
let collection_plugin = Arc::new(ModuleCollectionPlugin::new());
options.input = Some(
entries
.iter()
.map(|entry| InputItem {
name: entry.name.clone(),
import: entry.import.clone(),
})
.collect(),
);
if options.cwd.is_none() {
options.cwd = cwd;
}
if !virtual_files.is_empty() {
use crate::builders::virtual_file_plugin::VirtualFilePlugin;
let virtual_plugin = VirtualFilePlugin::new(virtual_files)?;
plugins.push(Arc::new(virtual_plugin));
}
let asset_registry = Arc::new(AssetRegistry::new());
let asset_extensions = vec![
".wasm".to_string(),
".png".to_string(),
".jpg".to_string(),
".jpeg".to_string(),
".gif".to_string(),
".svg".to_string(),
".webp".to_string(),
".ico".to_string(),
".ttf".to_string(),
".woff".to_string(),
".woff2".to_string(),
];
let runtime = match runtime {
Some(rt) => rt,
None => {
#[cfg(not(target_family = "wasm"))]
{
eprintln!("[FOB_BUILD] No runtime provided, using NativeRuntime");
Arc::new(crate::NativeRuntime::new())
}
#[cfg(target_family = "wasm")]
{
return Err(Error::InvalidConfig(
"Runtime is required for asset detection plugin. \
On WASM, you must provide a Runtime implementation via BuildOptions::runtime()."
.to_string(),
));
}
}
};
let asset_plugin = AssetDetectionPlugin::new(
Arc::clone(&asset_registry),
&scan_cwd,
asset_extensions,
Arc::clone(&runtime),
);
plugins.push(Arc::new(asset_plugin));
eprintln!("[FOB_BUILD] Added asset detection plugin");
plugins.push(collection_plugin.clone());
eprintln!("[FOB_BUILD] Added module collection plugin");
let mut bundler = RolldownBundlerBuilder::default()
.with_options(options)
.with_plugins(plugins)
.build()
.map_err(|e| Error::from_rolldown_batch(&e))?;
let bundle = bundler
.generate()
.await
.map_err(|e| Error::from_rolldown_batch(&e))?;
eprintln!(
"[FOB_BUILD] Bundle complete. Asset registry has {} assets",
asset_registry.len()
);
let asset_registry_opt = if !asset_registry.is_empty() {
Some(asset_registry)
} else {
None
};
let collection_data = collection_plugin.take_data();
eprintln!(
"[FOB_BUILD] Collected {} modules",
collection_data.modules.len()
);
let graph = fob_graph::ModuleGraph::from_collected_data(collection_data).map_err(|e| {
Error::Bundler(vec![diagnostics::ExtractedDiagnostic {
kind: diagnostics::DiagnosticKind::Other("GraphBuildError".to_string()),
severity: diagnostics::DiagnosticSeverity::Error,
message: format!("Failed to build module graph from collected data: {}", e),
file: None,
line: None,
column: None,
help: None,
context: None,
}])
})?;
let stats = compute_stats(&graph)?;
let entry_points = graph.entry_points()?;
let symbol_stats = graph.symbol_statistics()?;
let analysis = AnalysisResult {
graph,
entry_points,
warnings: Vec::new(),
errors: Vec::new(),
stats,
symbol_stats,
};
let cache = CacheAnalysis::default();
let trace = if std::env::var_os("JOY_TRACE").is_some() {
Some(TransformationTrace::default())
} else {
None
};
Ok(AnalyzedBundle {
bundle,
analysis,
cache,
trace,
asset_registry: asset_registry_opt,
})
}