@@ -45,6 +45,28 @@ describeWithMockConnection('CompilerScriptMapping', () => {
4545 } ) ;
4646 } ) ;
4747
48+ /**
49+ * Holds back loading the source map at `url` until the returned function is called,
50+ * so that source maps can finish loading out of parse order. Calling the returned
51+ * function also restores `loadResource`.
52+ */
53+ const delaySourceMapLoad = ( url : string ) : ( ) => void => {
54+ const { promise : sourceMapLoadReleased , resolve} = Promise . withResolvers < void > ( ) ;
55+ const pageResourceLoader = SDK . PageResourceLoader . PageResourceLoader . instance ( ) ;
56+ const loadResource = pageResourceLoader . loadResource . bind ( pageResourceLoader ) ;
57+ const loadResourceStub =
58+ sinon . stub ( pageResourceLoader , 'loadResource' ) . callsFake ( async ( resourceUrl , initiator ) => {
59+ if ( resourceUrl === url ) {
60+ await sourceMapLoadReleased ;
61+ }
62+ return await loadResource ( resourceUrl , initiator ) ;
63+ } ) ;
64+ return ( ) => {
65+ loadResourceStub . restore ( ) ;
66+ resolve ( ) ;
67+ } ;
68+ } ;
69+
4870 it ( 'creates UISourceCodes with the correct content type' , async ( ) => {
4971 const target = createTarget ( ) ;
5072
@@ -518,6 +540,111 @@ describeWithMockConnection('CompilerScriptMapping', () => {
518540 originalScript . debuggerModel . createRawLocation ( originalScript , 0 , 0 ) ) ,
519541 originalLibUISourceCode . uiLocation ( 0 , 0 ) ) ;
520542 } ) ;
543+
544+ it ( 'supports script replacement when source maps finish loading out of order' , async ( ) => {
545+ // When DevTools attaches to a page where a script replaced code of an earlier script
546+ // (e.g. on hot reload), the scripts are reported in parse order, but the small source
547+ // map of the replacement script sometimes finishes loading before the large source map
548+ // of the original bundle. The replacement script must keep mapping to `app.js` regardless.
549+ const target = createTarget ( ) ;
550+ const sourceRoot = 'webpack:///src' ;
551+
552+ const bundleScriptInfo = {
553+ url : 'http://example.com/bundle.js' ,
554+ content : 'const f = console.log;\nf("Hello from the original bundle");' ,
555+ } ;
556+ const bundleSourceMapInfo = {
557+ url : `${ bundleScriptInfo . url } .map` ,
558+ content : encodeSourceMap (
559+ [
560+ '0:0 => lib.js:0:0' ,
561+ 'lib.js: const f = console.log;' ,
562+ '1:0 => app.js:0:0' ,
563+ 'app.js: f("Hello from the original bundle")' ,
564+ ] ,
565+ sourceRoot ) ,
566+ } ;
567+ const replacementScriptInfo = {
568+ url : 'http://example.com/replacement.js' ,
569+ content : 'f("Hello from the replacement");' ,
570+ } ;
571+ const replacementSourceMapInfo = {
572+ url : `${ replacementScriptInfo . url } .map` ,
573+ content : encodeSourceMap ( [ '0:0 => app.js:0:0' , 'app.js: f("Hello from the replacement")' ] , sourceRoot ) ,
574+ } ;
575+
576+ const loadBundleSourceMap = delaySourceMapLoad ( bundleSourceMapInfo . url ) ;
577+
578+ const bundleScriptPromise = backend . addScript ( target , bundleScriptInfo , bundleSourceMapInfo ) ;
579+ const [ replacementAppUISourceCode , replacementScript ] = await Promise . all ( [
580+ waitForUISourceCodeAdded ( `${ sourceRoot } /app.js` , target ) ,
581+ backend . addScript ( target , replacementScriptInfo , replacementSourceMapInfo ) ,
582+ ] ) ;
583+
584+ const [ libUISourceCode , bundleScript ] = await Promise . all ( [
585+ waitForUISourceCodeAdded ( `${ sourceRoot } /lib.js` , target ) ,
586+ bundleScriptPromise ,
587+ loadBundleSourceMap ( ) ,
588+ ] ) ;
589+
590+ // The replacement script still maps to `app.js`...
591+ assert . strictEqual ( workspace . uiSourceCodeForURL ( urlString `${ sourceRoot } /app.js` ) , replacementAppUISourceCode ) ;
592+ assert . deepEqual ( await debuggerWorkspaceBinding . rawLocationToUILocation (
593+ replacementScript . debuggerModel . createRawLocation ( replacementScript , 0 , 0 ) ) ,
594+ replacementAppUISourceCode . uiLocation ( 0 , 0 ) ) ;
595+ assert . deepEqual ( await debuggerWorkspaceBinding . uiLocationToRawLocations ( replacementAppUISourceCode , 0 , 0 ) , [
596+ replacementScript . debuggerModel . createRawLocation ( replacementScript , 0 , 0 ) ,
597+ ] ) ;
598+
599+ // ...the stale `app.js` mapping of `bundle.js` is not used...
600+ const { uiSourceCode} = ( await debuggerWorkspaceBinding . rawLocationToUILocation (
601+ bundleScript . debuggerModel . createRawLocation ( bundleScript , 1 , 0 ) ) ) ! ;
602+ assert . notStrictEqual ( uiSourceCode , replacementAppUISourceCode ) ;
603+
604+ // ...while `bundle.js` still maps to `lib.js`.
605+ assert . deepEqual ( await debuggerWorkspaceBinding . rawLocationToUILocation (
606+ bundleScript . debuggerModel . createRawLocation ( bundleScript , 0 , 0 ) ) ,
607+ libUISourceCode . uiLocation ( 0 , 0 ) ) ;
608+ } ) ;
609+
610+ it ( 'shares UISourceCodes with compatible source maps that finish loading out of order' , async ( ) => {
611+ const target = createTarget ( ) ;
612+ const sourceRoot = 'webpack:///src' ;
613+
614+ const route1ScriptInfo = {
615+ url : 'http://example.com/route1.js' ,
616+ content : 'function f(x){}\nf(1)' ,
617+ } ;
618+ const route1SourceMapInfo = {
619+ url : `${ route1ScriptInfo . url } .map` ,
620+ content : encodeSourceMap ( [ '0:0 => shared.js:0:0' , '1:0 => route1.js:0:0' ] , sourceRoot ) ,
621+ } ;
622+ const route2ScriptInfo = {
623+ url : 'http://example.com/route2.js' ,
624+ content : 'function f(x){}\nf(2)' ,
625+ } ;
626+ const route2SourceMapInfo = {
627+ url : `${ route2ScriptInfo . url } .map` ,
628+ content : encodeSourceMap ( [ '0:0 => shared.js:0:0' , '1:0 => route2.js:0:0' ] , sourceRoot ) ,
629+ } ;
630+
631+ const loadRoute1SourceMap = delaySourceMapLoad ( route1SourceMapInfo . url ) ;
632+
633+ const route1ScriptPromise = backend . addScript ( target , route1ScriptInfo , route1SourceMapInfo ) ;
634+ const [ , route2Script ] = await Promise . all ( [
635+ waitForUISourceCodeAdded ( `${ sourceRoot } /shared.js` , target ) ,
636+ backend . addScript ( target , route2ScriptInfo , route2SourceMapInfo ) ,
637+ ] ) ;
638+
639+ const [ sharedUISourceCode , route1Script ] = await Promise . all ( [
640+ waitForUISourceCodeAdded ( `${ sourceRoot } /shared.js` , target ) ,
641+ route1ScriptPromise ,
642+ loadRoute1SourceMap ( ) ,
643+ ] ) ;
644+
645+ const sharedLocations = await debuggerWorkspaceBinding . uiLocationToRawLocations ( sharedUISourceCode , 0 , 0 ) ;
646+ assert . sameMembers ( sharedLocations . map ( location => location . script ( ) ) , [ route1Script , route2Script ] ) ;
647+ } ) ;
521648 } ) ;
522649
523650 it ( 'assumes UTF-8 encoding for source files embedded in source maps' , async ( ) => {
0 commit comments