diff --git a/packages/cloudflare/src/vite/transform.ts b/packages/cloudflare/src/vite/transform.ts index 273cfb16a766..e640de3a45d1 100644 --- a/packages/cloudflare/src/vite/transform.ts +++ b/packages/cloudflare/src/vite/transform.ts @@ -604,6 +604,11 @@ function wrapDefaultSpecifier(localName: string, state: TransformState): string /** * `export { default } from './handler'`. The handler may already be wrapped in its own module, * which this transform cannot see, so the emitted guard returns such a handler as-is. + * + * The original re-export is a live binding to the handler module's export, but the wrapper is not + * set until this module's body runs. A circular import that reads the default export before then + * gets `undefined` instead of the handler. The binding is a `var` so that the read does not throw, + * as a `const` would, and stop the Worker from starting. See `wrapDefaultSpecifier`. */ function wrapDefaultReexport( localName: string, @@ -616,7 +621,7 @@ function wrapDefaultReexport( localName === DEFAULT_IMPORT ? `import ${target} from ${sourceLiteral};` : `import { ${localName} as ${target} } from ${sourceLiteral};`, - `const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, ${state.optionsFn}, ${target});`, + `var __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, ${state.optionsFn}, ${target});`, ); state.needsImport = true; state.autoWrapped.add(DEFAULT_EXPORT); diff --git a/packages/cloudflare/test/vite/transform.test.ts b/packages/cloudflare/test/vite/transform.test.ts index 539b66c98961..76129b034923 100644 --- a/packages/cloudflare/test/vite/transform.test.ts +++ b/packages/cloudflare/test/vite/transform.test.ts @@ -241,7 +241,7 @@ describe('default export wrapping', () => { [ "import * as __SENTRY__ from '@sentry/cloudflare';", "import __SENTRY_REEXPORT_default__ from './handler';", - 'const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);', + 'var __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);', 'export { __SENTRY_DEFAULT_EXPORT__ as default };', ].join('\n'), ); @@ -254,12 +254,39 @@ describe('default export wrapping', () => { [ "import * as __SENTRY__ from '@sentry/cloudflare';", "import { handler as __SENTRY_REEXPORT_default__ } from './handler';", - 'const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);', + 'var __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);', 'export { __SENTRY_DEFAULT_EXPORT__ as default };', ].join('\n'), ); }); + it('does not throw when a circular import reads a default re-export during module evaluation', () => { + // `b` reads the entry's default export after `./handler.mjs` has run but before the entry has. + const entry = [ + "export { default } from './handler.mjs';", + "import { readDefault } from './b.mjs';", + 'console.log(readDefault());', + ].join('\n'); + const transformed = transform(entry, ctx)!.code.replace( + "import * as __SENTRY__ from '@sentry/cloudflare';", + 'const __SENTRY__ = { withSentry: (_options, handler) => handler, _INTERNAL_wrapUnlessInstrumented: (wrap, options, handler) => wrap(options, handler) };', + ); + + const dir = mkdtempSync(join(tmpdir(), 'sentry-cf-cycle-')); + try { + writeFileSync(join(dir, 'a.mjs'), transformed); + writeFileSync(join(dir, 'handler.mjs'), 'export default { fetch() {} };\n'); + writeFileSync( + join(dir, 'b.mjs'), + "import handler from './a.mjs';\nconst kind = typeof handler;\nexport function readDefault() { return kind; }\n", + ); + // Plain Node: Vitest's module runner does not evaluate import cycles like native ESM does. + expect(execFileSync(process.execPath, [join(dir, 'a.mjs')], { encoding: 'utf8' }).trim()).toBe('undefined'); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it('leaves sibling specifiers of a default re-export untouched', () => { const code = "export { default, helper } from './handler';"; @@ -267,7 +294,7 @@ describe('default export wrapping', () => { [ "import * as __SENTRY__ from '@sentry/cloudflare';", "import __SENTRY_REEXPORT_default__ from './handler';", - 'const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);', + 'var __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);', 'export { __SENTRY_DEFAULT_EXPORT__ as default };', "export { helper } from './handler';", ].join('\n'),