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
5 changes: 5 additions & 0 deletions .changeset/mighty-windows-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/devtools': patch
---

feat: add performance tab
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"license": "MIT",
"description": "Qwik devtools monorepo",
"scripts": {
"dev": "pnpm --filter plugin build && MODE=dev pnpm --parallel dev",
"playground": "MODE=dev DEBUG=qwik:devtools:* pnpm --filter playground dev",
"build": "tsx scripts/build-devtools.ts",
"change": "changeset",
Expand Down
4 changes: 2 additions & 2 deletions packages/devtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"README.md"
],
"peerDependencies": {
"@qwik.dev/core": "2.0.0-beta.11",
"@qwik.dev/router": "2.0.0-beta.11",
"@qwik.dev/core": "2.0.0-beta.15",
"@qwik.dev/router": "2.0.0-beta.15",
"vite": "7.1.3",
"@tailwindcss/postcss": "^4.1.14",
"@tailwindcss/vite": "^4.1.14",
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@typescript-eslint/parser": "7.16.1",
"cpy-cli": "^5.0.0",
"eslint": "8.57.0",
"eslint-plugin-qwik": "2.0.0-beta.11",
"eslint-plugin-qwik": "2.0.0-beta.15",
"np": "^8.0.4",
"prettier": "3.3.3",
"typescript": "5.4.5",
Expand Down
19 changes: 11 additions & 8 deletions packages/kit/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,37 @@ import {
} from './globals';
import { ServerRpc, ClientRpc } from './types';

type GlobalTarget = Record<string, unknown>;
const t = target as unknown as GlobalTarget;

export function getViteClientContext(): ViteClientContext {
return target[CLIENT_CTX];
return t[CLIENT_CTX] as ViteClientContext;
}

export function setViteClientContext(ctx: ViteClientContext) {
target[CLIENT_CTX] = ctx;
t[CLIENT_CTX] = ctx;
}

export function getViteServerContext() {
return target[SERVER_CTX];
return t[SERVER_CTX] as ViteServerContext;
}

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

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

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

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

export function setViteClientRpc(rpc: ClientRpc) {
target[CLIENT_RPC] = rpc;
t[CLIENT_RPC] = rpc;
}
72 changes: 67 additions & 5 deletions packages/kit/src/globals.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ViteDevServer } from 'vite';
import { ClientRpc, ServerRpc } from './types';
import { ClientRpc, ParsedStructure, ServerRpc } from './types';

