use std::sync::Arc;
use crate::{
HookBuildEndArgs, HookLoadArgs, HookLoadReturn, HookNoopReturn, HookResolveIdArgs,
HookResolveIdReturn, HookTransformArgs, PluginContext, PluginDriver, TransformPluginContext,
pluginable::HookTransformAstReturn,
types::{
hook_resolve_id_skipped::HookResolveIdSkipped, hook_transform_ast_args::HookTransformAstArgs,
},
};
use anyhow::{Context, Result};
use rolldown_common::{
ModuleInfo, ModuleType, NormalModule, PluginIdx, SharedNormalizedBundlerOptions,
SourcemapChainElement, SourcemapHires, side_effects::HookSideEffects,
};
use rolldown_debug::{action, trace_action};
use rolldown_error::CausedPlugin;
use rolldown_sourcemap::SourceMap;
use rolldown_utils::unique_arc::UniqueArc;
use string_wizard::{MagicString, SourceMapOptions};
use tracing::{Instrument, debug_span};
impl PluginDriver {
#[tracing::instrument(level = "trace", skip_all)]
pub async fn build_start(&self, opts: &SharedNormalizedBundlerOptions) -> HookNoopReturn {
for (_, plugin, ctx) in self.iter_plugin_with_context_by_order(&self.order_by_build_start_meta)
{
plugin
.call_build_start(ctx, &crate::HookBuildStartArgs { options: opts })
.await
.with_context(|| CausedPlugin::new(plugin.call_name()))?;
}
Ok(())
}
#[inline]
fn get_resolve_call_skipped_plugins(
specifier: &str,
importer: Option<&str>,
skipped_resolve_calls: Option<&Vec<Arc<HookResolveIdSkipped>>>,
) -> Vec<PluginIdx> {
let mut skipped_plugins = vec![];
if let Some(skipped_resolve_calls) = skipped_resolve_calls {
for skip_resolve_call in skipped_resolve_calls {
if skip_resolve_call.specifier == specifier
&& skip_resolve_call.importer.as_deref() == importer
{
skipped_plugins.push(skip_resolve_call.plugin_idx);
}
}
}
skipped_plugins
}
pub async fn resolve_id(
&self,
args: &HookResolveIdArgs<'_>,
skipped_resolve_calls: Option<&Vec<Arc<HookResolveIdSkipped>>>,
) -> HookResolveIdReturn {
let skipped_plugins =
Self::get_resolve_call_skipped_plugins(args.specifier, args.importer, skipped_resolve_calls);
for (plugin_idx, plugin, ctx) in
self.iter_plugin_with_context_by_order(&self.order_by_resolve_id_meta)
{
if skipped_plugins.contains(&plugin_idx) {
continue;
}
let ret = async {
trace_action!(action::HookResolveIdCallStart {
action: "HookResolveIdCallStart",
importer: args.importer.map(ToString::to_string),
module_request: args.specifier.to_string(),
import_kind: args.kind.to_string(),
plugin_name: plugin.call_name().to_string(),
plugin_id: plugin_idx.raw(),
trigger: "${hook_resolve_id_trigger}",
call_id: "${call_id}",
});
if let Some(r) = plugin
.call_resolve_id(
&skipped_resolve_calls.map_or_else(
|| ctx.clone(),
|skipped_resolve_calls| {
PluginContext::new_shared_with_skipped_resolve_calls(
ctx,
skipped_resolve_calls.clone(),
)
},
),
args,
)
.await?
{
trace_action!(action::HookResolveIdCallEnd {
action: "HookResolveIdCallEnd",
resolved_id: Some(r.id.to_string()),
is_external: r.external.map(|v| v.is_external()),
plugin_name: plugin.call_name().to_string(),
plugin_id: plugin_idx.raw(),
trigger: "${hook_resolve_id_trigger}",
call_id: "${call_id}",
});
anyhow::Ok(Some(r))
} else {
trace_action!(action::HookResolveIdCallEnd {
action: "HookResolveIdCallEnd",
resolved_id: None,
is_external: None,
plugin_name: plugin.call_name().to_string(),
plugin_id: plugin_idx.raw(),
trigger: "${hook_resolve_id_trigger}",
call_id: "${call_id}",
});
Ok(None)
}
}
.instrument(tracing::trace_span!(
"HookResolveIdCall",
CONTEXT_call_id = rolldown_utils::uuid::uuid_v4()
))
.await
.with_context(|| CausedPlugin::new(plugin.call_name()))?;
if ret.is_some() {
return Ok(ret);
}
}
Ok(None)
}
#[expect(deprecated)]
pub async fn resolve_dynamic_import(
&self,
args: &HookResolveIdArgs<'_>,
skipped_resolve_calls: Option<&Vec<Arc<HookResolveIdSkipped>>>,
) -> HookResolveIdReturn {
let skipped_plugins =
Self::get_resolve_call_skipped_plugins(args.specifier, args.importer, skipped_resolve_calls);
for (plugin_idx, plugin, ctx) in
self.iter_plugin_with_context_by_order(&self.order_by_resolve_dynamic_import_meta)
{
if skipped_plugins.contains(&plugin_idx) {
continue;
}
if let Some(r) = plugin
.call_resolve_dynamic_import(
&skipped_resolve_calls.map_or_else(
|| ctx.clone(),
|skipped_resolve_calls| {
PluginContext::new_shared_with_skipped_resolve_calls(
ctx,
skipped_resolve_calls.clone(),
)
},
),
args,
)
.await
.with_context(|| CausedPlugin::new(plugin.call_name()))?
{
return Ok(Some(r));
}
}
Ok(None)
}
pub async fn load(&self, args: &HookLoadArgs<'_>) -> HookLoadReturn {
for (plugin_idx, plugin, ctx) in
self.iter_plugin_with_context_by_order(&self.order_by_load_meta)
{
let ret = async {
trace_action!(action::HookLoadCallStart {
action: "HookLoadCallStart",
module_id: args.id.to_string(),
plugin_name: plugin.call_name().to_string(),
plugin_id: plugin_idx.raw(),
call_id: "${call_id}",
});
if let Some(r) = plugin.call_load(ctx, args).await? {
trace_action!(action::HookLoadCallEnd {
action: "HookLoadCallEnd",
module_id: args.id.to_string(),
content: Some(r.code.to_string()),
plugin_name: plugin.call_name().to_string(),
plugin_id: plugin_idx.raw(),
call_id: "${call_id}",
});
anyhow::Ok(Some(r))
} else {
trace_action!(action::HookLoadCallEnd {
action: "HookLoadCallEnd",
module_id: args.id.to_string(),
content: None,
plugin_name: plugin.call_name().to_string(),
plugin_id: plugin_idx.raw(),
call_id: "${call_id}",
});
Ok(None)
}
}
.instrument(tracing::trace_span!(
"HookLoadCall",
CONTEXT_call_id = rolldown_utils::uuid::uuid_v4()
))
.await
.with_context(|| CausedPlugin::new(plugin.call_name()))?;
if ret.is_some() {
return Ok(ret);
}
}
Ok(None)
}
#[tracing::instrument(target = "devtool", level = "trace", skip_all)]
#[expect(clippy::too_many_arguments)]
pub async fn transform(
&self,
id: &str,
module_idx: rolldown_common::ModuleIdx,
original_code: String,
sourcemap_chain: &mut Vec<SourcemapChainElement>,
side_effects: &mut Option<HookSideEffects>,
module_type: &mut ModuleType,
magic_string_tx: Option<Arc<std::sync::mpsc::Sender<rolldown_common::SourceMapGenMsg>>>,
) -> Result<String> {
let mut code = original_code;
let mut original_sourcemap_chain = std::mem::take(sourcemap_chain);
let mut plugin_sourcemap_chain = UniqueArc::new(original_sourcemap_chain);
for (plugin_idx, plugin, ctx) in
self.iter_plugin_with_context_by_order(&self.order_by_transform_meta)
{
let call_id = tracing::enabled!(tracing::Level::TRACE).then(rolldown_utils::uuid::uuid_v4);
trace_action!(action::HookTransformCallStart {
action: "HookTransformCallStart",
module_id: id.to_string(),
content: code.clone(),
plugin_name: plugin.call_name().to_string(),
plugin_id: plugin_idx.raw(),
call_id: call_id.clone().unwrap_or_default(),
});
if let Some(r) = plugin
.call_transform(
Arc::new(TransformPluginContext::new(
ctx.clone(),
plugin_sourcemap_chain.weak_ref(),
code.as_str().into(),
id.into(),
module_idx,
plugin_idx,
magic_string_tx.clone(),
)),
&HookTransformArgs { id, code: &code, module_type: &*module_type },
)
.await
.with_context(|| CausedPlugin::new(plugin.call_name()))?
{
original_sourcemap_chain = plugin_sourcemap_chain.into_inner();
if let Some(map) = self.normalize_transform_sourcemap(r.map, id, &code, r.code.as_ref()) {
original_sourcemap_chain.push(SourcemapChainElement::Transform((plugin_idx, map)));
}
plugin_sourcemap_chain = UniqueArc::new(original_sourcemap_chain);
if let Some(v) = r.side_effects {
*side_effects = Some(v);
}
if let Some(v) = r.code {
code = v;
trace_action!(action::HookTransformCallEnd {
action: "HookTransformCallEnd",
module_id: id.to_string(),
content: Some(code.to_string()),
plugin_name: plugin.call_name().to_string(),
plugin_id: plugin_idx.raw(),
call_id: call_id.unwrap_or_default()
});
}
if let Some(ty) = r.module_type {
*module_type = ty;
}
} else {
trace_action!(action::HookTransformCallEnd {
action: "HookTransformCallEnd",
module_id: id.to_string(),
content: Some(code.to_string()),
plugin_name: plugin.call_name().to_string(),
plugin_id: plugin_idx.raw(),
call_id: call_id.unwrap_or_default()
});
}
}
*sourcemap_chain = plugin_sourcemap_chain.into_inner();
Ok(code)
}
#[inline]
fn normalize_transform_sourcemap(
&self,
map: Option<SourceMap>,
id: &str,
original_code: &str,
code: Option<&String>,
) -> Option<SourceMap> {
if let Some(mut map) = map {
let source = map.get_source(0);
if source.is_none_or(|s| s.is_empty())
|| (map.get_sources().count() == 1 && (source.map(AsRef::as_ref) != Some(id)))
{
map.set_sources(vec![id]);
}
if map.get_source_content(0).is_none_or(|s| s.is_empty()) {
map.set_source_contents(vec![Some(original_code)]);
}
Some(map)
} else if let Some(code) = code {
if original_code == code {
None
} else {
let magic_string = MagicString::new(original_code);
let hires = self
.options
.experimental
.transform_hires_sourcemap
.unwrap_or(SourcemapHires::Boolean(true));
Some(magic_string.source_map(SourceMapOptions {
hires: hires.into(),
include_content: true,
source: id.into(),
}))
}
} else {
None
}
}
pub async fn transform_ast(&self, mut args: HookTransformAstArgs<'_>) -> HookTransformAstReturn {
for (_, plugin, ctx) in
self.iter_plugin_with_context_by_order(&self.order_by_transform_ast_meta)
{
args.ast = plugin
.call_transform_ast(
ctx,
HookTransformAstArgs {
cwd: args.cwd,
ast: args.ast,
id: args.id,
stable_id: args.stable_id,
is_user_defined_entry: args.is_user_defined_entry,
module_type: args.module_type,
},
)
.instrument(debug_span!("transform_ast_hook", plugin_name = plugin.call_name().as_ref()))
.await
.with_context(|| CausedPlugin::new(plugin.call_name()))?;
}
Ok(args.ast)
}
pub async fn module_parsed(
&self,
module_info: Arc<ModuleInfo>,
normal_module: &NormalModule,
) -> HookNoopReturn {
for (_, plugin, ctx) in
self.iter_plugin_with_context_by_order(&self.order_by_module_parsed_meta)
{
plugin
.call_module_parsed(ctx, Arc::clone(&module_info), normal_module)
.await
.with_context(|| CausedPlugin::new(plugin.call_name()))?;
}
Ok(())
}
pub async fn build_end(&self, args: Option<&HookBuildEndArgs<'_>>) -> HookNoopReturn {
for (_, plugin, ctx) in self.iter_plugin_with_context_by_order(&self.order_by_build_end_meta) {
plugin
.call_build_end(ctx, args)
.await
.with_context(|| CausedPlugin::new(plugin.call_name()))?;
}
Ok(())
}
}