Skip to main content

rspack_loader_runner/
loader.rs

1use std::{
2  fmt::Display,
3  ops::Deref,
4  sync::{
5    Arc,
6    atomic::{AtomicBool, Ordering},
7  },
8};
9
10use async_trait::async_trait;
11use derive_more::Debug;
12use rspack_cacheable::cacheable_dyn;
13use rspack_collections::Identifier;
14use rspack_error::Result;
15use rspack_paths::{Utf8Path, Utf8PathBuf};
16use rspack_util::identifier::strip_zero_width_space_for_fragment;
17
18use super::{LoaderContext, LoaderRunnerOptions};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum LoaderExecutionKind {
22  Native,
23  JavaScript,
24}
25
26#[derive(Debug)]
27pub struct LoaderItem<Context: Send> {
28  #[debug("{}", loader.identifier())]
29  loader: Arc<dyn Loader<Context>>,
30  /// Loader identifier
31  request: Identifier,
32  /// An absolute path or a virtual path for represent the loader.
33  /// The absolute path is used to represent a loader stayed on the JS side.
34  /// `$` split chain may be used to represent a composed loader chain from the JS side.
35  /// Virtual path with a builtin protocol to represent a loader from the native side. e.g "builtin:".
36  #[allow(dead_code)]
37  path: Utf8PathBuf,
38  /// Query of a loader, starts with `?`
39  #[allow(dead_code)]
40  query: Option<String>,
41  /// Fragment of a loader, starts with `#`.
42  #[allow(dead_code)]
43  fragment: Option<String>,
44  /// Data shared between pitching and normal
45  data: serde_json::Value,
46  r#type: String,
47  cache_options: Option<Box<LoaderRunnerOptions>>,
48  execution_kind: LoaderExecutionKind,
49  pitch_executed: AtomicBool,
50  normal_executed: AtomicBool,
51  /// Whether loader was called with [LoaderContext::finish_with].
52  ///
53  /// Indicates that the loader has finished its work,
54  /// otherwise loader runner will reset [`LoaderContext::content`], [`LoaderContext::source_map`], [`LoaderContext::additional_data`].
55  ///
56  /// This flag is used to align with webpack's behavior:
57  /// If nothing is modified in the loader, the loader will reset the content, source map, and additional data.
58  finish_called: AtomicBool,
59}
60
61impl<C: Send> LoaderItem<C> {
62  #[inline]
63  pub fn execution_kind(&self) -> LoaderExecutionKind {
64    self.execution_kind
65  }
66
67  pub fn loader(&self) -> &Arc<dyn Loader<C>> {
68    &self.loader
69  }
70
71  #[inline]
72  pub fn request(&self) -> Identifier {
73    self.request
74  }
75
76  #[inline]
77  pub fn path(&self) -> &Utf8Path {
78    &self.path
79  }
80
81  #[inline]
82  pub fn query(&self) -> Option<&str> {
83    self.query.as_deref()
84  }
85
86  #[inline]
87  pub fn r#type(&self) -> &str {
88    &self.r#type
89  }
90
91  #[inline]
92  pub fn cache(&self) -> bool {
93    self.cache_options.is_some()
94  }
95
96  #[inline]
97  pub fn loader_name(&self) -> &str {
98    self
99      .cache_options
100      .as_deref()
101      .map_or("", |options| &options.loader_name)
102  }
103
104  #[inline]
105  pub fn options_cache_key(&self) -> &str {
106    self
107      .cache_options
108      .as_deref()
109      .map_or("", |options| &options.options_cache_key)
110  }
111
112  #[inline]
113  pub fn loader_version(&self) -> &str {
114    self
115      .cache_options
116      .as_deref()
117      .map_or("", |options| &options.loader_version)
118  }
119
120  #[inline]
121  pub fn cache_options(&self) -> Option<&LoaderRunnerOptions> {
122    self.cache_options.as_deref()
123  }
124
125  #[inline]
126  pub fn data(&self) -> &serde_json::Value {
127    &self.data
128  }
129
130  #[inline]
131  #[doc(hidden)]
132  pub fn set_data(&mut self, data: serde_json::Value) {
133    self.data = data;
134  }
135
136  #[inline]
137  #[doc(hidden)]
138  pub fn pitch_executed(&self) -> bool {
139    self.pitch_executed.load(Ordering::Relaxed)
140  }
141
142  #[inline]
143  pub fn normal_executed(&self) -> bool {
144    self.normal_executed.load(Ordering::Relaxed)
145  }
146
147  #[inline]
148  #[doc(hidden)]
149  pub fn finish_called(&self) -> bool {
150    self.finish_called.load(Ordering::Relaxed)
151  }
152
153  #[inline]
154  #[doc(hidden)]
155  pub fn set_pitch_executed(&self) {
156    self.pitch_executed.store(true, Ordering::Relaxed)
157  }
158
159  #[inline]
160  #[doc(hidden)]
161  pub fn set_normal_executed(&self) {
162    self.normal_executed.store(true, Ordering::Relaxed)
163  }
164
165  #[inline]
166  #[doc(hidden)]
167  pub fn set_finish_called(&self) {
168    self.finish_called.store(true, Ordering::Relaxed)
169  }
170}
171
172impl<C: Send> Display for LoaderItem<C> {
173  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174    write!(f, "{}", self.loader.identifier())
175  }
176}
177
178#[derive(Debug)]
179pub struct LoaderItemList<'a, Context: Send>(pub &'a [LoaderItem<Context>]);
180
181impl<Context: Send> Deref for LoaderItemList<'_, Context> {
182  type Target = [LoaderItem<Context>];
183
184  fn deref(&self) -> &Self::Target {
185    self.0
186  }
187}
188
189impl<Context: Send> Default for LoaderItemList<'_, Context> {
190  fn default() -> Self {
191    Self(&[])
192  }
193}
194
195pub trait DisplayWithSuffix: Display {
196  fn display_with_suffix(&self, suffix: &str) -> String {
197    let s = self.to_string();
198    if s.is_empty() {
199      return suffix.to_string();
200    }
201    self.to_string() + "!" + suffix
202  }
203}
204
205impl<Context: Send> DisplayWithSuffix for LoaderItemList<'_, Context> {}
206impl<Context: Send> DisplayWithSuffix for LoaderItem<Context> {}
207impl<Context: Send> Display for LoaderItemList<'_, Context> {
208  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209    let s = self
210      .0
211      .iter()
212      .map(|item| item.to_string())
213      .collect::<Vec<_>>()
214      .join("!");
215
216    write!(f, "{s}")
217  }
218}
219
220#[cacheable_dyn]
221#[async_trait]
222pub trait Loader<Context = ()>: Send + Sync
223where
224  Context: Send,
225{
226  /// Returns the unique identifier for this loader
227  fn identifier(&self) -> Identifier;
228
229  async fn run(&self, loader_context: &mut LoaderContext<Context>) -> Result<()> {
230    // If loader does not implement normal stage,
231    // it should inherit the result from the previous loader.
232    loader_context.current_loader().set_finish_called();
233    Ok(())
234  }
235
236  async fn pitch(&self, _loader_context: &mut LoaderContext<Context>) -> Result<()> {
237    // noop
238    Ok(())
239  }
240
241  /// Returns the loader type based on the module's package.json type field or file extension.
242  /// This affects how the loader context interprets the module (e.g., "commonjs", "module").
243  fn r#type(&self) -> Option<&str> {
244    None
245  }
246
247  /// Version identity used by loader caching.
248  fn cache_version(&self) -> Option<&str> {
249    None
250  }
251
252  /// Selects the runtime responsible for executing this loader.
253  fn execution_kind(&self) -> LoaderExecutionKind {
254    LoaderExecutionKind::Native
255  }
256}
257
258impl<C: Send> From<Arc<dyn Loader<C>>> for LoaderItem<C> {
259  fn from(loader: Arc<dyn Loader<C>>) -> Self {
260    Self::new(loader, LoaderRunnerOptions::default())
261  }
262}
263
264impl<C: Send> LoaderItem<C> {
265  pub(crate) fn new(loader: Arc<dyn Loader<C>>, options: LoaderRunnerOptions) -> Self {
266    let cache_options = options.cache.then(|| Box::new(options));
267    let ident = &**loader.identifier();
268    let execution_kind = loader.execution_kind();
269    if let Some(r#type) = loader.r#type() {
270      let ResourceParsedData {
271        path,
272        query,
273        fragment,
274      } = parse_resource(ident).expect("identifier should be valid");
275      let ty = r#type.to_string();
276      return Self {
277        loader,
278        request: ident.into(),
279        path,
280        query,
281        fragment,
282        data: serde_json::Value::Null,
283        r#type: ty,
284        cache_options,
285        execution_kind,
286        pitch_executed: AtomicBool::new(false),
287        normal_executed: AtomicBool::new(false),
288        finish_called: AtomicBool::new(false),
289      };
290    }
291    let ident = loader.identifier();
292    let ResourceParsedData {
293      path,
294      query,
295      fragment,
296    } = parse_resource(&ident).expect("identifier should be valid");
297    Self {
298      loader,
299      request: ident,
300      path,
301      query,
302      fragment,
303      data: serde_json::Value::Null,
304      r#type: String::default(),
305      cache_options,
306      execution_kind,
307      pitch_executed: AtomicBool::new(false),
308      normal_executed: AtomicBool::new(false),
309      finish_called: AtomicBool::new(false),
310    }
311  }
312}
313
314#[derive(Debug)]
315pub struct ResourceParsedData {
316  pub path: Utf8PathBuf,
317  pub query: Option<String>,
318  pub fragment: Option<String>,
319}
320
321pub fn parse_resource(resource: &str) -> Option<ResourceParsedData> {
322  let (path, query, fragment) = path_query_fragment(resource).ok()?;
323
324  Some(ResourceParsedData {
325    path: strip_zero_width_space_for_fragment(path)
326      .into_owned()
327      .into(),
328    query: query.map(|q| strip_zero_width_space_for_fragment(q).into_owned()),
329    fragment: fragment.map(|f| f.to_owned()),
330  })
331}
332
333#[cfg(not(windows))]
334fn path_query_fragment(input: &str) -> winnow::ModalResult<(&str, Option<&str>, Option<&str>)> {
335  path_query_fragment_impl(input)
336}
337
338#[cfg(windows)]
339fn path_query_fragment(input: &str) -> winnow::ModalResult<(&str, Option<&str>, Option<&str>)> {
340  let prefix_len = rspack_paths::dos_device_path_prefix_len(input);
341  let (path, query, fragment) = path_query_fragment_impl(&input[prefix_len..])?;
342  let path = &input[..prefix_len + path.len()];
343  Ok((path, query, fragment))
344}
345
346fn path_query_fragment_impl(
347  mut input: &str,
348) -> winnow::ModalResult<(&str, Option<&str>, Option<&str>)> {
349  use winnow::{
350    combinator::{alt, opt, repeat},
351    prelude::*,
352    token::{any, none_of, rest},
353  };
354
355  let path = alt((
356    ('\u{200b}', any).take(),
357    none_of(('?', '#', '\u{200b}')).take(),
358  ));
359  let query = alt((('\u{200b}', any).take(), none_of(('#', '\u{200b}')).take()));
360  let fragment = rest;
361
362  let mut parser = (
363    repeat::<_, _, (), _, _>(.., path).take(),
364    opt(('?', repeat::<_, _, (), _, _>(.., query)).take()),
365    opt(('#', fragment).take()),
366  );
367
368  parser.parse_next(&mut input)
369}
370
371#[cfg(test)]
372pub(crate) mod test {
373  use std::{path::PathBuf, sync::Arc};
374
375  use rspack_cacheable::{cacheable, cacheable_dyn};
376  use rspack_collections::Identifier;
377
378  use super::{Loader, LoaderItem};
379
380  #[cacheable]
381  #[allow(dead_code)]
382  pub(crate) struct Custom;
383  #[cacheable_dyn]
384  #[async_trait::async_trait]
385  impl Loader<()> for Custom {
386    fn identifier(&self) -> Identifier {
387      "/rspack/custom-loader-1/index.js?foo=1#baz".into()
388    }
389  }
390
391  #[cacheable]
392  #[allow(dead_code)]
393  pub(crate) struct Custom2;
394  #[cacheable_dyn]
395  #[async_trait::async_trait]
396  impl Loader<()> for Custom2 {
397    fn identifier(&self) -> Identifier {
398      "/rspack/custom-loader-2/index.js?bar=2#baz".into()
399    }
400  }
401
402  #[cacheable]
403  #[allow(dead_code)]
404  pub(crate) struct Builtin;
405  #[cacheable_dyn]
406  #[async_trait::async_trait]
407  impl Loader<()> for Builtin {
408    fn identifier(&self) -> Identifier {
409      "builtin:test-loader".into()
410    }
411  }
412
413  #[cacheable]
414  pub(crate) struct PosixNonLenBlankUnicode;
415
416  #[cacheable_dyn]
417  #[async_trait::async_trait]
418  impl Loader<()> for PosixNonLenBlankUnicode {
419    fn identifier(&self) -> Identifier {
420      "/a/b/c.js?{\"c\": \"\u{200b}#foo\"}".into()
421    }
422  }
423
424  #[cacheable]
425  pub(crate) struct WinNonLenBlankUnicode;
426  #[cacheable_dyn]
427  #[async_trait::async_trait]
428  impl Loader<()> for WinNonLenBlankUnicode {
429    fn identifier(&self) -> Identifier {
430      "\\a\\b\\c.js?{\"c\": \"\u{200b}#foo\"}".into()
431    }
432  }
433
434  #[test]
435  fn should_handle_posix_non_len_blank_unicode_correctly() {
436    let c1 = Arc::new(PosixNonLenBlankUnicode) as Arc<dyn Loader<()>>;
437    let l: LoaderItem<()> = c1.into();
438    assert_eq!(l.path, PathBuf::from("/a/b/c.js"));
439    assert_eq!(l.query, Some("?{\"c\": \"#foo\"}".into()));
440    assert_eq!(l.fragment, None);
441  }
442
443  #[test]
444  fn should_handle_win_non_len_blank_unicode_correctly() {
445    let c1 = Arc::new(WinNonLenBlankUnicode) as Arc<dyn Loader<()>>;
446    let l: LoaderItem<()> = c1.into();
447    assert_eq!(l.path, PathBuf::from(r#"\a\b\c.js"#));
448    assert_eq!(l.query, Some("?{\"c\": \"#foo\"}".into()));
449    assert_eq!(l.fragment, None);
450  }
451}