rspack_plugin_javascript/plugin/
inline_exports_plugin.rs1use itertools::Itertools;
2use rayon::prelude::*;
3use rspack_core::{
4 Compilation, CompilationOptimizeDependencies, Dependency, DependencyId, ExportMode,
5 ExportProvided, ExportsInfo, ExportsInfoGetter, GetUsedNameParam, ModuleGraph,
6 ModuleGraphConnection, ModuleIdentifier, Plugin, PrefetchExportsInfoMode, RuntimeSpec,
7 SideEffectsOptimizeArtifact, UsageState, UsedName, UsedNameItem,
8 build_module_graph::BuildModuleGraphArtifact, incremental::IncrementalPasses,
9};
10use rspack_error::{Diagnostic, Result};
11use rspack_hook::{plugin, plugin_hook};
12use rspack_util::atom::Atom;
13use rustc_hash::{FxHashMap, FxHashSet};
14
15use crate::dependency::{ESMExportImportedSpecifierDependency, ESMImportSpecifierDependency};
16
17fn inline_enabled(dependency_id: &DependencyId, mg: &ModuleGraph) -> bool {
18 let module = mg
19 .get_module_by_dependency_id(dependency_id)
20 .expect("should have target module");
21 module.build_info().inline_exports
22}
23
24pub fn is_export_inlined(
25 mg: &ModuleGraph,
26 module: &ModuleIdentifier,
27 ids: &[Atom],
28 runtime: Option<&RuntimeSpec>,
29) -> bool {
30 let used_name = if ids.is_empty() {
31 let exports_info_used = mg.get_prefetched_exports_info_used(module, runtime);
32 ExportsInfoGetter::get_used_name(
33 GetUsedNameParam::WithoutNames(&exports_info_used),
34 runtime,
35 ids,
36 )
37 } else {
38 let exports_info = mg.get_prefetched_exports_info(module, PrefetchExportsInfoMode::Nested(ids));
39 ExportsInfoGetter::get_used_name(GetUsedNameParam::WithNames(&exports_info), runtime, ids)
40 };
41 matches!(used_name, Some(UsedName::Inlined(_)))
42}
43
44pub fn connection_active_inline_value_for_esm_import_specifier(
45 dependency: &ESMImportSpecifierDependency,
46 connection: &ModuleGraphConnection,
47 runtime: Option<&RuntimeSpec>,
48 mg: &ModuleGraph,
49) -> bool {
50 if !inline_enabled(dependency.id(), mg) {
51 return true;
52 }
53 let module = connection.module_identifier();
54 let ids = dependency.get_ids(mg);
55 !is_export_inlined(mg, module, ids, runtime)
56}
57
58pub fn connection_active_inline_value_for_esm_export_imported_specifier(
59 dependency: &ESMExportImportedSpecifierDependency,
60 mode: &ExportMode,
61 connection: &ModuleGraphConnection,
62 runtime: Option<&RuntimeSpec>,
63 mg: &ModuleGraph,
64) -> bool {
65 if !inline_enabled(dependency.id(), mg) {
66 return true;
67 }
68 let ExportMode::NormalReexport(mode) = mode else {
69 return true;
70 };
71 let module = connection.module_identifier();
72 let exports_info = mg.get_exports_info_data(module);
73 if exports_info.other_exports_info().get_used(runtime) != UsageState::Unused {
74 return true;
75 }
76 for item in &mode.items {
77 if item.hidden || item.checked {
78 return true;
79 }
80 if !is_export_inlined(mg, module, &item.ids, runtime) {
81 return true;
82 }
83 }
84 false
85}
86
87#[plugin]
88#[derive(Debug, Default)]
89pub struct InlineExportsPlugin;
90
91#[plugin_hook(CompilationOptimizeDependencies for InlineExportsPlugin, stage = 100)]
97async fn optimize_dependencies(
98 &self,
99 compilation: &Compilation,
100 _side_effect_optimize_artifact: &mut SideEffectsOptimizeArtifact,
101 build_module_graph_artifact: &mut BuildModuleGraphArtifact,
102 diagnostics: &mut Vec<Diagnostic>,
103) -> Result<Option<bool>> {
104 if let Some(diagnostic) = compilation.incremental.disable_passes(
105 IncrementalPasses::MODULES_HASHES,
106 "InlineExportsPlugin (optimization.inlineExports = true)",
107 "it requires calculating the export names of all the modules, which is a global effect",
108 ) {
109 diagnostics.extend(diagnostic);
110 }
111
112 let mg = build_module_graph_artifact.get_module_graph_mut();
113 let modules = mg.modules();
114
115 let mut visited: FxHashSet<ExportsInfo> = FxHashSet::default();
116
117 let mut q = modules
118 .keys()
119 .filter_map(|mid| {
120 let mgm = mg.module_graph_module_by_identifier(mid)?;
121 Some(mgm.exports)
122 })
123 .collect_vec();
124
125 while !q.is_empty() {
126 let items = std::mem::take(&mut q);
127 let batch = items
128 .par_iter()
129 .filter_map(|exports_info| {
130 let exports_info_data =
131 ExportsInfoGetter::prefetch(exports_info, mg, PrefetchExportsInfoMode::Default);
132 let export_list = {
133 if exports_info_data.other_exports_info().get_used(None) != UsageState::Unused {
136 return None;
137 }
138 exports_info_data
139 .exports()
140 .map(|(_, export_info_data)| {
141 let do_inline = !export_info_data.has_used_name()
142 && export_info_data.can_inline() == Some(true)
143 && matches!(export_info_data.provided(), Some(ExportProvided::Provided));
144
145 let nested_exports_info = if export_info_data.exports_info_owned() {
146 let used = export_info_data.get_used(None);
147 if used == UsageState::OnlyPropertiesUsed || used == UsageState::Unused {
148 export_info_data.exports_info()
149 } else {
150 None
151 }
152 } else {
153 None
154 };
155 (export_info_data.id(), nested_exports_info, do_inline)
156 })
157 .collect::<Vec<_>>()
158 };
159
160 Some((*exports_info, export_list))
161 })
162 .collect::<FxHashMap<_, _>>();
163
164 visited.extend(batch.keys());
165 for (_, export_list) in batch {
166 q.extend(
167 export_list
168 .into_iter()
169 .filter_map(|(export_info, nested_exports_info, do_inline)| {
170 if do_inline {
171 let data = export_info.as_data_mut(mg);
172 data.set_used_name(UsedNameItem::Inlined(
173 data
174 .can_inline_provide()
175 .expect("should have provided inline value")
176 .clone(),
177 ));
178 }
179 nested_exports_info
180 })
181 .filter(|e| !visited.contains(e)),
182 );
183 }
184 }
185
186 Ok(None)
187}
188
189impl Plugin for InlineExportsPlugin {
190 fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
191 ctx
192 .compilation_hooks
193 .optimize_dependencies
194 .tap(optimize_dependencies::new(self));
195 Ok(())
196 }
197}