diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts index d60ecce2e53..916c1d9f332 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -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 @@ -2017,5 +2027,9 @@ export interface ThreadsafeNodeFS { mkdir: (name: string) => Promise | void mkdirp: (name: string) => Promise | string | void removeDirAll: (name: string) => Promise | string | void + readDir: (name: string) => Promise | string[] | void + readFile: (name: string) => Promise | Buffer | string | void + stat: (name: string) => Promise | NodeFsStats | void + lstat: (name: string) => Promise | NodeFsStats | void } diff --git a/crates/rspack_core/src/compiler/mod.rs b/crates/rspack_core/src/compiler/mod.rs index f78f9f44b32..c803305470a 100644 --- a/crates/rspack_core/src/compiler/mod.rs +++ b/crates/rspack_core/src/compiler/mod.rs @@ -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, }; @@ -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 { diff --git a/crates/rspack_fs/src/async.rs b/crates/rspack_fs/src/async.rs index 042ef38192d..a0a38b0703a 100644 --- a/crates/rspack_fs/src/async.rs +++ b/crates/rspack_fs/src/async.rs @@ -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. /// @@ -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>>; + + /// Read the entire contents of a file into a bytes vector. + fn read_file<'a>(&'a self, file: &'a Utf8Path) -> BoxFuture<'a, Result>>; + + fn stat<'a>(&'a self, file: &'a Utf8Path) -> BoxFuture<'a, Result>; } pub trait AsyncReadableFileSystem: Debug { diff --git a/crates/rspack_fs/src/lib.rs b/crates/rspack_fs/src/lib.rs index 36b3555138b..0e0ee5e2efd 100644 --- a/crates/rspack_fs/src/lib.rs +++ b/crates/rspack_fs/src/lib.rs @@ -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; diff --git a/crates/rspack_fs/src/native.rs b/crates/rspack_fs/src/native.rs index 518434b8552..05903afed8e 100644 --- a/crates/rspack_fs/src/native.rs +++ b/crates/rspack_fs/src/native.rs @@ -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; @@ -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>> { + 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>> { + 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> { + 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 { @@ -88,3 +114,36 @@ impl AsyncReadableFileSystem for AsyncNativeFileSystem { Box::pin(fut) } } + +impl TryFrom for FileStat { + fn try_from(metadata: Metadata) -> Result { + 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; +} diff --git a/crates/rspack_fs_node/src/async.rs b/crates/rspack_fs_node/src/async.rs index 7a61d99ae9d..18c51d7689a 100644 --- a/crates/rspack_fs_node/src/async.rs +++ b/crates/rspack_fs_node/src/async.rs @@ -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; @@ -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>> { + 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>> { + 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> { + 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) + } } diff --git a/crates/rspack_fs_node/src/node.rs b/crates/rspack_fs_node/src/node.rs index 7864331736d..c2187cc35aa 100644 --- a/crates/rspack_fs_node/src/node.rs +++ b/crates/rspack_fs_node/src/node.rs @@ -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 { @@ -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")] @@ -69,4 +73,36 @@ pub struct ThreadsafeNodeFS { pub mkdirp: ThreadsafeFunction>, #[napi(ts_type = "(name: string) => Promise | string | void")] pub remove_dir_all: ThreadsafeFunction>, + #[napi(ts_type = "(name: string) => Promise | string[] | void")] + pub read_dir: ThreadsafeFunction, ()>>, + #[napi(ts_type = "(name: string) => Promise | Buffer | string | void")] + pub read_file: ThreadsafeFunction>, + #[napi(ts_type = "(name: string) => Promise | NodeFsStats | void")] + pub stat: ThreadsafeFunction>, + #[napi(ts_type = "(name: string) => Promise | NodeFsStats | void")] + pub lstat: ThreadsafeFunction>, +} + +#[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 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, + } + } } diff --git a/packages/rspack/src/FileSystem.ts b/packages/rspack/src/FileSystem.ts index fbabc163e9a..e7a1eb811c5 100644 --- a/packages/rspack/src/FileSystem.ts +++ b/packages/rspack/src/FileSystem.ts @@ -1,7 +1,7 @@ import util from "node:util"; -import type { ThreadsafeNodeFS } from "@rspack/binding"; +import type { NodeFsStats, ThreadsafeNodeFS } from "@rspack/binding"; -import { type OutputFileSystem, mkdirp, rmrf } from "./util/fs"; +import { type IStats, type OutputFileSystem, mkdirp, rmrf } from "./util/fs"; import { memoizeFn } from "./util/memoize"; const NOOP_FILESYSTEM: ThreadsafeNodeFS = { @@ -9,7 +9,11 @@ const NOOP_FILESYSTEM: ThreadsafeNodeFS = { removeFile() {}, mkdir() {}, mkdirp() {}, - removeDirAll() {} + removeDirAll() {}, + readDir: () => {}, + readFile: () => {}, + stat: () => {}, + lstat: () => {} }; class ThreadsafeWritableNodeFS implements ThreadsafeNodeFS { @@ -18,6 +22,12 @@ class ThreadsafeWritableNodeFS implements ThreadsafeNodeFS { mkdir!: (name: string) => Promise | void; mkdirp!: (name: string) => Promise | string | void; removeDirAll!: (name: string) => Promise | string | void; + readDir!: (name: string) => Promise | string[] | void; + readFile!: ( + name: string + ) => Promise | Buffer | string | void; + stat!: (name: string) => Promise | NodeFsStats | void; + lstat!: (name: string) => Promise | NodeFsStats | void; constructor(fs?: OutputFileSystem) { if (!fs) { @@ -30,11 +40,55 @@ class ThreadsafeWritableNodeFS implements ThreadsafeNodeFS { this.mkdir = memoizeFn(() => util.promisify(fs.mkdir.bind(fs))); this.mkdirp = memoizeFn(() => util.promisify(mkdirp.bind(null, fs))); this.removeDirAll = memoizeFn(() => util.promisify(rmrf.bind(null, fs))); + this.readDir = memoizeFn(() => { + const readDirFn = util.promisify(fs.readdir.bind(fs)); + return async (filePath: string) => { + const res = await readDirFn(filePath); + return res as string[]; + }; + }); + this.readFile = memoizeFn(() => util.promisify(fs.readFile.bind(fs))); + this.stat = memoizeFn(() => { + const statFn = util.promisify(fs.stat.bind(fs)); + return async (filePath: string) => { + const res = await statFn(filePath); + return ( + res && { + isFile: res.isFile(), + isDirectory: res.isDirectory(), + atimeMs: res.atimeMs, + mtimeMs: res.atimeMs, + ctimeMs: res.atimeMs, + birthtimeMs: res.birthtimeMs, + size: res.size + } + ); + }; + }); + this.lstat = memoizeFn(() => { + const statFn = util.promisify((fs.lstat || fs.stat).bind(fs)); + return async (filePath: string) => { + const res = await statFn(filePath); + return res && ThreadsafeWritableNodeFS.__to_binding_stat(res); + }; + }); } static __to_binding(fs?: OutputFileSystem) { return new this(fs); } + + static __to_binding_stat(stat: IStats): NodeFsStats { + return { + isFile: stat.isFile(), + isDirectory: stat.isDirectory(), + atimeMs: stat.atimeMs, + mtimeMs: stat.atimeMs, + ctimeMs: stat.atimeMs, + birthtimeMs: stat.birthtimeMs, + size: stat.size + }; + } } export { ThreadsafeWritableNodeFS };