Skip to content

Commit 21086bb

Browse files
authored
perf(bundler-plugins): Use fast component annotation for Webpack and Turbopack (#24448)
Stacked on #24440. Webpack and the Next.js Turbopack loader still annotated React components with Babel. This PR moves them to the fast oxc-based path, so every bundler integration now uses it. Babel stays as the fallback when `oxc-parser` cannot load or cannot parse a file. The next PR in the stack removes that fallback. `createComponentNameAnnotateHooks` now tries the fast path first and uses Babel only when the fast path does not handle the file. Webpack, the Rollup and Vite plugins, and the Turbopack loader already call this function, so the Rollup plugin no longer needs its own fast-path setup. The fast path's source maps listed each annotated file by its base name only, which dropped the directory. That is the bug from #23561, which was fixed for the Babel path, and it would have come back for Webpack. The maps now keep the full path, so Rollup and Vite builds that use the fast path get full paths too. Ref #24415
1 parent 7070bd6 commit 21086bb

5 files changed

Lines changed: 102 additions & 98 deletions

File tree

‎packages/bundler-plugins/src/core/component-annotation-oxc.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,9 @@ async function annotateWithOxcParser(
149149

150150
return {
151151
code: magicString.toString(),
152+
// No `file`, because magic-string would then make `source` relative to it
153+
// and drop the directory from `sources`.
152154
map: magicString.generateMap?.({
153-
file: id,
154155
source: idWithoutQueryAndHash,
155156
includeContent: true,
156157
hires: true,

‎packages/bundler-plugins/src/core/index.ts‎

Lines changed: 85 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@ import { CodeInjection, containsOnlyImports, stripQueryAndHashFromPath } from '.
33
import type { transformAsync as babelTransformAsync } from '@babel/core';
44
import type componentNameAnnotatePlugin from '../babel-plugin';
55
import type { experimentalComponentNameAnnotatePlugin } from '../babel-plugin';
6+
import type {
7+
ComponentAnnotationTransformMeta,
8+
ComponentAnnotationTransformResult,
9+
ParseAstAsync,
10+
} from './component-annotation-oxc-ast';
11+
12+
type FastAnnotationHooks = {
13+
transform(
14+
code: string,
15+
id: string,
16+
meta?: ComponentAnnotationTransformMeta,
17+
): Promise<ComponentAnnotationTransformResult>;
18+
};
619

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

7386
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
74-
export function createComponentNameAnnotateHooks(ignoredComponents: string[], injectIntoHtml: boolean) {
75-
return {
76-
async transform(this: void, code: string, id: string) {
77-
// id may contain query and hash which will trip up our file extension logic below
78-
const idWithoutQueryAndHash = stripQueryAndHashFromPath(id);
87+
export function createComponentNameAnnotateHooks(
88+
ignoredComponents: string[],
89+
injectIntoHtml: boolean,
90+
getParseAstAsync?: () => Promise<ParseAstAsync | null>,
91+
) {
92+
let fastHooksPromise: Promise<FastAnnotationHooks> | undefined;
7993

80-
if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) {
81-
return null;
94+
return {
95+
async transform(this: void, code: string, id: string, meta?: ComponentAnnotationTransformMeta) {
96+
if (!fastHooksPromise) {
97+
fastHooksPromise = import('./component-annotation-oxc').then(
98+
({ createOxcComponentNameAnnotateHooks, getOxcParseAstAsync }) =>
99+
createOxcComponentNameAnnotateHooks(
100+
ignoredComponents,
101+
getParseAstAsync ?? getOxcParseAstAsync,
102+
injectIntoHtml,
103+
),
104+
);
82105
}
83106

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

89-
const parserPlugins: BabelParserPlugins = [];
90-
if (idWithoutQueryAndHash.endsWith('.jsx')) {
91-
parserPlugins.push('jsx');
92-
} else if (idWithoutQueryAndHash.endsWith('.tsx')) {
93-
parserPlugins.push('jsx', 'typescript');
94-
}
112+
return transformWithBabel(code, id, ignoredComponents, injectIntoHtml);
113+
},
114+
};
115+
}
95116

96-
const { transformAsync, componentNameAnnotatePlugin, experimentalComponentNameAnnotatePlugin } =
97-
await loadBabelAnnotationRuntime();
98-
const plugin = injectIntoHtml ? experimentalComponentNameAnnotatePlugin : componentNameAnnotatePlugin;
99-
100-
try {
101-
const result = await transformAsync(code, {
102-
plugins: [[plugin, { ignoredComponents }]],
103-
filename: id,
104-
sourceFileName: idWithoutQueryAndHash,
105-
parserOpts: {
106-
sourceType: 'module',
107-
allowAwaitOutsideFunction: true,
108-
plugins: parserPlugins,
109-
},
110-
generatorOpts: {
111-
decoratorsBeforeExport: true,
112-
},
113-
sourceMaps: true,
114-
});
117+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
118+
async function transformWithBabel(code: string, id: string, ignoredComponents: string[], injectIntoHtml: boolean) {
119+
// id may contain query and hash which will trip up our file extension logic below
120+
const idWithoutQueryAndHash = stripQueryAndHashFromPath(id);
115121

116-
return {
117-
code: result?.code ?? code,
118-
map: result?.map,
119-
};
120-
} catch (e) {
121-
debug.error(`Failed to apply react annotate plugin`, e);
122-
}
122+
if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) {
123+
return null;
124+
}
123125

124-
return { code };
125-
},
126-
};
126+
// We will only apply this plugin on jsx and tsx files
127+
if (!['.jsx', '.tsx'].some(ending => idWithoutQueryAndHash.endsWith(ending))) {
128+
return null;
129+
}
130+
131+
const parserPlugins: BabelParserPlugins = [];
132+
if (idWithoutQueryAndHash.endsWith('.jsx')) {
133+
parserPlugins.push('jsx');
134+
} else if (idWithoutQueryAndHash.endsWith('.tsx')) {
135+
parserPlugins.push('jsx', 'typescript');
136+
}
137+
138+
const { transformAsync, componentNameAnnotatePlugin, experimentalComponentNameAnnotatePlugin } =
139+
await loadBabelAnnotationRuntime();
140+
const plugin = injectIntoHtml ? experimentalComponentNameAnnotatePlugin : componentNameAnnotatePlugin;
141+
142+
try {
143+
const result = await transformAsync(code, {
144+
plugins: [[plugin, { ignoredComponents }]],
145+
filename: id,
146+
sourceFileName: idWithoutQueryAndHash,
147+
parserOpts: {
148+
sourceType: 'module',
149+
allowAwaitOutsideFunction: true,
150+
plugins: parserPlugins,
151+
},
152+
generatorOpts: {
153+
decoratorsBeforeExport: true,
154+
},
155+
sourceMaps: true,
156+
});
157+
158+
return {
159+
code: result?.code ?? code,
160+
map: result?.map,
161+
};
162+
} catch (e) {
163+
debug.error(`Failed to apply react annotate plugin`, e);
164+
}
165+
166+
return { code };
127167
}
128168

129169
export function getDebugIdSnippet(debugId: string): CodeInjection {

‎packages/bundler-plugins/src/rollup/index.ts‎

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ import {
1515
stampDebugId,
1616
getCodeInjectionPosition,
1717
} from '../core';
18-
import type {
19-
ComponentAnnotationTransformMeta,
20-
ComponentAnnotationTransformResult,
21-
} from '../core/component-annotation-oxc';
18+
import type { ComponentAnnotationTransformMeta } from '../core/component-annotation-oxc';
2219
import type { SourceMap } from 'magic-string';
2320
import MagicString from 'magic-string';
2421
import * as path from 'node:path';
@@ -41,13 +38,6 @@ type ViteModule = {
4138
};
4239

4340
type ViteParseAstAsync = NonNullable<ViteModule['parseAstAsync']>;
44-
type FastAnnotationHooks = {
45-
transform(
46-
code: string,
47-
id: string,
48-
meta?: ComponentAnnotationTransformMeta,
49-
): Promise<ComponentAnnotationTransformResult>;
50-
};
5141

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

@@ -166,31 +156,10 @@ export function _rollupPluginInternal(
166156
? createComponentNameAnnotateHooks(
167157
options.reactComponentAnnotation?.ignoredComponents || [],
168158
!!options.reactComponentAnnotation?._experimentalInjectIntoHtml,
159+
// Vite 8 already loads an oxc-based parser, so reuse it.
160+
buildTool === 'vite' && buildToolMajorVersion === '8' ? getViteParseAstAsync : undefined,
169161
)
170162
: undefined;
171-
const transformFastAnnotations = options.reactComponentAnnotation?.enabled
172-
? (() => {
173-
let fastAnnotationHooksPromise: Promise<FastAnnotationHooks> | undefined;
174-
175-
return {
176-
transform(code: string, id: string, meta?: ComponentAnnotationTransformMeta) {
177-
if (!fastAnnotationHooksPromise) {
178-
fastAnnotationHooksPromise = import('../core/component-annotation-oxc').then(
179-
({ createOxcComponentNameAnnotateHooks, getOxcParseAstAsync }) =>
180-
createOxcComponentNameAnnotateHooks(
181-
options.reactComponentAnnotation?.ignoredComponents || [],
182-
// Vite 8 already loads an oxc-based parser, so reuse it.
183-
buildTool === 'vite' && buildToolMajorVersion === '8' ? getViteParseAstAsync : getOxcParseAstAsync,
184-
!!options.reactComponentAnnotation?._experimentalInjectIntoHtml,
185-
),
186-
);
187-
}
188-
189-
return fastAnnotationHooksPromise.then(hooks => hooks.transform(code, id, meta));
190-
},
191-
};
192-
})()
193-
: undefined;
194163

195164
const transformReplace = Object.keys(replacementValues).length > 0;
196165
const shouldTransform = transformAnnotations || transformReplace;
@@ -208,21 +177,8 @@ export function _rollupPluginInternal(
208177
): Promise<TransformResult> {
209178
// Component annotations are only in user code and boolean flag replacements are
210179
// only in Sentry code. If we successfully add annotations, we can return early.
211-
let shouldRunBabelAnnotations = true;
212-
213-
if (transformFastAnnotations?.transform) {
214-
const result = await transformFastAnnotations.transform(code, id, meta);
215-
if (result) {
216-
return result;
217-
}
218-
219-
if (result === null) {
220-
shouldRunBabelAnnotations = false;
221-
}
222-
}
223-
224-
if (shouldRunBabelAnnotations && transformAnnotations?.transform) {
225-
const result = await transformAnnotations.transform(code, id);
180+
if (transformAnnotations) {
181+
const result = await transformAnnotations.transform(code, id, meta);
226182
if (result) {
227183
return result;
228184
}

‎packages/bundler-plugins/test/rollup/public-api.test.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,22 @@ test('Rollup plugin should exist', () => {
5353
expect(typeof sentryRollupPlugin).toBe('function');
5454
});
5555

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

5959
const hooks = createComponentNameAnnotateHooks([], false);
6060

61-
await hooks.transform('const x = 1;', '/src/plain.js');
61+
annotationTransformMock.mockResolvedValueOnce(null as never);
62+
await expect(hooks.transform('const x = 1;', '/src/plain.js')).resolves.toBeNull();
63+
64+
await expect(hooks.transform('export function App() { return <div />; }', '/src/app.jsx')).resolves.toEqual({
65+
code: 'fast-path',
66+
map: null,
67+
});
6268

6369
expect(babelCoreImportMock).not.toHaveBeenCalled();
6470

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

6774
expect(babelCoreImportMock).toHaveBeenCalledTimes(1);

‎packages/nextjs/src/config/loaders/componentAnnotationLoader.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export type ComponentAnnotationLoaderOptions = {
1010
* `data-sentry-element`, and `data-sentry-source-file` attributes.
1111
*
1212
* This is the Turbopack equivalent of what `@sentry/bundler-plugins/webpack` does
13-
* via the `reactComponentAnnotation` option and `@sentry/babel-plugin-component-annotate`.
13+
* via the `reactComponentAnnotation` option.
1414
*
1515
* Options:
1616
* - `ignoredComponents`: List of component names to exclude from annotation.

0 commit comments

Comments
 (0)