interface EventEmitter {
on: (name: string, handler: (data: any) => void) => void;
Expand All @@ -14,9 +14,71 @@ export const SERVER_CTX = '__qwik_server_ctx__';
export const SERVER_RPC = '__qwik_server_rpc__';
export const CLIENT_RPC = '__qwik_client_rpc__';

// Devtools global state types
export type QwikPerfPhaseRemembered = 'ssr' | 'csr';

export interface QwikPerfErrorRemembered {
name: string;
message: string;
}

export interface QwikPerfEntryRemembered {
id: number;
component: string;
phase: QwikPerfPhaseRemembered;
duration: number;
start: number;
end: number;
error?: QwikPerfErrorRemembered;
/**
* Present for wrapped `_component_` render-function modules; helps de-dupe.
*/
viteId?: string;
/**
* Present for wrapped `_component_` render-function modules.
*/
renderCount?: number;
}

export interface QwikPerfStoreRemembered {
ssr: QwikPerfEntryRemembered[];
csr: QwikPerfEntryRemembered[];

}

export interface DevtoolsRenderStats {
/**
* In-memory performance store written by devtools instrumentation.
* (Populated at runtime; optional in types.)
*/
perf?: QwikPerfStoreRemembered;
}
export interface ComponentDevtoolsState {
hooks: ParsedStructure[];
stats: DevtoolsRenderStats;
}


declare global {
interface Window {
QWIK_DEVTOOLS_GLOBAL_STATE?: Record<string, ComponentDevtoolsState>;
/**
* Performance store (CSR + injected SSR snapshot).
* Written by `@devtools/plugin` instrumentation.
*/
__QWIK_PERF__?: QwikPerfStoreRemembered;
}
}

declare global {
var __qwik_client_ctx__: ViteClientContext;
var __qwik_server_ctx__: ViteServerContext;
var __qwik_server_rpc__: ServerRpc;
var __qwik_client_rpc__: ClientRpc;
// SSR collector lives on `process` (preferred) or `globalThis` via dynamic properties.
// We type the `process` case here to avoid `any` in plugin code.
namespace NodeJS {
interface Process {
__QWIK_SSR_PERF__?: QwikPerfEntryRemembered[];
__QWIK_SSR_PERF_SET__?: Set<string>;
__QWIK_SSR_PERF_ID__?: number;
__QWIK_SSR_PERF_INDEX__?: Record<string, number>;
}
}
}
3 changes: 2 additions & 1 deletion packages/kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './client';
export * from './server';
export * from './context';
export * from './types';
export * from './constants';
export * from './constants';
export * from './globals';
8 changes: 4 additions & 4 deletions packages/playgrounds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"build.preview": "vite build --ssr src/entry.preview.tsx",
"build.types": "tsc --incremental --noEmit",
"deploy": "echo 'Run \"npm run qwik add\" to install a server adapter'",
"dev": "MODE=dev vite --mode ssr",
"dev": "MODE=dev vite --mode ssr --port 5174",
"dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force",
"fmt": "prettier --write .",
"fmt.check": "prettier --check .",
Expand All @@ -29,14 +29,14 @@
"devDependencies": {
"@devtools/plugin": "workspace:*",
"@devtools/ui": "workspace:*",
"@qwik.dev/core": "2.0.0-beta.11",
"@qwik.dev/router": "2.0.0-beta.11",
"@qwik.dev/core": "2.0.0-beta.15",
"@qwik.dev/router": "2.0.0-beta.15",
"@types/eslint": "8.56.10",
"@types/node": "20.14.11",
"@typescript-eslint/eslint-plugin": "7.16.1",
"@typescript-eslint/parser": "7.16.1",
"eslint": "8.57.0",
"eslint-plugin-qwik": "2.0.0-beta.11",
"eslint-plugin-qwik": "2.0.0-beta.15",
"prettier": "3.3.3",
"typescript": "5.4.5",
"vite": "7.1.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"devDependencies": {
"@babel/types": "^7.26.7",
"@devtools/kit": "workspace:*",
"@qwik.dev/core": "2.0.0-beta.11",
"@qwik.dev/core": "2.0.0-beta.15",
"@types/eslint": "8.56.10",
"@types/node": "20.14.11",
"@typescript-eslint/eslint-plugin": "7.16.1",
Expand Down
115 changes: 1 addition & 114 deletions packages/plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,114 +1 @@
import { ResolvedConfig, type Plugin } from 'vite';
import { getServerFunctions } from './rpc';
import { createServerRpc, setViteServerContext, VIRTUAL_QWIK_DEVTOOLS_KEY, INNER_USE_HOOK } from '@devtools/kit';
import VueInspector from 'vite-plugin-inspect'
import useCollectHooksSource from './utils/useCollectHooks'
import { parseQwikCode } from './parse/parse';
import { startPreloading } from './npm/index';
import updateConf from './utils/updateConf';
import {debug} from 'debug'

const log = debug('qwik:devtools:plugin');
export function qwikDevtools(): Plugin[] {
let _config: ResolvedConfig;
const qwikData = new Map<string, any>();
let preloadStarted = false;
const qwikDevtoolsPlugin: Plugin = {
name: 'vite-plugin-qwik-devtools',
apply: 'serve',
resolveId(id) {
// Normalize to a stable, absolute-like id so Qwik can generate runtime chunks
const clean = id.split('?')[0].split('#')[0];
if (
clean === VIRTUAL_QWIK_DEVTOOLS_KEY ||
clean === `/${VIRTUAL_QWIK_DEVTOOLS_KEY}` ||
clean === `\u0000${VIRTUAL_QWIK_DEVTOOLS_KEY}` ||
clean === `/@id/${VIRTUAL_QWIK_DEVTOOLS_KEY}`
) {
return `/${VIRTUAL_QWIK_DEVTOOLS_KEY}`;
}
},
load(id) {
if (
id === `/${VIRTUAL_QWIK_DEVTOOLS_KEY}` ||
id === VIRTUAL_QWIK_DEVTOOLS_KEY ||
id === `\u0000${VIRTUAL_QWIK_DEVTOOLS_KEY}` ||
id === `/@id/${VIRTUAL_QWIK_DEVTOOLS_KEY}`
) {
return {
code: useCollectHooksSource,
map: { mappings: '' },
};
}
},
configResolved(viteConfig) {
_config = viteConfig;
updateConf(_config);
// Start preloading as early as possible, right after config is resolved
if (!preloadStarted) {
preloadStarted = true;
startPreloading({ config: _config }).catch((err) => {
log('[Qwik DevTools] Failed to start preloading:', err);
});
}
},
transform: {
order: 'pre',
handler(code, id) {
const mode = process.env.MODE;
// Ensure virtual import is present at the very top once when a component$ is present
if (id.endsWith('.tsx') && code.includes('component$')) {
if (!code.includes(VIRTUAL_QWIK_DEVTOOLS_KEY)) {

const importLine = `import { ${INNER_USE_HOOK} } from '${VIRTUAL_QWIK_DEVTOOLS_KEY}';\n`
code = importLine + code
}else {
log('importing virtual qwik devtools', VIRTUAL_QWIK_DEVTOOLS_KEY, code);
}
code = parseQwikCode(code, {path: id})
}
// Only transform the root component file
if (id.endsWith('root.tsx')) {
const importPath =
mode === 'dev' ? '@devtools/ui' : '@qwik.dev/devtools/ui';
// Check if QwikDevtools import already exists
if (!code.includes(importPath)) {
// Add import for QwikDevtools using the correct package name
code = `import { QwikDevtools } from '${importPath}';\n${code}`;
}

// Find the closing body tag and append QwikDevtools at the end of body
const match = code.match(/<body[^>]*>([\s\S]*?)<\/body>/);
if (match) {
const bodyContent = match[1];
const newBodyContent = `${bodyContent}\n <QwikDevtools />`;
code = code.replace(bodyContent, newBodyContent);
}

return {
code,
map: null,
};
}

return {
code,
map: { mappings: '' },
};
},
},
configureServer(server) {
setViteServerContext(server as any);

const rpcFunctions = getServerFunctions({ server, config: _config, qwikData });

createServerRpc(rpcFunctions);

// Preloading should have already started in configResolved
},
}
return [
qwikDevtoolsPlugin,
VueInspector(), // Add the VueInspector plugin instance
];
}
export { qwikDevtools } from './plugin';
Loading
Loading