Skip to content

Commit 475077b

Browse files
isaacsclaude
andauthored
fix(cloudflare): auto-instrument a re-exported default export
The Vite plugin did not wrap a Worker entry whose default export comes from another module: ```js export { default } from './handler'; export { handler as default } from './handler'; ``` The transform now imports the handler under a private name, wraps it with `withSentry`, and exports the wrapper as the default. The handler may already be wrapped in its own module, which the build cannot see. So the wrap goes through `_INTERNAL_wrapUnlessInstrumented`, the same guard that class re-exports use. For a handler object, a second `withSentry` call adds nothing anyway, because each method is patched only once. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 1698a1d commit 475077b

2 files changed

Lines changed: 76 additions & 1 deletion

File tree

‎packages/cloudflare/src/vite/transform.ts‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,28 @@ function wrapDefaultSpecifier(localName: string, state: TransformState): string
559559
return '__SENTRY_DEFAULT_EXPORT__ as default';
560560
}
561561

562+
/**
563+
* `export { default } from './handler'`. The handler may already be wrapped in its own module,
564+
* which this transform cannot see, so the emitted guard returns such a handler as-is.
565+
*/
566+
function wrapDefaultReexport(
567+
localName: string,
568+
sourceLiteral: string,
569+
state: TransformState,
570+
prelude: string[],
571+
): string {
572+
const target = '__SENTRY_REEXPORT_default__';
573+
prelude.push(
574+
localName === DEFAULT_IMPORT
575+
? `import ${target} from ${sourceLiteral};`
576+
: `import { ${localName} as ${target} } from ${sourceLiteral};`,
577+
`const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, ${state.optionsFn}, ${target});`,
578+
);
579+
state.needsImport = true;
580+
state.autoWrapped.add(DEFAULT_EXPORT);
581+
return '__SENTRY_DEFAULT_EXPORT__ as default';
582+
}
583+
562584
/**
563585
* Handle one export specifier, returning the `Wrapped as Exported` pair to emit for a class that
564586
* gets wrapped. The import/wrapper statements a cross-module pair depends on are pushed onto
@@ -586,7 +608,10 @@ function wrapSpecifier(
586608

587609
if (!exportedName || !localName) return undefined;
588610
if (!kind) {
589-
return exportedName === DEFAULT_IMPORT && !sourceLiteral ? wrapDefaultSpecifier(localName, state) : undefined;
611+
if (exportedName !== DEFAULT_IMPORT) return undefined;
612+
return sourceLiteral
613+
? wrapDefaultReexport(localName, sourceLiteral, state, prelude)
614+
: wrapDefaultSpecifier(localName, state);
590615
}
591616

592617
// Without a `from` clause the specifier points at a module-local binding.

‎packages/cloudflare/test/vite/transform.test.ts‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,46 @@ describe('default export wrapping', () => {
225225
expect(transform(code, ctx)).toBeUndefined();
226226
});
227227

228+
it('wraps a default re-export from another module', () => {
229+
const code = "export { default } from './handler';";
230+
231+
expect(transform(code, ctx)!.code).toBe(
232+
[
233+
"import * as __SENTRY__ from '@sentry/cloudflare';",
234+
"import __SENTRY_REEXPORT_default__ from './handler';",
235+
'const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);',
236+
'export { __SENTRY_DEFAULT_EXPORT__ as default };',
237+
].join('\n'),
238+
);
239+
});
240+
241+
it('wraps a named export of another module re-exported as default', () => {
242+
const code = "export { handler as default } from './handler';";
243+
244+
expect(transform(code, ctx)!.code).toBe(
245+
[
246+
"import * as __SENTRY__ from '@sentry/cloudflare';",
247+
"import { handler as __SENTRY_REEXPORT_default__ } from './handler';",
248+
'const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);',
249+
'export { __SENTRY_DEFAULT_EXPORT__ as default };',
250+
].join('\n'),
251+
);
252+
});
253+
254+
it('leaves sibling specifiers of a default re-export untouched', () => {
255+
const code = "export { default, helper } from './handler';";
256+
257+
expect(transform(code, ctx)!.code).toBe(
258+
[
259+
"import * as __SENTRY__ from '@sentry/cloudflare';",
260+
"import __SENTRY_REEXPORT_default__ from './handler';",
261+
'const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);',
262+
'export { __SENTRY_DEFAULT_EXPORT__ as default };',
263+
"export { helper } from './handler';",
264+
].join('\n'),
265+
);
266+
});
267+
228268
it('uses custom options callback', () => {
229269
const custom: TransformContext = {
230270
classWrappers: doWrappers(),
@@ -1093,6 +1133,16 @@ describe('same-worker RPC binding floor', () => {
10931133
expect(result.code).toContain('rpcTracePropagationBindings: ["SELF",');
10941134
});
10951135

1136+
it('enables a self service binding when it wrapped a default re-export', () => {
1137+
const result = transform("export { default } from './handler';", {
1138+
classWrappers: doWrappers(),
1139+
optionsFn: '() => undefined',
1140+
sameWorkerBindings: [{ bindingName: 'SELF', className: DEFAULT_EXPORT }],
1141+
})!;
1142+
1143+
expect(result.code).toContain('rpcTracePropagationBindings: ["SELF",');
1144+
});
1145+
10961146
it('enables a self service binding when the default export re-exports an already wrapped class', () => {
10971147
const code = [
10981148
"import { WorkerEntrypoint } from 'cloudflare:workers';",

0 commit comments

Comments
 (0)