Skip to main content

rspack_core/
chunk_group.rs

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