Skip to main content

rspack_plugin_runtime/
helpers.rs

1use std::{hash::Hash, sync::LazyLock};
2
3use itertools::Itertools;
4use regex::Regex;
5use rspack_collections::IdentifierLinkedMap;
6use rspack_core::{
7  Chunk, ChunkGraph, ChunkGroupByUkey, ChunkGroupUkey, ChunkLoading, ChunkLoadingType, ChunkUkey,
8  Compilation, PathData, RuntimeCodeTemplate, RuntimeGlobals, RuntimeVariable, SourceType,
9  chunk_graph_chunk::ChunkIdSet,
10  get_js_chunk_filename_template,
11  rspack_sources::{BoxSource, RawStringSource, SourceExt},
12};
13use rspack_error::{Result, error};
14use rspack_hash::RspackHash;
15use rspack_plugin_javascript::runtime::stringify_chunks_to_array;
16use rspack_util::fx_hash::FxIndexSet;
17use rustc_hash::FxHashSet as HashSet;
18
19use crate::runtime_module::is_enabled_for_chunk;
20
21pub fn should_export_webpack_require_for_module_chunk_loading(
22  chunk_ukey: &ChunkUkey,
23  compilation: &Compilation,
24) -> bool {
25  let chunk_loading = ChunkLoading::Enable(ChunkLoadingType::Import);
26  is_enabled_for_chunk(chunk_ukey, &chunk_loading, compilation)
27    && compilation
28      .build_chunk_graph_artifact
29      .chunk_graph
30      .has_chunk_entry_dependent_chunks(
31        chunk_ukey,
32        &compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
33      )
34}
35
36pub fn update_hash_for_entry_startup(
37  hasher: &mut RspackHash,
38  compilation: &Compilation,
39  entries: &IdentifierLinkedMap<ChunkGroupUkey>,
40  chunk: &ChunkUkey,
41) {
42  for (module, entry) in entries {
43    if let Some(module_id) = compilation
44      .get_module_graph()
45      .module_graph_module_by_identifier(module)
46      .and_then(|module| {
47        ChunkGraph::get_module_id(&compilation.module_ids_artifact, module.module_identifier)
48      })
49    {
50      module_id.hash(hasher);
51    }
52
53    if let Some(runtime_chunk) = compilation
54      .build_chunk_graph_artifact
55      .chunk_group_by_ukey
56      .get(entry)
57      .map(|e| e.get_runtime_chunk(&compilation.build_chunk_graph_artifact.chunk_group_by_ukey))
58    {
59      for chunk_ukey in get_all_chunks(
60        entry,
61        chunk,
62        Some(&runtime_chunk),
63        &compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
64      ) {
65        if let Some(chunk) = compilation
66          .build_chunk_graph_artifact
67          .chunk_by_ukey
68          .get(&chunk_ukey)
69        {
70          chunk.id().hash(hasher);
71        }
72      }
73    }
74  }
75}
76
77pub fn get_all_chunks(
78  entrypoint: &ChunkGroupUkey,
79  exclude_chunk1: &ChunkUkey,
80  exclude_chunk2: Option<&ChunkUkey>,
81  chunk_group_by_ukey: &ChunkGroupByUkey,
82) -> FxIndexSet<ChunkUkey> {
83  fn add_chunks(
84    chunk_group_by_ukey: &ChunkGroupByUkey,
85    chunks: &mut FxIndexSet<ChunkUkey>,
86    entrypoint_ukey: &ChunkGroupUkey,
87    exclude_chunk1: &ChunkUkey,
88    exclude_chunk2: Option<&ChunkUkey>,
89    visit_chunk_groups: &mut FxIndexSet<ChunkGroupUkey>,
90  ) {
91    if let Some(entrypoint) = chunk_group_by_ukey.get(entrypoint_ukey) {
92      for chunk in &entrypoint.chunks {
93        if chunk == exclude_chunk1 {
94          continue;
95        }
96        if let Some(exclude_chunk2) = exclude_chunk2
97          && chunk == exclude_chunk2
98        {
99          continue;
100        }
101        chunks.insert(*chunk);
102      }
103
104      for parent in entrypoint.parents_iterable() {
105        if visit_chunk_groups.contains(parent) {
106          continue;
107        }
108        visit_chunk_groups.insert(*parent);
109        if let Some(chunk_group) = chunk_group_by_ukey.get(parent)
110          && chunk_group.is_initial()
111        {
112          add_chunks(
113            chunk_group_by_ukey,
114            chunks,
115            &chunk_group.ukey,
116            exclude_chunk1,
117            exclude_chunk2,
118            visit_chunk_groups,
119          );
120        }
121      }
122    }
123  }
124
125  let mut chunks = FxIndexSet::default();
126  let mut visit_chunk_groups = FxIndexSet::default();
127
128  add_chunks(
129    chunk_group_by_ukey,
130    &mut chunks,
131    entrypoint,
132    exclude_chunk1,
133    exclude_chunk2,
134    &mut visit_chunk_groups,
135  );
136
137  chunks
138}
139
140pub async fn get_runtime_chunk_output_name(
141  compilation: &Compilation,
142  chunk_ukey: &ChunkUkey,
143) -> Result<String> {
144  let entry_point = {
145    let entry_points = compilation
146      .build_chunk_graph_artifact
147      .chunk_graph
148      .get_chunk_entry_modules_with_chunk_group_iterable(chunk_ukey);
149
150    let (_, entry_point_ukey) = entry_points
151      .iter()
152      .next()
153      .ok_or_else(|| error!("should has entry point ukey"))?;
154
155    compilation
156      .build_chunk_graph_artifact
157      .chunk_group_by_ukey
158      .expect_get(entry_point_ukey)
159  };
160
161  let runtime_chunk = compilation
162    .build_chunk_graph_artifact
163    .chunk_by_ukey
164    .expect_get(
165      &entry_point.get_runtime_chunk(&compilation.build_chunk_graph_artifact.chunk_group_by_ukey),
166    );
167
168  get_chunk_output_name(runtime_chunk, compilation).await
169}
170
171pub fn runtime_chunk_has_hash(compilation: &Compilation, chunk_ukey: &ChunkUkey) -> Result<bool> {
172  let entry_point = {
173    let entry_points = compilation
174      .build_chunk_graph_artifact
175      .chunk_graph
176      .get_chunk_entry_modules_with_chunk_group_iterable(chunk_ukey);
177
178    let (_, entry_point_ukey) = entry_points
179      .iter()
180      .next()
181      .ok_or_else(|| error!("should has entry point ukey"))?;
182
183    compilation
184      .build_chunk_graph_artifact
185      .chunk_group_by_ukey
186      .expect_get(entry_point_ukey)
187  };
188
189  let runtime_chunk_ukey =
190    entry_point.get_runtime_chunk(&compilation.build_chunk_graph_artifact.chunk_group_by_ukey);
191  let runtime_chunk = compilation
192    .build_chunk_graph_artifact
193    .chunk_by_ukey
194    .expect_get(&runtime_chunk_ukey);
195
196  let filename = get_js_chunk_filename_template(
197    runtime_chunk,
198    &compilation.options.output,
199    &compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
200  );
201
202  if filename.has_hash_placeholder() {
203    return Ok(true);
204  }
205
206  if filename.has_content_hash_placeholder()
207    && (compilation
208      .build_chunk_graph_artifact
209      .chunk_graph
210      .has_chunk_full_hash_modules(&runtime_chunk_ukey, &compilation.runtime_modules)
211      || compilation
212        .build_chunk_graph_artifact
213        .chunk_graph
214        .has_chunk_dependent_hash_modules(&runtime_chunk_ukey, &compilation.runtime_modules))
215  {
216    return Ok(true);
217  }
218
219  Ok(false)
220}
221
222pub fn generate_entry_startup(
223  compilation: &Compilation,
224  chunk: &ChunkUkey,
225  entries: &IdentifierLinkedMap<ChunkGroupUkey>,
226  passive: bool,
227  runtime_template: &RuntimeCodeTemplate<'_>,
228) -> BoxSource {
229  let mut module_id_exprs = vec![];
230  let mut chunks_ids = ChunkIdSet::default();
231  let module_graph = compilation.get_module_graph();
232  for (module, entry) in entries {
233    if let Some(module_id) = module_graph
234      .module_by_identifier(module)
235      .filter(|module| {
236        module
237          .source_types(module_graph)
238          .contains(&SourceType::JavaScript)
239      })
240      .and_then(|module| {
241        ChunkGraph::get_module_id(&compilation.module_ids_artifact, module.identifier())
242      })
243    {
244      let module_id_expr = rspack_util::json_stringify(module_id);
245      module_id_exprs.push(module_id_expr);
246    } else {
247      continue;
248    }
249
250    if let Some(runtime_chunk) = compilation
251      .build_chunk_graph_artifact
252      .chunk_group_by_ukey
253      .get(entry)
254      .map(|e| e.get_runtime_chunk(&compilation.build_chunk_graph_artifact.chunk_group_by_ukey))
255    {
256      let chunks = get_all_chunks(
257        entry,
258        chunk,
259        Some(&runtime_chunk),
260        &compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
261      );
262      chunks_ids.extend(
263        chunks
264          .iter()
265          .map(|chunk_ukey| {
266            let chunk = compilation
267              .build_chunk_graph_artifact
268              .chunk_by_ukey
269              .expect_get(chunk_ukey);
270            chunk.expect_id().clone()
271          })
272          .collect::<HashSet<_>>(),
273      );
274    }
275  }
276
277  let mut source = String::default();
278  source.push_str(&format!(
279    "var {} = function(moduleId) {{ return {}({} = moduleId) }}\n",
280    runtime_template.render_runtime_variable(&RuntimeVariable::StartupExec),
281    runtime_template.render_runtime_globals(&RuntimeGlobals::REQUIRE),
282    runtime_template.render_runtime_globals(&RuntimeGlobals::ENTRY_MODULE_ID)
283  ));
284
285  let exports_name = runtime_template.render_runtime_variable(&RuntimeVariable::Exports);
286
287  let module_ids_code = &module_id_exprs
288    .iter()
289    .map(|module_id_expr| {
290      format!(
291        "{}({module_id_expr})",
292        runtime_template.render_runtime_variable(&RuntimeVariable::StartupExec)
293      )
294    })
295    .collect::<Vec<_>>()
296    .join(", ");
297  if chunks_ids.is_empty() {
298    if !module_ids_code.is_empty() {
299      if passive {
300        source.push_str(&format!("var {exports_name} = ("));
301        source.push_str(module_ids_code);
302        source.push_str(");\n");
303      } else {
304        // Ensure STARTUP_ENTRYPOINT wrappers run even without dependent chunks (e.g. async startup).
305        source.push_str(&format!(
306          "var {exports_name} = {}(0, [], function() {{\n        return {};\n      }});\n",
307          runtime_template.render_runtime_globals(&RuntimeGlobals::STARTUP_ENTRYPOINT),
308          module_ids_code
309        ));
310      }
311    }
312  } else {
313    if !passive {
314      source.push_str(&format!("var {exports_name} = "));
315    }
316    source.push_str(&format!(
317      "{}(0, {}, function() {{
318        return {};
319      }});\n",
320      if passive {
321        runtime_template.render_runtime_globals(&RuntimeGlobals::ON_CHUNKS_LOADED)
322      } else {
323        runtime_template.render_runtime_globals(&RuntimeGlobals::STARTUP_ENTRYPOINT)
324      },
325      stringify_chunks_to_array(&chunks_ids),
326      module_ids_code
327    ));
328    if passive {
329      source.push_str(&format!(
330        "var {exports_name} = {}();\n",
331        runtime_template.render_runtime_globals(&RuntimeGlobals::ON_CHUNKS_LOADED)
332      ));
333    }
334  }
335
336  RawStringSource::from(source).boxed()
337}
338
339/**
340 * This is ported from https://github.com/webpack/webpack/blob/87660921808566ef3b8796f8df61bd79fc026108/lib/esm/ModuleChunkFormatPlugin.js#L98
341 */
342pub fn get_relative_path(base_chunk_output_name: &str, other_chunk_output_name: &str) -> String {
343  let mut base_chunk_output_name_arr = base_chunk_output_name.split('/').collect::<Vec<_>>();
344  base_chunk_output_name_arr.pop();
345  let mut other_chunk_output_name_arr = other_chunk_output_name.split('/').collect::<Vec<_>>();
346  while !base_chunk_output_name_arr.is_empty()
347    && !other_chunk_output_name_arr.is_empty()
348    && base_chunk_output_name_arr[0] == other_chunk_output_name_arr[0]
349  {
350    base_chunk_output_name_arr.remove(0);
351    other_chunk_output_name_arr.remove(0);
352  }
353  let path = if base_chunk_output_name_arr.is_empty() {
354    "./".to_string()
355  } else {
356    "../".repeat(base_chunk_output_name_arr.len())
357  };
358  format!("{path}{}", other_chunk_output_name_arr.join("/"))
359}
360
361pub async fn get_chunk_output_name(chunk: &Chunk, compilation: &Compilation) -> Result<String> {
362  let hash = chunk.rendered_hash(
363    &compilation.chunk_hashes_artifact,
364    compilation.options.output.hash_digest_length,
365  );
366  let filename = get_js_chunk_filename_template(
367    chunk,
368    &compilation.options.output,
369    &compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
370  );
371  compilation
372    .get_path(
373      &filename,
374      PathData::default()
375        .chunk_id_optional(chunk.id().map(|id| id.as_str()))
376        .chunk_hash_optional(chunk.rendered_hash(
377          &compilation.chunk_hashes_artifact,
378          compilation.options.output.hash_digest_length,
379        ))
380        .chunk_name_optional(chunk.name_for_filename_template())
381        .content_hash_optional(chunk.rendered_content_hash_by_source_type(
382          &compilation.chunk_hashes_artifact,
383          &SourceType::JavaScript,
384          compilation.options.output.hash_digest_length,
385        ))
386        .hash_optional(hash),
387    )
388    .await
389}
390
391pub fn get_chunk_runtime_requirements<'a>(
392  compilation: &'a Compilation,
393  chunk_ukey: &ChunkUkey,
394) -> &'a RuntimeGlobals {
395  ChunkGraph::get_chunk_runtime_requirements(compilation, chunk_ukey)
396}
397
398/// Regex to match EJS `<%- RUNTIME_GLOBALS %>` where the identifier is uppercase (RuntimeGlobals enum style).
399static EJS_RUNTIME_GLOBALS_RE: LazyLock<Regex> = LazyLock::new(|| {
400  Regex::new(r"<%\-\s*([A-Z][A-Z0-9_]*)\s*%>").expect("invalid EJS runtime globals regex")
401});
402
403/// Extracts all RuntimeGlobals references from an EJS template string.
404///
405/// Matches patterns like `<%- PUBLIC_PATH %>` or `<%- GET_CHUNK_SCRIPT_FILENAME %>`
406/// where the identifier is uppercase letters, digits and underscores (RuntimeGlobals enum variant naming).
407/// Does not match other EJS placeholders such as `<%- basicFunction(...) %>` or `<%- _modules %>`.
408pub fn extract_runtime_globals_from_ejs(ejs_content: &str) -> RuntimeGlobals {
409  let names = EJS_RUNTIME_GLOBALS_RE
410    .captures_iter(ejs_content)
411    .map(|cap| cap[1].to_string())
412    // script nonce is always optional
413    .filter(|name| name.as_str() != "SCRIPT_NONCE")
414    .collect_vec();
415  RuntimeGlobals::from_names(&names)
416}
417
418#[cfg(test)]
419mod tests {
420  use super::{RuntimeGlobals, extract_runtime_globals_from_ejs};
421
422  fn expected_globals(names: &[&str]) -> RuntimeGlobals {
423    let names: Vec<String> = names.iter().map(|s| (*s).to_string()).collect();
424    RuntimeGlobals::from_names(&names)
425  }
426
427  #[test]
428  fn test_extract_runtime_globals_empty() {
429    assert!(extract_runtime_globals_from_ejs("").is_empty());
430    assert!(extract_runtime_globals_from_ejs("plain text").is_empty());
431  }
432
433  #[test]
434  fn test_extract_runtime_globals_single() {
435    assert_eq!(
436      extract_runtime_globals_from_ejs("<%- PUBLIC_PATH %>"),
437      expected_globals(&["PUBLIC_PATH"])
438    );
439    assert_eq!(
440      extract_runtime_globals_from_ejs("var x = <%- REQUIRE %>;"),
441      expected_globals(&["REQUIRE"])
442    );
443  }
444
445  #[test]
446  fn test_extract_runtime_globals_multiple_unique() {
447    let ejs = r#"link.href = <%- PUBLIC_PATH %> + <%- GET_CHUNK_SCRIPT_FILENAME %>(chunkId);
448    if (<%- HAS_OWN_PROPERTY %>(installedChunks, chunkId)) {}"#;
449    assert_eq!(
450      extract_runtime_globals_from_ejs(ejs),
451      expected_globals(&[
452        "PUBLIC_PATH",
453        "GET_CHUNK_SCRIPT_FILENAME",
454        "HAS_OWN_PROPERTY"
455      ])
456    );
457  }
458
459  #[test]
460  fn test_extract_runtime_globals_deduplicate() {
461    let ejs = "<%- REQUIRE %>; <%- PUBLIC_PATH %>; <%- REQUIRE %>; <%- PUBLIC_PATH %>";
462    assert_eq!(
463      extract_runtime_globals_from_ejs(ejs),
464      expected_globals(&["REQUIRE", "PUBLIC_PATH"])
465    );
466  }
467
468  #[test]
469  fn test_extract_runtime_globals_with_spaces() {
470    assert_eq!(
471      extract_runtime_globals_from_ejs("<%-  ENSURE_CHUNK  %>"),
472      expected_globals(&["ENSURE_CHUNK"])
473    );
474  }
475
476  #[test]
477  fn test_extract_runtime_globals_ignore_non_globals() {
478    // Should not match: lowercase, underscore prefix, function calls
479    let ejs = r#"<%- basicFunction("resolve, reject")  %>
480    <%- _modules %>
481    <%- _cross_origin %>
482    <%- MODULE_CACHE %>"#;
483    assert_eq!(
484      extract_runtime_globals_from_ejs(ejs),
485      expected_globals(&["MODULE_CACHE"])
486    );
487  }
488
489  #[test]
490  fn test_extract_runtime_globals_uppercase_with_underscores() {
491    let ejs = "<%- GET_CHUNK_UPDATE_SCRIPT_FILENAME %> <%- HMR_DOWNLOAD_UPDATE_HANDLERS %>";
492    assert_eq!(
493      extract_runtime_globals_from_ejs(ejs),
494      expected_globals(&[
495        "GET_CHUNK_UPDATE_SCRIPT_FILENAME",
496        "HMR_DOWNLOAD_UPDATE_HANDLERS"
497      ])
498    );
499  }
500
501  #[test]
502  fn test_extract_runtime_globals_real_ejs_snippet() {
503    let ejs = r#"var installChunk = <%- basicFunction("data") %> {
504    var <%- _modules %> = data.<%- _modules %>;
505    for (moduleId in <%- _modules %>) {
506        if (<%- HAS_OWN_PROPERTY %>(<%- _modules %>, moduleId)) {
507            <%- MODULE_FACTORIES %>[moduleId] = <%- _modules %>[moduleId];
508        }
509    }
510    if (__rspack_esm_runtime) __rspack_esm_runtime(<%- REQUIRE %>);
511};"#;
512    assert_eq!(
513      extract_runtime_globals_from_ejs(ejs),
514      expected_globals(&["HAS_OWN_PROPERTY", "MODULE_FACTORIES", "REQUIRE"])
515    );
516  }
517}