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 pub children: FxIndexSet<ChunkGroupUkey>,
36 async_entrypoints: FxHashSet<ChunkGroupUkey>,
37 pub(crate) next_pre_order_index: u32,
39 pub(crate) next_post_order_index: u32,
40 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 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 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 None => false,
248 Some(old_idx) => {
250 match self.chunks.iter().position(|x| x == new_chunk) {
251 None => {
253 self.chunks[old_idx] = *new_chunk;
254 true
255 }
256 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#[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}