diff --git a/packages/bundler-plugins/src/esbuild/index.ts b/packages/bundler-plugins/src/esbuild/index.ts index b369bc67a3ec..136b4a20665a 100644 --- a/packages/bundler-plugins/src/esbuild/index.ts +++ b/packages/bundler-plugins/src/esbuild/index.ts @@ -55,6 +55,7 @@ interface EsbuildInitialOptions { define?: Record; write?: boolean; absWorkingDir?: string; + logOverride?: Record; } interface EsbuildPluginBuild { @@ -199,6 +200,10 @@ export function sentryEsbuildPlugin(userOptions: Options = {}): any { // rebuild wraps the entry points again. onStart(() => debugIdWrappedPaths.clear()); + // The proxy module re-exports `OriginalModule.default` statically so downstream bundlers can tree-shake it, + // but esbuild warns about that access when the entry point has no default export. + initialOptions.logOverride = { 'import-is-undefined': 'silent', ...initialOptions.logOverride }; + if (!initialOptions.bundle) { logger.warn( 'The Sentry esbuild plugin only supports esbuild with `bundle: true` being set in the esbuild build options. Esbuild will probably crash now. Sorry about that. If you need to upload sourcemaps without `bundle: true`, it is recommended to use Sentry CLI instead: https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/cli/', diff --git a/packages/bundler-plugins/test/esbuild/entry-without-default-export.test.ts b/packages/bundler-plugins/test/esbuild/entry-without-default-export.test.ts new file mode 100644 index 000000000000..1ff087262907 --- /dev/null +++ b/packages/bundler-plugins/test/esbuild/entry-without-default-export.test.ts @@ -0,0 +1,63 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as esbuild from 'esbuild'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { sentryEsbuildPlugin } from '../../src/esbuild'; + +describe('entry points without a default export', () => { + let tmpDir: string; + let entry: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-esbuild-entry-exports-')); + entry = path.join(tmpDir, 'entry.js'); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + function build(logOverride?: Record): Promise { + return esbuild.build({ + entryPoints: [entry], + bundle: true, + write: false, + format: 'esm', + logLevel: 'silent', + absWorkingDir: tmpDir, + logOverride, + plugins: [sentryEsbuildPlugin({ telemetry: false, sourcemaps: { disable: 'disable-upload' } })], + }); + } + + it.each([ + ['an ES module without exports', 'export {};'], + ['an ES module with only named exports', 'export const answer = 42;'], + ['a plain script', 'console.log("hello");'], + ])('does not warn for %s', async (_name, source) => { + fs.writeFileSync(entry, source); + + const result = await build(); + + expect(result.warnings).toEqual([]); + expect(result.outputFiles?.[0]?.text).toContain('_sentryDebugIds'); + }); + + it('keeps named exports of the entry point', async () => { + fs.writeFileSync(entry, 'export const answer = 42;'); + + const result = await build(); + + expect(result.warnings).toEqual([]); + expect(result.outputFiles?.[0]?.text).toMatch(/export\s*{[^}]*answer[^}]*}/); + }); + + it('keeps a user-provided override for the same esbuild message', async () => { + fs.writeFileSync(entry, 'export {};'); + + await expect(build({ 'import-is-undefined': 'error' })).rejects.toThrow( + /import-is-undefined|will always be undefined/, + ); + }); +});