@@ -114,7 +114,10 @@ const sentrySourcemapPlugin: Plugin = {
114114 pluginBuild . onEnd ( async ( buildResult ) => {
115115 const outputs = Object . keys ( buildResult . metafile ?. outputs ?? { } ) ;
116116 const jsFiles = outputs . filter (
117- ( p ) => p . endsWith ( ".cjs" ) || ( p . endsWith ( ".js" ) && ! p . endsWith ( ".map" ) )
117+ ( p ) =>
118+ p . endsWith ( ".cjs" ) ||
119+ p . endsWith ( ".mjs" ) ||
120+ ( p . endsWith ( ".js" ) && ! p . endsWith ( ".map" ) )
118121 ) ;
119122
120123 if ( jsFiles . length === 0 ) {
@@ -164,6 +167,9 @@ const sentrySourcemapPlugin: Plugin = {
164167const REQUIRE_ALIAS_FILTER =
165168 / (?: d b [ \\ / ] (?: i n d e x | s c h e m a | s q l i t e ) | l i s t - c o m m a n d | t e l e m e t r y ) \. t s $ / ;
166169const REQUIRE_ALIAS_RE = / \b _ r e q u i r e \( / g;
170+ const IDENTIFIER_FILTER_RE = / ^ [ A - Z a - z _ $ ] [ \w $ ] * $ / ;
171+ const NODE_ZLIB_RE = / ^ n o d e : z l i b $ / ;
172+ const ANY_RE = / .* / ;
167173
168174/** Transform _require() → require() so esbuild resolves lazy relative requires. */
169175const requireAliasPlugin : Plugin = {
@@ -179,6 +185,66 @@ const requireAliasPlugin: Plugin = {
179185 } ,
180186} ;
181187
188+ /**
189+ * ESM-only: make `node:zlib` imports resolve through a namespace shim so that
190+ * named imports of version-gated exports (the `zstd*` family, added in Node
191+ * 22.15) become dynamic property reads instead of static named imports.
192+ *
193+ * In the CJS build these are `require("node:zlib").zstdCompress` — a lazy
194+ * property that is simply `undefined` on older Node, which the code
195+ * feature-detects and falls back to gzip. In ESM, esbuild hoists them to
196+ * `import { zstdCompress } from "node:zlib"`, which Node validates at link
197+ * time; on Node < 22.15 the missing export makes the WHOLE module fail to
198+ * load, breaking the package's advertised Node 18+ support. Routing through
199+ * `import * as zlib` keeps every access dynamic and link-safe.
200+ */
201+ const zlibNamespaceShimPlugin : Plugin = {
202+ name : "zlib-namespace-shim" ,
203+ setup ( b ) {
204+ const NS = "sentry:node-zlib-shim" ;
205+
206+ // Re-export every name node:zlib exposes on the (newest) build runtime plus
207+ // the zstd family, each as a dynamic property read. The build node is a
208+ // superset of older runtimes, so this covers every name any consumer imports
209+ // (createGzip, brotli*, zstd*, …); names absent at runtime read as undefined,
210+ // which the code feature-detects. `\bimport`/valid-identifier filtering keeps
211+ // the generated module syntactically valid.
212+ const zlibExportNames = Object . keys ( require ( "node:zlib" ) ) ;
213+ const names = new Set (
214+ [
215+ ...zlibExportNames ,
216+ "zstdCompress" ,
217+ "zstdCompressSync" ,
218+ "zstdDecompress" ,
219+ "zstdDecompressSync" ,
220+ "createZstdCompress" ,
221+ "createZstdDecompress" ,
222+ ] . filter ( ( n ) => IDENTIFIER_FILTER_RE . test ( n ) && n !== "default" )
223+ ) ;
224+
225+ // Redirect node:zlib to the shim — EXCEPT the shim's own re-export below,
226+ // which must reach the real builtin (otherwise it resolves to itself and
227+ // every member reads back as undefined). esbuild marks the real builtin
228+ // external so it stays a runtime lookup.
229+ b . onResolve ( { filter : NODE_ZLIB_RE } , ( args ) =>
230+ args . namespace === NS
231+ ? { path : "node:zlib" , external : true }
232+ : { path : NS , namespace : NS }
233+ ) ;
234+
235+ b . onLoad ( { filter : ANY_RE , namespace : NS } , ( ) => ( {
236+ contents : [
237+ `import * as zlib from "node:zlib";` ,
238+ "export default zlib;" ,
239+ ...[ ...names ] . map (
240+ ( n ) => `export const ${ n } = zlib[${ JSON . stringify ( n ) } ];`
241+ ) ,
242+ ] . join ( "\n" ) ,
243+ loader : "js" ,
244+ } ) ) ;
245+ } ,
246+ } ;
247+
182248const plugins : Plugin [ ] = [
183249 sentrySourcemapPlugin ,
184250 textImportPlugin ,
@@ -193,40 +259,29 @@ if (process.env.SENTRY_AUTH_TOKEN) {
193259 ) ;
194260}
195261
196- const result = await build ( {
262+ // Options shared by both the CJS and ESM library builds. Only the module
263+ // format, output path, and format-specific interop shims differ between them.
264+ const commonBuildOptions = {
197265 entryPoints : [ "./src/index.ts" ] ,
198266 bundle : true ,
199267 minify : true ,
200268 treeShaking : true ,
201- // No banner — warning suppression moved to dist/bin.cjs (CLI-only).
202- // The library bundle must not suppress the host application 's warnings.
269+ // No banner (beyond format shims) — warning suppression moved to dist/bin.cjs
270+ // (CLI-only). The library bundle must not suppress the host app 's warnings.
203271 sourcemap : true ,
204272 platform : "node" ,
205273 // Target Node.js 18 — the published package's floor (engines.node).
206274 // Older Node.js uses the bundled WASM SQLite driver (node-sqlite3-wasm);
207275 // 22.15+ uses the native node:sqlite. Downlevels newer syntax accordingly.
208276 target : "node18" ,
209- format : "cjs" ,
210- outfile : "./dist/index.cjs" ,
211- // Inject Bun polyfills and import.meta.url shim for CJS compatibility
212- inject : [ "./script/node-polyfills.ts" , "./script/import-meta-url.js" ] ,
213- define : {
214- SENTRY_CLI_VERSION : JSON . stringify ( VERSION ) ,
215- SENTRY_CLIENT_ID_BUILD : JSON . stringify ( SENTRY_CLIENT_ID ) ,
216- "process.env.NODE_ENV" : JSON . stringify ( "production" ) ,
217- __SENTRY_DEBUG_ID__ : JSON . stringify ( PLACEHOLDER_DEBUG_ID ) ,
218- // Replace import.meta.url with the injected shim variable for CJS
219- "import.meta.url" : "import_meta_url" ,
220- } ,
221277 // Externalize Node.js built-ins, plus Ink + React + companions.
222- // These packages are NOT bundled into the main CJS output because
223- // they use top-level await (esbuild can't emit that in CJS).
224- // Instead, the Ink UI lives in a separate self-contained ESM
225- // sidecar (`dist/ink-app.js`) that the text-import-plugin
226- // pre-bundles with all deps inlined. The main bundle references
227- // the sidecar via a path string and loads it lazily via dynamic
228- // `import()` at runtime. The external list here prevents esbuild
229- // from trying to resolve these packages in the main bundle graph.
278+ // These packages are NOT bundled into the main output because they use
279+ // top-level await (esbuild can't emit that in CJS). Instead, the Ink UI
280+ // lives in a separate self-contained ESM sidecar (`dist/ink-app.js`) that
281+ // the text-import-plugin pre-bundles with all deps inlined. The main bundle
282+ // references the sidecar via a path string and loads it lazily via dynamic
283+ // `import()` at runtime. The external list here prevents esbuild from trying
284+ // to resolve these packages in the main bundle graph.
230285 external : [
231286 "node:*" ,
232287 // The DIF loader resolves this .wasm at runtime (dev only); never bundle it.
@@ -242,6 +297,54 @@ const result = await build({
242297 ] ,
243298 metafile : true ,
244299 plugins,
300+ } satisfies Parameters < typeof build > [ 0 ] ;
301+
302+ // Defines shared by both formats. The CJS build additionally rewrites
303+ // `import.meta.url` to a shim; the ESM build uses it natively.
304+ const commonDefine : Record < string , string > = {
305+ SENTRY_CLI_VERSION : JSON . stringify ( VERSION ) ,
306+ SENTRY_CLIENT_ID_BUILD : JSON . stringify ( SENTRY_CLIENT_ID ) ,
307+ "process.env.NODE_ENV" : JSON . stringify ( "production" ) ,
308+ __SENTRY_DEBUG_ID__ : JSON . stringify ( PLACEHOLDER_DEBUG_ID ) ,
309+ } ;
310+
311+ // In an ESM bundle, `require`, `__dirname`, and `__filename` don't exist, but
312+ // the inlined `node-sqlite3-wasm` Emscripten glue (and other lazily-required
313+ // modules) still reference them. Recreate them from `import.meta.url` so the
314+ // WASM SQLite fallback resolves `dist/node-sqlite3-wasm.wasm` correctly.
315+ const ESM_INTEROP_BANNER = [
316+ `import { createRequire as __sentryCreateRequire } from "node:module";` ,
317+ `import { fileURLToPath as __sentryFileURLToPath } from "node:url";` ,
318+ `import { dirname as __sentryDirname } from "node:path";` ,
319+ "const require = __sentryCreateRequire(import.meta.url);" ,
320+ "const __filename = __sentryFileURLToPath(import.meta.url);" ,
321+ "const __dirname = __sentryDirname(__filename);" ,
322+ ] . join ( "\n" ) ;
323+
324+ const result = await build ( {
325+ ...commonBuildOptions ,
326+ format : "cjs" ,
327+ outfile : "./dist/index.cjs" ,
328+ // Inject Bun polyfills and import.meta.url shim for CJS compatibility
329+ inject : [ "./script/node-polyfills.ts" , "./script/import-meta-url.js" ] ,
330+ define : {
331+ ...commonDefine ,
332+ // Replace import.meta.url with the injected shim variable for CJS
333+ "import.meta.url" : "import_meta_url" ,
334+ } ,
335+ } ) ;
336+
337+ const esmResult = await build ( {
338+ ...commonBuildOptions ,
339+ format : "esm" ,
340+ outfile : "./dist/index.mjs" ,
341+ // ESM has native import.meta.url — only the Bun polyfills need injecting.
342+ inject : [ "./script/node-polyfills.ts" ] ,
343+ define : { ...commonDefine } ,
344+ banner : { js : ESM_INTEROP_BANNER } ,
345+ // ESM-only: keep node:zlib access dynamic so version-gated zstd exports don't
346+ // become link-time-fatal named imports on Node < 22.15 (see plugin comment).
347+ plugins : [ ...commonBuildOptions . plugins , zlibNamespaceShimPlugin ] ,
245348} ) ;
246349
247350// Write the CLI bin wrapper (tiny — shebang + version check + dispatch).
@@ -296,10 +399,14 @@ export default createSentrySDK;
296399const sdkTypes = await readFile ( "./src/sdk.generated.d.cts" , "utf-8" ) ;
297400
298401const TYPE_DECLARATIONS = `${ CORE_DECLARATIONS } \n${ sdkTypes } \n` ;
402+ // The declarations are self-contained (no relative imports), so the same content
403+ // serves both module systems: `.d.cts` for the `require` condition and `.d.mts`
404+ // for the `import` condition.
299405await writeFile ( "./dist/index.d.cts" , TYPE_DECLARATIONS ) ;
406+ await writeFile ( "./dist/index.d.mts" , TYPE_DECLARATIONS ) ;
300407
301408console . log ( " -> dist/bin.cjs (CLI wrapper)" ) ;
302- console . log ( " -> dist/index.d.cts (type declarations)" ) ;
409+ console . log ( " -> dist/index.d.cts + dist/index.d.mts (type declarations)" ) ;
303410
304411// The `ink-app.js` sidecar (pre-bundled by text-import-plugin) ships
305412// with the npm package so `npx sentry@latest init` can load the
@@ -330,10 +437,14 @@ await copyFile(
330437console . log ( " -> dist/node-sqlite3-wasm.wasm (WASM SQLite fallback)" ) ;
331438
332439// Calculate bundle size (only the main bundle, not source maps)
333- const bundleOutput = result . metafile ?. outputs [ "dist/index.cjs" ] ;
334- const bundleSize = bundleOutput ?. bytes ?? 0 ;
335- const bundleSizeKB = ( bundleSize / 1024 ) . toFixed ( 1 ) ;
440+ const bundleSizeKB = (
441+ ( result . metafile ?. outputs [ "dist/index.cjs" ] ?. bytes ?? 0 ) / 1024
442+ ) . toFixed ( 1 ) ;
443+ const esmBundleSizeKB = (
444+ ( esmResult . metafile ?. outputs [ "dist/index.mjs" ] ?. bytes ?? 0 ) / 1024
445+ ) . toFixed ( 1 ) ;
336446
337447console . log ( `\n -> dist/index.cjs (${ bundleSizeKB } KB)` ) ;
448+ console . log ( ` -> dist/index.mjs (${ esmBundleSizeKB } KB)` ) ;
338449console . log ( `\n${ "=" . repeat ( 40 ) } ` ) ;
339450console . log ( "Bundle complete!" ) ;
0 commit comments