diff --git a/crates/node_binding/napi-binding.d.ts b/crates/node_binding/napi-binding.d.ts index 9b0f1845f558..c64d60d5254e 100644 --- a/crates/node_binding/napi-binding.d.ts +++ b/crates/node_binding/napi-binding.d.ts @@ -3,8 +3,8 @@ /* -- banner.d.ts -- */ export type JsFilename = - | string - | ((pathData: JsPathData, assetInfo?: AssetInfo) => string); + | string + | ((pathData: JsPathData, assetInfo?: AssetInfo) => string); export type RawLazyCompilationTest = RegExp | ((module: Module) => boolean); @@ -26,45 +26,45 @@ export const COMMIT_CUSTOM_FIELDS_SYMBOL: unique symbol; export const RUST_ERROR_SYMBOL: unique symbol; interface KnownBuildInfo { - [BUILD_INFO_ASSETS_SYMBOL]: Assets, - [BUILD_INFO_FILE_DEPENDENCIES_SYMBOL]: string[], - [BUILD_INFO_CONTEXT_DEPENDENCIES_SYMBOL]: string[], - [BUILD_INFO_MISSING_DEPENDENCIES_SYMBOL]: string[], - [BUILD_INFO_BUILD_DEPENDENCIES_SYMBOL]: string[], - [COMMIT_CUSTOM_FIELDS_SYMBOL](): void; + [BUILD_INFO_ASSETS_SYMBOL]: Assets, + [BUILD_INFO_FILE_DEPENDENCIES_SYMBOL]: string[], + [BUILD_INFO_CONTEXT_DEPENDENCIES_SYMBOL]: string[], + [BUILD_INFO_MISSING_DEPENDENCIES_SYMBOL]: string[], + [BUILD_INFO_BUILD_DEPENDENCIES_SYMBOL]: string[], + [COMMIT_CUSTOM_FIELDS_SYMBOL](): void; } export type BuildInfo = KnownBuildInfo & Record; export interface Module { - [MODULE_IDENTIFIER_SYMBOL]: string; - readonly type: string; - get context(): string | undefined; - get layer(): string | undefined; - get factoryMeta(): JsFactoryMeta - set factoryMeta(factoryMeta: JsFactoryMeta); - get useSourceMap(): boolean; - get useSimpleSourceMap(): boolean; - buildInfo: BuildInfo; - buildMeta: Record; + [MODULE_IDENTIFIER_SYMBOL]: string; + readonly type: string; + get context(): string | undefined; + get layer(): string | undefined; + get factoryMeta(): JsFactoryMeta + set factoryMeta(factoryMeta: JsFactoryMeta); + get useSourceMap(): boolean; + get useSimpleSourceMap(): boolean; + buildInfo: BuildInfo; + buildMeta: Record; } interface NormalModuleConstructor { - new(): NormalModule; - readonly prototype: NormalModule; + new(): NormalModule; + readonly prototype: NormalModule; } export var NormalModule: NormalModuleConstructor; export interface NormalModule extends Module { - readonly resource: string; - readonly request: string; - readonly userRequest: string; - readonly rawRequest: string; - readonly resourceResolveData: Readonly | undefined; - readonly loaders: JsLoaderItem[]; - get matchResource(): string | undefined; - set matchResource(val: string | undefined); + readonly resource: string; + readonly request: string; + readonly userRequest: string; + readonly rawRequest: string; + readonly resourceResolveData: Readonly | undefined; + readonly loaders: JsLoaderItem[]; + get matchResource(): string | undefined; + set matchResource(val: string | undefined); } export interface ConcatenatedModule extends Module { @@ -74,19 +74,19 @@ export interface ContextModule extends Module { } export interface ExternalModule extends Module { - readonly userRequest: string; + readonly userRequest: string; } export interface RspackError extends Error { - name: string; - message: string; - details?: string; - module?: null | Module; - loc?: DependencyLocation; - file?: string; - stack?: string; - hideStack?: boolean; - error?: Error; + name: string; + message: string; + details?: string; + module?: null | Module; + loc?: DependencyLocation; + file?: string; + stack?: string; + hideStack?: boolean; + error?: Error; } export type DependencyLocation = SyntheticDependencyLocation | RealDependencyLocation; @@ -386,7 +386,7 @@ export declare class JsExportsInfo { isUsed(runtime: string | string[] | undefined): boolean isModuleUsed(runtime: string | string[] | undefined): boolean setUsedInUnknownWay(runtime: string | string[] | undefined): boolean - getUsed(name: string | string[], runtime: string | string[] | undefined): 0 | 1 | 2 | 3 | 4 + getUsed(name: string | string[], runtime: string | string[] | undefined): 0 | 1 | 2 | 3 | 4 } export declare class JsModuleGraph { @@ -443,7 +443,7 @@ export declare class ModuleGraphConnection { export declare class NativeWatcher { constructor(options: NativeWatcherOptions) - watch(files: [Array, Array], directories: [Array, Array], missing: [Array, Array], callback: (err: Error | null, result: NativeWatchResult) => void, callbackUndelayed: (path: string) => void): void + watch(files: [Array, Array], directories: [Array, Array], missing: [Array, Array], callback: (err: Error | null, result: NativeWatchResult) => void, callbackUndelayed: (type: 'change' | 'remove', path: string, mtime?: number) => void): void triggerEvent(kind: 'change' | 'remove' | 'create', path: string): void /** * # Safety @@ -1969,7 +1969,7 @@ export interface RawCopyPattern { * Allows to modify the file contents. * @default undefined */ - transform?: { transformer: (input: Buffer, absoluteFilename: string) => string | Buffer | Promise | Promise } | ((input: Buffer, absoluteFilename: string) => string | Buffer | Promise | Promise) + transform?: { transformer: (input: Buffer, absoluteFilename: string) => string | Buffer | Promise | Promise } | ((input: Buffer, absoluteFilename: string) => string | Buffer | Promise | Promise) } export interface RawCopyRspackPluginOptions { @@ -2106,22 +2106,22 @@ export interface RawExperimentCacheOptionsPersistent { export interface RawExperiments { layers: boolean topLevelAwait: boolean -incremental?: false | { [key: string]: boolean } -parallelCodeSplitting: boolean -rspackFuture?: RawRspackFuture -cache: boolean | { type: "persistent" } & RawExperimentCacheOptionsPersistent | { type: "memory" } -useInputFileSystem?: false | Array -css?: boolean -inlineConst: boolean -inlineEnum: boolean -typeReexportsPresence: boolean -lazyBarrel: boolean + incremental?: false | { [key: string]: boolean } + parallelCodeSplitting: boolean + rspackFuture?: RawRspackFuture + cache: boolean | { type: "persistent" } & RawExperimentCacheOptionsPersistent | { type: "memory" } + useInputFileSystem?: false | Array + css?: boolean + inlineConst: boolean + inlineEnum: boolean + typeReexportsPresence: boolean + lazyBarrel: boolean } export interface RawExperimentSnapshotOptions { - immutablePaths: Array - unmanagedPaths: Array - managedPaths: Array + immutablePaths: Array + unmanagedPaths: Array + managedPaths: Array } export interface RawExposeOptions { @@ -2349,7 +2349,7 @@ export interface RawJsonParserOptions { } export interface RawLazyCompilationOption { - currentActiveModules: ((err: Error | null, ) => Set) + currentActiveModules: ((err: Error | null,) => Set) test?: RawLazyCompilationTest entries: boolean imports: boolean @@ -2857,7 +2857,7 @@ export interface RealDependencyLocation { * Author Donny/강동윤 * Copyright (c) */ -export declare function registerGlobalTrace(filter: string, layer: "logger" | "perfetto" , output: string): void +export declare function registerGlobalTrace(filter: string, layer: "logger" | "perfetto", output: string): void export declare enum RegisterJsTapKind { CompilerThisCompilation = 0, diff --git a/crates/rspack_binding_api/src/native_watcher.rs b/crates/rspack_binding_api/src/native_watcher.rs index a1716a6d6d83..f97dbd111906 100644 --- a/crates/rspack_binding_api/src/native_watcher.rs +++ b/crates/rspack_binding_api/src/native_watcher.rs @@ -1,9 +1,11 @@ use std::{ + alloc::System, boxed::Box, path::{Path, PathBuf}, + time::SystemTime, }; -use napi::bindgen_prelude::*; +use napi::{JsUndefined, bindgen_prelude::*}; use napi_derive::*; use rspack_fs::{FsEventKind, FsWatcher, FsWatcherIgnored, FsWatcherOptions}; use rspack_paths::ArcPath; @@ -78,7 +80,8 @@ impl NativeWatcher { missing: (Vec, Vec), #[napi(ts_arg_type = "(err: Error | null, result: NativeWatchResult) => void")] callback: Function<'static>, - #[napi(ts_arg_type = "(path: string) => void")] callback_undelayed: Function<'static>, + #[napi(ts_arg_type = "(type: 'change' | 'remove', path: string, mtime?: number) => void")] + callback_undelayed: Function<'static>, env: Env, ) -> napi::Result<()> { if self.closed { @@ -216,9 +219,9 @@ impl rspack_fs::EventAggregateHandler for JsEventHandler { struct JsEventHandlerUndelayed { inner: napi::threadsafe_function::ThreadsafeFunction< - String, + (String, String, Option), napi::Unknown<'static>, - String, + (String, String, Option), Status, false, false, @@ -229,7 +232,7 @@ struct JsEventHandlerUndelayed { impl JsEventHandlerUndelayed { fn new(callback: Function<'static>) -> napi::Result { let callback = callback - .build_threadsafe_function::() + .build_threadsafe_function::<(String, String, Option)>() .weak::() .max_queue_size::<1>() .build_callback( @@ -241,9 +244,20 @@ impl JsEventHandlerUndelayed { } impl rspack_fs::EventHandler for JsEventHandlerUndelayed { - fn on_change(&self, changed_file: String) -> rspack_error::Result<()> { + fn on_change(&self, changed_file: String, mtime: Option) -> rspack_error::Result<()> { + let mtime = mtime + .and_then(|m| m.duration_since(SystemTime::UNIX_EPOCH).ok()) + .map(|d| d.as_millis() as u64); self.inner.call( - changed_file, + ("change".to_string(), changed_file, mtime).into(), + napi::threadsafe_function::ThreadsafeFunctionCallMode::NonBlocking, + ); + Ok(()) + } + + fn on_remove(&self, _removed_file: String) -> rspack_error::Result<()> { + self.inner.call( + ("remove".to_string(), _removed_file, None).into(), napi::threadsafe_function::ThreadsafeFunctionCallMode::NonBlocking, ); Ok(()) diff --git a/crates/rspack_fs/src/watcher/executor.rs b/crates/rspack_fs/src/watcher/executor.rs index 1c4fd23c93da..fa0f8979bd69 100644 --- a/crates/rspack_fs/src/watcher/executor.rs +++ b/crates/rspack_fs/src/watcher/executor.rs @@ -206,12 +206,12 @@ fn create_execute_task( let path = event.path.to_string_lossy().to_string(); match event.kind { super::FsEventKind::Change | super::FsEventKind::Create => { - if event_handler.on_change(path).is_err() { + if event_handler.on_change(path, None).is_err() { break; } } super::FsEventKind::Remove => { - if event_handler.on_delete(path).is_err() { + if event_handler.on_remove(path).is_err() { break; } } diff --git a/crates/rspack_fs/src/watcher/mod.rs b/crates/rspack_fs/src/watcher/mod.rs index 2c87eccdc910..5584d17ae82d 100644 --- a/crates/rspack_fs/src/watcher/mod.rs +++ b/crates/rspack_fs/src/watcher/mod.rs @@ -6,7 +6,7 @@ mod paths; mod scanner; mod trigger; -use std::sync::Arc; +use std::{sync::Arc, time::SystemTime}; use analyzer::{Analyzer, RecommendedAnalyzer}; use disk_watcher::DiskWatcher; @@ -61,12 +61,16 @@ pub trait EventAggregateHandler { /// It provides methods to handle changes and deletions of files. pub trait EventHandler { /// Handle a change in a file. - fn on_change(&self, _changed_file: String) -> rspack_error::Result<()> { + fn on_change( + &self, + _changed_file: String, + _mtime: Option, + ) -> rspack_error::Result<()> { Ok(()) } /// Handle a deletion of a file. - fn on_delete(&self, _deleted_file: String) -> rspack_error::Result<()> { + fn on_remove(&self, _removed_file: String) -> rspack_error::Result<()> { Ok(()) } } diff --git a/packages/rspack/etc/core.api.md b/packages/rspack/etc/core.api.md index ba69c7266d0c..239400d1deb9 100644 --- a/packages/rspack/etc/core.api.md +++ b/packages/rspack/etc/core.api.md @@ -8986,6 +8986,18 @@ type WatchFiles = { // @public (undocumented) interface WatchFileSystem { + // (undocumented) + emit?(event: 'change', filepath: string, mtime: number): boolean; + // (undocumented) + emit?(event: 'remove', filepath: string): boolean; + // (undocumented) + on?(event: 'change', listener: (filepath: string, mtime: number) => void): this; + // (undocumented) + on?(event: 'remove', listener: (filepath: string) => void): this; + // (undocumented) + once?(event: 'change', listener: (filepath: string, mtime: number) => void): this; + // (undocumented) + once?(event: 'remove', listener: (filepath: string) => void): this; // (undocumented) watch(files: Iterable & { added?: Iterable; diff --git a/packages/rspack/src/NativeWatchFileSystem.ts b/packages/rspack/src/NativeWatchFileSystem.ts index 538d4a41e02c..c7745059e692 100644 --- a/packages/rspack/src/NativeWatchFileSystem.ts +++ b/packages/rspack/src/NativeWatchFileSystem.ts @@ -1,4 +1,5 @@ import binding from "@rspack/binding"; +import { EventEmitter } from "stream"; import type Watchpack from "watchpack"; import type { FileSystemInfoEntry, @@ -40,6 +41,7 @@ export default class NativeWatchFileSystem implements WatchFileSystem { #inner: binding.NativeWatcher | undefined; #isFirstWatch = true; #inputFileSystem: InputFileSystem; + #emitter = new EventEmitter(); constructor(inputFileSystem: InputFileSystem) { this.#inputFileSystem = inputFileSystem; @@ -128,9 +130,19 @@ export default class NativeWatchFileSystem implements WatchFileSystem { new Set(removedFiles) ); }, - (fileName: string) => { - // TODO: add real change time - callbackUndelayed(fileName, Date.now()); + (type: "change" | "remove", fileName: string, mtime?: number) => { + // FIXME: napi-rs will pass all arguments as array + // @ts-ignore + console.log("NativeWatchFileSystem event", type, fileName, mtime); + if (type === "change") { + const modifiedTime = mtime || Date.now(); + callbackUndelayed(fileName, modifiedTime); + this.#emitter.emit("change", fileName, modifiedTime); + } + + if (type === "remove") { + this.#emitter.emit("remove", fileName); + } } ); @@ -205,4 +217,47 @@ export default class NativeWatchFileSystem implements WatchFileSystem { ]; } } + + once( + event: "change", + listener: (filepath: string, mtime: number) => void + ): this; + once(event: "remove", listener: (filepath: string) => void): this; + once( + event: "change" | "remove", + listener: + | ((filepath: string, mtime: number) => void) + | ((filepath: string) => void) + ): this { + this.#emitter.once(event, listener); + return this; + } + on( + event: "change", + listener: (filepath: string, mtime: number) => void + ): this; + on(event: "remove", listener: (filepath: string) => void): this; + on( + event: "change" | "remove", + listener: + | ((filepath: string, mtime: number) => void) + | ((filepath: string) => void) + ): this { + this.#emitter.on(event, listener); + return this; + } + + emit(event: "change", filename: string, mtime: number): boolean; + emit(event: "remove", filename: string): boolean; + emit(event: "change" | "remove", filename: string, _mtime?: number): boolean { + if (event === "change") { + return this.#inner?.triggerEvent("change", filename) ?? false; + } + + if (event === "remove") { + return this.#inner?.triggerEvent("remove", filename) ?? false; + } + + return false; + } } diff --git a/packages/rspack/src/node/NodeWatchFileSystem.ts b/packages/rspack/src/node/NodeWatchFileSystem.ts index 1260df933423..21ad15d2427b 100644 --- a/packages/rspack/src/node/NodeWatchFileSystem.ts +++ b/packages/rspack/src/node/NodeWatchFileSystem.ts @@ -8,9 +8,9 @@ * https://github.com/webpack/webpack/blob/main/LICENSE */ +import type EventEmitter from "node:events"; import util from "node:util"; import type Watchpack from "watchpack"; - import type { FileSystemInfoEntry, InputFileSystem, @@ -18,6 +18,12 @@ import type { WatchFileSystem } from "../util/fs"; +interface WatchpackWatcher extends EventEmitter { + path: string; +} + +type WatchpackWatchers = Map; + export default class NodeWatchFileSystem implements WatchFileSystem { inputFileSystem: InputFileSystem; watcherOptions: Watchpack.WatchOptions; @@ -193,4 +199,80 @@ export default class NodeWatchFileSystem implements WatchFileSystem { } }; } + + once( + event: "change", + listener: (filepath: string, mtime: number) => void + ): this; + once(event: "remove", listener: (filepath: string) => void): this; + once( + event: "change" | "remove", + listener: + | ((filepath: string, mtime: number) => void) + | ((filepath: string) => void) + ): this { + if (event === "change") { + this.watcher?.once( + event, + (filepath: string, modifiedTime: number, _explanation: string) => { + listener(filepath, modifiedTime); + } + ); + } else { + this.watcher?.once(event, (filepath: string, _explanation: string) => { + (listener as (filepath: string) => void)(filepath); + }); + } + return this; + } + on( + event: "change", + listener: (filepath: string, mtime: number) => void + ): this; + on(event: "remove", listener: (filepath: string) => void): this; + on( + event: "change" | "remove", + listener: + | ((filepath: string, mtime: number) => void) + | ((filepath: string) => void) + ): this { + if (event === "change") { + this.watcher?.on( + event, + (filepath: string, modifiedTime: number, _explanation: string) => { + listener(filepath, modifiedTime); + } + ); + } else { + this.watcher?.on(event, (filepath: string, _explanation: string) => { + (listener as (filepath: string) => void)(filepath); + }); + } + return this; + } + + emit(event: "change", filename: string, mtime: number): boolean; + emit(event: "remove", filename: string): boolean; + emit(event: "change" | "remove", filename: string, mtime?: number): boolean { + const fileWatchers = this.watcher?.fileWatchers as unknown as + | WatchpackWatchers + | undefined; + const dirWatchers = this.watcher?.dirWatchers as unknown as + | WatchpackWatchers + | undefined; + + const fileWatcher = fileWatchers?.get(filename); + let r1 = false; + if (fileWatcher) { + r1 = fileWatcher.watcher.emit(event, filename, mtime); + } + const dirWatcher = dirWatchers?.get(filename); + + let r2 = false; + if (dirWatcher) { + r2 = dirWatcher.watcher.emit(event, filename, mtime); + } + + return r1 || r2; + } } diff --git a/packages/rspack/src/util/fs.ts b/packages/rspack/src/util/fs.ts index ead88e0954f2..552fe1618676 100644 --- a/packages/rspack/src/util/fs.ts +++ b/packages/rspack/src/util/fs.ts @@ -720,4 +720,16 @@ export interface WatchFileSystem { ) => void, callbackUndelayed: (fileName: string, changeTime: number) => void ): Watcher; + once?( + event: "change", + listener: (filepath: string, mtime: number) => void + ): this; + once?(event: "remove", listener: (filepath: string) => void): this; + on?( + event: "change", + listener: (filepath: string, mtime: number) => void + ): this; + on?(event: "remove", listener: (filepath: string) => void): this; + emit?(event: "change", filepath: string, mtime: number): boolean; + emit?(event: "remove", filepath: string): boolean; }