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