@@ -93,7 +93,7 @@ export async function prepareBundleForDebugIdUpload(
9393 *
9494 * The string pattern is injected via the debug ID injection snipped.
9595 */
96- function determineDebugIdFromBundleSource ( code : string ) : string | undefined {
96+ export function determineDebugIdFromBundleSource ( code : string ) : string | undefined {
9797 const match = code . match (
9898 / s e n t r y - d b i d - ( [ 0 - 9 a - f A - F ] { 8 } \b - [ 0 - 9 a - f A - F ] { 4 } \b - [ 0 - 9 a - f A - F ] { 4 } \b - [ 0 - 9 a - f A - F ] { 4 } \b - [ 0 - 9 a - f A - F ] { 12 } ) / ,
9999 ) ;
@@ -119,13 +119,135 @@ function addDebugIdToBundleSource(bundleSource: string, debugId: string): string
119119 }
120120}
121121
122+ function setDebugIdOnSourceMap ( map : Record < string , unknown > , debugId : string ) : void {
123+ // For now we write both fields until we know what will become the standard - if ever.
124+ map [ 'debug_id' ] = debugId ;
125+ map [ 'debugId' ] = debugId ;
126+ }
127+
128+ export type StampedArtifacts = {
129+ bundleSource : string ;
130+ sourceMapSource : string ;
131+ } ;
132+
133+ /**
134+ * Stamps the debug ID injected into `bundleSource` into both the bundle (as `//# debugId=` comment)
135+ * and its source map (as `debug_id`/`debugId` fields).
136+ *
137+ * This exists for `sourcemaps.disable: "disable-upload"`: the regular upload path only stamps
138+ * temporary copies of the artifacts (see `prepareBundleForDebugIdUpload`), so without this the
139+ * emitted artifacts would carry no debug ID and a later manual upload could not match them.
140+ * Callers must apply the result inside the bundler's asset pipeline (or, for bundlers without one,
141+ * before the build resolves) so that integrity hashes computed by later build steps include it.
142+ *
143+ * @returns the stamped artifacts, or `undefined` if the bundle carries no debug ID or the map
144+ * isn't valid JSON.
145+ */
146+ export function addDebugIdToBundleAndSourceMap (
147+ bundleSource : string ,
148+ sourceMapSource : string ,
149+ ) : StampedArtifacts | undefined {
150+ const debugId = determineDebugIdFromBundleSource ( bundleSource ) ;
151+ if ( debugId === undefined ) {
152+ return undefined ;
153+ }
154+
155+ let map : unknown ;
156+ try {
157+ map = JSON . parse ( sourceMapSource ) ;
158+ } catch {
159+ return undefined ;
160+ }
161+
162+ if ( ! map || typeof map !== 'object' ) {
163+ return undefined ;
164+ }
165+
166+ setDebugIdOnSourceMap ( map as Record < string , unknown > , debugId ) ;
167+
168+ return {
169+ bundleSource : addDebugIdToBundleSource ( bundleSource , debugId ) ,
170+ sourceMapSource : JSON . stringify ( map ) ,
171+ } ;
172+ }
173+
174+ /**
175+ * Warns about bundles whose source map is inlined into the bundle. Those have no separate map
176+ * to stamp, so they can't be symbolicated after a manual upload.
177+ */
178+ export function warnAboutInlineSourceMaps ( bundleFileNames : string [ ] , logger : Logger ) : void {
179+ if ( bundleFileNames . length === 0 ) {
180+ return ;
181+ }
182+
183+ logger . warn (
184+ `${ bundleFileNames . length } bundle(s) inline their source map, which can't be stamped with a debug ID. Emit source maps as separate files so they can be symbolicated after a manual upload: ${ bundleFileNames . join ( ', ' ) } ` ,
185+ ) ;
186+ }
187+
188+ export type EmittedArtifactsStampResult = 'stamped' | 'inline-source-map' | 'skipped' ;
189+
190+ /**
191+ * Stamps the debug ID of an emitted bundle into the bundle and its source map on disk.
192+ *
193+ * Used by bundlers that offer no hook to modify assets before they are written (esbuild).
194+ */
195+ export async function addDebugIdToEmittedArtifacts (
196+ bundleFilePath : string ,
197+ logger : Logger ,
198+ resolveSourceMapHook : ResolveSourceMapHook | undefined ,
199+ ) : Promise < EmittedArtifactsStampResult > {
200+ let bundleSource : string ;
201+ try {
202+ bundleSource = await fs . promises . readFile ( bundleFilePath , 'utf8' ) ;
203+ } catch ( e ) {
204+ logger . error ( `Could not read bundle to stamp debug ID: ${ bundleFilePath } ` , e ) ;
205+ return 'skipped' ;
206+ }
207+
208+ const sourceMapPath = await determineSourceMapPathFromBundle (
209+ bundleFilePath ,
210+ bundleSource ,
211+ logger ,
212+ resolveSourceMapHook ,
213+ ) ;
214+ if ( ! sourceMapPath ) {
215+ return bundleHasInlineSourceMap ( bundleSource ) ? 'inline-source-map' : 'skipped' ;
216+ }
217+
218+ let sourceMapSource : string ;
219+ try {
220+ sourceMapSource = await fs . promises . readFile ( sourceMapPath , 'utf8' ) ;
221+ } catch ( e ) {
222+ logger . error ( `Could not read source map to stamp debug ID: ${ sourceMapPath } ` , e ) ;
223+ return 'skipped' ;
224+ }
225+
226+ const stamped = addDebugIdToBundleAndSourceMap ( bundleSource , sourceMapSource ) ;
227+ if ( stamped === undefined ) {
228+ logger . debug ( `Could not stamp debug ID (no debug ID in bundle or invalid source map): ${ bundleFilePath } ` ) ;
229+ return 'skipped' ;
230+ }
231+
232+ try {
233+ await Promise . all ( [
234+ fs . promises . writeFile ( sourceMapPath , stamped . sourceMapSource , 'utf8' ) ,
235+ fs . promises . writeFile ( bundleFilePath , stamped . bundleSource , 'utf8' ) ,
236+ ] ) ;
237+ return 'stamped' ;
238+ } catch ( e ) {
239+ logger . error ( `Could not write debug ID into build artifacts: ${ bundleFilePath } ` , e ) ;
240+ return 'skipped' ;
241+ }
242+ }
243+
122244/**
123245 * Whether the bundle carries its source map inlined as `sourceMappingURL=data:`
124246 * URI, rather than referencing a separate `.map` file. Such bundles must still
125247 * be uploaded even when no `.map` file is found, because the source file itself
126248 * contains the map.
127249 */
128- function bundleHasInlineSourceMap ( bundleSource : string ) : boolean {
250+ export function bundleHasInlineSourceMap ( bundleSource : string ) : boolean {
129251 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 ) ;
130252}
131253
@@ -223,9 +345,7 @@ async function prepareSourceMapForDebugIdUpload(
223345 let map : Record < string , unknown > ;
224346 try {
225347 map = JSON . parse ( sourceMapFileContent ) as { sources : unknown ; [ key : string ] : unknown } ;
226- // For now we write both fields until we know what will become the standard - if ever.
227- map [ 'debug_id' ] = debugId ;
228- map [ 'debugId' ] = debugId ;
348+ setDebugIdOnSourceMap ( map , debugId ) ;
229349 } catch {
230350 logger . error ( `Failed to parse source map for debug ID upload: ${ sourceMapPath } ` ) ;
231351 return ;
0 commit comments