rspack_core 0.100.1

rspack core
Documentation
mod helper;

use std::{collections::VecDeque, path::PathBuf, sync::Arc};

use rspack_error::Result;
use rspack_fs::ReadableFileSystem;
use rspack_paths::{ArcPath, ArcPathSet, AssertUtf8};
use rustc_hash::FxHashSet as HashSet;

use self::helper::{Helper, is_node_package_path};
use super::{
  snapshot::{Snapshot, SnapshotScope},
  storage::Storage,
};

pub const SCOPE: &str = "build_dependencies";

pub type BuildDepsOptions = Vec<PathBuf>;

/// Build dependencies manager
#[derive(Debug)]
pub struct BuildDeps {
  /// The build dependencies has been added to snapshot.
  ///
  /// This field is used to avoid adding duplicate build dependencies to the snapshot.
  added: ArcPathSet,
  /// The pending dependencies.
  ///
  /// The next time the add method is called, this path will be additionally added.
  pending: ArcPathSet,
  /// The snapshot which is used to save build dependencies.
  snapshot: Arc<Snapshot>,
  fs: Arc<dyn ReadableFileSystem>,
}

impl BuildDeps {
  pub fn new(
    options: &BuildDepsOptions,
    fs: Arc<dyn ReadableFileSystem>,
    snapshot: Arc<Snapshot>,
  ) -> Self {
    Self {
      added: Default::default(),
      pending: options.iter().map(|v| ArcPath::from(v.as_path())).collect(),
      snapshot,
      fs,
    }
  }

  /// Reset build dependencies scope in storage
  pub fn reset(&self, storage: &mut dyn Storage) {
    storage.reset(SnapshotScope::BUILD.name());
  }

  /// Add build dependencies
  ///
  /// For performance reasons, recursive searches will stop for build dependencies in node_modules.
  pub async fn add(
    &mut self,
    storage: &mut dyn Storage,
    data: impl Iterator<Item = ArcPath>,
  ) -> Vec<String> {
    let mut helper = Helper::new(self.fs.clone());
    let mut new_deps = HashSet::default();
    let mut queue = VecDeque::new();
    queue.extend(std::mem::take(&mut self.pending));
    queue.extend(data);
    while let Some(current) = queue.pop_front() {
      if !self.added.insert(current.clone()) {
        continue;
      }
      new_deps.insert(current.clone());
      if is_node_package_path(&current) {
        // node package path skip recursive search.
        continue;
      }
      if let Some(children) = helper.resolve(current.assert_utf8()).await {
        queue.extend(children.iter().map(|item| item.as_path().into()));
      }
    }

    self
      .snapshot
      .add(storage, SnapshotScope::BUILD, new_deps.into_iter())
      .await;
    helper.into_warnings()
  }

  /// Validate build dependencies
  ///
  /// If any build dependencies have changed, this method will return false.
  pub async fn validate(&mut self, storage: &dyn Storage) -> Result<bool> {
    let (_, modified_files, removed_files, no_changed_files) = self
      .snapshot
      .calc_modified_paths(storage, SnapshotScope::BUILD)
      .await?;

    if !modified_files.is_empty() || !removed_files.is_empty() {
      tracing::info!(
        "BuildDependencies: cache invalidate by modified_files {modified_files:?} and removed_files {removed_files:?}"
      );
      return Ok(false);
    }
    self.added = no_changed_files;
    Ok(true)
  }
}

#[cfg(test)]
mod test {
  use std::{path::PathBuf, sync::Arc};

  use rspack_fs::{MemoryFileSystem, WritableFileSystem};

  use super::{
    super::{
      codec::CacheCodec,
      snapshot::{Snapshot, SnapshotOptions, SnapshotScope},
      storage::{MemoryStorage, Storage},
    },
    BuildDeps,
  };
  #[tokio::test]
  async fn build_dependencies_test() {
    let scope = SnapshotScope::BUILD.name();
    let fs = Arc::new(MemoryFileSystem::default());
    fs.create_dir_all("/configs/test".into()).await.unwrap();
    fs.write("/configs/a.js".into(), r#"console.log('a')"#.as_bytes())
      .await
      .unwrap();
    fs.write(
      "/configs/test/b.js".into(),
      r#"console.log('b')"#.as_bytes(),
    )
    .await
    .unwrap();
    fs.write(
      "/configs/test/b1.js".into(),
      r#"console.log('b1')"#.as_bytes(),
    )
    .await
    .unwrap();
    fs.write("/configs/c.txt".into(), r#"123"#.as_bytes())
      .await
      .unwrap();
    fs.write("/a.js".into(), r#"require("./b")"#.as_bytes())
      .await
      .unwrap();
    fs.write("/b.js".into(), r#"require("./c"); console.log("#.as_bytes())
      .await
      .unwrap();
    fs.write("/c.js".into(), r#"console.log('c')"#.as_bytes())
      .await
      .unwrap();
    fs.write("/index.js".into(), r#"import "./a""#.as_bytes())
      .await
      .unwrap();

    let options = vec![PathBuf::from("/index.js"), PathBuf::from("/configs")];
    let mut storage = MemoryStorage::default();
    let codec = Arc::new(CacheCodec::new(None));
    let snapshot = Arc::new(Snapshot::new(SnapshotOptions::default(), fs.clone(), codec));

    let mut build_deps = BuildDeps::new(&options, fs.clone(), snapshot.clone());

    let warnings = build_deps.add(&mut storage, vec![].into_iter()).await;
    assert_eq!(warnings.len(), 1);
    let data = storage.load(scope).await.expect("should load success");
    assert_eq!(data.len(), 9);

    let mut build_deps = BuildDeps::new(&options, fs.clone(), snapshot.clone());

    fs.write("/b.js".into(), r#"require("./c")"#.as_bytes())
      .await
      .unwrap();
    let validate_result = build_deps
      .validate(&storage)
      .await
      .expect("should validate success");
    assert!(!validate_result);
    storage.reset(scope);

    let data = storage.load(scope).await.expect("should load success");
    assert_eq!(data.len(), 0);
    let warnings = build_deps.add(&mut storage, vec![].into_iter()).await;
    assert_eq!(warnings.len(), 0);
    let data = storage.load(scope).await.expect("should load success");
    assert_eq!(data.len(), 10);
  }
}