Skip to content

Commit 2a53357

Browse files
isaacsclaude
andauthored
fix(cloudflare): Bind a default re-export with var in the Vite transform (#25663)
The transform rewrote `export { default } from './handler'` to bind the wrapped handler with a `const`. A module in an import cycle that read the entry's default export during evaluation hit the `const` before its initialization, threw a `ReferenceError`, and stopped the Worker from starting. The binding is now a `var`, as `wrapDefaultSpecifier` and `wrapDefaultExport` already use. A cyclic read gets `undefined` and does not throw. It does not get the handler as the original live binding did, because the wrapper is not set until the entry's body runs. The JSDoc states this limit. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 5b2de99 commit 2a53357

2 files changed

Lines changed: 36 additions & 4 deletions

File tree

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,11 @@ function wrapDefaultSpecifier(localName: string, state: TransformState): string
604604
/**
605605
* `export { default } from './handler'`. The handler may already be wrapped in its own module,
606606
* which this transform cannot see, so the emitted guard returns such a handler as-is.
607+
*
608+
* The original re-export is a live binding to the handler module's export, but the wrapper is not
609+
* set until this module's body runs. A circular import that reads the default export before then
610+
* gets `undefined` instead of the handler. The binding is a `var` so that the read does not throw,
611+
* as a `const` would, and stop the Worker from starting. See `wrapDefaultSpecifier`.
607612
*/
608613
function wrapDefaultReexport(
609614
localName: string,
@@ -616,7 +621,7 @@ function wrapDefaultReexport(
616621
localName === DEFAULT_IMPORT
617622
? `import ${target} from ${sourceLiteral};`
618623
: `import { ${localName} as ${target} } from ${sourceLiteral};`,
619-
`const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, ${state.optionsFn}, ${target});`,
624+
`var __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, ${state.optionsFn}, ${target});`,
620625
);
621626
state.needsImport = true;
622627
state.autoWrapped.add(DEFAULT_EXPORT);

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

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ describe('default export wrapping', () => {
241241
[
242242
"import * as __SENTRY__ from '@sentry/cloudflare';",
243243
"import __SENTRY_REEXPORT_default__ from './handler';",
244-
'const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);',
244+
'var __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);',
245245
'export { __SENTRY_DEFAULT_EXPORT__ as default };',
246246
].join('\n'),
247247
);
@@ -254,20 +254,47 @@ describe('default export wrapping', () => {
254254
[
255255
"import * as __SENTRY__ from '@sentry/cloudflare';",
256256
"import { handler as __SENTRY_REEXPORT_default__ } from './handler';",
257-
'const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);',
257+
'var __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);',
258258
'export { __SENTRY_DEFAULT_EXPORT__ as default };',
259259
].join('\n'),
260260
);
261261
});
262262

263+
it('does not throw when a circular import reads a default re-export during module evaluation', () => {
264+
// `b` reads the entry's default export after `./handler.mjs` has run but before the entry has.
265+
const entry = [
266+
"export { default } from './handler.mjs';",
267+
"import { readDefault } from './b.mjs';",
268+
'console.log(readDefault());',
269+
].join('\n');
270+
const transformed = transform(entry, ctx)!.code.replace(
271+
"import * as __SENTRY__ from '@sentry/cloudflare';",
272+
'const __SENTRY__ = { withSentry: (_options, handler) => handler, _INTERNAL_wrapUnlessInstrumented: (wrap, options, handler) => wrap(options, handler) };',
273+
);
274+
275+
const dir = mkdtempSync(join(tmpdir(), 'sentry-cf-cycle-'));
276+
try {
277+
writeFileSync(join(dir, 'a.mjs'), transformed);
278+
writeFileSync(join(dir, 'handler.mjs'), 'export default { fetch() {} };\n');
279+
writeFileSync(
280+
join(dir, 'b.mjs'),
281+
"import handler from './a.mjs';\nconst kind = typeof handler;\nexport function readDefault() { return kind; }\n",
282+
);
283+
// Plain Node: Vitest's module runner does not evaluate import cycles like native ESM does.
284+
expect(execFileSync(process.execPath, [join(dir, 'a.mjs')], { encoding: 'utf8' }).trim()).toBe('undefined');
285+
} finally {
286+
rmSync(dir, { recursive: true, force: true });
287+
}
288+
});
289+
263290
it('leaves sibling specifiers of a default re-export untouched', () => {
264291
const code = "export { default, helper } from './handler';";
265292

266293
expect(transform(code, ctx)!.code).toBe(
267294
[
268295
"import * as __SENTRY__ from '@sentry/cloudflare';",
269296
"import __SENTRY_REEXPORT_default__ from './handler';",
270-
'const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);',
297+
'var __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);',
271298
'export { __SENTRY_DEFAULT_EXPORT__ as default };',
272299
"export { helper } from './handler';",
273300
].join('\n'),

0 commit comments

Comments
 (0)