Skip to main content

rspack_plugin_runtime/runtime_module/
utils.rs

1use std::fmt::Write as _;
2
3use rspack_core::{
4  Chunk, ChunkLoading, ChunkUkey, Compilation, PathData, RuntimeCodeTemplate, SourceType,
5  chunk_graph_chunk::{ChunkId, ChunkIdSet},
6  get_js_chunk_filename_template, get_undo_path,
7};
8use rspack_error::Result;
9use rspack_util::{
10  fx_hash::{FxIndexMap, FxIndexSet},
11  test::is_hot_test,
12};
13
14pub fn get_initial_chunk_ids(
15  chunk: Option<ChunkUkey>,
16  compilation: &Compilation,
17  filter_fn: impl Fn(&ChunkUkey, &Compilation) -> bool,
18) -> ChunkIdSet {
19  match chunk {
20    Some(chunk_ukey) => match compilation
21      .build_chunk_graph_artifact
22      .chunk_by_ukey
23      .get(&chunk_ukey)
24    {
25      Some(chunk) => {
26        let mut js_chunks = chunk
27          .get_all_initial_chunks(&compilation.build_chunk_graph_artifact.chunk_group_by_ukey)
28          .iter()
29          .filter(|key| !(chunk_ukey.eq(key) || filter_fn(key, compilation)))
30          .map(|chunk_ukey| {
31            let chunk = compilation
32              .build_chunk_graph_artifact
33              .chunk_by_ukey
34              .expect_get(chunk_ukey);
35            chunk.expect_id().clone()
36          })
37          .collect::<ChunkIdSet>();
38        js_chunks.insert(chunk.expect_id().clone());
39        js_chunks
40      }
41      None => ChunkIdSet::default(),
42    },
43    None => ChunkIdSet::default(),
44  }
45}
46
47pub fn stringify_chunks(chunks: &ChunkIdSet, value: u8) -> String {
48  let mut v = chunks.iter().collect::<Vec<_>>();
49  v.sort_unstable();
50
51  let mut result = String::with_capacity(v.len() * 8 + 2);
52  result.push('{');
53  for chunk_id in v {
54    let key = rspack_util::json_stringify(chunk_id);
55    result.reserve(key.len() + 4);
56    result.push_str(&key);
57    result.push_str(": ");
58    write!(result, "{value},").expect("infallible write to String");
59  }
60  result.push('}');
61  result
62}
63
64pub fn chunk_has_css(chunk: &ChunkUkey, compilation: &Compilation) -> bool {
65  compilation
66    .build_chunk_graph_artifact
67    .chunk_graph
68    .has_chunk_module_by_source_type(chunk, SourceType::Css, compilation.get_module_graph())
69}
70
71pub async fn get_output_dir(
72  chunk: &Chunk,
73  compilation: &Compilation,
74  enforce_relative: bool,
75) -> rspack_error::Result<String> {
76  let filename = get_js_chunk_filename_template(
77    chunk,
78    &compilation.options.output,
79    &compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
80  );
81  let output_dir = compilation
82    .get_path(
83      &filename,
84      PathData::default()
85        .chunk_id_optional(chunk.id().map(|id| id.as_str()))
86        .chunk_hash_optional(chunk.rendered_hash(
87          &compilation.chunk_hashes_artifact,
88          compilation.options.output.hash_digest_length,
89        ))
90        .chunk_name_optional(chunk.name_for_filename_template())
91        .content_hash_optional(chunk.rendered_content_hash_by_source_type(
92          &compilation.chunk_hashes_artifact,
93          &SourceType::JavaScript,
94          compilation.options.output.hash_digest_length,
95        )),
96    )
97    .await?;
98  Ok(get_undo_path(
99    output_dir.as_str(),
100    compilation.options.output.path.as_str().to_string(),
101    enforce_relative,
102  ))
103}
104
105pub fn is_enabled_for_chunk(
106  chunk_ukey: &ChunkUkey,
107  expected: &ChunkLoading,
108  compilation: &Compilation,
109) -> bool {
110  let chunk_loading = compilation
111    .build_chunk_graph_artifact
112    .chunk_by_ukey
113    .get(chunk_ukey)
114    .and_then(|chunk| {
115      chunk.get_entry_options(&compilation.build_chunk_graph_artifact.chunk_group_by_ukey)
116    })
117    .and_then(|options| options.chunk_loading.as_ref())
118    .unwrap_or(&compilation.options.output.chunk_loading);
119  chunk_loading == expected
120}
121
122pub fn unquoted_stringify(chunk_id: Option<&ChunkId>, str: &str) -> String {
123  if let Some(chunk_id) = chunk_id
124    && str.len() >= 5
125    && str == chunk_id.as_str()
126  {
127    return "\" + chunkId + \"".to_string();
128  }
129  let result = rspack_util::json_stringify_str(str);
130  result[1..result.len() - 1].to_string()
131}
132
133pub fn stringify_dynamic_chunk_map<F>(
134  f: F,
135  chunks: &FxIndexSet<ChunkUkey>,
136  chunk_map: &FxIndexMap<ChunkUkey, &Chunk>,
137) -> String
138where
139  F: Fn(&Chunk) -> Option<String>,
140{
141  let mut entries = Vec::with_capacity(chunks.len());
142  let mut use_id = false;
143
144  for chunk_ukey in chunks.iter() {
145    if let Some(chunk) = chunk_map.get(chunk_ukey)
146      && let Some(chunk_id) = chunk.id()
147      && let Some(value) = f(chunk)
148    {
149      if value.as_str() == chunk_id.as_str() {
150        use_id = true;
151      } else {
152        entries.push((chunk_id, rspack_util::json_stringify_str(&value)));
153      }
154    }
155  }
156
157  let content = match entries.as_mut_slice() {
158    [] => "chunkId".to_string(),
159    [(chunk_id, value)] => {
160      if use_id {
161        format!(
162          "(chunkId === {} ? {} : chunkId)",
163          rspack_util::json_stringify(*chunk_id),
164          value
165        )
166      } else {
167        value.clone()
168      }
169    }
170    entries => {
171      let map = stringify_map(entries);
172      if use_id {
173        format!("({map}[chunkId] || chunkId)")
174      } else {
175        format!("{map}[chunkId]")
176      }
177    }
178  };
179  format!("\" + {content} + \"")
180}
181
182pub fn stringify_static_chunk_map(filename: &str, chunk_ids: &[&ChunkId]) -> String {
183  let condition = if chunk_ids.len() == 1 {
184    let mut condition = String::from("chunkId === ");
185    let chunk_id = chunk_ids.first().expect("should have one chunk id");
186    condition.push_str(&rspack_util::json_stringify(*chunk_id));
187    condition
188  } else {
189    let mut sorted_chunk_ids = chunk_ids.to_vec();
190    sorted_chunk_ids.sort_unstable();
191
192    let mut condition = String::with_capacity(sorted_chunk_ids.len() * 8 + 14);
193    condition.push('{');
194    condition.push(' ');
195    for (idx, chunk_id) in sorted_chunk_ids.iter().enumerate() {
196      if idx != 0 {
197        condition.push(',');
198      }
199      let key = rspack_util::json_stringify(*chunk_id);
200      condition.reserve(key.len() + 2);
201      condition.push_str(&key);
202      condition.push_str(":1");
203    }
204    condition.push_str(" }[chunkId]");
205    condition
206  };
207  let mut result = String::with_capacity(condition.len() + filename.len() + 14);
208  result.push_str("if (");
209  result.push_str(&condition);
210  result.push_str(") return ");
211  result.push_str(filename);
212  result.push(';');
213  result
214}
215
216fn stringify_map<T: std::fmt::Display>(entries: &mut [(&ChunkId, T)]) -> String {
217  entries.sort_unstable_by_key(|(left, _)| *left);
218
219  let mut result = String::with_capacity(entries.len() * 8 + 2);
220  result.push('{');
221  for (chunk_id, value) in entries.iter() {
222    let key = rspack_util::json_stringify(*chunk_id);
223    result.reserve(key.len() + 4);
224    result.push_str(&key);
225    result.push_str(": ");
226    write!(result, "{value},").expect("infallible write to String");
227  }
228  result.push('}');
229  result
230}
231
232pub fn generate_javascript_hmr_runtime(
233  key: &str,
234  method: &str,
235  runtime_template: &RuntimeCodeTemplate,
236) -> Result<String> {
237  runtime_template.render(
238    key,
239    Some(serde_json::json!({
240      "_loading_method": method,
241      "_is_hot_test": is_hot_test(),
242    })),
243  )
244}
245
246#[cfg(test)]
247mod tests {
248  use rspack_core::chunk_graph_chunk::{ChunkId, ChunkIdSet};
249
250  use super::{stringify_chunks, stringify_map, stringify_static_chunk_map};
251
252  #[test]
253  fn stringify_chunks_keeps_sorted_numeric_ids() {
254    let mut chunks = ChunkIdSet::default();
255    chunks.insert(ChunkId::from("2"));
256    chunks.insert(ChunkId::from("10"));
257
258    assert_eq!(stringify_chunks(&chunks, 1), "{10: 1,2: 1,}");
259  }
260
261  #[test]
262  fn stringify_map_keeps_sorted_and_quoted_values() {
263    let chunk_a = ChunkId::from("a");
264    let chunk_b = ChunkId::from("b");
265    let mut entries = vec![
266      (&chunk_b, rspack_util::json_stringify_str("beta")),
267      (&chunk_a, rspack_util::json_stringify_str("alpha")),
268    ];
269
270    assert_eq!(
271      stringify_map(&mut entries),
272      r#"{"a": "alpha","b": "beta",}"#
273    );
274  }
275
276  #[test]
277  fn stringify_static_chunk_map_single_chunk_keeps_condition_shape() {
278    let filename = "\"style.css\"".to_string();
279    let chunk_id = ChunkId::from("main");
280
281    assert_eq!(
282      stringify_static_chunk_map(&filename, &[&chunk_id]),
283      r#"if (chunkId === "main") return "style.css";"#
284    );
285  }
286
287  #[test]
288  fn stringify_static_chunk_map_multiple_chunks_keeps_sorted_object_shape() {
289    let filename = "\"style.css\"".to_string();
290    let chunk_a = ChunkId::from("b");
291    let chunk_b = ChunkId::from("a");
292
293    assert_eq!(
294      stringify_static_chunk_map(&filename, &[&chunk_a, &chunk_b]),
295      r#"if ({ "a":1,"b":1 }[chunkId]) return "style.css";"#
296    );
297  }
298}