rspack_plugin_devtool/
source_map_dev_tool_plugin.rs

1use std::{
2  borrow::Cow,
3  hash::Hasher,
4  path::{Component, Path, PathBuf},
5  sync::{Arc, LazyLock},
6};
7
8use cow_utils::CowUtils;
9use derive_more::Debug;
10use futures::future::{BoxFuture, join_all};
11use itertools::Itertools;
12use rayon::prelude::*;
13use regex::Regex;
14use rspack_collections::DatabaseItem;
15use rspack_core::{
16  AssetInfo, Chunk, ChunkUkey, Compilation, CompilationAsset, CompilationProcessAssets, Filename,
17  Logger, ModuleIdentifier, PathData, Plugin,
18  rspack_sources::{ConcatSource, MapOptions, RawStringSource, Source, SourceExt},
19};
20use rspack_error::{Result, ToStringResultToRspackResultExt, error, miette::IntoDiagnostic};
21use rspack_hash::RspackHash;
22use rspack_hook::{plugin, plugin_hook};
23use rspack_util::{asset_condition::AssetConditions, identifier::make_paths_absolute};
24use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
25use sugar_path::SugarPath;
26
27use crate::{
28  ModuleFilenameTemplateFn, ModuleOrSource, generate_debug_id::generate_debug_id,
29  mapped_assets_cache::MappedAssetsCache, module_filename_helpers::ModuleFilenameHelpers,
30};
31
32static SCHEMA_SOURCE_REGEXP: LazyLock<Regex> =
33  LazyLock::new(|| Regex::new(r"^(data|https?):").expect("failed to compile SCHEMA_SOURCE_REGEXP"));
34
35static CSS_EXTENSION_DETECT_REGEXP: LazyLock<Regex> = LazyLock::new(|| {
36  Regex::new(r"\.css($|\?)").expect("failed to compile CSS_EXTENSION_DETECT_REGEXP")
37});
38static URL_FORMATTING_REGEXP: LazyLock<Regex> = LazyLock::new(|| {
39  Regex::new(r"^\n\/\/(.*)$").expect("failed to compile URL_FORMATTING_REGEXP regex")
40});
41
42#[derive(Clone)]
43pub enum ModuleFilenameTemplate {
44  String(String),
45  Fn(ModuleFilenameTemplateFn),
46}
47
48type AppendFn = Box<dyn Fn(PathData) -> BoxFuture<'static, Result<String>> + Sync + Send>;
49
50pub enum Append {
51  String(String),
52  Fn(AppendFn),
53  Disabled,
54}
55
56#[derive(Debug)]
57pub struct SourceMapDevToolPluginOptions {
58  // Appends the given value to the original asset. Usually the #sourceMappingURL comment. [url] is replaced with a URL to the source map file. false disables the appending.
59  #[debug(skip)]
60  pub append: Option<Append>,
61  // Indicates whether column mappings should be used (defaults to true).
62  pub columns: bool,
63  // Generator string or function to create identifiers of modules for the 'sources' array in the SourceMap used only if 'moduleFilenameTemplate' would result in a conflict.
64  #[debug(skip)]
65  pub fallback_module_filename_template: Option<ModuleFilenameTemplate>,
66  // Path prefix to which the [file] placeholder is relative to.
67  pub file_context: Option<String>,
68  // Defines the output filename of the SourceMap (will be inlined if no value is provided).
69  pub filename: Option<String>,
70  // Indicates whether SourceMaps from loaders should be used (defaults to true).
71  pub module: bool,
72  // Generator string or function to create identifiers of modules for the 'sources' array in the SourceMap.
73  #[debug(skip)]
74  pub module_filename_template: Option<ModuleFilenameTemplate>,
75  // Namespace prefix to allow multiple webpack roots in the devtools.
76  pub namespace: Option<String>,
77  // Omit the 'sourceContents' array from the SourceMap.
78  pub no_sources: bool,
79  // Provide a custom public path for the SourceMapping comment.
80  pub public_path: Option<String>,
81  // Provide a custom value for the 'sourceRoot' property in the SourceMap.
82  pub source_root: Option<String>,
83  pub test: Option<AssetConditions>,
84  pub include: Option<AssetConditions>,
85  pub exclude: Option<AssetConditions>,
86  pub debug_ids: bool,
87}
88
89enum SourceMappingUrlComment {
90  String(String),
91  Fn(AppendFn),
92}
93
94enum SourceMappingUrlCommentRef<'a> {
95  String(Cow<'a, str>),
96  Fn(&'a AppendFn),
97}
98
99#[derive(Debug, Clone)]
100pub(crate) struct MappedAsset {
101  pub(crate) asset: (String, CompilationAsset),
102  pub(crate) source_map: Option<(String, CompilationAsset)>,
103}
104
105#[plugin]
106#[derive(Debug)]
107pub struct SourceMapDevToolPlugin {
108  source_map_filename: Option<Filename>,
109  #[debug(skip)]
110  source_mapping_url_comment: Option<SourceMappingUrlComment>,
111  file_context: Option<String>,
112  #[debug(skip)]
113  module_filename_template: ModuleFilenameTemplate,
114  #[debug(skip)]
115  fallback_module_filename_template: ModuleFilenameTemplate,
116  namespace: String,
117  columns: bool,
118  no_sources: bool,
119  public_path: Option<String>,
120  #[expect(dead_code)]
121  module: bool,
122  source_root: Option<Arc<str>>,
123  test: Option<AssetConditions>,
124  include: Option<AssetConditions>,
125  exclude: Option<AssetConditions>,
126  debug_ids: bool,
127  mapped_assets_cache: MappedAssetsCache,
128}
129
130fn match_object(obj: &SourceMapDevToolPlugin, str: &str) -> bool {
131  if let Some(condition) = &obj.test
132    && !condition.try_match(str)
133  {
134    return false;
135  }
136  if let Some(condition) = &obj.include
137    && !condition.try_match(str)
138  {
139    return false;
140  }
141  if let Some(condition) = &obj.exclude
142    && condition.try_match(str)
143  {
144    return false;
145  }
146  true
147}
148
149impl SourceMapDevToolPlugin {
150  pub fn new(options: SourceMapDevToolPluginOptions) -> Self {
151    let source_mapping_url_comment = match options.append {
152      Some(append) => match append {
153        Append::String(s) => Some(SourceMappingUrlComment::String(s)),
154        Append::Fn(f) => Some(SourceMappingUrlComment::Fn(f)),
155        Append::Disabled => None,
156      },
157      None => Some(SourceMappingUrlComment::String(
158        "\n//# sourceMappingURL=[url]".to_string(),
159      )),
160    };
161
162    let fallback_module_filename_template =
163      options
164        .fallback_module_filename_template
165        .unwrap_or(ModuleFilenameTemplate::String(
166          "webpack://[namespace]/[resourcePath]?[hash]".to_string(),
167        ));
168
169    let module_filename_template =
170      options
171        .module_filename_template
172        .unwrap_or(ModuleFilenameTemplate::String(
173          "webpack://[namespace]/[resourcePath]".to_string(),
174        ));
175
176    Self::new_inner(
177      options.filename.map(Filename::from),
178      source_mapping_url_comment,
179      options.file_context,
180      module_filename_template,
181      fallback_module_filename_template,
182      options.namespace.unwrap_or("".to_string()),
183      options.columns,
184      options.no_sources,
185      options.public_path,
186      options.module,
187      options.source_root.map(Arc::from),
188      options.test,
189      options.include,
190      options.exclude,
191      options.debug_ids,
192      MappedAssetsCache::new(),
193    )
194  }
195
196  async fn map_assets(
197    &self,
198    compilation: &Compilation,
199    file_to_chunk: &HashMap<&String, &Chunk>,
200    raw_assets: Vec<(String, &CompilationAsset)>,
201  ) -> Result<Vec<MappedAsset>> {
202    let output_options = &compilation.options.output;
203    let map_options = MapOptions::new(self.columns);
204    let need_match = self.test.is_some() || self.include.is_some() || self.exclude.is_some();
205
206    let mut mapped_sources = raw_assets
207      .into_par_iter()
208      .filter_map(|(file, asset)| {
209        let is_match = if need_match {
210          match_object(self, &file)
211        } else {
212          true
213        };
214
215        if is_match {
216          asset.get_source().map(|source| {
217            let source_map = source.map(&map_options);
218            (file, source, source_map)
219          })
220        } else {
221          None
222        }
223      })
224      .collect::<Vec<_>>();
225
226    let source_map_modules = mapped_sources
227      .par_iter()
228      .filter_map(|(file, _asset, source_map)| source_map.as_ref().map(|s| (file, s)))
229      .flat_map(|(file, source_map)| {
230        source_map
231          .sources()
232          .iter()
233          .map(|i| (file, i))
234          .collect::<Vec<_>>()
235      })
236      .map(|(file, source)| {
237        let module_or_source = if let Some(stripped) = source.strip_prefix("webpack://") {
238          let source = make_paths_absolute(compilation.options.context.as_str(), stripped);
239          let identifier = ModuleIdentifier::from(source.as_str());
240          match compilation
241            .get_module_graph()
242            .module_by_identifier(&identifier)
243          {
244            Some(module) => ModuleOrSource::Module(module.identifier()),
245            None => ModuleOrSource::Source(source),
246          }
247        } else {
248          ModuleOrSource::Source(source.to_string())
249        };
250
251        (source.to_string(), (file.to_string(), module_or_source))
252      })
253      .collect::<HashMap<_, _>>();
254
255    let mut module_to_source_name = match &self.module_filename_template {
256      ModuleFilenameTemplate::String(template) => {
257        let source_names = rspack_futures::scope::<_, Result<_>>(|token| {
258          source_map_modules.values()
259            .for_each(|(file, module_or_source)| {
260              let s = unsafe {
261                token.used((
262                  self.namespace.as_str(),
263                  &compilation,
264                  file,
265                  module_or_source,
266                  file_to_chunk,
267                  template,
268                ))
269              };
270              s.spawn(
271                |(namespace, compilation, file, module_or_source, file_to_chunk, template)| async move {
272                  if let ModuleOrSource::Source(source) = module_or_source
273                    && SCHEMA_SOURCE_REGEXP.is_match(source) {
274                      return Ok(source.to_string());
275                    }
276
277                  let chunk = file_to_chunk.get(file);
278                  let path_data = PathData::default()
279                    .chunk_id_optional(
280                      chunk
281                        .and_then(|c| c.id(&compilation.chunk_ids_artifact).map(|id| id.as_str())),
282                    )
283                    .chunk_name_optional(chunk.and_then(|c| c.name()))
284                    .chunk_hash_optional(chunk.and_then(|c| {
285                      c.rendered_hash(
286                        &compilation.chunk_hashes_artifact,
287                        compilation.options.output.hash_digest_length,
288                      )
289                    }));
290
291                  let filename = Filename::from(namespace);
292                  let namespace = compilation.get_path(&filename, path_data).await?;
293
294                  let source_name = ModuleFilenameHelpers::create_filename_of_string_template(
295                    module_or_source,
296                    compilation,
297                    template,
298                    &compilation.options.output,
299                    &namespace,
300                  );
301                  Ok(source_name)
302                },
303              );
304            })
305        })
306        .await
307        .into_iter()
308        .map(|r| r.to_rspack_result())
309        .collect::<Result<Vec<_>>>()?;
310
311        let mut res = HashMap::default();
312
313        for ((_, module_or_source), source_name) in
314          source_map_modules.values().zip(source_names.into_iter())
315        {
316          res.insert(module_or_source, source_name?);
317        }
318
319        res
320      }
321      ModuleFilenameTemplate::Fn(f) => {
322        // the tsfn will be called sync in javascript side so there is no need to use rspack futures to parallelize it
323        let tasks = source_map_modules
324          .values()
325          .map(|(_, module_or_source)| async move {
326            if let ModuleOrSource::Source(source) = module_or_source
327              && SCHEMA_SOURCE_REGEXP.is_match(source)
328            {
329              return Ok((module_or_source, source.to_string()));
330            }
331
332            let source_name = ModuleFilenameHelpers::create_filename_of_fn_template(
333              module_or_source,
334              compilation,
335              f,
336              output_options,
337              &self.namespace,
338            )
339            .await?;
340            Ok((module_or_source, source_name))
341          })
342          .collect::<Vec<_>>();
343        join_all(tasks)
344          .await
345          .into_iter()
346          .collect::<Result<HashMap<_, _>>>()?
347      }
348    };
349
350    let mut used_names_set = HashSet::<&String>::default();
351    for (module_or_source, source_name) in
352      module_to_source_name
353        .iter_mut()
354        .sorted_by(|(key_a, _), (key_b, _)| {
355          let ident_a = match key_a {
356            ModuleOrSource::Module(identifier) => identifier,
357            ModuleOrSource::Source(source) => source.as_str(),
358          };
359          let ident_b = match key_b {
360            ModuleOrSource::Module(identifier) => identifier,
361            ModuleOrSource::Source(source) => source.as_str(),
362          };
363          ident_a.len().cmp(&ident_b.len())
364        })
365    {
366      let mut has_name = used_names_set.contains(source_name);
367      if !has_name {
368        used_names_set.insert(source_name);
369        continue;
370      }
371
372      // Try the fallback name first
373      let mut new_source_name = match &self.fallback_module_filename_template {
374        ModuleFilenameTemplate::String(s) => {
375          ModuleFilenameHelpers::create_filename_of_string_template(
376            module_or_source,
377            compilation,
378            s,
379            output_options,
380            self.namespace.as_str(),
381          )
382        }
383        ModuleFilenameTemplate::Fn(f) => {
384          ModuleFilenameHelpers::create_filename_of_fn_template(
385            module_or_source,
386            compilation,
387            f,
388            output_options,
389            self.namespace.as_str(),
390          )
391          .await?
392        }
393      };
394
395      has_name = used_names_set.contains(&new_source_name);
396      if !has_name {
397        *source_name = new_source_name;
398        used_names_set.insert(source_name);
399        continue;
400      }
401
402      // Otherwise, append stars until we have a valid name
403      while has_name {
404        new_source_name.push('*');
405        has_name = used_names_set.contains(&new_source_name);
406      }
407      *source_name = new_source_name;
408      used_names_set.insert(source_name);
409    }
410
411    for (filename, _asset, source_map) in mapped_sources.iter_mut() {
412      if let Some(source_map) = source_map {
413        source_map.set_file(Some(filename.clone()));
414
415        source_map.set_sources(
416          source_map
417            .sources()
418            .iter()
419            .map(|source| {
420              let module_or_source = source_map_modules
421                .get(source)
422                .expect("expected a module or source");
423              module_to_source_name
424                .get(&module_or_source.1)
425                .expect("expected a filename at the given index but found None")
426                .clone()
427            })
428            .collect::<Vec<_>>(),
429        );
430        if self.no_sources {
431          source_map.set_sources_content([]);
432        }
433        if let Some(source_root) = &self.source_root {
434          source_map.set_source_root(Some(source_root.clone()));
435        }
436      }
437    }
438
439    let mapped_assets = rspack_futures::scope::<_, Result<_>>(|token| {
440      mapped_sources
441        .into_iter()
442        .for_each(|(source_filename, source, source_map)| {
443          let s = unsafe {
444            token.used((
445              &self,
446              compilation,
447              file_to_chunk,
448              source_filename,
449              source,
450              source_map,
451            ))
452          };
453
454          s.spawn(
455            |(plugin, compilation, file_to_chunk, source_filename, source, source_map)| async move {
456              let (source_map_json, debug_id) = match source_map {
457                Some(mut map) => {
458                  let debug_id = plugin.debug_ids.then(|| {
459                    let debug_id = generate_debug_id(&source_filename, &source.buffer());
460                    map.set_debug_id(Some(debug_id.clone()));
461                    debug_id
462                  });
463
464                  (Some(map.to_json().into_diagnostic()?), debug_id)
465                }
466                None => (None, None),
467              };
468
469              let mut asset = compilation
470                .assets()
471                .get(&source_filename)
472                .unwrap_or_else(|| {
473                  panic!(
474                    "expected to find filename '{}' in compilation.assets, but it was not present",
475                    &source_filename
476                  )
477                })
478                .clone();
479              let Some(source_map_json) = source_map_json else {
480                return Ok(MappedAsset {
481                  asset: (source_filename, asset),
482                  source_map: None,
483                });
484              };
485              let css_extension_detected = CSS_EXTENSION_DETECT_REGEXP.is_match(&source_filename);
486              let current_source_mapping_url_comment = match &plugin.source_mapping_url_comment {
487                Some(SourceMappingUrlComment::String(s)) => {
488                  let s = if css_extension_detected {
489                    URL_FORMATTING_REGEXP.replace_all(s, "\n/*$1*/")
490                  } else {
491                    Cow::from(s)
492                  };
493                  Some(SourceMappingUrlCommentRef::String(s))
494                }
495                Some(SourceMappingUrlComment::Fn(f)) => Some(SourceMappingUrlCommentRef::Fn(f)),
496                None => None,
497              };
498
499              if let Some(source_map_filename_config) = &plugin.source_map_filename {
500                let chunk = file_to_chunk.get(&source_filename);
501                let filename = match &plugin.file_context {
502                  Some(file_context) => Cow::Owned(
503                    Path::new(&source_filename)
504                      .relative(Path::new(file_context))
505                      .to_string_lossy()
506                      .to_string(),
507                  ),
508                  None => Cow::Borrowed(&source_filename),
509                };
510
511                let mut hasher = RspackHash::from(&compilation.options.output);
512                hasher.write(source_map_json.as_bytes());
513                let digest = hasher.digest(&compilation.options.output.hash_digest);
514
515                let data = PathData::default().filename(&filename);
516                let data = match chunk {
517                  Some(chunk) => data
518                    .chunk_id_optional(
519                      chunk
520                        .id(&compilation.chunk_ids_artifact)
521                        .map(|id| id.as_str()),
522                    )
523                    .chunk_hash_optional(chunk.rendered_hash(
524                      &compilation.chunk_hashes_artifact,
525                      compilation.options.output.hash_digest_length,
526                    ))
527                    .chunk_name_optional(
528                      chunk.name_for_filename_template(&compilation.chunk_ids_artifact),
529                    )
530                    .content_hash_optional(Some(digest.encoded())),
531                  None => data,
532                };
533                let source_map_filename = compilation
534                  .get_asset_path(source_map_filename_config, data)
535                  .await?;
536
537                if let Some(current_source_mapping_url_comment) = current_source_mapping_url_comment
538                {
539                  let source_map_url = if let Some(public_path) = &plugin.public_path {
540                    format!("{public_path}{source_map_filename}")
541                  } else {
542                    let mut file_path = PathBuf::new();
543                    file_path.push(Component::RootDir);
544                    file_path.extend(Path::new(filename.as_ref()).components());
545
546                    let mut source_map_path = PathBuf::new();
547                    source_map_path.push(Component::RootDir);
548                    source_map_path.extend(Path::new(&source_map_filename).components());
549
550                    source_map_path
551                      .relative(
552                        #[allow(clippy::unwrap_used)]
553                        file_path.parent().unwrap(),
554                      )
555                      .to_string_lossy()
556                      .to_string()
557                  };
558                  let data = data.url(&source_map_url);
559                  let current_source_mapping_url_comment = match &current_source_mapping_url_comment
560                  {
561                    SourceMappingUrlCommentRef::String(s) => {
562                      compilation
563                        .get_asset_path(&Filename::from(s.as_ref()), data)
564                        .await?
565                    }
566                    SourceMappingUrlCommentRef::Fn(f) => {
567                      let comment = f(data).await?;
568                      Filename::from(comment).render(data, None).await?
569                    }
570                  };
571                  let current_source_mapping_url_comment = current_source_mapping_url_comment
572                    .cow_replace("[url]", &source_map_url)
573                    .into_owned();
574
575                  let debug_id_comment = debug_id
576                    .map(|id| format!("\n//# debugId={id}"))
577                    .unwrap_or_default();
578
579                  asset.source = Some(
580                    ConcatSource::new([
581                      source.clone(),
582                      RawStringSource::from(debug_id_comment).boxed(),
583                      RawStringSource::from(current_source_mapping_url_comment).boxed(),
584                    ])
585                    .boxed(),
586                  );
587                  asset.info.related.source_map = Some(source_map_filename.clone());
588                } else {
589                  asset.source = Some(source.clone());
590                }
591                let mut source_map_asset_info = AssetInfo::default().with_development(Some(true));
592                if let Some(asset) = compilation.assets().get(&source_filename) {
593                  // set source map asset version to be the same as the target asset
594                  source_map_asset_info.version = asset.info.version.clone();
595                }
596                let source_map_asset = CompilationAsset::new(
597                  Some(RawStringSource::from(source_map_json).boxed()),
598                  source_map_asset_info,
599                );
600                Ok(MappedAsset {
601                  asset: (source_filename, asset),
602                  source_map: Some((source_map_filename, source_map_asset)),
603                })
604              } else {
605                let current_source_mapping_url_comment = current_source_mapping_url_comment.expect(
606                  "SourceMapDevToolPlugin: append can't be false when no filename is provided.",
607                );
608                let current_source_mapping_url_comment = match &current_source_mapping_url_comment {
609                  SourceMappingUrlCommentRef::String(s) => s,
610                  SourceMappingUrlCommentRef::Fn(_) => {
611                    return Err(error!(
612                  "SourceMapDevToolPlugin: append can't be a function when no filename is provided"
613                ))
614                  }
615                };
616                let base64 = rspack_base64::encode_to_string(source_map_json.as_bytes());
617                asset.source = Some(
618                  ConcatSource::new([
619                    source.clone(),
620                    RawStringSource::from(
621                      current_source_mapping_url_comment
622                        .cow_replace(
623                          "[url]",
624                          &format!("data:application/json;charset=utf-8;base64,{base64}"),
625                        )
626                        .into_owned(),
627                    )
628                    .boxed(),
629                  ])
630                  .boxed(),
631                );
632                Ok(MappedAsset {
633                  asset: (source_filename, asset),
634                  source_map: None,
635                })
636              }
637            },
638          );
639        });
640    })
641    .await
642    .into_iter()
643    .map(|r| r.to_rspack_result())
644    .collect::<Result<Vec<_>>>()?;
645
646    mapped_assets.into_iter().collect::<Result<Vec<_>>>()
647  }
648}
649
650#[plugin_hook(CompilationProcessAssets for SourceMapDevToolPlugin, stage = Compilation::PROCESS_ASSETS_STAGE_DEV_TOOLING)]
651async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
652  let logger = compilation.get_logger("rspack.SourceMapDevToolPlugin");
653
654  // use to read
655  let mut file_to_chunk: HashMap<&String, &Chunk> = HashMap::default();
656  // use to write
657  let mut file_to_chunk_ukey: HashMap<String, ChunkUkey> = HashMap::default();
658  for chunk in compilation.chunk_by_ukey.values() {
659    for file in chunk.files() {
660      file_to_chunk.insert(file, chunk);
661      file_to_chunk_ukey.insert(file.to_string(), chunk.ukey());
662    }
663    for file in chunk.auxiliary_files() {
664      file_to_chunk.insert(file, chunk);
665      file_to_chunk_ukey.insert(file.to_string(), chunk.ukey());
666    }
667  }
668
669  let start = logger.time("collect source maps");
670  let raw_assets = compilation
671    .assets()
672    .iter()
673    .filter(|(_filename, asset)| asset.info.related.source_map.is_none())
674    .collect::<Vec<_>>();
675  let mapped_asstes = self
676    .mapped_assets_cache
677    .use_cache(raw_assets, |assets| {
678      self.map_assets(compilation, &file_to_chunk, assets)
679    })
680    .await?;
681  logger.time_end(start);
682
683  let start = logger.time("emit source map assets");
684  for mapped_asset in mapped_asstes {
685    let MappedAsset {
686      asset: (source_filename, mut source_asset),
687      source_map,
688    } = mapped_asset;
689    if let Some(asset) = compilation.assets_mut().remove(&source_filename) {
690      source_asset.info = asset.info;
691      if let Some((ref source_map_filename, _)) = source_map {
692        source_asset.info.related.source_map = Some(source_map_filename.clone());
693      }
694    }
695
696    let chunk_ukey = file_to_chunk_ukey.get(&source_filename);
697    compilation.emit_asset(source_filename, source_asset);
698    if let Some((source_map_filename, source_map_asset)) = source_map {
699      compilation.emit_asset(source_map_filename.to_owned(), source_map_asset);
700
701      let chunk = chunk_ukey.map(|ukey| compilation.chunk_by_ukey.expect_get_mut(ukey));
702      if let Some(chunk) = chunk {
703        chunk.add_auxiliary_file(source_map_filename);
704      }
705    }
706  }
707  logger.time_end(start);
708
709  Ok(())
710}
711
712impl Plugin for SourceMapDevToolPlugin {
713  fn name(&self) -> &'static str {
714    "rspack.SourceMapDevToolPlugin"
715  }
716
717  fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
718    ctx
719      .compilation_hooks
720      .process_assets
721      .tap(process_assets::new(self));
722    Ok(())
723  }
724}