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
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,9 @@ async function annotateWithOxcParser(

return {
code: magicString.toString(),
// No `file`, because magic-string would then make `source` relative to it
// and drop the directory from `sources`.
map: magicString.generateMap?.({
file: id,
source: idWithoutQueryAndHash,
includeContent: true,
hires: true,
Expand Down
130 changes: 85 additions & 45 deletions packages/bundler-plugins/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ import { CodeInjection, containsOnlyImports, stripQueryAndHashFromPath } from '.
import type { transformAsync as babelTransformAsync } from '@babel/core';
import type componentNameAnnotatePlugin from '../babel-plugin';
import type { experimentalComponentNameAnnotatePlugin } from '../babel-plugin';
import type {
ComponentAnnotationTransformMeta,
ComponentAnnotationTransformResult,
ParseAstAsync,
} from './component-annotation-oxc-ast';

type FastAnnotationHooks = {
transform(
code: string,
id: string,
meta?: ComponentAnnotationTransformMeta,
): Promise<ComponentAnnotationTransformResult>;
};

type BabelTransformAsync = typeof babelTransformAsync;
type BabelParserPlugins = NonNullable<NonNullable<Parameters<BabelTransformAsync>[1]>['parserOpts']>['plugins'];
Expand Down Expand Up @@ -71,59 +84,86 @@ export { globFiles } from './glob';
export { getCodeInjectionPosition } from './get-code-injection-position';

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createComponentNameAnnotateHooks(ignoredComponents: string[], injectIntoHtml: boolean) {
return {
async transform(this: void, code: string, id: string) {
// id may contain query and hash which will trip up our file extension logic below
const idWithoutQueryAndHash = stripQueryAndHashFromPath(id);
export function createComponentNameAnnotateHooks(
ignoredComponents: string[],
injectIntoHtml: boolean,
getParseAstAsync?: () => Promise<ParseAstAsync | null>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The Rollup and Vite plugins lack error handling for the dynamic import('./component-annotation-oxc'). An import failure will cause an unhandled rejection, bypassing the intended Babel fallback.
Severity: MEDIUM

Suggested Fix

Wrap the await (await fastHooksPromise).transform(...) call within a try/catch block. In the catch block, implement the fallback to the Babel transformation. Alternatively, add a .catch() handler to the fastHooksPromise promise chain to gracefully handle potential import failures, similar to the existing implementation in the Turbopack loader.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/bundler-plugins/src/core/index.ts#L90

Potential issue: In the Rollup and Vite plugins, the `transform` function awaits
`fastHooksPromise`, which is derived from a dynamic
`import('./component-annotation-oxc')`. If this import fails due to build-time issues
like file corruption or resolution problems, the promise will reject. This rejection is
not handled by a `try/catch` block or a `.catch()` handler. As a result, the unhandled
rejection will propagate up, crashing the bundler's transform hook and bypassing the
intended fallback mechanism that uses Babel for transformation.

) {
let fastHooksPromise: Promise<FastAnnotationHooks> | undefined;

if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) {
return null;
return {
async transform(this: void, code: string, id: string, meta?: ComponentAnnotationTransformMeta) {
if (!fastHooksPromise) {
fastHooksPromise = import('./component-annotation-oxc').then(
({ createOxcComponentNameAnnotateHooks, getOxcParseAstAsync }) =>
createOxcComponentNameAnnotateHooks(
ignoredComponents,
getParseAstAsync ?? getOxcParseAstAsync,
injectIntoHtml,
),
);
}

// We will only apply this plugin on jsx and tsx files
if (!['.jsx', '.tsx'].some(ending => idWithoutQueryAndHash.endsWith(ending))) {
return null;
const fastResult = await (await fastHooksPromise).transform(code, id, meta);
if (fastResult !== undefined) {
return fastResult;
}

const parserPlugins: BabelParserPlugins = [];
if (idWithoutQueryAndHash.endsWith('.jsx')) {
parserPlugins.push('jsx');
} else if (idWithoutQueryAndHash.endsWith('.tsx')) {
parserPlugins.push('jsx', 'typescript');
}
return transformWithBabel(code, id, ignoredComponents, injectIntoHtml);
},
};
}

const { transformAsync, componentNameAnnotatePlugin, experimentalComponentNameAnnotatePlugin } =
await loadBabelAnnotationRuntime();
const plugin = injectIntoHtml ? experimentalComponentNameAnnotatePlugin : componentNameAnnotatePlugin;

try {
const result = await transformAsync(code, {
plugins: [[plugin, { ignoredComponents }]],
filename: id,
sourceFileName: idWithoutQueryAndHash,
parserOpts: {
sourceType: 'module',
allowAwaitOutsideFunction: true,
plugins: parserPlugins,
},
generatorOpts: {
decoratorsBeforeExport: true,
},
sourceMaps: true,
});
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
async function transformWithBabel(code: string, id: string, ignoredComponents: string[], injectIntoHtml: boolean) {
// id may contain query and hash which will trip up our file extension logic below
const idWithoutQueryAndHash = stripQueryAndHashFromPath(id);

return {
code: result?.code ?? code,
map: result?.map,
};
} catch (e) {
debug.error(`Failed to apply react annotate plugin`, e);
}
if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) {
return null;
}

return { code };
},
};
// We will only apply this plugin on jsx and tsx files
if (!['.jsx', '.tsx'].some(ending => idWithoutQueryAndHash.endsWith(ending))) {
return null;
}

const parserPlugins: BabelParserPlugins = [];
if (idWithoutQueryAndHash.endsWith('.jsx')) {
parserPlugins.push('jsx');
} else if (idWithoutQueryAndHash.endsWith('.tsx')) {
parserPlugins.push('jsx', 'typescript');
}

const { transformAsync, componentNameAnnotatePlugin, experimentalComponentNameAnnotatePlugin } =
await loadBabelAnnotationRuntime();
const plugin = injectIntoHtml ? experimentalComponentNameAnnotatePlugin : componentNameAnnotatePlugin;

try {
const result = await transformAsync(code, {
plugins: [[plugin, { ignoredComponents }]],
filename: id,
sourceFileName: idWithoutQueryAndHash,
parserOpts: {
sourceType: 'module',
allowAwaitOutsideFunction: true,
plugins: parserPlugins,
},
generatorOpts: {
decoratorsBeforeExport: true,
},
sourceMaps: true,
});

return {
code: result?.code ?? code,
map: result?.map,
};
} catch (e) {
debug.error(`Failed to apply react annotate plugin`, e);
}

return { code };
}

export function getDebugIdSnippet(debugId: string): CodeInjection {
Expand Down
54 changes: 5 additions & 49 deletions packages/bundler-plugins/src/rollup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ import {
stampDebugId,
getCodeInjectionPosition,
} from '../core';
import type {
ComponentAnnotationTransformMeta,
ComponentAnnotationTransformResult,
} from '../core/component-annotation-oxc';
import type { ComponentAnnotationTransformMeta } from '../core/component-annotation-oxc';
import type { SourceMap } from 'magic-string';
import MagicString from 'magic-string';
import * as path from 'node:path';
Expand All @@ -41,13 +38,6 @@ type ViteModule = {
};

type ViteParseAstAsync = NonNullable<ViteModule['parseAstAsync']>;
type FastAnnotationHooks = {
transform(
code: string,
id: string,
meta?: ComponentAnnotationTransformMeta,
): Promise<ComponentAnnotationTransformResult>;
};

let viteParseAstAsyncPromise: Promise<ViteParseAstAsync | null> | undefined;

Expand Down Expand Up @@ -166,31 +156,10 @@ export function _rollupPluginInternal(
? createComponentNameAnnotateHooks(
options.reactComponentAnnotation?.ignoredComponents || [],
!!options.reactComponentAnnotation?._experimentalInjectIntoHtml,
// Vite 8 already loads an oxc-based parser, so reuse it.
buildTool === 'vite' && buildToolMajorVersion === '8' ? getViteParseAstAsync : undefined,
)
: undefined;
const transformFastAnnotations = options.reactComponentAnnotation?.enabled
? (() => {
let fastAnnotationHooksPromise: Promise<FastAnnotationHooks> | undefined;

return {
transform(code: string, id: string, meta?: ComponentAnnotationTransformMeta) {
if (!fastAnnotationHooksPromise) {
fastAnnotationHooksPromise = import('../core/component-annotation-oxc').then(
({ createOxcComponentNameAnnotateHooks, getOxcParseAstAsync }) =>
createOxcComponentNameAnnotateHooks(
options.reactComponentAnnotation?.ignoredComponents || [],
// Vite 8 already loads an oxc-based parser, so reuse it.
buildTool === 'vite' && buildToolMajorVersion === '8' ? getViteParseAstAsync : getOxcParseAstAsync,
!!options.reactComponentAnnotation?._experimentalInjectIntoHtml,
),
);
}

return fastAnnotationHooksPromise.then(hooks => hooks.transform(code, id, meta));
},
};
})()
: undefined;

const transformReplace = Object.keys(replacementValues).length > 0;
const shouldTransform = transformAnnotations || transformReplace;
Expand All @@ -208,21 +177,8 @@ export function _rollupPluginInternal(
): Promise<TransformResult> {
// Component annotations are only in user code and boolean flag replacements are
// only in Sentry code. If we successfully add annotations, we can return early.
let shouldRunBabelAnnotations = true;

if (transformFastAnnotations?.transform) {
const result = await transformFastAnnotations.transform(code, id, meta);
if (result) {
return result;
}

if (result === null) {
shouldRunBabelAnnotations = false;
}
}

if (shouldRunBabelAnnotations && transformAnnotations?.transform) {
const result = await transformAnnotations.transform(code, id);
if (transformAnnotations) {
const result = await transformAnnotations.transform(code, id, meta);
if (result) {
return result;
}
Expand Down
11 changes: 9 additions & 2 deletions packages/bundler-plugins/test/rollup/public-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,22 @@ test('Rollup plugin should exist', () => {
expect(typeof sentryRollupPlugin).toBe('function');
});

test('component annotations only load Babel when the Babel transform runs', async () => {
test('component annotations only load Babel when the fast path cannot annotate a file', async () => {
expect(babelCoreImportMock).not.toHaveBeenCalled();

const hooks = createComponentNameAnnotateHooks([], false);

await hooks.transform('const x = 1;', '/src/plain.js');
annotationTransformMock.mockResolvedValueOnce(null as never);
await expect(hooks.transform('const x = 1;', '/src/plain.js')).resolves.toBeNull();

await expect(hooks.transform('export function App() { return <div />; }', '/src/app.jsx')).resolves.toEqual({
code: 'fast-path',
map: null,
});

expect(babelCoreImportMock).not.toHaveBeenCalled();

annotationTransformMock.mockResolvedValueOnce(undefined as never);
await hooks.transform('export function App() { return <div />; }', '/src/app.jsx');

expect(babelCoreImportMock).toHaveBeenCalledTimes(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type ComponentAnnotationLoaderOptions = {
* `data-sentry-element`, and `data-sentry-source-file` attributes.
*
* This is the Turbopack equivalent of what `@sentry/bundler-plugins/webpack` does
* via the `reactComponentAnnotation` option and `@sentry/babel-plugin-component-annotate`.
* via the `reactComponentAnnotation` option.
*
* Options:
* - `ignoredComponents`: List of component names to exclude from annotation.
Comment thread
sentry[bot] marked this conversation as resolved.
Expand Down
Loading