use std::{
collections::{BTreeMap, BTreeSet},
fmt::Debug,
hash::BuildHasherDefault,
};
use dyn_clone::{DynClone, clone_trait_object};
use hashlink::LinkedHashSet;
use indexmap::IndexMap;
use rspack_cacheable::{
cacheable, cacheable_dyn,
with::{AsPreset, AsTuple2, AsVec},
};
use rspack_error::Result;
use rspack_hash::{RspackHash, RspackHasher};
use rspack_sources::{BoxSource, ConcatSource, RawStringSource, SourceExt};
use rspack_util::ext::IntoAny;
use rustc_hash::FxHasher;
use swc_core::ecma::atoms::Atom;
use crate::{
ExportsArgument, GenerateContext, ModuleCodeTemplate, RuntimeCondition, RuntimeGlobals,
merge_runtime, property_name,
};
pub struct InitFragmentContents {
pub start: String,
pub end: Option<String>,
}
#[cacheable]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum InitFragmentKey {
ESMImport(String),
ESMExports,
ESMEmptyReexport(String),
ESMUnusedReexport(String),
ESMFakeNamespaceObjectFragment(String),
ESMDeferImportNamespaceObjectFragment(String),
ESMDynamicReexport(String),
CommonJsExports(String),
ModuleExternal(String),
ExternalModule(String),
AwaitDependencies,
AsyncBoundary(String ),
ESMCompatibility,
ModuleDecorator(String ),
Const(String),
}
impl RspackHash for InitFragmentKey {
fn hash(&self, state: &mut RspackHasher) {
match self {
Self::ESMImport(value) => {
"esm-import".hash(state);
value.hash(state);
}
Self::ESMExports => "esm-exports".hash(state),
Self::ESMEmptyReexport(value) => {
"esm-empty-reexport".hash(state);
value.hash(state);
}
Self::ESMUnusedReexport(value) => {
"esm-unused-reexport".hash(state);
value.hash(state);
}
Self::ESMFakeNamespaceObjectFragment(value) => {
"esm-fake-namespace-object".hash(state);
value.hash(state);
}
Self::ESMDeferImportNamespaceObjectFragment(value) => {
"esm-defer-import-namespace-object".hash(state);
value.hash(state);
}
Self::ESMDynamicReexport(value) => {
"esm-dynamic-reexport".hash(state);
value.hash(state);
}
Self::CommonJsExports(value) => {
"commonjs-exports".hash(state);
value.hash(state);
}
Self::ModuleExternal(value) => {
"module-external".hash(state);
value.hash(state);
}
Self::ExternalModule(value) => {
"external-module".hash(state);
value.hash(state);
}
Self::AwaitDependencies => "await-dependencies".hash(state),
Self::AsyncBoundary(value) => {
"async-boundary".hash(state);
value.hash(state);
}
Self::ESMCompatibility => "esm-compatibility".hash(state),
Self::ModuleDecorator(value) => {
"module-decorator".hash(state);
value.hash(state);
}
Self::Const(value) => {
"const".hash(state);
value.hash(state);
}
}
}
}
impl InitFragmentKey {
pub fn merge_fragments(&self, fragments: Vec<BoxInitFragment>) -> BoxInitFragment {
match self {
InitFragmentKey::ESMImport(_) => {
let mut iter = fragments.into_iter();
let first = iter
.next()
.expect("keyed_fragments should at least have one value");
let first = first
.into_any()
.downcast::<ConditionalInitFragment>()
.expect("fragment of InitFragmentKey::ESMImport should be a ConditionalInitFragment");
if matches!(first.runtime_condition, RuntimeCondition::Boolean(true)) {
return first;
}
let mut res = first;
for fragment in iter {
let fragment = fragment
.into_any()
.downcast::<ConditionalInitFragment>()
.expect("fragment of InitFragmentKey::ESMImport should be a ConditionalInitFragment");
res = ConditionalInitFragment::merge(res, fragment);
if matches!(res.runtime_condition, RuntimeCondition::Boolean(true)) {
return res;
}
}
res
}
InitFragmentKey::ESMExports => {
let mut export_map: Vec<(Atom, ESMExportBinding)> = vec![];
let mut iter = fragments.into_iter();
let first = iter
.next()
.expect("keyed_fragments should at least have one value");
let first = first
.into_any()
.downcast::<ESMExportInitFragment>()
.expect("fragment of InitFragmentKey::ESMExports should be a ESMExportInitFragment");
let export_argument = first.exports_argument;
let is_circular_module = first.is_circular_module;
export_map.extend(first.export_map);
for fragment in iter {
let fragment = fragment
.into_any()
.downcast::<ESMExportInitFragment>()
.expect("fragment of InitFragmentKey::ESMExports should be a ESMExportInitFragment");
debug_assert_eq!(is_circular_module, fragment.is_circular_module);
export_map.extend(fragment.export_map);
}
ESMExportInitFragment::new(export_argument, export_map, is_circular_module).boxed()
}
InitFragmentKey::AwaitDependencies => {
let promises = fragments.into_iter().map(|f| f.into_any().downcast::<AwaitDependenciesInitFragment>().expect("fragment of InitFragmentKey::AwaitDependencies should be a AwaitDependenciesInitFragment")).flat_map(|f| f.promises).collect();
AwaitDependenciesInitFragment::new(promises).boxed()
}
InitFragmentKey::ExternalModule(_) => {
let mut iter = fragments.into_iter();
let first = iter
.next()
.expect("keyed_fragments should at least have one value");
let first = first
.into_any()
.downcast::<ExternalModuleInitFragment>()
.expect(
"fragment of InitFragmentKey::ExternalModule should be a ExternalModuleInitFragment",
);
let mut res = first;
for fragment in iter {
let fragment = fragment
.into_any()
.downcast::<ExternalModuleInitFragment>()
.expect(
"fragment of InitFragmentKey::ExternalModule should be a ExternalModuleInitFragment",
);
res = ExternalModuleInitFragment::merge(*res, *fragment);
}
res
}
InitFragmentKey::ESMFakeNamespaceObjectFragment(_)
| InitFragmentKey::ESMDeferImportNamespaceObjectFragment(_)
| InitFragmentKey::AsyncBoundary(_)
| InitFragmentKey::ESMEmptyReexport(_)
| InitFragmentKey::ESMUnusedReexport(_)
| InitFragmentKey::ESMDynamicReexport(_)
| InitFragmentKey::ModuleExternal(_)
| InitFragmentKey::ModuleDecorator(_)
| InitFragmentKey::CommonJsExports(_)
| InitFragmentKey::ESMCompatibility
| InitFragmentKey::Const(_) => first(fragments),
}
}
}
fn first(fragments: Vec<BoxInitFragment>) -> BoxInitFragment {
fragments
.into_iter()
.next()
.expect("should at least have one fragment")
}
pub trait InitFragmentRenderContext {
fn runtime_condition_expression(&mut self, runtime_condition: &RuntimeCondition) -> String;
fn runtime_template(&mut self) -> &mut ModuleCodeTemplate;
}
#[cacheable_dyn]
pub trait InitFragment: IntoAny + RspackHash + DynClone + Debug + Sync + Send {
fn contents(
self: Box<Self>,
context: &mut dyn InitFragmentRenderContext,
) -> Result<InitFragmentContents>;
fn stage(&self) -> InitFragmentStage;
fn position(&self) -> i32;
fn key(&self) -> &InitFragmentKey;
fn top_level_decl_symbols(&self) -> &[Atom] {
&[]
}
}
clone_trait_object!(InitFragment);
pub trait InitFragmentExt {
fn boxed(self) -> BoxInitFragment;
}
impl<T: InitFragment + 'static> InitFragmentExt for T {
fn boxed(self) -> BoxInitFragment {
Box::new(self)
}
}
#[cacheable]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum InitFragmentStage {
StageConstants,
StageAsyncBoundary,
StageESMExports,
StageESMImports,
StageProvides,
StageAsyncDependencies,
StageAsyncESMImports,
}
impl RspackHash for InitFragmentStage {
fn hash(&self, state: &mut RspackHasher) {
match self {
Self::StageConstants => "constants",
Self::StageAsyncBoundary => "async-boundary",
Self::StageESMExports => "esm-exports",
Self::StageESMImports => "esm-imports",
Self::StageProvides => "provides",
Self::StageAsyncDependencies => "async-dependencies",
Self::StageAsyncESMImports => "async-esm-imports",
}
.hash(state);
}
}
pub fn render_init_fragments(
source: BoxSource,
mut fragments: Vec<BoxInitFragment>,
context: &mut dyn InitFragmentRenderContext,
) -> Result<BoxSource> {
fragments.sort_by(|a, b| {
let stage = a.stage().cmp(&b.stage());
if !stage.is_eq() {
return stage;
}
a.position().cmp(&b.position())
});
let mut keyed_fragments: IndexMap<
InitFragmentKey,
Vec<BoxInitFragment>,
BuildHasherDefault<FxHasher>,
> = IndexMap::default();
for fragment in fragments {
let key = fragment.key();
if let Some(value) = keyed_fragments.get_mut(key) {
value.push(fragment);
} else {
keyed_fragments.insert(key.clone(), vec![fragment]);
}
}
let mut end_contents = vec![];
let mut concat_source = ConcatSource::default();
for (key, fragments) in keyed_fragments {
let f = key.merge_fragments(fragments);
let contents = f.contents(context)?;
concat_source.add(RawStringSource::from(contents.start));
if let Some(end_content) = contents.end {
end_contents.push(RawStringSource::from(end_content))
}
}
concat_source.add(source);
for content in end_contents.into_iter().rev() {
concat_source.add(content);
}
Ok(concat_source.boxed())
}
pub type BoxInitFragment = Box<dyn InitFragment>;
pub type BoxModuleInitFragment = BoxInitFragment;
pub type BoxChunkInitFragment = BoxInitFragment;
pub type ModuleInitFragments = Vec<BoxModuleInitFragment>;
pub type ChunkInitFragments = Vec<BoxChunkInitFragment>;
impl InitFragmentRenderContext for GenerateContext<'_> {
fn runtime_condition_expression(&mut self, runtime_condition: &RuntimeCondition) -> String {
self.runtime_template.runtime_condition_expression(
&self.compilation.build_chunk_graph_artifact.chunk_graph,
Some(runtime_condition),
self.runtime,
)
}
fn runtime_template(&mut self) -> &mut ModuleCodeTemplate {
self.runtime_template
}
}
pub struct ChunkRenderContext;
impl InitFragmentRenderContext for ChunkRenderContext {
fn runtime_condition_expression(&mut self, _runtime_condition: &RuntimeCondition) -> String {
unreachable!("should not call runtime condition expression in chunk render context")
}
fn runtime_template(&mut self) -> &mut ModuleCodeTemplate {
unreachable!("should not call runtime template in chunk render context")
}
}
#[cacheable]
#[derive(Debug, Clone, rspack_hash::RspackHash)]
pub struct NormalInitFragment {
content: String,
stage: InitFragmentStage,
position: i32,
key: InitFragmentKey,
end_content: Option<String>,
#[cacheable(with=AsVec<AsPreset>)]
top_level_decl_symbols: Vec<Atom>,
}
impl NormalInitFragment {
pub fn new(
content: String,
stage: InitFragmentStage,
position: i32,
key: InitFragmentKey,
end_content: Option<String>,
) -> Self {
NormalInitFragment {
content,
stage,
position,
key,
end_content,
top_level_decl_symbols: Vec::new(),
}
}
pub fn with_top_level_decl_symbols(mut self, top_level_decl_symbols: Vec<Atom>) -> Self {
self.top_level_decl_symbols = top_level_decl_symbols;
self
}
}
#[cacheable_dyn]
impl InitFragment for NormalInitFragment {
fn contents(
self: Box<Self>,
_context: &mut dyn InitFragmentRenderContext,
) -> Result<InitFragmentContents> {
Ok(InitFragmentContents {
start: self.content,
end: self.end_content,
})
}
fn stage(&self) -> InitFragmentStage {
self.stage
}
fn position(&self) -> i32 {
self.position
}
fn key(&self) -> &InitFragmentKey {
&self.key
}
fn top_level_decl_symbols(&self) -> &[Atom] {
&self.top_level_decl_symbols
}
}
#[cacheable]
#[derive(Debug, Clone)]
pub enum ESMExportBinding {
Getter(#[cacheable(with=AsPreset)] Atom),
Value(#[cacheable(with=AsPreset)] Atom),
}
impl RspackHash for ESMExportBinding {
fn hash(&self, state: &mut RspackHasher) {
match self {
ESMExportBinding::Getter(value) => {
"getter".hash(state);
value.hash(state);
}
ESMExportBinding::Value(value) => {
"value".hash(state);
value.hash(state);
}
}
}
}
#[cacheable]
#[derive(Debug, Clone, rspack_hash::RspackHash)]
pub struct ESMExportInitFragment {
exports_argument: ExportsArgument,
#[cacheable(with=AsVec<AsTuple2<AsPreset>>)]
export_map: Vec<(Atom, ESMExportBinding)>,
is_circular_module: Option<bool>,
}
impl ESMExportInitFragment {
pub fn new(
exports_argument: ExportsArgument,
export_map: Vec<(Atom, ESMExportBinding)>,
is_circular_module: Option<bool>,
) -> Self {
Self {
exports_argument,
export_map,
is_circular_module,
}
}
}
#[cacheable_dyn]
impl InitFragment for ESMExportInitFragment {
fn contents(
mut self: Box<Self>,
context: &mut dyn InitFragmentRenderContext,
) -> Result<InitFragmentContents> {
let runtime_template = context.runtime_template();
self.export_map.sort_by(|a, b| a.0.cmp(&b.0));
let mut content =
runtime_template.render_runtime_globals(&RuntimeGlobals::DEFINE_PROPERTY_GETTERS);
content.push('(');
content.push_str(&runtime_template.render_exports_argument(self.exports_argument));
content.push_str(", {");
let mut getters = self
.export_map
.iter()
.filter_map(|(key, value)| match value {
ESMExportBinding::Getter(getter) => Some((key, getter)),
_ => None,
});
if let Some((key, getter)) = getters.next() {
content.push_str("\n ");
content.push_str(&property_name(key)?);
content.push_str(": ");
content.push_str(&runtime_template.returning_function(getter, ""));
}
for (key, getter) in getters {
content.push_str(",\n ");
content.push_str(&property_name(key)?);
content.push_str(": ");
content.push_str(&runtime_template.returning_function(getter, ""));
}
content.push_str("\n}");
let mut values_content = String::new();
let mut values = self
.export_map
.iter()
.filter_map(|(key, value)| match value {
ESMExportBinding::Value(value) => Some((key, value)),
_ => None,
});
if let Some((key, value)) = values.next() {
values_content.push_str("\n ");
values_content.push_str(&property_name(key)?);
values_content.push_str(": ");
values_content.push_str(value);
}
for (key, value) in values {
values_content.push_str(",\n ");
values_content.push_str(&property_name(key)?);
values_content.push_str(": ");
values_content.push_str(value);
}
if values_content.is_empty() {
content.push_str(");\n");
} else {
content.push_str(", {");
content.push_str(&values_content);
content.push_str("\n});\n");
}
let res = if matches!(self.is_circular_module, None | Some(true)) {
InitFragmentContents {
start: content,
end: None,
}
} else {
InitFragmentContents {
start: String::new(),
end: Some(format!("\n{content}")),
}
};
Ok(res)
}
fn stage(&self) -> InitFragmentStage {
InitFragmentStage::StageESMExports
}
fn position(&self) -> i32 {
1
}
fn key(&self) -> &InitFragmentKey {
&InitFragmentKey::ESMExports
}
}
#[cacheable]
#[derive(Debug, Clone)]
pub struct AwaitDependenciesInitFragment {
#[cacheable(with=AsVec)]
promises: LinkedHashSet<String, BuildHasherDefault<FxHasher>>,
}
impl AwaitDependenciesInitFragment {
pub fn new(promises: LinkedHashSet<String, BuildHasherDefault<FxHasher>>) -> Self {
Self { promises }
}
pub fn new_single(promise: String) -> Self {
let mut promises = LinkedHashSet::default();
promises.insert(promise);
Self { promises }
}
}
impl RspackHash for AwaitDependenciesInitFragment {
fn hash(&self, state: &mut RspackHasher) {
for promise in &self.promises {
promise.hash(state);
}
}
}
#[cacheable_dyn]
impl InitFragment for AwaitDependenciesInitFragment {
fn contents(
self: Box<Self>,
_context: &mut dyn InitFragmentRenderContext,
) -> Result<InitFragmentContents> {
if self.promises.is_empty() {
Ok(InitFragmentContents {
start: String::new(),
end: None,
})
} else if self.promises.len() == 1 {
let sep = self.promises.front().expect("at least have one");
Ok(InitFragmentContents {
start: format!(
"var __rspack_async_deps = __rspack_load_async_deps([{sep}]);\n{sep} = (__rspack_async_deps.then ? (await __rspack_async_deps)() : __rspack_async_deps)[0];"
),
end: None,
})
} else {
let sep = Vec::from_iter(self.promises).join(", ");
Ok(InitFragmentContents {
start: format!(
"var __rspack_async_deps = __rspack_load_async_deps([{sep}]);\n([{sep}] = __rspack_async_deps.then ? (await __rspack_async_deps)() : __rspack_async_deps);"
),
end: None,
})
}
}
fn stage(&self) -> InitFragmentStage {
InitFragmentStage::StageAsyncDependencies
}
fn position(&self) -> i32 {
0
}
fn key(&self) -> &InitFragmentKey {
&InitFragmentKey::AwaitDependencies
}
}
#[cacheable]
#[derive(Debug, Clone, rspack_hash::RspackHash)]
pub struct ConditionalInitFragment {
content: String,
stage: InitFragmentStage,
position: i32,
key: InitFragmentKey,
end_content: Option<String>,
runtime_condition: RuntimeCondition,
}
impl ConditionalInitFragment {
pub fn new(
content: String,
stage: InitFragmentStage,
position: i32,
key: InitFragmentKey,
end_content: Option<String>,
runtime_condition: RuntimeCondition,
) -> Self {
ConditionalInitFragment {
content,
stage,
position,
key,
end_content,
runtime_condition,
}
}
pub fn content(&self) -> &str {
&self.content
}
pub fn merge(
one: Box<ConditionalInitFragment>,
other: Box<ConditionalInitFragment>,
) -> Box<ConditionalInitFragment> {
if matches!(one.runtime_condition, RuntimeCondition::Boolean(true)) {
return one;
}
if matches!(other.runtime_condition, RuntimeCondition::Boolean(true)) {
return other;
}
if matches!(one.runtime_condition, RuntimeCondition::Boolean(false)) {
return other;
}
if matches!(other.runtime_condition, RuntimeCondition::Boolean(false)) {
return one;
}
Box::new(Self {
content: one.content,
stage: one.stage,
position: one.position,
key: one.key,
end_content: one.end_content,
runtime_condition: RuntimeCondition::Spec(merge_runtime(
one.runtime_condition.as_spec().expect("should be spec"),
other.runtime_condition.as_spec().expect("should be spec"),
)),
})
}
}
#[cacheable_dyn]
impl InitFragment for ConditionalInitFragment {
fn contents(
self: Box<Self>,
context: &mut dyn InitFragmentRenderContext,
) -> Result<InitFragmentContents> {
Ok(
if matches!(self.runtime_condition, RuntimeCondition::Boolean(false))
|| self.content.is_empty()
{
InitFragmentContents {
start: String::new(),
end: Some(String::new()),
}
} else if matches!(self.runtime_condition, RuntimeCondition::Boolean(true)) {
InitFragmentContents {
start: self.content,
end: self.end_content,
}
} else {
let condition = context.runtime_condition_expression(&self.runtime_condition);
if condition == "true" {
InitFragmentContents {
start: self.content,
end: self.end_content,
}
} else {
InitFragmentContents {
start: wrap_in_condition(&condition, &self.content),
end: self.end_content.map(|c| wrap_in_condition(&condition, &c)),
}
}
},
)
}
fn stage(&self) -> InitFragmentStage {
self.stage
}
fn position(&self) -> i32 {
self.position
}
fn key(&self) -> &InitFragmentKey {
&self.key
}
}
fn wrap_in_condition(condition: &str, source: &str) -> String {
format!(
r#"if ({condition}) {{
{source}
}}"#
)
}
#[cacheable]
#[derive(Debug, Clone, rspack_hash::RspackHash)]
pub struct ExternalModuleInitFragment {
imported_module: String,
import_specifiers: BTreeMap<String, BTreeSet<String>>,
default_import: Option<String>,
#[cacheable(with=AsVec<AsPreset>)]
top_level_decl_symbols: Vec<Atom>,
stage: InitFragmentStage,
position: i32,
key: InitFragmentKey,
}
impl ExternalModuleInitFragment {
pub fn new(
imported_module: String,
import_specifiers: Vec<(String, String)>,
default_import: Option<String>,
stage: InitFragmentStage,
position: i32,
) -> Self {
let mut self_import_specifiers: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for (name, value) in import_specifiers {
if let Some(set) = self_import_specifiers.get_mut(&name) {
set.insert(value);
} else {
let mut set = BTreeSet::new();
set.insert(value);
self_import_specifiers.insert(name, set);
}
}
let key = InitFragmentKey::ExternalModule(format!(
"external module imports|{}|{}",
imported_module,
default_import.clone().unwrap_or_else(|| "null".to_string()),
));
let top_level_decl_symbols =
Self::collect_top_level_decl_symbols(&self_import_specifiers, default_import.as_deref());
Self {
imported_module,
import_specifiers: self_import_specifiers,
default_import,
top_level_decl_symbols,
stage,
position,
key,
}
}
fn collect_top_level_decl_symbols(
import_specifiers: &BTreeMap<String, BTreeSet<String>>,
default_import: Option<&str>,
) -> Vec<Atom> {
let mut symbols = import_specifiers
.values()
.flatten()
.cloned()
.collect::<BTreeSet<_>>();
if let Some(default_import) = default_import {
symbols.insert(default_import.to_string());
}
symbols.into_iter().map(Atom::from).collect()
}
pub fn merge(
one: ExternalModuleInitFragment,
other: ExternalModuleInitFragment,
) -> Box<ExternalModuleInitFragment> {
let Self {
imported_module,
mut import_specifiers,
default_import,
stage,
position,
key,
..
} = one;
for (name, value) in other.import_specifiers {
if let Some(set) = import_specifiers.get_mut(&name) {
set.extend(value);
} else {
import_specifiers.insert(name, value);
}
}
let top_level_decl_symbols =
Self::collect_top_level_decl_symbols(&import_specifiers, default_import.as_deref());
Box::new(Self {
imported_module,
import_specifiers,
default_import,
top_level_decl_symbols,
stage,
position,
key,
})
}
}
#[cacheable_dyn]
impl InitFragment for ExternalModuleInitFragment {
fn contents(
self: Box<Self>,
_context: &mut dyn InitFragmentRenderContext,
) -> Result<InitFragmentContents> {
let mut named_imports = vec![];
for (name, specifiers) in self.import_specifiers {
for spec in specifiers {
if name == spec {
named_imports.push(spec);
} else {
named_imports.push(format!("{name} as {spec}"));
}
}
}
let mut imports_string: String;
imports_string = if named_imports.is_empty() {
String::new()
} else {
format!("{{{}}}", named_imports.join(", "))
};
if let Some(default_import) = self.default_import {
imports_string = format!(
"{}{}",
default_import,
if imports_string.is_empty() {
String::new()
} else {
format!(", {imports_string}")
}
);
}
let start = format!(
"import {} from {};\n",
imports_string,
rspack_util::json_stringify_str(&self.imported_module)
);
Ok(InitFragmentContents { start, end: None })
}
fn stage(&self) -> InitFragmentStage {
self.stage
}
fn position(&self) -> i32 {
self.position
}
fn key(&self) -> &InitFragmentKey {
&self.key
}
fn top_level_decl_symbols(&self) -> &[Atom] {
&self.top_level_decl_symbols
}
}