Skip to content

Commit 8a44c70

Browse files
authored
feat: support upload and online analysis (#1288)
1 parent 5078b1a commit 8a44c70

File tree

18 files changed

+515
-39
lines changed

18 files changed

+515
-39
lines changed
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { expect, test } from '@playwright/test';
2+
import { compileByRspack } from '@scripts/test-helper';
3+
import path from 'path';
4+
import fs from 'fs/promises';
5+
import { Constants } from '@rsdoctor/types';
6+
import { getSDK, setSDK } from '@rsdoctor/core/plugins';
7+
import type { Compiler } from '@rspack/core';
8+
import { createRsdoctorPlugin } from '../doctor-rsbuild/test-utils';
9+
10+
async function rspackCompile(
11+
_tapName: string,
12+
compile: typeof compileByRspack,
13+
) {
14+
const file = path.resolve(__dirname, './fixtures/c.js');
15+
16+
const res = await compile(file, {
17+
resolve: {
18+
extensions: ['.ts', '.js'],
19+
},
20+
output: {
21+
path: path.join(__dirname, './dist'),
22+
},
23+
module: {
24+
rules: [
25+
{
26+
test: /\.[jt]s$/,
27+
use: {
28+
loader: 'builtin:swc-loader',
29+
options: {
30+
jsc: {
31+
parser: {
32+
syntax: 'typescript',
33+
},
34+
externalHelpers: true,
35+
preserveAllComments: false,
36+
},
37+
},
38+
},
39+
type: 'javascript/auto',
40+
},
41+
],
42+
},
43+
plugins: [
44+
createRsdoctorPlugin({
45+
disableClientServer: false,
46+
output: {
47+
mode: 'brief',
48+
options: {
49+
type: ['json'],
50+
},
51+
},
52+
port: 8681,
53+
}),
54+
{
55+
name: 'Foo',
56+
apply(compiler: Compiler) {
57+
compiler.hooks.beforeRun.tapPromise(
58+
{ name: 'Foo', stage: 99999 },
59+
async () => {
60+
const sdk = getSDK();
61+
if (!sdk) {
62+
throw new Error('SDK is undefined');
63+
}
64+
setSDK(
65+
new Proxy(sdk as object, {
66+
get(target, key, receiver) {
67+
switch (key) {
68+
case 'reportLoader':
69+
return null;
70+
case 'reportLoaderStartOrEnd':
71+
return (_data: any) => {};
72+
default:
73+
return Reflect.get(target, key, receiver);
74+
}
75+
},
76+
set(target, key, value, receiver) {
77+
return Reflect.set(target, key, value, receiver);
78+
},
79+
defineProperty(target, p, attrs) {
80+
return Reflect.defineProperty(target, p, attrs);
81+
},
82+
}) as any,
83+
);
84+
},
85+
);
86+
},
87+
},
88+
],
89+
});
90+
91+
return res;
92+
}
93+
94+
// Integration test that uses real build artifacts
95+
test.describe('Uploader Integration Tests', () => {
96+
let manifestPath: string;
97+
let manifestData: any;
98+
99+
test.beforeAll(async () => {
100+
const tapName = 'Foo';
101+
await rspackCompile(tapName, compileByRspack);
102+
103+
manifestPath = path.resolve(
104+
__dirname,
105+
'../../.rsdoctor/rsdoctor-data.json',
106+
);
107+
108+
await new Promise((resolve) => setTimeout(resolve, 2000));
109+
110+
try {
111+
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
112+
manifestData = JSON.parse(manifestContent);
113+
} catch (error) {
114+
console.error('Failed to read manifest file:', error);
115+
// Create minimal test data if file doesn't exist
116+
manifestData = {
117+
data: {
118+
errors: [],
119+
moduleGraph: {
120+
dependencies: [],
121+
modules: [],
122+
moduleGraphModules: [],
123+
exports: [],
124+
sideEffects: [],
125+
variables: [],
126+
layers: [],
127+
},
128+
chunkGraph: { assets: [], chunks: [], entrypoints: [] },
129+
},
130+
clientRoutes: ['Overall', 'Bundle.ModuleGraph', 'Bundle.BundleSize'],
131+
};
132+
}
133+
});
134+
135+
test('should upload and analyze real build manifest', async ({ page }) => {
136+
// Start a local client server or navigate to existing one
137+
await page.goto('http://localhost:8681/#/resources/uploader');
138+
139+
// Verify uploader is loaded
140+
await expect(page.locator('.ant-upload-btn')).toBeVisible();
141+
142+
// Create file content for upload
143+
const fileContent = JSON.stringify(manifestData);
144+
145+
// Execute file upload in browser console
146+
const uploadResult = await page.evaluate(async (fileContent: any) => {
147+
// Create a File object from the JSON content
148+
const fileName = 'rsdoctor-manifest.json';
149+
const file = new File([fileContent], fileName, {
150+
type: 'application/json',
151+
});
152+
153+
// Find the file input element
154+
const fileInput = document.querySelector(
155+
'input[type="file"]',
156+
) as HTMLInputElement;
157+
if (!fileInput) {
158+
throw new Error('File input not found');
159+
}
160+
161+
// Create a DataTransfer object to simulate file selection
162+
const dataTransfer = new DataTransfer();
163+
dataTransfer.items.add(file);
164+
165+
// Set the files property of the input
166+
Object.defineProperty(fileInput, 'files', {
167+
value: dataTransfer.files,
168+
writable: false,
169+
});
170+
171+
// Dispatch change event to trigger upload
172+
const changeEvent = new Event('change', { bubbles: true });
173+
fileInput.dispatchEvent(changeEvent);
174+
175+
// Wait a bit for the upload to process
176+
await new Promise((resolve) => setTimeout(resolve, 1000));
177+
178+
return {
179+
success: true,
180+
fileName: fileName,
181+
fileSize: file.size,
182+
currentUrl: window.location.href,
183+
};
184+
}, fileContent);
185+
186+
console.log('Upload result:', uploadResult);
187+
188+
// Wait for navigation to overall page
189+
await page.waitForURL(/.*#\/overall.*/, { timeout: 10000 });
190+
191+
// Verify successful navigation to overall page
192+
expect(page.url()).toContain('#/overall');
193+
194+
// Verify data is properly mounted using browser console execution
195+
const windowData = await page.evaluate((tag) => {
196+
return (window as any)[tag];
197+
}, Constants.WINDOW_RSDOCTOR_TAG);
198+
199+
expect(windowData).toBeDefined();
200+
201+
// Verify the mounted data structure
202+
if (manifestData.data) {
203+
expect(windowData).toHaveProperty('errors');
204+
expect(windowData).toHaveProperty('moduleGraph');
205+
expect(windowData).toHaveProperty('chunkGraph');
206+
}
207+
208+
// Verify enableRoutes are set
209+
if (manifestData.clientRoutes) {
210+
expect(windowData.enableRoutes).toEqual(manifestData.clientRoutes);
211+
}
212+
213+
// Test that menus are rendered based on enableRoutes
214+
if (
215+
manifestData.clientRoutes &&
216+
manifestData.clientRoutes.includes('Overall')
217+
) {
218+
await expect(page.locator("text='Bundle Overall'").first()).toBeVisible();
219+
}
220+
});
221+
222+
test.afterAll(async () => {
223+
try {
224+
await fs.rm(path.resolve(__dirname, './dist'), {
225+
recursive: true,
226+
force: true,
227+
});
228+
} catch (error) {}
229+
});
230+
});

examples/rsbuild-minimal/rsbuild.config.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ export default defineConfig({
1010
chain.plugin('Rsdoctor').use(RsdoctorRspackPlugin, [
1111
{
1212
disableClientServer: !process.env.ENABLE_CLIENT_SERVER,
13-
mode: 'brief',
1413
output: {
15-
// mode: 'brief',
16-
// options: {
17-
// type: ['json', 'html'],
18-
// },
14+
mode: 'brief',
15+
options: {
16+
type: ['json', 'html'],
17+
},
1918
reportCodeType: {
2019
noCode: true,
2120
},

packages/cli/src/commands/stats-analyze.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const statsAnalyze: Command<
3232
})
3333
.option('port', {
3434
type: 'number',
35-
description: 'port for Web Doctor Server',
35+
description: 'port for Rsdoctor Server',
3636
})
3737
.option('type', {
3838
type: 'string',

packages/client/src/router.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
RuleIndex,
1111
TreeShaking,
1212
BundleDiff,
13+
Uploader,
1314
} from '@rsdoctor/components/pages';
1415

1516
export default function Router(): React.ReactElement {
@@ -52,7 +53,7 @@ export default function Router(): React.ReactElement {
5253
<Route key={e.path} path={e.path} element={e.element} />
5354
))}
5455
<Route path={BundleDiff.route} element={<BundleDiff.Page />} />
55-
{/* <Route path="*" element={<NotFound />} /> TODO:: add page NotFound */}
56+
<Route path={Uploader.route} element={<Uploader.Page />} />
5657
</Routes>
5758
);
5859
}

packages/components/src/components/Layout/header.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import './header.sass';
1010
import { Client } from '@rsdoctor/types';
1111
import { useNavigate } from 'react-router-dom';
1212

13-
export const Header: React.FC = () => {
13+
export interface HeaderProps {
14+
enableRoutes?: string[];
15+
}
16+
17+
export const Header: React.FC<HeaderProps> = ({ enableRoutes }) => {
1418
const { i18n } = useI18n();
1519

1620
const navigate = useNavigate();
@@ -74,7 +78,10 @@ export const Header: React.FC = () => {
7478
<BuilderSelect />
7579
</div>
7680
</Col>
77-
<Menus style={{ transition: 'none' }} />
81+
<Menus
82+
key={enableRoutes ? JSON.stringify(enableRoutes) : 'default'}
83+
style={{ transition: 'none' }}
84+
/>
7885

7986
<Col flex={1}>
8087
<Row

packages/components/src/components/Layout/index.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PropsWithChildren, useContext, useEffect } from 'react';
1+
import { PropsWithChildren, useContext, useEffect, useState } from 'react';
22
import { FloatButton, Layout as L } from 'antd';
33
import { Language, MAIN_BG, Size } from '../../constants';
44
import { Header } from './header';
@@ -8,6 +8,8 @@ import {
88
getFirstVisitFromStorage,
99
setFirstVisitToStorage,
1010
getLanguage,
11+
useUrlQuery,
12+
getEnableRoutesFromUrlQuery,
1113
} from '../../utils';
1214
import { Progress } from './progress';
1315
import { ConfigContext } from '../../config';
@@ -20,6 +22,10 @@ export const Layout = (props: PropsWithChildren<LayoutProps>): JSX.Element => {
2022
const locale = useLocale();
2123
const { i18n } = useI18n();
2224
const { children } = props;
25+
const query = useUrlQuery();
26+
const [enableRoutes, setEnableRoutes] = useState<string[] | undefined>(
27+
() => getEnableRoutesFromUrlQuery() || undefined,
28+
);
2329

2430
useEffect(() => {
2531
let currentLocale = locale;
@@ -40,10 +46,16 @@ export const Layout = (props: PropsWithChildren<LayoutProps>): JSX.Element => {
4046
}
4147
}, [locale]);
4248

49+
// Listen for enableRoutes changes in URL query parameters
50+
useEffect(() => {
51+
const newEnableRoutes = getEnableRoutesFromUrlQuery();
52+
setEnableRoutes(newEnableRoutes || undefined);
53+
}, [query]);
54+
4355
const ctx = useContext(ConfigContext);
4456
return (
4557
<L>
46-
{!ctx.embedded ? <Header /> : null}
58+
{!ctx.embedded ? <Header enableRoutes={enableRoutes} /> : null}
4759
<Progress />
4860
<L.Content
4961
style={{

packages/components/src/components/Layout/menus.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import React, { useEffect, useState } from 'react';
1111
import { useLocation, useNavigate } from 'react-router-dom';
1212
import { Size } from '../../constants';
1313
import * as OverallConstants from '../../pages/Overall/constants';
14-
import { useI18n, hasBundle, hasCompile } from '../../utils';
14+
import {
15+
useI18n,
16+
hasBundle,
17+
hasCompile,
18+
getEnableRoutesFromUrlQuery,
19+
} from '../../utils';
1520
import { withServerAPI } from '../Manifest';
1621
import OverallActive from 'src/common/svg/navbar/overall-active.svg';
1722
import OverallInActive from 'src/common/svg/navbar/overall-inactive.svg';
@@ -43,7 +48,14 @@ const MenusBase: React.FC<{
4348
const { pathname } = useLocation();
4449
const navigate = useNavigate();
4550
const [navIcon, setNavIcon] = useState(defaultInActive);
46-
const { routes: enableRoutes } = props;
51+
const { routes: apiRoutes } = props;
52+
53+
// Get enableRoutes from URL query as fallback
54+
const urlEnableRoutes = getEnableRoutesFromUrlQuery();
55+
const enableRoutes =
56+
apiRoutes && apiRoutes.length > 0
57+
? apiRoutes
58+
: (urlEnableRoutes as Manifest.RsdoctorManifestClientRoutes[]) || [];
4759

4860
useEffect(() => {
4961
if (pathname.includes('webpack')) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Client } from '@rsdoctor/types';
2+
3+
export const name = 'Upload and Analysis';
4+
5+
export const route = Client.RsdoctorClientRoutes.Uploader;

0 commit comments

Comments
 (0)