rspack_plugin_library 0.100.1

rspack library plugin
Documentation
use std::hash::Hash;

use rspack_core::{
  ChunkUkey, Compilation, CompilationParams, CompilerCompilation, ExportProvided, ExportsType,
  LibraryOptions, ModuleGraph, ModuleIdentifier, Plugin, RuntimeCodeTemplate, RuntimeVariable,
  UsedNameItem, property_access,
  rspack_sources::{ConcatSource, RawStringSource, SourceExt},
  to_identifier, to_module_export_name,
};
use rspack_error::{Result, error_bail};
use rspack_hash::RspackHash;
use rspack_hook::{plugin, plugin_hook};
use rspack_plugin_javascript::{
  JavascriptModulesChunkHash, JavascriptModulesRenderStartup, JsPlugin, RenderSource,
};

use crate::utils::{COMMON_LIBRARY_NAME_MESSAGE, get_options_for_chunk};

const PLUGIN_NAME: &str = "rspack.ModuleLibraryPlugin";

#[plugin]
#[derive(Debug, Default)]
pub struct ModuleLibraryPlugin;

impl ModuleLibraryPlugin {
  fn parse_options(&self, library: &LibraryOptions) -> Result<()> {
    if library.name.is_some() {
      error_bail!("Library name must be unset. {COMMON_LIBRARY_NAME_MESSAGE}")
    }
    Ok(())
  }

  fn get_options_for_chunk(
    &self,
    compilation: &Compilation,
    chunk_ukey: &ChunkUkey,
  ) -> Result<Option<()>> {
    get_options_for_chunk(compilation, chunk_ukey)
      .filter(|library| library.library_type == "module")
      .map(|library| self.parse_options(library))
      .transpose()
  }
}

#[plugin_hook(CompilerCompilation for ModuleLibraryPlugin)]
async fn compilation(
  &self,
  compilation: &mut Compilation,
  _params: &mut CompilationParams,
) -> Result<()> {
  let hooks = JsPlugin::get_compilation_hooks_mut(compilation.id());
  let mut hooks = hooks.write().await;
  hooks.render_startup.tap(render_startup::new(self));
  hooks.chunk_hash.tap(js_chunk_hash::new(self));
  Ok(())
}

#[plugin_hook(JavascriptModulesRenderStartup for ModuleLibraryPlugin)]
async fn render_startup(
  &self,
  compilation: &Compilation,
  chunk_ukey: &ChunkUkey,
  module: &ModuleIdentifier,
  render_source: &mut RenderSource,
  runtime_template: &RuntimeCodeTemplate<'_>,
) -> Result<()> {
  let Some(_) = self.get_options_for_chunk(compilation, chunk_ukey)? else {
    return Ok(());
  };
  let exports_name = runtime_template.render_runtime_variable(&RuntimeVariable::Exports);
  let mut source = ConcatSource::default();
  let is_async = ModuleGraph::is_async(&compilation.async_modules_artifact, module);
  let module_graph = compilation.get_module_graph();
  source.add(render_source.source.clone());
  // export { local as exported }
  let mut exports: Vec<(String, Option<String>)> = vec![];
  if is_async {
    source.add(RawStringSource::from(format!(
      "{exports_name} = await {exports_name};\n"
    )));
  }
  let exports_info = compilation
    .exports_info_artifact
    .get_exports_info_data(module);
  let boxed_module = module_graph
    .module_by_identifier(module)
    .expect("should have build meta");
  let exports_type = boxed_module.get_exports_type(
    module_graph,
    &compilation.module_graph_cache_artifact,
    &compilation.exports_info_artifact,
    boxed_module.build_info().strict,
  );
  for export_info in exports_info.exports().values() {
    if matches!(export_info.provided(), Some(ExportProvided::NotProvided)) {
      continue;
    };

    let chunk = compilation
      .build_chunk_graph_artifact
      .chunk_by_ukey
      .expect_get(chunk_ukey);
    let info_name = export_info.name().expect("should have name");
    let used_name = export_info
      .get_used_name(Some(info_name), Some(chunk.runtime()))
      .expect("name can't be empty");
    let var_name = format!("{exports_name}{}", to_identifier(info_name));

    if info_name == "default"
      && matches!(
        exports_type,
        ExportsType::DefaultOnly | ExportsType::DefaultWithNamed | ExportsType::Dynamic
      )
    {
      source.add(RawStringSource::from(format!(
        "var {var_name} = {exports_name};\n",
      )));
    } else {
      source.add(RawStringSource::from(format!(
        "var {var_name} = {};\n",
        match used_name {
          UsedNameItem::Str(used_name) =>
            format!("{exports_name}{}", property_access(vec![used_name], 0)),
          UsedNameItem::Inlined(inlined) => inlined.render(""),
        }
      )));
    }

    exports.push((var_name, Some(to_module_export_name(info_name))))
  }
  if !exports.is_empty() {
    let exports_string = match exports_type {
      ExportsType::DefaultOnly => render_as_default_only_export(&exports),
      ExportsType::DefaultWithNamed | ExportsType::Dynamic => {
        render_as_default_with_named_exports(&exports)
      }
      ExportsType::Namespace => render_as_named_exports(&exports),
    };
    source.add(RawStringSource::from(exports_string));
  }
  render_source.source = source.boxed();
  Ok(())
}

