Skip to main content

rspack_plugin_devtool/
eval_dev_tool_module_plugin.rs

1use std::{borrow::Cow, hash::Hash};
2
3use cow_utils::CowUtils;
4use dashmap::DashMap;
5use derive_more::Debug;
6use rspack_core::{
7  ChunkInitFragments, ChunkUkey, Compilation, CompilationAdditionalModuleRuntimeRequirements,
8  CompilationParams, CompilerCompilation, Filename, Module, ModuleIdentifier, PathData, Plugin,
9  RuntimeGlobals,
10  rspack_sources::{BoxSource, RawStringSource, Source, SourceExt},
11};
12use rspack_error::Result;
13use rspack_hash::RspackHash;
14use rspack_hook::{plugin, plugin_hook};
15use rspack_plugin_javascript::{
16  JavascriptModulesChunkHash, JavascriptModulesInlineInRuntimeBailout,
17  JavascriptModulesRenderModuleContent, JsPlugin, RenderSource,
18};
19
20use crate::{
21  ModuleFilenameTemplate, SourceReference, module_filename_helpers::ModuleFilenameHelpers,
22};
23
24#[derive(Clone, Debug)]
25pub struct EvalDevToolModulePluginOptions {
26  pub namespace: Option<String>,
27  #[debug(skip)]
28  pub module_filename_template: Option<ModuleFilenameTemplate>,
29  pub source_url_comment: Option<String>,
30}
31
32const EVAL_DEV_TOOL_MODULE_PLUGIN_NAME: &str = "rspack.EvalDevToolModulePlugin";
33
34#[plugin]
35#[derive(Debug)]
36pub struct EvalDevToolModulePlugin {
37  namespace: String,
38  source_url_comment: String,
39  #[debug(skip)]
40  module_filename_template: ModuleFilenameTemplate,
41  cache: DashMap<BoxSource, BoxSource>,
42}
43
44impl EvalDevToolModulePlugin {
45  pub fn new(options: EvalDevToolModulePluginOptions) -> Self {
46    let namespace = options.namespace.unwrap_or("".to_string());
47
48    let source_url_comment = options
49      .source_url_comment
50      .unwrap_or("\n//# sourceURL=[url]".to_string());
51
52    let module_filename_template =
53      options
54        .module_filename_template
55        .unwrap_or(ModuleFilenameTemplate::String(
56          "webpack://[namespace]/[resource-path]?[hash]".to_string(),
57        ));
58
59    Self::new_inner(
60      namespace,
61      source_url_comment,
62      module_filename_template,
63      Default::default(),
64    )
65  }
66}
67
68#[plugin_hook(CompilerCompilation for EvalDevToolModulePlugin)]
69async fn compilation(
70  &self,
71  compilation: &mut Compilation,
72  _params: &mut CompilationParams,
73) -> Result<()> {
74  let hooks = JsPlugin::get_compilation_hooks_mut(compilation.id());
75  let mut hooks = hooks.write().await;
76  hooks
77    .render_module_content
78    .tap(render_module_content::new(self));
79  hooks.chunk_hash.tap(js_chunk_hash::new(self));
80  hooks
81    .inline_in_runtime_bailout
82    .tap(inline_in_runtime_bailout::new(self));
83
84  Ok(())
85}
86
87#[plugin_hook(JavascriptModulesRenderModuleContent for EvalDevToolModulePlugin,tracing=false)]
88async fn render_module_content(
89  &self,
90  compilation: &Compilation,
91  chunk_ukey: &ChunkUkey,
92  module: &dyn Module,
93  render_source: &mut RenderSource,
94  _init_fragments: &mut ChunkInitFragments,
95) -> Result<()> {
96  let origin_source = render_source.source.clone();
97  if let Some(cached_source) = self.cache.get(&origin_source) {
98    render_source.source = cached_source.value().clone();
99    return Ok(());
100  } else if module.as_external_module().is_some() {
101    return Ok(());
102  }
103
104  let Some(chunk) = compilation.chunk_by_ukey.get(chunk_ukey) else {
105    return Ok(());
106  };
107  let path_data = PathData::default()
108    .chunk_id_optional(chunk.id().map(|id| id.as_str()))
109    .chunk_name_optional(chunk.name())
110    .chunk_hash_optional(chunk.rendered_hash(
111      &compilation.chunk_hashes_artifact,
112      compilation.options.output.hash_digest_length,
113    ));
114
115  let filename = Filename::from(self.namespace.as_str());
116  let namespace = compilation.get_path(&filename, path_data).await?;
117
118  let output_options = &compilation.options.output;
119  let str = match &self.module_filename_template {
120    ModuleFilenameTemplate::String(s) => ModuleFilenameHelpers::create_filename_of_string_template(
121      &SourceReference::Module(module.identifier()),
122      compilation,
123      s,
124      output_options,
125      &namespace,
126      None,
127    ),
128    ModuleFilenameTemplate::Fn(f) => {
129      ModuleFilenameHelpers::create_filename_of_fn_template(
130        &SourceReference::Module(module.identifier()),
131        compilation,
132        f,
133        output_options,
134        &namespace,
135        None,
136      )
137      .await?
138    }
139  };
140  let source = {
141    let source = &origin_source.source().into_string_lossy();
142    let footer = format!(
143      "\n{}",
144      &self.source_url_comment.cow_replace(
145        "[url]",
146        encode_uri(&str)
147          .cow_replace("%2F", "/")
148          .cow_replace("%20", "_")
149          .cow_replace("%5E", "^")
150          .cow_replace("%5C", "\\")
151          .trim_start_matches('/')
152      )
153    );
154
155    let module_content =
156      simd_json::to_string(&format!("{{{source}{footer}\n}}")).expect("failed to parse string");
157    RawStringSource::from(format!(
158      "eval({});",
159      if compilation.options.output.trusted_types.is_some() {
160        format!(
161          "{}({})",
162          compilation
163            .runtime_template
164            .render_runtime_globals(&RuntimeGlobals::CREATE_SCRIPT),
165          module_content
166        )
167      } else {
168        module_content
169      }
170    ))
171    .boxed()
172  };
173
174  self.cache.insert(origin_source, source.clone());
175  render_source.source = source;
176  Ok(())
177}
178
179#[plugin_hook(JavascriptModulesChunkHash for EvalDevToolModulePlugin)]
180async fn js_chunk_hash(
181  &self,
182  _compilation: &Compilation,
183  _chunk_ukey: &ChunkUkey,
184  hasher: &mut RspackHash,
185) -> Result<()> {
186  EVAL_DEV_TOOL_MODULE_PLUGIN_NAME.hash(hasher);
187  Ok(())
188}
189
190#[plugin_hook(JavascriptModulesInlineInRuntimeBailout for EvalDevToolModulePlugin)]
191async fn inline_in_runtime_bailout(&self, _compilation: &Compilation) -> Result<Option<String>> {
192  Ok(Some("the eval devtool is used.".to_string()))
193}
194
195impl Plugin for EvalDevToolModulePlugin {
196  fn name(&self) -> &'static str {
197    EVAL_DEV_TOOL_MODULE_PLUGIN_NAME
198  }
199
200  fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
201    ctx.compiler_hooks.compilation.tap(compilation::new(self));
202    ctx
203      .compilation_hooks
204      .additional_module_runtime_requirements
205      .tap(additional_module_runtime_requirements::new(self));
206    Ok(())
207  }
208}
209
210#[plugin_hook(CompilationAdditionalModuleRuntimeRequirements for EvalDevToolModulePlugin,tracing=false)]
211async fn additional_module_runtime_requirements(
212  &self,
213  compilation: &Compilation,
214  _module: &ModuleIdentifier,
215  runtime_requirements: &mut RuntimeGlobals,
216) -> Result<()> {
217  if compilation.options.output.trusted_types.is_some() {
218    runtime_requirements.insert(RuntimeGlobals::CREATE_SCRIPT);
219  }
220
221  Ok(())
222}
223
224// https://tc39.es/ecma262/#sec-encode
225// UNESCAPED is combined by ALWAYS_UNESCAPED and ";/?:@&=+$,#"
226const fn is_unescape(c: u8) -> bool {
227  const TABLE: &[u8] =
228    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!*'();/?:@&=+$,#";
229
230  const TABLE_BIT: u128 = {
231    let mut table: u128 = 0;
232
233    let mut i = 0;
234    while i < TABLE.len() {
235      let c = TABLE[i];
236      table |= 1 << c;
237      i += 1;
238    }
239
240    if i > u128::BITS as usize {
241      panic!("bitset overflow");
242    }
243
244    table
245  };
246
247  (TABLE_BIT & (1 << c)) != 0
248}
249
250// https://tc39.es/ecma262/#sec-encode
251fn encode_uri(string: &str) -> Cow<'_, str> {
252  use std::fmt::Write;
253
254  // Let R be the empty String.
255  let mut r = Cow::Borrowed(string);
256  // Let alwaysUnescaped be the string-concatenation of the ASCII word characters and "-.!~*'()".
257  for (byte_idx, c) in string.char_indices() {
258    let is_unescape = c
259      .try_into()
260      .ok()
261      .filter(|&ascii| is_unescape(ascii))
262      .is_some();
263    if is_unescape {
264      match r {
265        Cow::Borrowed(_) => {
266          continue;
267        }
268        Cow::Owned(mut inner) => {
269          inner.push(c);
270          r = Cow::Owned(inner);
271        }
272      }
273    } else {
274      if let Cow::Borrowed(_) = r {
275        r = Cow::Owned(string[0..byte_idx].to_owned());
276      }
277
278      if let Cow::Owned(mut inner) = r {
279        let mut b = [0u8; 4];
280        for &octet in c.encode_utf8(&mut b).as_bytes() {
281          write!(&mut inner, "%{octet:02X}").expect("write failed");
282        }
283        r = Cow::Owned(inner);
284      }
285    }
286  }
287  r
288}
289
290#[cfg(test)]
291mod test {
292  use super::*;
293
294  // https://github.com/tc39/test262/blob/c47b716e8d6bea0c4510d449fd22b7ed5f8b0151/test/built-ins/encodeURI/S15.1.3.3_A4_T2.js#L6
295  #[test]
296  fn check_russian_alphabet() {
297    assert_eq!(
298      encode_uri("http://ru.wikipedia.org/wiki/Юникод"),
299      "http://ru.wikipedia.org/wiki/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4"
300    );
301    assert_eq!(
302      encode_uri("http://ru.wikipedia.org/wiki/Юникод#Ссылки"),
303      "http://ru.wikipedia.org/wiki/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4#%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B8"
304    );
305    assert_eq!(
306      encode_uri("http://ru.wikipedia.org/wiki/Юникод#Версии Юникода"),
307      "http://ru.wikipedia.org/wiki/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4#%D0%92%D0%B5%D1%80%D1%81%D0%B8%D0%B8%20%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4%D0%B0"
308    );
309  }
310}