@zealakit/css-doctor-rsbuild 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +61 -2
- package/README_CN.md +62 -0
- package/README_VI.md +62 -0
- package/dist/index.d.ts +116 -0
- package/dist/index.js +775 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zealamic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# @zealakit/css-doctor-rsbuild
|
|
4
|
+
|
|
5
|
+
**Rsbuild/Rspack integration for page-coverage and size-budget CSS splitting.**
|
|
6
|
+
|
|
7
|
+
[English](https://github.com/zealakit/css-doctor/blob/main/packages/css-doctor-rsbuild/README.md) | [Tiếng Việt](https://github.com/zealakit/css-doctor/blob/main/packages/css-doctor-rsbuild/README_VI.md) | [简体中文](https://github.com/zealakit/css-doctor/blob/main/packages/css-doctor-rsbuild/README_CN.md)
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add -D @zealakit/css-doctor-rsbuild
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { defineConfig } from "@rsbuild/core";
|
|
25
|
+
import { pluginReact } from "@rsbuild/plugin-react";
|
|
26
|
+
import { pluginCssDoctor } from "@zealakit/css-doctor-rsbuild";
|
|
27
|
+
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
plugins: [
|
|
30
|
+
pluginReact(),
|
|
31
|
+
pluginCssDoctor({
|
|
32
|
+
pageCoverage: { enabled: true, minPageRatio: 0.5 },
|
|
33
|
+
sizeBudget: { enabled: true, maxChunkSize: 40 * 1024, isolateVariables: true },
|
|
34
|
+
report: true,
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Options
|
|
41
|
+
|
|
42
|
+
| Option | Default | Description |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `pageCoverage.enabled` | `true` | Enable page/class coverage classification. |
|
|
45
|
+
| `pageCoverage.minPageRatio` | `0.5` | Minimum ratio (0.0-1.0) for the common chunk. |
|
|
46
|
+
| `pageCoverage.forceCommon` | `[]` | Selectors or classes forced into common CSS. |
|
|
47
|
+
| `sizeBudget.enabled` | `true` | Enable byte-budget splitting. |
|
|
48
|
+
| `sizeBudget.maxChunkSize` | `60 * 1024` | Maximum bytes per emitted chunk. |
|
|
49
|
+
| `sizeBudget.isolateVariables` | `true` | Extract `:root` and `@theme` variables first. |
|
|
50
|
+
| `minify` | production | Minify emitted CSS with LightningCSS. |
|
|
51
|
+
| `report` | `true` | Print coverage and chunk diagnostics. |
|
|
52
|
+
| `exclude` | CSS Modules safety | Skip matching CSS assets. |
|
|
53
|
+
|
|
54
|
+
## Safety and limitations
|
|
55
|
+
|
|
56
|
+
- Production build only; works with the Rspack bundler.
|
|
57
|
+
- Peer range @rsbuild/core >=1 remains broad.
|
|
58
|
+
- CSS Modules and scoped framework styles are left untouched.
|
|
59
|
+
|
|
60
|
+
## Full documentation
|
|
61
|
+
|
|
62
|
+
Project README: [English](https://github.com/zealakit/css-doctor/blob/main/README.md) | [Tiếng Việt](https://github.com/zealakit/css-doctor/blob/main/README_VI.md) | [简体中文](https://github.com/zealakit/css-doctor/blob/main/README_CN.md)
|
package/README_CN.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @zealakit/css-doctor-rsbuild
|
|
4
|
+
|
|
5
|
+
**用于页面覆盖率与体积预算 CSS 分割的 Rsbuild/Rspack 集成。**
|
|
6
|
+
|
|
7
|
+
[English](https://github.com/zealakit/css-doctor/blob/main/packages/css-doctor-rsbuild/README.md) | [Tiếng Việt](https://github.com/zealakit/css-doctor/blob/main/packages/css-doctor-rsbuild/README_VI.md) | [简体中文](https://github.com/zealakit/css-doctor/blob/main/packages/css-doctor-rsbuild/README_CN.md)
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add -D @zealakit/css-doctor-rsbuild
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 使用
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { defineConfig } from "@rsbuild/core";
|
|
25
|
+
import { pluginReact } from "@rsbuild/plugin-react";
|
|
26
|
+
import { pluginCssDoctor } from "@zealakit/css-doctor-rsbuild";
|
|
27
|
+
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
plugins: [
|
|
30
|
+
pluginReact(),
|
|
31
|
+
pluginCssDoctor({
|
|
32
|
+
pageCoverage: { enabled: true, minPageRatio: 0.5 },
|
|
33
|
+
sizeBudget: { enabled: true, maxChunkSize: 40 * 1024, isolateVariables: true },
|
|
34
|
+
report: true,
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 配置
|
|
41
|
+
|
|
42
|
+
| 参数 | 默认值 | 说明 |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `pageCoverage.enabled` | `true` | 启用页面/class 覆盖率分类。 |
|
|
45
|
+
| `pageCoverage.minPageRatio` | `0.5` | 进入 common chunk 的最小比例(0.0-1.0)。 |
|
|
46
|
+
| `pageCoverage.forceCommon` | `[]` | 强制保留在 common CSS 中的 selector/class。 |
|
|
47
|
+
| `sizeBudget.enabled` | `true` | 启用按字节预算分割。 |
|
|
48
|
+
| `sizeBudget.maxChunkSize` | `60 * 1024` | 每个 chunk 的最大字节数。 |
|
|
49
|
+
| `sizeBudget.isolateVariables` | `true` | 先提取 `:root` 与 `@theme` 变量。 |
|
|
50
|
+
| `minify` | production | 使用 LightningCSS 压缩输出 CSS。 |
|
|
51
|
+
| `report` | `true` | 输出覆盖率和 chunk 诊断。 |
|
|
52
|
+
| `exclude` | CSS Modules safety | 跳过匹配的 CSS 资源。 |
|
|
53
|
+
|
|
54
|
+
## 安全与限制
|
|
55
|
+
|
|
56
|
+
- 仅用于生产构建;适用于 Rspack bundler。
|
|
57
|
+
- peer 范围 @rsbuild/core >=1 保持宽泛。
|
|
58
|
+
- 不会修改 CSS Modules 与框架 scoped styles。
|
|
59
|
+
|
|
60
|
+
## 完整文档
|
|
61
|
+
|
|
62
|
+
项目 README:[English](https://github.com/zealakit/css-doctor/blob/main/README.md) | [Tiếng Việt](https://github.com/zealakit/css-doctor/blob/main/README_VI.md) | [简体中文](https://github.com/zealakit/css-doctor/blob/main/README_CN.md)
|
package/README_VI.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @zealakit/css-doctor-rsbuild
|
|
4
|
+
|
|
5
|
+
**Tích hợp Rsbuild/Rspack phân tách CSS theo page coverage và size budget.**
|
|
6
|
+
|
|
7
|
+
[English](https://github.com/zealakit/css-doctor/blob/main/packages/css-doctor-rsbuild/README.md) | [Tiếng Việt](https://github.com/zealakit/css-doctor/blob/main/packages/css-doctor-rsbuild/README_VI.md) | [简体中文](https://github.com/zealakit/css-doctor/blob/main/packages/css-doctor-rsbuild/README_CN.md)
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## Cài đặt
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add -D @zealakit/css-doctor-rsbuild
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Sử dụng
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { defineConfig } from "@rsbuild/core";
|
|
25
|
+
import { pluginReact } from "@rsbuild/plugin-react";
|
|
26
|
+
import { pluginCssDoctor } from "@zealakit/css-doctor-rsbuild";
|
|
27
|
+
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
plugins: [
|
|
30
|
+
pluginReact(),
|
|
31
|
+
pluginCssDoctor({
|
|
32
|
+
pageCoverage: { enabled: true, minPageRatio: 0.5 },
|
|
33
|
+
sizeBudget: { enabled: true, maxChunkSize: 40 * 1024, isolateVariables: true },
|
|
34
|
+
report: true,
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Tùy chọn
|
|
41
|
+
|
|
42
|
+
| Tùy chọn | Mặc định | Mô tả |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `pageCoverage.enabled` | `true` | Bật phân loại page/class coverage. |
|
|
45
|
+
| `pageCoverage.minPageRatio` | `0.5` | Tỷ lệ tối thiểu (0.0-1.0) để vào common chunk. |
|
|
46
|
+
| `pageCoverage.forceCommon` | `[]` | Selector/class luôn giữ trong common CSS. |
|
|
47
|
+
| `sizeBudget.enabled` | `true` | Bật chia nhỏ theo byte budget. |
|
|
48
|
+
| `sizeBudget.maxChunkSize` | `60 * 1024` | Số byte tối đa mỗi chunk. |
|
|
49
|
+
| `sizeBudget.isolateVariables` | `true` | Tách biến `:root` và `@theme` trước. |
|
|
50
|
+
| `minify` | production | Minify CSS bằng LightningCSS. |
|
|
51
|
+
| `report` | `true` | In chẩn đoán coverage và chunk. |
|
|
52
|
+
| `exclude` | CSS Modules safety | Bỏ qua asset CSS khớp mẫu. |
|
|
53
|
+
|
|
54
|
+
## An toàn và giới hạn
|
|
55
|
+
|
|
56
|
+
- Chỉ production build; hoạt động với Rspack bundler.
|
|
57
|
+
- Peer range @rsbuild/core >=1 được giữ rộng.
|
|
58
|
+
- CSS Modules và scoped styles của framework không bị động tới.
|
|
59
|
+
|
|
60
|
+
## Tài liệu đầy đủ
|
|
61
|
+
|
|
62
|
+
README dự án: [English](https://github.com/zealakit/css-doctor/blob/main/README.md) | [Tiếng Việt](https://github.com/zealakit/css-doctor/blob/main/README_VI.md) | [简体中文](https://github.com/zealakit/css-doctor/blob/main/README_CN.md)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { RsbuildPlugin } from '@rsbuild/core';
|
|
2
|
+
import { JsSelectorStat } from '@zealakit/css-doctor-binding';
|
|
3
|
+
|
|
4
|
+
/** Tier 1: semantic page-coverage splitting. */
|
|
5
|
+
type PageCoverageOptions = {
|
|
6
|
+
/** Enable page-coverage splitting. @default true */
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
/** Minimum page coverage for common rules. Range: 0..1. @default 0.5 */
|
|
9
|
+
minPageRatio?: number;
|
|
10
|
+
/** Selectors or classes that always stay in the common chunk. */
|
|
11
|
+
forceCommon?: string[];
|
|
12
|
+
};
|
|
13
|
+
/** Tier 2: physical size-budget splitting. */
|
|
14
|
+
type SizeBudgetOptions = {
|
|
15
|
+
/** Enable size-budget splitting. @default true */
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
/** Maximum serialized bytes per body chunk. @default 60 * 1024 */
|
|
18
|
+
maxChunkSize?: number;
|
|
19
|
+
/** Isolate CSS variables before splitting. @default true */
|
|
20
|
+
isolateVariables?: boolean;
|
|
21
|
+
};
|
|
22
|
+
/** Configuration for the Rsbuild integration. */
|
|
23
|
+
type CssDoctorRsbuildOptions = {
|
|
24
|
+
/** Tier 1 configuration. */
|
|
25
|
+
pageCoverage?: PageCoverageOptions;
|
|
26
|
+
/** Tier 2 configuration. */
|
|
27
|
+
sizeBudget?: SizeBudgetOptions;
|
|
28
|
+
/** Minify emitted CSS. Defaults to true for production builds. */
|
|
29
|
+
minify?: boolean;
|
|
30
|
+
/** Print the diagnostics summary. @default true */
|
|
31
|
+
report?: boolean;
|
|
32
|
+
/** Only process CSS assets whose name matches these patterns. */
|
|
33
|
+
include?: Array<string | RegExp>;
|
|
34
|
+
/** Skip CSS assets whose name matches these patterns. */
|
|
35
|
+
exclude?: Array<string | RegExp>;
|
|
36
|
+
};
|
|
37
|
+
/** One final CSS chunk produced by either pipeline tier. */
|
|
38
|
+
type ChunkInfo = {
|
|
39
|
+
name: string;
|
|
40
|
+
size: number;
|
|
41
|
+
isVariableChunk?: boolean;
|
|
42
|
+
isPartChunk?: boolean;
|
|
43
|
+
};
|
|
44
|
+
/** Compact result summary used by the reporter and diagnostics. */
|
|
45
|
+
type OptimizationSummary = {
|
|
46
|
+
totalOriginalSize: number;
|
|
47
|
+
totalOptimizedSize: number;
|
|
48
|
+
deadCodeSelectorsCount: number;
|
|
49
|
+
chunks: ChunkInfo[];
|
|
50
|
+
};
|
|
51
|
+
/** One final CSS chunk with Rsbuild-specific metadata. */
|
|
52
|
+
type EmittedCssChunk = ChunkInfo & {
|
|
53
|
+
fileName: string;
|
|
54
|
+
sourceChunk: string;
|
|
55
|
+
kind: "common" | "page" | "variables" | "part";
|
|
56
|
+
page?: string;
|
|
57
|
+
embedded: boolean;
|
|
58
|
+
oversized: boolean;
|
|
59
|
+
splitDueToSize: boolean;
|
|
60
|
+
};
|
|
61
|
+
/** A page-specific CSS file emitted by the plugin. */
|
|
62
|
+
type EmittedPageChunk = {
|
|
63
|
+
page: string;
|
|
64
|
+
fileName: string;
|
|
65
|
+
sizeBytes: number;
|
|
66
|
+
};
|
|
67
|
+
/** Tier 2 summary for the diagnostics report. */
|
|
68
|
+
type SizeBudgetDiagnostics = {
|
|
69
|
+
enabled: boolean;
|
|
70
|
+
maxChunkSize: number;
|
|
71
|
+
isolateVariables: boolean;
|
|
72
|
+
splitChunks: number;
|
|
73
|
+
oversizedChunks: number;
|
|
74
|
+
};
|
|
75
|
+
/** Analysis metadata for one CSS asset. */
|
|
76
|
+
type CssAssetDiagnostic = {
|
|
77
|
+
fileName: string;
|
|
78
|
+
sizeBeforeBytes: number;
|
|
79
|
+
sizeAfterBytes: number;
|
|
80
|
+
selectorCount: number;
|
|
81
|
+
ruleCount: number;
|
|
82
|
+
associatedPages: string[];
|
|
83
|
+
deadCodeCount: number;
|
|
84
|
+
error?: string;
|
|
85
|
+
};
|
|
86
|
+
/** Complete diagnostics emitted by the Rsbuild plugin. */
|
|
87
|
+
type CssDoctorRsbuildDiagnostics = OptimizationSummary & {
|
|
88
|
+
plugin: "css-doctor:rsbuild";
|
|
89
|
+
generatedAt: string;
|
|
90
|
+
durationMs: number;
|
|
91
|
+
minify: boolean;
|
|
92
|
+
pages: string[];
|
|
93
|
+
pageUsage: Record<string, number>;
|
|
94
|
+
splitApplied: boolean;
|
|
95
|
+
totalCssAssets: number;
|
|
96
|
+
analyzedCssAssets: number;
|
|
97
|
+
skippedCssAssets: number;
|
|
98
|
+
failedCssAssets: number;
|
|
99
|
+
totalSelectors: number;
|
|
100
|
+
totalRules: number;
|
|
101
|
+
commonCssBytes: number;
|
|
102
|
+
pageSpecificCssBytes: number;
|
|
103
|
+
emittedPageChunks: EmittedPageChunk[];
|
|
104
|
+
emittedCssChunks: EmittedCssChunk[];
|
|
105
|
+
sizeBudget: SizeBudgetDiagnostics;
|
|
106
|
+
usageMatrix: JsSelectorStat[];
|
|
107
|
+
files: CssAssetDiagnostic[];
|
|
108
|
+
errors: string[];
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Adds the css-doctor Rspack plugin to production Rsbuild builds.
|
|
113
|
+
*/
|
|
114
|
+
declare const pluginCssDoctor: (options?: CssDoctorRsbuildOptions) => RsbuildPlugin;
|
|
115
|
+
|
|
116
|
+
export { type ChunkInfo, type CssAssetDiagnostic, type CssDoctorRsbuildDiagnostics, type CssDoctorRsbuildOptions, type EmittedCssChunk, type EmittedPageChunk, type OptimizationSummary, type PageCoverageOptions, type SizeBudgetDiagnostics, type SizeBudgetOptions, pluginCssDoctor };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
// src/rspack-plugin.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import { posix } from "path";
|
|
5
|
+
import { performance } from "perf_hooks";
|
|
6
|
+
import { analyzeCss, optimizeCss } from "@zealakit/css-doctor-binding";
|
|
7
|
+
|
|
8
|
+
// src/reporter.ts
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
function printReport(diagnostics) {
|
|
11
|
+
const status = diagnostics.minify ? pc.green("Minified: YES") : pc.yellow("Minified: NO");
|
|
12
|
+
const lines = [
|
|
13
|
+
"",
|
|
14
|
+
`${pc.cyan(pc.bold("[css-doctor]"))} ${pc.bold("Rsbuild")} ${pc.dim("\xB7")} ${diagnostics.pages.length} pages ${pc.dim("\xB7")} ${diagnostics.analyzedCssAssets}/${diagnostics.totalCssAssets} CSS assets ${pc.dim("\xB7")} ${formatBytes(
|
|
15
|
+
diagnostics.totalOriginalSize
|
|
16
|
+
)} ${pc.dim("\u2192")} ${formatBytes(diagnostics.totalOptimizedSize)} ${pc.dim(
|
|
17
|
+
"\xB7"
|
|
18
|
+
)} ${formatDuration(diagnostics.durationMs)} ${pc.dim("\xB7")} ${status}`
|
|
19
|
+
];
|
|
20
|
+
for (const file of diagnostics.files) {
|
|
21
|
+
if (file.error) {
|
|
22
|
+
lines.push(
|
|
23
|
+
` ${pc.red("\u2717")} ${pc.magenta(file.fileName)} ${pc.red(file.error)}`
|
|
24
|
+
);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
lines.push(
|
|
28
|
+
` ${pc.green("\u2713")} ${pc.magenta(file.fileName)} ${pc.dim(
|
|
29
|
+
`${formatBytes(file.sizeBeforeBytes)} \u2192 ${formatBytes(
|
|
30
|
+
file.sizeAfterBytes
|
|
31
|
+
)}`
|
|
32
|
+
)} ${pc.dim("\xB7")} ${file.selectorCount} selectors ${pc.dim(
|
|
33
|
+
"\xB7"
|
|
34
|
+
)} pages: ${file.associatedPages.join(", ") || "none"}`
|
|
35
|
+
);
|
|
36
|
+
lines.push(
|
|
37
|
+
` split common ${formatBytes(
|
|
38
|
+
file.sizeAfterBytes
|
|
39
|
+
)} ${pc.dim("\xB7")} dead ${file.deadCodeCount}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (diagnostics.emittedPageChunks.length > 0) {
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push(
|
|
45
|
+
` ${pc.bold(`Emitted page chunks (${diagnostics.emittedPageChunks.length})`)}`
|
|
46
|
+
);
|
|
47
|
+
for (const chunk of diagnostics.emittedPageChunks) {
|
|
48
|
+
lines.push(
|
|
49
|
+
` ${pc.magenta(chunk.fileName)} ${pc.dim(formatBytes(chunk.sizeBytes))}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push(
|
|
55
|
+
` ${pc.bold("Size budget")} ${pc.dim(
|
|
56
|
+
`(max ${formatBytes(diagnostics.sizeBudget.maxChunkSize)} \xB7 variables ${diagnostics.sizeBudget.isolateVariables ? "isolated" : "inline"})`
|
|
57
|
+
)}`
|
|
58
|
+
);
|
|
59
|
+
if (diagnostics.emittedCssChunks.length === 0) {
|
|
60
|
+
lines.push(` ${pc.dim("No CSS chunks were emitted.")}`);
|
|
61
|
+
} else {
|
|
62
|
+
for (const chunk of diagnostics.emittedCssChunks) {
|
|
63
|
+
const markers = [];
|
|
64
|
+
if (chunk.isVariableChunk) {
|
|
65
|
+
markers.push("vars");
|
|
66
|
+
}
|
|
67
|
+
if (chunk.isPartChunk) {
|
|
68
|
+
markers.push("part");
|
|
69
|
+
}
|
|
70
|
+
if (chunk.embedded) {
|
|
71
|
+
markers.push("embedded");
|
|
72
|
+
}
|
|
73
|
+
if (chunk.splitDueToSize) {
|
|
74
|
+
markers.push(pc.yellow("split"));
|
|
75
|
+
}
|
|
76
|
+
if (chunk.oversized) {
|
|
77
|
+
markers.push(pc.red("oversized"));
|
|
78
|
+
}
|
|
79
|
+
lines.push(
|
|
80
|
+
` ${pc.magenta(chunk.fileName)} ${pc.dim(
|
|
81
|
+
formatBytes(chunk.size)
|
|
82
|
+
)}${markers.length > 0 ? ` ${pc.dim("\xB7")} ${markers.join(" \xB7 ")}` : ""}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (diagnostics.deadCodeSelectorsCount > 0) {
|
|
87
|
+
lines.push("");
|
|
88
|
+
lines.push(
|
|
89
|
+
` ${pc.yellow(
|
|
90
|
+
`\u26A0 Dead code eliminated: ${diagnostics.deadCodeSelectorsCount} selector occurrences`
|
|
91
|
+
)}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (diagnostics.errors.length > 0) {
|
|
95
|
+
lines.push("");
|
|
96
|
+
for (const error of diagnostics.errors) {
|
|
97
|
+
lines.push(` ${pc.red(`\u2717 ${error}`)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
console.log(lines.join("\n"));
|
|
101
|
+
}
|
|
102
|
+
function formatDuration(milliseconds) {
|
|
103
|
+
if (milliseconds < 1) {
|
|
104
|
+
return "<1 ms";
|
|
105
|
+
}
|
|
106
|
+
return `${milliseconds.toFixed(1)} ms`;
|
|
107
|
+
}
|
|
108
|
+
function formatBytes(bytes) {
|
|
109
|
+
if (bytes < 1024) {
|
|
110
|
+
return `${bytes} B`;
|
|
111
|
+
}
|
|
112
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/rspack-plugin.ts
|
|
116
|
+
var PLUGIN_NAME = "css-doctor:rsbuild";
|
|
117
|
+
var DEFAULT_MIN_PAGE_RATIO = 0.5;
|
|
118
|
+
var DEFAULT_MAX_CHUNK_SIZE = 60 * 1024;
|
|
119
|
+
var CssDoctorRspackPlugin = class {
|
|
120
|
+
options;
|
|
121
|
+
constructor(options = {}) {
|
|
122
|
+
const minPageRatio = options.pageCoverage?.minPageRatio ?? DEFAULT_MIN_PAGE_RATIO;
|
|
123
|
+
const maxChunkSize = options.sizeBudget?.maxChunkSize ?? DEFAULT_MAX_CHUNK_SIZE;
|
|
124
|
+
if (!Number.isFinite(minPageRatio) || minPageRatio < 0 || minPageRatio > 1) {
|
|
125
|
+
throw new RangeError(
|
|
126
|
+
"css-doctor: minPageRatio must be a number between 0 and 1"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (!Number.isInteger(maxChunkSize) || maxChunkSize <= 0) {
|
|
130
|
+
throw new RangeError(
|
|
131
|
+
"css-doctor: maxChunkSize must be a positive integer"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
this.options = {
|
|
135
|
+
pageCoverage: {
|
|
136
|
+
enabled: options.pageCoverage?.enabled ?? true,
|
|
137
|
+
minPageRatio,
|
|
138
|
+
forceCommon: options.pageCoverage?.forceCommon ?? []
|
|
139
|
+
},
|
|
140
|
+
sizeBudget: {
|
|
141
|
+
enabled: options.sizeBudget?.enabled ?? true,
|
|
142
|
+
maxChunkSize,
|
|
143
|
+
isolateVariables: options.sizeBudget?.isolateVariables ?? true
|
|
144
|
+
},
|
|
145
|
+
minify: options.minify ?? true,
|
|
146
|
+
report: options.report ?? true,
|
|
147
|
+
include: options.include ?? [],
|
|
148
|
+
exclude: options.exclude ?? []
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
apply(compiler) {
|
|
152
|
+
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
|
153
|
+
compilation.hooks.processAssets.tapPromise(
|
|
154
|
+
{
|
|
155
|
+
name: PLUGIN_NAME,
|
|
156
|
+
stage: compiler.rspack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE
|
|
157
|
+
},
|
|
158
|
+
async () => {
|
|
159
|
+
try {
|
|
160
|
+
await this.process(compiler, compilation);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
compilation.getLogger(PLUGIN_NAME).warn(
|
|
163
|
+
`[css-doctor] diagnostics failed without interrupting the build: ${describeError(
|
|
164
|
+
error
|
|
165
|
+
)}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
async process(compiler, compilation) {
|
|
173
|
+
const startedAt = performance.now();
|
|
174
|
+
const logger = compilation.getLogger(PLUGIN_NAME);
|
|
175
|
+
const pagesResult = await collectPages(compilation);
|
|
176
|
+
const pageNames = pagesResult.pages.map((page) => page.name);
|
|
177
|
+
const allPages = new Set(pageNames);
|
|
178
|
+
const pageUsage = {};
|
|
179
|
+
const pageUsageCounts = {};
|
|
180
|
+
for (const page of pagesResult.pages) {
|
|
181
|
+
const classes = [...page.classes].sort();
|
|
182
|
+
pageUsage[page.name] = classes;
|
|
183
|
+
pageUsageCounts[page.name] = classes.length;
|
|
184
|
+
}
|
|
185
|
+
const hasPageClasses = pagesResult.pages.some(
|
|
186
|
+
(page) => page.classes.size > 0
|
|
187
|
+
);
|
|
188
|
+
const pageCoverageApplied = this.options.pageCoverage.enabled && hasPageClasses;
|
|
189
|
+
const diagnostics = {
|
|
190
|
+
plugin: PLUGIN_NAME,
|
|
191
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
192
|
+
durationMs: 0,
|
|
193
|
+
totalOriginalSize: 0,
|
|
194
|
+
totalOptimizedSize: 0,
|
|
195
|
+
deadCodeSelectorsCount: 0,
|
|
196
|
+
chunks: [],
|
|
197
|
+
minify: this.options.minify,
|
|
198
|
+
pages: pageNames,
|
|
199
|
+
pageUsage: pageUsageCounts,
|
|
200
|
+
splitApplied: pageCoverageApplied,
|
|
201
|
+
totalCssAssets: 0,
|
|
202
|
+
analyzedCssAssets: 0,
|
|
203
|
+
skippedCssAssets: 0,
|
|
204
|
+
failedCssAssets: 0,
|
|
205
|
+
totalSelectors: 0,
|
|
206
|
+
totalRules: 0,
|
|
207
|
+
commonCssBytes: 0,
|
|
208
|
+
pageSpecificCssBytes: 0,
|
|
209
|
+
emittedPageChunks: [],
|
|
210
|
+
emittedCssChunks: [],
|
|
211
|
+
sizeBudget: {
|
|
212
|
+
enabled: this.options.sizeBudget.enabled,
|
|
213
|
+
maxChunkSize: this.options.sizeBudget.maxChunkSize,
|
|
214
|
+
isolateVariables: this.options.sizeBudget.isolateVariables,
|
|
215
|
+
splitChunks: 0,
|
|
216
|
+
oversizedChunks: 0
|
|
217
|
+
},
|
|
218
|
+
usageMatrix: [],
|
|
219
|
+
files: [],
|
|
220
|
+
errors: []
|
|
221
|
+
};
|
|
222
|
+
const matrix = /* @__PURE__ */ new Map();
|
|
223
|
+
const pageFiles = /* @__PURE__ */ new Map();
|
|
224
|
+
const usedAssetNames = new Set(
|
|
225
|
+
compilation.getAssets().map((asset) => asset.name)
|
|
226
|
+
);
|
|
227
|
+
const RawSource = compiler.rspack.sources.RawSource;
|
|
228
|
+
const emitCssChunk = (chunk) => {
|
|
229
|
+
const hash = createHash("sha256").update(chunk.css).digest("hex").slice(0, 8);
|
|
230
|
+
const safeName = chunk.name.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "chunk";
|
|
231
|
+
let fileName = `static/css/${safeName}.${hash}.css`;
|
|
232
|
+
let index = 2;
|
|
233
|
+
while (usedAssetNames.has(fileName)) {
|
|
234
|
+
fileName = `static/css/${safeName}.${hash}-${index}.css`;
|
|
235
|
+
index += 1;
|
|
236
|
+
}
|
|
237
|
+
compilation.emitAsset(fileName, new RawSource(chunk.css));
|
|
238
|
+
usedAssetNames.add(fileName);
|
|
239
|
+
return fileName;
|
|
240
|
+
};
|
|
241
|
+
for (const asset of compilation.getAssets()) {
|
|
242
|
+
if (!asset.name.endsWith(".css")) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
diagnostics.totalCssAssets += 1;
|
|
246
|
+
if (this.shouldSkipAsset(asset.name, sourceToString(asset.source.source()))) {
|
|
247
|
+
diagnostics.skippedCssAssets += 1;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const associatedPages = [
|
|
251
|
+
...pagesResult.pagesByCss.get(asset.name) ?? allPages
|
|
252
|
+
].sort();
|
|
253
|
+
const originalCss = sourceToString(asset.source.source());
|
|
254
|
+
const file = createDiagnostic(asset.name, originalCss, associatedPages);
|
|
255
|
+
try {
|
|
256
|
+
const analysis = analyzeCss(originalCss);
|
|
257
|
+
file.selectorCount = analysis.stats.selectorCount;
|
|
258
|
+
file.ruleCount = analysis.stats.ruleCount;
|
|
259
|
+
const moduleClasses = collectCssModuleClasses(originalCss);
|
|
260
|
+
const assetPageUsage = moduleClasses.length > 0 ? withCommonClasses(pageUsage, moduleClasses) : pageUsage;
|
|
261
|
+
const pipeline = optimizeCss({
|
|
262
|
+
cssContent: originalCss,
|
|
263
|
+
pageUsage: assetPageUsage,
|
|
264
|
+
pageCoverage: {
|
|
265
|
+
enabled: pageCoverageApplied,
|
|
266
|
+
minPageRatio: this.options.pageCoverage.minPageRatio,
|
|
267
|
+
forceCommon: this.options.pageCoverage.forceCommon
|
|
268
|
+
},
|
|
269
|
+
sizeBudget: {
|
|
270
|
+
enabled: this.options.sizeBudget.enabled,
|
|
271
|
+
maxChunkSize: this.options.sizeBudget.maxChunkSize,
|
|
272
|
+
isolateVariables: this.options.sizeBudget.isolateVariables
|
|
273
|
+
},
|
|
274
|
+
minify: this.options.minify
|
|
275
|
+
});
|
|
276
|
+
const commonChunks = pipeline.chunks.filter(
|
|
277
|
+
(chunk) => chunk.page === void 0 && chunk.sourceChunk === "common"
|
|
278
|
+
);
|
|
279
|
+
const pageChunks = pipeline.chunks.filter(
|
|
280
|
+
(chunk) => chunk.page !== void 0 && (associatedPages.includes(chunk.page) || !pagesResult.pagesByCss.has(asset.name))
|
|
281
|
+
);
|
|
282
|
+
const commonWasSplit = commonChunks.length > 1 || commonChunks.some((chunk) => chunk.splitDueToSize);
|
|
283
|
+
const embeddedCommon = commonWasSplit ? void 0 : commonChunks[0];
|
|
284
|
+
const commonFileNames = [];
|
|
285
|
+
let commonSize = 0;
|
|
286
|
+
for (const chunk of commonChunks) {
|
|
287
|
+
const embedded = chunk === embeddedCommon;
|
|
288
|
+
const fileName = embedded ? asset.name : emitCssChunk(chunk);
|
|
289
|
+
commonSize += chunk.sizeBytes;
|
|
290
|
+
commonFileNames.push(fileName);
|
|
291
|
+
diagnostics.emittedCssChunks.push(
|
|
292
|
+
toEmittedChunk(chunk, fileName, embedded)
|
|
293
|
+
);
|
|
294
|
+
if (chunk.splitDueToSize) {
|
|
295
|
+
diagnostics.sizeBudget.splitChunks += 1;
|
|
296
|
+
}
|
|
297
|
+
if (chunk.oversized) {
|
|
298
|
+
diagnostics.sizeBudget.oversizedChunks += 1;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (commonWasSplit) {
|
|
302
|
+
const imports = commonFileNames.map((fileName) => {
|
|
303
|
+
const relativePath = posix.relative(
|
|
304
|
+
posix.dirname(asset.name),
|
|
305
|
+
fileName
|
|
306
|
+
);
|
|
307
|
+
return `@import "./${relativePath}";`;
|
|
308
|
+
}).join("\n");
|
|
309
|
+
compilation.updateAsset(asset.name, new RawSource(imports));
|
|
310
|
+
} else {
|
|
311
|
+
compilation.updateAsset(
|
|
312
|
+
asset.name,
|
|
313
|
+
new RawSource(embeddedCommon?.css ?? "")
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
let pageSpecificSize = 0;
|
|
317
|
+
for (const chunk of pageChunks) {
|
|
318
|
+
if (!chunk.page) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const fileName = emitCssChunk(chunk);
|
|
322
|
+
pageSpecificSize += chunk.sizeBytes;
|
|
323
|
+
const files = pageFiles.get(chunk.page) ?? [];
|
|
324
|
+
files.push(fileName);
|
|
325
|
+
pageFiles.set(chunk.page, files);
|
|
326
|
+
diagnostics.emittedPageChunks.push({
|
|
327
|
+
page: chunk.page,
|
|
328
|
+
fileName,
|
|
329
|
+
sizeBytes: chunk.sizeBytes
|
|
330
|
+
});
|
|
331
|
+
diagnostics.emittedCssChunks.push(
|
|
332
|
+
toEmittedChunk(chunk, fileName, false)
|
|
333
|
+
);
|
|
334
|
+
if (chunk.splitDueToSize) {
|
|
335
|
+
diagnostics.sizeBudget.splitChunks += 1;
|
|
336
|
+
}
|
|
337
|
+
if (chunk.oversized) {
|
|
338
|
+
diagnostics.sizeBudget.oversizedChunks += 1;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
file.sizeAfterBytes = commonWasSplit ? commonSize : Buffer.byteLength(embeddedCommon?.css ?? "", "utf8");
|
|
342
|
+
file.deadCodeCount = pipeline.deadCodeCount;
|
|
343
|
+
diagnostics.deadCodeSelectorsCount += pipeline.deadCodeCount;
|
|
344
|
+
diagnostics.commonCssBytes += commonSize;
|
|
345
|
+
diagnostics.pageSpecificCssBytes += pageSpecificSize;
|
|
346
|
+
diagnostics.totalOriginalSize += file.sizeBeforeBytes;
|
|
347
|
+
diagnostics.totalOptimizedSize += commonSize + pageSpecificSize;
|
|
348
|
+
for (const stat of pipeline.stats) {
|
|
349
|
+
const previous = matrix.get(stat.selector);
|
|
350
|
+
if (!previous || stat.totalOccurrences > previous.totalOccurrences) {
|
|
351
|
+
matrix.set(stat.selector, stat);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} catch (error) {
|
|
355
|
+
file.error = describeError(error);
|
|
356
|
+
diagnostics.failedCssAssets += 1;
|
|
357
|
+
diagnostics.errors.push(`${asset.name}: ${file.error}`);
|
|
358
|
+
logger.warn(`[css-doctor] ${asset.name}: ${file.error}`);
|
|
359
|
+
}
|
|
360
|
+
diagnostics.analyzedCssAssets += 1;
|
|
361
|
+
diagnostics.totalSelectors += file.selectorCount;
|
|
362
|
+
diagnostics.totalRules += file.ruleCount;
|
|
363
|
+
diagnostics.files.push(file);
|
|
364
|
+
}
|
|
365
|
+
if (pageFiles.size > 0) {
|
|
366
|
+
injectPageCssLoaders(
|
|
367
|
+
compilation,
|
|
368
|
+
pageFiles,
|
|
369
|
+
new Map(pagesResult.pages.map((page) => [page.name, page])),
|
|
370
|
+
RawSource
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
diagnostics.chunks = diagnostics.emittedCssChunks.map(toChunkInfo);
|
|
374
|
+
diagnostics.usageMatrix = [...matrix.values()].sort(
|
|
375
|
+
(left, right) => left.selector.localeCompare(right.selector)
|
|
376
|
+
);
|
|
377
|
+
diagnostics.durationMs = roundMs(performance.now() - startedAt);
|
|
378
|
+
if (this.options.report && (diagnostics.totalCssAssets > 0 || diagnostics.errors.length > 0)) {
|
|
379
|
+
try {
|
|
380
|
+
printReport(diagnostics);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
logger.warn(`[css-doctor] reporter failed: ${describeError(error)}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
shouldSkipAsset(assetName, source) {
|
|
387
|
+
if (/\.module\.(css|scss|sass|less|styl|stylus)$/i.test(assetName)) {
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
if (/\.[A-Za-z0-9_-]+_[A-Za-z0-9_-]+__[A-Za-z0-9_-]+/.test(source)) {
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
if (hasRsbuildCssModuleClass(source)) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
if (this.options.include.length > 0) {
|
|
397
|
+
if (!matchesPatterns(assetName, this.options.include)) {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return this.options.exclude.length > 0 && matchesPatterns(assetName, this.options.exclude);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
function createDiagnostic(fileName, css, associatedPages) {
|
|
405
|
+
return {
|
|
406
|
+
fileName,
|
|
407
|
+
sizeBeforeBytes: Buffer.byteLength(css, "utf8"),
|
|
408
|
+
sizeAfterBytes: Buffer.byteLength(css, "utf8"),
|
|
409
|
+
selectorCount: 0,
|
|
410
|
+
ruleCount: 0,
|
|
411
|
+
associatedPages,
|
|
412
|
+
deadCodeCount: 0
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function toEmittedChunk(chunk, fileName, embedded) {
|
|
416
|
+
return {
|
|
417
|
+
name: chunk.name,
|
|
418
|
+
fileName,
|
|
419
|
+
sourceChunk: chunk.sourceChunk,
|
|
420
|
+
kind: chunk.kind,
|
|
421
|
+
page: chunk.page,
|
|
422
|
+
size: chunk.sizeBytes,
|
|
423
|
+
isVariableChunk: chunk.variables,
|
|
424
|
+
isPartChunk: chunk.part !== void 0,
|
|
425
|
+
embedded,
|
|
426
|
+
oversized: chunk.oversized,
|
|
427
|
+
splitDueToSize: chunk.splitDueToSize
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function toChunkInfo(chunk) {
|
|
431
|
+
return {
|
|
432
|
+
name: chunk.name,
|
|
433
|
+
size: chunk.size,
|
|
434
|
+
isVariableChunk: chunk.isVariableChunk,
|
|
435
|
+
isPartChunk: chunk.isPartChunk
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
async function collectPages(compilation) {
|
|
439
|
+
const pages = [];
|
|
440
|
+
const pagesByCss = /* @__PURE__ */ new Map();
|
|
441
|
+
const sourceCache = /* @__PURE__ */ new Map();
|
|
442
|
+
const entrypoints = compilation.entrypoints;
|
|
443
|
+
for (const [entryName, entry] of entrypoints) {
|
|
444
|
+
const pageName = normalizePageName(entryName);
|
|
445
|
+
const page = {
|
|
446
|
+
name: pageName,
|
|
447
|
+
classes: /* @__PURE__ */ new Set(),
|
|
448
|
+
cssFiles: /* @__PURE__ */ new Set(),
|
|
449
|
+
jsFiles: /* @__PURE__ */ new Set()
|
|
450
|
+
};
|
|
451
|
+
const files = entryFiles(entry);
|
|
452
|
+
for (const file of files) {
|
|
453
|
+
if (file.endsWith(".css")) {
|
|
454
|
+
if (!/\.module\.(css|scss|sass|less|styl|stylus)$/i.test(file)) {
|
|
455
|
+
page.cssFiles.add(file);
|
|
456
|
+
}
|
|
457
|
+
} else if (file.endsWith(".js")) {
|
|
458
|
+
page.jsFiles.add(file);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
for (const chunk of entry.chunks ?? []) {
|
|
462
|
+
for (const file of chunk.files ?? []) {
|
|
463
|
+
if (file.endsWith(".css")) {
|
|
464
|
+
page.cssFiles.add(file);
|
|
465
|
+
} else if (file.endsWith(".js")) {
|
|
466
|
+
page.jsFiles.add(file);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
for (const module of chunkModules(compilation, chunk)) {
|
|
470
|
+
await scanModule(module, page.classes, sourceCache);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
for (const cssFile of page.cssFiles) {
|
|
474
|
+
const owners = pagesByCss.get(cssFile) ?? /* @__PURE__ */ new Set();
|
|
475
|
+
owners.add(pageName);
|
|
476
|
+
pagesByCss.set(cssFile, owners);
|
|
477
|
+
}
|
|
478
|
+
pages.push(page);
|
|
479
|
+
}
|
|
480
|
+
if (pages.length === 0) {
|
|
481
|
+
pages.push({
|
|
482
|
+
name: "main",
|
|
483
|
+
classes: /* @__PURE__ */ new Set(),
|
|
484
|
+
cssFiles: /* @__PURE__ */ new Set(),
|
|
485
|
+
jsFiles: /* @__PURE__ */ new Set()
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (entrypoints.size === 1 && pages.length === 1) {
|
|
489
|
+
const shell = pages[0];
|
|
490
|
+
const entryChunks = /* @__PURE__ */ new Set();
|
|
491
|
+
for (const [, entry] of entrypoints) {
|
|
492
|
+
for (const chunk of entry.chunks ?? []) {
|
|
493
|
+
entryChunks.add(chunk);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const pageChunks = findPageChunks(compilation, entryChunks);
|
|
497
|
+
if (pageChunks.length > 0 && shell) {
|
|
498
|
+
pages.length = 0;
|
|
499
|
+
pagesByCss.clear();
|
|
500
|
+
for (const { chunk: pageChunk, name: pageName } of pageChunks) {
|
|
501
|
+
const page = {
|
|
502
|
+
name: pageName,
|
|
503
|
+
classes: new Set(shell.classes),
|
|
504
|
+
cssFiles: new Set(shell.cssFiles),
|
|
505
|
+
jsFiles: /* @__PURE__ */ new Set()
|
|
506
|
+
};
|
|
507
|
+
for (const file of pageChunk.files ?? []) {
|
|
508
|
+
if (file.endsWith(".css")) {
|
|
509
|
+
page.cssFiles.add(file);
|
|
510
|
+
} else if (file.endsWith(".js")) {
|
|
511
|
+
page.jsFiles.add(file);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
for (const file of pageChunk.auxiliaryFiles ?? []) {
|
|
515
|
+
if (file.endsWith(".css")) {
|
|
516
|
+
page.cssFiles.add(file);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
for (const module of chunkModules(compilation, pageChunk)) {
|
|
520
|
+
await scanModule(module, page.classes, sourceCache);
|
|
521
|
+
}
|
|
522
|
+
for (const cssFile of page.cssFiles) {
|
|
523
|
+
const owners = pagesByCss.get(cssFile) ?? /* @__PURE__ */ new Set();
|
|
524
|
+
owners.add(pageName);
|
|
525
|
+
pagesByCss.set(cssFile, owners);
|
|
526
|
+
}
|
|
527
|
+
pages.push(page);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return { pages, pagesByCss };
|
|
532
|
+
}
|
|
533
|
+
function findPageChunks(compilation, entryChunks) {
|
|
534
|
+
const pages = /* @__PURE__ */ new Map();
|
|
535
|
+
const chunkGraph = compilation.chunkGraph;
|
|
536
|
+
const modules = compilation.modules;
|
|
537
|
+
for (const module of modules) {
|
|
538
|
+
const pageName = pageNameFromModule(module);
|
|
539
|
+
if (!pageName) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
for (const chunk of chunkGraph.getModuleChunksIterable(module)) {
|
|
543
|
+
if (!entryChunks.has(chunk) && !pages.has(chunk)) {
|
|
544
|
+
pages.set(chunk, pageName);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const chunks = compilation.chunks;
|
|
549
|
+
for (const chunk of chunks) {
|
|
550
|
+
if (entryChunks.has(chunk) || pages.has(chunk)) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const pageName = findPageName(compilation, chunk);
|
|
554
|
+
if (pageName) {
|
|
555
|
+
pages.set(chunk, pageName);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return [...pages].map(([chunk, name]) => ({ chunk, name }));
|
|
559
|
+
}
|
|
560
|
+
function pageNameFromModule(module) {
|
|
561
|
+
const resource = module.resource?.split("?")[0] ?? module.resource;
|
|
562
|
+
const match = resource?.match(/\/pages\/([^/]+?)\.(?:tsx?|jsx?)$/i);
|
|
563
|
+
if (match?.[1]) {
|
|
564
|
+
return normalizePageName(match[1]).replace(/^page-/i, "") || "page";
|
|
565
|
+
}
|
|
566
|
+
for (const child of module.modules ?? []) {
|
|
567
|
+
const pageName = pageNameFromModule(child);
|
|
568
|
+
if (pageName) {
|
|
569
|
+
return pageName;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
function findPageName(compilation, chunk) {
|
|
575
|
+
for (const module of chunkModules(compilation, chunk)) {
|
|
576
|
+
const pageName = pageNameFromModule(module);
|
|
577
|
+
if (pageName) {
|
|
578
|
+
return pageName;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const name = chunk.name;
|
|
582
|
+
if (name && /^page-/i.test(name)) {
|
|
583
|
+
return normalizePageName(name).replace(/^page-/i, "") || "page";
|
|
584
|
+
}
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
function entryFiles(entry) {
|
|
588
|
+
const files = /* @__PURE__ */ new Set();
|
|
589
|
+
if (typeof entry.getFiles === "function") {
|
|
590
|
+
for (const file of entry.getFiles()) {
|
|
591
|
+
files.add(file);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return files;
|
|
595
|
+
}
|
|
596
|
+
function chunkModules(compilation, chunk) {
|
|
597
|
+
const chunkGraph = compilation.chunkGraph;
|
|
598
|
+
try {
|
|
599
|
+
return chunkGraph.getChunkModulesIterable(chunk);
|
|
600
|
+
} catch {
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async function scanModule(module, classes, sourceCache) {
|
|
605
|
+
const nested = module.modules;
|
|
606
|
+
if (nested) {
|
|
607
|
+
for (const child of nested) {
|
|
608
|
+
await scanModule(child, classes, sourceCache);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const resource = module.resource;
|
|
612
|
+
if (resource && isStyleFile(resource)) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const source = readModuleSource(module);
|
|
616
|
+
if (source) {
|
|
617
|
+
scanClasses(source, classes);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (!resource) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const filePath = resource.split("?")[0];
|
|
624
|
+
if (!filePath) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
let pending = sourceCache.get(filePath);
|
|
628
|
+
if (!pending) {
|
|
629
|
+
pending = readFile(filePath, "utf8").catch(() => void 0);
|
|
630
|
+
sourceCache.set(filePath, pending);
|
|
631
|
+
}
|
|
632
|
+
const fresh = await pending;
|
|
633
|
+
if (fresh) {
|
|
634
|
+
scanClasses(fresh, classes);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function readModuleSource(module) {
|
|
638
|
+
try {
|
|
639
|
+
const source = module.originalSource?.();
|
|
640
|
+
if (!source) {
|
|
641
|
+
return void 0;
|
|
642
|
+
}
|
|
643
|
+
return sourceToString(source.source());
|
|
644
|
+
} catch {
|
|
645
|
+
return void 0;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function scanClasses(source, classes) {
|
|
649
|
+
const patterns = [
|
|
650
|
+
/(?:class|className)\s*=\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)/g,
|
|
651
|
+
/(?:^|[,{;\s])(?:class|className)\s*:\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)/g,
|
|
652
|
+
/classList\.(?:add|remove|toggle|contains)\(\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)/g
|
|
653
|
+
];
|
|
654
|
+
for (const pattern of patterns) {
|
|
655
|
+
for (const match of source.matchAll(pattern)) {
|
|
656
|
+
const value = match[1] ?? match[2] ?? match[3];
|
|
657
|
+
if (!value) {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
for (const token of value.split(/\s+/)) {
|
|
661
|
+
if (/^-?[_a-zA-Z][\w-]*$/.test(token)) {
|
|
662
|
+
classes.add(token.toLowerCase());
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function injectPageCssLoaders(compilation, cssByPage, pageByName, RawSource) {
|
|
669
|
+
const publicPath = compilation.options.output.publicPath;
|
|
670
|
+
const base = typeof publicPath === "string" && publicPath.length > 0 ? publicPath : "/";
|
|
671
|
+
const normalizedBase = base.endsWith("/") ? base : `${base}/`;
|
|
672
|
+
for (const [page, files] of cssByPage) {
|
|
673
|
+
const jsFiles = pageByName.get(page)?.jsFiles;
|
|
674
|
+
if (!jsFiles || files.length === 0) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
const hrefs = files.map((file) => `${normalizedBase}${file}`);
|
|
678
|
+
const snippet = `(()=>{const __cssDoctorPageStyles=${JSON.stringify(hrefs)};if(typeof document!=="undefined"){for(const __cssDoctorPageHref of __cssDoctorPageStyles){if(!Array.from(document.querySelectorAll("link[data-css-doctor-page]")).some((link)=>link.dataset.cssDoctorPage===__cssDoctorPageHref)){const __cssDoctorPageLink=document.createElement("link");__cssDoctorPageLink.rel="stylesheet";__cssDoctorPageLink.href=__cssDoctorPageHref;__cssDoctorPageLink.dataset.cssDoctorPage=__cssDoctorPageHref;document.head.append(__cssDoctorPageLink);}}}})();
|
|
679
|
+
`;
|
|
680
|
+
for (const jsFile of jsFiles) {
|
|
681
|
+
const asset = compilation.getAsset(jsFile);
|
|
682
|
+
if (!asset) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
compilation.updateAsset(
|
|
686
|
+
jsFile,
|
|
687
|
+
new RawSource(snippet + sourceToString(asset.source.source()))
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function normalizePageName(name) {
|
|
693
|
+
const normalized = name.trim();
|
|
694
|
+
if (!normalized) {
|
|
695
|
+
return "main";
|
|
696
|
+
}
|
|
697
|
+
return normalized.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "main";
|
|
698
|
+
}
|
|
699
|
+
function isStyleFile(filePath) {
|
|
700
|
+
return /\.(css|scss|sass|less|styl|stylus)(\?|$)/i.test(filePath);
|
|
701
|
+
}
|
|
702
|
+
function hasRsbuildCssModuleClass(css) {
|
|
703
|
+
const pattern = /\.([A-Za-z_][A-Za-z0-9_-]*-[A-Za-z0-9]{6})(?=[\s,{:.#>+~[])/g;
|
|
704
|
+
for (const match of css.matchAll(pattern)) {
|
|
705
|
+
const className = match[1];
|
|
706
|
+
if (className && isRsbuildCssModuleClass(className)) {
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
function isRsbuildCssModuleClass(className) {
|
|
713
|
+
const hash = className.split("-").at(-1) ?? "";
|
|
714
|
+
return hash.length === 6 && /[A-Z]/.test(hash) && /[0-9]/.test(hash);
|
|
715
|
+
}
|
|
716
|
+
function collectCssModuleClasses(css) {
|
|
717
|
+
const classes = /* @__PURE__ */ new Set();
|
|
718
|
+
const pattern = /\.([A-Za-z_][A-Za-z0-9_-]*-[A-Za-z0-9]{6})(?=[\s,{:.#>+~[])/g;
|
|
719
|
+
for (const match of css.matchAll(pattern)) {
|
|
720
|
+
const className = match[1];
|
|
721
|
+
if (className && isRsbuildCssModuleClass(className)) {
|
|
722
|
+
classes.add(className.toLowerCase());
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return [...classes];
|
|
726
|
+
}
|
|
727
|
+
function withCommonClasses(pageUsage, classes) {
|
|
728
|
+
const result = {};
|
|
729
|
+
for (const [page, pageClasses] of Object.entries(pageUsage)) {
|
|
730
|
+
result[page] = [.../* @__PURE__ */ new Set([...pageClasses, ...classes])];
|
|
731
|
+
}
|
|
732
|
+
return result;
|
|
733
|
+
}
|
|
734
|
+
function matchesPatterns(value, patterns) {
|
|
735
|
+
return patterns.some(
|
|
736
|
+
(pattern) => typeof pattern === "string" ? value.includes(pattern) : pattern.test(value)
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
function sourceToString(source) {
|
|
740
|
+
if (typeof source === "string") {
|
|
741
|
+
return source;
|
|
742
|
+
}
|
|
743
|
+
if (source instanceof Uint8Array) {
|
|
744
|
+
return Buffer.from(source).toString("utf8");
|
|
745
|
+
}
|
|
746
|
+
return String(source);
|
|
747
|
+
}
|
|
748
|
+
function describeError(error) {
|
|
749
|
+
return error instanceof Error ? error.message : String(error);
|
|
750
|
+
}
|
|
751
|
+
function roundMs(value) {
|
|
752
|
+
return Math.round(value * 10) / 10;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/index.ts
|
|
756
|
+
var pluginCssDoctor = (options = {}) => ({
|
|
757
|
+
name: "css-doctor:rsbuild",
|
|
758
|
+
apply: "build",
|
|
759
|
+
setup(api) {
|
|
760
|
+
if (api.context.bundlerType !== "rspack") {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
api.modifyRspackConfig((config, { isProd }) => {
|
|
764
|
+
if (!isProd || process.env.NODE_ENV !== "production") {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
config.plugins ??= [];
|
|
768
|
+
config.plugins.push(new CssDoctorRspackPlugin(options));
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
export {
|
|
773
|
+
pluginCssDoctor
|
|
774
|
+
};
|
|
775
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rspack-plugin.ts","../src/reporter.ts","../src/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport { posix } from \"node:path\";\nimport { performance } from \"node:perf_hooks\";\n\nimport { analyzeCss, optimizeCss } from \"@zealakit/css-doctor-binding\";\nimport type {\n JsChunkOutput,\n JsSelectorStat,\n} from \"@zealakit/css-doctor-binding\";\nimport type { Compilation, Compiler } from \"@rspack/core\";\n\nimport { printReport } from \"./reporter\";\nimport type {\n ChunkInfo,\n CssAssetDiagnostic,\n CssDoctorRsbuildDiagnostics,\n CssDoctorRsbuildOptions,\n EmittedCssChunk,\n PageCoverageOptions,\n SizeBudgetOptions,\n} from \"./types\";\n\nconst PLUGIN_NAME = \"css-doctor:rsbuild\";\nconst DEFAULT_MIN_PAGE_RATIO = 0.5;\nconst DEFAULT_MAX_CHUNK_SIZE = 60 * 1_024;\n\ntype RspackChunkLike = {\n name?: string;\n files?: Iterable<string>;\n auxiliaryFiles?: Iterable<string>;\n};\n\ntype RspackEntrypointLike = {\n name?: string;\n chunks?: Iterable<RspackChunkLike>;\n getFiles?: () => Iterable<string>;\n};\n\ntype RspackModuleLike = {\n resource?: string;\n modules?: Iterable<RspackModuleLike>;\n originalSource?: () => { source: () => unknown } | null;\n};\n\ntype PageInfo = {\n name: string;\n classes: Set<string>;\n cssFiles: Set<string>;\n jsFiles: Set<string>;\n};\n\ntype PagesResult = {\n pages: PageInfo[];\n pagesByCss: Map<string, Set<string>>;\n};\n\ntype ResolvedOptions = {\n pageCoverage: Required<PageCoverageOptions> & { forceCommon: string[] };\n sizeBudget: Required<SizeBudgetOptions>;\n minify: boolean;\n report: boolean;\n include: Array<string | RegExp>;\n exclude: Array<string | RegExp>;\n};\n\n/**\n * Rspack compilation plugin that runs the css-doctor two-tier pipeline for\n * global CSS assets. CSS Modules assets are deliberately skipped.\n */\nexport class CssDoctorRspackPlugin {\n private readonly options: ResolvedOptions;\n\n constructor(options: CssDoctorRsbuildOptions = {}) {\n const minPageRatio =\n options.pageCoverage?.minPageRatio ?? DEFAULT_MIN_PAGE_RATIO;\n const maxChunkSize =\n options.sizeBudget?.maxChunkSize ?? DEFAULT_MAX_CHUNK_SIZE;\n\n if (\n !Number.isFinite(minPageRatio) ||\n minPageRatio < 0 ||\n minPageRatio > 1\n ) {\n throw new RangeError(\n \"css-doctor: minPageRatio must be a number between 0 and 1\",\n );\n }\n\n if (!Number.isInteger(maxChunkSize) || maxChunkSize <= 0) {\n throw new RangeError(\n \"css-doctor: maxChunkSize must be a positive integer\",\n );\n }\n\n this.options = {\n pageCoverage: {\n enabled: options.pageCoverage?.enabled ?? true,\n minPageRatio,\n forceCommon: options.pageCoverage?.forceCommon ?? [],\n },\n sizeBudget: {\n enabled: options.sizeBudget?.enabled ?? true,\n maxChunkSize,\n isolateVariables: options.sizeBudget?.isolateVariables ?? true,\n },\n minify: options.minify ?? true,\n report: options.report ?? true,\n include: options.include ?? [],\n exclude: options.exclude ?? [],\n };\n }\n\n apply(compiler: Compiler): void {\n compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {\n compilation.hooks.processAssets.tapPromise(\n {\n name: PLUGIN_NAME,\n stage: compiler.rspack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,\n },\n async () => {\n try {\n await this.process(compiler, compilation);\n } catch (error) {\n compilation\n .getLogger(PLUGIN_NAME)\n .warn(\n `[css-doctor] diagnostics failed without interrupting the build: ${describeError(\n error,\n )}`,\n );\n }\n },\n );\n });\n }\n\n private async process(\n compiler: Compiler,\n compilation: Compilation,\n ): Promise<void> {\n const startedAt = performance.now();\n const logger = compilation.getLogger(PLUGIN_NAME);\n const pagesResult = await collectPages(compilation);\n const pageNames = pagesResult.pages.map((page) => page.name);\n const allPages = new Set(pageNames);\n const pageUsage: Record<string, string[]> = {};\n const pageUsageCounts: Record<string, number> = {};\n\n for (const page of pagesResult.pages) {\n const classes = [...page.classes].sort();\n pageUsage[page.name] = classes;\n pageUsageCounts[page.name] = classes.length;\n }\n\n const hasPageClasses = pagesResult.pages.some(\n (page) => page.classes.size > 0,\n );\n const pageCoverageApplied =\n this.options.pageCoverage.enabled && hasPageClasses;\n const diagnostics: CssDoctorRsbuildDiagnostics = {\n plugin: PLUGIN_NAME,\n generatedAt: new Date().toISOString(),\n durationMs: 0,\n totalOriginalSize: 0,\n totalOptimizedSize: 0,\n deadCodeSelectorsCount: 0,\n chunks: [],\n minify: this.options.minify,\n pages: pageNames,\n pageUsage: pageUsageCounts,\n splitApplied: pageCoverageApplied,\n totalCssAssets: 0,\n analyzedCssAssets: 0,\n skippedCssAssets: 0,\n failedCssAssets: 0,\n totalSelectors: 0,\n totalRules: 0,\n commonCssBytes: 0,\n pageSpecificCssBytes: 0,\n emittedPageChunks: [],\n emittedCssChunks: [],\n sizeBudget: {\n enabled: this.options.sizeBudget.enabled,\n maxChunkSize: this.options.sizeBudget.maxChunkSize,\n isolateVariables: this.options.sizeBudget.isolateVariables,\n splitChunks: 0,\n oversizedChunks: 0,\n },\n usageMatrix: [],\n files: [],\n errors: [],\n };\n\n const matrix = new Map<string, JsSelectorStat>();\n const pageFiles = new Map<string, string[]>();\n const usedAssetNames = new Set(\n compilation.getAssets().map((asset) => asset.name),\n );\n const RawSource = compiler.rspack.sources.RawSource;\n\n const emitCssChunk = (chunk: JsChunkOutput): string => {\n const hash = createHash(\"sha256\")\n .update(chunk.css)\n .digest(\"hex\")\n .slice(0, 8);\n const safeName =\n chunk.name\n .replace(/[^a-zA-Z0-9_-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .toLowerCase() || \"chunk\";\n let fileName = `static/css/${safeName}.${hash}.css`;\n let index = 2;\n\n while (usedAssetNames.has(fileName)) {\n fileName = `static/css/${safeName}.${hash}-${index}.css`;\n index += 1;\n }\n\n compilation.emitAsset(fileName, new RawSource(chunk.css));\n usedAssetNames.add(fileName);\n return fileName;\n };\n\n for (const asset of compilation.getAssets()) {\n if (!asset.name.endsWith(\".css\")) {\n continue;\n }\n\n diagnostics.totalCssAssets += 1;\n\n if (\n this.shouldSkipAsset(asset.name, sourceToString(asset.source.source()))\n ) {\n diagnostics.skippedCssAssets += 1;\n continue;\n }\n\n const associatedPages = [\n ...(pagesResult.pagesByCss.get(asset.name) ?? allPages),\n ].sort();\n const originalCss = sourceToString(asset.source.source());\n const file = createDiagnostic(asset.name, originalCss, associatedPages);\n\n try {\n const analysis = analyzeCss(originalCss);\n file.selectorCount = analysis.stats.selectorCount;\n file.ruleCount = analysis.stats.ruleCount;\n const moduleClasses = collectCssModuleClasses(originalCss);\n const assetPageUsage =\n moduleClasses.length > 0\n ? withCommonClasses(pageUsage, moduleClasses)\n : pageUsage;\n const pipeline = optimizeCss({\n cssContent: originalCss,\n pageUsage: assetPageUsage,\n pageCoverage: {\n enabled: pageCoverageApplied,\n minPageRatio: this.options.pageCoverage.minPageRatio,\n forceCommon: this.options.pageCoverage.forceCommon,\n },\n sizeBudget: {\n enabled: this.options.sizeBudget.enabled,\n maxChunkSize: this.options.sizeBudget.maxChunkSize,\n isolateVariables: this.options.sizeBudget.isolateVariables,\n },\n minify: this.options.minify,\n });\n\n const commonChunks = pipeline.chunks.filter(\n (chunk) => chunk.page === undefined && chunk.sourceChunk === \"common\",\n );\n const pageChunks = pipeline.chunks.filter(\n (chunk) =>\n chunk.page !== undefined &&\n (associatedPages.includes(chunk.page) ||\n !pagesResult.pagesByCss.has(asset.name)),\n );\n const commonWasSplit =\n commonChunks.length > 1 ||\n commonChunks.some((chunk) => chunk.splitDueToSize);\n const embeddedCommon = commonWasSplit ? undefined : commonChunks[0];\n const commonFileNames: string[] = [];\n let commonSize = 0;\n\n for (const chunk of commonChunks) {\n const embedded = chunk === embeddedCommon;\n const fileName = embedded ? asset.name : emitCssChunk(chunk);\n commonSize += chunk.sizeBytes;\n commonFileNames.push(fileName);\n\n diagnostics.emittedCssChunks.push(\n toEmittedChunk(chunk, fileName, embedded),\n );\n\n if (chunk.splitDueToSize) {\n diagnostics.sizeBudget.splitChunks += 1;\n }\n\n if (chunk.oversized) {\n diagnostics.sizeBudget.oversizedChunks += 1;\n }\n }\n\n if (commonWasSplit) {\n const imports = commonFileNames\n .map((fileName) => {\n const relativePath = posix.relative(\n posix.dirname(asset.name),\n fileName,\n );\n return `@import \"./${relativePath}\";`;\n })\n .join(\"\\n\");\n compilation.updateAsset(asset.name, new RawSource(imports));\n } else {\n compilation.updateAsset(\n asset.name,\n new RawSource(embeddedCommon?.css ?? \"\"),\n );\n }\n\n let pageSpecificSize = 0;\n\n for (const chunk of pageChunks) {\n if (!chunk.page) {\n continue;\n }\n\n const fileName = emitCssChunk(chunk);\n pageSpecificSize += chunk.sizeBytes;\n const files = pageFiles.get(chunk.page) ?? [];\n files.push(fileName);\n pageFiles.set(chunk.page, files);\n\n diagnostics.emittedPageChunks.push({\n page: chunk.page,\n fileName,\n sizeBytes: chunk.sizeBytes,\n });\n diagnostics.emittedCssChunks.push(\n toEmittedChunk(chunk, fileName, false),\n );\n\n if (chunk.splitDueToSize) {\n diagnostics.sizeBudget.splitChunks += 1;\n }\n\n if (chunk.oversized) {\n diagnostics.sizeBudget.oversizedChunks += 1;\n }\n }\n\n file.sizeAfterBytes = commonWasSplit\n ? commonSize\n : Buffer.byteLength(embeddedCommon?.css ?? \"\", \"utf8\");\n file.deadCodeCount = pipeline.deadCodeCount;\n diagnostics.deadCodeSelectorsCount += pipeline.deadCodeCount;\n diagnostics.commonCssBytes += commonSize;\n diagnostics.pageSpecificCssBytes += pageSpecificSize;\n diagnostics.totalOriginalSize += file.sizeBeforeBytes;\n diagnostics.totalOptimizedSize += commonSize + pageSpecificSize;\n\n for (const stat of pipeline.stats) {\n const previous = matrix.get(stat.selector);\n\n if (!previous || stat.totalOccurrences > previous.totalOccurrences) {\n matrix.set(stat.selector, stat);\n }\n }\n } catch (error) {\n file.error = describeError(error);\n diagnostics.failedCssAssets += 1;\n diagnostics.errors.push(`${asset.name}: ${file.error}`);\n logger.warn(`[css-doctor] ${asset.name}: ${file.error}`);\n }\n\n diagnostics.analyzedCssAssets += 1;\n diagnostics.totalSelectors += file.selectorCount;\n diagnostics.totalRules += file.ruleCount;\n diagnostics.files.push(file);\n }\n\n if (pageFiles.size > 0) {\n injectPageCssLoaders(\n compilation,\n pageFiles,\n new Map(pagesResult.pages.map((page) => [page.name, page])),\n RawSource,\n );\n }\n\n diagnostics.chunks = diagnostics.emittedCssChunks.map(toChunkInfo);\n diagnostics.usageMatrix = [...matrix.values()].sort((left, right) =>\n left.selector.localeCompare(right.selector),\n );\n diagnostics.durationMs = roundMs(performance.now() - startedAt);\n\n if (\n this.options.report &&\n (diagnostics.totalCssAssets > 0 || diagnostics.errors.length > 0)\n ) {\n try {\n printReport(diagnostics);\n } catch (error) {\n logger.warn(`[css-doctor] reporter failed: ${describeError(error)}`);\n }\n }\n }\n\n private shouldSkipAsset(assetName: string, source: string): boolean {\n if (/\\.module\\.(css|scss|sass|less|styl|stylus)$/i.test(assetName)) {\n return true;\n }\n\n // Rspack may hash CSS Modules assets so the original `.module.css` name is\n // no longer present. Next/Rsbuild default local identifiers use `_local__hash`.\n if (/\\.[A-Za-z0-9_-]+_[A-Za-z0-9_-]+__[A-Za-z0-9_-]+/.test(source)) {\n return true;\n }\n\n // Rsbuild/Rspack default CSS Modules local identifiers are\n // `<local>-<6-char-hash>`, e.g. `.card-Glm32v`.\n if (hasRsbuildCssModuleClass(source)) {\n return true;\n }\n\n if (this.options.include.length > 0) {\n if (!matchesPatterns(assetName, this.options.include)) {\n return true;\n }\n }\n\n return (\n this.options.exclude.length > 0 &&\n matchesPatterns(assetName, this.options.exclude)\n );\n }\n}\n\nfunction createDiagnostic(\n fileName: string,\n css: string,\n associatedPages: string[],\n): CssAssetDiagnostic {\n return {\n fileName,\n sizeBeforeBytes: Buffer.byteLength(css, \"utf8\"),\n sizeAfterBytes: Buffer.byteLength(css, \"utf8\"),\n selectorCount: 0,\n ruleCount: 0,\n associatedPages,\n deadCodeCount: 0,\n };\n}\n\nfunction toEmittedChunk(\n chunk: JsChunkOutput,\n fileName: string,\n embedded: boolean,\n): EmittedCssChunk {\n return {\n name: chunk.name,\n fileName,\n sourceChunk: chunk.sourceChunk,\n kind: chunk.kind as EmittedCssChunk[\"kind\"],\n page: chunk.page,\n size: chunk.sizeBytes,\n isVariableChunk: chunk.variables,\n isPartChunk: chunk.part !== undefined,\n embedded,\n oversized: chunk.oversized,\n splitDueToSize: chunk.splitDueToSize,\n };\n}\n\nfunction toChunkInfo(chunk: EmittedCssChunk): ChunkInfo {\n return {\n name: chunk.name,\n size: chunk.size,\n isVariableChunk: chunk.isVariableChunk,\n isPartChunk: chunk.isPartChunk,\n };\n}\n\nasync function collectPages(compilation: Compilation): Promise<PagesResult> {\n const pages: PageInfo[] = [];\n const pagesByCss = new Map<string, Set<string>>();\n const sourceCache = new Map<string, Promise<string | undefined>>();\n const entrypoints = compilation.entrypoints as ReadonlyMap<\n string,\n RspackEntrypointLike\n >;\n\n for (const [entryName, entry] of entrypoints) {\n const pageName = normalizePageName(entryName);\n const page: PageInfo = {\n name: pageName,\n classes: new Set<string>(),\n cssFiles: new Set<string>(),\n jsFiles: new Set<string>(),\n };\n\n const files = entryFiles(entry);\n for (const file of files) {\n if (file.endsWith(\".css\")) {\n if (!/\\.module\\.(css|scss|sass|less|styl|stylus)$/i.test(file)) {\n page.cssFiles.add(file);\n }\n } else if (file.endsWith(\".js\")) {\n page.jsFiles.add(file);\n }\n }\n\n for (const chunk of entry.chunks ?? []) {\n for (const file of chunk.files ?? []) {\n if (file.endsWith(\".css\")) {\n page.cssFiles.add(file);\n } else if (file.endsWith(\".js\")) {\n page.jsFiles.add(file);\n }\n }\n\n for (const module of chunkModules(compilation, chunk)) {\n await scanModule(module, page.classes, sourceCache);\n }\n }\n\n for (const cssFile of page.cssFiles) {\n const owners = pagesByCss.get(cssFile) ?? new Set<string>();\n owners.add(pageName);\n pagesByCss.set(cssFile, owners);\n }\n\n pages.push(page);\n }\n\n if (pages.length === 0) {\n pages.push({\n name: \"main\",\n classes: new Set<string>(),\n cssFiles: new Set<string>(),\n jsFiles: new Set<string>(),\n });\n }\n\n // Rsbuild SPA apps have one entrypoint and route pages in async chunks.\n // Replace the shell-only page with page chunks when they are discoverable.\n if (entrypoints.size === 1 && pages.length === 1) {\n const shell = pages[0];\n const entryChunks = new Set<RspackChunkLike>();\n\n for (const [, entry] of entrypoints) {\n for (const chunk of entry.chunks ?? []) {\n entryChunks.add(chunk);\n }\n }\n\n const pageChunks = findPageChunks(compilation, entryChunks);\n\n if (pageChunks.length > 0 && shell) {\n pages.length = 0;\n pagesByCss.clear();\n\n for (const { chunk: pageChunk, name: pageName } of pageChunks) {\n const page: PageInfo = {\n name: pageName,\n classes: new Set(shell.classes),\n cssFiles: new Set(shell.cssFiles),\n jsFiles: new Set<string>(),\n };\n\n for (const file of pageChunk.files ?? []) {\n if (file.endsWith(\".css\")) {\n page.cssFiles.add(file);\n } else if (file.endsWith(\".js\")) {\n page.jsFiles.add(file);\n }\n }\n\n for (const file of pageChunk.auxiliaryFiles ?? []) {\n if (file.endsWith(\".css\")) {\n page.cssFiles.add(file);\n }\n }\n\n for (const module of chunkModules(compilation, pageChunk)) {\n await scanModule(module, page.classes, sourceCache);\n }\n\n for (const cssFile of page.cssFiles) {\n const owners = pagesByCss.get(cssFile) ?? new Set<string>();\n owners.add(pageName);\n pagesByCss.set(cssFile, owners);\n }\n\n pages.push(page);\n }\n }\n }\n\n return { pages, pagesByCss };\n}\n\ntype PageChunk = {\n chunk: RspackChunkLike;\n name: string;\n};\n\nfunction findPageChunks(\n compilation: Compilation,\n entryChunks: Set<RspackChunkLike>,\n): PageChunk[] {\n const pages = new Map<RspackChunkLike, string>();\n const chunkGraph = compilation.chunkGraph as unknown as {\n getModuleChunksIterable: (\n module: RspackModuleLike,\n ) => Iterable<RspackChunkLike>;\n };\n const modules = compilation.modules as unknown as Iterable<RspackModuleLike>;\n\n for (const module of modules) {\n const pageName = pageNameFromModule(module);\n\n if (!pageName) {\n continue;\n }\n\n for (const chunk of chunkGraph.getModuleChunksIterable(module)) {\n if (!entryChunks.has(chunk) && !pages.has(chunk)) {\n pages.set(chunk, pageName);\n }\n }\n }\n\n const chunks = compilation.chunks as unknown as Iterable<RspackChunkLike>;\n\n for (const chunk of chunks) {\n if (entryChunks.has(chunk) || pages.has(chunk)) {\n continue;\n }\n\n const pageName = findPageName(compilation, chunk);\n\n if (pageName) {\n pages.set(chunk, pageName);\n }\n }\n\n return [...pages].map(([chunk, name]) => ({ chunk, name }));\n}\n\nfunction pageNameFromModule(module: RspackModuleLike): string | null {\n const resource = module.resource?.split(\"?\")[0] ?? module.resource;\n const match = resource?.match(/\\/pages\\/([^/]+?)\\.(?:tsx?|jsx?)$/i);\n\n if (match?.[1]) {\n return normalizePageName(match[1]).replace(/^page-/i, \"\") || \"page\";\n }\n\n for (const child of module.modules ?? []) {\n const pageName = pageNameFromModule(child);\n\n if (pageName) {\n return pageName;\n }\n }\n\n return null;\n}\nfunction findPageName(\n compilation: Compilation,\n chunk: RspackChunkLike,\n): string | null {\n for (const module of chunkModules(compilation, chunk)) {\n const pageName = pageNameFromModule(module);\n\n if (pageName) {\n return pageName;\n }\n }\n\n const name = chunk.name;\n\n if (name && /^page-/i.test(name)) {\n return normalizePageName(name).replace(/^page-/i, \"\") || \"page\";\n }\n\n return null;\n}\n\nfunction entryFiles(entry: RspackEntrypointLike): Set<string> {\n const files = new Set<string>();\n\n if (typeof entry.getFiles === \"function\") {\n for (const file of entry.getFiles()) {\n files.add(file);\n }\n }\n\n return files;\n}\n\nfunction chunkModules(\n compilation: Compilation,\n chunk: RspackChunkLike,\n): Iterable<RspackModuleLike> {\n const chunkGraph = compilation.chunkGraph as unknown as {\n getChunkModulesIterable: (chunk: unknown) => Iterable<RspackModuleLike>;\n };\n\n try {\n return chunkGraph.getChunkModulesIterable(chunk);\n } catch {\n return [];\n }\n}\n\nasync function scanModule(\n module: RspackModuleLike,\n classes: Set<string>,\n sourceCache: Map<string, Promise<string | undefined>>,\n): Promise<void> {\n const nested = module.modules;\n\n if (nested) {\n for (const child of nested) {\n await scanModule(child, classes, sourceCache);\n }\n }\n\n const resource = module.resource;\n\n if (resource && isStyleFile(resource)) {\n return;\n }\n\n const source = readModuleSource(module);\n\n if (source) {\n scanClasses(source, classes);\n return;\n }\n\n if (!resource) {\n return;\n }\n\n const filePath = resource.split(\"?\")[0];\n\n if (!filePath) {\n return;\n }\n\n let pending = sourceCache.get(filePath);\n\n if (!pending) {\n pending = readFile(filePath, \"utf8\").catch(() => undefined);\n sourceCache.set(filePath, pending);\n }\n\n const fresh = await pending;\n\n if (fresh) {\n scanClasses(fresh, classes);\n }\n}\n\nfunction readModuleSource(module: RspackModuleLike): string | undefined {\n try {\n const source = module.originalSource?.();\n\n if (!source) {\n return undefined;\n }\n\n return sourceToString(source.source());\n } catch {\n return undefined;\n }\n}\n\nfunction scanClasses(source: string, classes: Set<string>): void {\n const patterns = [\n /(?:class|className)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|`([^`]*)`)/g,\n /(?:^|[,{;\\s])(?:class|className)\\s*:\\s*(?:\"([^\"]*)\"|'([^']*)'|`([^`]*)`)/g,\n /classList\\.(?:add|remove|toggle|contains)\\(\\s*(?:\"([^\"]*)\"|'([^']*)'|`([^`]*)`)/g,\n ];\n\n for (const pattern of patterns) {\n for (const match of source.matchAll(pattern)) {\n const value = match[1] ?? match[2] ?? match[3];\n\n if (!value) {\n continue;\n }\n\n for (const token of value.split(/\\s+/)) {\n if (/^-?[_a-zA-Z][\\w-]*$/.test(token)) {\n classes.add(token.toLowerCase());\n }\n }\n }\n }\n}\n\nfunction injectPageCssLoaders(\n compilation: Compilation,\n cssByPage: Map<string, string[]>,\n pageByName: Map<string, PageInfo>,\n RawSource: new (source: string) => unknown,\n): void {\n const publicPath = compilation.options.output.publicPath;\n const base =\n typeof publicPath === \"string\" && publicPath.length > 0 ? publicPath : \"/\";\n const normalizedBase = base.endsWith(\"/\") ? base : `${base}/`;\n\n for (const [page, files] of cssByPage) {\n const jsFiles = pageByName.get(page)?.jsFiles;\n\n if (!jsFiles || files.length === 0) {\n continue;\n }\n\n const hrefs = files.map((file) => `${normalizedBase}${file}`);\n const snippet = `(()=>{const __cssDoctorPageStyles=${JSON.stringify(hrefs)};if(typeof document!==\"undefined\"){for(const __cssDoctorPageHref of __cssDoctorPageStyles){if(!Array.from(document.querySelectorAll(\"link[data-css-doctor-page]\")).some((link)=>link.dataset.cssDoctorPage===__cssDoctorPageHref)){const __cssDoctorPageLink=document.createElement(\"link\");__cssDoctorPageLink.rel=\"stylesheet\";__cssDoctorPageLink.href=__cssDoctorPageHref;__cssDoctorPageLink.dataset.cssDoctorPage=__cssDoctorPageHref;document.head.append(__cssDoctorPageLink);}}}})();\\n`;\n\n for (const jsFile of jsFiles) {\n const asset = compilation.getAsset(jsFile);\n\n if (!asset) {\n continue;\n }\n\n compilation.updateAsset(\n jsFile,\n new RawSource(snippet + sourceToString(asset.source.source())) as never,\n );\n }\n }\n}\n\nfunction normalizePageName(name: string): string {\n const normalized = name.trim();\n\n if (!normalized) {\n return \"main\";\n }\n\n return (\n normalized\n .replace(/[^a-zA-Z0-9_-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .toLowerCase() || \"main\"\n );\n}\n\nfunction isStyleFile(filePath: string): boolean {\n return /\\.(css|scss|sass|less|styl|stylus)(\\?|$)/i.test(filePath);\n}\n\nfunction hasRsbuildCssModuleClass(css: string): boolean {\n const pattern =\n /\\.([A-Za-z_][A-Za-z0-9_-]*-[A-Za-z0-9]{6})(?=[\\s,{:.#>+~[])/g;\n\n for (const match of css.matchAll(pattern)) {\n const className = match[1];\n\n if (className && isRsbuildCssModuleClass(className)) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction isRsbuildCssModuleClass(className: string): boolean {\n const hash = className.split(\"-\").at(-1) ?? \"\";\n\n return hash.length === 6 && /[A-Z]/.test(hash) && /[0-9]/.test(hash);\n}\n\nfunction collectCssModuleClasses(css: string): string[] {\n const classes = new Set<string>();\n const pattern =\n /\\.([A-Za-z_][A-Za-z0-9_-]*-[A-Za-z0-9]{6})(?=[\\s,{:.#>+~[])/g;\n\n for (const match of css.matchAll(pattern)) {\n const className = match[1];\n\n if (className && isRsbuildCssModuleClass(className)) {\n classes.add(className.toLowerCase());\n }\n }\n\n return [...classes];\n}\n\nfunction withCommonClasses(\n pageUsage: Record<string, string[]>,\n classes: string[],\n): Record<string, string[]> {\n const result: Record<string, string[]> = {};\n\n for (const [page, pageClasses] of Object.entries(pageUsage)) {\n result[page] = [...new Set([...pageClasses, ...classes])];\n }\n\n return result;\n}\n\nfunction matchesPatterns(\n value: string,\n patterns: Array<string | RegExp>,\n): boolean {\n return patterns.some((pattern) =>\n typeof pattern === \"string\" ? value.includes(pattern) : pattern.test(value),\n );\n}\n\nfunction sourceToString(source: unknown): string {\n if (typeof source === \"string\") {\n return source;\n }\n\n if (source instanceof Uint8Array) {\n return Buffer.from(source).toString(\"utf8\");\n }\n\n return String(source);\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction roundMs(value: number): number {\n return Math.round(value * 10) / 10;\n}\n","import pc from \"picocolors\";\n\nimport type { CssDoctorRsbuildDiagnostics } from \"./types\";\n\nexport function printReport(diagnostics: CssDoctorRsbuildDiagnostics): void {\n const status = diagnostics.minify\n ? pc.green(\"Minified: YES\")\n : pc.yellow(\"Minified: NO\");\n const lines = [\n \"\",\n `${pc.cyan(pc.bold(\"[css-doctor]\"))} ${pc.bold(\"Rsbuild\")} ${pc.dim(\"·\")} ${\n diagnostics.pages.length\n } pages ${pc.dim(\"·\")} ${diagnostics.analyzedCssAssets}/${\n diagnostics.totalCssAssets\n } CSS assets ${pc.dim(\"·\")} ${formatBytes(\n diagnostics.totalOriginalSize,\n )} ${pc.dim(\"→\")} ${formatBytes(diagnostics.totalOptimizedSize)} ${pc.dim(\n \"·\",\n )} ${formatDuration(diagnostics.durationMs)} ${pc.dim(\"·\")} ${status}`,\n ];\n\n for (const file of diagnostics.files) {\n if (file.error) {\n lines.push(\n ` ${pc.red(\"✗\")} ${pc.magenta(file.fileName)} ${pc.red(file.error)}`,\n );\n continue;\n }\n\n lines.push(\n ` ${pc.green(\"✓\")} ${pc.magenta(file.fileName)} ${pc.dim(\n `${formatBytes(file.sizeBeforeBytes)} → ${formatBytes(\n file.sizeAfterBytes,\n )}`,\n )} ${pc.dim(\"·\")} ${file.selectorCount} selectors ${pc.dim(\n \"·\",\n )} pages: ${file.associatedPages.join(\", \") || \"none\"}`,\n );\n lines.push(\n ` split common ${formatBytes(\n file.sizeAfterBytes,\n )} ${pc.dim(\"·\")} dead ${file.deadCodeCount}`,\n );\n }\n\n if (diagnostics.emittedPageChunks.length > 0) {\n lines.push(\"\");\n lines.push(\n ` ${pc.bold(`Emitted page chunks (${diagnostics.emittedPageChunks.length})`)}`,\n );\n\n for (const chunk of diagnostics.emittedPageChunks) {\n lines.push(\n ` ${pc.magenta(chunk.fileName)} ${pc.dim(formatBytes(chunk.sizeBytes))}`,\n );\n }\n }\n\n lines.push(\"\");\n lines.push(\n ` ${pc.bold(\"Size budget\")} ${pc.dim(\n `(max ${formatBytes(diagnostics.sizeBudget.maxChunkSize)} · variables ${\n diagnostics.sizeBudget.isolateVariables ? \"isolated\" : \"inline\"\n })`,\n )}`,\n );\n\n if (diagnostics.emittedCssChunks.length === 0) {\n lines.push(` ${pc.dim(\"No CSS chunks were emitted.\")}`);\n } else {\n for (const chunk of diagnostics.emittedCssChunks) {\n const markers: string[] = [];\n\n if (chunk.isVariableChunk) {\n markers.push(\"vars\");\n }\n\n if (chunk.isPartChunk) {\n markers.push(\"part\");\n }\n\n if (chunk.embedded) {\n markers.push(\"embedded\");\n }\n\n if (chunk.splitDueToSize) {\n markers.push(pc.yellow(\"split\"));\n }\n\n if (chunk.oversized) {\n markers.push(pc.red(\"oversized\"));\n }\n\n lines.push(\n ` ${pc.magenta(chunk.fileName)} ${pc.dim(\n formatBytes(chunk.size),\n )}${markers.length > 0 ? ` ${pc.dim(\"·\")} ${markers.join(\" · \")}` : \"\"}`,\n );\n }\n }\n\n if (diagnostics.deadCodeSelectorsCount > 0) {\n lines.push(\"\");\n lines.push(\n ` ${pc.yellow(\n `⚠ Dead code eliminated: ${diagnostics.deadCodeSelectorsCount} selector occurrences`,\n )}`,\n );\n }\n\n if (diagnostics.errors.length > 0) {\n lines.push(\"\");\n for (const error of diagnostics.errors) {\n lines.push(` ${pc.red(`✗ ${error}`)}`);\n }\n }\n\n console.log(lines.join(\"\\n\"));\n}\n\nfunction formatDuration(milliseconds: number): string {\n if (milliseconds < 1) {\n return \"<1 ms\";\n }\n\n return `${milliseconds.toFixed(1)} ms`;\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) {\n return `${bytes} B`;\n }\n\n return `${(bytes / 1024).toFixed(1)} KB`;\n}\n","import type { RsbuildPlugin } from \"@rsbuild/core\";\n\nimport { CssDoctorRspackPlugin } from \"./rspack-plugin\";\nimport type { CssDoctorRsbuildOptions } from \"./types\";\n\n/**\n * Adds the css-doctor Rspack plugin to production Rsbuild builds.\n */\nexport const pluginCssDoctor = (\n options: CssDoctorRsbuildOptions = {},\n): RsbuildPlugin => ({\n name: \"css-doctor:rsbuild\",\n apply: \"build\",\n setup(api) {\n if (api.context.bundlerType !== \"rspack\") {\n return;\n }\n\n api.modifyRspackConfig((config, { isProd }) => {\n if (!isProd || process.env.NODE_ENV !== \"production\") {\n return;\n }\n\n config.plugins ??= [];\n config.plugins.push(new CssDoctorRspackPlugin(options));\n });\n },\n});\n\nexport type * from \"./types\";\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zealakit/css-doctor-rsbuild",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Rsbuild/Rspack integration for css-doctor page-coverage and size-budget CSS splitting",
|
|
5
|
+
"author": "nqhed - Huy Edward Nguyen",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"README_VI.md",
|
|
21
|
+
"README_CN.md"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"picocolors": "^1.1.1",
|
|
25
|
+
"@zealakit/css-doctor-binding": "1.0.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@rsbuild/core": ">=1.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@rsbuild/core": "^1.7.6",
|
|
32
|
+
"@rspack/core": "^2.2.9",
|
|
33
|
+
"@types/node": "^22.20.5",
|
|
34
|
+
"tsup": "^8.5.1",
|
|
35
|
+
"typescript": "^5.9.3"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/zealakit/css-doctor.git",
|
|
43
|
+
"directory": "packages/css-doctor-rsbuild"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/zealakit/css-doctor/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/zealakit/css-doctor#readme",
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup",
|
|
51
|
+
"dev": "tsup --watch",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"lint": "biome lint src",
|
|
54
|
+
"format": "biome format --write src",
|
|
55
|
+
"format:check": "biome format src",
|
|
56
|
+
"check": "biome check src"
|
|
57
|
+
}
|
|
6
58
|
}
|