use rolldown::{BundlerOptions, GlobalsOutputOption, IsExternal, RawMinifyOptions, ResolveOptions};
use rustc_hash::FxHashMap;
use std::path::{Path, PathBuf};
use crate::Result;
use crate::analysis::AnalyzedBundle;
use crate::builders::common::{BundlePlan, EntrySpec, execute_bundle};
use crate::builders::unified::{BuildOptions, BuildOutput, BuildResult, EntryPoints};
#[cfg(feature = "dts-generation")]
use std::sync::Arc;
#[cfg(feature = "dts-generation")]
use crate::DtsEmitPlugin;
pub async fn execute_build(options: BuildOptions) -> Result<BuildResult> {
match (&options.entry, options.bundle, options.splitting) {
(EntryPoints::Single(_), false, _) => execute_library_build(options).await,
(EntryPoints::Multiple(_) | EntryPoints::Named(_), true, false) => {
execute_components_build(options).await
}
(EntryPoints::Multiple(_) | EntryPoints::Named(_), true, true) => {
execute_app_build(options).await
}
(EntryPoints::Single(_), true, _) => execute_single_bundle(options).await,
(EntryPoints::Multiple(_) | EntryPoints::Named(_), false, _) => {
execute_multiple_libraries(options).await
}
}
}
async fn execute_library_build(options: BuildOptions) -> Result<BuildResult> {
let entry = match &options.entry {
EntryPoints::Single(e) => e,
_ => unreachable!("execute_library_build called with non-single entry"),
};
let rolldown_options = configure_rolldown_options(&options);
#[cfg(feature = "dts-generation")]
let plugins = if let Some(dts_opts) = &options.dts {
let mut plugins = options.plugins.clone();
if let Some(plugin) = configure_dts_plugin(dts_opts, entry) {
plugins.push(plugin);
}
plugins
} else {
options.plugins.clone()
};
#[cfg(not(feature = "dts-generation"))]
let plugins = options.plugins.clone();
let plan = BundlePlan {
entries: vec![EntrySpec {
name: None,
import: entry.clone(),
}],
options: rolldown_options,
plugins,
cwd: options.cwd.clone(),
virtual_files: options.virtual_files.clone(),
runtime: options.runtime.clone(),
};
let analyzed = execute_bundle(plan).await?;
Ok(build_result_from_analyzed(analyzed, BuildOutput::Single))
}
async fn execute_components_build(options: BuildOptions) -> Result<BuildResult> {
let entries = match &options.entry {
EntryPoints::Multiple(v) => v,
EntryPoints::Named(m) => &m.values().cloned().collect::<Vec<_>>(),
_ => unreachable!("execute_components_build called with single entry"),
};
let mut bundles = FxHashMap::default();
let merged_graph = fob_graph::ModuleGraph::new()?;
let mut all_entry_points = Vec::new();
let mut all_warnings = Vec::new();
let mut all_errors = Vec::new();
let mut first_cache = None;
let mut first_trace = None;
let mut first_asset_registry = None;
for entry in entries {
let analyzed = build_single_component(&options, entry).await?;
let name = entry_to_name(entry);
let modules = analyzed.analysis.graph.modules()?;
let entry_points_set: std::collections::HashSet<_> = analyzed
.analysis
.graph
.entry_points()?
.into_iter()
.collect();
for module in modules {
let module_id = module.id.clone();
merged_graph.add_module(module)?;
if entry_points_set.contains(&module_id) {
merged_graph.add_entry_point(module_id.clone())?;
}
let deps = analyzed.analysis.graph.dependencies(&module_id)?;
for dep in deps {
merged_graph.add_dependency(module_id.clone(), dep)?;
}
}
all_entry_points.extend(analyzed.analysis.entry_points);
all_warnings.extend(analyzed.analysis.warnings);
all_errors.extend(analyzed.analysis.errors);
if first_cache.is_none() {
first_cache = Some(analyzed.cache);
}
if first_trace.is_none() {
first_trace = Some(analyzed.trace);
}
if first_asset_registry.is_none() {
first_asset_registry = analyzed.asset_registry;
}
bundles.insert(name, analyzed.bundle);
}
let stats = fob_graph::analysis::stats::compute_stats(&merged_graph)?;
let symbol_stats = merged_graph.symbol_statistics()?;
let analysis = fob_graph::AnalysisResult {
graph: merged_graph,
entry_points: all_entry_points,
warnings: all_warnings,
errors: all_errors,
stats,
symbol_stats,
};
Ok(BuildResult {
output: BuildOutput::Multiple(bundles),
analysis,
cache: first_cache.unwrap_or_default(),
trace: first_trace.unwrap_or_default(),
asset_registry: first_asset_registry,
})
}
async fn build_single_component(options: &BuildOptions, entry: &str) -> Result<AnalyzedBundle> {
let rolldown_options = configure_rolldown_options(options);
let plan = BundlePlan {
entries: vec![EntrySpec {
name: None,
import: entry.to_string(),
}],
options: rolldown_options,
plugins: options.plugins.clone(),
cwd: options.cwd.clone(),
virtual_files: options.virtual_files.clone(),
runtime: options.runtime.clone(),
};
execute_bundle(plan).await
}
async fn execute_app_build(options: BuildOptions) -> Result<BuildResult> {
let entries = match &options.entry {
EntryPoints::Multiple(v) => v
.iter()
.map(|e| EntrySpec {
name: None,
import: e.clone(),
})
.collect(),
EntryPoints::Named(m) => m
.iter()
.map(|(name, import)| EntrySpec {
name: Some(name.clone()),
import: import.clone(),
})
.collect(),
_ => unreachable!("execute_app_build called with single entry"),
};
let mut rolldown_options = configure_rolldown_options(&options);
rolldown_options.advanced_chunks = Some(rolldown::AdvancedChunksOptions {
min_size: Some(20000.0), min_share_count: Some(2), max_size: None, min_module_size: None, max_module_size: None, include_dependencies_recursively: None, groups: Some(vec![]), });
let plan = BundlePlan {
entries,
options: rolldown_options,
plugins: options.plugins.clone(),
cwd: options.cwd.clone(),
virtual_files: options.virtual_files.clone(),
runtime: options.runtime.clone(),
};
let analyzed = execute_bundle(plan).await?;
Ok(build_result_from_analyzed(analyzed, BuildOutput::Single))
}
async fn execute_single_bundle(options: BuildOptions) -> Result<BuildResult> {
let entry = match &options.entry {
EntryPoints::Single(e) => e,
_ => unreachable!("execute_single_bundle called with non-single entry"),
};
let rolldown_options = configure_rolldown_options(&options);
let plan = BundlePlan {
entries: vec![EntrySpec {
name: None,
import: entry.clone(),
}],
options: rolldown_options,
plugins: options.plugins.clone(),
cwd: options.cwd.clone(),
virtual_files: options.virtual_files.clone(),
runtime: options.runtime.clone(),
};
let analyzed = execute_bundle(plan).await?;
Ok(build_result_from_analyzed(analyzed, BuildOutput::Single))
}
async fn execute_multiple_libraries(options: BuildOptions) -> Result<BuildResult> {
execute_components_build(options).await
}
fn configure_rolldown_options(options: &BuildOptions) -> BundlerOptions {
let mut rolldown_options = BundlerOptions {
format: Some(options.format),
sourcemap: options.sourcemap,
..Default::default()
};
if options.bundle {
if !options.external.is_empty() {
rolldown_options.external = Some(IsExternal::from(options.external.clone()));
}
} else {
rolldown_options.external = Some(IsExternal::from(vec![
"^[^./]".to_string(),
]));
}
if !options.globals.is_empty() {
rolldown_options.globals = Some(GlobalsOutputOption::from(options.globals.clone()));
}
if options.minify {
rolldown_options.minify = Some(RawMinifyOptions::from(true));
}
rolldown_options.platform = Some(options.platform);
if let Some(decorator) = &options.decorator {
let transform = rolldown_common::BundlerTransformOptions {
decorator: Some(decorator.clone()),
..Default::default()
};
rolldown_options.transform = Some(transform);
}
rolldown_options.resolve = Some(configure_resolution(
options.cwd.as_ref(),
&options.path_aliases,
));
rolldown_options
}
fn configure_resolution(
cwd: Option<&PathBuf>,
path_aliases: &FxHashMap<String, String>,
) -> ResolveOptions {
let modules = if let Some(cwd_path) = cwd {
let mut paths = vec![];
let mut current = cwd_path.as_path();
loop {
paths.push(current.join("node_modules").to_string_lossy().to_string());
match current.parent() {
Some(parent) => current = parent,
None => break,
}
}
paths.push("node_modules".to_string()); paths
} else {
vec!["node_modules".to_string()]
};
let aliases = if !path_aliases.is_empty() {
Some(convert_aliases_to_absolute(path_aliases, cwd))
} else {
None
};
ResolveOptions {
alias: aliases,
main_fields: Some(vec![
"browser".to_string(),
"module".to_string(),
"main".to_string(),
]),
condition_names: Some(vec![
"browser".to_string(),
"import".to_string(),
"module".to_string(),
"default".to_string(),
]),
extensions: Some(vec![
".js".to_string(),
".json".to_string(),
".mjs".to_string(),
".ts".to_string(),
".tsx".to_string(),
".mdx".to_string(),
]),
modules: Some(modules),
symlinks: Some(true), ..Default::default()
}
}
fn convert_aliases_to_absolute(
aliases: &FxHashMap<String, String>,
cwd: Option<&PathBuf>,
) -> Vec<(String, Vec<Option<String>>)> {
let base_dir = cwd
.cloned()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
aliases
.iter()
.map(|(alias, target)| {
let target_path = Path::new(target);
let absolute_target = if target_path.is_absolute() {
target_path.to_path_buf()
} else {
base_dir.join(target_path)
};
(
alias.clone(),
vec![Some(absolute_target.to_string_lossy().to_string())],
)
})
.collect()
}
#[cfg(feature = "dts-generation")]
fn configure_dts_plugin(
dts_opts: &crate::builders::unified::DtsOptions,
entry: &str,
) -> Option<crate::SharedPluginable> {
let should_emit = dts_opts.emit.unwrap_or_else(|| is_typescript_entry(entry));
if should_emit {
let plugin = DtsEmitPlugin::new(
dts_opts.strip_internal,
dts_opts.sourcemap,
dts_opts.outdir.clone(),
);
Some(Arc::new(plugin))
} else {
None
}
}
#[cfg(feature = "dts-generation")]
fn is_typescript_entry(entry: &str) -> bool {
Path::new(entry)
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| matches!(ext, "ts" | "tsx" | "mts" | "cts"))
.unwrap_or(false)
}
fn build_result_from_analyzed<F>(analyzed: AnalyzedBundle, wrapper: F) -> BuildResult
where
F: FnOnce(crate::BundleOutput) -> BuildOutput,
{
BuildResult {
output: wrapper(analyzed.bundle),
analysis: analyzed.analysis,
cache: analyzed.cache,
trace: analyzed.trace,
asset_registry: analyzed.asset_registry,
}
}
fn entry_to_name(entry: &str) -> String {
Path::new(entry)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("index")
.to_string()
}