#[plugin_hook(JavascriptModulesChunkHash for ModuleLibraryPlugin)]
async fn js_chunk_hash(
  &self,
  compilation: &Compilation,
  chunk_ukey: &ChunkUkey,
  hasher: &mut RspackHash,
) -> Result<()> {
  let Some(_) = self.get_options_for_chunk(compilation, chunk_ukey)? else {
    return Ok(());
  };
  PLUGIN_NAME.hash(hasher);
  Ok(())
}

impl Plugin for ModuleLibraryPlugin {
  fn name(&self) -> &'static str {
    PLUGIN_NAME
  }

  fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
    ctx.compiler_hooks.compilation.tap(compilation::new(self));
    Ok(())
  }
}

pub fn render_as_default_only_export(exports: &[(String, Option<String>)]) -> String {
  render_as_default_export_impl(exports)
}

pub fn render_as_named_exports(exports: &[(String, Option<String>)]) -> String {
  render_as_named_exports_impl(exports, false)
}

pub fn render_as_default_with_named_exports(exports: &[(String, Option<String>)]) -> String {
  format!(
    "{}\n{}",
    render_as_named_exports_impl(exports, true),
    render_as_default_only_export(exports),
  )
}

fn render_as_named_exports_impl(
  exports: &[(String, Option<String>)],
  ignore_default: bool,
) -> String {
  format!(
    "export {{ {} }};\n",
    exports
      .iter()
      .filter(|(_, exported)| {
        if ignore_default {
          !matches!(exported.as_deref(), Some("default"))
        } else {
          true
        }
      })
      .map(|(local, exported)| {
        if let Some(exported) = exported {
          format!("{local} as {exported}")
        } else {
          local.clone()
        }
      })
      .collect::<Vec<_>>()
      .join(", ")
  )
}

fn render_as_default_export_impl(exports: &[(String, Option<String>)]) -> String {
  if let Some((local, _)) = exports
    .iter()
    .find(|(_, exported)| matches!(exported.as_deref(), Some("default")))
  {
    return format!("export {{ {local} as default }};\n",);
  }

  format!(
    "var __rspack_exports_default = {{ {} }};\nexport default __rspack_exports_default;\n",
    exports
      .iter()
      .map(|(local, exported)| {
        if let Some(exported) = exported {
          format!("{exported}: {local}")
        } else {
          local.clone()
        }
      })
      .collect::<Vec<_>>()
      .join(", ")
  )
}