Skip to content

Commit dd24eec

Browse files
yifancongCopilot
andauthored
feat(cli): support bundle-diff cli to output json data (#1561)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 47352f1 commit dd24eec

4 files changed

Lines changed: 224 additions & 4 deletions

File tree

packages/cli/src/commands/bundle-diff.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
SDK,
1616
Constants,
1717
} from '@rsdoctor/types';
18-
import { Manifest, Algorithm } from '@rsdoctor/utils/common';
18+
import { Manifest, Algorithm, Graph } from '@rsdoctor/utils/common';
1919
import { RsdoctorSDK } from '@rsdoctor/sdk';
2020
import { createRequire } from 'node:module';
2121

@@ -24,6 +24,7 @@ interface Options {
2424
baseline: string;
2525
open?: boolean;
2626
html?: boolean;
27+
json?: boolean | string;
2728
output?: string;
2829
}
2930

@@ -49,17 +50,22 @@ example: ${bin} ${Commands.BundleDiff} --baseline="x.json" --current="x.json"
4950
'the url or file path of the profile json as the baseline',
5051
)
5152
.option('--html', 'output as a standalone HTML file')
53+
.option(
54+
'--json [path]',
55+
'output as a JSON file, optionally specify file path (default: rsdoctor-diff.json)',
56+
)
5257
.option(
5358
'--output <path>',
54-
'output file path for HTML mode (default: rsdoctor-diff.html)',
59+
'output file path for HTML mode (default: rsdoctor-diff.html). For JSON output, use --json [path] to specify the file path.',
5560
);
5661
},
5762
async action({
5863
baseline,
5964
current,
6065
open = true,
6166
html = false,
62-
output = 'rsdoctor-diff.html',
67+
json = false,
68+
output, // ????
6369
}) {
6470
const spinner = ora({ prefixText: cyan(`[${name}]`) }).start();
6571

@@ -127,6 +133,30 @@ example: ${bin} ${Commands.BundleDiff} --baseline="x.json" --current="x.json"
127133
}
128134
}
129135

136+
if (json && html) {
137+
spinner.fail(
138+
red('Options "--json" and "--html" cannot be used together. Please choose one.'),
139+
);
140+
return null;
141+
} else if (json) {
142+
spinner.text = 'Generating JSON output file...';
143+
144+
const jsonOutputFile =
145+
typeof json === 'string' ? json : output || 'rsdoctor-diff.json';
146+
const outputPath = path.resolve(cwd, jsonOutputFile);
147+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
148+
149+
const jsonData = Graph.getBundleDiffResult(
150+
baselineDataValue,
151+
currentDataValue,
152+
);
153+
154+
fs.writeFileSync(outputPath, JSON.stringify(jsonData, null, 2), 'utf-8');
155+
156+
spinner.succeed(`Generated JSON output file at: ${outputPath}`);
157+
return null;
158+
}
159+
130160
// Only start server if not in HTML mode
131161
if (!html) {
132162
spinner.text = `start server`;
@@ -285,7 +315,7 @@ example: ${bin} ${Commands.BundleDiff} --baseline="x.json" --current="x.json"
285315
);
286316

287317
// Write the output file
288-
const outputPath = path.resolve(cwd, output);
318+
const outputPath = path.resolve(cwd, output || 'rsdoctor-diff.html');
289319
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
290320
fs.writeFileSync(outputPath, htmlContent, 'utf-8');
291321

