@@ -107,6 +107,131 @@ function addDebugIdToBundleSource(bundleSource: string, debugId: string): string
107107 }
108108}
109109
110+ function bundleHasInlineSourceMap ( bundleSource : string ) : boolean {
111+ return / ^ \s * \/ \/ # s o u r c e M a p p i n g U R L = d a t a : / m. test ( bundleSource ) ;
112+ }
113+
114+ function setDebugIdOnSourceMap ( map : Record < string , unknown > , debugId : string ) : void {
115+ // For now we write both fields until we know what will become the standard - if ever.
116+ map [ 'debug_id' ] = debugId ;
117+ map [ 'debugId' ] = debugId ;
118+ }
119+
120+ export type StampedArtifacts = {
121+ bundleSource : string ;
122+ /** `undefined` when the bundle has no separate source map (i.e. the map is inlined). */
123+ sourceMapSource : string | undefined ;
124+ } ;
125+
126+ function parseSourceMap ( sourceMapSource : string ) : Record < string , unknown > | undefined {
127+ let map : unknown ;
128+ try {
129+ map = JSON . parse ( sourceMapSource ) ;
130+ } catch {
131+ return undefined ;
132+ }
133+
134+ return map && typeof map === 'object' ? ( map as Record < string , unknown > ) : undefined ;
135+ }
136+
137+ /**
138+ * Stamps the debug ID injected into `bundleSource` into the bundle (as `//# debugId=` comment) and its
139+ * source map (as `debug_id`/`debugId` fields).
140+ *
141+ * This exists for `sourcemaps.disable: "disable-upload"`: the regular upload path only stamps
142+ * temporary copies of the artifacts (see `prepareBundleForDebugIdUpload`), so without this the
143+ * emitted artifacts would carry no debug ID and a later manual upload could not match them.
144+ * Callers must apply the result inside the bundler's asset pipeline (or, for bundlers without one,
145+ * before the build resolves) so that integrity hashes computed by later build steps include it.
146+ *
147+ * Pass `sourceMapSource: undefined` when the bundle has no separate source map. A bundle with an
148+ * inlined map still gets the comment, which is all the CLI and Symbolicator read the debug ID from.
149+ *
150+ * @returns the stamped artifacts, or `undefined` for bundles without a debug ID, without any source
151+ * map, or with an unparseable map.
152+ */
153+ export function stampDebugId ( bundleSource : string , sourceMapSource : string | undefined ) : StampedArtifacts | undefined {
154+ const debugId = determineDebugIdFromBundleSource ( bundleSource ) ;
155+ if ( debugId === undefined ) {
156+ return undefined ;
157+ }
158+
159+ if ( sourceMapSource === undefined ) {
160+ if ( ! bundleHasInlineSourceMap ( bundleSource ) ) {
161+ return undefined ;
162+ }
163+
164+ return { bundleSource : addDebugIdToBundleSource ( bundleSource , debugId ) , sourceMapSource : undefined } ;
165+ }
166+
167+ const map = parseSourceMap ( sourceMapSource ) ;
168+ if ( ! map ) {
169+ return undefined ;
170+ }
171+
172+ setDebugIdOnSourceMap ( map , debugId ) ;
173+
174+ return {
175+ bundleSource : addDebugIdToBundleSource ( bundleSource , debugId ) ,
176+ sourceMapSource : JSON . stringify ( map ) ,
177+ } ;
178+ }
179+
180+ /**
181+ * Stamps the debug ID of an emitted bundle into the bundle and its source map on disk.
182+ *
183+ * Used by bundlers that offer no hook to modify assets before they are written (esbuild).
184+ */
185+ export async function addDebugIdToEmittedArtifacts (
186+ bundleFilePath : string ,
187+ logger : Logger ,
188+ resolveSourceMapHook : ResolveSourceMapHook | undefined ,
189+ ) : Promise < void > {
190+ let bundleSource : string ;
191+ try {
192+ bundleSource = await fs . promises . readFile ( bundleFilePath , 'utf8' ) ;
193+ } catch ( e ) {
194+ logger . error ( `Could not read bundle to stamp debug ID: ${ bundleFilePath } ` , e ) ;
195+ return ;
196+ }
197+
198+ const sourceMapPath = await determineSourceMapPathFromBundle (
199+ bundleFilePath ,
200+ bundleSource ,
201+ logger ,
202+ resolveSourceMapHook ,
203+ ) ;
204+
205+ let sourceMapSource : string | undefined ;
206+ if ( sourceMapPath ) {
207+ try {
208+ sourceMapSource = await fs . promises . readFile ( sourceMapPath , 'utf8' ) ;
209+ } catch ( e ) {
210+ logger . error ( `Could not read source map to stamp debug ID: ${ sourceMapPath } ` , e ) ;
211+ return ;
212+ }
213+ }
214+
215+ const stamped = stampDebugId ( bundleSource , sourceMapSource ) ;
216+ if ( ! stamped ) {
217+ logger . debug (
218+ `Could not stamp debug ID (no debug ID in bundle, no source map, or invalid source map): ${ bundleFilePath } ` ,
219+ ) ;
220+ return ;
221+ }
222+
223+ const writes = [ fs . promises . writeFile ( bundleFilePath , stamped . bundleSource , 'utf8' ) ] ;
224+ if ( sourceMapPath && stamped . sourceMapSource !== undefined ) {
225+ writes . push ( fs . promises . writeFile ( sourceMapPath , stamped . sourceMapSource , 'utf8' ) ) ;
226+ }
227+
228+ try {
229+ await Promise . all ( writes ) ;
230+ } catch ( e ) {
231+ logger . error ( `Could not write debug ID into build artifacts: ${ bundleFilePath } ` , e ) ;
232+ }
233+ }
234+
110235/**
111236 * Applies a set of heuristics to find the source map for a particular bundle.
112237 *
@@ -201,9 +326,7 @@ async function prepareSourceMapForDebugIdUpload(
201326 let map : Record < string , unknown > ;
202327 try {
203328 map = JSON . parse ( sourceMapFileContent ) as { sources : unknown ; [ key : string ] : unknown } ;
204- // For now we write both fields until we know what will become the standard - if ever.
205- map [ 'debug_id' ] = debugId ;
206- map [ 'debugId' ] = debugId ;
329+ setDebugIdOnSourceMap ( map , debugId ) ;
207330 } catch {
208331 logger . error ( `Failed to parse source map for debug ID upload: ${ sourceMapPath } ` ) ;
209332 return ;
0 commit comments