Skip to main content

rspack_plugin_library/
amd_library_plugin.rs

1use std::hash::Hash;
2
3use rspack_core::{
4  ChunkUkey, Compilation, CompilationAdditionalChunkRuntimeRequirements, CompilationParams,
5  CompilerCompilation, ExternalModule, Filename, LibraryName, LibraryNonUmdObject, LibraryOptions,
6  LibraryType, PathData, Plugin, RuntimeCodeTemplate, RuntimeGlobals, RuntimeModule, SourceType,
7  rspack_sources::{ConcatSource, RawStringSource, SourceExt},
8};
9use rspack_error::{Result, error_bail};
10use rspack_hash::RspackHash;
11use rspack_hook::{plugin, plugin_hook};
12use rspack_plugin_javascript::{
13  JavascriptModulesChunkHash, JavascriptModulesRender, JsPlugin, RenderSource,
14};
15
16use crate::utils::{
17  COMMON_LIBRARY_NAME_MESSAGE, external_arguments, externals_dep_array, get_options_for_chunk,
18};
19
20const PLUGIN_NAME: &str = "rspack.AmdLibraryPlugin";
21
22#[derive(Debug)]
23struct AmdLibraryPluginParsed<'a> {
24  name: Option<&'a str>,
25  amd_container: Option<&'a str>,
26}
27
28#[plugin]
29#[derive(Debug)]
30pub struct AmdLibraryPlugin {
31  require_as_wrapper: bool,
32  library_type: LibraryType,
33}
34
35impl AmdLibraryPlugin {
36  pub fn new(require_as_wrapper: bool, library_type: LibraryType) -> Self {
37    Self::new_inner(require_as_wrapper, library_type)
38  }
39
40  fn parse_options<'a>(&self, library: &'a LibraryOptions) -> Result<AmdLibraryPluginParsed<'a>> {
41    if self.require_as_wrapper {
42      if library.name.is_some() {
43        error_bail!("AMD library name must be unset. {COMMON_LIBRARY_NAME_MESSAGE}")
44      }
45    } else if let Some(name) = &library.name
46      && !matches!(
47        name,
48        LibraryName::NonUmdObject(LibraryNonUmdObject::String(_))
49      )
50    {
51      error_bail!(
52        "AMD library name must be a simple string or unset. {COMMON_LIBRARY_NAME_MESSAGE}"
53      )
54    }
55    Ok(AmdLibraryPluginParsed {
56      name: library.name.as_ref().map(|name| match name {
57        LibraryName::NonUmdObject(LibraryNonUmdObject::String(s)) => s.as_str(),
58        _ => unreachable!("AMD library name must be a simple string or unset."),
59      }),
60      amd_container: library.amd_container.as_deref(),
61    })
62  }
63
64  fn get_options_for_chunk<'a>(
65    &self,
66    compilation: &'a Compilation,
67    chunk_ukey: &'a ChunkUkey,
68  ) -> Result<Option<AmdLibraryPluginParsed<'a>>> {
69    get_options_for_chunk(compilation, chunk_ukey)
70      .filter(|library| library.library_type == self.library_type)
71      .map(|library| self.parse_options(library))
72      .transpose()
73  }
74}
75
76#[plugin_hook(CompilerCompilation for AmdLibraryPlugin)]
77async fn compilation(
78  &self,
79  compilation: &mut Compilation,
80  _params: &mut CompilationParams,
81) -> Result<()> {
82  let hooks = JsPlugin::get_compilation_hooks_mut(compilation.id());
83  let mut hooks = hooks.write().await;
84  hooks.render.tap(render::new(self));
85  hooks.chunk_hash.tap(js_chunk_hash::new(self));
86  Ok(())
87}
88
89#[plugin_hook(JavascriptModulesRender for AmdLibraryPlugin)]
90async fn render(
91  &self,
92  compilation: &Compilation,
93  chunk_ukey: &ChunkUkey,
94  render_source: &mut RenderSource,
95  _runtime_template: &RuntimeCodeTemplate<'_>,
96) -> Result<()> {
97  let Some(options) = self.get_options_for_chunk(compilation, chunk_ukey)? else {
98    return Ok(());
99  };
100  let chunk = compilation
101    .build_chunk_graph_artifact
102    .chunk_by_ukey
103    .expect_get(chunk_ukey);
104  let module_graph = compilation.get_module_graph();
105  let modules = compilation
106    .build_chunk_graph_artifact
107    .chunk_graph
108    .get_chunk_modules_identifier(chunk_ukey)
109    .iter()
110    .filter_map(|identifier| {
111      module_graph
112        .module_by_identifier(identifier)
113        .and_then(|module| module.as_external_module())
114        .and_then(|m| {
115          let ty = m.get_external_type();
116          (ty == "amd" || ty == "amd-require").then_some(m)
117        })
118    })
119    .collect::<Vec<&ExternalModule>>();
120  let externals_deps_array = externals_dep_array(&modules)?;
121  let external_arguments = external_arguments(&modules, compilation);
122  let mut fn_start = format!("function({external_arguments}){{\n");
123  if compilation.options.output.iife
124    || !chunk.has_runtime(&compilation.build_chunk_graph_artifact.chunk_group_by_ukey)
125  {
126    fn_start.push_str(" return ");
127  }
128  let mut source = ConcatSource::default();
129  let amd_container_prefix = options
130    .amd_container
131    .map(|c| format!("{c}."))
132    .unwrap_or_default();
133  if self.require_as_wrapper {
134    source.add(RawStringSource::from(format!(
135      "{amd_container_prefix}require({externals_deps_array}, {fn_start}"
136    )));
137  } else if let Some(name) = options.name {
138    let normalize_name = compilation
139      .get_path(
140        &Filename::from(name),
141        PathData::default()
142          .chunk_id_optional(chunk.id().map(|id| id.as_str()))
143          .chunk_hash_optional(chunk.rendered_hash(
144            &compilation.chunk_hashes_artifact,
145            compilation.options.output.hash_digest_length,
146          ))
147          .chunk_name_optional(chunk.name_for_filename_template())
148          .content_hash_optional(chunk.rendered_content_hash_by_source_type(
149            &compilation.chunk_hashes_artifact,
150            &SourceType::JavaScript,
151            compilation.options.output.hash_digest_length,
152          )),
153      )
154      .await?;
155    source.add(RawStringSource::from(format!(
156      "{amd_container_prefix}define('{normalize_name}', {externals_deps_array}, {fn_start}"
157    )));
158  } else if modules.is_empty() {
159    source.add(RawStringSource::from(format!(
160      "{amd_container_prefix}define({fn_start}"
161    )));
162  } else {
163    source.add(RawStringSource::from(format!(
164      "{amd_container_prefix}define({externals_deps_array}, {fn_start}"
165    )));
166  }
167  source.add(render_source.source.clone());
168  source.add(RawStringSource::from_static("\n})"));
169  render_source.source = source.boxed();
170  Ok(())
171}
172
173#[plugin_hook(JavascriptModulesChunkHash for AmdLibraryPlugin)]
174async fn js_chunk_hash(
175  &self,
176  compilation: &Compilation,
177  chunk_ukey: &ChunkUkey,
178  hasher: &mut RspackHash,
179) -> Result<()> {
180  let Some(options) = self.get_options_for_chunk(compilation, chunk_ukey)? else {
181    return Ok(());
182  };
183  PLUGIN_NAME.hash(hasher);
184  if self.require_as_wrapper {
185    self.require_as_wrapper.hash(hasher);
186  } else if let Some(name) = options.name {
187    "named".hash(hasher);
188    name.hash(hasher);
189  } else if let Some(amd_container) = options.amd_container {
190    "amdContainer".hash(hasher);
191    amd_container.hash(hasher);
192  }
193  Ok(())
194}
195
196#[plugin_hook(CompilationAdditionalChunkRuntimeRequirements for AmdLibraryPlugin)]
197async fn additional_chunk_runtime_requirements(
198  &self,
199  compilation: &Compilation,
200  chunk_ukey: &ChunkUkey,
201  runtime_requirements: &mut RuntimeGlobals,
202  _runtime_modules: &mut Vec<Box<dyn RuntimeModule>>,
203) -> Result<()> {
204  if self
205    .get_options_for_chunk(compilation, chunk_ukey)?
206    .is_none()
207  {
208    return Ok(());
209  }
210  runtime_requirements.insert(RuntimeGlobals::RETURN_EXPORTS_FROM_RUNTIME);
211  Ok(())
212}
213
214impl Plugin for AmdLibraryPlugin {
215  fn name(&self) -> &'static str {
216    PLUGIN_NAME
217  }
218
219  fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
220    ctx.compiler_hooks.compilation.tap(compilation::new(self));
221    ctx
222      .compilation_hooks
223      .additional_chunk_runtime_requirements
224      .tap(additional_chunk_runtime_requirements::new(self));
225    Ok(())
226  }
227}