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

fix: click-to-source opens the right line when devtools or mcp is enabled
4 changes: 2 additions & 2 deletions packages/devtools/kit/src/rpc-core.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import SuperJSON from 'superjson';
import { createBirpc, type EventOptions } from 'birpc';
import { createBirpc, type BirpcResolver } from 'birpc';

interface RpcChannel {
post: (serialized: string) => void;
on: (handler: (serialized: string) => void) => void;
resolver?: EventOptions['resolver'];
resolver?: BirpcResolver<unknown>;
}

const RPC_TIMEOUT = 120_000;
Expand Down
228 changes: 26 additions & 202 deletions packages/devtools/plugin/src/parse/__snapshots__/parse.unit.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ interface ButtonProps {
testValue: Signal<string>;
}

export default component$<ButtonProps>((props) => {
const collecthook = useCollectHooks("/abs/path/Button.tsx_default")
export default component$<ButtonProps>((props) => { const collecthook = useCollectHooks("/abs/path/Button.tsx_default");
const { class: className = '', onClick$ } = props;
const testValue2 = props.testValue
console.log('testValue', testValue2)
Expand All @@ -48,97 +47,27 @@ export default component$<ButtonProps>((props) => {
dd:12,
cc: 33,
aa: [1,2,3 ]
});
collecthook({
variableName: 'store',
hookType: 'useStore',
category: 'VariableDeclarator',
data: store
});

const signal = useSignal<any>('111');
collecthook({
variableName: 'signal',
hookType: 'useSignal',
category: 'VariableDeclarator',
data: signal
});

const constantValue = useConstant(() => 'CONST');
collecthook({
variableName: 'constantValue',
hookType: 'useConstant',
category: 'VariableDeclarator',
data: constantValue
});

const serverData = useServerData<any>('demo-key');
collecthook({
variableName: 'serverData',
hookType: 'useServerData',
category: 'VariableDeclarator',
data: serverData
});

const location = useLocation();
collecthook({
variableName: 'location',
hookType: 'useLocation',
category: 'VariableDeclarator',
data: location
});

const navigate = useNavigate();
collecthook({
variableName: 'navigate',
hookType: 'useNavigate',
category: 'VariableDeclarator',
data: navigate
});

const content = useContent();
collecthook({
variableName: 'content',
hookType: 'useContent',
category: 'VariableDeclarator',
data: content
});

const head = useDocumentHead();
collecthook({
variableName: 'head',
hookType: 'useDocumentHead',
category: 'VariableDeclarator',
data: head
});

}); collecthook({ variableName: 'store', hookType: 'useStore', category: 'VariableDeclarator', data: store });
const signal = useSignal<any>('111'); collecthook({ variableName: 'signal', hookType: 'useSignal', category: 'VariableDeclarator', data: signal });
const constantValue = useConstant(() => 'CONST'); collecthook({ variableName: 'constantValue', hookType: 'useConstant', category: 'VariableDeclarator', data: constantValue });
const serverData = useServerData<any>('demo-key'); collecthook({ variableName: 'serverData', hookType: 'useServerData', category: 'VariableDeclarator', data: serverData });
const location = useLocation(); collecthook({ variableName: 'location', hookType: 'useLocation', category: 'VariableDeclarator', data: location });
const navigate = useNavigate(); collecthook({ variableName: 'navigate', hookType: 'useNavigate', category: 'VariableDeclarator', data: navigate });
const content = useContent(); collecthook({ variableName: 'content', hookType: 'useContent', category: 'VariableDeclarator', data: content });
const head = useDocumentHead(); collecthook({ variableName: 'head', hookType: 'useDocumentHead', category: 'VariableDeclarator', data: head });



useTask$(({ track }) => {
track(() => store.count);
signal.value = '33333'
})
collecthook({
variableName: 'useTask',
hookType: 'useTask',
category: 'expressionStatement',
data: undefined
});

}); collecthook({ variableName: 'useTask', hookType: 'useTask', category: 'expressionStatement', data: undefined });

