Skip to content

Commit 69a2463

Browse files
authored
fix(js): jsx plugin integration (#482)
* fix(js): use babel parser plugins based on type extension * fix(): comments * fix(): duplicated variable
1 parent 620264f commit 69a2463

2 files changed

Lines changed: 176 additions & 14 deletions

File tree

workers/javascript/src/index.ts

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { beautifyUserAgent } from './utils';
1414
import { Collection } from 'mongodb';
1515
import { parse } from '@babel/parser';
1616
import traverse from '@babel/traverse';
17+
import { extname } from 'path';
1718
/* eslint-disable-next-line no-unused-vars */
1819
import { memoize } from '../../../lib/memoize';
1920

@@ -231,7 +232,11 @@ export default class JavascriptEventWorker extends EventWorker {
231232

232233
const originalContent = consumer.sourceContentFor(originalLocation.source);
233234

234-
functionContext = await this.getFunctionContext(originalContent, originalLocation.line) ?? originalLocation.name;
235+
functionContext = await this.getFunctionContext(
236+
originalContent,
237+
originalLocation.line,
238+
originalLocation.source
239+
) ?? originalLocation.name;
235240
} catch (e) {
236241
HawkCatcher.send(e);
237242
this.logger.error('Can\'t get function context');
@@ -253,28 +258,20 @@ export default class JavascriptEventWorker extends EventWorker {
253258
*
254259
* @param sourceCode - content of the source file
255260
* @param line - number of the line from the stack trace
261+
* @param sourcePath - original source path from the source map (used to pick parser plugins)
256262
* @returns {string | null} - string of the function context or null if it could not be parsed
257263
*/
258-
private getFunctionContext(sourceCode: string, line: number): string | null {
264+
private getFunctionContext(sourceCode: string, line: number, sourcePath?: string): string | null {
259265
let functionName: string | null = null;
260266
let className: string | null = null;
261267
let isAsync = false;
262268

263269
try {
264-
// @todo choose plugins based on source code file extention (related to possible jsx parser usage in future)
270+
const parserPlugins = this.getBabelParserPluginsForFile(sourcePath);
271+
265272
const ast = parse(sourceCode, {
266273
sourceType: 'module',
267-
plugins: [
268-
'jsx',
269-
'typescript',
270-
'classProperties',
271-
'decorators',
272-
'optionalChaining',
273-
'nullishCoalescingOperator',
274-
'dynamicImport',
275-
'bigInt',
276-
'topLevelAwait',
277-
],
274+
plugins: parserPlugins,
278275
});
279276

280277
traverse(ast as any, {
@@ -454,4 +451,55 @@ export default class JavascriptEventWorker extends EventWorker {
454451
this.logger.error(`Error on source-map consumer initialization: ${e}`);
455452
}
456453
}
454+
455+
/**
456+
* Choose babel parser plugins based on source file extension
457+
*
458+
* @param sourcePath - original file path from source map (e.g. "src/App.tsx")
459+
*/
460+
private getBabelParserPluginsForFile(sourcePath?: string): any[] {
461+
const basePlugins: string[] = [
462+
'classProperties',
463+
'decorators',
464+
'optionalChaining',
465+
'nullishCoalescingOperator',
466+
'dynamicImport',
467+
'bigInt',
468+
'topLevelAwait',
469+
];
470+
471+
/**
472+
* Default - use only typescript plugin because it's more stable and less likely will produce errors
473+
*/
474+
let enableTypeScript = true;
475+
let enableJSX = false;
476+
477+
if (sourcePath) {
478+
// remove query/hash if there is any
479+
const cleanPath = sourcePath.split('?')[0].split('#')[0];
480+
const ext = extname(cleanPath).toLowerCase();
481+
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;
493+
}
494+
495+
if (enableTypeScript) {
496+
basePlugins.push('typescript');
497+
}
498+
499+
if (enableJSX) {
500+
basePlugins.push('jsx');
501+
}
502+
503+
return basePlugins;
504+
}
457505
}

workers/javascript/tests/index.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,4 +442,118 @@ describe('JavaScript event worker', () => {
442442

443443
await worker.finish();
444444
});
445+
446+
it('should resolve function context for TypeScript function with angle-bracket type assertion', () => {
447+
const worker = new JavascriptEventWorker();
448+
449+
const tsSource = `
450+
const foo: string | null = 'bar';
451+
452+
function throwError() {
453+
const value = <string>foo;
454+
throw new Error(value);
455+
}
456+
457+
export { throwError };
458+
`;
459+
460+
/**
461+
* String with throw new Error(...) is the 6th line (if counting from 1)
462+
* 1: ''
463+
* 2: const foo...
464+
* 3: ''
465+
* 4: function throwError() {
466+
* 5: const value = <string>foo;
467+
* 6: throw new Error(value);
468+
* ...
469+
*/
470+
const context = (worker as any).getFunctionContext(tsSource, 6, 'example.ts');
471+
472+
/**
473+
* We expect that the build with fixes will return the function name,
474+
* but with the current configuration the jsx+typescript parser fails
475+
* and getFunctionContext returns null.
476+
*/
477+
expect(context).toBe('throwError');
478+
});
479+
480+
it('should resolve function context for TypeScript generic arrow function', () => {
481+
const worker = new JavascriptEventWorker();
482+
483+
const tsSource = `
484+
type User = {
485+
id: string;
486+
name: string;
487+
};
488+
489+
const wrap = <T>(value: T): T => {
490+
return value;
491+
};
492+
493+
export const useUser = () => {
494+
const user: User = wrap<User>({ id: '1', name: 'John' });
495+
496+
return user;
497+
};
498+
`;
499+
500+
/**
501+
* String inside useUser - where we want to get context:
502+
* 1: ''
503+
* 2: type User = { ...
504+
* ...
505+
* 7: const wrap = <T>(value: T): T => {
506+
* ...
507+
* 12: export const useUser = () => {
508+
* 13: const user: User = wrap<User>({ id: '1', name: 'John' });
509+
* 14:
510+
* 15: return user;
511+
* 16: };
512+
*/
513+
const context = (worker as any).getFunctionContext(tsSource, 13, 'example.ts');
514+
515+
expect(context).toBe('useUser');
516+
});
517+
518+
it('should resolve class method context for TypeScript class with type assertion', () => {
519+
const worker = new JavascriptEventWorker();
520+
521+
const tsSource = `
522+
class ApiClient {
523+
private baseUrl: string = 'https://example.com';
524+
525+
public request() {
526+
const raw = '{"ok":true}';
527+
const parsed = <Record<string, unknown>>JSON.parse(raw);
528+
529+
if (!parsed.ok) {
530+
throw new Error('Request failed');
531+
}
532+
533+
return parsed;
534+
}
535+
}
536+
537+
export default ApiClient;
538+
`;
539+
540+
/**
541+
* String where we want to get context - inside the request method:
542+
* 1: ''
543+
* 2: class ApiClient {
544+
* 3: private baseUrl...
545+
* 4:
546+
* 5: public request() {
547+
* 6: const raw = '{"ok":true}';
548+
* 7: const parsed = <Record<string, unknown>>JSON.parse(raw);
549+
* 8:
550+
* 9: if (!parsed.ok) {
551+
* 10: throw new Error('Request failed');
552+
* ...
553+
*/
554+
const context = (worker as any).getFunctionContext(tsSource, 7, 'example.ts');
555+
556+
// We expect "ApiClient.request"
557+
expect(context).toBe('ApiClient.request');
558+
});
445559
});

0 commit comments

Comments
 (0)