turbopack/module_options/
rule_condition.rs

1use anyhow::{Result, bail};
2use serde::{Deserialize, Serialize};
3use smallvec::SmallVec;
4use turbo_esregex::EsRegex;
5use turbo_tasks::{NonLocalValue, ReadRef, ResolvedVc, primitives::Regex, trace::TraceRawVcs};
6use turbo_tasks_fs::{FileSystemPath, glob::Glob};
7use turbopack_core::{
8    reference_type::ReferenceType, source::Source, virtual_source::VirtualSource,
9};
10
11#[derive(Debug, Clone, Serialize, Deserialize, TraceRawVcs, PartialEq, Eq, NonLocalValue)]
12pub enum RuleCondition {
13    All(Vec<RuleCondition>),
14    Any(Vec<RuleCondition>),
15    Not(Box<RuleCondition>),
16    ReferenceType(ReferenceType),
17    ResourceIsVirtualSource,
18    ResourcePathEquals(ReadRef<FileSystemPath>),
19    ResourcePathHasNoExtension,
20    ResourcePathEndsWith(String),
21    ResourcePathInDirectory(String),
22    ResourcePathInExactDirectory(ReadRef<FileSystemPath>),
23    ContentTypeStartsWith(String),
24    ContentTypeEmpty,
25    ResourcePathRegex(#[turbo_tasks(trace_ignore)] Regex),
26    ResourcePathEsRegex(#[turbo_tasks(trace_ignore)] ReadRef<EsRegex>),
27    /// For paths that are within the same filesystem as the `base`, it need to
28    /// match the relative path from base to resource. This includes `./` or
29    /// `../` prefix. For paths in a different filesystem, it need to match
30    /// the resource path in that filesystem without any prefix. This means
31    /// any glob starting with `./` or `../` will only match paths in the
32    /// project. Globs starting with `**` can match any path.
33    ResourcePathGlob {
34        base: ReadRef<FileSystemPath>,
35        #[turbo_tasks(trace_ignore)]
36        glob: ReadRef<Glob>,
37    },
38    ResourceBasePathGlob(#[turbo_tasks(trace_ignore)] ReadRef<Glob>),
39}
40
41impl RuleCondition {
42    pub fn all(conditions: Vec<RuleCondition>) -> RuleCondition {
43        RuleCondition::All(conditions)
44    }
45
46    pub fn any(conditions: Vec<RuleCondition>) -> RuleCondition {
47        RuleCondition::Any(conditions)
48    }
49
50    #[allow(clippy::should_implement_trait)]
51    pub fn not(condition: RuleCondition) -> RuleCondition {
52        RuleCondition::Not(Box::new(condition))
53    }
54}
55
56impl RuleCondition {
57    pub async fn matches(
58        &self,
59        source: ResolvedVc<Box<dyn Source>>,
60        path: &FileSystemPath,
61        reference_type: &ReferenceType,
62    ) -> Result<bool> {
63        enum Op<'a> {
64            All(&'a [RuleCondition]), // Remaining conditions in an All
65            Any(&'a [RuleCondition]), // Remaining conditions in an Any
66            Not,                      // Inverts the previous condition
67        }
68
69        // Evaluates the condition returning the result and possibly pushing additional operations
70        // onto the stack as a kind of continuation.
71        async fn process_condition<'a, const SZ: usize>(
72            source: ResolvedVc<Box<dyn Source + 'static>>,
73            path: &FileSystemPath,
74            reference_type: &ReferenceType,
75            stack: &mut SmallVec<[Op<'a>; SZ]>,
76            mut cond: &'a RuleCondition,
77        ) -> Result<bool, anyhow::Error> {
78            // Use a loop to avoid recursion and unnecessary stack operations.
79            loop {
80                match cond {
81                    RuleCondition::All(conditions) => {
82                        if conditions.is_empty() {
83                            return Ok(true);
84                        } else {
85                            if conditions.len() > 1 {
86                                stack.push(Op::All(&conditions[1..]));
87                            }
88                            cond = &conditions[0];
89                            // jump directly to the next condition, no need to deal with
90                            // the stack.
91                            continue;
92                        }
93                    }
94                    RuleCondition::Any(conditions) => {
95                        if conditions.is_empty() {
96                            return Ok(false);
97                        } else {
98                            if conditions.len() > 1 {
99                                stack.push(Op::Any(&conditions[1..]));
100                            }
101                            cond = &conditions[0];
102                            continue;
103                        }
104                    }
105                    RuleCondition::Not(inner) => {
106                        stack.push(Op::Not);
107                        cond = inner.as_ref();
108                        continue;
109                    }
110                    RuleCondition::ReferenceType(condition_ty) => {
111                        return Ok(condition_ty.includes(reference_type));
112                    }
113                    RuleCondition::ResourceIsVirtualSource => {
114                        return Ok(ResolvedVc::try_downcast_type::<VirtualSource>(source).is_some());
115                    }
116                    RuleCondition::ResourcePathEquals(other) => {
117                        return Ok(path == &**other);
118                    }
119                    RuleCondition::ResourcePathEndsWith(end) => {
120                        return Ok(path.path.ends_with(end));
121                    }
122                    RuleCondition::ResourcePathHasNoExtension => {
123                        return Ok(if let Some(i) = path.path.rfind('.') {
124                            if let Some(j) = path.path.rfind('/') {
125                                j > i
126                            } else {
127                                false
128                            }
129                        } else {
130                            true
131                        });
132                    }
133                    RuleCondition::ResourcePathInDirectory(dir) => {
134                        return Ok(path.path.starts_with(&format!("{dir}/"))
135                            || path.path.contains(&format!("/{dir}/")));
136                    }
137                    RuleCondition::ResourcePathInExactDirectory(parent_path) => {
138                        return Ok(path.is_inside_ref(parent_path));
139                    }
140                    RuleCondition::ContentTypeStartsWith(start) => {
141                        let content_type = &source.ident().await?.content_type;
142                        return Ok(content_type
143                            .as_ref()
144                            .is_some_and(|ct| ct.starts_with(start)));
145                    }
146                    RuleCondition::ContentTypeEmpty => {
147                        return Ok(source.ident().await?.content_type.is_none());
148                    }
149                    RuleCondition::ResourcePathGlob { glob, base } => {
150                        return Ok(if let Some(rel_path) = base.get_relative_path_to(path) {
151                            glob.matches(&rel_path)
152                        } else {
153                            glob.matches(&path.path)
154                        });
155                    }
156                    RuleCondition::ResourceBasePathGlob(glob) => {
157                        let basename = path
158                            .path
159                            .rsplit_once('/')
160                            .map_or(path.path.as_str(), |(_, b)| b);
161                        return Ok(glob.matches(basename));
162                    }
163                    RuleCondition::ResourcePathRegex(_) => {
164                        bail!("ResourcePathRegex not implemented yet");
165                    }
166                    RuleCondition::ResourcePathEsRegex(regex) => {
167                        return Ok(regex.is_match(&path.path));
168                    }
169                }
170            }
171        }
172        // Allocate a small inline stack to avoid heap allocations in the common case where
173        // conditions are not deeply stacked.  Additionally we take care to avoid stack
174        // operations unless strictly necessary.
175        const EXPECTED_SIZE: usize = 8;
176        let mut stack = SmallVec::<[Op; EXPECTED_SIZE]>::with_capacity(EXPECTED_SIZE);
177        let mut result = process_condition(source, path, reference_type, &mut stack, self).await?;
178        while let Some(op) = stack.pop() {
179            match op {
180                Op::All(remaining) => {
181                    // Previous was true, keep going
182                    if result {
183                        if remaining.len() > 1 {
184                            stack.push(Op::All(&remaining[1..]));
185                        }
186                        result = process_condition(
187                            source,
188                            path,
189                            reference_type,
190                            &mut stack,
191                            &remaining[0],
192                        )
193                        .await?;
194                    }
195                }
196                Op::Any(remaining) => {
197                    // Previous was false, keep going
198                    if !result {
199                        if remaining.len() > 1 {
200                            stack.push(Op::Any(&remaining[1..]));
201                        }
202                        // If the stack didn't change, we can loop inline, but we would still need
203                        // to pop the item.  This might be faster since we would avoid the `match`
204                        // but overall, that is quite minor for an enum with 3 cases.
205                        result = process_condition(
206                            source,
207                            path,
208                            reference_type,
209                            &mut stack,
210                            &remaining[0],
211                        )
212                        .await?;
213                    }
214                }
215                Op::Not => {
216                    result = !result;
217                }
218            }
219        }
220        Ok(result)
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use turbo_tasks::Vc;
227    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
228    use turbo_tasks_fs::{FileContent, FileSystem, VirtualFileSystem};
229    use turbopack_core::{asset::AssetContent, file_source::FileSource};
230
231    use super::*;
232
233    #[tokio::test]
234    async fn test_rule_condition_leaves() {
235        crate::register();
236        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
237            BackendOptions::default(),
238            noop_backing_storage(),
239        ));
240        tt.run_once(async {
241            let fs = VirtualFileSystem::new();
242            let virtual_path = fs.root().join("foo.js".into());
243            let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
244                virtual_path,
245                AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
246            ))
247            .to_resolved()
248            .await?;
249
250            let non_virtual_path = fs.root().join("bar.js".into());
251            let non_virtual_source =
252                Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path))
253                    .to_resolved()
254                    .await?;
255
256            {
257                let condition = RuleCondition::ReferenceType(ReferenceType::Runtime);
258                assert!(
259                    condition
260                        .matches(
261                            virtual_source,
262                            &*virtual_path.await?,
263                            &ReferenceType::Runtime
264                        )
265                        .await
266                        .unwrap()
267                );
268                assert!(
269                    !condition
270                        .matches(
271                            non_virtual_source,
272                            &*non_virtual_path.await?,
273                            &ReferenceType::Css(
274                                turbopack_core::reference_type::CssReferenceSubType::Compose
275                            )
276                        )
277                        .await
278                        .unwrap()
279                );
280            }
281
282            {
283                let condition = RuleCondition::ResourceIsVirtualSource;
284                assert!(
285                    condition
286                        .matches(
287                            virtual_source,
288                            &*virtual_path.await?,
289                            &ReferenceType::Undefined
290                        )
291                        .await
292                        .unwrap()
293                );
294                assert!(
295                    !condition
296                        .matches(
297                            non_virtual_source,
298                            &*non_virtual_path.await?,
299                            &ReferenceType::Undefined
300                        )
301                        .await
302                        .unwrap()
303                );
304            }
305            {
306                let condition = RuleCondition::ResourcePathEquals(virtual_path.await?);
307                assert!(
308                    condition
309                        .matches(
310                            virtual_source,
311                            &*virtual_path.await?,
312                            &ReferenceType::Undefined
313                        )
314                        .await
315                        .unwrap()
316                );
317                assert!(
318                    !condition
319                        .matches(
320                            non_virtual_source,
321                            &*non_virtual_path.await?,
322                            &ReferenceType::Undefined
323                        )
324                        .await
325                        .unwrap()
326                );
327            }
328            {
329                let condition = RuleCondition::ResourcePathHasNoExtension;
330                assert!(
331                    condition
332                        .matches(
333                            virtual_source,
334                            &*fs.root().join("foo".into()).await?,
335                            &ReferenceType::Undefined
336                        )
337                        .await
338                        .unwrap()
339                );
340                assert!(
341                    !condition
342                        .matches(
343                            non_virtual_source,
344                            &*non_virtual_path.await?,
345                            &ReferenceType::Undefined
346                        )
347                        .await
348                        .unwrap()
349                );
350            }
351            {
352                let condition = RuleCondition::ResourcePathEndsWith("foo.js".to_string());
353                assert!(
354                    condition
355                        .matches(
356                            virtual_source,
357                            &*virtual_path.await?,
358                            &ReferenceType::Undefined
359                        )
360                        .await
361                        .unwrap()
362                );
363                assert!(
364                    !condition
365                        .matches(
366                            non_virtual_source,
367                            &*non_virtual_path.await?,
368                            &ReferenceType::Undefined
369                        )
370                        .await
371                        .unwrap()
372                );
373            }
374            anyhow::Ok(())
375        })
376        .await
377        .unwrap();
378    }
379
380    #[tokio::test]
381    async fn test_rule_condition_tree() {
382        crate::register();
383        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
384            BackendOptions::default(),
385            noop_backing_storage(),
386        ));
387        tt.run_once(async {
388            let fs = VirtualFileSystem::new();
389            let virtual_path = fs.root().join("foo.js".into());
390            let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
391                virtual_path,
392                AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
393            ))
394            .to_resolved()
395            .await?;
396
397            let non_virtual_path = fs.root().join("bar.js".into());
398            let non_virtual_source =
399                Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path))
400                    .to_resolved()
401                    .await?;
402
403            {
404                // not
405                let condition = RuleCondition::not(RuleCondition::ResourceIsVirtualSource);
406                assert!(
407                    !condition
408                        .matches(
409                            virtual_source,
410                            &*virtual_path.await?,
411                            &ReferenceType::Undefined
412                        )
413                        .await
414                        .unwrap()
415                );
416                assert!(
417                    condition
418                        .matches(
419                            non_virtual_source,
420                            &*non_virtual_path.await?,
421                            &ReferenceType::Undefined
422                        )
423                        .await
424                        .unwrap()
425                );
426            }
427            {
428                // any
429                // Only one of the conditions matches our virtual source
430                let condition = RuleCondition::any(vec![
431                    RuleCondition::ResourcePathInDirectory("doesnt/exist".to_string()),
432                    RuleCondition::ResourceIsVirtualSource,
433                    RuleCondition::ResourcePathHasNoExtension,
434                ]);
435                assert!(
436                    condition
437                        .matches(
438                            virtual_source,
439                            &*virtual_path.await?,
440                            &ReferenceType::Undefined
441                        )
442                        .await
443                        .unwrap()
444                );
445                assert!(
446                    !condition
447                        .matches(
448                            non_virtual_source,
449                            &*non_virtual_path.await?,
450                            &ReferenceType::Undefined
451                        )
452                        .await
453                        .unwrap()
454                );
455            }
456            {
457                // all
458                // Only one of the conditions matches our virtual source
459                let condition = RuleCondition::all(vec![
460                    RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
461                    RuleCondition::ResourceIsVirtualSource,
462                    RuleCondition::ResourcePathEquals(virtual_path.await?),
463                ]);
464                assert!(
465                    condition
466                        .matches(
467                            virtual_source,
468                            &*virtual_path.await?,
469                            &ReferenceType::Undefined
470                        )
471                        .await
472                        .unwrap()
473                );
474                assert!(
475                    !condition
476                        .matches(
477                            non_virtual_source,
478                            &*non_virtual_path.await?,
479                            &ReferenceType::Undefined
480                        )
481                        .await
482                        .unwrap()
483                );
484            }
485            {
486                // bigger tree
487
488                // Build a simple tree to cover our various composite conditions
489                let condition = RuleCondition::all(vec![
490                    RuleCondition::ResourceIsVirtualSource,
491                    RuleCondition::ResourcePathEquals(virtual_path.await?),
492                    RuleCondition::Not(Box::new(RuleCondition::ResourcePathHasNoExtension)),
493                    RuleCondition::Any(vec![
494                        RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
495                        RuleCondition::ContentTypeEmpty,
496                    ]),
497                ]);
498                assert!(
499                    condition
500                        .matches(
501                            virtual_source,
502                            &*virtual_path.await?,
503                            &ReferenceType::Undefined
504                        )
505                        .await
506                        .unwrap()
507                );
508                assert!(
509                    !condition
510                        .matches(
511                            non_virtual_source,
512                            &*non_virtual_path.await?,
513                            &ReferenceType::Undefined
514                        )
515                        .await
516                        .unwrap()
517                );
518            }
519            anyhow::Ok(())
520        })
521        .await
522        .unwrap();
523    }
524}