packages/types/src/client.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { ModuleSize } from './sdk/module';
2+
13
export enum RsdoctorClientUrlQuery {
24
BundleDiffFiles = '__bundle_files__',
35
ManifestFile = 'manifest',
@@ -113,3 +115,55 @@ export interface RsdoctorClientAssetsSummary {
113115
total: AssetInfo;
114116
};
115117
}
118+
119+
export interface RsdoctorClientModuleDiffItem {
120+
path: string;
121+
size: {
122+
baseline: ModuleSize;
123+
current: ModuleSize;
124+
};
125+
/** Percent change based on parsedSize */
126+
percent: number;
127+
state: RsdoctorClientDiffState;
128+
}
129+
130+
export interface RsdoctorClientModulesDiffResult {
131+
added: Array<{ path: string; size: ModuleSize }>;
132+
removed: Array<{ path: string; size: ModuleSize }>;
133+
changed: RsdoctorClientModuleDiffItem[];
134+
}
135+
136+
export interface RsdoctorClientPackageDiffItem {
137+
name: string;
138+
version: string;
139+
root: string;
140+
size: {
141+
baseline: ModuleSize;
142+
current: ModuleSize;
143+
};
144+
/** Percent change based on parsedSize */
145+
percent: number;
146+
state: RsdoctorClientDiffState;
147+
}
148+
149+
export interface RsdoctorClientPackagesDiffResult {
150+
added: Array<{
151+
name: string;
152+
version: string;
153+
root: string;
154+
size: ModuleSize;
155+
}>;
156+
removed: Array<{
157+
name: string;
158+
version: string;
159+
root: string;
160+
size: ModuleSize;
161+
}>;
162+
changed: RsdoctorClientPackageDiffItem[];
163+
}
164+
165+
export interface RsdoctorClientBundleDiffResult {
166+
assets: RsdoctorClientAssetsDiffResult;
167+
modules: RsdoctorClientModulesDiffResult;
168+
packages: RsdoctorClientPackagesDiffResult;
169+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { Client, SDK } from '@rsdoctor/types';
2+
import { getAssetsDiffResult, diffSize } from './assets';
3+
4+
/**
5+
* Compute module diff between baseline and current.
6+
* Modules are matched by their file path.
7+
*/
8+
export function getModulesDiffResult(
9+
baseline: SDK.ModuleGraphData,
10+
current: SDK.ModuleGraphData,
11+
): Client.RsdoctorClientModulesDiffResult {
12+
const baselineMap = new Map<string, SDK.ModuleData>();
13+
for (const m of baseline.modules) {
14+
baselineMap.set(m.path, m);
15+
}
16+
17+
const currentMap = new Map<string, SDK.ModuleData>();
18+
for (const m of current.modules) {
19+
currentMap.set(m.path, m);
20+
}
21+
22+
const added: Client.RsdoctorClientModulesDiffResult['added'] = [];
23+
const removed: Client.RsdoctorClientModulesDiffResult['removed'] = [];
24+
const changed: Client.RsdoctorClientModuleDiffItem[] = [];
25+
26+
for (const [path, bModule] of baselineMap) {
27+
const cModule = currentMap.get(path);
28+
if (!cModule) {
29+
removed.push({ path, size: bModule.size });
30+
} else if (bModule.size.parsedSize !== cModule.size.parsedSize) {
31+
const { percent, state } = diffSize(
32+
bModule.size.parsedSize,
33+
cModule.size.parsedSize,
34+
);
35+
changed.push({
36+
path,
37+
size: { baseline: bModule.size, current: cModule.size },
38+
percent,
39+
state,
40+
});
41+
}
42+
}
43+
44+
for (const [path, cModule] of currentMap) {
45+
if (!baselineMap.has(path)) {
46+
added.push({ path, size: cModule.size });
47+
}
48+
}
49+
50+
added.sort((a, b) => a.path.localeCompare(b.path));
51+
removed.sort((a, b) => a.path.localeCompare(b.path));
52+
changed.sort((a, b) => a.path.localeCompare(b.path));
53+
return { added, removed, changed };
54+
}
55+
56+
/**
57+
* Compute package diff between baseline and current.
58+
* Packages are matched by name@version.
59+
*/
60+
export function getPackagesDiffResult(
61+
baseline: SDK.PackageGraphData,
62+
current: SDK.PackageGraphData,
63+
): Client.RsdoctorClientPackagesDiffResult {
64+
const toKey = (p: SDK.PackageData) => `${p.name}@${p.version}`;
65+
66+
const baselineMap = new Map<string, SDK.PackageData>();
67+
for (const p of baseline.packages) {
68+
baselineMap.set(toKey(p), p);
69+
}
70+
71+
const currentMap = new Map<string, SDK.PackageData>();
72+
for (const p of current.packages) {
73+
currentMap.set(toKey(p), p);
74+
}
75+
76+
const added: Client.RsdoctorClientPackagesDiffResult['added'] = [];
77+
const removed: Client.RsdoctorClientPackagesDiffResult['removed'] = [];
78+
const changed: Client.RsdoctorClientPackageDiffItem[] = [];
79+
80+
for (const [key, bPkg] of baselineMap) {
81+
const cPkg = currentMap.get(key);
82+
if (!cPkg) {
83+
removed.push({
84+
name: bPkg.name,
85+
version: bPkg.version,
86+
root: bPkg.root,
87+
size: bPkg.size,
88+
});
89+
} else if (bPkg.size.parsedSize !== cPkg.size.parsedSize) {
90+
const { percent, state } = diffSize(
91+
bPkg.size.parsedSize,
92+
cPkg.size.parsedSize,
93+
);
94+
changed.push({
95+
name: bPkg.name,
96+
version: bPkg.version,
97+
root: bPkg.root,
98+
size: { baseline: bPkg.size, current: cPkg.size },
99+
percent,
100+
state,
101+
});
102+
}
103+
}
104+
105+
for (const [key, cPkg] of currentMap) {
106+
if (!baselineMap.has(key)) {
107+
added.push({
108+
name: cPkg.name,
109+
version: cPkg.version,
110+
root: cPkg.root,
111+
size: cPkg.size,
112+
});
113+
}
114+
}
115+
116+
return { added, removed, changed };
117+
}
118+
119+
/**
120+
* Compute the full bundle diff result between baseline and current manifest data.
121+
* Combines asset, module, and package diffs into a single result.
122+
*/
123+
export function getBundleDiffResult(
124+
baseline: SDK.BuilderStoreData,
125+
current: SDK.BuilderStoreData,
126+
): Client.RsdoctorClientBundleDiffResult {
127+
return {
128+
assets: getAssetsDiffResult(baseline.chunkGraph, current.chunkGraph),
129+
modules: getModulesDiffResult(baseline.moduleGraph, current.moduleGraph),
130+
packages: getPackagesDiffResult(
131+
baseline.packageGraph,
132+
current.packageGraph,
133+
),
134+
};
135+
}

packages/utils/src/common/graph/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './assets';
2+
export * from './bundle-diff';
23
export * from './chunk';
34
export * from './modules';
45
export * from './dependency';

0 commit comments

Comments
 (0)