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
224const 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
250fn encode_uri(string: &str) -> Cow<'_, str> {
252 use std::fmt::Write;
253
254 let mut r = Cow::Borrowed(string);
256 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 #[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}