use std::{hash::Hash, sync::Arc};
use derive_more::Debug;
use futures::future::join_all;
use rspack_core::{
ChunkGraph, ChunkInitFragments, ChunkUkey, Compilation,
CompilationAdditionalModuleRuntimeRequirements, CompilationParams, CompilerCompilation, Filename,
Module, ModuleIdentifier, PathData, Plugin, RuntimeCodeTemplate, RuntimeGlobals,
rspack_sources::{BoxSource, MapOptions, ObjectPool, RawStringSource, Source, SourceExt},
};
use rspack_error::Result;
use rspack_hash::{RspackHash, RspackHashDigest};
use rspack_hook::{plugin, plugin_hook};
use rspack_plugin_javascript::{
JavascriptModulesChunkHash, JavascriptModulesInlineInRuntimeBailout,
JavascriptModulesRenderModuleContent, JsPlugin, RenderSource,
};
use rspack_util::{
asset_condition::{AssetConditions, AssetConditionsObject, match_object},
base64,
fx_hash::FxDashMap,
identifier::make_paths_absolute,
};
use crate::{
ModuleFilenameTemplate, SourceMapDevToolPluginOptions, SourceReference,
generate_debug_id::generate_debug_id, module_filename_helpers::ModuleFilenameHelpers,
};
const EVAL_SOURCE_MAP_DEV_TOOL_PLUGIN_NAME: &str = "rspack.EvalSourceMapDevToolPlugin";
#[plugin]
#[derive(Debug)]
pub struct EvalSourceMapDevToolPlugin {
columns: bool,
no_sources: bool,
#[debug(skip)]
module_filename_template: ModuleFilenameTemplate,
namespace: String,
source_root: Option<String>,
debug_ids: bool,
ignore_list: Option<AssetConditions>,
test: Option<AssetConditions>,
include: Option<AssetConditions>,
exclude: Option<AssetConditions>,
cache: FxDashMap<RspackHashDigest, BoxSource>,
}
impl EvalSourceMapDevToolPlugin {
pub fn new(options: SourceMapDevToolPluginOptions) -> Self {
let module_filename_template =
options
.module_filename_template
.unwrap_or(ModuleFilenameTemplate::String(
"webpack://[namespace]/[resource-path]?[hash]".to_string(),
));
let namespace = options.namespace.unwrap_or_default();
Self::new_inner(
options.columns,
options.no_sources,
module_filename_template,
namespace,
options.source_root,
options.debug_ids,
options.ignore_list,
options.test,
options.include,
options.exclude,
Default::default(),
)
}
}
#[plugin_hook(CompilerCompilation for EvalSourceMapDevToolPlugin)]
async fn compilation(
&self,
compilation: &mut Compilation,
_params: &mut CompilationParams,
) -> Result<()> {
let hooks = JsPlugin::get_compilation_hooks_mut(compilation.id());
let mut hooks = hooks.write().await;
hooks
.render_module_content
.tap(render_module_content::new(self));
hooks.chunk_hash.tap(js_chunk_hash::new(self));
hooks
.inline_in_runtime_bailout
.tap(inline_in_runtime_bailout::new(self));
Ok(())
}
#[plugin_hook(JavascriptModulesRenderModuleContent for EvalSourceMapDevToolPlugin,tracing=false)]
async fn render_module_content(
&self,
compilation: &Compilation,
chunk: &ChunkUkey,
module: &dyn Module,
render_source: &mut RenderSource,
_init_fragments: &mut ChunkInitFragments,
runtime_template: &RuntimeCodeTemplate<'_>,
) -> Result<()> {
let output_options = &compilation.options.output;
let chunk = compilation
.build_chunk_graph_artifact
.chunk_by_ukey
.expect_get(chunk);
let module_hash = compilation
.code_generation_results
.get_hash(&module.identifier(), Some(chunk.runtime()))
.expect("should have codegen results hash in process assets");
let condition_object = AssetConditionsObject {
test: self.test.as_ref(),
include: self.include.as_ref(),
exclude: self.exclude.as_ref(),
};
if module
.as_normal_module()
.is_some_and(|m| !match_object(&condition_object, m.resource_resolved_data().resource()))
{
return Ok(());
}
if module.as_concatenated_module().is_some_and(|c| {
let mg = compilation.get_module_graph();
let root_id = c.get_root();
mg.module_by_identifier(&root_id)
.and_then(|m| m.as_normal_module())
.is_some_and(|m| !match_object(&condition_object, m.resource_resolved_data().resource()))
}) {
return Ok(());
}
let origin_source = render_source.source.clone();
if let Some(cached_source) = self.cache.get(module_hash) {
render_source.source = cached_source.value().clone();
return Ok(());
} else if let Some(mut map) =
origin_source.map(&ObjectPool::default(), &MapOptions::new(self.columns))
{
let source = {
let source = origin_source.source().into_string_lossy();
{
let modules = map.sources().iter().map(|source| {
if let Some(stripped) = source.strip_prefix("webpack://") {
let source = make_paths_absolute(compilation.options.context.as_str(), stripped);
let identifier = ModuleIdentifier::from(source.as_str());
match compilation
.get_module_graph()
.module_by_identifier(&identifier)
{
Some(module) => SourceReference::Module(module.identifier()),
None => SourceReference::Source(Arc::from(source)),
}
} else {
SourceReference::Source(Arc::from(source.clone()))
}
});
let path_data = PathData::default()
.chunk_id_optional(chunk.id().map(|id| id.as_str()))
.chunk_name_optional(chunk.name())
.chunk_hash_optional(chunk.rendered_hash(
&compilation.chunk_hashes_artifact,
compilation.options.output.hash_digest_length,
));
let filename = Filename::from(self.namespace.as_str());
let namespace = compilation.get_path(&filename, path_data).await?;
let module_filenames = match &self.module_filename_template {
ModuleFilenameTemplate::String(s) => modules
.map(|source_reference| {
ModuleFilenameHelpers::create_filename_of_string_template(
&source_reference,
compilation,
s,
output_options,
&namespace,
None,
)
})
.collect::<Vec<_>>(),
ModuleFilenameTemplate::Fn(f) => {
let modules = modules.collect::<Vec<_>>();
let features = modules.iter().map(|source_reference| {
ModuleFilenameHelpers::create_filename_of_fn_template(
source_reference,
compilation,
f,
output_options,
&namespace,
None,
)
});
join_all(features)
.await
.into_iter()
.collect::<Result<Vec<_>>>()?
}
};
let module_filenames =
ModuleFilenameHelpers::replace_duplicates(module_filenames, |mut filename, _, n| {
filename.extend(std::iter::repeat_n('*', n));
filename
});
map.set_sources(module_filenames);
}
if let Some(asset_conditions) = &self.ignore_list {
let ignore_list = map
.sources()
.iter()
.enumerate()
.filter_map(|(idx, source)| {
if asset_conditions.try_match(source) {
Some(idx as u32)
} else {
None
}
})
.collect::<Vec<_>>();
map.set_ignore_list(Some(ignore_list));
}
if self.no_sources {
map.set_sources_content([]);
}
map.set_source_root(self.source_root.clone());
map.set_file(Some(module.identifier().to_string()));
if self.debug_ids {
map.set_debug_id(Some(generate_debug_id(
module.identifier().as_str(),
source.as_bytes(),
)));
}
let module_ids = &compilation.module_ids_artifact;
let module_id =
if let Some(module_id) = ChunkGraph::get_module_id(module_ids, module.identifier()) {
module_id.as_str()
} else {
"unknown"
};
let source_map = map.to_json();
let base64 = base64::encode_to_string(&source_map);
let footer = format!(
r#"
//# sourceMappingURL=data:application/json;charset=utf-8;base64,{base64}
//# sourceURL=webpack-internal:///{module_id}
"#
);
let module_content =
simd_json::to_string(&format!("{{{source}{footer}\n}}")).expect("should convert to string");
RawStringSource::from(format!(
"eval({});",
if compilation.options.output.trusted_types.is_some() {
format!(
"{}({})",
runtime_template.render_runtime_globals(&RuntimeGlobals::CREATE_SCRIPT),
module_content
)
} else {
module_content
}
))
.boxed()
};
self.cache.insert(module_hash.clone(), source.clone());
render_source.source = source;
return Ok(());
}
Ok(())
}
#[plugin_hook(JavascriptModulesChunkHash for EvalSourceMapDevToolPlugin)]
async fn js_chunk_hash(
&self,
_compilation: &Compilation,
_chunk_ukey: &ChunkUkey,
hasher: &mut RspackHash,
) -> Result<()> {
EVAL_SOURCE_MAP_DEV_TOOL_PLUGIN_NAME.hash(hasher);
Ok(())
}
#[plugin_hook(JavascriptModulesInlineInRuntimeBailout for EvalSourceMapDevToolPlugin)]
async fn inline_in_runtime_bailout(&self, _compilation: &Compilation) -> Result<Option<String>> {
Ok(Some("the eval-source-map devtool is used.".to_string()))
}
#[plugin_hook(CompilationAdditionalModuleRuntimeRequirements for EvalSourceMapDevToolPlugin,tracing=false)]
async fn additional_module_runtime_requirements(
&self,
compilation: &Compilation,
_module: &ModuleIdentifier,
runtime_requirements: &mut RuntimeGlobals,
) -> Result<()> {
if compilation.options.output.trusted_types.is_some() {
runtime_requirements.insert(RuntimeGlobals::CREATE_SCRIPT);
}
Ok(())
}
impl Plugin for EvalSourceMapDevToolPlugin {
fn name(&self) -> &'static str {
EVAL_SOURCE_MAP_DEV_TOOL_PLUGIN_NAME
}
fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
ctx.compiler_hooks.compilation.tap(compilation::new(self));
ctx
.compilation_hooks
.additional_module_runtime_requirements
.tap(additional_module_runtime_requirements::new(self));
Ok(())
}
}