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 request: Identifier,
32 #[allow(dead_code)]
37 path: Utf8PathBuf,
38 #[allow(dead_code)]
40 query: Option<String>,
41 #[allow(dead_code)]
43 fragment: Option<String>,
44 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 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 fn identifier(&self) -> Identifier;
228
229 async fn run(&self, loader_context: &mut LoaderContext<Context>) -> Result<()> {
230 loader_context.current_loader().set_finish_called();
233 Ok(())
234 }
235
236 async fn pitch(&self, _loader_context: &mut LoaderContext<Context>) -> Result<()> {
237 Ok(())
239 }
240
241 fn r#type(&self) -> Option<&str> {
244 None
245 }
246
247 fn cache_version(&self) -> Option<&str> {
249 None
250 }
251
252 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}