Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
File renamed without changes.
9 changes: 9 additions & 0 deletions .changeset/huge-clubs-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@qwik.dev/devtools': patch
---

feat: add preload/build analysis tooling and richer devtools instrumentation

- Added new `Preloads` and `Build Analysis` panels, plus an improved `Inspect` view that resolves correctly from the app base URL on deep routes.
- Added runtime instrumentation for SSR/CSR performance and preload tracking, including SSR preload snapshots, QRL-to-resource correlation, and richer diagnostics surfaced in DevTools.
- Expanded the plugin and RPC layer to generate and serve build-analysis reports, expose the new preload/performance data to the UI, and add server-side guards around build-analysis execution.
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,7 @@ testem.log
Thumbs.db
.vite-inspect
.pnpm-store/*
related-qwik/*
.cursor/skills/*
qwik/*
.cursor/skills/*
related-folder/**
**/.qwik-devtools/
15 changes: 10 additions & 5 deletions packages/devtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@
"version": "0.2.8",
"license": "MIT",
"main": "./dist/plugin/index.mjs",
"types": "./dist/plugin/index.d.mts",
"description": "Qwik devtools package",
"qwik": "./dist/ui/index.qwik.mjs",
"exports": {
".": {
"import": "./dist/plugin/index.js",
"types": "./dist/plugin/index.d.ts"
"import": "./dist/plugin/index.mjs",
"types": "./dist/plugin/index.d.mts"
},
"./ui": {
"import": "./dist/ui/index.qwik.mjs",
"types": "./dist/ui/lib-types/ui/src/index.d.ts",
"style": "./dist/ui/style.css"
"style": "./dist/ui/styles.css"
},
"./ui/styles.css": {
"import": "./dist/ui/styles.css"
},
"./style": {
"import": "./dist/ui/theme.css"
"import": "./dist/ui/styles.css"
}
},
"files": [
Expand All @@ -35,6 +39,7 @@
"birpc": "^4.0.0",
"dree": "^5.1.5",
"oxc-parser": "^0.120.0",
"rollup-plugin-visualizer": "^6.0.3",
"superjson": "^2.2.6",
"vite-hot-client": "^2.1.0",
"vite-plugin-inspect": "^11.3.3"
Expand Down Expand Up @@ -64,4 +69,4 @@
"url": "https://github.com/QwikDev/devtools/issues"
},
"homepage": "https://github.com/QwikDev/devtools#readme"
}
}
13 changes: 5 additions & 8 deletions packages/kit/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import SuperJSON from 'superjson';
import { ClientFunctions, ServerFunctions } from './types';
import { createBirpc } from 'birpc';
import { DEVTOOLS_VITE_MESSAGING_EVENT } from './constants';
import { getViteClientContext, setViteClientRpc } from './context';
import { createSerializedRpc } from './rpc-core';

