Repository: web-infra-dev/rsbuild (TypeScript)

Issue:
[Feature]: Support only print total file sizes
### What problem does this feature solve?

Some users may only care about the total size, Rsbuild should support only print total file sizes, 

### What does the proposed API look like?

```ts
export default {
  performance: {
    printFileSize: {
      // whether to print total size
      total?: boolean;
      // whether to print size of each file
      detail?: boolean;
    }
  }
}
```



Fix (patch):
diff --git a/.changeset/gorgeous-swans-compete.md b/.changeset/gorgeous-swans-compete.md
new file mode 100644
index 0000000000..1e388fd4e2
--- /dev/null
+++ b/.changeset/gorgeous-swans-compete.md
@@ -0,0 +1,6 @@
+---
+'@rsbuild/shared': patch
+'@rsbuild/core': patch
+---
+
+feat(core): support printFileSize.detail and printFileSize.total
diff --git a/packages/core/src/plugins/fileSize.ts b/packages/core/src/plugins/fileSize.ts
index db31a63e2c..914f52d2b5 100644
--- a/packages/core/src/plugins/fileSize.ts
+++ b/packages/core/src/plugins/fileSize.ts
@@ -5,7 +5,12 @@
 import path from 'path';
 import { fse } from '@rsbuild/shared';
 import { color, logger } from '@rsbuild/shared';
-import type { Stats, MultiStats, StatsAsset } from '@rsbuild/shared';
+import type {
+  Stats,
+  MultiStats,
+  StatsAsset,
+  PrintFileSizeOptions,
+} from '@rsbuild/shared';
 import type { RsbuildPlugin } from '../types';
 
 /** Filter source map and license files */
@@ -45,7 +50,15 @@ const calcFileSize = (len: number) => {
   return `${val.toFixed(val < 1 ? 2 : 1)} kB`;
 };
 
