use itertools::Itertools;
use rayon::prelude::*;
use rspack_core::{
Compilation, CompilationOptimizeDependencies, Dependency, DependencyId, ExportMode,
ExportProvided, ExportsInfo, ExportsInfoGetter, GetUsedNameParam, ModuleGraph,
ModuleGraphConnection, ModuleIdentifier, Plugin, PrefetchExportsInfoMode, RuntimeSpec,
SideEffectsOptimizeArtifact, UsageState, UsedName, UsedNameItem,
build_module_graph::BuildModuleGraphArtifact, incremental::IncrementalPasses,
};
use rspack_error::{Diagnostic, Result};
use rspack_hook::{plugin, plugin_hook};
use rspack_util::atom::Atom;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::dependency::{ESMExportImportedSpecifierDependency, ESMImportSpecifierDependency};
fn inline_enabled(dependency_id: &DependencyId, mg: &ModuleGraph) -> bool {
let module = mg
.get_module_by_dependency_id(dependency_id)
.expect("should have target module");
module.build_info().inline_exports
}
pub fn is_export_inlined(
mg: &ModuleGraph,
module: &ModuleIdentifier,
ids: &[Atom],
runtime: Option<&RuntimeSpec>,
) -> bool {
let used_name = if ids.is_empty() {
let exports_info_used = mg.get_prefetched_exports_info_used(module, runtime);
ExportsInfoGetter::get_used_name(
GetUsedNameParam::WithoutNames(&exports_info_used),
runtime,
ids,
)
} else {
let exports_info = mg.get_prefetched_exports_info(module, PrefetchExportsInfoMode::Nested(ids));
ExportsInfoGetter::get_used_name(GetUsedNameParam::WithNames(&exports_info), runtime, ids)
};
matches!(used_name, Some(UsedName::Inlined(_)))
}
pub fn connection_active_inline_value_for_esm_import_specifier(
dependency: &ESMImportSpecifierDependency,
connection: &ModuleGraphConnection,
runtime: Option<&RuntimeSpec>,
mg: &ModuleGraph,
) -> bool {
if !inline_enabled(dependency.id(), mg) {
return true;
}
let module = connection.module_identifier();
let ids = dependency.get_ids(mg);
!is_export_inlined(mg, module, ids, runtime)
}
pub fn connection_active_inline_value_for_esm_export_imported_specifier(
dependency: &ESMExportImportedSpecifierDependency,
mode: &ExportMode,
connection: &ModuleGraphConnection,
runtime: Option<&RuntimeSpec>,
mg: &ModuleGraph,
) -> bool {
if !inline_enabled(dependency.id(), mg) {
return true;
}
let ExportMode::NormalReexport(mode) = mode else {
return true;
};
let module = connection.module_identifier();
let exports_info = mg.get_exports_info_data(module);
if exports_info.other_exports_info().get_used(runtime) != UsageState::Unused {
return true;
}
for item in &mode.items {
if item.hidden || item.checked {
return true;
}
if !is_export_inlined(mg, module, &item.ids, runtime) {
return true;
}
}
false
}
#[plugin]
#[derive(Debug, Default)]
pub struct InlineExportsPlugin;
#[plugin_hook(CompilationOptimizeDependencies for InlineExportsPlugin, stage = 100)]
async fn optimize_dependencies(
&self,
compilation: &Compilation,
_side_effect_optimize_artifact: &mut SideEffectsOptimizeArtifact,
build_module_graph_artifact: &mut BuildModuleGraphArtifact,
diagnostics: &mut Vec<Diagnostic>,
) -> Result<Option<bool>> {
if let Some(diagnostic) = compilation.incremental.disable_passes(
IncrementalPasses::MODULES_HASHES,
"InlineExportsPlugin (optimization.inlineExports = true)",
"it requires calculating the export names of all the modules, which is a global effect",
) {
diagnostics.extend(diagnostic);
}
let mg = build_module_graph_artifact.get_module_graph_mut();
let modules = mg.modules();
let mut visited: FxHashSet<ExportsInfo> = FxHashSet::default();
let mut q = modules
.keys()
.filter_map(|mid| {
let mgm = mg.module_graph_module_by_identifier(mid)?;
Some(mgm.exports)
})
.collect_vec();
while !q.is_empty() {
let items = std::mem::take(&mut q);
let batch = items
.par_iter()
.filter_map(|exports_info| {
let exports_info_data =
ExportsInfoGetter::prefetch(exports_info, mg, PrefetchExportsInfoMode::Default);
let export_list = {
if exports_info_data.other_exports_info().get_used(None) != UsageState::Unused {
return None;
}
exports_info_data
.exports()
.map(|(_, export_info_data)| {
let do_inline = !export_info_data.has_used_name()
&& export_info_data.can_inline() == Some(true)
&& matches!(export_info_data.provided(), Some(ExportProvided::Provided));
let nested_exports_info = if export_info_data.exports_info_owned() {
let used = export_info_data.get_used(None);
if used == UsageState::OnlyPropertiesUsed || used == UsageState::Unused {
export_info_data.exports_info()
} else {
None
}
} else {
None
};
(export_info_data.id(), nested_exports_info, do_inline)
})
.collect::<Vec<_>>()
};
Some((*exports_info, export_list))
})
.collect::<FxHashMap<_, _>>();
visited.extend(batch.keys());
for (_, export_list) in batch {
q.extend(
export_list
.into_iter()
.filter_map(|(export_info, nested_exports_info, do_inline)| {
if do_inline {
let data = export_info.as_data_mut(mg);
data.set_used_name(UsedNameItem::Inlined(
data
.can_inline_provide()
.expect("should have provided inline value")
.clone(),
));
}
nested_exports_info
})
.filter(|e| !visited.contains(e)),
);
}
}
Ok(None)
}
impl Plugin for InlineExportsPlugin {
fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
ctx
.compilation_hooks
.optimize_dependencies
.tap(optimize_dependencies::new(self));
Ok(())
}
}