Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add read API to output file system #8274

Merged
merged 2 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/node_binding/binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,16 @@ export interface NodeFS {
mkdirp: (...args: any[]) => any
}

export interface NodeFsStats {
isFile: boolean
isDirectory: boolean
atimeMs: number
mtimeMs: number
ctimeMs: number
birthtimeMs: number
size: number
}

export interface PathWithInfo {
path: string
info: JsAssetInfo
Expand Down Expand Up @@ -2017,5 +2027,9 @@ export interface ThreadsafeNodeFS {
mkdir: (name: string) => Promise<void> | void
mkdirp: (name: string) => Promise<string | void> | string | void
removeDirAll: (name: string) => Promise<string | void> | string | void
readDir: (name: string) => Promise<string[] | void> | string[] | void
readFile: (name: string) => Promise<Buffer | string | void> | Buffer | string | void
stat: (name: string) => Promise<NodeFsStats | void> | NodeFsStats | void
lstat: (name: string) => Promise<NodeFsStats | void> | NodeFsStats | void
}

45 changes: 25 additions & 20 deletions crates/rspack_core/src/compiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod make;
mod module_executor;
use std::sync::Arc;

use rspack_error::{error, Result};
use rspack_error::Result;
use rspack_fs::{
AsyncNativeFileSystem, AsyncWritableFileSystem, NativeFileSystem, ReadableFileSystem,
};
Expand Down Expand Up @@ -362,33 +362,38 @@ impl Compiler {
|| include_hash(filename, &asset.info.full_hash));
}

let stat = match self
.output_filesystem
.stat(file_path.as_path().as_ref())
.await
{
Ok(stat) => Some(stat),
Err(_) => None,
};

