1- import type { Options } from '../core' ;
1+ import type { Options , SourceMapKind , UploadFilter } from '../core' ;
22import {
33 createSentryBuildPluginManager ,
44 generateReleaseInjectorCode ,
@@ -7,8 +7,10 @@ import {
77 createDebugIdUploadFunction ,
88 CodeInjection ,
99 addDebugIdToEmittedArtifacts ,
10+ hasDebugId ,
1011 isJsFile ,
1112} from '../core' ;
13+ import * as fs from 'node:fs' ;
1214import * as path from 'node:path' ;
1315import { createRequire } from 'node:module' ;
1416import { randomUUID } from 'node:crypto' ;
@@ -48,13 +50,33 @@ interface EsbuildOnEndArgs {
4850 } ;
4951}
5052
53+ type EsbuildSourceMapSetting = boolean | 'inline' | 'external' | 'linked' | 'both' | undefined ;
54+
5155interface EsbuildInitialOptions {
5256 bundle ?: boolean ;
5357 inject ?: string [ ] ;
5458 metafile ?: boolean ;
5559 define ?: Record < string , string > ;
5660 write ?: boolean ;
5761 absWorkingDir ?: string ;
62+ sourcemap ?: EsbuildSourceMapSetting ;
63+ }
64+
65+ /** Maps esbuild's `sourcemap` setting onto the plugin's source map kinds. */
66+ function sourceMapKindFromEsbuildSetting ( setting : EsbuildSourceMapSetting ) : SourceMapKind {
67+ switch ( setting ) {
68+ case undefined :
69+ return 'unset' ;
70+ case false :
71+ return 'off' ;
72+ case 'external' :
73+ return 'hidden' ;
74+ case 'inline' :
75+ case 'both' :
76+ return 'inline' ;
77+ default :
78+ return 'visible' ;
79+ }
5880}
5981
6082interface EsbuildPluginBuild {
@@ -280,6 +302,13 @@ export function sentryEsbuildPlugin(userOptions: Options = {}): any {
280302 } ) ;
281303 }
282304
305+ const sourceMapDecision = sentryBuildPluginManager . sourceMaps . decide (
306+ sourceMapKindFromEsbuildSetting ( initialOptions . sourcemap ) ,
307+ ) ;
308+ if ( sourceMapDecision . enableHidden ) {
309+ initialOptions . sourcemap = 'external' ;
310+ }
311+
283312 // Create release and optionally upload
284313 const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts ( ) ;
285314 const upload = createDebugIdUploadFunction ( { sentryBuildPluginManager } ) ;
@@ -290,27 +319,38 @@ export function sentryEsbuildPlugin(userOptions: Options = {}): any {
290319 await sentryBuildPluginManager . createRelease ( ) ;
291320
292321 if ( sourcemapsEnabled ) {
293- const buildArtifacts = result . metafile ? Object . keys ( result . metafile . outputs ) : [ ] ;
322+ // The metafile lists outputs relative to `absWorkingDir`, not to the process.
323+ const outputDir = initialOptions . absWorkingDir ?? process . cwd ( ) ;
324+ const buildArtifacts = result . metafile
325+ ? Object . keys ( result . metafile . outputs ) . map ( output => path . resolve ( outputDir , output ) )
326+ : [ ] ;
294327
295328 if ( options . sourcemaps ?. disable !== 'disable-upload' ) {
296- await upload ( buildArtifacts ) ;
329+ // The upload globs the paths, and glob reads backslashes as escapes.
330+ const { uploaded } = await upload ( buildArtifacts . map ( artifact => artifact . split ( path . sep ) . join ( '/' ) ) ) ;
331+
332+ // esbuild writes before `onEnd` runs, so the maps are deleted from disk
333+ // afterwards instead of being dropped from the output like in other bundlers.
334+ if ( sourceMapDecision . deleteAfterUpload && initialOptions . write !== false ) {
335+ const { shouldUpload } = sentryBuildPluginManager . sourceMaps ;
336+ const sourceMaps = await getUploadedSourceMaps ( buildArtifacts , shouldUpload ) ;
337+
338+ if ( uploaded ) {
339+ await Promise . all ( sourceMaps . map ( sourceMap => fs . promises . rm ( sourceMap , { force : true } ) ) ) ;
340+ } else if ( sourceMaps . length > 0 ) {
341+ logger . error ( 'The source maps were not uploaded, so they stay in the build output.' ) ;
342+ }
343+ }
297344 } else if ( initialOptions . write === false ) {
298345 logger . debug ( 'Build output is not written to disk. Skipping debug ID injection into build artifacts.' ) ;
299346 } else {
300347 // The upload routine (which stamps debug IDs into temp copies of the artifacts) is
301348 // skipped with `disable-upload`. esbuild has no hook to modify outputs before they are
302349 // written, so the emitted artifacts get stamped on disk instead.
303- const outputDir = initialOptions . absWorkingDir ?? process . cwd ( ) ;
304350 await Promise . all (
305351 buildArtifacts
306352 . filter ( isJsFile )
307- . map ( bundle =>
308- addDebugIdToEmittedArtifacts (
309- path . resolve ( outputDir , bundle ) ,
310- logger ,
311- options . sourcemaps ?. resolveSourceMap ,
312- ) ,
313- ) ,
353+ . map ( bundle => addDebugIdToEmittedArtifacts ( bundle , logger , options . sourcemaps ?. resolveSourceMap ) ) ,
314354 ) ;
315355 }
316356 }
@@ -326,3 +366,24 @@ export function sentryEsbuildPlugin(userOptions: Options = {}): any {
326366// eslint-disable-next-line @typescript-eslint/no-explicit-any
327367export default sentryEsbuildPlugin ;
328368export type { Options as SentryEsbuildPluginOptions } from '../core' ;
369+
370+ /**
371+ * The maps of the chunks the upload took: those that pass `sourcemaps.assets` and `ignore` and
372+ * carry a debug ID. Shared chunks and CSS get none, so their maps stay in the build output.
373+ */
374+ async function getUploadedSourceMaps ( buildArtifacts : string [ ] , shouldUpload : UploadFilter ) : Promise < string [ ] > {
375+ const sourceMaps = new Set ( buildArtifacts . filter ( artifact => artifact . endsWith ( '.map' ) ) ) ;
376+ const uploaded : string [ ] = [ ] ;
377+
378+ for ( const artifact of buildArtifacts ) {
379+ if ( ! isJsFile ( artifact ) || ! sourceMaps . has ( `${ artifact } .map` ) || ! shouldUpload ( artifact ) ) {
380+ continue ;
381+ }
382+ const code = await fs . promises . readFile ( artifact , 'utf8' ) . catch ( ( ) => '' ) ;
383+ if ( hasDebugId ( code ) ) {
384+ uploaded . push ( `${ artifact } .map` ) ;
385+ }
386+ }
387+
388+ return uploaded ;
389+ }
0 commit comments