Skip to content

Commit 5ccb7c3

Browse files
committed
feat: enhance Qwik development tools with build analysis and SSR performance tracking
- Introduced new `Preloads` and `Build Analysis` panels in DevTools for improved insights. - Added runtime instrumentation for SSR/CSR performance and preload tracking. - Expanded plugin capabilities to generate build-analysis reports and enforce security around RPC calls. - Updated SSR performance middleware to normalize accept headers for better request handling.
1 parent 7fbc30d commit 5ccb7c3

5 files changed

Lines changed: 97 additions & 13 deletions

File tree

packages/kit/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export interface BuildAnalysisStatus {
2929
exists: boolean;
3030
reportPath: string;
3131
buildCommand: string | null;
32+
canTriggerBuild: boolean;
33+
buildTriggerHint?: string;
3234
}
3335

3436
export interface BuildAnalysisRunResult {

packages/plugin/src/build-analysis/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { ServerContext } from '../types';
1414
import { detectPackageManager } from '../npm';
1515
import {
1616
getBuildAnalysisRpcGuardError,
17+
getBuildAnalysisRpcGuardHint,
1718
isBuildAnalysisRpcAllowed,
1819
} from './security';
1920

@@ -258,11 +259,16 @@ export function getBuildAnalysisFunctions(
258259
async getBuildAnalysisStatus(): Promise<BuildAnalysisStatus> {
259260
const reportPath = resolveBuildAnalysisHtmlPath(ctx.config.root);
260261
const { command } = await resolveBuildScript(ctx.config.root);
262+
const rpcClient = getServerRpcRequestContext()?.client;
263+
const canTriggerBuild = isBuildAnalysisRpcAllowed(rpcClient);
261264

262265
return {
263266
exists: await fileExists(reportPath),
264267
reportPath,
265268
buildCommand: command,
269+
canTriggerBuild,
270+
buildTriggerHint:
271+
command && !canTriggerBuild ? getBuildAnalysisRpcGuardHint() : undefined,
266272
};
267273
},
268274
async buildBuildAnalysisReport(): Promise<BuildAnalysisRunResult> {

packages/plugin/src/build-analysis/security.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ describe('build analysis RPC security', () => {
1313
expect(isBuildAnalysisRpcAllowed({ socket: { remoteAddress: '::ffff:127.0.0.1' } }, {})).toBe(
1414
true,
1515
);
16+
expect(
17+
isBuildAnalysisRpcAllowed(
18+
{ socket: { _socket: { remoteAddress: '127.0.0.1' } } },
19+
{},
20+
),
21+
).toBe(true);
1622
});
1723

1824
test('rejects non-loopback websocket clients by default', () => {
@@ -39,6 +45,9 @@ describe('build analysis RPC security', () => {
3945
expect(getRpcClientRemoteAddress({ socket: { remoteAddress: '127.0.0.1' } })).toBe(
4046
'127.0.0.1',
4147
);
48+
expect(
49+
getRpcClientRemoteAddress({ socket: { _socket: { remoteAddress: '127.0.0.1' } } }),
50+
).toBe('127.0.0.1');
4251
expect(getRpcClientRemoteAddress({ _socket: { remoteAddress: '127.0.0.1' } })).toBe(
4352
'127.0.0.1',
4453
);

packages/plugin/src/build-analysis/security.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const TRUTHY_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']);
33

44
type RpcClientSocket = {
55
remoteAddress?: string | null;
6+
socket?: unknown;
7+
_socket?: unknown;
68
};
79

810
type RpcClientLike = {
@@ -24,7 +26,41 @@ export function getRpcClientRemoteAddress(client: unknown): string | undefined {
2426
}
2527

2628
const candidate = client as RpcClientLike;
27-
return candidate.socket?.remoteAddress ?? candidate._socket?.remoteAddress ?? undefined;
29+
return (
30+
getSocketRemoteAddress(candidate.socket) ?? getSocketRemoteAddress(candidate._socket)
31+
);
32+
}
33+
34+
function getSocketRemoteAddress(socket: unknown): string | undefined {
35+
if (!socket || typeof socket !== 'object') {
36+
return undefined;
37+
}
38+
39+
const queue: RpcClientSocket[] = [socket as RpcClientSocket];
40+
const seen = new Set<object>();
41+
42+
while (queue.length > 0) {
43+
const current = queue.shift();
44+
if (!current || seen.has(current)) {
45+
continue;
46+
}
47+
48+
seen.add(current);
49+
50+
if (typeof current.remoteAddress === 'string' && current.remoteAddress.length > 0) {
51+
return current.remoteAddress;
52+
}
53+
54+
if (current.socket && typeof current.socket === 'object') {
55+
queue.push(current.socket as RpcClientSocket);
56+
}
57+
58+
if (current._socket && typeof current._socket === 'object') {
59+
queue.push(current._socket as RpcClientSocket);
60+
}
61+
}
62+
63+
return undefined;
2864
}
2965

3066
export function isLoopbackAddress(address: string | undefined): boolean {
@@ -50,3 +86,7 @@ export function isBuildAnalysisRpcAllowed(
5086
export function getBuildAnalysisRpcGuardError(): string {
5187
return `Refusing to run the project build from a non-local DevTools RPC client. Reconnect from localhost or set ${REMOTE_BUILD_ANALYSIS_ENV}=1 to opt in to remote build-analysis execution.`;
5288
}
89+
90+
export function getBuildAnalysisRpcGuardHint(): string {
91+
return `Automatic rebuild is unavailable from this DevTools client. Reconnect from localhost or set ${REMOTE_BUILD_ANALYSIS_ENV}=1 to opt in to remote build-analysis execution.`;
92+
}

packages/ui/src/features/BuildAnalysis/BuildAnalysis.tsx

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ export const BuildAnalysis = component$(() => {
1313
const isChecking = useSignal(true);
1414
const isBuilding = useSignal(false);
1515
const hasReport = useSignal(false);
16+
const canTriggerBuild = useSignal(false);
1617
const reportPath = useSignal('');
1718
const buildCommand = useSignal<string | null>(null);
19+
const buildTriggerHint = useSignal('');
1820
const errorMessage = useSignal('');
1921

2022
const loadStatus = $(async () => {
@@ -26,8 +28,10 @@ export const BuildAnalysis = component$(() => {
2628
const status = await rpc.getBuildAnalysisStatus();
2729

2830
hasReport.value = status.exists;
31+
canTriggerBuild.value = status.canTriggerBuild;
2932
reportPath.value = status.reportPath;
3033
buildCommand.value = status.buildCommand;
34+
buildTriggerHint.value = status.buildTriggerHint || '';
3135
} catch (error) {
3236
errorMessage.value =
3337
error instanceof Error ? error.message : 'Failed to load build analysis status.';
@@ -47,6 +51,12 @@ export const BuildAnalysis = component$(() => {
4751
return;
4852
}
4953

54+
if (!canTriggerBuild.value) {
55+
errorMessage.value =
56+
buildTriggerHint.value || 'Automatic rebuild is unavailable from this DevTools client.';
57+
return;
58+
}
59+
5060
const confirmed =
5161
globalThis.confirm?.(
5262
`Build analysis needs a fresh build to generate the report.\n\nRun: ${buildCommand.value} ?`,
@@ -96,6 +106,11 @@ export const BuildAnalysis = component$(() => {
96106
{buildCommand.value}
97107
</code>
98108
) : null}
109+
{buildTriggerHint.value ? (
110+
<div class="rounded-xl border border-amber-500/20 bg-amber-500/10 px-3 py-2 text-sm text-amber-200">
111+
{buildTriggerHint.value}
112+
</div>
113+
) : null}
99114
{errorMessage.value ? (
100115
<div class="rounded-xl border border-red-500/20 bg-red-500/10 px-3 py-2 text-sm text-red-300">
101116
{errorMessage.value}
@@ -110,7 +125,17 @@ export const BuildAnalysis = component$(() => {
110125
</div>
111126
) : hasReport.value ? (
112127
<>
113-
<div class="flex items-center justify-end">
128+
<div class="flex items-center justify-end gap-3">
129+
{buildCommand.value && canTriggerBuild.value ? (
130+
<button
131+
type="button"
132+
onClick$={buildReport}
133+
disabled={isBuilding.value}
134+
class="border-border bg-foreground/5 hover:bg-foreground/10 disabled:cursor-not-allowed disabled:opacity-50 rounded-xl border px-4 py-2 text-sm transition-colors"
135+
>
136+
{isBuilding.value ? 'Rebuilding...' : 'Rebuild Report'}
137+
</button>
138+
) : null}
114139
<button
115140
type="button"
116141
onClick$={refreshFrame}
@@ -134,22 +159,24 @@ export const BuildAnalysis = component$(() => {
134159
<div class="flex max-w-3xl flex-col gap-4">
135160
<div class="text-base font-semibold">No build analysis report yet</div>
136161
<div class="text-muted-foreground text-sm leading-6">
137-
The visualizer HTML file does not exist yet. You need to build the
138-
current project first, and DevTools will ask for your confirmation
139-
before running that build.
162+
{canTriggerBuild.value
163+
? 'The visualizer HTML file does not exist yet. You need to build the current project first, and DevTools will ask for your confirmation before running that build.'
164+
: 'The visualizer HTML file does not exist yet. Run the build command locally to generate it, or reconnect from localhost if you want DevTools to trigger it for you.'}
140165
</div>
141166
<code class="text-muted-foreground rounded-lg bg-foreground/5 px-3 py-2 text-xs">
142167
{reportPath.value}
143168
</code>
144169
<div>
145-
<button
146-
type="button"
147-
onClick$={buildReport}
148-
disabled={isBuilding.value || !buildCommand.value}
149-
class="border-border bg-foreground/5 hover:bg-foreground/10 disabled:cursor-not-allowed disabled:opacity-50 rounded-xl border px-4 py-2 text-sm transition-colors"
150-
>
151-
{isBuilding.value ? 'Building...' : 'Build Report'}
152-
</button>
170+
{buildCommand.value && canTriggerBuild.value ? (
171+
<button
172+
type="button"
173+
onClick$={buildReport}
174+
disabled={isBuilding.value}
175+
class="border-border bg-foreground/5 hover:bg-foreground/10 disabled:cursor-not-allowed disabled:opacity-50 rounded-xl border px-4 py-2 text-sm transition-colors"
176+
>
177+
{isBuilding.value ? 'Building...' : 'Build Report'}
178+
</button>
179+
) : null}
153180
</div>
154181
</div>
155182
</div>

0 commit comments

Comments
 (0)