use oxc::{
allocator::{Allocator, Box as ArenaBox, IntoIn, TakeIn},
ast::{
AstBuilder, NONE,
ast::{self, ExportDefaultDeclarationKind, Expression, ObjectPropertyKind, Statement},
},
semantic::{IsGlobalReference, Scoping, SymbolId},
span::{Atom, SPAN, Span},
};
use rolldown_common::{
ExternalModule, ImportRecordIdx, IndexModules, Module, ModuleIdx, NormalModule,
};
use rolldown_ecmascript::CJS_REQUIRE_REF_ATOM;
use rolldown_ecmascript_utils::{AstSnippet, BindingIdentifierExt, ExpressionExt};
use rolldown_utils::indexmap::{FxIndexMap, FxIndexSet};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::hmr::utils::{HmrAstBuilder, MODULE_EXPORTS_NAME_FOR_ESM};
pub struct HmrAstFinalizer<'me, 'ast> {
pub alloc: &'ast Allocator,
pub snippet: AstSnippet<'ast>,
pub builder: &'me AstBuilder<'ast>,
pub modules: &'me IndexModules,
pub module: &'me NormalModule,
pub affected_module_idx_to_init_fn_name: &'me FxHashMap<ModuleIdx, String>,
pub use_pife_for_module_wrappers: bool,
pub unique_index: usize,
pub import_bindings: FxHashMap<SymbolId, String>,
pub exports: oxc::allocator::Vec<'ast, ObjectPropertyKind<'ast>>,
pub re_export_all_dependencies: FxIndexSet<ModuleIdx>,
pub dependencies: FxIndexSet<ModuleIdx>,
pub imports: FxHashSet<ModuleIdx>,
pub generated_static_import_infos: FxHashMap<ModuleIdx, String>,
pub generated_static_import_stmts_from_external: FxIndexMap<ModuleIdx, ast::Statement<'ast>>,
pub named_exports: FxHashMap<Atom<'ast>, NamedExport>,
}
impl<'ast> HmrAstFinalizer<'_, 'ast> {
#[expect(clippy::too_many_lines)]
pub fn handle_top_level_stmt(
&mut self,
program_body: &mut oxc::allocator::Vec<'ast, ast::Statement<'ast>>,
mut node: ast::Statement<'ast>,
scoping: &Scoping,
) {
match node {
ref mut module_decl @ ast::match_module_declaration!(Statement) => {
let module_decl = module_decl.to_module_declaration_mut();
match module_decl {
ast::ModuleDeclaration::ImportDeclaration(import_decl) => {
let rec_id = self.module.imports[&import_decl.span];
let rec = &self.module.import_records[rec_id];
let importee = &self.modules[rec.resolved_module];
self.dependencies.insert(rec.resolved_module);
let binding_name =
self.ensure_static_import_info(rec.resolved_module, rec_id).to_string();
import_decl.specifiers.as_ref().inspect(|specifiers| {
specifiers.iter().for_each(|spec| match spec {
ast::ImportDeclarationSpecifier::ImportSpecifier(import_specifier) => {
self.import_bindings.insert(
import_specifier.local.expect_symbol_id(),
format!("{binding_name}.{}", import_specifier.imported.name()),
);
}
ast::ImportDeclarationSpecifier::ImportDefaultSpecifier(
import_default_specifier,
) => {
self.import_bindings.insert(
import_default_specifier.local.expect_symbol_id(),
format!("{binding_name}.default"),
);
}
ast::ImportDeclarationSpecifier::ImportNamespaceSpecifier(
import_namespace_specifier,
) => {
self.import_bindings.insert(
import_namespace_specifier.local.expect_symbol_id(),
binding_name.to_string(),
);
}
});
});
match importee {
Module::Normal(_) => {
if let Some(stmt) =
self.create_load_exports_call_stmt(importee, &binding_name, import_decl.span)
{
program_body.push(stmt);
}
}
Module::External(importee_ext) => {
self.create_static_import_stmt_from_external_module(
importee_ext,
&binding_name,
import_decl.span,
);
}
}
}
ast::ModuleDeclaration::ExportNamedDeclaration(decl) => {
if let Some(_source) = &decl.source {
let rec_id = self.module.imports[&decl.span];
let rec = &self.module.import_records[rec_id];
let importee = &self.modules[rec.resolved_module];
self.dependencies.insert(rec.resolved_module);
let binding_name =
self.ensure_static_import_info(rec.resolved_module, rec_id).to_string();
self.exports.extend(decl.specifiers.iter().map(|specifier| {
self.snippet.object_property_kind_object_property(
&specifier.exported.name(),
match &specifier.local {
ast::ModuleExportName::IdentifierName(ident) => {
Expression::StaticMemberExpression(
self.snippet.builder.alloc_static_member_expression(
SPAN,
self.snippet.id_ref_expr(&binding_name, SPAN),
self.snippet.builder.identifier_name(SPAN, ident.name.as_str()),
false,
),
)
}
ast::ModuleExportName::StringLiteral(str) => {
Expression::ComputedMemberExpression(
self.snippet.builder.alloc_computed_member_expression(
SPAN,
self.snippet.id_ref_expr(&binding_name, SPAN),
self.snippet.builder.expression_string_literal(
SPAN, str.value.as_str(), None
),
false,
),
)
}
ast::ModuleExportName::IdentifierReference(_) => {
unreachable!(
"ModuleExportName IdentifierReference is invalid in ExportNamedDeclaration with source"
)
}
},
matches!(specifier.exported, ast::ModuleExportName::StringLiteral(_))
)
}));
if let Some(stmt) =
self.create_load_exports_call_stmt(importee, &binding_name, decl.span)
{
program_body.push(stmt);
}
} else if let Some(decl) = &mut decl.declaration {
match decl {
ast::Declaration::VariableDeclaration(var_decl) => {
self.exports.extend(var_decl.declarations.iter().filter_map(|decl| {
decl.id.get_identifier_name().map(|ident| {
self.snippet.object_property_kind_object_property(
ident.as_str(),
self.snippet.id_ref_expr(ident.as_str(), SPAN),
false,
)
})
}));
}
ast::Declaration::FunctionDeclaration(fn_decl) => {
let id = fn_decl.id.as_ref().unwrap().name.as_str();
self.exports.push(self.snippet.object_property_kind_object_property(
id,
self.snippet.id_ref_expr(id, SPAN),
false,
));
}
ast::Declaration::ClassDeclaration(cls_decl) => {
let id = cls_decl.id.as_ref().unwrap().name.as_str();
self.exports.push(self.snippet.object_property_kind_object_property(
id,
self.snippet.id_ref_expr(id, SPAN),
false,
));
}
_ => unreachable!("doesn't support ts now"),
}
program_body.push(ast::Statement::from(decl.take_in(self.alloc)));
} else {
decl.specifiers.iter().for_each(|specifier| {
if let Some(symbol_id) = scoping.get_root_binding(&specifier.local.name()) {
self
.named_exports
.insert(specifier.exported.name(), NamedExport { local_binding: symbol_id });
} else {
}
});
}
}
ast::ModuleDeclaration::ExportDefaultDeclaration(decl) => match &mut decl.declaration {
ast::ExportDefaultDeclarationKind::FunctionDeclaration(function) => {
if let Some(id) = &function.id {
self.exports.push(self.snippet.object_property_kind_object_property(
"default",
self.snippet.id_ref_expr(&id.name, SPAN),
false,
));
} else {
function.id = Some(self.snippet.id("__rolldown_default__", SPAN));
self.exports.push(self.snippet.object_property_kind_object_property(
"default",
self.snippet.id_ref_expr("__rolldown_default__", SPAN),
false,
));
}
program_body.push(ast::Statement::FunctionDeclaration(ArenaBox::new_in(
function.as_mut().take_in(self.alloc),
self.alloc,
)));
}
ast::ExportDefaultDeclarationKind::ClassDeclaration(class) => {
if let Some(id) = &class.id {
self.exports.push(self.snippet.object_property_kind_object_property(
"default",
self.snippet.id_ref_expr(&id.name, SPAN),
false,
));
} else {
class.id = Some(self.snippet.id("__rolldown_default__", SPAN));
self.exports.push(self.snippet.object_property_kind_object_property(
"default",
self.snippet.id_ref_expr("__rolldown_default__", SPAN),
false,
));
}
program_body.push(ast::Statement::ClassDeclaration(ArenaBox::new_in(
class.as_mut().take_in(self.alloc),
self.alloc,
)));
}
expr @ ast::match_expression!(ExportDefaultDeclarationKind) => {
let expr = expr.to_expression_mut();
program_body
.push(self.snippet.var_decl_stmt("__rolldown_default__", expr.take_in(self.alloc)));
self.exports.push(self.snippet.object_property_kind_object_property(
"default",
self.snippet.id_ref_expr("__rolldown_default__", SPAN),
false,
));
}
unhandled_kind => {
unreachable!("Unexpected export default declaration kind: {unhandled_kind:#?}");
}
},
ast::ModuleDeclaration::ExportAllDeclaration(export_all_decl) => {
let rec_id = self.module.imports[&export_all_decl.span];
let rec = &self.module.import_records[rec_id];
let importee = &self.modules[rec.resolved_module];
self.dependencies.insert(rec.resolved_module);
let binding_name =
self.ensure_static_import_info(rec.resolved_module, rec_id).to_string();
if let Some(stmt) =
self.create_load_exports_call_stmt(importee, &binding_name, export_all_decl.span)
{
program_body.push(stmt);
}
if let Some(stmt) =
self.create_re_export_call_stmt(importee, &binding_name, export_all_decl.span)
{
program_body.push(stmt);
}
}
ast::ModuleDeclaration::TSExportAssignment(_)
| ast::ModuleDeclaration::TSNamespaceExportDeclaration(_) => {
program_body.push(node);
}
}
}
_ => {
program_body.push(node);
}
}
}
pub fn ensure_static_import_info(
&mut self,
importee_idx: ModuleIdx,
rec_id: ImportRecordIdx,
) -> &str {
self.generated_static_import_infos.entry(importee_idx).or_insert_with(|| {
let importee = &self.modules[importee_idx];
format!("import_{}_{}{}", importee.repr_name(), self.unique_index, rec_id.raw())
})
}
pub fn module_exports_name(&self) -> &'static str {
if self.module.exports_kind.is_commonjs() {
"module.exports"
} else {
MODULE_EXPORTS_NAME_FOR_ESM
}
}
pub fn generate_runtime_module_register_for_hmr(
&mut self,
scoping: &Scoping,
) -> Vec<ast::Statement<'ast>> {
let mut ret = vec![];
if self.module.exports_kind == rolldown_common::ExportsKind::Esm {
let binding_name_for_namespace_object_ref = self.module_exports_name();
ret.extend(self.generate_declaration_of_module_namespace_object(
binding_name_for_namespace_object_ref,
scoping,
));
}
ret.push(self.create_register_module_stmt());
ret
}
pub fn rewrite_hot_accept_call_deps(&self, call_expr: &mut ast::CallExpression<'ast>) {
if !call_expr.callee.is_import_meta_hot_accept() {
return;
}
if call_expr.arguments.is_empty() {
return;
}
match &mut call_expr.arguments[0] {
ast::Argument::StringLiteral(string_literal) => {
let import_record = &self.module.import_records
[self.module.hmr_info.module_request_to_import_record_idx[string_literal.value.as_str()]];
string_literal.value =
self.snippet.builder.atom(self.modules[import_record.resolved_module].stable_id());
}
ast::Argument::ArrayExpression(array_expression) => {
array_expression.elements.iter_mut().for_each(|element| {
if let ast::ArrayExpressionElement::StringLiteral(string_literal) = element {
let import_record =
&self.module.import_records[self.module.hmr_info.module_request_to_import_record_idx
[string_literal.value.as_str()]];
string_literal.value =
self.snippet.builder.atom(self.modules[import_record.resolved_module].stable_id());
}
});
}
_ => {}
}
}
pub fn rewrite_import_meta_hot(&self, expr: &mut ast::Expression<'ast>) {
if expr.is_import_meta_hot() {
let hot_name = format!("hot_{}", self.module.repr_name);
*expr = self.snippet.id_ref_expr(&hot_name, SPAN);
}
}
fn create_load_exports_call_stmt(
&mut self,
importee: &Module,
binding_name: &str,
span: Span,
) -> Option<Statement<'ast>> {
if self.imports.contains(&importee.idx()) {
return None;
}
self.imports.insert(importee.idx());
let id = &importee.stable_id();
let interop = match importee {
Module::Normal(importee) => self.module.interop(importee),
Module::External(_) => None,
};
let call_expr = self.snippet.call_expr_with_arg_expr(
self.snippet.literal_prop_access_member_expr_expr("__rolldown_runtime__", "loadExports"),
self.snippet.string_literal_expr(id, SPAN),
false,
);
let stmt = self.snippet.variable_declarator_require_call_stmt(
binding_name,
self.snippet.to_esm_call_with_interop("__rolldown_runtime__.__toESM", call_expr, interop),
span,
);
Some(stmt)
}
fn create_static_import_stmt_from_external_module(
&mut self,
importee: &ExternalModule,
binding_name: &str,
_span: Span,
) {
if self.generated_static_import_stmts_from_external.contains_key(&importee.idx) {
return;
}
let module_request = &importee.id;
let stmt = self.snippet.import_star_stmt(module_request, binding_name);
self.generated_static_import_stmts_from_external.insert(importee.idx, stmt);
}
fn create_re_export_call_stmt(
&mut self,
importee: &Module,
binding_name: &str,
span: Span,
) -> Option<Statement<'ast>> {
if self.re_export_all_dependencies.contains(&importee.idx()) {
return None;
}
self.re_export_all_dependencies.insert(importee.idx());
let self_exports = self.module_exports_name();
let call_expr = self.snippet.call_expr_with_2arg_expr(
self.snippet.literal_prop_access_member_expr_expr("__rolldown_runtime__", "__reExport"),
self.snippet.id_ref_expr(self_exports, SPAN),
self.snippet.id_ref_expr(binding_name, SPAN),
);
Some(ast::Statement::ExpressionStatement(
self.snippet.builder.alloc_expression_statement(span, call_expr),
))
}
fn generate_declaration_of_module_namespace_object(
&mut self,
binding_name_for_namespace_object_ref: &str,
scoping: &Scoping,
) -> Vec<ast::Statement<'ast>> {
let mut arg_obj_expr = self
.snippet
.builder
.alloc_object_expression(SPAN, self.snippet.builder.vec_with_capacity(self.exports.len()));
arg_obj_expr.properties.extend(self.exports.drain(..));
arg_obj_expr.properties.extend(self.named_exports.iter().map(|(exported, named_export)| {
let expr = if let Some(local_binding) = self.import_bindings.get(&named_export.local_binding)
{
self.snippet.id_ref_expr(local_binding, SPAN)
} else {
let name = scoping.symbol_name(named_export.local_binding);
self.snippet.id_ref_expr(name, SPAN)
};
self.snippet.object_property_kind_object_property(exported, expr, false)
}));
let export_call_expr = self.snippet.builder.expression_call(
SPAN,
self.snippet.id_ref_expr("__rolldown_runtime__.__export", SPAN),
NONE,
self
.snippet
.builder
.vec_from_array([ast::Argument::ObjectExpression(arg_obj_expr.into_in(self.alloc))]),
false,
);
let decl_stmt =
self.snippet.var_decl_stmt(binding_name_for_namespace_object_ref, export_call_expr);
vec![decl_stmt]
}
pub fn try_rewrite_dynamic_import(&self, it: &mut ast::Expression<'ast>) {
let ast::Expression::ImportExpression(import_expr) = it else {
return;
};
let Some(rec_idx) = self.module.imports.get(&import_expr.span) else {
return;
};
let importee_idx = &self.module.import_records[*rec_idx].resolved_module;
let Module::Normal(importee) = &self.modules[*importee_idx] else {
return;
};
let is_importee_cjs = importee.exports_kind == rolldown_common::ExportsKind::CommonJs;
let mut load_exports_call_expr =
ast::Expression::CallExpression(self.snippet.builder.alloc_call_expression(
SPAN,
self.snippet.id_ref_expr("__rolldown_runtime__.loadExports", SPAN),
NONE,
self.snippet.builder.vec1(ast::Argument::StringLiteral(
self.snippet.builder.alloc_string_literal(
SPAN,
self.snippet.builder.atom(&importee.stable_id),
None,
),
)),
false,
));
if is_importee_cjs {
let is_node_cjs = importee.def_format.is_commonjs();
let mut args = self.snippet.builder.vec1(ast::Argument::from(load_exports_call_expr));
if is_node_cjs {
args.push(ast::Argument::from(self.snippet.builder.expression_numeric_literal(
SPAN,
1.0,
None,
ast::NumberBase::Decimal,
)));
}
load_exports_call_expr = self.snippet.builder.expression_call(
SPAN,
self.snippet.id_ref_expr("__rolldown_runtime__.__toDynamicImportESM", SPAN),
NONE,
args,
false,
);
}
if let Some(init_fn_name) = self.affected_module_idx_to_init_fn_name.get(importee_idx) {
let init_fn_call = self.snippet.builder.alloc_call_expression(
SPAN,
self.snippet.id_ref_expr(init_fn_name, SPAN),
NONE,
self.snippet.builder.vec(),
false,
);
let promise_resolve_then_load_exports =
self.snippet.promise_resolve_then_call_expr(load_exports_call_expr);
let ret_expr =
ast::Expression::SequenceExpression(self.snippet.builder.alloc_sequence_expression(
SPAN,
self.snippet.builder.vec_from_array([
ast::Expression::CallExpression(init_fn_call),
promise_resolve_then_load_exports,
]),
));
*it = ret_expr;
} else {
*it = self.snippet.promise_resolve_then_call_expr(load_exports_call_expr);
}
}
pub fn try_rewrite_require(
&self,
it: &mut ast::Expression<'ast>,
ctx: &oxc_traverse::TraverseCtx<'ast, ()>,
) {
let scoping = ctx.scoping();
if let Some(id_ref) = it.as_identifier()
&& id_ref.name == CJS_REQUIRE_REF_ATOM
&& id_ref.is_global_reference(scoping)
&& !ctx.parent().is_call_expression()
{
*it =
self.snippet.literal_prop_access_member_expr_expr("__rolldown_runtime__", "loadExports");
}
let ast::Expression::CallExpression(call_expr) = it else {
return;
};
if !call_expr
.callee
.as_identifier()
.is_some_and(|id| id.name == CJS_REQUIRE_REF_ATOM && id.is_global_reference(scoping))
{
return;
}
let Some(rec_idx) = self.module.imports.get(&call_expr.span) else {
return;
};
let importee_idx = &self.module.import_records[*rec_idx].resolved_module;
let Module::Normal(importee) = &self.modules[*importee_idx] else {
return;
};
let is_importee_cjs = importee.exports_kind == rolldown_common::ExportsKind::CommonJs;
let init_fn_name = &self.affected_module_idx_to_init_fn_name[importee_idx];
if is_importee_cjs {
*it = self.snippet.seq2_in_paren_expr(
self.snippet.call_expr_expr(init_fn_name),
self.snippet.call_expr_with_arg_expr(
self.snippet.literal_prop_access_member_expr_expr("__rolldown_runtime__", "loadExports"),
self.snippet.string_literal_expr(&importee.stable_id, SPAN),
false,
),
);
} else {
*it = self.snippet.seq2_in_paren_expr(
self.snippet.call_expr_expr(init_fn_name),
self.snippet.call_expr_with_arg_expr(
self.snippet.literal_prop_access_member_expr_expr("__rolldown_runtime__", "loadExports"),
self.snippet.string_literal_expr(&importee.stable_id, SPAN),
false,
),
);
}
}
}
pub struct NamedExport {
pub local_binding: SymbolId,
}