From 5e08cb3a839b2f40f0bd763aa1bb6294fa522fa4 Mon Sep 17 00:00:00 2001 From: Aditya Reddy <82123567+adityareddy-dev@users.noreply.github.com> Date: Thu, 5 Nov 2026 05:11:47 -0600 Subject: [PATCH] esbuild: avoid missing default export warnings --- packages/bundler-plugins/src/esbuild/index.ts | 17 +- .../test/esbuild/entry-exports.test.ts | 151 ++++++++++++++++++ 2 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 packages/bundler-plugins/test/esbuild/entry-exports.test.ts diff --git a/packages/bundler-plugins/src/esbuild/index.ts b/packages/bundler-plugins/src/esbuild/index.ts index b369bc67a3ec..78ed70bc18b1 100644 --- a/packages/bundler-plugins/src/esbuild/index.ts +++ b/packages/bundler-plugins/src/esbuild/index.ts @@ -227,19 +227,20 @@ export function sentryEsbuildPlugin(userOptions: Options = {}): any { return { pluginName, - path: resolvedPath, + // Treat the generated proxy as dependency code without changing the user's entry point. + path: path.join( + path.dirname(resolvedPath), + 'node_modules', + '@sentry', + 'esbuild-plugin', + path.basename(resolvedPath), + ), + namespace: 'sentry-debug-id-proxy', pluginData: { isDebugIdProxy: true, originalPath: args.path, originalResolveDir: args.resolveDir, }, - // We need to add a suffix here, otherwise esbuild will mark the entrypoint as resolved and won't traverse - // the module tree any further down past the proxy module because we're essentially creating a dependency - // loop back to the proxy module. - // By setting a suffix we're telling esbuild that the entrypoint and proxy module are two different things, - // making it re-resolve the entrypoint when it is imported from the proxy module. - // Super confusing? Yes. Works? Apparently... Let's see. - suffix: '?sentryDebugIdProxy=true', }; }); diff --git a/packages/bundler-plugins/test/esbuild/entry-exports.test.ts b/packages/bundler-plugins/test/esbuild/entry-exports.test.ts new file mode 100644 index 000000000000..4ed2a4c2ebb0 --- /dev/null +++ b/packages/bundler-plugins/test/esbuild/entry-exports.test.ts @@ -0,0 +1,151 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { runInNewContext } from 'node:vm'; +import * as esbuild from 'esbuild'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { sentryEsbuildPlugin } from '../../src/esbuild'; + +describe('entry point exports', () => { + 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 }); + }); + + it.each([ + ['an empty ES module', 'export {};', { default: undefined }], + ['a script without exports', '', { default: {} }], + ['named exports', 'export const answer = 42;', { answer: 42, default: undefined }], + ['a default export', 'export default 42;', { default: 42 }], + ['named and default exports', 'export const answer = 42; export default 7;', { answer: 42, default: 7 }], + ['CommonJS exports', 'module.exports = { answer: 42 };', { default: { answer: 42 } }], + ])('preserves %s without warnings', async (_name, source, expectedExports) => { + fs.writeFileSync(entry, `globalThis.entryRuns = (globalThis.entryRuns || 0) + 1;\n${source}`); + + const result = await esbuild.build({ + entryPoints: [entry], + bundle: true, + format: 'cjs', + write: false, + logLevel: 'silent', + plugins: [sentryEsbuildPlugin({ telemetry: false, silent: true, sourcemaps: { disable: 'disable-upload' } })], + }); + + expect(result.warnings).toEqual([]); + const output = result.outputFiles[0]!.text; + expect(output).toContain('_sentryDebugIds'); + + const module = { exports: {} }; + const context = { module, entryRuns: 0 }; + runInNewContext(output, context); + expect(module.exports).toMatchObject(expectedExports); + expect(context.entryRuns).toBe(1); + }); + + it('removes unused named exports when a downstream bundle imports only the default', async () => { + const marker = 'UNUSED_EXPORT_PAYLOAD_'; + fs.writeFileSync( + entry, + `globalThis.entryRuns = (globalThis.entryRuns || 0) + 1; + export const unused = ${JSON.stringify(marker + 'x'.repeat(10000))}; + export default 42;`, + ); + const library = path.join(tmpDir, 'library.js'); + + const libraryResult = await esbuild.build({ + entryPoints: [entry], + outfile: library, + bundle: true, + minify: true, + format: 'esm', + logLevel: 'silent', + plugins: [ + sentryEsbuildPlugin({ + telemetry: false, + silent: true, + release: { inject: false }, + sourcemaps: { disable: 'disable-upload' }, + }), + ], + }); + expect(libraryResult.warnings).toEqual([]); + expect(fs.readFileSync(library, 'utf8')).toContain(marker); + + const consumerResult = await esbuild.build({ + stdin: { + contents: 'import value from "./library.js"; globalThis.result = value;', + resolveDir: tmpDir, + }, + bundle: true, + minify: true, + format: 'iife', + write: false, + logLevel: 'silent', + }); + expect(consumerResult.warnings).toEqual([]); + const output = consumerResult.outputFiles[0]!.text; + expect(output).not.toContain(marker); + expect(output).toContain('_sentryDebugIds'); + + const context = { result: undefined, entryRuns: 0 }; + runInNewContext(output, context); + expect(context.result).toBe(42); + expect(context.entryRuns).toBe(1); + }); + + it('still reports missing exports accessed by user code', async () => { + fs.writeFileSync(path.join(tmpDir, 'named.js'), 'export const answer = 42;'); + fs.writeFileSync( + entry, + 'import * as named from "./named.js"; globalThis.value = named.missing; export default 42;', + ); + + const result = await esbuild.build({ + entryPoints: [entry], + absWorkingDir: tmpDir, + bundle: true, + write: false, + logLevel: 'silent', + plugins: [sentryEsbuildPlugin({ telemetry: false, silent: true, sourcemaps: { disable: 'disable-upload' } })], + }); + + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0]).toMatchObject({ + id: 'import-is-undefined', + location: { file: 'entry.js' }, + }); + }); + + it('preserves output paths with multiple entry points and an explicit outbase', async () => { + for (const name of ['one', 'two']) { + const dir = path.join(tmpDir, 'src', name); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, 'index.js'), 'export default 42;'); + } + + const result = await esbuild.build({ + absWorkingDir: tmpDir, + entryPoints: ['src/one/index.js', 'src/two/index.js'], + outbase: 'src', + outdir: 'dist', + bundle: true, + format: 'esm', + write: false, + logLevel: 'silent', + plugins: [sentryEsbuildPlugin({ telemetry: false, silent: true, sourcemaps: { disable: 'disable-upload' } })], + }); + + expect(result.warnings).toEqual([]); + expect(result.outputFiles.map(file => path.relative(tmpDir, file.path)).sort()).toEqual([ + path.join('dist', 'one', 'index.js'), + path.join('dist', 'two', 'index.js'), + ]); + }); +});