#![allow(clippy::comparison_chain)]
mod drive;
mod impl_plugin_for_css_plugin;
use std::cmp::{self, Reverse};
pub use drive::*;
use rspack_collections::IdentifierSet;
use rspack_core::{
Chunk, ChunkUkey, Compilation, Module, ModuleIdentifier, compare_modules_by_identifier,
};
use rspack_hook::plugin;
#[plugin]
#[derive(Debug, Default)]
pub struct CssPlugin;
#[derive(Debug)]
pub struct CssOrderConflicts {
pub chunk: ChunkUkey,
pub failed_module: ModuleIdentifier,
pub selected_module: ModuleIdentifier,
}
impl CssPlugin {
pub(crate) fn get_ordered_chunk_css_modules<'a>(
chunk: &Chunk,
compilation: &Compilation,
mut css_import_modules: Vec<&'a dyn Module>,
mut css_modules: Vec<&'a dyn Module>,
) -> (Vec<&'a dyn Module>, Option<Vec<CssOrderConflicts>>) {
css_import_modules.sort_unstable_by_key(|module| module.identifier());
let (mut external_css_modules, conflicts_external) =
Self::get_modules_in_order(chunk, css_import_modules, compilation);
css_modules.sort_unstable_by_key(|module| module.identifier());
let (mut css_modules, conflicts) = Self::get_modules_in_order(chunk, css_modules, compilation);
external_css_modules.append(&mut css_modules);
let conflicts = match (conflicts_external, conflicts) {
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(Some(mut a), Some(mut b)) => {
a.append(&mut b);
Some(a)
}
(None, None) => None,
};
(external_css_modules, conflicts)
}
pub fn get_modules_in_order<'module>(
chunk: &Chunk,
modules: Vec<&'module dyn Module>,
compilation: &Compilation,
) -> (Vec<&'module dyn Module>, Option<Vec<CssOrderConflicts>>) {
if modules.is_empty() {
return (vec![], None);
};
let modules_list = modules.clone();
let mut modules_by_chunk_group = chunk
.groups()
.iter()
.map(|group| {
compilation
.build_chunk_graph_artifact
.chunk_group_by_ukey
.expect_get(group)
})
.map(|chunk_group| {
let mut indexed_modules = modules_list
.clone()
.into_iter()
.filter_map(|module| {
chunk_group
.module_post_order_index(&module.identifier())
.map(|index| (index, module))
})
.collect::<Vec<_>>();
let sorted_modules = {
indexed_modules.sort_by_key(|item| Reverse(item.0));
indexed_modules
.into_iter()
.map(|item| item.1)
.collect::<Vec<_>>()
};
SortedModules {
set: sorted_modules.iter().map(|m| m.identifier()).collect(),
list: sorted_modules,
}
})
.collect::<Vec<_>>();
if modules_by_chunk_group.len() == 1 {
let mut ret = modules_by_chunk_group
.into_iter()
.next()
.expect("must have one")
.list;
ret.reverse();
return (ret, None);
};
modules_by_chunk_group.sort_unstable_by(compare_module_lists);
let mut final_modules: Vec<&'module dyn Module> = vec![];
let mut conflicts: Option<Vec<CssOrderConflicts>> = None;
loop {
let mut failed_modules: IdentifierSet = Default::default();
let list = modules_by_chunk_group[0].list.clone();
if list.is_empty() {
break;
}
let mut selected_module = *list.last().expect("list should not be empty");
let mut has_failed = None;
'outer: loop {
for SortedModules { set, list } in &modules_by_chunk_group {
if list.is_empty() {
continue;
}
let last_module = *list.last().expect("list should not be empty");
if last_module.identifier() == selected_module.identifier() {
continue;
}
if !set.contains(&selected_module.identifier()) {
continue;
}
failed_modules.insert(selected_module.identifier());
if failed_modules.contains(&last_module.identifier()) {
has_failed = Some(last_module);
continue;
}
selected_module = last_module;
has_failed = None;
continue 'outer;
}
break;
}
if let Some(has_failed) = has_failed {
tracing::warn!("Conflicting order between");
let conflict = CssOrderConflicts {
chunk: chunk.ukey(),
failed_module: has_failed.identifier(),
selected_module: selected_module.identifier(),
};
if let Some(conflicts) = &mut conflicts {
conflicts.push(conflict);
} else {
conflicts = Some(vec![conflict])
}
selected_module = has_failed;
}
final_modules.push(selected_module);
for SortedModules { set, list } in &mut modules_by_chunk_group {
let last_module = list.last();
if last_module
.is_some_and(|last_module| last_module.identifier() == selected_module.identifier())
{
list.pop();
set.remove(&selected_module.identifier());
} else if has_failed.is_some() && set.contains(&selected_module.identifier()) {
let idx = list
.iter()
.position(|m| m.identifier() == selected_module.identifier());
if let Some(idx) = idx {
list.remove(idx);
}
}
}
modules_by_chunk_group.sort_unstable_by(compare_module_lists);
}
(final_modules, conflicts)
}
}
#[derive(Debug)]
struct SortedModules<'module> {
pub list: Vec<&'module dyn Module>,
pub set: IdentifierSet,
}
fn compare_module_lists(a: &SortedModules, b: &SortedModules) -> cmp::Ordering {
let a = &a.list;
let b = &b.list;
if a.is_empty() {
if b.is_empty() {
cmp::Ordering::Equal
} else {
cmp::Ordering::Greater
}
} else if b.is_empty() {
cmp::Ordering::Less
} else {
compare_modules_by_identifier(
&a.last().expect("Must have a module").identifier(),
&b.last().expect("Must have a module").identifier(),
)
}
}