Skip to content

Commit d9ab8d6

Browse files
authored
perf(bundler-plugins): Use fast component annotation for Webpack and Turbopack
1 parent c1264e7 commit d9ab8d6

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'];
@@ -76,59 +89,86 @@ export function shouldSkipCodeInjection(code: string, facadeModuleId: string | n
7689
export { globFiles } from './glob';
7790

7891
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
79-
export function createComponentNameAnnotateHooks(ignoredComponents: string[], injectIntoHtml: boolean) {
80-
return {
81-
async transform(this: void, code: string, id: string) {
82-
// id may contain query and hash which will trip up our file extension logic below
83-
const idWithoutQueryAndHash = stripQueryAndHashFromPath(id);
92+
export function createComponentNameAnnotateHooks(
93+
ignoredComponents: string[],
94+
injectIntoHtml: boolean,
95+
getParseAstAsync?: () => Promise<ParseAstAsync | null>,
96+
) {
97+
let fastHooksPromise: Promise<FastAnnotationHooks> | undefined;
8498

85-
if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) {
86-
return null;
99+
return {
100+
async transform(this: void, code: string, id: string, meta?: ComponentAnnotationTransformMeta) {
101+
if (!fastHooksPromise) {
102+
fastHooksPromise = import('./component-annotation-oxc').then(
103+
({ createOxcComponentNameAnnotateHooks, getOxcParseAstAsync }) =>
104+
createOxcComponentNameAnnotateHooks(
105+
ignoredComponents,
106+
getParseAstAsync ?? getOxcParseAstAsync,
107+
injectIntoHtml,
108+
),
109+
);
87110
}
88111

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

94-
const parserPlugins: BabelParserPlugins = [];
95-
if (idWithoutQueryAndHash.endsWith('.jsx')) {
96-
parserPlugins.push('jsx');
97-
} else if (idWithoutQueryAndHash.endsWith('.tsx')) {
98-
parserPlugins.push('jsx', 'typescript');
99-
}
117+
return transformWithBabel(code, id, ignoredComponents, injectIntoHtml);
118+
},
119+
};
120+
}
100121

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

121-
return {
122-
code: result?.code ?? code,
123-
map: result?.map,
124-
};
125-
} catch (e) {
126-
debug.error(`Failed to apply react annotate plugin`, e);
127-
}
127+
if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) {
128+
return null;
129+
}
128130

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

134174
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
CodeInjection,
1616
stampDebugId,
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
@@ -52,15 +52,22 @@ test('Rollup plugin should exist', () => {
5252
expect(typeof sentryRollupPlugin).toBe('function');
5353
});
5454

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

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

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

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

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

6673
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)