// eslint-disable-next-line qwik/no-use-visible-task
useVisibleTask$(({ track }) => {
track(() => store.count);
signal.value = '2227'
})
collecthook({
variableName: 'useVisibleTask',
hookType: 'useVisibleTask',
category: 'expressionStatement',
data: undefined
});

}); collecthook({ variableName: 'useVisibleTask', hookType: 'useVisibleTask', category: 'expressionStatement', data: undefined });

const qwikContainer = useComputed$(() => {
try {
Expand All @@ -151,102 +80,32 @@ export default component$<ButtonProps>((props) => {
console.error(error);
return null;
}
});
collecthook({
variableName: 'qwikContainer',
hookType: 'useComputed',
category: 'VariableDeclarator',
data: qwikContainer
});

}); collecthook({ variableName: 'qwikContainer', hookType: 'useComputed', category: 'VariableDeclarator', data: qwikContainer });

const asyncComputedValue = useAsyncComputed$(({ track }) =>
Promise.resolve(track(signal) + 3),
);
collecthook({
variableName: 'asyncComputedValue',
hookType: 'useAsyncComputed',
category: 'VariableDeclarator',
data: asyncComputedValue
});

); collecthook({ variableName: 'asyncComputedValue', hookType: 'useAsyncComputed', category: 'VariableDeclarator', data: asyncComputedValue });

useContextProvider(ButtonContext, {
theme: 'primary',
size: 'large'
});
collecthook({
variableName: 'useContextProvider',
hookType: 'useContextProvider',
category: 'expressionStatement',
data: undefined
});

let _customhook_0 = useHooks('Button');
collecthook({
variableName: '_customhook_0',
hookType: 'customhook',
category: 'VariableDeclarator',
data: _customhook_0
});

const context = useContext(ButtonContext);
collecthook({
variableName: 'context',
hookType: 'useContext',
category: 'VariableDeclarator',
data: context
});


const buttonId = useId();
collecthook({
variableName: 'buttonId',
hookType: 'useId',
category: 'VariableDeclarator',
data: buttonId
});
}); collecthook({ variableName: 'useContextProvider', hookType: 'useContextProvider', category: 'expressionStatement', data: undefined });
let _customhook_0 = useHooks('Button'); collecthook({ variableName: '_customhook_0', hookType: 'customhook', category: 'VariableDeclarator', data: _customhook_0 });
const context = useContext(ButtonContext); collecthook({ variableName: 'context', hookType: 'useContext', category: 'VariableDeclarator', data: context });

const buttonId = useId(); collecthook({ variableName: 'buttonId', hookType: 'useId', category: 'VariableDeclarator', data: buttonId });

useOnDocument('keydown', $(() => {
console.log('Document keydown event');
}));
collecthook({
variableName: 'useOnDocument',
hookType: 'useOnDocument',
category: 'expressionStatement',
data: undefined
});

const dd = useHooks('Button1')
collecthook({
variableName: 'dd',
hookType: 'customhook',
category: 'VariableDeclarator',
data: dd
});

})); collecthook({ variableName: 'useOnDocument', hookType: 'useOnDocument', category: 'expressionStatement', data: undefined });
const dd = useHooks('Button1'); collecthook({ variableName: 'dd', hookType: 'customhook', category: 'VariableDeclarator', data: dd });
useOnWindow('resize', $(() => {
console.log('Window resized');
}));
collecthook({
variableName: 'useOnWindow',
hookType: 'useOnWindow',
category: 'expressionStatement',
data: undefined
});

})); collecthook({ variableName: 'useOnWindow', hookType: 'useOnWindow', category: 'expressionStatement', data: undefined });

useOn('click', $(() => {
console.log('Host clicked');
}));
collecthook({
variableName: 'useOn',
hookType: 'useOn',
category: 'expressionStatement',
data: undefined
});

})); collecthook({ variableName: 'useOn', hookType: 'useOn', category: 'expressionStatement', data: undefined });

