Skip to content

Commit 15d1189

Browse files
committed
feat(): arrange babel plugins respectfully to vue and svetle frameworks
1 parent 0bf70da commit 15d1189

3 files changed

Lines changed: 190 additions & 30 deletions

File tree

workers/javascript/src/index.ts

Lines changed: 104 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { JavaScriptEventWorkerTask } from '../types/javascript-event-worker-task
1010
import { BeautifyBacktracePayload } from '../types/beautify-backtrace-payload';
1111
import HawkCatcher from '@hawk.so/nodejs';
1212
import { BacktraceFrame, CatcherMessagePayload, CatcherMessageType, ErrorsCatcherType, SourceCodeLine, SourceMapDataExtended } from '@hawk.so/types';
13-
import { beautifyUserAgent } from './utils';
13+
import { beautifyUserAgent, cleanSourcePath, countLineBreaks } from './utils';
1414
import { Collection } from 'mongodb';
1515
import { parse } from '@babel/parser';
1616
import traverse from '@babel/traverse';
@@ -262,14 +262,24 @@ export default class JavascriptEventWorker extends EventWorker {
262262
* @returns {string | null} - string of the function context or null if it could not be parsed
263263
*/
264264
private getFunctionContext(sourceCode: string, line: number, sourcePath?: string): string | null {
265+
if (!sourceCode) {
266+
return null;
267+
}
268+
269+
const {
270+
code: codeToParse,
271+
targetLine,
272+
hasTypeScriptLang,
273+
} = this.prepareSourceForParsing(sourceCode, line, sourcePath);
274+
265275
let functionName: string | null = null;
266276
let className: string | null = null;
267277
let isAsync = false;
268278

269279
try {
270-
const parserPlugins = this.getBabelParserPluginsForFile(sourcePath);
280+
const parserPlugins = this.getBabelParserPluginsForFile(sourcePath, hasTypeScriptLang);
271281

272-
const ast = parse(sourceCode, {
282+
const ast = parse(codeToParse, {
273283
sourceType: 'module',
274284
plugins: parserPlugins,
275285
});
@@ -281,8 +291,8 @@ export default class JavascriptEventWorker extends EventWorker {
281291
* @param path
282292
*/
283293
ClassDeclaration(path) {
284-
if (path.node.loc && path.node.loc.start.line <= line && path.node.loc.end.line >= line) {
285-
console.log(`class declaration: loc: ${path.node.loc}, line: ${line}, node.start.line: ${path.node.loc.start.line}, node.end.line: ${path.node.loc.end.line}`);
294+
if (path.node.loc && path.node.loc.start.line <= targetLine && path.node.loc.end.line >= targetLine) {
295+
console.log(`class declaration: loc: ${path.node.loc}, targetLine: ${targetLine}, node.start.line: ${path.node.loc.start.line}, node.end.line: ${path.node.loc.end.line}`);
286296

287297
className = path.node.id.name || null;
288298
}
@@ -294,8 +304,8 @@ export default class JavascriptEventWorker extends EventWorker {
294304
* @param path
295305
*/
296306
ClassMethod(path) {
297-
if (path.node.loc && path.node.loc.start.line <= line && path.node.loc.end.line >= line) {
298-
console.log(`class declaration: loc: ${path.node.loc}, line: ${line}, node.start.line: ${path.node.loc.start.line}, node.end.line: ${path.node.loc.end.line}`);
307+
if (path.node.loc && path.node.loc.start.line <= targetLine && path.node.loc.end.line >= targetLine) {
308+
console.log(`class declaration: loc: ${path.node.loc}, targetLine: ${targetLine}, node.start.line: ${path.node.loc.start.line}, node.end.line: ${path.node.loc.end.line}`);
299309

300310
// Handle different key types
301311
if (path.node.key.type === 'Identifier') {
@@ -310,8 +320,8 @@ export default class JavascriptEventWorker extends EventWorker {
310320
* @param path
311321
*/
312322
FunctionDeclaration(path) {
313-
if (path.node.loc && path.node.loc.start.line <= line && path.node.loc.end.line >= line) {
314-
console.log(`function declaration: loc: ${path.node.loc}, line: ${line}, node.start.line: ${path.node.loc.start.line}, node.end.line: ${path.node.loc.end.line}`);
323+
if (path.node.loc && path.node.loc.start.line <= targetLine && path.node.loc.end.line >= targetLine) {
324+
console.log(`function declaration: loc: ${path.node.loc}, targetLine: ${targetLine}, node.start.line: ${path.node.loc.start.line}, node.end.line: ${path.node.loc.end.line}`);
315325

316326
functionName = path.node.id.name || null;
317327
isAsync = path.node.async;
@@ -327,10 +337,10 @@ export default class JavascriptEventWorker extends EventWorker {
327337
path.node.init &&
328338
(path.node.init.type === 'FunctionExpression' || path.node.init.type === 'ArrowFunctionExpression') &&
329339
path.node.loc &&
330-
path.node.loc.start.line <= line &&
331-
path.node.loc.end.line >= line
340+
path.node.loc.start.line <= targetLine &&
341+
path.node.loc.end.line >= targetLine
332342
) {
333-
console.log(`variable declaration: node.type: ${path.node.init.type}, line: ${line}, `);
343+
console.log(`variable declaration: node.type: ${path.node.init.type}, targetLine: ${targetLine}, `);
334344

335345
// Handle different id types
336346
if (path.node.id.type === 'Identifier') {
@@ -350,6 +360,64 @@ export default class JavascriptEventWorker extends EventWorker {
350360
return functionName ? `${isAsync ? 'async ' : ''}${className ? `${className}.` : ''}${functionName}` : null;
351361
}
352362

363+
/**
364+
* Method that extracts source code and target line from the source code related to js frameworks
365+
* It is used to extract inner part of the <script> tag with its lang specifier
366+
*
367+
* @param sourceCode - content of the source file
368+
* @param originalLine - number of the line from the stack trace where the error occurred
369+
* @param sourcePath - original source path from the source map (used to pick parser plugins)
370+
* @returns - object with source code, target line and if it has TypeScript language specifier
371+
*/
372+
private prepareSourceForParsing(
373+
sourceCode: string,
374+
originalLine: number,
375+
sourcePath?: string
376+
): { code: string; targetLine: number; hasTypeScriptLang: boolean } {
377+
const defaultResult = {
378+
code: sourceCode,
379+
targetLine: originalLine,
380+
hasTypeScriptLang: false,
381+
};
382+
383+
if (!sourcePath) {
384+
return defaultResult;
385+
}
386+
387+
const cleanPath = cleanSourcePath(sourcePath);
388+
const ext = extname(cleanPath).toLowerCase();
389+
const frameworkExtensions = new Set([ '.vue', '.svelte' ]);
390+
391+
if (!frameworkExtensions.has(ext)) {
392+
return defaultResult;
393+
}
394+
395+
const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
396+
let match: RegExpExecArray | null;
397+
398+
while ((match = scriptRegex.exec(sourceCode)) !== null) {
399+
const attrs = match[1] ?? '';
400+
const content = match[2] ?? '';
401+
const before = sourceCode.slice(0, match.index);
402+
const startLine = countLineBreaks(before) + 1;
403+
const linesInBlock = countLineBreaks(content) + 1;
404+
const endLine = startLine + linesInBlock - 1;
405+
406+
if (originalLine >= startLine && originalLine <= endLine) {
407+
const relativeLine = originalLine - startLine + 1;
408+
const hasTypeScriptLang = /lang\s*=\s*["']?(ts|typescript)["']?/i.test(attrs);
409+
410+
return {
411+
code: content,
412+
targetLine: relativeLine,
413+
hasTypeScriptLang,
414+
};
415+
}
416+
}
417+
418+
return defaultResult;
419+
}
420+
353421
/**
354422
* Downloads source map file from Grid FS
355423
*
@@ -457,7 +525,7 @@ export default class JavascriptEventWorker extends EventWorker {
457525
*
458526
* @param sourcePath - original file path from source map (e.g. "src/App.tsx")
459527
*/
460-
private getBabelParserPluginsForFile(sourcePath?: string): any[] {
528+
private getBabelParserPluginsForFile(sourcePath?: string, hasTypeScriptLang?: boolean): any[] {
461529
const basePlugins: string[] = [
462530
'classProperties',
463531
'decorators',
@@ -468,28 +536,34 @@ export default class JavascriptEventWorker extends EventWorker {
468536
'topLevelAwait',
469537
];
470538

471-
/**
472-
* Default - use only typescript plugin because it's more stable and less likely will produce errors
473-
*/
474-
let enableTypeScript = true;
539+
let enableTypeScript = Boolean(hasTypeScriptLang);
475540
let enableJSX = false;
476541

477542
if (sourcePath) {
478-
// remove query/hash if there is any
479-
const cleanPath = sourcePath.split('?')[0].split('#')[0];
543+
const hasTypeScriptQuery = /(lang\s*=\s*["']?ts["']?)|(lang\.ts)/i.test(sourcePath);
544+
const cleanPath = cleanSourcePath(sourcePath);
480545
const ext = extname(cleanPath).toLowerCase();
481546

482-
const isTs = ext === '.ts' || ext === '.d.ts';
483-
const isTsx = ext === '.tsx';
484-
const isJs = ext === '.js' || ext === '.mjs' || ext === '.cjs';
485-
const isJsx = ext === '.jsx';
486-
487-
enableTypeScript = isTs || isTsx;
488-
// JSX:
489-
// - for .ts/.d.ts — DISABLE
490-
// - for .tsx/.jsx — ENABLE
491-
// - for .js — keep enabled, to not break App.js with JSX
492-
enableJSX = isTsx || isJsx || isJs;
547+
const isTypeScript = ext === '.ts' || ext === '.d.ts';
548+
const isTypeScriptWithJsx = ext === '.tsx';
549+
const isJavaScript = ext === '.js' || ext === '.mjs' || ext === '.cjs';
550+
const isJavaScriptWithJsx = ext === '.jsx';
551+
const isFrameworkFile = ext === '.vue' || ext === '.svelte';
552+
553+
if (isTypeScriptWithJsx) {
554+
enableTypeScript = true;
555+
enableJSX = true;
556+
} else {
557+
if (isTypeScript || hasTypeScriptQuery || enableTypeScript) {
558+
enableTypeScript = true;
559+
}
560+
561+
if (isJavaScript || isJavaScriptWithJsx || isFrameworkFile) {
562+
enableJSX = true;
563+
}
564+
}
565+
} else {
566+
enableTypeScript = true;
493567
}
494568

495569
if (enableTypeScript) {

workers/javascript/src/utils.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,27 @@ export function beautifyUserAgent(userAgent: string): JavaScriptAddons['beautifi
2929

3030
return beautifiedAgent;
3131
}
32+
33+
/**
34+
* Count line breaks in the provided string.
35+
*
36+
* @param value - string to inspect
37+
*/
38+
export function countLineBreaks(value: string): number {
39+
if (!value) {
40+
return 0;
41+
}
42+
43+
const matches = value.match(/\r\n|\r|\n/g);
44+
45+
return matches ? matches.length : 0;
46+
}
47+
48+
/**
49+
* Strip query and hash fragments from a source path.
50+
*
51+
* @param sourcePath - path that may contain query/hash suffix
52+
*/
53+
export function cleanSourcePath(sourcePath: string): string {
54+
return sourcePath.split('?')[0].split('#')[0];
55+
}

workers/javascript/tests/index.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,4 +556,66 @@ describe('JavaScript event worker', () => {
556556
// We expect "ApiClient.request"
557557
expect(context).toBe('ApiClient.request');
558558
});
559+
560+
it('should resolve function context inside Vue SFC with template block', () => {
561+
const worker = new JavascriptEventWorker();
562+
const vueSource = `
563+
<template>
564+
<div>Hello</div>
565+
</template>
566+
567+
<script>
568+
export function handleClick() {
569+
throw new Error('Test');
570+
}
571+
</script>
572+
`;
573+
574+
const targetLine = vueSource.split('\n').findIndex((line) => line.includes('throw new Error')) + 1;
575+
const context = (worker as any).getFunctionContext(vueSource, targetLine, 'Component.vue');
576+
577+
expect(context).toBe('handleClick');
578+
});
579+
580+
it('should resolve function context inside Vue SFC script with lang="ts"', () => {
581+
const worker = new JavascriptEventWorker();
582+
const vueSource = `
583+
<template>
584+
<div>Hello</div>
585+
</template>
586+
587+
<script lang="ts">
588+
export function useData(): string {
589+
const value: string = 'test';
590+
591+
throw new Error(value);
592+
}
593+
</script>
594+
`;
595+
596+
const targetLine = vueSource.split('\n').findIndex((line) => line.includes('throw new Error')) + 1;
597+
const context = (worker as any).getFunctionContext(vueSource, targetLine, 'Component.vue?vue&type=script&lang=ts');
598+
599+
expect(context).toBe('useData');
600+
});
601+
602+
it('should resolve function context inside Svelte component with markup outside script', () => {
603+
const worker = new JavascriptEventWorker();
604+
const svelteSource = `
605+
<script>
606+
export function load() {
607+
throw new Error('Load failed');
608+
}
609+
</script>
610+
611+
<main>
612+
<h1>Page</h1>
613+
</main>
614+
`;
615+
616+
const targetLine = svelteSource.split('\n').findIndex((line) => line.includes('throw new Error')) + 1;
617+
const context = (worker as any).getFunctionContext(svelteSource, targetLine, 'routes/+page.svelte');
618+
619+
expect(context).toBe('load');
620+
});
559621
});

0 commit comments

Comments
 (0)