let need_write = if !self.options.output.compare_before_emit {
// write when compare_before_emit is false
true
} else if !file_path.exists() {
// write when file not exist
} else if !stat.as_ref().is_some_and(|stat| stat.is_file) {
// write when not exists or not a file
true
} else if immutable {
// do not write when asset is immutable and the file exists
false
} else {
// TODO: webpack use outputFileSystem to get metadata and file content
// should also use outputFileSystem after aligning with webpack
let metadata = self
.input_filesystem
.metadata(file_path.as_path().as_ref())
.map_err(|e| error!("failed to read metadata: {e}"))?;
if (content.len() as u64) == metadata.len() {
match self.input_filesystem.read(file_path.as_path().as_ref()) {
// write when content is different
Ok(c) => content != c,
// write when file can not be read
Err(_) => true,
}
} else {
// write if content length is different
true
} else if (content.len() as u64) == stat.as_ref().unwrap_or_else(|| unreachable!()).size {
match self
.output_filesystem
.read_file(file_path.as_path().as_ref())
.await
{
// write when content is different
Ok(c) => content != c,
// write when file can not be read
Err(_) => true,
}
} else {
// write if content length is different
true
};

if need_write {
Expand Down
18 changes: 18 additions & 0 deletions crates/rspack_fs/src/async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ use rspack_paths::Utf8Path;

use crate::Result;

#[derive(Debug)]
pub struct FileStat {
pub is_file: bool,
pub is_directory: bool,
pub atime_ms: u64,
pub mtime_ms: u64,
pub ctime_ms: u64,
pub size: u64,
}

pub trait AsyncWritableFileSystem: Debug {
/// Creates a new, empty directory at the provided path.
///
Expand All @@ -30,6 +40,14 @@ pub trait AsyncWritableFileSystem: Debug {

/// Removes a directory at this path, after removing all its contents. Use carefully.
fn remove_dir_all<'a>(&'a self, dir: &'a Utf8Path) -> BoxFuture<'a, Result<()>>;

/// Returns a list of all files in a directory.
fn read_dir<'a>(&'a self, dir: &'a Utf8Path) -> BoxFuture<'a, Result<Vec<String>>>;

/// Read the entire contents of a file into a bytes vector.
fn read_file<'a>(&'a self, file: &'a Utf8Path) -> BoxFuture<'a, Result<Vec<u8>>>;

fn stat<'a>(&'a self, file: &'a Utf8Path) -> BoxFuture<'a, Result<FileStat>>;
}

pub trait AsyncReadableFileSystem: Debug {
Expand Down
2 changes: 1 addition & 1 deletion crates/rspack_fs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
pub mod r#async;
mod macros;
mod native;
pub use r#async::{AsyncFileSystem, AsyncReadableFileSystem, AsyncWritableFileSystem};
pub use r#async::{AsyncFileSystem, AsyncReadableFileSystem, AsyncWritableFileSystem, FileStat};
pub mod sync;
pub use sync::{FileSystem, ReadableFileSystem, WritableFileSystem};
mod error;
Expand Down
61 changes: 60 additions & 1 deletion crates/rspack_fs/src/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl ReadableFileSystem for NativeFileSystem {

use futures::future::BoxFuture;

use crate::{AsyncReadableFileSystem, AsyncWritableFileSystem};
use crate::{r#async::FileStat, AsyncReadableFileSystem, AsyncWritableFileSystem};

#[derive(Debug)]
pub struct AsyncNativeFileSystem;
Expand Down Expand Up @@ -80,6 +80,32 @@ impl AsyncWritableFileSystem for AsyncNativeFileSystem {
let fut = async move { tokio::fs::remove_dir_all(dir).await.map_err(Error::from) };
Box::pin(fut)
}

fn read_dir<'a>(&'a self, dir: &'a Utf8Path) -> BoxFuture<'a, Result<Vec<String>>> {
let dir = dir.to_path_buf();
let fut = async move {
let mut reader = tokio::fs::read_dir(dir).await.map_err(Error::from)?;
let mut res = vec![];
while let Some(entry) = reader.next_entry().await.map_err(Error::from)? {
res.push(entry.file_name().to_string_lossy().to_string());
}
Ok(res)
};
Box::pin(fut)
}

fn read_file<'a>(&'a self, file: &'a Utf8Path) -> BoxFuture<'a, Result<Vec<u8>>> {
let fut = async move { tokio::fs::read(file).await.map_err(Error::from) };
Box::pin(fut)
}

fn stat<'a>(&'a self, file: &'a Utf8Path) -> BoxFuture<'a, Result<crate::r#async::FileStat>> {
let fut = async move {
let metadata = tokio::fs::metadata(file).await.map_err(Error::from)?;
FileStat::try_from(metadata)
};
Box::pin(fut)
}
}

impl AsyncReadableFileSystem for AsyncNativeFileSystem {
Expand All @@ -88,3 +114,36 @@ impl AsyncReadableFileSystem for AsyncNativeFileSystem {
Box::pin(fut)
}
}

impl TryFrom<Metadata> for FileStat {
fn try_from(metadata: Metadata) -> Result<Self> {
let mtime_ms = metadata
.modified()
.map_err(Error::from)?
.duration_since(std::time::UNIX_EPOCH)
.expect("mtime is before unix epoch")
.as_millis() as u64;
let ctime_ms = metadata
.created()
.map_err(Error::from)?
.duration_since(std::time::UNIX_EPOCH)
.expect("ctime is before unix epoch")
.as_millis() as u64;
let atime_ms = metadata
.accessed()
.map_err(Error::from)?
.duration_since(std::time::UNIX_EPOCH)
.expect("atime is before unix epoch")
.as_millis() as u64;
Ok(Self {
is_directory: metadata.is_dir(),
is_file: metadata.is_file(),
size: metadata.len(),
mtime_ms,
ctime_ms,
atime_ms,
})
}

type Error = Error;
}
67 changes: 66 additions & 1 deletion crates/rspack_fs_node/src/async.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use futures::future::BoxFuture;
use rspack_fs::r#async::AsyncWritableFileSystem;
use napi::{bindgen_prelude::Either3, Either};
use rspack_fs::r#async::{AsyncWritableFileSystem, FileStat};
use rspack_paths::Utf8Path;

use crate::node::ThreadsafeNodeFS;
Expand Down Expand Up @@ -112,4 +113,68 @@ impl AsyncWritableFileSystem for AsyncNodeWritableFileSystem {
};
Box::pin(fut)
}

// TODO: support read_dir options
fn read_dir<'a>(&'a self, dir: &'a Utf8Path) -> BoxFuture<'a, rspack_fs::Result<Vec<String>>> {
let fut = async {
let dir = dir.as_str().to_string();
let res = self.0.read_dir.call(dir).await.map_err(|e| {
rspack_fs::Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
e.to_string(),
))
})?;
match res {
Either::A(files) => Ok(files),
Either::B(_) => Err(rspack_fs::Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"output file system call read dir failed",
))),
}
};
Box::pin(fut)
}

