rspack_plugin_javascript 0.100.1

rspack javascript plugin
Documentation
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;

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