const resourceData = useResource$(async ({ track }) => {
track(() => store.count);
Expand All @@ -255,29 +114,15 @@ export default component$<ButtonProps>((props) => {
message: \`Resource data for count: \${store.count}\`,
timestamp: Date.now()
};
});
collecthook({
variableName: 'resourceData',
hookType: 'useResource',
category: 'VariableDeclarator',
data: resourceData
});

}); collecthook({ variableName: 'resourceData', hookType: 'useResource', category: 'VariableDeclarator', data: resourceData });

const customSerialized = useSerializer$(() => ({
deserialize: () => ({ n: store.count }),
update: (current: { n: number }) => {
current.n = store.count;
return current;
}
}));
collecthook({
variableName: 'customSerialized',
hookType: 'useSerializer',
category: 'VariableDeclarator',
data: customSerialized
});

})); collecthook({ variableName: 'customSerialized', hookType: 'useSerializer', category: 'VariableDeclarator', data: customSerialized });

useStyles$(\`
.custom-button {
Expand All @@ -292,14 +137,7 @@ export default component$<ButtonProps>((props) => {
.custom-button:hover {
transform: scale(1.05);
}
\`);
collecthook({
variableName: 'useStyles',
hookType: 'useStyles',
category: 'expressionStatement',
data: undefined
});

\`); collecthook({ variableName: 'useStyles', hookType: 'useStyles', category: 'expressionStatement', data: undefined });

useStylesScoped$(\`
.scoped-button {
Expand All @@ -310,27 +148,13 @@ export default component$<ButtonProps>((props) => {
border-radius: 3px;
cursor: pointer;
}
\`);
collecthook({
variableName: 'useStylesScoped',
hookType: 'useStylesScoped',
category: 'expressionStatement',
data: undefined
});

\`); collecthook({ variableName: 'useStylesScoped', hookType: 'useStylesScoped', category: 'expressionStatement', data: undefined });
const debounce = useDebouncer(
$((value: string) => {
signal.value = value;
}),
1000
);
collecthook({
variableName: 'debounce',
hookType: 'customhook',
category: 'VariableDeclarator',
data: debounce
});

); collecthook({ variableName: 'debounce', hookType: 'customhook', category: 'VariableDeclarator', data: debounce });


const handleClick = $(async () => {
Expand Down
40 changes: 3 additions & 37 deletions packages/devtools/plugin/src/parse/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,36 +77,9 @@ export function isCustomHook(hookName: string): boolean {
}

// ============================================================================
// Code Position Utilities
// Statement Utilities
// ============================================================================

/** Finds the start position of the line containing the given index */
export function findLineStart(code: string, index: number): number {
for (let i = index - 1; i >= 0; i--) {
const ch = code[i];
if (ch === '\n' || ch === '\r') {
return i + 1;
}
}
return 0;
}

/** Reads the indentation (spaces/tabs) starting from a given position */
export function readIndent(code: string, indexFrom: number): string {
let indent = '';
let i = indexFrom;
while (i < code.length) {
const ch = code[i];
if (ch === ' ' || ch === '\t') {
indent += ch;
i++;
} else {
break;
}
}
return indent;
}

/** Removes trailing semicolon and whitespace from a code segment */
export function trimStatementSemicolon(segment: string): string {
return segment.trim().replace(/;?\s*$/, '');
Expand All @@ -118,21 +91,14 @@ export function trimStatementSemicolon(segment: string): string {

export type CollecthookCategory = 'VariableDeclarator' | 'expressionStatement';

/** Builds the collecthook() call code with proper indentation */
/** Builds a single-line collecthook() call so injected tracking never adds source lines */
export function buildCollecthookPayload(
indent: string,
variableName: string,
hookType: string,
category: CollecthookCategory,
hookExpression: string | 'undefined'
): string {
const dataValue = hookExpression === 'undefined' ? 'undefined' : hookExpression;
return `${indent}collecthook({
${indent} variableName: '${variableName}',
${indent} hookType: '${hookType}',
${indent} category: '${category}',
${indent} data: ${dataValue}
${indent}});\n`;
return `collecthook({ variableName: '${variableName}', hookType: '${hookType}', category: '${category}', data: ${hookExpression} });`;
}

/** Checks if a collecthook call already exists after the given position (by variable ID) */
Expand Down
Loading