diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts index 5e9759b80bee..14f8c3e395ed 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -518,6 +518,17 @@ export interface JsChunkGroupOrigin { request?: string } +/** + * File clean options + * + * This matches with: + * - keep: + * - If a string, keep the files under this path + */ +export interface JsCleanOptions { + keep?: string +} + export interface JsCodegenerationResult { sources: Record } @@ -1744,7 +1755,7 @@ export interface RawOptions { export interface RawOutputOptions { path: string pathinfo: boolean | "verbose" - clean: boolean + clean: boolean | JsCleanOptions publicPath: "auto" | JsFilename assetModuleFilename: JsFilename wasmLoading: string diff --git a/crates/rspack_binding_options/src/options/raw_output.rs b/crates/rspack_binding_options/src/options/raw_output.rs index 57af3b1b0ce7..6b1f9e20eadd 100644 --- a/crates/rspack_binding_options/src/options/raw_output.rs +++ b/crates/rspack_binding_options/src/options/raw_output.rs @@ -1,8 +1,8 @@ use napi::Either; use napi_derive::napi; use rspack_binding_values::library::JsLibraryOptions; -use rspack_binding_values::JsFilename; -use rspack_core::{CrossOriginLoading, Environment, PathInfo}; +use rspack_binding_values::{JsCleanOptions, JsFilename}; +use rspack_core::{CleanOptions, CrossOriginLoading, Environment, PathInfo}; use rspack_core::{OutputOptions, TrustedTypes}; #[derive(Debug)] @@ -66,7 +66,7 @@ pub struct RawOutputOptions { pub path: String, #[napi(ts_type = "boolean | \"verbose\"")] pub pathinfo: Either, - pub clean: bool, + pub clean: Either, #[napi(ts_type = "\"auto\" | JsFilename")] pub public_path: JsFilename, pub asset_module_filename: JsFilename, @@ -120,10 +120,15 @@ impl TryFrom for OutputOptions { Either::B(s) => PathInfo::String(s), }; + let clean = match value.clean { + Either::A(b) => CleanOptions::CleanAll(b), + Either::B(cop) => cop.to_clean_options(), + }; + Ok(OutputOptions { path: value.path.into(), pathinfo, - clean: value.clean, + clean, public_path: value.public_path.into(), asset_module_filename: value.asset_module_filename.into(), wasm_loading: value.wasm_loading.as_str().into(), diff --git a/crates/rspack_binding_values/src/clean_options.rs b/crates/rspack_binding_values/src/clean_options.rs new file mode 100644 index 000000000000..05513c369031 --- /dev/null +++ b/crates/rspack_binding_values/src/clean_options.rs @@ -0,0 +1,31 @@ +use napi_derive::napi; +use rspack_core::CleanOptions; +use rspack_napi::napi; + +/// File clean options +/// +/// This matches with: +/// - keep: +/// - If a string, keep the files under this path +#[napi(object, object_to_js = false)] +#[derive(Debug)] +pub struct JsCleanOptions { + pub keep: Option, + // todo: + // - support RegExp type + // if path match the RegExp, keep the file + // - support function type + // if the fn returns true on path str, keep the file +} + +impl JsCleanOptions { + pub fn to_clean_options(&self) -> CleanOptions { + let keep = self.keep.as_ref(); + if let Some(path) = keep { + let p = path.as_str(); + CleanOptions::from(p) + } else { + CleanOptions::CleanAll(false) + } + } +} diff --git a/crates/rspack_binding_values/src/lib.rs b/crates/rspack_binding_values/src/lib.rs index 41aca9602bba..5d3c270d3661 100644 --- a/crates/rspack_binding_values/src/lib.rs +++ b/crates/rspack_binding_values/src/lib.rs @@ -4,6 +4,7 @@ mod asset_condition; mod chunk; mod chunk_graph; mod chunk_group; +mod clean_options; mod codegen_result; mod compilation; mod context_module_factory; @@ -30,6 +31,7 @@ pub use asset_condition::*; pub use chunk::*; pub use chunk_graph::*; pub use chunk_group::*; +pub use clean_options::*; pub use codegen_result::*; pub use compilation::*; pub use context_module_factory::*; diff --git a/crates/rspack_core/src/compiler/mod.rs b/crates/rspack_core/src/compiler/mod.rs index c593bee3ab35..447f69b8c4be 100644 --- a/crates/rspack_core/src/compiler/mod.rs +++ b/crates/rspack_core/src/compiler/mod.rs @@ -20,8 +20,8 @@ use crate::cache::{new_cache, Cache}; use crate::incremental::IncrementalPasses; use crate::old_cache::Cache as OldCache; use crate::{ - fast_set, include_hash, BoxPlugin, CompilerOptions, Logger, PluginDriver, ResolverFactory, - SharedPluginDriver, + fast_set, include_hash, trim_dir, BoxPlugin, CleanOptions, CompilerOptions, Logger, PluginDriver, + ResolverFactory, SharedPluginDriver, }; use crate::{ContextModuleFactory, NormalModuleFactory}; @@ -73,7 +73,7 @@ impl Compiler { options: CompilerOptions, plugins: Vec, buildtime_plugins: Vec, - output_filesystem: Option>, + output_filesystem: Option>, // only supports passing input_filesystem in rust api, no support for js api input_filesystem: Option>, // no need to pass resolve_factory in rust api @@ -264,32 +264,7 @@ impl Compiler { #[instrument(name = "emit_assets", skip_all)] pub async fn emit_assets(&mut self) -> Result<()> { - if self.options.output.clean { - if self.emitted_asset_versions.is_empty() { - self - .output_filesystem - .remove_dir_all(&self.options.output.path) - .await?; - } else { - // clean unused file - let assets = self.compilation.assets(); - let _ = self - .emitted_asset_versions - .iter() - .filter_map(|(filename, _version)| { - if !assets.contains_key(filename) { - let filename = filename.to_owned(); - Some(async { - let filename = Utf8Path::new(&self.options.output.path).join(filename); - let _ = self.output_filesystem.remove_file(&filename).await; - }) - } else { - None - } - }) - .collect::>(); - } - } + self.run_clean_options().await?; self .plugin_driver @@ -421,6 +396,58 @@ impl Compiler { Ok(()) } + async fn run_clean_options(&mut self) -> Result<()> { + let clean_options = &self.options.output.clean; + + // keep all + if let CleanOptions::CleanAll(false) = clean_options { + return Ok(()); + } + + if self.emitted_asset_versions.is_empty() { + if let CleanOptions::KeepPath(p) = clean_options { + let path_to_keep = self.options.output.path.join(Utf8Path::new(p)); + trim_dir( + &*self.output_filesystem, + &self.options.output.path, + &path_to_keep, + ) + .await?; + return Ok(()); + } + + // CleanOptions::CleanAll(true) only + debug_assert!(matches!(clean_options, CleanOptions::CleanAll(true))); + + self + .output_filesystem + .remove_dir_all(&self.options.output.path) + .await?; + return Ok(()); + } + + let assets = self.compilation.assets(); + let _ = self + .emitted_asset_versions + .iter() + .filter_map(|(filename, _version)| { + if !assets.contains_key(filename) { + let filename = filename.to_owned(); + Some(async { + if !clean_options.keep(filename.as_str()) { + let filename = Utf8Path::new(&self.options.output.path).join(filename); + let _ = self.output_filesystem.remove_file(&filename).await; + } + }) + } else { + None + } + }) + .collect::>(); + + Ok(()) + } + fn new_compilation_params(&self) -> CompilationParams { CompilationParams { normal_module_factory: Arc::new(NormalModuleFactory::new( diff --git a/crates/rspack_core/src/options/clean_options.rs b/crates/rspack_core/src/options/clean_options.rs new file mode 100644 index 000000000000..f504a978b077 --- /dev/null +++ b/crates/rspack_core/src/options/clean_options.rs @@ -0,0 +1,51 @@ +use std::{path::PathBuf, str::FromStr}; + +use rspack_paths::Utf8PathBuf; + +/// rust representation of the clean options +// TODO: support RegExp and function type +#[derive(Debug)] +pub enum CleanOptions { + // if true, clean all files + CleanAll(bool), + // keep the files under this path + KeepPath(Utf8PathBuf), +} + +impl CleanOptions { + pub fn keep(&self, path: &str) -> bool { + match self { + Self::CleanAll(value) => !*value, + Self::KeepPath(value) => { + let path = PathBuf::from(path); + path.starts_with(value) + } + } + } +} + +impl From for CleanOptions { + fn from(value: bool) -> Self { + Self::CleanAll(value) + } +} + +impl From<&'_ str> for CleanOptions { + fn from(value: &str) -> Self { + let pb = Utf8PathBuf::from_str(value).expect("should be a valid path"); + Self::KeepPath(pb) + } +} +impl From<&String> for CleanOptions { + fn from(value: &String) -> Self { + let pb = Utf8PathBuf::from_str(value).expect("should be a valid path"); + Self::KeepPath(pb) + } +} + +impl From for CleanOptions { + fn from(value: String) -> Self { + let pb = Utf8PathBuf::from_str(&value).expect("should be a valid path"); + Self::KeepPath(pb) + } +} diff --git a/crates/rspack_core/src/options/mod.rs b/crates/rspack_core/src/options/mod.rs index aab65794a94e..dfa36d4e1fa9 100644 --- a/crates/rspack_core/src/options/mod.rs +++ b/crates/rspack_core/src/options/mod.rs @@ -31,3 +31,5 @@ mod node; pub use node::*; mod filename; pub use filename::*; +mod clean_options; +pub use clean_options::*; diff --git a/crates/rspack_core/src/options/output.rs b/crates/rspack_core/src/options/output.rs index 0c74ac1197f5..cfbe9d859a30 100644 --- a/crates/rspack_core/src/options/output.rs +++ b/crates/rspack_core/src/options/output.rs @@ -9,6 +9,7 @@ use rspack_macros::MergeFrom; use rspack_paths::{AssertUtf8, Utf8Path, Utf8PathBuf}; use sugar_path::SugarPath; +use super::CleanOptions; use crate::{Chunk, ChunkGroupByUkey, ChunkKind, Compilation, Filename, FilenameTemplate}; #[derive(Debug)] @@ -21,7 +22,7 @@ pub enum PathInfo { pub struct OutputOptions { pub path: Utf8PathBuf, pub pathinfo: PathInfo, - pub clean: bool, + pub clean: CleanOptions, pub public_path: PublicPath, pub asset_module_filename: Filename, pub wasm_loading: WasmLoading, diff --git a/crates/rspack_core/src/utils/fs_trim.rs b/crates/rspack_core/src/utils/fs_trim.rs new file mode 100644 index 000000000000..f17d9b8a72b4 --- /dev/null +++ b/crates/rspack_core/src/utils/fs_trim.rs @@ -0,0 +1,119 @@ +use rspack_fs::Result; +use rspack_fs::WritableFileSystem; +use rspack_paths::Utf8Path; + +/// Remove all files and directories in the given directory except the given directory +/// +/// example: +/// ``` +/// #[tokio::test] +/// async fn remove_dir_except() { +/// use crate::rspack_fs::ReadableFileSystem; +/// use crate::rspack_fs::WritableFileSystem; +/// use crate::rspack_fs::WritableFileSystemExt; +/// use crate::rspack_paths::Utf8Path; +/// let fs = crate::rspack_fs::NativeFileSystem; +/// +/// // adding files and directories +/// fs.create_dir_all(&Utf8Path::new("path/to/dir/except")) +/// .await +/// .unwrap(); +/// fs.create_dir_all(&Utf8Path::new("path/to/dir/rm1")) +/// .await +/// .unwrap(); +/// fs.create_dir_all(&Utf8Path::new("path/to/dir/rm2")) +/// .await +/// .unwrap(); +/// +/// let dir = Utf8Path::new("path/to/dir"); +/// let except = Utf8Path::new("path/to/dir/except"); +/// +/// trim_dir(fs, &dir, &except).await.unwrap(); +/// assert_eq!( +/// fs.read_dir(&dir).await.unwrap(), +/// vec![String::from("path/to/dir/except")] +/// ); +/// +/// fs.remove_dir_all(&dir).await.unwrap(); +/// } +/// ``` +pub async fn trim_dir<'a>( + fs: &'a dyn WritableFileSystem, + dir: &'a Utf8Path, + except: &'a Utf8Path, +) -> Result<()> { + if dir.starts_with(except) { + return Ok(()); + } + if !except.starts_with(dir) { + return fs.remove_dir_all(dir).await; + } + + let mut to_clean = dir; + while to_clean != except { + let mut matched = None; + for entry in fs.read_dir(dir).await? { + let path = dir.join(entry); + if except.starts_with(&path) { + matched = Some(except); + continue; + } + if fs.stat(&path).await?.is_directory { + fs.remove_dir_all(&path).await?; + } else { + fs.remove_file(&path).await?; + } + } + let Some(child_to_clean) = matched else { + break; + }; + to_clean = child_to_clean; + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use rspack_fs::{MemoryFileSystem, WritableFileSystem}; + use rspack_paths::Utf8Path; + + use crate::trim_dir; + + #[tokio::test] + async fn async_fs_test() { + let fs = MemoryFileSystem::default(); + assert!( + WritableFileSystem::create_dir_all(&fs, Utf8Path::new("/ex/a1/b1")) + .await + .is_ok() + ); + + assert!( + WritableFileSystem::create_dir_all(&fs, Utf8Path::new("/ex/a2/b1")) + .await + .is_ok() + ); + + assert!( + WritableFileSystem::create_dir_all(&fs, Utf8Path::new("/ex/a2/b2")) + .await + .is_ok() + ); + + assert!( + WritableFileSystem::create_dir_all(&fs, Utf8Path::new("/ex/a3/b1")) + .await + .is_ok() + ); + + assert!(trim_dir(&fs, Utf8Path::new("/ex"), Utf8Path::new("/ex/a2")) + .await + .is_ok()); + + let children = WritableFileSystem::read_dir(&fs, Utf8Path::new("/ex")) + .await + .unwrap(); + assert_eq!(children, vec!["a2"]); + } +} diff --git a/crates/rspack_core/src/utils/mod.rs b/crates/rspack_core/src/utils/mod.rs index b88898a1aee4..9c570be723ed 100644 --- a/crates/rspack_core/src/utils/mod.rs +++ b/crates/rspack_core/src/utils/mod.rs @@ -17,6 +17,8 @@ mod extract_url_and_global; mod fast_actions; mod file_counter; mod find_graph_roots; +mod fs_trim; +pub use fs_trim::*; mod hash; mod identifier; mod module_rules; diff --git a/crates/rspack_fs/src/memory_fs.rs b/crates/rspack_fs/src/memory_fs.rs index 574447484e30..9f2e76e11f58 100644 --- a/crates/rspack_fs/src/memory_fs.rs +++ b/crates/rspack_fs/src/memory_fs.rs @@ -258,7 +258,6 @@ mod tests { use rspack_paths::Utf8Path; use super::{MemoryFileSystem, ReadableFileSystem, WritableFileSystem}; - #[tokio::test] async fn async_fs_test() { let fs = MemoryFileSystem::default(); diff --git a/crates/rspack_fs/src/native_fs.rs b/crates/rspack_fs/src/native_fs.rs index b2f3fa607a5a..461e93b8113f 100644 --- a/crates/rspack_fs/src/native_fs.rs +++ b/crates/rspack_fs/src/native_fs.rs @@ -79,6 +79,7 @@ impl ReadableFileSystem for NativeFileSystem { let path = dunce::canonicalize(path)?; Ok(path.assert_utf8()) } + fn async_read<'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) diff --git a/packages/rspack-test-tools/package.json b/packages/rspack-test-tools/package.json index 2709377eb111..986130fea707 100644 --- a/packages/rspack-test-tools/package.json +++ b/packages/rspack-test-tools/package.json @@ -50,6 +50,7 @@ "@babel/template": "7.22.15", "@babel/traverse": "7.23.2", "@babel/types": "7.23.0", + "cross-env": "^7.0.3", "csv-to-markdown-table": "^1.3.0", "deepmerge": "^4.3.1", "filenamify": "4.3.0", diff --git a/packages/rspack-test-tools/tests/configCases/clean/enabled/index.js b/packages/rspack-test-tools/tests/configCases/clean/enabled/index.js new file mode 100644 index 000000000000..47ec94a70e2b --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/clean/enabled/index.js @@ -0,0 +1,2 @@ + +it("should compiles", () => { }) diff --git a/packages/rspack-test-tools/tests/configCases/clean/enabled/readdir.js b/packages/rspack-test-tools/tests/configCases/clean/enabled/readdir.js new file mode 100644 index 000000000000..b2f404e7f742 --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/clean/enabled/readdir.js @@ -0,0 +1,38 @@ +const fs = require('fs'); +const path = require('path'); + +function handlePath(path) { + return path.replace(/\\/g, "/"); +} + +module.exports = function readDir(from) { + const collectedFiles = []; + const collectedDirectories = []; + const stack = [from]; + let cursor; + + while ((cursor = stack.pop())) { + const stat = fs.statSync(cursor); + + if (stat.isDirectory()) { + const items = fs.readdirSync(cursor); + + if (from !== cursor) { + const relative = path.relative(from, cursor); + collectedDirectories.push(handlePath(relative)); + } + + for (let i = 0; i < items.length; i++) { + stack.push(path.join(cursor, items[i])); + } + } else { + const relative = path.relative(from, cursor); + collectedFiles.push(handlePath(relative)); + } + } + + return { + files: collectedFiles, + directories: collectedDirectories + }; +} diff --git a/packages/rspack-test-tools/tests/configCases/clean/enabled/rspack.config.js b/packages/rspack-test-tools/tests/configCases/clean/enabled/rspack.config.js new file mode 100644 index 000000000000..88a21b894153 --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/clean/enabled/rspack.config.js @@ -0,0 +1,50 @@ +const fs = require("fs"); +const path = require("path"); +const { RawSource } = require("webpack-sources"); +const readDir = require("./readdir"); + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + output: { + clean: true + }, + plugins: [ + compiler => { + let once = true; + compiler.hooks.thisCompilation.tap("Test", compilation => { + compilation.hooks.processAssets.tap("Test", assets => { + if (once) { + const outputPath = compilation.getPath(compiler.outputPath, {}); + const customDir = path.join( + outputPath, + "this/dir/should/be/removed" + ); + fs.mkdirSync(customDir, { recursive: true }); + fs.writeFileSync(path.join(customDir, "file.ext"), ""); + once = false; + } + assets["this/dir/should/not/be/removed/file.ext"] = new RawSource(""); + }); + }); + compiler.hooks.afterEmit.tap("Test", compilation => { + const outputPath = compilation.getPath(compiler.outputPath, {}); + expect(readDir(outputPath)).toMatchInlineSnapshot(` + Object { + directories: Array [ + this, + this/dir, + this/dir/should, + this/dir/should/not, + this/dir/should/not/be, + this/dir/should/not/be/removed, + ], + files: Array [ + this/dir/should/not/be/removed/file.ext, + bundle0.js, + ], + } + `); + }); + } + ] +}; diff --git a/packages/rspack-test-tools/tests/configCases/clean/ignore-string/index.js b/packages/rspack-test-tools/tests/configCases/clean/ignore-string/index.js new file mode 100644 index 000000000000..067d54b5a893 --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/clean/ignore-string/index.js @@ -0,0 +1 @@ +it("should compiles", () => { }) diff --git a/packages/rspack-test-tools/tests/configCases/clean/ignore-string/rspack.config.js b/packages/rspack-test-tools/tests/configCases/clean/ignore-string/rspack.config.js new file mode 100644 index 000000000000..1b077c2fda4a --- /dev/null +++ b/packages/rspack-test-tools/tests/configCases/clean/ignore-string/rspack.config.js @@ -0,0 +1,58 @@ +const fs = require("fs"); +const path = require("path"); +const { RawSource } = require("webpack-sources"); +const readDir = require("../enabled/readdir"); + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + output: { + clean: { + keep: "ignored/dir" + } + }, + plugins: [ + compiler => { + let once = true; + compiler.hooks.thisCompilation.tap("Test", compilation => { + compilation.hooks.processAssets.tap("Test", assets => { + if (once) { + const outputPath = compilation.getPath(compiler.outputPath, {}); + const customDir = path.join( + outputPath, + "this/dir/should/be/removed" + ); + const ignoredDir = path.join( + outputPath, + "this/is/ignored/dir/that/should/not/be/removed" + ); + fs.mkdirSync(customDir, { recursive: true }); + fs.writeFileSync(path.join(customDir, "file.ext"), ""); + fs.mkdirSync(ignoredDir, { recursive: true }); + fs.writeFileSync(path.join(ignoredDir, "file.ext"), ""); + once = false; + } + assets["this/dir/should/not/be/removed/file.ext"] = new RawSource(""); + }); + }); + compiler.hooks.afterEmit.tap("Test", compilation => { + const outputPath = compilation.getPath(compiler.outputPath, {}); + expect(readDir(outputPath)).toMatchInlineSnapshot(` + Object { + directories: Array [ + this, + this/dir, + this/dir/should, + this/dir/should/not, + this/dir/should/not/be, + this/dir/should/not/be/removed, + ], + files: Array [ + this/dir/should/not/be/removed/file.ext, + bundle0.js, + ], + } + `); + }); + } + ] +}; diff --git a/packages/rspack/etc/core.api.md b/packages/rspack/etc/core.api.md index 5336752a1c51..bd280bbce8c2 100644 --- a/packages/rspack/etc/core.api.md +++ b/packages/rspack/etc/core.api.md @@ -525,7 +525,9 @@ export type ChunkLoadingGlobal = string; export type ChunkLoadingType = string | "jsonp" | "import-scripts" | "require" | "async-node" | "import"; // @public -export type Clean = boolean; +export type Clean = boolean | { + keep?: string; +}; // @public (undocumented) class CodeGenerationResult { @@ -5721,7 +5723,13 @@ export const rspackOptions: z.ZodObject<{ output: z.ZodOptional; pathinfo: z.ZodOptional]>>; - clean: z.ZodOptional; + clean: z.ZodOptional; + }, "strict", z.ZodTypeAny, { + keep?: string | undefined; + }, { + keep?: string | undefined; + }>]>>; publicPath: z.ZodOptional, z.ZodUnion<[z.ZodString, z.ZodFunction, z.ZodOptional>], z.ZodUnknown>, z.ZodString>]>]>>; filename: z.ZodOptional, z.ZodOptional>], z.ZodUnknown>, z.ZodString>]>>; chunkFilename: z.ZodOptional, z.ZodOptional>], z.ZodUnknown>, z.ZodString>]>>; @@ -5973,7 +5981,9 @@ export const rspackOptions: z.ZodObject<{ crossOriginLoading?: false | "anonymous" | "use-credentials" | undefined; uniqueName?: string | undefined; pathinfo?: boolean | "verbose" | undefined; - clean?: boolean | undefined; + clean?: boolean | { + keep?: string | undefined; + } | undefined; cssFilename?: string | ((args_0: PathData, args_1: JsAssetInfo | undefined, ...args: unknown[]) => string) | undefined; cssChunkFilename?: string | ((args_0: PathData, args_1: JsAssetInfo | undefined, ...args: unknown[]) => string) | undefined; hotUpdateMainFilename?: string | undefined; @@ -6069,7 +6079,9 @@ export const rspackOptions: z.ZodObject<{ crossOriginLoading?: false | "anonymous" | "use-credentials" | undefined; uniqueName?: string | undefined; pathinfo?: boolean | "verbose" | undefined; - clean?: boolean | undefined; + clean?: boolean | { + keep?: string | undefined; + } | undefined; cssFilename?: string | ((args_0: PathData, args_1: JsAssetInfo | undefined, ...args: unknown[]) => string) | undefined; cssChunkFilename?: string | ((args_0: PathData, args_1: JsAssetInfo | undefined, ...args: unknown[]) => string) | undefined; hotUpdateMainFilename?: string | undefined; @@ -8643,7 +8655,9 @@ export const rspackOptions: z.ZodObject<{ crossOriginLoading?: false | "anonymous" | "use-credentials" | undefined; uniqueName?: string | undefined; pathinfo?: boolean | "verbose" | undefined; - clean?: boolean | undefined; + clean?: boolean | { + keep?: string | undefined; + } | undefined; cssFilename?: string | ((args_0: PathData, args_1: JsAssetInfo | undefined, ...args: unknown[]) => string) | undefined; cssChunkFilename?: string | ((args_0: PathData, args_1: JsAssetInfo | undefined, ...args: unknown[]) => string) | undefined; hotUpdateMainFilename?: string | undefined; @@ -9266,7 +9280,9 @@ export const rspackOptions: z.ZodObject<{ crossOriginLoading?: false | "anonymous" | "use-credentials" | undefined; uniqueName?: string | undefined; pathinfo?: boolean | "verbose" | undefined; - clean?: boolean | undefined; + clean?: boolean | { + keep?: string | undefined; + } | undefined; cssFilename?: string | ((args_0: PathData, args_1: JsAssetInfo | undefined, ...args: unknown[]) => string) | undefined; cssChunkFilename?: string | ((args_0: PathData, args_1: JsAssetInfo | undefined, ...args: unknown[]) => string) | undefined; hotUpdateMainFilename?: string | undefined; diff --git a/packages/rspack/src/config/types.ts b/packages/rspack/src/config/types.ts index 9899b6adab19..a008c8cd669e 100644 --- a/packages/rspack/src/config/types.ts +++ b/packages/rspack/src/config/types.ts @@ -281,7 +281,7 @@ export type ChunkLoadingGlobal = string; export type EnabledLibraryTypes = string[]; /** Whether delete all files in the output directory. */ -export type Clean = boolean; +export type Clean = boolean | { keep?: string }; /** Output JavaScript files as module type. */ export type OutputModule = boolean; diff --git a/packages/rspack/src/config/zod.ts b/packages/rspack/src/config/zod.ts index 3d1957ee1397..1239ccde2e45 100644 --- a/packages/rspack/src/config/zod.ts +++ b/packages/rspack/src/config/zod.ts @@ -236,7 +236,12 @@ const enabledLibraryTypes = z.array( libraryType ) satisfies z.ZodType; -const clean = z.boolean() satisfies z.ZodType; +const clean = z.union([ + z.boolean(), + z.strictObject({ + keep: z.string().optional() + }) +]) satisfies z.ZodType; const outputModule = z.boolean() satisfies z.ZodType; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1cb13193246d..9fb87129aeb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -458,6 +458,9 @@ importers: '@babel/types': specifier: 7.23.0 version: 7.23.0 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 csv-to-markdown-table: specifier: ^1.3.0 version: 1.4.1 diff --git a/tests/webpack-test/configCases/clean/ignore-hook/index.js b/tests/webpack-test/configCases/clean/ignore-hook/index.js index bbd9de4153f6..55781abf9fb5 100644 --- a/tests/webpack-test/configCases/clean/ignore-hook/index.js +++ b/tests/webpack-test/configCases/clean/ignore-hook/index.js @@ -1 +1 @@ -it("should compile and run the test", function() {}); +it("should compile and run the test", function () { });