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(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 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]), 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)]
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 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 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 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 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}