// TODO: support read_file options
fn read_file<'a>(&'a self, file: &'a Utf8Path) -> BoxFuture<'a, rspack_fs::Result<Vec<u8>>> {
let fut = async {
let file = file.as_str().to_string();
let res = self.0.read_file.call(file).await.map_err(|e| {
rspack_fs::Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
e.to_string(),
))
})?;

match res {
Either3::A(data) => Ok(data.to_vec()),
Either3::B(str) => Ok(str.into_bytes()),
Either3::C(_) => Err(rspack_fs::Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"output file system call read file failed",
))),
}
};
Box::pin(fut)
}

fn stat<'a>(&'a self, file: &'a Utf8Path) -> BoxFuture<'a, rspack_fs::Result<FileStat>> {
let fut = async {
let file = file.as_str().to_string();
let res = self.0.stat.call(file).await.map_err(|e| {
rspack_fs::Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
e.to_string(),
))
})?;
match res {
Either::A(stat) => Ok(FileStat::from(stat)),
Either::B(_) => Err(rspack_fs::Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"output file system call stat failed",
))),
}
};
Box::pin(fut)
}
}
38 changes: 37 additions & 1 deletion crates/rspack_fs_node/src/node.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use napi::{bindgen_prelude::Buffer, Env, JsFunction, Ref};
use napi::{
bindgen_prelude::{Buffer, Either3},
Env, JsFunction, Ref,
};
use napi_derive::napi;

pub(crate) struct JsFunctionRef {
Expand Down Expand Up @@ -55,6 +58,7 @@ pub(crate) struct NodeFSRef {
}

use napi::Either;
use rspack_fs::r#async::FileStat;
use rspack_napi::threadsafe_function::ThreadsafeFunction;

#[napi(object, object_to_js = false, js_name = "ThreadsafeNodeFS")]
Expand All @@ -69,4 +73,36 @@ pub struct ThreadsafeNodeFS {
pub mkdirp: ThreadsafeFunction<String, Either<String, ()>>,
#[napi(ts_type = "(name: string) => Promise<string | void> | string | void")]
pub remove_dir_all: ThreadsafeFunction<String, Either<String, ()>>,
#[napi(ts_type = "(name: string) => Promise<string[] | void> | string[] | void")]
pub read_dir: ThreadsafeFunction<String, Either<Vec<String>, ()>>,
#[napi(ts_type = "(name: string) => Promise<Buffer | string | void> | Buffer | string | void")]
pub read_file: ThreadsafeFunction<String, Either3<Buffer, String, ()>>,
#[napi(ts_type = "(name: string) => Promise<NodeFsStats | void> | NodeFsStats | void")]
pub stat: ThreadsafeFunction<String, Either<NodeFsStats, ()>>,
#[napi(ts_type = "(name: string) => Promise<NodeFsStats | void> | NodeFsStats | void")]
pub lstat: ThreadsafeFunction<String, Either<NodeFsStats, ()>>,
}

#[napi(object, object_to_js = false)]
pub struct NodeFsStats {
pub is_file: bool,
pub is_directory: bool,
pub atime_ms: u32,
pub mtime_ms: u32,
pub ctime_ms: u32,
pub birthtime_ms: u32,
pub size: u32,
}

impl From<NodeFsStats> for FileStat {
fn from(value: NodeFsStats) -> Self {
Self {
is_file: value.is_file,
is_directory: value.is_directory,
atime_ms: value.atime_ms as u64,
mtime_ms: value.mtime_ms as u64,
ctime_ms: value.ctime_ms as u64,
size: value.size as u64,
}
}
}
Loading
Loading