use itertools::Itertools;
use rayon::prelude::*;
use rspack_core::{
Compilation, CompilationOptimizeDependencies, Dependency, DependencyId, ExportMode,
ExportProvided, ExportsInfo, ExportsInfoArtifact, ModuleGraph, ModuleGraphConnection,
ModuleIdentifier, Plugin, 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(
exports_info_artifact: &ExportsInfoArtifact,
module: &ModuleIdentifier,
ids: &[Atom],
runtime: Option<&RuntimeSpec>,
) -> bool {
if ids.is_empty() {
return false;
}
if ids.len() == 1 {
let export_name = &ids[0];
let exports_info = exports_info_artifact.get_exports_info_data(module);
let export_info = exports_info
.named_exports(export_name)
.unwrap_or_else(|| exports_info.other_exports_info());
return matches!(
export_info.get_used_name(Some(export_name), runtime),
Some(UsedNameItem::Inlined(_))
);
}
let exports_info = exports_info_artifact.get_exports_info_data(module);
let used_name = exports_info.get_used_name(exports_info_artifact, 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,
exports_info_artifact: &ExportsInfoArtifact,
) -> bool {
let ids = dependency.get_ids(mg);
if ids.is_empty() {
return true;
}
if !inline_enabled(dependency.id(), mg) {
return true;
}
let module = connection.module_identifier();
!is_export_inlined(exports_info_artifact, 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,
exports_info_artifact: &ExportsInfoArtifact,
) -> 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 = exports_info_artifact.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(exports_info_artifact, 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,
exports_info_artifact: &mut ExportsInfoArtifact,
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 mut visited: FxHashSet<ExportsInfo> = FxHashSet::default();
let mut q = mg
.modules_keys()
.map(|mid| exports_info_artifact.get_exports_info(mid))
.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 = exports_info.as_data(exports_info_artifact);
let export_list = {
if exports_info_data.other_exports_info().get_used(None) != UsageState::Unused {
return None;
}
exports_info_data
.exports()
.values()
.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(exports_info_artifact);
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(())
}
}