Skip to main content

rspack_plugin_javascript/plugin/
inline_exports_plugin.rs

1use 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// We put it to optimize_dependencies hook instead of optimize_code_generation hook like MangleExportsPlugin
92// because inline can affect side effects optimization (not the SideEffectsFlagPlugin does, the buildChunkGraph
93// does), buildChunkGraph can use dependency condition to determine if a dependency still active, if the dependency
94// imported export is inlined, then the dependency is inactive and will not be processed by buildChunkGraph, if a
95// module's all exports are all being inlined, then the module can be eliminated by buildChunkGraph
96#[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 there are other usage (e.g. `import { Kind } from './enum'; Kind;`) in any runtime,
134          // then we cannot inline this export.
135          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}