export function createClientRpc(functions: ClientFunctions) {
const client = getViteClientContext();

const rpc = createBirpc<ServerFunctions, ClientFunctions>(functions, {
post: (data) =>
client.send(DEVTOOLS_VITE_MESSAGING_EVENT, SuperJSON.stringify(data)),
on: (fn) =>
const rpc = createSerializedRpc<ServerFunctions, ClientFunctions>(functions, {
post: (data) => client.send(DEVTOOLS_VITE_MESSAGING_EVENT, data),
on: (handler) =>
client.on(DEVTOOLS_VITE_MESSAGING_EVENT, (data) => {
fn(SuperJSON.parse(data));
handler(data);
}),
timeout: 120_000,
});

setViteClientRpc(rpc);
Expand Down
98 changes: 41 additions & 57 deletions packages/kit/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,42 @@
export const DEVTOOLS_VITE_MESSAGING_EVENT = 'qwik_tools:vite_messaging_event';
export const USE_HOOK_LIST = [
'useAsyncComputed',
'useComputed',
'useConstant',
'useContext',
'useContextProvider',
'useErrorBoundary',
'useId',
'useOn',
'useOnDocument',
'useOnWindow',
'useResource',
'useSerializer',
'useServerData',
'useSignal',
'useStore',
'useStyles',
'useStylesScoped',
'useTask',
'useVisibleTask',
'useLocation',
'useNavigate',
'usePreventNavigate',
'useContent',
'useDocumentHead',
] as const


export const VARIABLE_DECLARATION_LIST = [
'useStore',
'useSignal',
'useComputed',
'useAsyncComputed',
'useContext',
'useId',
'useStyles',
'useStylesScoped',
'useConstant',
'useErrorBoundary',
'useSerializer',
'useServerData',
'useLocation',
'useNavigate',
'useContent',
'useDocumentHead',
] as const


export const EXPRESSION_STATEMENT_LIST = [
'useVisibleTask',
'useTask',
'useResource',
'useContextProvider',
'usePreventNavigate',
] as const
const HOOK_GROUPS = {
variableDeclaration: [
'useStore',
'useSignal',
'useComputed',
'useAsyncComputed',
'useContext',
'useId',
'useStyles',
'useStylesScoped',
'useConstant',
'useErrorBoundary',
'useSerializer',
'useServerData',
'useLocation',
'useNavigate',
'useContent',
'useDocumentHead',
] as const,
expressionStatement: [
'useVisibleTask',
'useTask',
'useResource',
'useContextProvider',
'usePreventNavigate',
] as const,
listener: ['useOn', 'useOnDocument', 'useOnWindow'] as const,
noReturn: ['useVisibleTask', 'useTask'] as const,
} as const;

export const VARIABLE_DECLARATION_LIST = HOOK_GROUPS.variableDeclaration;
export const EXPRESSION_STATEMENT_LIST = HOOK_GROUPS.expressionStatement;
export const USE_HOOK_LIST = [
...VARIABLE_DECLARATION_LIST,
...HOOK_GROUPS.listener,
...EXPRESSION_STATEMENT_LIST,
] as const;

export const QSEQ = 'q:seq';
export const QPROPS = 'q:props';
Expand All @@ -62,13 +45,14 @@ export const QTYPE = 'q:type';

export const VIRTUAL_QWIK_DEVTOOLS_KEY = 'virtual-qwik-devtools.ts';

export const INNER_USE_HOOK= 'useCollectHooks'
export const INNER_USE_HOOK = 'useCollectHooks';

export const QWIK_DEVTOOLS_GLOBAL_STATE = 'QWIK_DEVTOOLS_GLOBAL_STATE'
export const QWIK_DEVTOOLS_GLOBAL_STATE = 'QWIK_DEVTOOLS_GLOBAL_STATE';
export const QWIK_PRELOADS_UPDATE_EVENT = 'qwik:preloads-update';

export const QRL_KEY = '$qrl$';
export const COMPUTED_QRL_KEY = '$computeQrl$';
export const CHUNK_KEY = '$chunk$';
export const CAPTURE_REF_KEY = '$captureRef$';

export const NORETURN_HOOK = [ 'useVisibleTask', 'useTask'] as const
export const NORETURN_HOOK = HOOK_GROUPS.noReturn;
46 changes: 15 additions & 31 deletions packages/kit/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { target } from './shared';
import {
ViteClientContext,
CLIENT_CTX,
Expand All @@ -8,38 +7,23 @@ import {
CLIENT_RPC,
} from './globals';
import { ServerRpc, ClientRpc } from './types';
import { createGlobalAccessor } from './global-store';

type GlobalTarget = Record<string, unknown>;
const t = target as unknown as GlobalTarget;
const clientContextAccessor =
createGlobalAccessor<ViteClientContext>(CLIENT_CTX);
const serverContextAccessor =
createGlobalAccessor<ViteServerContext>(SERVER_CTX);
const serverRpcAccessor = createGlobalAccessor<ServerRpc>(SERVER_RPC);
const clientRpcAccessor = createGlobalAccessor<ClientRpc>(CLIENT_RPC);

export function getViteClientContext(): ViteClientContext {
return t[CLIENT_CTX] as ViteClientContext;
}
export const getViteClientContext = clientContextAccessor.get;
export const setViteClientContext = clientContextAccessor.set;

export function setViteClientContext(ctx: ViteClientContext) {
t[CLIENT_CTX] = ctx;
}
export const getViteServerContext = serverContextAccessor.get;
export const setViteServerContext = serverContextAccessor.set;

export function getViteServerContext() {
return t[SERVER_CTX] as ViteServerContext;
}
export const getViteServerRpc = serverRpcAccessor.get;
export const setViteServerRpc = serverRpcAccessor.set;

export function setViteServerContext(ctx: ViteServerContext) {
t[SERVER_CTX] = ctx;
}

export function getViteServerRpc() {
return t[SERVER_RPC] as ServerRpc;
}

export function setViteServerRpc(rpc: ServerRpc) {
t[SERVER_RPC] = rpc;
}

export function getViteClientRpc() {
return t[CLIENT_RPC] as ClientRpc;
}

export function setViteClientRpc(rpc: ClientRpc) {
t[CLIENT_RPC] = rpc;
}
export const getViteClientRpc = clientRpcAccessor.get;
export const setViteClientRpc = clientRpcAccessor.set;
14 changes: 14 additions & 0 deletions packages/kit/src/global-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { target } from './shared';

type GlobalStore = Record<string, unknown>;

const globalStore = target as unknown as GlobalStore;

export function createGlobalAccessor<T>(key: string) {
return {
get: () => globalStore[key] as T,
set: (value: T) => {
globalStore[key] = value;
},
};
}
79 changes: 79 additions & 0 deletions packages/kit/src/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,82 @@ export interface QwikPerfStoreRemembered {

}

export type QwikPreloadStatus = 'pending' | 'loaded' | 'error' | 'unknown';
export type QwikPreloadSource =
| 'initial-dom'
| 'mutation'
| 'performance'
| 'qrl-correlation';
export type QwikPreloadOriginKind =
| 'current-project'
| 'vite-plugin-injected'
| 'node_modules'
| 'virtual-module'
| 'generated'
| 'external'
| 'unknown';
export type QwikPreloadPhase = 'csr' | 'ssr' | 'unknown';
export type QwikPreloadMatchMode =
| 'href'
| 'normalized-href'
| 'chunk-hash'
| 'resource-name'
| 'none';
export type QwikPreloadLoadMatchQuality = 'best-effort' | 'none';

export interface QwikPreloadQrlRequestRemembered {
symbol: string;
href?: string;
normalizedHref?: string;
requestedAt: number;
originKind?: QwikPreloadOriginKind;
phase?: QwikPreloadPhase;
matchedEntryId?: number;
}

export interface QwikPreloadEntryRemembered {
id: number;
href: string;
normalizedHref: string;
rel: string;
as: string;
resourceType: string;
status: QwikPreloadStatus;
source: QwikPreloadSource;
originKind: QwikPreloadOriginKind;
phase: QwikPreloadPhase;
discoveredAt: number;
requestedAt?: number;
completedAt?: number;
importDuration?: number;
loadDuration?: number;
duration?: number;
transferSize?: number;
decodedBodySize?: number;
initiatorType?: string;
qrlSymbol?: string;
qrlRequestedAt?: number;
qrlToLoadDuration?: number;
loadMatchQuality?: QwikPreloadLoadMatchQuality;
matchedBy: QwikPreloadMatchMode;
error?: string;
}

export type QwikSsrPreloadSnapshotRemembered =
Partial<QwikPreloadEntryRemembered> &
Pick<QwikPreloadEntryRemembered, 'href'>;

export interface QwikPreloadStoreRemembered {
entries: QwikPreloadEntryRemembered[];
qrlRequests: QwikPreloadQrlRequestRemembered[];
startedAt: number;
clear: () => void;
_id: number;
_initialized: boolean;
_byHref: Record<string, number>;
_byId: Record<number, QwikPreloadEntryRemembered>;
}

export interface DevtoolsRenderStats {
/**
* In-memory performance store written by devtools instrumentation.
Expand All @@ -67,6 +143,8 @@ declare global {
* Written by `@devtools/plugin` instrumentation.
*/
__QWIK_PERF__?: QwikPerfStoreRemembered;
__QWIK_PRELOADS__?: QwikPreloadStoreRemembered;
__QWIK_SSR_PRELOADS__?: QwikSsrPreloadSnapshotRemembered[];
}
}

Expand All @@ -76,6 +154,7 @@ declare global {
namespace NodeJS {
interface Process {
__QWIK_SSR_PERF__?: QwikPerfEntryRemembered[];
__QWIK_SSR_PRELOADS__?: QwikSsrPreloadSnapshotRemembered[];
__QWIK_SSR_PERF_SET__?: Set<string>;
__QWIK_SSR_PERF_ID__?: number;
__QWIK_SSR_PERF_INDEX__?: Record<string, number>;
Expand Down
Loading
Loading