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}