rspack_core/
chunk_group.rs

1use std::{
2  cmp::Ordering,
3  fmt::{self, Display},
4};
5
6use indexmap::IndexSet;
7use itertools::Itertools;
8use rspack_cacheable::cacheable;
9use rspack_collections::{DatabaseItem, IdentifierMap, UkeySet};
10use rspack_error::{Result, error};
11use rustc_hash::FxHashMap as HashMap;
12
13use crate::{
14  Chunk, ChunkByUkey, ChunkGroupByUkey, ChunkGroupUkey, ChunkLoading, ChunkUkey, Compilation,
15  DependencyLocation, DynamicImportFetchPriority, Filename, LibraryOptions, ModuleIdentifier,
16  ModuleLayer, PublicPath, compare_chunk_group,
17};
18
19#[derive(Debug, Clone)]
20pub struct OriginRecord {
21  pub module: Option<ModuleIdentifier>,
22  pub loc: Option<DependencyLocation>,
23  pub request: Option<String>,
24}
25
26impl DatabaseItem for ChunkGroup {
27  type ItemUkey = ChunkGroupUkey;
28
29  fn ukey(&self) -> Self::ItemUkey {
30    self.ukey
31  }
32}
33
34#[derive(Debug, Clone)]
35pub struct ChunkGroup {
36  pub ukey: ChunkGroupUkey,
37  pub kind: ChunkGroupKind,
38  pub chunks: Vec<ChunkUkey>,
39  pub index: Option<u32>,
40  pub parents: UkeySet<ChunkGroupUkey>,
41  pub(crate) module_pre_order_indices: IdentifierMap<usize>,
42  pub(crate) module_post_order_indices: IdentifierMap<usize>,
43
44  // keep order for children
45  pub children: IndexSet<ChunkGroupUkey>,
46  async_entrypoints: UkeySet<ChunkGroupUkey>,
47  // ChunkGroupInfo
48  pub(crate) next_pre_order_index: usize,
49  pub(crate) next_post_order_index: usize,
50  // Entrypoint
51  pub(crate) runtime_chunk: Option<ChunkUkey>,
52  pub(crate) entrypoint_chunk: Option<ChunkUkey>,
53  origins: Vec<OriginRecord>,
54  pub(crate) is_over_size_limit: Option<bool>,
55}
56
57impl Default for ChunkGroup {
58  fn default() -> Self {
59    Self::new(ChunkGroupKind::Normal {
60      options: Default::default(),
61    })
62  }
63}
64
65impl ChunkGroup {
66  pub fn new(kind: ChunkGroupKind) -> Self {
67    Self {
68      ukey: ChunkGroupUkey::new(),
69      chunks: vec![],
70      module_post_order_indices: Default::default(),
71      module_pre_order_indices: Default::default(),
72      parents: Default::default(),
73      children: Default::default(),
74      async_entrypoints: Default::default(),
75      kind,
76      next_pre_order_index: 0,
77      next_post_order_index: 0,
78      runtime_chunk: None,
79      entrypoint_chunk: None,
80      index: None,
81      origins: vec![],
82      is_over_size_limit: None,
83    }
84  }
85
86  pub fn parents_iterable(&self) -> impl Iterator<Item = &ChunkGroupUkey> {
87    self.parents.iter()
88  }
89
90  pub fn module_pre_order_index(&self, module_identifier: &ModuleIdentifier) -> Option<usize> {
91    // A module could split into another ChunkGroup, which doesn't have the module_post_order_indices of the module
92    self
93      .module_pre_order_indices
94      .get(module_identifier)
95      .copied()
96  }
97
98  pub fn children_iterable(&self) -> impl Iterator<Item = &ChunkGroupUkey> {
99    self.children.iter()
100  }
101
102  pub fn module_post_order_index(&self, module_identifier: &ModuleIdentifier) -> Option<usize> {
103    // A module could split into another ChunkGroup, which doesn't have the module_post_order_indices of the module
104    self
105      .module_post_order_indices
106      .get(module_identifier)
107      .copied()
108  }
109
110  pub fn get_files(&self, chunk_by_ukey: &ChunkByUkey) -> Vec<String> {
111    self
112      .chunks
113      .iter()
114      .flat_map(|chunk_ukey| {
115        chunk_by_ukey
116          .expect_get(chunk_ukey)
117          .files()
118          .iter()
119          .map(|file| file.to_string())
120      })
121      .collect()
122  }
123
124  pub(crate) fn connect_chunk(&mut self, chunk: &mut Chunk) {
125    self.chunks.push(chunk.ukey());
126    chunk.add_group(self.ukey);
127  }
128
129  pub fn unshift_chunk(&mut self, chunk: ChunkUkey) -> bool {
130    if let Ok(index) = self.chunks.binary_search(&chunk) {
131      if index > 0 {
132        self.chunks.remove(index);
133        self.chunks.insert(0, chunk);
134      }
135      false
136    } else {
137      self.chunks.insert(0, chunk);
138      true
139    }
140  }
141
142  pub fn is_initial(&self) -> bool {
143    matches!(self.kind, ChunkGroupKind::Entrypoint { initial, .. } if initial)
144  }
145
146  pub fn set_runtime_chunk(&mut self, chunk_ukey: ChunkUkey) {
147    self.runtime_chunk = Some(chunk_ukey);
148  }
149
150  pub fn get_runtime_chunk(&self, chunk_group_by_ukey: &ChunkGroupByUkey) -> ChunkUkey {
151    match self.kind {
152      ChunkGroupKind::Entrypoint { .. } => self.runtime_chunk.unwrap_or_else(|| {
153        for parent in self.parents_iterable() {
154          let parent = chunk_group_by_ukey.expect_get(parent);
155          if matches!(parent.kind, ChunkGroupKind::Entrypoint { .. }) {
156            return parent.get_runtime_chunk(chunk_group_by_ukey);
157          }
158        }
159        panic!(
160          "Entrypoint({:?}) should set_runtime_chunk at build_chunk_graph before get_runtime_chunk",
161          self.name()
162        )
163      }),
164      ChunkGroupKind::Normal { .. } => {
165        unreachable!("Normal chunk group doesn't have runtime chunk")
166      }
167    }
168  }
169
170  pub fn set_entrypoint_chunk(&mut self, chunk_ukey: ChunkUkey) {
171    self.entrypoint_chunk = Some(chunk_ukey);
172  }
173
174  pub fn get_entrypoint_chunk(&self) -> ChunkUkey {
175    match self.kind {
176      ChunkGroupKind::Entrypoint { .. } => self
177        .entrypoint_chunk
178        .expect("EntryPoint runtime chunk not set"),
179      ChunkGroupKind::Normal { .. } => {
180        unreachable!("Normal chunk group doesn't have runtime chunk")
181      }
182    }
183  }
184
185  pub fn add_async_entrypoint(&mut self, async_entrypoint: ChunkGroupUkey) -> bool {
186    self.async_entrypoints.insert(async_entrypoint)
187  }
188
189  pub fn async_entrypoints_iterable(&self) -> impl Iterator<Item = &ChunkGroupUkey> {
190    self.async_entrypoints.iter()
191  }
192
193  pub fn ancestors(&self, chunk_group_by_ukey: &ChunkGroupByUkey) -> UkeySet<ChunkGroupUkey> {
194    let mut queue = vec![];
195    let mut ancestors = UkeySet::default();
196
197    queue.extend(self.parents.iter().copied());
198
199    while let Some(chunk_group_ukey) = queue.pop() {
200      if ancestors.contains(&chunk_group_ukey) {
201        continue;
202      }
203      ancestors.insert(chunk_group_ukey);
204      let chunk_group = chunk_group_by_ukey.expect_get(&chunk_group_ukey);
205      for parent in &chunk_group.parents {
206        queue.push(*parent);
207      }
208    }
209
210    ancestors
211  }
212
213  pub fn insert_chunk(&mut self, chunk: ChunkUkey, before: ChunkUkey) -> bool {
214    let old_idx = self.chunks.iter().position(|ukey| *ukey == chunk);
215    let idx = self
216      .chunks
217      .iter()
218      .position(|ukey| *ukey == before)
219      .expect("before chunk not found");
220
221    if let Some(old_idx) = old_idx
222      && old_idx > idx
223    {
224      self.chunks.remove(old_idx);
225      self.chunks.insert(idx, chunk);
226    } else if old_idx.is_none() {
227      self.chunks.insert(idx, chunk);
228      return true;
229    }
230
231    false
232  }
233
234  pub fn remove_chunk(&mut self, chunk: &ChunkUkey) -> bool {
235    let idx = self.chunks.iter().position(|ukey| ukey == chunk);
236    if let Some(idx) = idx {
237      self.chunks.remove(idx);
238      return true;
239    }
240
241    false
242  }
243
244  pub fn replace_chunk(&mut self, old_chunk: &ChunkUkey, new_chunk: &ChunkUkey) -> bool {
245    if let Some(runtime_chunk) = self.runtime_chunk
246      && runtime_chunk == *old_chunk
247    {
248      self.runtime_chunk = Some(*new_chunk);
249    }
250
251    if let Some(entry_point_chunk) = self.entrypoint_chunk
252      && entry_point_chunk == *old_chunk
253    {
254      self.entrypoint_chunk = Some(*new_chunk);
255    }
256
257    match self.chunks.iter().position(|x| x == old_chunk) {
258      // when old_chunk doesn't exist
259      None => false,
260      // when old_chunk exists
261      Some(old_idx) => {
262        match self.chunks.iter().position(|x| x == new_chunk) {
263          // when new_chunk doesn't exist
264          None => {
265            self.chunks[old_idx] = *new_chunk;
266            true
267          }
268          // when new_chunk exists
269          Some(new_idx) => {
270            if new_idx < old_idx {
271              self.chunks.remove(old_idx);
272              true
273            } else if new_idx != old_idx {
274              self.chunks[old_idx] = *new_chunk;
275              self.chunks.remove(new_idx);
276              true
277            } else {
278              false
279            }
280          }
281        }
282      }
283    }
284  }
285
286  pub fn id(&self, compilation: &Compilation) -> String {
287    self
288      .chunks
289      .iter()
290      .filter_map(|chunk| {
291        compilation
292          .chunk_by_ukey
293          .get(chunk)
294          .and_then(|item| item.id(&compilation.chunk_ids_artifact))
295      })
296      .join("+")
297  }
298
299  pub fn get_parents<'a>(
300    &'a self,
301    chunk_group_by_ukey: &'a ChunkGroupByUkey,
302  ) -> Vec<&'a ChunkGroup> {
303    self
304      .parents_iterable()
305      .map(|ukey| chunk_group_by_ukey.expect_get(ukey))
306      .collect_vec()
307  }
308
309  pub fn name(&self) -> Option<&str> {
310    match &self.kind {
311      ChunkGroupKind::Entrypoint { options, .. } => options.name.as_deref(),
312      ChunkGroupKind::Normal { options } => options.name.as_deref(),
313    }
314  }
315
316  pub fn add_child(&mut self, child_group: ChunkGroupUkey) -> bool {
317    let size = self.children.len();
318    self.children.insert(child_group);
319    size != self.children.len()
320  }
321
322  pub fn add_parent(&mut self, parent_group: ChunkGroupUkey) -> bool {
323    self.parents.insert(parent_group)
324  }
325
326  pub fn add_origin(
327    &mut self,
328    module_id: Option<ModuleIdentifier>,
329    loc: Option<DependencyLocation>,
330    request: Option<String>,
331  ) {
332    self.origins.push(OriginRecord {
333      module: module_id,
334      loc,
335      request,
336    });
337  }
338
339  pub fn origins(&self) -> &[OriginRecord] {
340    &self.origins
341  }
342
343  pub fn get_children_by_orders(
344    &self,
345    compilation: &Compilation,
346  ) -> HashMap<ChunkGroupOrderKey, Vec<ChunkGroupUkey>> {
347    let mut children_by_orders = HashMap::<ChunkGroupOrderKey, Vec<ChunkGroupUkey>>::default();
348
349    let orders = vec![ChunkGroupOrderKey::Preload, ChunkGroupOrderKey::Prefetch];
350
351    for order_key in orders {
352      let mut list = vec![];
353      for child_ukey in &self.children {
354        let Some(child_group) = compilation.chunk_group_by_ukey.get(child_ukey) else {
355          continue;
356        };
357        if let Some(order) = child_group
358          .kind
359          .get_normal_options()
360          .and_then(|o| match order_key {
361            ChunkGroupOrderKey::Prefetch => o.prefetch_order,
362            ChunkGroupOrderKey::Preload => o.preload_order,
363          })
364        {
365          list.push((order, child_group.ukey));
366        }
367      }
368
369      list.sort_by(|a, b| {
370        let cmp = b.0.cmp(&a.0);
371        match cmp {
372          Ordering::Equal => compare_chunk_group(&a.1, &b.1, compilation),
373          _ => cmp,
374        }
375      });
376
377      children_by_orders.insert(order_key, list.iter().map(|i| i.1).collect_vec());
378    }
379
380    children_by_orders
381  }
382
383  pub fn set_is_over_size_limit(&mut self, v: bool) {
384    self.is_over_size_limit = Some(v);
385  }
386}
387
388#[derive(Debug, Clone)]
389pub enum ChunkGroupKind {
390  Entrypoint {
391    initial: bool,
392    options: Box<EntryOptions>,
393  },
394  Normal {
395    options: ChunkGroupOptions,
396  },
397}
398
399impl ChunkGroupKind {
400  pub fn new_entrypoint(initial: bool, options: Box<EntryOptions>) -> Self {
401    Self::Entrypoint { initial, options }
402  }
403
404  pub fn is_entrypoint(&self) -> bool {
405    matches!(self, Self::Entrypoint { .. })
406  }
407
408  pub fn get_entry_options(&self) -> Option<&EntryOptions> {
409    match self {
410      ChunkGroupKind::Entrypoint { options, .. } => Some(options),
411      ChunkGroupKind::Normal { .. } => None,
412    }
413  }
414
415  pub fn get_normal_options(&self) -> Option<&ChunkGroupOptions> {
416    match self {
417      ChunkGroupKind::Entrypoint { .. } => None,
418      ChunkGroupKind::Normal { options, .. } => Some(options),
419    }
420  }
421
422  pub fn name(&self) -> Option<&str> {
423    match self {
424      ChunkGroupKind::Entrypoint { options, .. } => options.name.as_deref(),
425      ChunkGroupKind::Normal { options } => options.name.as_deref(),
426    }
427  }
428}
429
430#[cacheable]
431#[derive(Debug, Default, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
432pub enum EntryRuntime {
433  String(String),
434  #[default]
435  False,
436}
437
438impl From<&str> for EntryRuntime {
439  fn from(value: &str) -> Self {
440    Self::String(value.to_owned())
441  }
442}
443
444impl From<String> for EntryRuntime {
445  fn from(value: String) -> Self {
446    Self::String(value)
447  }
448}
449
450impl EntryRuntime {
451  pub fn as_string(&self) -> Option<&str> {
452    match self {
453      EntryRuntime::String(s) => Some(s),
454      EntryRuntime::False => None,
455    }
456  }
457}
458
459// pub type EntryRuntime = String;
460#[cacheable]
461#[derive(Debug, Default, Clone, Hash, PartialEq, Eq)]
462pub struct EntryOptions {
463  pub name: Option<String>,
464  pub runtime: Option<EntryRuntime>,
465  pub chunk_loading: Option<ChunkLoading>,
466  pub async_chunks: Option<bool>,
467  pub public_path: Option<PublicPath>,
468  pub base_uri: Option<String>,
469  pub filename: Option<Filename>,
470  pub library: Option<LibraryOptions>,
471  pub depend_on: Option<Vec<String>>,
472  pub layer: Option<ModuleLayer>,
473}
474
475impl EntryOptions {
476  pub fn merge(&mut self, other: EntryOptions) -> Result<()> {
477    macro_rules! merge_field {
478      ($field:ident) => {
479        if Self::should_merge_field(
480          self.$field.as_ref(),
481          other.$field.as_ref(),
482          stringify!($field),
483        )? {
484          self.$field = other.$field;
485        }
486      };
487    }
488    merge_field!(name);
489    merge_field!(runtime);
490    merge_field!(chunk_loading);
491    merge_field!(async_chunks);
492    merge_field!(public_path);
493    merge_field!(base_uri);
494    merge_field!(filename);
495    merge_field!(library);
496    merge_field!(depend_on);
497    merge_field!(layer);
498    Ok(())
499  }
500
501  fn should_merge_field<T: Eq + fmt::Debug>(
502    a: Option<&T>,
503    b: Option<&T>,
504    key: &str,
505  ) -> Result<bool> {
506    match (a, b) {
507      (Some(a), Some(b)) if a != b => {
508        Err(error!("Conflicting entry option {key} = ${a:?} vs ${b:?}"))
509      }
510      (None, Some(_)) => Ok(true),
511      _ => Ok(false),
512    }
513  }
514}
515
516#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
517pub enum ChunkGroupOrderKey {
518  Preload,
519  Prefetch,
520}
521
522impl Display for ChunkGroupOrderKey {
523  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
524    f.write_str(match self {
525      ChunkGroupOrderKey::Preload => "preload",
526      ChunkGroupOrderKey::Prefetch => "prefetch",
527    })
528  }
529}
530
531#[cacheable]
532#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
533pub struct ChunkGroupOptions {
534  pub name: Option<String>,
535  pub preload_order: Option<i32>,
536  pub prefetch_order: Option<i32>,
537  pub fetch_priority: Option<DynamicImportFetchPriority>,
538}
539
540impl ChunkGroupOptions {
541  pub fn new(
542    name: Option<String>,
543    preload_order: Option<i32>,
544    prefetch_order: Option<i32>,
545    fetch_priority: Option<DynamicImportFetchPriority>,
546  ) -> Self {
547    Self {
548      name,
549      preload_order,
550      prefetch_order,
551      fetch_priority,
552    }
553  }
554  pub fn name_optional(mut self, name: Option<String>) -> Self {
555    self.name = name;
556    self
557  }
558}
559
560#[cacheable]
561#[derive(Debug, Clone, Hash, PartialEq, Eq)]
562pub enum GroupOptions {
563  Entrypoint(Box<EntryOptions>),
564  ChunkGroup(ChunkGroupOptions),
565}
566
567impl GroupOptions {
568  pub fn name(&self) -> Option<&str> {
569    match self {
570      Self::Entrypoint(e) => e.name.as_deref(),
571      Self::ChunkGroup(n) => n.name.as_deref(),
572    }
573  }
574
575  pub fn entry_options(&self) -> Option<&EntryOptions> {
576    match self {
577      GroupOptions::Entrypoint(e) => Some(e),
578      GroupOptions::ChunkGroup(_) => None,
579    }
580  }
581
582  pub fn normal_options(&self) -> Option<&ChunkGroupOptions> {
583    match self {
584      GroupOptions::Entrypoint(_) => None,
585      GroupOptions::ChunkGroup(e) => Some(e),
586    }
587  }
588}