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 pub children: IndexSet<ChunkGroupUkey>,
46 async_entrypoints: UkeySet<ChunkGroupUkey>,
47 pub(crate) next_pre_order_index: usize,
49 pub(crate) next_post_order_index: usize,
50 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 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 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 None => false,
260 Some(old_idx) => {
262 match self.chunks.iter().position(|x| x == new_chunk) {
263 None => {
265 self.chunks[old_idx] = *new_chunk;
266 true
267 }
268 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#[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}