Skip to content

Commit

Permalink
feat: support output.compareBeforeEmit (#8245)
Browse files Browse the repository at this point in the history
  • Loading branch information
LingyuCoder authored Oct 29, 2024
1 parent a50b5d9 commit ef40355
Show file tree
Hide file tree
Showing 18 changed files with 194 additions and 14 deletions.
1 change: 1 addition & 0 deletions crates/node_binding/binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1690,6 +1690,7 @@ export interface RawOutputOptions {
workerPublicPath: string
scriptType: "module" | "text/javascript" | "false"
environment: RawEnvironment
compareBeforeEmit: boolean
}

export interface RawParserOptions {
Expand Down
2 changes: 2 additions & 0 deletions crates/rspack_binding_options/src/options/raw_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ pub struct RawOutputOptions {
#[napi(ts_type = r#""module" | "text/javascript" | "false""#)]
pub script_type: String,
pub environment: RawEnvironment,
pub compare_before_emit: bool,
}

impl TryFrom<RawOutputOptions> for OutputOptions {
Expand Down Expand Up @@ -160,6 +161,7 @@ impl TryFrom<RawOutputOptions> for OutputOptions {
environment: value.environment.into(),
charset: value.charset,
chunk_load_timeout: value.chunk_load_timeout,
compare_before_emit: value.compare_before_emit,
})
}
}
56 changes: 46 additions & 10 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::Result;
use rspack_error::{error, Result};
use rspack_fs::{
AsyncNativeFileSystem, AsyncWritableFileSystem, NativeFileSystem, ReadableFileSystem,
};
Expand All @@ -21,7 +21,8 @@ pub use self::module_executor::{ExecuteModuleId, ExecutedRuntimeModule, ModuleEx
use crate::incremental::IncrementalPasses;
use crate::old_cache::Cache as OldCache;
use crate::{
fast_set, BoxPlugin, CompilerOptions, Logger, PluginDriver, ResolverFactory, SharedPluginDriver,
fast_set, include_hash, BoxPlugin, CompilerOptions, Logger, PluginDriver, ResolverFactory,
SharedPluginDriver,
};
use crate::{ContextModuleFactory, NormalModuleFactory};

Expand Down Expand Up @@ -340,9 +341,7 @@ impl Compiler {
asset: &CompilationAsset,
) -> Result<()> {
if let Some(source) = asset.get_source() {
let filename = filename
.split_once('?')
.map_or(filename, |(filename, _query)| filename);
let (filename, query) = filename.split_once('?').unwrap_or((filename, ""));
let file_path = output_path.join(filename);
self
.output_filesystem
Expand All @@ -353,12 +352,49 @@ impl Compiler {
)
.await?;

self
.output_filesystem
.write(&file_path, source.buffer().as_ref())
.await?;
let content = source.buffer();

self.compilation.emitted_assets.insert(filename.to_string());
let mut immutable = asset.info.immutable.unwrap_or(false);
if !query.is_empty() {
immutable = immutable
&& (include_hash(filename, &asset.info.content_hash)
|| include_hash(filename, &asset.info.chunk_hash)
|| include_hash(filename, &asset.info.full_hash));
}

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
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
}
};

if need_write {
self.output_filesystem.write(&file_path, &content).await?;
self.compilation.emitted_assets.insert(filename.to_string());
}

let info = AssetEmittedInfo {
output_path: output_path.to_owned(),
Expand Down
1 change: 1 addition & 0 deletions crates/rspack_core/src/options/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub struct OutputOptions {
pub worker_public_path: String,
pub script_type: String,
pub environment: Environment,
pub compare_before_emit: bool,
}

impl From<&OutputOptions> for RspackHash {
Expand Down
6 changes: 6 additions & 0 deletions crates/rspack_core/src/utils/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::borrow::Cow;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

use rustc_hash::FxHashSet as HashSet;

pub fn calc_hash<T: Hash>(t: &T) -> u64 {
let mut s = DefaultHasher::new();
t.hash(&mut s);
Expand Down Expand Up @@ -29,6 +31,10 @@ pub fn extract_hash_pattern(pattern: &str, key: &str) -> Option<ExtractedHashPat
})
}

pub fn include_hash(filename: &str, hashes: &HashSet<String>) -> bool {
hashes.iter().any(|hash| filename.contains(hash))
}

pub trait Replacer {
fn get_replacer(&mut self, hash_len: Option<usize>) -> Cow<'_, str>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ Object {
chunkLoading: jsonp,
chunkLoadingGlobal: webpackChunk_rspack_test_tools,
clean: false,
compareBeforeEmit: true,
crossOriginLoading: false,
cssChunkFilename: [name].css,
cssFilename: [name].css,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const path = require("path");
const fs = require("fs");
const rimraf = require("rimraf");

let first_asset_mtime;

/** @type {import('../../dist').TCompilerCaseConfig} */
module.exports = {
description: "should write same content to same file",
options(context) {
return {
output: {
path: context.getDist(),
filename: "main.js",
compareBeforeEmit: false
},
context: context.getSource(),
entry: "./d",
};
},
async build(context, compiler) {
rimraf.sync(context.getDist());
await new Promise(resolve => {
compiler.run(() => {
first_asset_mtime = fs.statSync(path.join(context.getDist("main.js")))?.mtime;
compiler.run(() => {
resolve();
});
});
});
},
async check(context) {
let second_asset_mtime = fs.statSync(path.join(context.getDist("main.js")))?.mtime;
expect(first_asset_mtime).not.toEqual(second_asset_mtime);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const path = require("path");
const fs = require("fs");
const rimraf = require("rimraf");

let first_asset_mtime;

/** @type {import('../..').TCompilerCaseConfig} */
module.exports = {
description: "should write emit same content to same file",
options(context) {
return {
output: {
path: context.getDist(),
filename: "main.js",
},
context: context.getSource(),
entry: "./d",
};
},
async build(context, compiler) {
rimraf.sync(context.getDist());
await new Promise(resolve => {
compiler.run(() => {
first_asset_mtime = fs.statSync(path.join(context.getDist("main.js")))?.mtime;
compiler.run(() => {
resolve();
});
});
});
},
async check(context) {
let second_asset_mtime = fs.statSync(path.join(context.getDist("main.js")))?.mtime;
expect(first_asset_mtime).toEqual(second_asset_mtime);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const path = require("path");
module.exports = {
entry: "./index",
output: {
path: path.join(__dirname, './dist'),
filename: "[id].xxxx.js",
chunkFilename: "[id].xxxx.js"
}
Expand Down
8 changes: 8 additions & 0 deletions packages/rspack/etc/core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3878,6 +3878,7 @@ export type Output = {
chunkLoadTimeout?: number;
charset?: boolean;
environment?: Environment;
compareBeforeEmit?: boolean;
};

// @public (undocumented)
Expand Down Expand Up @@ -3930,6 +3931,8 @@ export interface OutputNormalized {
// (undocumented)
clean?: Clean;
// (undocumented)
compareBeforeEmit?: boolean;
// (undocumented)
crossOriginLoading?: CrossOriginLoading;
// (undocumented)
cssChunkFilename?: CssChunkFilename;
Expand Down Expand Up @@ -5507,6 +5510,7 @@ export const rspackOptions: z.ZodObject<{
templateLiteral?: boolean | undefined;
asyncFunction?: boolean | undefined;
}>>;
compareBeforeEmit: z.ZodOptional<z.ZodBoolean>;
}, "strict", z.ZodTypeAny, {
module?: boolean | undefined;
filename?: string | ((args_0: PathData, args_1: JsAssetInfo | undefined, ...args: unknown[]) => string) | undefined;
Expand Down Expand Up @@ -5599,6 +5603,7 @@ export const rspackOptions: z.ZodObject<{
charset?: boolean | undefined;
chunkLoadTimeout?: number | undefined;
cssHeadDataCompression?: boolean | undefined;
compareBeforeEmit?: boolean | undefined;
libraryExport?: string | string[] | undefined;
libraryTarget?: string | undefined;
strictModuleExceptionHandling?: boolean | undefined;
Expand Down Expand Up @@ -5694,6 +5699,7 @@ export const rspackOptions: z.ZodObject<{
charset?: boolean | undefined;
chunkLoadTimeout?: number | undefined;
cssHeadDataCompression?: boolean | undefined;
compareBeforeEmit?: boolean | undefined;
libraryExport?: string | string[] | undefined;
libraryTarget?: string | undefined;
strictModuleExceptionHandling?: boolean | undefined;
Expand Down Expand Up @@ -8151,6 +8157,7 @@ export const rspackOptions: z.ZodObject<{
charset?: boolean | undefined;
chunkLoadTimeout?: number | undefined;
cssHeadDataCompression?: boolean | undefined;
compareBeforeEmit?: boolean | undefined;
libraryExport?: string | string[] | undefined;
libraryTarget?: string | undefined;
strictModuleExceptionHandling?: boolean | undefined;
Expand Down Expand Up @@ -8755,6 +8762,7 @@ export const rspackOptions: z.ZodObject<{
charset?: boolean | undefined;
chunkLoadTimeout?: number | undefined;
cssHeadDataCompression?: boolean | undefined;
compareBeforeEmit?: boolean | undefined;
libraryExport?: string | string[] | undefined;
libraryTarget?: string | undefined;
strictModuleExceptionHandling?: boolean | undefined;
Expand Down
3 changes: 2 additions & 1 deletion packages/rspack/src/config/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ function getRawOutput(output: OutputNormalized): RawOptions["output"] {
scriptType: output.scriptType === false ? "false" : output.scriptType!,
charset: output.charset!,
chunkLoadTimeout: output.chunkLoadTimeout!,
environment: output.environment!
environment: output.environment!,
compareBeforeEmit: output.compareBeforeEmit!
};
}

Expand Down
1 change: 1 addition & 0 deletions packages/rspack/src/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ const applyOutputDefaults = (
D(output, "cssHeadDataCompression", !development);
D(output, "assetModuleFilename", "[hash][ext][query]");
D(output, "webassemblyModuleFilename", "[hash].module.wasm");
D(output, "compareBeforeEmit", true);
F(output, "path", () => path.join(process.cwd(), "dist"));
F(output, "pathinfo", () => development);
D(
Expand Down
4 changes: 3 additions & 1 deletion packages/rspack/src/config/normalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ export const getNormalizedRspackOptions = (
output.devtoolFallbackModuleFilenameTemplate,
chunkLoadTimeout: output.chunkLoadTimeout,
charset: output.charset,
environment: cloneObject(output.environment)
environment: cloneObject(output.environment),
compareBeforeEmit: output.compareBeforeEmit
};
}),
resolve: nestedConfig(config.resolve, resolve => ({
Expand Down Expand Up @@ -519,6 +520,7 @@ export interface OutputNormalized {
charset?: boolean;
chunkLoadTimeout?: number;
cssHeadDataCompression?: boolean;
compareBeforeEmit?: boolean;
}

export interface ModuleOptionsNormalized {
Expand Down
5 changes: 5 additions & 0 deletions packages/rspack/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,11 @@ export type Output = {

/** Tell Rspack what kind of ES-features may be used in the generated runtime-code. */
environment?: Environment;

/**
* Check if to be emitted file already exists and have the same content before writing to output filesystem.
*/
compareBeforeEmit?: boolean;
};

//#endregion
Expand Down
3 changes: 2 additions & 1 deletion packages/rspack/src/config/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,8 @@ const output = z.strictObject({
devtoolFallbackModuleFilenameTemplate.optional(),
chunkLoadTimeout: z.number().optional(),
charset: z.boolean().optional(),
environment: environment.optional()
environment: environment.optional(),
compareBeforeEmit: z.boolean().optional()
}) satisfies z.ZodType<t.Output>;
//#endregion

Expand Down
1 change: 1 addition & 0 deletions tests/plugin-test/copy-plugin/helpers/getCompiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports = (config = {}) => {
context: path.resolve(__dirname, "../fixtures"),
entry: path.resolve(__dirname, "../helpers/enter.js"),
output: {
compareBeforeEmit: false,
path: path.resolve(__dirname, "../build")
},
module: {
Expand Down
22 changes: 22 additions & 0 deletions website/docs/en/config/output.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,28 @@ module.exports = {
};
```

## output.compareBeforeEmit

<ApiMeta addedVersion={'1.1.0'} />

- **Type:** `boolean`
- **Default:** `true`

Tells Rspack to check if to be emitted file already exists and has the same content before writing to the output file system.

:::warning
Rspack will not write output file when file already exists on disk with the same content.
:::

```js title="rspack.config.js"
module.exports = {
//...
output: {
compareBeforeEmit: false,
},
};
```

## output.crossOriginLoading

- **Type:** `false | 'anonymous' | 'use-credentials'`
Expand Down
Loading

2 comments on commit ef40355

@rspack-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Ran ecosystem CI: Open

suite result
modernjs ✅ success
_selftest ✅ success
rspress ✅ success
rslib ✅ success
rsbuild ✅ success
examples ✅ success
devserver ✅ success

@rspack-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Benchmark detail: Open

Name Base (2024-10-29 a901d54) Current Change
10000_development-mode + exec 2.11 s ± 23 ms 2.12 s ± 29 ms +0.29 %
10000_development-mode_hmr + exec 672 ms ± 22 ms 648 ms ± 2.2 ms -3.47 %
10000_production-mode + exec 2.71 s ± 62 ms 2.71 s ± 28 ms -0.05 %
arco-pro_development-mode + exec 1.78 s ± 66 ms 1.81 s ± 57 ms +1.58 %
arco-pro_development-mode_hmr + exec 430 ms ± 8.4 ms 428 ms ± 1.1 ms -0.40 %
arco-pro_production-mode + exec 3.2 s ± 91 ms 3.23 s ± 94 ms +1.19 %
arco-pro_production-mode_generate-package-json-webpack-plugin + exec 3.23 s ± 81 ms 3.24 s ± 77 ms +0.14 %
threejs_development-mode_10x + exec 1.61 s ± 18 ms 1.63 s ± 19 ms +0.97 %
threejs_development-mode_10x_hmr + exec 765 ms ± 11 ms 776 ms ± 12 ms +1.38 %
threejs_production-mode_10x + exec 5.02 s ± 35 ms 5.03 s ± 16 ms +0.21 %
10000_big_production-mode + exec - 51.5 s ± 484 ms -

Please sign in to comment.