-async function printFileSizes(stats: Stats | MultiStats, distPath: string) {
+async function printFileSizes(
+  config: PrintFileSizeOptions,
+  stats: Stats | MultiStats,
+  distPath: string,
+) {
+  if (config.detail === false && config.total === false) {
+    return;
+  }
+
   const { default: gzipSize } = await import('@rsbuild/shared/gzip-size');
 
   const formatAsset = (asset: StatsAsset) => {
@@ -92,14 +105,16 @@ async function printFileSizes(stats: Stats | MultiStats, distPath: string) {
 
   assets.sort((a, b) => b.size - a.size);
 
+  logger.info(`Production file sizes:\n`);
+
   const longestLabelLength = Math.max(...assets.map((a) => a.sizeLabel.length));
   const longestFileLength = Math.max(
     ...assets.map((a) => (a.folder + path.sep + a.name).length),
   );
 
-  logger.info(`Production file sizes:\n`);
-
-  printHeader(longestFileLength, longestLabelLength);
+  if (config.detail !== false) {
+    printHeader(longestFileLength, longestLabelLength);
+  }
 
   let totalSize = 0;
   let totalGzipSize = 0;
@@ -113,29 +128,33 @@ async function printFileSizes(stats: Stats | MultiStats, distPath: string) {
     totalSize += asset.size;
     totalGzipSize += asset.gzippedSize;
 
-    if (sizeLength < longestLabelLength) {
-      const rightPadding = ' '.repeat(longestLabelLength - sizeLength);
-      sizeLabel += rightPadding;
-    }
+    if (config.detail !== false) {
+      if (sizeLength < longestLabelLength) {
+        const rightPadding = ' '.repeat(longestLabelLength - sizeLength);
+        sizeLabel += rightPadding;
+      }
 
-    let fileNameLabel =
-      color.dim(asset.folder + path.sep) + color.cyan(asset.name);
+      let fileNameLabel =
+        color.dim(asset.folder + path.sep) + color.cyan(asset.name);
 
-    if (fileNameLength < longestFileLength) {
-      const rightPadding = ' '.repeat(longestFileLength - fileNameLength);
-      fileNameLabel += rightPadding;
-    }
+      if (fileNameLength < longestFileLength) {
+        const rightPadding = ' '.repeat(longestFileLength - fileNameLength);
+        fileNameLabel += rightPadding;
+      }
 
-    logger.log(`  ${fileNameLabel}    ${sizeLabel}    ${gzipSizeLabel}`);
+      logger.log(`  ${fileNameLabel}    ${sizeLabel}    ${gzipSizeLabel}`);
+    }
   });
 
-  const totalSizeLabel = `${color.bold(
-    color.blue('Total size:'),
-  )}  ${calcFileSize(totalSize)}`;
-  const gzippedSizeLabel = `${color.bold(
-    color.blue('Gzipped size:'),
-  )}  ${calcFileSize(totalGzipSize)}`;
-  logger.log(`\n  ${totalSizeLabel}\n  ${gzippedSizeLabel}\n`);
+  if (config.total !== false) {
+    const totalSizeLabel = `${color.bold(
+      color.blue('Total size:'),
+    )}  ${calcFileSize(totalSize)}`;
+    const gzippedSizeLabel = `${color.bold(
+      color.blue('Gzipped size:'),
+    )}  ${calcFileSize(totalGzipSize)}`;
+    logger.log(`\n  ${totalSizeLabel}\n  ${gzippedSizeLabel}\n`);
+  }
 }
 
 export const pluginFileSize = (): RsbuildPlugin => ({
@@ -143,11 +162,27 @@ export const pluginFileSize = (): RsbuildPlugin => ({
 
   setup(api) {
     api.onAfterBuild(async ({ stats }) => {
-      const config = api.getNormalizedConfig();
+      const { printFileSize } = api.getNormalizedConfig().performance;
+
+      if (printFileSize === false) {
+        return;
+      }
+
+      const printFileSizeConfig =
+        typeof printFileSize === 'boolean'
+          ? {
+              total: true,
+              detail: true,
+            }
+          : printFileSize;
 
-      if (config.performance.printFileSize && stats) {
+      if (stats) {
         try {
-          await printFileSizes(stats, api.context.distPath);
+          await printFileSizes(
+            printFileSizeConfig,
+            stats,
+            api.context.distPath,
+          );
         } catch (err) {
           logger.warn('Failed to print file size.');
           logger.warn(err as Error);
diff --git a/packages/document/docs/en/shared/config/performance/printFileSize.md b/packages/document/docs/en/shared/config/performance/printFileSize.md
index 2b18db1784..15f8f084fd 100644
--- a/packages/document/docs/en/shared/config/performance/printFileSize.md
+++ b/packages/document/docs/en/shared/config/performance/printFileSize.md
@@ -1,21 +1,54 @@
-- **Type:** `boolean`
+- **Type:**
+
+```ts
+type PrintFileSizeOptions =
+  | {
+      /**
+       * whether to print total size
+       */
+      total?: boolean;
+      /**
+       * whether to print size of each file
+       */
+      detail?: boolean;
+    }
+  | boolean;
+```
+
 - **Default:** `true`
 
 Whether to print the file sizes after production build.
 
+If it is a Boolean type, it will be decided according to the config whether to print the file sizes. The default output is as follows.
+
 ```bash
 info    Production file sizes:
 
-  File                                      Size         Gzipped
-  dist/static/js/lib-react.09721b5c.js      152.6 kB     49.0 kB
-  dist/html/main/index.html                 5.8 kB       2.5 kB
-  dist/static/js/main.3568a38e.js           3.5 kB       1.4 kB
-  dist/static/css/main.03221f72.css         1.4 kB       741 B
+  File                                    Size        Gzipped
+  dist/static/js/lib-react.b0714b60.js    140.4 kB    45.0 kB
+  dist/static/js/index.f3fde9c7.js        1.9 kB      0.97 kB
+  dist/index.html                         0.39 kB     0.25 kB
+  dist/static/css/index.2960ac62.css      0.35 kB     0.26 kB
+
+  Total size:  143.0 kB
+  Gzipped size:  46.5 kB
 ```
 
-### Example
+You can also configure whether to print the sum of the size of all static resource files and whether to print the size of each static resource file through the Object type.
+
+If you don't want to print the size of each static resource file, you can:
+
+```ts
+export default {
+  performance: {
+    printFileSize: {
+      detail: false,
+    },
+  },
+};
+```
 
-Disable the logs:
+If you don't want to print any information, you can disable it by setting `printFileSize` to `false`:
 
 ```ts
 export default {
diff --git a/packages/document/docs/zh/shared/config/performance/printFileSize.md b/packages/document/docs/zh/shared/config/performance/printFileSize.md
index 21c6501061..306b1c69d6 100644
--- a/packages/document/docs/zh/shared/config/performance/printFileSize.md
+++ b/packages/document/docs/zh/shared/config/performance/printFileSize.md
@@ -1,21 +1,54 @@
-- **类型：** `boolean`
+- **类型：**
+
+```ts
+type PrintFileSizeOptions =
+  | {
+      /**
+       * 是否输出所有静态资源文件的总体积
+       */
+      total?: boolean;
+      /**
+       * 是否输出每个静态资源文件的体积
+       */
+      detail?: boolean;
+    }
+  | boolean;
+```
+
 - **默认值：** `true`
 
 是否在生产环境构建后输出所有静态资源文件的体积。
 
+如果是 Boolean 类型，将根据配置决定是否输出所有静态资源文件的体积，默认输出如下。
+
 ```bash
 info    Production file sizes:
 
-  File                                      Size         Gzipped
-  dist/static/js/lib-react.09721b5c.js      152.6 kB     49.0 kB
-  dist/html/main/index.html                 5.8 kB       2.5 kB
-  dist/static/js/main.3568a38e.js           3.5 kB       1.4 kB
-  dist/static/css/main.03221f72.css         1.4 kB       741 B
+  File                                    Size        Gzipped
+  dist/static/js/lib-react.b0714b60.js    140.4 kB    45.0 kB
+  dist/static/js/index.f3fde9c7.js        1.9 kB      0.97 kB
+  dist/index.html                         0.39 kB     0.25 kB
+  dist/static/css/index.2960ac62.css      0.35 kB     0.26 kB
+
+  Total size:  143.0 kB
+  Gzipped size:  46.5 kB
 ```
 
-### 示例
+你也可以通过 Object 类型分别对输出所有静态资源文件的体积和以及输出每个静态资源文件的体积进行配置。
+
+如果你不想输出每个静态资源文件的体积，可以：
+
+```ts
+export default {
+  performance: {
+    printFileSize: {
+      detail: false,
+    },
+  },
+};
+```
 
-禁用相关日志：
+如果你不想输出任何信息，可以将 `printFileSize` 置为 `false` 将其禁用掉：
 
 ```ts
 export default {
diff --git a/packages/shared/src/types/config/performance.ts b/packages/shared/src/types/config/performance.ts
index 7a714b63b9..6e16158ebc 100644
--- a/packages/shared/src/types/config/performance.ts
+++ b/packages/shared/src/types/config/performance.ts
@@ -10,6 +10,11 @@ export type BuildCacheOptions = {
   cacheDigest?: Array<string | undefined>;
 };
 
+export type PrintFileSizeOptions = {
+  total?: boolean;
+  detail?: boolean;
+};
+
 export interface PreconnectOption {
   href: string;
   crossorigin?: boolean;
@@ -58,7 +63,7 @@ export interface PerformanceConfig {
   /**
    * Whether to print the file sizes after production build.
    */
-  printFileSize?: boolean;
+  printFileSize?: PrintFileSizeOptions | boolean;
   /**
    * Configure the chunk splitting strategy.
    */
@@ -105,7 +110,7 @@ export interface PerformanceConfig {
 }
 
 export interface NormalizedPerformanceConfig extends PerformanceConfig {
-  printFileSize: boolean;
+  printFileSize: PrintFileSizeOptions | boolean;
   buildCache: BuildCacheOptions | boolean;
   chunkSplit: RsbuildChunkSplit;
 }