Skip to main content

rspack_plugin_library/
assign_library_plugin.rs

1use std::sync::LazyLock;
2
3use futures::future::join_all;
4use regex::Regex;
5use rspack_core::{
6  AsyncModulesArtifact, BoxModule, CanInlineUse, Chunk, ChunkUkey,
7  CodeGenerationDataTopLevelDeclarations, Compilation,
8  CompilationAdditionalChunkRuntimeRequirements, CompilationFinishModules, CompilationParams,
9  CompilerCompilation, EntryData, ExportProvided, ExportsInfoArtifact, Filename, LibraryExport,
10  LibraryName, LibraryNonUmdObject, LibraryOptions, ModuleIdentifier, PathData, Plugin,
11  RuntimeCodeTemplate, RuntimeGlobals, RuntimeModule, RuntimeVariable, SideEffectsStateArtifact,
12  SourceType, UsageState, get_entry_runtime, property_access,
13  rspack_sources::{ConcatSource, RawStringSource, SourceExt},
14  to_identifier,
15};
16use rspack_error::{Result, ToStringResultToRspackResultExt, error, error_bail};
17use rspack_hash::{RspackHash, RspackHasher};
18use rspack_hook::{plugin, plugin_hook};
19use rspack_plugin_javascript::{
20  JavascriptModulesChunkHash, JavascriptModulesEmbedInRuntimeBailout, JavascriptModulesRender,
21  JavascriptModulesRenderStartup, JavascriptModulesStrictRuntimeBailout, JsPlugin, RenderSource,
22};
23use swc_core::atoms::Atom;
24
25use crate::utils::{COMMON_LIBRARY_NAME_MESSAGE, get_options_for_chunk};
26
27const PLUGIN_NAME: &str = "rspack.AssignLibraryPlugin";
28
29#[derive(Debug)]
30pub enum Unnamed {
31  Error,
32  Static,
33  Copy,
34  Assign,
35}
36
37#[derive(Debug)]
38pub enum Named {
39  Copy,
40  Assign,
41}
42
43#[derive(Debug)]
44pub enum Prefix {
45  Global,
46  Array(Vec<String>),
47}
48
49impl Prefix {
50  pub fn value(&self, compilation: &Compilation) -> Vec<String> {
51    match self {
52      Prefix::Global => vec![compilation.options.output.global_object.clone()],
53      Prefix::Array(v) => v.clone(),
54    }
55  }
56
57  pub fn len(&self) -> usize {
58    match self {
59      Prefix::Global => 1,
60      Prefix::Array(v) => v.len(),
61    }
62  }
63
64  pub fn is_empty(&self) -> bool {
65    self.len() == 0
66  }
67}
68
69#[derive(Debug)]
70pub struct AssignLibraryPluginOptions {
71  pub library_type: String,
72  pub prefix: Prefix,
73  pub declare: bool,
74  pub unnamed: Unnamed,
75  pub named: Option<Named>,
76}
77
78#[derive(Debug)]
79struct AssignLibraryPluginParsed<'a> {
80  name: Option<&'a LibraryNonUmdObject>,
81  export: Option<&'a LibraryExport>,
82}
83
84#[plugin]
85#[derive(Debug)]
86pub struct AssignLibraryPlugin {
87  options: AssignLibraryPluginOptions,
88}
89
90impl AssignLibraryPlugin {
91  pub fn new(options: AssignLibraryPluginOptions) -> Self {
92    Self::new_inner(options)
93  }
94
95  fn parse_options<'a>(
96    &self,
97    library: &'a LibraryOptions,
98  ) -> Result<AssignLibraryPluginParsed<'a>> {
99    if matches!(self.options.unnamed, Unnamed::Error) {
100      if !matches!(
101        library.name,
102        Some(LibraryName::NonUmdObject(
103          LibraryNonUmdObject::Array(_) | LibraryNonUmdObject::String(_)
104        ))
105      ) {
106        error_bail!("Library name must be a string or string array. {COMMON_LIBRARY_NAME_MESSAGE}")
107      }
108    } else if let Some(name) = &library.name
109      && !matches!(
110        name,
111        LibraryName::NonUmdObject(LibraryNonUmdObject::Array(_) | LibraryNonUmdObject::String(_))
112      )
113    {
114      error_bail!(
115        "Library name must be a string, string array or unset. {COMMON_LIBRARY_NAME_MESSAGE}"
116      )
117    }
118    Ok(AssignLibraryPluginParsed {
119      name: library.name.as_ref().map(|n| match n {
120        LibraryName::NonUmdObject(n) => n,
121        _ => unreachable!("Library name must be a string, string array or unset."),
122      }),
123      export: library.export.as_ref(),
124    })
125  }
126
127  fn get_options_for_chunk<'a>(
128    &self,
129    compilation: &'a Compilation,
130    chunk_ukey: &ChunkUkey,
131  ) -> Result<Option<AssignLibraryPluginParsed<'a>>> {
132    get_options_for_chunk(compilation, chunk_ukey)
133      .filter(|library| library.library_type == self.options.library_type)
134      .map(|library| self.parse_options(library))
135      .transpose()
136  }
137
138  fn is_copy(&self, options: &AssignLibraryPluginParsed) -> bool {
139    if options.name.is_some() {
140      matches!(self.options.named, Some(Named::Copy))
141    } else {
142      matches!(self.options.unnamed, Unnamed::Copy)
143    }
144  }
145
146  async fn get_resolved_full_name(
147    &self,
148    options: &AssignLibraryPluginParsed<'_>,
149    compilation: &Compilation,
150    chunk: &Chunk,
151  ) -> Result<Vec<String>> {
152    if let Some(name) = options.name {
153      let mut prefix = self.options.prefix.value(compilation);
154      let get_path = async |v: &str| {
155        compilation
156          .get_path(
157            &Filename::from(v),
158            PathData::default()
159              .chunk(chunk.ukey(), compilation)
160              .chunk_id_optional(chunk.id().map(|id| id.as_str()))
161              .chunk_hash_optional(chunk.rendered_hash(
162                &compilation.chunk_hashes_artifact,
163                compilation.options.output.hash_digest_length,
164              ))
165              .chunk_name_optional(chunk.name_for_filename_template())
166              .content_hash_optional(chunk.rendered_content_hash_by_source_type(
167                &compilation.chunk_hashes_artifact,
168                &SourceType::JavaScript,
169                compilation.options.output.hash_digest_length,
170              )),
171          )
172          .await
173      };
174      match name {
175        LibraryNonUmdObject::Array(arr) => {
176          let paths = join_all(arr.iter().map(|s| get_path(s)))
177            .await
178            .into_iter()
179            .collect::<Result<Vec<_>>>()?;
180          prefix.extend(paths);
181        }
182        LibraryNonUmdObject::String(s) => prefix.push(get_path(s).await?),
183      };
184      Ok(prefix)
185    } else {
186      Ok(self.options.prefix.value(compilation))
187    }
188  }
189}
190
191#[plugin_hook(CompilerCompilation for AssignLibraryPlugin)]
192async fn compilation(
193  &self,
194  compilation: &mut Compilation,
195  _params: &mut CompilationParams,
196) -> Result<()> {
197  let hooks = JsPlugin::get_compilation_hooks_mut(compilation.id());
198  let mut hooks = hooks.write().await;
199  hooks.render.tap(render::new(self));
200  hooks.render_startup.tap(render_startup::new(self));
201  hooks.chunk_hash.tap(js_chunk_hash::new(self));
202  hooks
203    .embed_in_runtime_bailout
204    .tap(embed_in_runtime_bailout::new(self));
205  hooks
206    .strict_runtime_bailout
207    .tap(strict_runtime_bailout::new(self));
208  Ok(())
209}
210
211#[plugin_hook(JavascriptModulesRender for AssignLibraryPlugin)]
212async fn render(
213  &self,
214  compilation: &Compilation,
215  chunk_ukey: &ChunkUkey,
216  render_source: &mut RenderSource,
217  _runtime_template: &RuntimeCodeTemplate,
218) -> Result<()> {
219  let Some(options) = self.get_options_for_chunk(compilation, chunk_ukey)? else {
220    return Ok(());
221  };
222  if self.options.declare {
223    let chunk = compilation
224      .build_chunk_graph_artifact
225      .chunk_by_ukey
226      .expect_get(chunk_ukey);
227    let base = &self
228      .get_resolved_full_name(&options, compilation, chunk)
229      .await?[0];
230    if !is_name_valid(base) {
231      let base_identifier = to_identifier(base);
232      return Err(error!(
233        "Library name base ({base}) must be a valid identifier when using a var declaring library type. Either use a valid identifier (e. g. {base_identifier}) or use a different library type (e. g. `type: 'global'`, which assign a property on the global scope instead of declaring a variable). {COMMON_LIBRARY_NAME_MESSAGE}"
234      ));
235    }
236    let mut source = ConcatSource::default();
237    source.add(RawStringSource::from(format!("var {base};\n")));
238    source.add(render_source.source.clone());
239    render_source.source = source.boxed();
240    return Ok(());
241  }
242  Ok(())
243}
244
245#[plugin_hook(JavascriptModulesRenderStartup for AssignLibraryPlugin)]
246async fn render_startup(
247  &self,
248  compilation: &Compilation,
249  chunk_ukey: &ChunkUkey,
250  module: &ModuleIdentifier,
251  render_source: &mut RenderSource,
252  runtime_template: &RuntimeCodeTemplate,
253) -> Result<()> {
254  let Some(options) = self.get_options_for_chunk(compilation, chunk_ukey)? else {
255    return Ok(());
256  };
257  let mut source = ConcatSource::default();
258  source.add(render_source.source.clone());
259  let chunk = compilation
260    .build_chunk_graph_artifact
261    .chunk_by_ukey
262    .expect_get(chunk_ukey);
263  let full_name_resolved = self
264    .get_resolved_full_name(&options, compilation, chunk)
265    .await?;
266  let export_access = options
267    .export
268    .map(|e| property_access(e, 0))
269    .unwrap_or_default();
270  let exports_name = runtime_template.render_runtime_variable(&RuntimeVariable::Exports);
271  if matches!(self.options.unnamed, Unnamed::Static) {
272    let export_target = access_with_init(&full_name_resolved, self.options.prefix.len(), true);
273    let exports_info = compilation
274      .exports_info_artifact
275      .get_exports_info_data(module);
276    let mut provided = vec![];
277    let exports_name = runtime_template.render_runtime_variable(&RuntimeVariable::Exports);
278    for export_info in exports_info.exports().values() {
279      if matches!(export_info.provided(), Some(ExportProvided::NotProvided)) {
280        continue;
281      }
282      let export_info_name = export_info.name().expect("should have name").to_string();
283      provided.push(export_info_name.clone());
284      let name_access = property_access([export_info_name], 0);
285      source.add(RawStringSource::from(format!(
286        "{export_target}{name_access} = {exports_name}{export_access}{name_access};\n"
287      )));
288    }
289
290    let mut exports = exports_name.as_str();
291    let exports_assign_export = "__rspack_exports_export";
292    if !export_access.is_empty() {
293      source.add(RawStringSource::from(format!(
294        "var {exports_assign_export} = {exports_name}{export_access};\n"
295      )));
296      exports = exports_assign_export;
297    }
298    source.add(RawStringSource::from(format!(
299      "for(var __rspack_i in {exports}) {{\n"
300    )));
301    let has_provided = !provided.is_empty();
302    if has_provided {
303      source.add(RawStringSource::from(format!(
304        "  if({}.indexOf(__rspack_i) === -1) {{\n",
305        simd_json::to_string(&provided).to_rspack_result()?
306      )));
307    }
308    source.add(RawStringSource::from(format!(
309      "{}  {export_target}[__rspack_i] = {exports}[__rspack_i];\n",
310      match has_provided {
311        true => "  ",
312        false => "",
313      }
314    )));
315
316    source.add(RawStringSource::from(if has_provided {
317      "  }\n}\n"
318    } else {
319      "}\n"
320    }));
321
322    source.add(RawStringSource::from(format!(
323      "Object.defineProperty({export_target}, '__esModule', {{ value: true }});\n",
324    )));
325  } else if self.is_copy(&options) {
326    let exports_assign = "__rspack_exports_target";
327    source.add(RawStringSource::from(format!(
328      "var {exports_assign} = {};\n",
329      access_with_init(&full_name_resolved, self.options.prefix.len(), true)
330    )));
331    let mut exports = exports_name.as_str();
332    let exports_assign_export = "__rspack_exports_export";
333    if !export_access.is_empty() {
334      source.add(RawStringSource::from(format!(
335        "var {exports_assign_export} = {exports_name}{export_access};\n"
336      )));
337      exports = exports_assign_export;
338    }
339    source.add(RawStringSource::from(format!(
340      "for(var __rspack_i in {exports}) {exports_assign}[__rspack_i] = {exports}[__rspack_i];\n"
341    )));
342    source.add(RawStringSource::from(format!(
343      "if({exports}.__esModule) Object.defineProperty({exports_assign}, '__esModule', {{ value: true }});\n"
344    )));
345  } else {
346    source.add(RawStringSource::from(format!(
347      "{} = {exports_name}{export_access};\n",
348      access_with_init(&full_name_resolved, self.options.prefix.len(), false)
349    )));
350  }
351  render_source.source = source.boxed();
352  Ok(())
353}
354
355#[plugin_hook(JavascriptModulesChunkHash for AssignLibraryPlugin)]
356async fn js_chunk_hash(
357  &self,
358  compilation: &Compilation,
359  chunk_ukey: &ChunkUkey,
360  hasher: &mut RspackHasher,
361) -> Result<()> {
362  let Some(options) = self.get_options_for_chunk(compilation, chunk_ukey)? else {
363    return Ok(());
364  };
365  PLUGIN_NAME.hash(hasher);
366  let chunk = compilation
367    .build_chunk_graph_artifact
368    .chunk_by_ukey
369    .expect_get(chunk_ukey);
370  let full_resolved_name = self
371    .get_resolved_full_name(&options, compilation, chunk)
372    .await?;
373  if self.is_copy(&options) {
374    "copy".hash(hasher);
375  }
376  if self.options.declare {
377    self.options.declare.hash(hasher);
378  }
379  full_resolved_name.join(".").hash(hasher);
380  if let Some(export) = options.export {
381    export.hash(hasher);
382  }
383  Ok(())
384}
385
386#[plugin_hook(JavascriptModulesEmbedInRuntimeBailout for AssignLibraryPlugin)]
387async fn embed_in_runtime_bailout(
388  &self,
389  compilation: &Compilation,
390  module: &BoxModule,
391  chunk: &Chunk,
392) -> Result<Option<String>> {
393  let Some(options) = self.get_options_for_chunk(compilation, &chunk.ukey())? else {
394    return Ok(None);
395  };
396  let codegen = compilation
397    .code_generation_results
398    .get(&module.identifier(), Some(chunk.runtime()));
399  let top_level_decls = codegen
400    .data()
401    .get::<CodeGenerationDataTopLevelDeclarations>()
402    .map(|d| d.inner())
403    .or_else(|| module.build_info().top_level_declarations.as_ref());
404  if let Some(top_level_decls) = top_level_decls {
405    let full_name = self
406      .get_resolved_full_name(&options, compilation, chunk)
407      .await?;
408    if let Some(base) = full_name.first()
409      && top_level_decls.contains(&Atom::new(base.as_str()))
410    {
411      return Ok(Some(format!(
412        "it declares '{base}' on top-level, which conflicts with the current library output."
413      )));
414    }
415    return Ok(None);
416  }
417  Ok(Some(
418    "it doesn't tell about top level declarations.".to_string(),
419  ))
420}
421
422#[plugin_hook(JavascriptModulesStrictRuntimeBailout for AssignLibraryPlugin)]
423async fn strict_runtime_bailout(
424  &self,
425  compilation: &Compilation,
426  chunk_ukey: &ChunkUkey,
427) -> Result<Option<String>> {
428  let Some(options) = self.get_options_for_chunk(compilation, chunk_ukey)? else {
429    return Ok(None);
430  };
431  if self.options.declare
432    || matches!(self.options.prefix, Prefix::Global)
433    || !self.options.prefix.is_empty()
434    || options.name.is_none()
435  {
436    return Ok(None);
437  }
438  Ok(Some(
439    "a global variable is assign and maybe created".to_string(),
440  ))
441}
442
443#[plugin_hook(CompilationFinishModules for AssignLibraryPlugin)]
444async fn finish_modules(
445  &self,
446  compilation: &Compilation,
447  _async_modules_artifact: &mut AsyncModulesArtifact,
448  exports_info_artifact: &mut ExportsInfoArtifact,
449  _side_effects_state_artifact: &mut SideEffectsStateArtifact,
450) -> Result<()> {
451  let module_graph = compilation.get_module_graph();
452  let mut runtime_info = Vec::with_capacity(compilation.entries.len());
453  for (entry_name, entry) in compilation.entries.iter() {
454    let EntryData {
455      dependencies,
456      options,
457      ..
458    } = entry;
459    let runtime = get_entry_runtime(entry_name, options, &compilation.entries);
460    let library_options = options
461      .library
462      .as_ref()
463      .or_else(|| compilation.options.output.library.as_ref());
464    let module_of_last_dep = dependencies
465      .last()
466      .and_then(|dep| module_graph.get_module_by_dependency_id(dep));
467    let Some(module_of_last_dep) = module_of_last_dep else {
468      continue;
469    };
470    let Some(library_options) = library_options else {
471      continue;
472    };
473    if let Some(export) = library_options
474      .export
475      .as_ref()
476      .and_then(|item| item.first())
477    {
478      runtime_info.push((
479        runtime,
480        Some(export.clone()),
481        module_of_last_dep.identifier(),
482      ));
483    } else {
484      runtime_info.push((runtime, None, module_of_last_dep.identifier()));
485    }
486  }
487
488  for (runtime, export, module_identifier) in runtime_info {
489    if let Some(export) = export {
490      let export_info = exports_info_artifact
491        .get_exports_info_data_mut(&module_identifier)
492        .ensure_export_info(&(export.as_str()).into());
493      let info = export_info.as_data_mut(exports_info_artifact);
494      info.set_used(UsageState::Used, Some(&runtime));
495      info.set_can_mangle_use(Some(false));
496      info.set_can_inline_use(Some(CanInlineUse::No));
497    } else {
498      exports_info_artifact
499        .get_exports_info_data_mut(&module_identifier)
500        .set_used_in_unknown_way(Some(&runtime));
501    }
502  }
503  Ok(())
504}
505
506impl Plugin for AssignLibraryPlugin {
507  fn name(&self) -> &'static str {
508    PLUGIN_NAME
509  }
510
511  fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
512    ctx.compiler_hooks.compilation.tap(compilation::new(self));
513    ctx
514      .compilation_hooks
515      .finish_modules
516      .tap(finish_modules::new(self));
517    ctx
518      .compilation_hooks
519      .additional_chunk_runtime_requirements
520      .tap(additional_chunk_runtime_requirements::new(self));
521    Ok(())
522  }
523}
524
525#[plugin_hook(CompilationAdditionalChunkRuntimeRequirements for AssignLibraryPlugin)]
526async fn additional_chunk_runtime_requirements(
527  &self,
528  compilation: &Compilation,
529  chunk_ukey: &ChunkUkey,
530  runtime_requirements: &mut RuntimeGlobals,
531  _runtime_modules: &mut Vec<Box<dyn RuntimeModule>>,
532) -> Result<()> {
533  if self
534    .get_options_for_chunk(compilation, chunk_ukey)?
535    .is_none()
536  {
537    return Ok(());
538  }
539  runtime_requirements.insert(RuntimeGlobals::EXPORTS);
540  Ok(())
541}
542
543fn access_with_init(accessor: &[String], existing_length: usize, init_last: bool) -> String {
544  let base = accessor[0].clone();
545  if accessor.len() == 1 && !init_last {
546    return base;
547  }
548
549  let mut current = if existing_length > 0 {
550    base.clone()
551  } else {
552    format!("({base} = typeof {base} === 'undefined' ? {{}} : {base})")
553  };
554  let mut i = 1;
555  let mut props_so_far = vec![];
556  if existing_length > i {
557    props_so_far = accessor[1..existing_length].to_vec();
558    i = existing_length;
559    current.push_str(property_access(&props_so_far, 0).as_str());
560  }
561
562  let init_until = if init_last {
563    accessor.len()
564  } else {
565    accessor.len() - 1
566  };
567
568  while i < init_until {
569    props_so_far.push(accessor[i].clone());
570    current = format!(
571      "({current}{} = {base}{} || {{}})",
572      property_access(vec![&accessor[i]], 0),
573      property_access(&props_so_far, 0)
574    );
575    i += 1;
576  }
577
578  if i < accessor.len() {
579    current = format!(
580      "{current}{}",
581      property_access([&accessor[accessor.len() - 1]], 0),
582    );
583  }
584
585  current
586}
587
588static KEYWORD_REGEXP: LazyLock<Regex> = LazyLock::new(|| {
589  Regex::new(r"^(await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|super|switch|static|this|throw|try|true|typeof|var|void|while|with|yield)$").expect("should init regex")
590});
591
592static IDENTIFIER_REGEXP: LazyLock<Regex> = LazyLock::new(|| {
593  Regex::new(r"^[\p{L}\p{Nl}$_][\p{L}\p{Nl}$\p{Mn}\p{Mc}\p{Nd}\p{Pc}]*$")
594    .expect("should init regex")
595});
596
597#[inline]
598fn is_name_valid(v: &str) -> bool {
599  !KEYWORD_REGEXP.is_match(v) && IDENTIFIER_REGEXP.is_match(v)
600}