rspack_resolver 0.7.0

ESM / CJS module resolution
Documentation
use std::{fs, io, path::Path};

use crate::{ResolveOptions, Resolver};

#[derive(Debug, Clone, Copy)]
enum FileType {
  File,
  Dir,
}

#[allow(unused_variables)]
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(
  original: P,
  link: Q,
  file_type: FileType,
) -> io::Result<()> {
  #[cfg(target_family = "unix")]
  {
    std::os::unix::fs::symlink(original, link)
  }

  #[cfg(target_family = "windows")]
  match file_type {
    FileType::File => std::os::windows::fs::symlink_file(original, link),
    FileType::Dir => std::os::windows::fs::symlink_dir(original, link),
  }
}

fn init(dirname: &Path, temp_path: &Path) -> io::Result<()> {
  if temp_path.exists() {
    _ = fs::remove_dir_all(temp_path);
  }
  fs::create_dir(temp_path)?;
  symlink(
    dirname.join("../lib/index.js"),
    temp_path.join("test"),
    FileType::File,
  )?;
  symlink(
    dirname.join("../lib"),
    temp_path.join("test2"),
    FileType::Dir,
  )?;
  fs::remove_file(temp_path.join("test"))?;
  fs::remove_file(temp_path.join("test2"))?;
  fs::remove_dir(temp_path)
}

fn create_symlinks(dirname: &Path, temp_path: &Path) -> io::Result<()> {
  fs::create_dir(temp_path).unwrap();
  symlink(
    dirname.join("../lib/index.js").canonicalize().unwrap(),
    temp_path.join("index.js"),
    FileType::File,
  )?;
  symlink(
    dirname.join("../lib").canonicalize().unwrap(),
    temp_path.join("lib"),
    FileType::Dir,
  )?;
  symlink(
    dirname.join("..").canonicalize().unwrap(),
    temp_path.join("this"),
    FileType::Dir,
  )?;
  symlink(
    temp_path.join("this"),
    temp_path.join("that"),
    FileType::Dir,
  )?;
  symlink(
    Path::new("../../lib/index.js"),
    temp_path.join("node.relative.js"),
    FileType::File,
  )?;
  symlink(
    Path::new("./node.relative.js"),
    temp_path.join("node.relative.sym.js"),
    FileType::File,
  )?;
  Ok(())
}

fn cleanup_symlinks(temp_path: &Path) {
  _ = fs::remove_dir_all(temp_path);
}

#[tokio::test]
async fn test() -> io::Result<()> {
  let root = super::fixture_root().join("enhanced_resolve");
  let dirname = root.join("test");
  let temp_path = dirname.join("temp");
  if !temp_path.exists() {
    let is_admin = init(&dirname, &temp_path).is_ok();
    if !is_admin {
      return Ok(());
    }
    if let Err(err) = create_symlinks(&dirname, &temp_path) {
      cleanup_symlinks(&temp_path);
      return Err(err);
    }
  }

  let resolver_without_symlinks = Resolver::new(ResolveOptions {
    symlinks: false,
    ..ResolveOptions::default()
  });
  let resolver_with_symlinks = Resolver::default();

  #[rustfmt::skip]
    let pass = [
        ("with a symlink to a file", temp_path.clone(), "./index.js"),
        ("with a relative symlink to a file", temp_path.clone(), "./node.relative.js"),
        ("with a relative symlink to a symlink to a file", temp_path.clone(), "./node.relative.sym.js"),
        ("with a symlink to a directory 1", temp_path.clone(), "./lib/index.js"),
        ("with a symlink to a directory 2", temp_path.clone(), "./this/lib/index.js"),
        ("with multiple symlinks in the path 1", temp_path.clone(), "./this/test/temp/index.js"),
        ("with multiple symlinks in the path 2", temp_path.clone(), "./this/test/temp/lib/index.js"),
        ("with multiple symlinks in the path 3", temp_path.clone(), "./this/test/temp/this/lib/index.js"),
        ("with a symlink to a directory 2 (chained)", temp_path.clone(), "./that/lib/index.js"),
        ("with multiple symlinks in the path 1 (chained)", temp_path.clone(), "./that/test/temp/index.js"),
        ("with multiple symlinks in the path 2 (chained)", temp_path.clone(), "./that/test/temp/lib/index.js"),
        ("with multiple symlinks in the path 3 (chained)", temp_path.clone(), "./that/test/temp/that/lib/index.js"),
        ("with symlinked directory as context 1", temp_path.join( "lib"), "./index.js"),
        ("with symlinked directory as context 2", temp_path.join( "this"), "./lib/index.js"),
        ("with symlinked directory as context and in path", temp_path.join( "this"), "./test/temp/lib/index.js"),
        ("with symlinked directory in context path", temp_path.join( "this/lib"), "./index.js"),
        ("with symlinked directory in context path and symlinked file", temp_path.join( "this/test"), "./temp/index.js"),
        ("with symlinked directory in context path and symlinked directory", temp_path.join( "this/test"), "./temp/lib/index.js"),
        ("with symlinked directory as context 2 (chained)", temp_path.join( "that"), "./lib/index.js"),
        ("with symlinked directory as context and in path (chained)", temp_path.join( "that"), "./test/temp/lib/index.js"),
        ("with symlinked directory in context path (chained)", temp_path.join( "that/lib"), "./index.js"),
        ("with symlinked directory in context path and symlinked file (chained)", temp_path.join( "that/test"), "./temp/index.js"),
        ("with symlinked directory in context path and symlinked directory (chained)", temp_path.join( "that/test"), "./temp/lib/index.js")
    ];

  for (comment, path, request) in pass {
    let filename = resolver_with_symlinks
      .resolve(&path, request)
      .await
      .map(|r| r.full_path());
    assert_eq!(filename, Ok(root.join("lib/index.js")), "{comment:?}");

    let mut ctx = &mut Default::default();
    let resolved_path = resolver_without_symlinks
      .resolve_with_context(&path, request, &mut ctx)
      .await
      .map(|r| r.full_path());
    assert_eq!(resolved_path, Ok(path.join(request)));
    assert!(
      ctx.file_dependencies.contains(&resolved_path.unwrap()),
      "file dependencies should contain resolved path {comment:?}"
    );
  }

  Ok(())
}