turbopack/module_options/
rule_condition.rs1use 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(FileSystemPath),
19 ResourcePathHasNoExtension,
20 ResourcePathEndsWith(String),
21 ResourcePathInDirectory(String),
22 ResourcePathInExactDirectory(FileSystemPath),
23 ContentTypeStartsWith(String),
24 ContentTypeEmpty,
25 ResourcePathRegex(#[turbo_tasks(trace_ignore)] Regex),
26 ResourcePathEsRegex(#[turbo_tasks(trace_ignore)] ReadRef<EsRegex>),
27 ResourcePathGlob {
34 base: 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]), Any(&'a [RuleCondition]), Not, }
68
69 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 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 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 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 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 if !result {
199 if remaining.len() > 1 {
200 stack.push(Op::Any(&remaining[1..]));
201 }
202 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)]
225pub mod 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 { run_leaves_test().await })
241 .await
242 .unwrap();
243 }
244
245 #[turbo_tasks::function]
246 pub async fn run_leaves_test() -> Result<()> {
247 let fs = VirtualFileSystem::new();
248 let virtual_path = fs.root().await?.join("foo.js")?;
249 let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
250 virtual_path.clone(),
251 AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
252 ))
253 .to_resolved()
254 .await?;
255
256 let non_virtual_path = fs.root().await?.join("bar.js")?;
257 let non_virtual_source =
258 Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path.clone()))
259 .to_resolved()
260 .await?;
261
262 {
263 let condition = RuleCondition::ReferenceType(ReferenceType::Runtime);
264 assert!(
265 condition
266 .matches(virtual_source, &virtual_path, &ReferenceType::Runtime)
267 .await
268 .unwrap()
269 );
270 assert!(
271 !condition
272 .matches(
273 non_virtual_source,
274 &non_virtual_path,
275 &ReferenceType::Css(
276 turbopack_core::reference_type::CssReferenceSubType::Compose
277 )
278 )
279 .await
280 .unwrap()
281 );
282 }
283
284 {
285 let condition = RuleCondition::ResourceIsVirtualSource;
286 assert!(
287 condition
288 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
289 .await
290 .unwrap()
291 );
292 assert!(
293 !condition
294 .matches(
295 non_virtual_source,
296 &non_virtual_path,
297 &ReferenceType::Undefined
298 )
299 .await
300 .unwrap()
301 );
302 }
303 {
304 let condition = RuleCondition::ResourcePathEquals(virtual_path.clone());
305 assert!(
306 condition
307 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
308 .await
309 .unwrap()
310 );
311 assert!(
312 !condition
313 .matches(
314 non_virtual_source,
315 &non_virtual_path,
316 &ReferenceType::Undefined
317 )
318 .await
319 .unwrap()
320 );
321 }
322 {
323 let condition = RuleCondition::ResourcePathHasNoExtension;
324 assert!(
325 condition
326 .matches(
327 virtual_source,
328 &fs.root().await?.join("foo")?,
329 &ReferenceType::Undefined
330 )
331 .await
332 .unwrap()
333 );
334 assert!(
335 !condition
336 .matches(
337 non_virtual_source,
338 &non_virtual_path,
339 &ReferenceType::Undefined
340 )
341 .await
342 .unwrap()
343 );
344 }
345 {
346 let condition = RuleCondition::ResourcePathEndsWith("foo.js".to_string());
347 assert!(
348 condition
349 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
350 .await
351 .unwrap()
352 );
353 assert!(
354 !condition
355 .matches(
356 non_virtual_source,
357 &non_virtual_path,
358 &ReferenceType::Undefined
359 )
360 .await
361 .unwrap()
362 );
363 }
364 anyhow::Ok(())
365 }
366
367 #[tokio::test]
368 async fn test_rule_condition_tree() {
369 crate::register();
370 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
371 BackendOptions::default(),
372 noop_backing_storage(),
373 ));
374 tt.run_once(async { run_rule_condition_tree_test().await })
375 .await
376 .unwrap();
377 }
378
379 #[turbo_tasks::function]
380 pub async fn run_rule_condition_tree_test() -> Result<()> {
381 let fs = VirtualFileSystem::new();
382 let virtual_path = fs.root().await?.join("foo.js")?;
383 let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
384 virtual_path.clone(),
385 AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
386 ))
387 .to_resolved()
388 .await?;
389
390 let non_virtual_path = fs.root().await?.join("bar.js")?;
391 let non_virtual_source =
392 Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path.clone()))
393 .to_resolved()
394 .await?;
395
396 {
397 let condition = RuleCondition::not(RuleCondition::ResourceIsVirtualSource);
399 assert!(
400 !condition
401 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
402 .await
403 .unwrap()
404 );
405 assert!(
406 condition
407 .matches(
408 non_virtual_source,
409 &non_virtual_path,
410 &ReferenceType::Undefined
411 )
412 .await
413 .unwrap()
414 );
415 }
416 {
417 let condition = RuleCondition::any(vec![
420 RuleCondition::ResourcePathInDirectory("doesnt/exist".to_string()),
421 RuleCondition::ResourceIsVirtualSource,
422 RuleCondition::ResourcePathHasNoExtension,
423 ]);
424 assert!(
425 condition
426 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
427 .await
428 .unwrap()
429 );
430 assert!(
431 !condition
432 .matches(
433 non_virtual_source,
434 &non_virtual_path,
435 &ReferenceType::Undefined
436 )
437 .await
438 .unwrap()
439 );
440 }
441 {
442 let condition = RuleCondition::all(vec![
445 RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
446 RuleCondition::ResourceIsVirtualSource,
447 RuleCondition::ResourcePathEquals(virtual_path.clone()),
448 ]);
449 assert!(
450 condition
451 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
452 .await
453 .unwrap()
454 );
455 assert!(
456 !condition
457 .matches(
458 non_virtual_source,
459 &non_virtual_path,
460 &ReferenceType::Undefined
461 )
462 .await
463 .unwrap()
464 );
465 }
466 {
467 let condition = RuleCondition::all(vec![
471 RuleCondition::ResourceIsVirtualSource,
472 RuleCondition::ResourcePathEquals(virtual_path.clone()),
473 RuleCondition::Not(Box::new(RuleCondition::ResourcePathHasNoExtension)),
474 RuleCondition::Any(vec![
475 RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
476 RuleCondition::ContentTypeEmpty,
477 ]),
478 ]);
479 assert!(
480 condition
481 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
482 .await
483 .unwrap()
484 );
485 assert!(
486 !condition
487 .matches(
488 non_virtual_source,
489 &non_virtual_path,
490 &ReferenceType::Undefined
491 )
492 .await
493 .unwrap()
494 );
495 }
496 anyhow::Ok(())
497 }
498}