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 request: Identifier,
26 #[allow(dead_code)]
31 path: Utf8PathBuf,
32 #[allow(dead_code)]
34 query: Option<String>,
35 #[allow(dead_code)]
37 fragment: Option<String>,
38 data: serde_json::Value,
40 r#type: String,
41 pitch_executed: AtomicBool,
42 normal_executed: AtomicBool,
43 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 fn identifier(&self) -> Identifier;
186
187 async fn run(&self, loader_context: &mut LoaderContext<Context>) -> Result<()> {
188 loader_context.current_loader().set_finish_called();
191 Ok(())
192 }
193
194 async fn pitch(&self, _loader_context: &mut LoaderContext<Context>) -> Result<()> {
195 Ok(())
197 }
198
199 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}