newSources.js (12908B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 /** 6 * Redux actions for the sources state 7 * 8 * @module actions/sources 9 */ 10 import { insertSourceActors } from "../../actions/source-actors"; 11 import { 12 makeSourceId, 13 createGeneratedSource, 14 createSourceMapOriginalSource, 15 createSourceActor, 16 } from "../../client/firefox/create"; 17 import { toggleBlackBox } from "./blackbox"; 18 import { syncPendingBreakpoint } from "../breakpoints/index"; 19 import { loadSourceText } from "./loadSourceText"; 20 import { prettyPrintAndSelectSource } from "./prettyPrint"; 21 import { toggleSourceMapIgnoreList } from "../ui"; 22 import { selectLocation, setBreakableLines } from "../sources/index"; 23 24 import { getRawSourceURL, isPrettyURL } from "../../utils/source"; 25 import { createLocation } from "../../utils/location"; 26 import { 27 getBlackBoxRanges, 28 getSource, 29 getSourceFromId, 30 hasSourceActor, 31 getSourceByActorId, 32 getPendingSelectedLocation, 33 getPendingBreakpointsForSource, 34 getSelectedLocation, 35 } from "../../selectors/index"; 36 37 import { prefs } from "../../utils/prefs"; 38 import sourceQueue from "../../utils/source-queue"; 39 import { validateSourceActor, ContextError } from "../../utils/context"; 40 41 function loadSourceMapsForSourceActors(sourceActors) { 42 return async function ({ dispatch }) { 43 try { 44 await Promise.all( 45 sourceActors.map(sourceActor => dispatch(loadSourceMap(sourceActor))) 46 ); 47 } catch (error) { 48 // This may throw a context error if we navigated while processing the source maps 49 if (!(error instanceof ContextError)) { 50 throw error; 51 } 52 } 53 54 // Once all the source maps, of all the bulk of new source actors are processed, 55 // flush the SourceQueue. This help aggregate all the original sources in one action. 56 await sourceQueue.flush(); 57 }; 58 } 59 60 /** 61 * @memberof actions/sources 62 * @static 63 */ 64 function loadSourceMap(sourceActor) { 65 return async function ({ dispatch, getState, sourceMapLoader, panel }) { 66 if (!prefs.clientSourceMapsEnabled || !sourceActor.sourceMapURL) { 67 return; 68 } 69 70 let sources, ignoreListUrls, resolvedSourceMapURL, exception; 71 try { 72 // Ignore sourceMapURL on scripts that are part of HTML files, since 73 // we currently treat sourcemaps as Source-wide, not SourceActor-specific. 74 const source = getSourceByActorId(getState(), sourceActor.id); 75 if (source) { 76 ({ sources, ignoreListUrls, resolvedSourceMapURL, exception } = 77 await sourceMapLoader.loadSourceMap({ 78 // Using source ID here is historical and eventually we'll want to 79 // switch to all of this being per-source-actor. 80 id: source.id, 81 url: sourceActor.url || "", 82 sourceMapBaseURL: sourceActor.sourceMapBaseURL || "", 83 sourceMapURL: sourceActor.sourceMapURL || "", 84 isWasm: sourceActor.introductionType === "wasm", 85 })); 86 } 87 } catch (e) { 88 exception = `Internal error: ${e.message}`; 89 } 90 91 if (resolvedSourceMapURL) { 92 dispatch({ 93 type: "RESOLVED_SOURCEMAP_URL", 94 sourceActor, 95 resolvedSourceMapURL, 96 }); 97 } 98 99 if (ignoreListUrls?.length) { 100 dispatch({ 101 type: "ADD_SOURCEMAP_IGNORE_LIST_SOURCES", 102 ignoreListUrls, 103 }); 104 } 105 106 if (exception) { 107 // Catch all errors and log them to the Web Console for users to see. 108 const message = L10N.getFormatStr( 109 "toolbox.sourceMapFailure", 110 exception, 111 sourceActor.url, 112 sourceActor.sourceMapURL 113 ); 114 panel.toolbox.commands.targetCommand.targetFront.logWarningInPage( 115 message, 116 "source map", 117 resolvedSourceMapURL 118 ); 119 120 dispatch({ 121 type: "SOURCE_MAP_ERROR", 122 sourceActor, 123 errorMessage: exception, 124 }); 125 126 // If this source doesn't have a sourcemap or there are no original files 127 // existing, enable it for pretty printing 128 dispatch({ 129 type: "CLEAR_SOURCE_ACTOR_MAP_URL", 130 sourceActor, 131 }); 132 return; 133 } 134 135 // Before dispatching this action, ensure that the related sourceActor is still registered 136 validateSourceActor(getState(), sourceActor); 137 138 for (const originalSource of sources) { 139 // The Source Map worker doesn't set the `sourceActor` attribute, 140 // which is handy to know what is the related bundle. 141 originalSource.sourceActor = sourceActor; 142 } 143 144 // Register all the new reported original sources in the queue to be flushed once all new bundles are processed. 145 sourceQueue.queueOriginalSources(sources); 146 }; 147 } 148 149 // If a request has been made to show this source, go ahead and 150 // select it. 151 function checkSelectedSource(sourceId) { 152 return async ({ dispatch, getState }) => { 153 const state = getState(); 154 const pendingLocation = getPendingSelectedLocation(state); 155 156 if (!pendingLocation || !pendingLocation.url) { 157 return; 158 } 159 160 const source = getSource(state, sourceId); 161 162 if (!source || !source.url) { 163 return; 164 } 165 166 const pendingUrl = pendingLocation.url; 167 const rawPendingUrl = getRawSourceURL(pendingUrl); 168 169 if (rawPendingUrl === source.url) { 170 if (isPrettyURL(pendingUrl)) { 171 const prettySource = await dispatch(prettyPrintAndSelectSource(source)); 172 dispatch(checkPendingBreakpoints(prettySource, null)); 173 return; 174 } 175 176 await dispatch( 177 selectLocation( 178 createLocation({ 179 source, 180 line: 181 typeof pendingLocation.line === "number" 182 ? pendingLocation.line 183 : 0, 184 column: pendingLocation.column, 185 }) 186 ) 187 ); 188 } 189 }; 190 } 191 192 function checkPendingBreakpoints(source, sourceActor) { 193 return async ({ dispatch, getState }) => { 194 const pendingBreakpoints = getPendingBreakpointsForSource( 195 getState(), 196 source 197 ); 198 199 if (pendingBreakpoints.length === 0) { 200 return; 201 } 202 203 // load the source text if there is a pending breakpoint for it 204 await dispatch(loadSourceText(source, sourceActor)); 205 await dispatch(setBreakableLines(createLocation({ source, sourceActor }))); 206 207 await Promise.all( 208 pendingBreakpoints.map(pendingBp => { 209 return dispatch(syncPendingBreakpoint(source, pendingBp)); 210 }) 211 ); 212 }; 213 } 214 215 function restoreBlackBoxedSources(sources) { 216 return async ({ dispatch, getState }) => { 217 const currentRanges = getBlackBoxRanges(getState()); 218 219 if (!Object.keys(currentRanges).length) { 220 return; 221 } 222 223 for (const source of sources) { 224 const ranges = currentRanges[source.url]; 225 if (ranges) { 226 // If the ranges is an empty then the whole source was blackboxed. 227 await dispatch(toggleBlackBox(source, true, ranges)); 228 } 229 } 230 231 if (prefs.sourceMapIgnoreListEnabled) { 232 await dispatch(toggleSourceMapIgnoreList(true)); 233 } 234 }; 235 } 236 237 export function newOriginalSources(originalSourcesInfo) { 238 return async ({ dispatch, getState }) => { 239 const state = getState(); 240 const seen = new Set(); 241 242 const actors = []; 243 const actorsSources = {}; 244 245 for (const { id, url, sourceActor } of originalSourcesInfo) { 246 if (seen.has(id) || getSource(state, id)) { 247 continue; 248 } 249 seen.add(id); 250 251 if (!actorsSources[sourceActor.actor]) { 252 actors.push(sourceActor); 253 actorsSources[sourceActor.actor] = []; 254 } 255 256 actorsSources[sourceActor.actor].push( 257 createSourceMapOriginalSource(id, url, sourceActor.sourceObject) 258 ); 259 } 260 261 // Add the original sources per the generated source actors that 262 // they are primarily from. 263 actors.forEach(sourceActor => { 264 dispatch({ 265 type: "ADD_ORIGINAL_SOURCES", 266 originalSources: actorsSources[sourceActor.actor], 267 generatedSourceActor: sourceActor, 268 }); 269 }); 270 271 // Accumulate the sources back into one list 272 const actorsSourcesValues = Object.values(actorsSources); 273 let sources = []; 274 if (actorsSourcesValues.length) { 275 sources = actorsSourcesValues.reduce((acc, sourceList) => 276 acc.concat(sourceList) 277 ); 278 } 279 280 await dispatch(checkNewSources(sources)); 281 282 for (const source of sources) { 283 dispatch(checkPendingBreakpoints(source, null)); 284 } 285 286 return sources; 287 }; 288 } 289 290 // Wrapper around newGeneratedSources, only used by tests 291 export function newGeneratedSource(sourceInfo) { 292 return async ({ dispatch }) => { 293 const sources = await dispatch(newGeneratedSources([sourceInfo])); 294 return sources[0]; 295 }; 296 } 297 298 export function newGeneratedSources(sourceResources) { 299 return async ({ dispatch, getState }) => { 300 if (!sourceResources.length) { 301 return []; 302 } 303 304 const resultIds = []; 305 const newSourcesObj = {}; 306 const newSourceActors = []; 307 308 for (const sourceResource of sourceResources) { 309 // By the time we process the sources, the related target 310 // might already have been destroyed. It means that the sources 311 // are also about to be destroyed, so ignore them. 312 // (This is covered by browser_toolbox_backward_forward_navigation.js) 313 if (sourceResource.targetFront.isDestroyed()) { 314 continue; 315 } 316 const id = makeSourceId(sourceResource); 317 318 if (!getSource(getState(), id) && !newSourcesObj[id]) { 319 newSourcesObj[id] = createGeneratedSource(sourceResource); 320 } 321 322 const actorId = sourceResource.actor; 323 324 // We are sometimes notified about a new source multiple times if we 325 // request a new source list and also get a source event from the server. 326 if (!hasSourceActor(getState(), actorId)) { 327 newSourceActors.push( 328 createSourceActor( 329 sourceResource, 330 getSource(getState(), id) || newSourcesObj[id] 331 ) 332 ); 333 } 334 335 resultIds.push(id); 336 } 337 338 const newSources = Object.values(newSourcesObj); 339 340 dispatch({ type: "ADD_SOURCES", sources: newSources }); 341 dispatch(insertSourceActors(newSourceActors)); 342 343 await dispatch(checkNewSources(newSources)); 344 345 (async () => { 346 await dispatch(loadSourceMapsForSourceActors(newSourceActors)); 347 348 // We have to force fetching the breakable lines for any incoming source actor 349 // related to HTML page as we may have the HTML page selected, 350 // and already fetched its breakable lines and won't try to update 351 // the breakable lines for any late coming inline <script> tag. 352 const selectedLocation = getSelectedLocation(getState()); 353 for (const sourceActor of newSourceActors) { 354 if ( 355 selectedLocation?.source == sourceActor.sourceObject && 356 sourceActor.sourceObject.isHTML 357 ) { 358 await dispatch( 359 setBreakableLines( 360 createLocation({ source: sourceActor.sourceObject, sourceActor }) 361 ) 362 ); 363 } 364 } 365 366 // We would like to sync breakpoints after we are done 367 // loading source maps as sometimes generated and original 368 // files share the same paths. 369 for (const sourceActor of newSourceActors) { 370 dispatch( 371 checkPendingBreakpoints(sourceActor.sourceObject, sourceActor) 372 ); 373 } 374 })(); 375 376 return resultIds.map(id => getSourceFromId(getState(), id)); 377 }; 378 } 379 380 /** 381 * Common operations done against generated and original sources, 382 * just after having registered them in the reducers: 383 * - automatically selecting the source if it matches the last known selected source 384 * (i.e. the pending selected location). 385 * - automatically notify the server about new sources that used to be blackboxed. 386 * The blackboxing is per Source Actor and so we need to notify them individually 387 * if the source used to be ignored. 388 */ 389 function checkNewSources(sources) { 390 return async ({ dispatch }) => { 391 // Waiting for `checkSelectedSource` completion is important for pretty printed sources. 392 // `checkPendingBreakpoints`, which is called after this method, is expected to be called 393 // only after the source is mapped and breakpoints positions are updated with the mapped locations. 394 // For source mapped sources, `loadSourceMapsForSourceActors` is called before calling 395 // `checkPendingBreakpoints` and will do all that. For pretty printed source, we rely on 396 // `checkSelectedSource` to do that. Selecting a minimized source which used to be pretty printed 397 // will automatically force pretty printing it and computing the mapped breakpoint positions. 398 await Promise.all( 399 sources.map( 400 async source => await dispatch(checkSelectedSource(source.id)) 401 ) 402 ); 403 404 await dispatch(restoreBlackBoxedSources(sources)); 405 406 return sources; 407 }; 408 }